diff --git a/README.md b/README.md index 05e0dbc0..7a5011bb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The app is quite stable now. Basic stuff, as well as more advanced operations, s # Self-hosting -Everything you need to host this app on your server is any static file server. The app is a single HTML file + cache manifest (optionally; for offline access). +Everything you need to host this app on your server is any static file server. The app is a single HTML file + a service worker (optionally; for offline access). You can download the latest distribution files from [gh-pages](https://github.com/keeweb/keeweb/archive/gh-pages.zip) branch. If you are using Docker: diff --git a/app/scripts/auto-type/helper/auto-type-helper-linux.js b/app/scripts/auto-type/helper/auto-type-helper-linux.js index 4534dd52..16514deb 100644 --- a/app/scripts/auto-type/helper/auto-type-helper-linux.js +++ b/app/scripts/auto-type/helper/auto-type-helper-linux.js @@ -9,7 +9,7 @@ AutoTypeHelper.prototype.getActiveWindowInfo = function(callback) { complete(err, out) { let windowInfo; if (out) { - const [id, title] = out.trim().split('\n'); + const [title, id] = out.trim().split('\n'); windowInfo = { id, title diff --git a/app/scripts/auto-type/index.js b/app/scripts/auto-type/index.js index 5b30e5f9..c5e9fafd 100644 --- a/app/scripts/auto-type/index.js +++ b/app/scripts/auto-type/index.js @@ -28,6 +28,7 @@ const AutoType = { Events.on('auto-type', e => this.handleEvent(e)); Events.on('main-window-blur', e => this.resetPendingEvent(e)); Events.on('main-window-will-close', e => this.resetPendingEvent(e)); + appModel.files.on('change', () => this.processPendingEvent()); }, handleEvent(e) { @@ -209,7 +210,6 @@ const AutoType = { const evt = { filter, windowInfo }; if (!this.appModel.files.hasOpenFiles()) { this.pendingEvent = evt; - this.appModel.files.once('change', this.processPendingEvent, this); logger.debug('auto-type event delayed'); this.focusMainWindow(); } else { @@ -262,7 +262,6 @@ const AutoType = { resetPendingEvent() { if (this.pendingEvent) { this.pendingEvent = null; - this.appModel.files.off('change', this.processPendingEvent, this); logger.debug('auto-type event cancelled'); } }, @@ -273,7 +272,6 @@ const AutoType = { } logger.debug('processing pending auto-type event'); const evt = this.pendingEvent; - this.appModel.files.off('change', this.processPendingEvent, this); this.pendingEvent = null; this.processEventWithFilter(evt); } diff --git a/app/scripts/comp/browser/auth-receiver.js b/app/scripts/comp/browser/auth-receiver.js index 8563d558..6c4cf79c 100644 --- a/app/scripts/comp/browser/auth-receiver.js +++ b/app/scripts/comp/browser/auth-receiver.js @@ -1,9 +1,8 @@ -import { Storage } from 'storage'; import { Features } from 'util/features'; const AuthReceiver = { receive() { - if (!Features.isPopup && !Features.isStandalone) { + if (!Features.isPopup) { return false; } const opener = window.opener || window.parent; @@ -12,16 +11,9 @@ const AuthReceiver = { if (!hasKeys) { return false; } - if (Features.isStandalone) { - if (sessionStorage.authStorage) { - Storage[sessionStorage.authStorage].handleOAuthReturnMessage(message); - } - return false; - } else { - opener.postMessage(message, window.location.origin); - window.close(); - return true; - } + opener.postMessage(message, window.location.origin); + window.close(); + return true; }, urlArgsToMessage(url) { diff --git a/app/scripts/framework/views/view.js b/app/scripts/framework/views/view.js index ba063645..55db5bea 100644 --- a/app/scripts/framework/views/view.js +++ b/app/scripts/framework/views/view.js @@ -4,9 +4,11 @@ import { Tip } from 'util/ui/tip'; import { KeyHandler } from 'comp/browser/key-handler'; import { Logger } from 'util/logger'; -const OnlyDirectEvents = { +const DoesNotBubble = { mouseenter: true, - mouseleave: true + mouseleave: true, + blur: true, + focus: true }; class View extends EventEmitter { @@ -19,6 +21,7 @@ class View extends EventEmitter { hidden = false; removed = false; eventListeners = {}; + elementEventListeners = []; debugLogger = localStorage.debugViews ? new Logger('view', this.constructor.name) : undefined; constructor(model = undefined, options = {}) { @@ -59,6 +62,7 @@ class View extends EventEmitter { if (this.el) { const mountRoot = this.options.ownParent ? this.el.firstChild : this.el; morphdom(mountRoot, html); + this.bindElementEvents(); } else { let parent = this.options.parent || this.parent; if (parent) { @@ -102,11 +106,9 @@ class View extends EventEmitter { if (spaceIx > 0) { event = eventDef.substr(0, spaceIx); selector = eventDef.substr(spaceIx + 1); - if (OnlyDirectEvents[event]) { - throw new Error( - `Event listener ${eventDef} defined in ${this.constructor.name} ` + - `can be installed only on the view itself` - ); + if (DoesNotBubble[event]) { + this.elementEventListeners.push({ event, selector, method, els: [] }); + continue; } } else { event = eventDef; @@ -117,17 +119,48 @@ class View extends EventEmitter { eventsMap[event].push({ selector, method }); } for (const [event, handlers] of Object.entries(eventsMap)) { - this.debugLogger && this.debugLogger.debug('Bind', event, handlers); + this.debugLogger && this.debugLogger.debug('Bind', 'view', event, handlers); const listener = e => this.eventListener(e, handlers); this.eventListeners[event] = listener; this.el.addEventListener(event, listener); } + this.bindElementEvents(); } unbindEvents() { for (const [event, listener] of Object.entries(this.eventListeners)) { this.el.removeEventListener(event, listener); } + this.unbindElementEvents(); + } + + bindElementEvents() { + if (!this.elementEventListeners.length) { + return; + } + this.unbindElementEvents(); + for (const cfg of this.elementEventListeners) { + const els = this.el.querySelectorAll(cfg.selector); + this.debugLogger && + this.debugLogger.debug('Bind', 'element', cfg.event, cfg.selector, els.length); + cfg.listener = e => this.eventListener(e, [cfg]); + for (const el of els) { + el.addEventListener(cfg.event, cfg.listener); + cfg.els.push(el); + } + } + } + + unbindElementEvents() { + if (!this.elementEventListeners.length) { + return; + } + for (const cfg of this.elementEventListeners) { + for (const el of cfg.els) { + el.removeEventListener(cfg.event, cfg.listener); + } + cfg.els = []; + } } eventListener(e, handlers) { diff --git a/app/scripts/models/file-info-model.js b/app/scripts/models/file-info-model.js index 8629f8aa..0f190f6f 100644 --- a/app/scripts/models/file-info-model.js +++ b/app/scripts/models/file-info-model.js @@ -24,6 +24,7 @@ FileInfoModel.defineModelProperties({ openDate: null, keyFileName: null, keyFileHash: null, + keyFilePath: null, opts: null, backup: null, fingerprint: null diff --git a/app/scripts/plugins/plugin-gallery.js b/app/scripts/plugins/plugin-gallery.js index 251645e1..e8fb7fbc 100644 --- a/app/scripts/plugins/plugin-gallery.js +++ b/app/scripts/plugins/plugin-gallery.js @@ -33,22 +33,33 @@ const PluginGallery = { this.logger.error('Network error loading plugins'); resolve(); }); - }).then(data => { - return this.verifySignature(data).then(gallery => { + }) + .then(data => { this.loading = false; - this.loadError = !gallery; - if (gallery) { - this.logger.debug( - `Loaded ${gallery.plugins.length} plugins`, - this.logger.ts(ts) - ); - this.gallery = gallery; - this.saveGallery(gallery); + if (!data) { + this.loadError = true; + Events.emit('plugin-gallery-load-complete'); + return; } + return this.verifySignature(data).then(gallery => { + this.loadError = !gallery; + if (gallery) { + this.logger.debug( + `Loaded ${gallery.plugins.length} plugins`, + this.logger.ts(ts) + ); + this.gallery = gallery; + this.saveGallery(gallery); + } + Events.emit('plugin-gallery-load-complete'); + return gallery; + }); + }) + .catch(e => { + this.loadError = true; + this.logger.error('Error loading plugin gallery', e); Events.emit('plugin-gallery-load-complete'); - return gallery; }); - }); }, verifySignature(gallery) { diff --git a/app/scripts/plugins/plugin-manager.js b/app/scripts/plugins/plugin-manager.js index 29af30b0..1d045408 100644 --- a/app/scripts/plugins/plugin-manager.js +++ b/app/scripts/plugins/plugin-manager.js @@ -64,7 +64,7 @@ class PluginManager extends Model { } installIfNew(url, expectedManifest, skipSignatureValidation) { - const plugin = this.plugins.find({ url }); + const plugin = this.plugins.find(p => p.url === url); if (plugin && plugin.status !== 'invalid') { return Promise.resolve(); } diff --git a/app/scripts/storage/storage-base.js b/app/scripts/storage/storage-base.js index cb21dbb5..3a3657c5 100644 --- a/app/scripts/storage/storage-base.js +++ b/app/scripts/storage/storage-base.js @@ -2,7 +2,6 @@ import { Events } from 'framework/events'; import { Links } from 'const/links'; import { AppSettingsModel } from 'models/app-settings-model'; import { RuntimeDataModel } from 'models/runtime-data-model'; -import { Features } from 'util/features'; import { Logger } from 'util/logger'; const MaxRequestRetries = 3; @@ -30,18 +29,6 @@ class StorageBase { } } this.logger = new Logger('storage-' + this.name); - if (this._oauthReturnMessage) { - this.logger.debug('OAuth return message', this._oauthReturnMessage); - this._oauthProcessReturn(this._oauthReturnMessage); - delete this._oauthReturnMessage; - delete sessionStorage.authStorage; - if (Features.isStandalone) { - const [url, urlParams] = location.href.split(/[?#]/); - if (urlParams) { - location.href = url; - } - } - } return this; } @@ -49,10 +36,6 @@ class StorageBase { this.enabled = enabled; } - handleOAuthReturnMessage(message) { - this._oauthReturnMessage = message; - } - _xhr(config) { const xhr = new XMLHttpRequest(); if (config.responseType) { @@ -94,8 +77,10 @@ class StorageBase { if (this._oauthToken && !config.skipAuth) { xhr.setRequestHeader('Authorization', 'Bearer ' + this._oauthToken.accessToken); } - for (const [key, value] of Object.entries(config.headers)) { - xhr.setRequestHeader(key, value); + if (config.headers) { + for (const [key, value] of Object.entries(config.headers)) { + xhr.setRequestHeader(key, value); + } } let data = config.data; if (data instanceof ArrayBuffer) { @@ -135,9 +120,6 @@ class StorageBase { settings = Object.keys(settings) .map(key => key + '=' + settings[key]) .join(','); - if (Features.isStandalone) { - sessionStorage.authStorage = this.name; - } return window.open(url, title, settings); } diff --git a/app/scripts/util/kdbxweb/kdbxweb-init.js b/app/scripts/util/kdbxweb/kdbxweb-init.js index f9ddeef0..1c6ac4c5 100644 --- a/app/scripts/util/kdbxweb/kdbxweb-init.js +++ b/app/scripts/util/kdbxweb/kdbxweb-init.js @@ -115,7 +115,8 @@ const KdbxwebInit = { }); }, - workerPostRun() { + // eslint-disable-next-line object-shorthand + workerPostRun: function() { self.postMessage({ op: 'postRun' }); self.onmessage = e => { try { @@ -128,7 +129,8 @@ const KdbxwebInit = { }; }, - calcHash(Module, args) { + // eslint-disable-next-line object-shorthand + calcHash: function(Module, args) { let { password, salt } = args; const { memory, iterations, length, parallelism, type, version } = args; const passwordLen = password.byteLength; diff --git a/app/scripts/views/settings/settings-plugins-view.js b/app/scripts/views/settings/settings-plugins-view.js index 8a671cb5..ab95f260 100644 --- a/app/scripts/views/settings/settings-plugins-view.js +++ b/app/scripts/views/settings/settings-plugins-view.js @@ -235,7 +235,7 @@ class SettingsPluginsView extends View { pluginMatchesFilter(plugin) { const searchStr = this.searchStr; const manifest = plugin.manifest; - return ( + return !!( !searchStr || manifest.name.toLowerCase().indexOf(searchStr) >= 0 || (manifest.description && manifest.description.toLowerCase().indexOf(searchStr) >= 0) || diff --git a/desktop/app.js b/desktop/app.js index fd8a9e71..af0d15ef 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -50,6 +50,8 @@ app.setPath('userData', path.join(tempUserDataPath, tempUserDataPathRand)); setEnv(); restorePreferences(); +const appSettings = readAppSettings() || {}; + app.on('window-all-closed', () => { if (restartPending) { app.relaunch(); @@ -62,11 +64,10 @@ app.on('window-all-closed', () => { }); app.on('ready', () => { appReady = true; - const appSettings = readAppSettings() || {}; setAppOptions(); setSystemAppearance(); - createMainWindow(appSettings); - setGlobalShortcuts(appSettings); + createMainWindow(); + setGlobalShortcuts(); subscribePowerEvents(); deleteOldTempFiles(); hookRequestHeaders(); @@ -154,7 +155,7 @@ function setSystemAppearance() { } } -function createMainWindow(appSettings) { +function createMainWindow() { const windowOptions = { show: false, width: 1000, @@ -394,7 +395,7 @@ function notifyOpenFile() { } } -function setGlobalShortcuts(appSettings) { +function setGlobalShortcuts() { const defaultShortcutModifiers = process.platform === 'darwin' ? 'Ctrl+Alt+' : 'Shift+Alt+'; const defaultShortcuts = { CopyPassword: { shortcut: defaultShortcutModifiers + 'C', event: 'copy-password' }, diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 45b4c9c0..ba9c93d5 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,6 +1,6 @@ { "name": "KeeWeb", - "version": "1.11.0", + "version": "1.11.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/desktop/package.json b/desktop/package.json index 1c740a59..a5ea80d8 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "KeeWeb", - "version": "1.11.0", + "version": "1.11.5", "description": "Free cross-platform password manager compatible with KeePass", "main": "main.js", "homepage": "https://keeweb.info", diff --git a/package-lock.json b/package-lock.json index 507b08b4..d37ea865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "keeweb", - "version": "1.11.0", + "version": "1.11.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a156e6e4..5214ddb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keeweb", - "version": "1.11.0", + "version": "1.11.5", "description": "Free cross-platform password manager compatible with KeePass", "main": "Gruntfile.js", "private": true, diff --git a/release-notes.md b/release-notes.md index 37fa5b78..1cc5064d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,26 @@ Release notes ------------- +##### v1.11.5 (2019-09-29) +`-` fix #1279: error opening files with saved keyfiles + +##### v1.11.4 (2019-09-29) +`-` fix #1277: auto-type window matching on Linux +`-` fix #1278: entry selection auto-type window issues +`-` fixed displaying errors on the plugins page + +##### v1.11.3 (2019-09-29) +`-` fix #1275: starting the app after closing on macOS +`-` fix #1276 GDrive connection issues + +##### v1.11.2 (2019-09-29) +`-` fix #1272: Argon2 error +`-` fixed Dropbox connection on iOS 13 homescreen +`-` fixed plugin search filtering + +##### v1.11.1 (2019-09-28) +`-` fix #1270: password change control focus +`-` fix #1271: loading custom plugins from config + ##### v1.11.0 (2019-09-28) `+` #1125: field actions: copy, reveal, auto-type `+` #107: multiline custom fields support