diff --git a/CHANGELOG b/CHANGELOG index 4c27d13..7cd8353 100755 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,38 @@ +1.2.0 (29-07-2018) +========================= +- Replace "Ignored Sites" with new "Site Preferences" settings page [#208] +- Fix checks throwing errors [#207, #222] +- Ignore more non-input elements [#212, 210] +- Fix URL matching when there is a trailing slash [#231] +- Allow infiting waiting on "Remember Credentials" popup [#232] +- Minor user interface adjustments [#233, #230, #213] +- Improve search field detection [#195] + +1.1.7 (13-06-2018) +========================= +- Fix credential field detection regression [#199] + +1.1.6 (12-06-2018) +========================= +- Disable single username field detection [#194] +- Fix ignored sites [#196] +- Detect credential fields without type [#198] + +1.1.5 (11-06-2018) +========================= +- Fix search fields being detected as username fields [#189] + +1.1.4 (10-06-2018) +========================= +- Improve performance of field detection and limit it to 100 fields [#166,#157,185] +- Improve option to ignore fields [#170] +- Automatically retrieve credentials after unlocking the database [#153] +- Fix option to only save the domain name not working as intended [#151] +- Fix credentials popup not being shown on some websites [#154] +- Improve detection of username fields [#164] +- Allow filling of TOTP fields when they are on a separate page [#162] +- Ignore invisible input fields more strictly [#176] + 1.1.3 (11-05-2018) ========================= - Remove autoreconnect to prevent proxy process leakage on Windows [#147] diff --git a/keepassxc-browser/background/browserAction.js b/keepassxc-browser/background/browserAction.js index c728f6d..caf095f 100755 --- a/keepassxc-browser/background/browserAction.js +++ b/keepassxc-browser/background/browserAction.js @@ -8,7 +8,7 @@ const BLINK_TIMEOUT_REDIRECT_COUNT_DEFAULT = 1; browserAction.show = function(callback, tab) { let data = {}; - if (!page.tabs[tab.id] || page.tabs[tab.id].stack.length == 0) { + if (!page.tabs[tab.id] || page.tabs[tab.id].stack.length === 0) { browserAction.showDefault(callback, tab); return; } @@ -36,7 +36,7 @@ browserAction.update = function(interval) { let data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1]; - if (typeof data.visibleForMilliSeconds !== 'undefined') { + if (data.visibleForMilliSeconds !== undefined && data.visibleForMilliSeconds !== -1) { if (data.visibleForMilliSeconds <= 0) { browserAction.stackPop(page.currentTabId); browserAction.show(null, {'id': page.currentTabId}); @@ -87,7 +87,7 @@ browserAction.showDefault = function(callback, tab) { }); }; -browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visibleForMilliSeconds, visibleForPageUpdates, redirectOffset, dontShow) { +browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visibleForMilliSeconds, visibleForPageUpdates, redirectOffset, dontShow) { const id = tab.id || page.currentTabId; if (!level) { @@ -103,15 +103,15 @@ browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visib stackData.popup = popup; } - if (visibleForMilliSeconds) { + if (visibleForMilliSeconds !== undefined) { stackData.visibleForMilliSeconds = visibleForMilliSeconds; } - if (visibleForPageUpdates) { + if (visibleForPageUpdates !== undefined) { stackData.visibleForPageUpdates = visibleForPageUpdates; } - if (redirectOffset) { + if (redirectOffset !== undefined) { stackData.redirectOffset = redirectOffset; } @@ -181,7 +181,7 @@ browserAction.removeRememberPopup = function(callback, tab, removeImmediately) { return; } - if( page.tabs[tab.id].stack.length == 0) { + if (page.tabs[tab.id].stack.length === 0) { page.clearCredentials(tab.id); return; } @@ -205,21 +205,23 @@ browserAction.setRememberPopup = function(tabId, username, password, url, userna browser.storage.local.get({'settings': {}}).then(function(item) { const settings = item.settings; - // Don't show anything if the site is in the ignore list - for (const site in settings.ignoredSites) { - if (site === url) { - return; + // Don't show anything if the site is in the ignore + if (settings.sitePreferences !== undefined) { + for (const site of settings.sitePreferences) { + if (site.ignore === IGNORE_NORMAL && (site.url === url || siteMatch(site.url, url))) { + return; + } } } const id = tabId || page.currentTabId; - let timeoutMinMillis = Number(getValueOrDefault(settings, 'blinkMinTimeout', BLINK_TIMEOUT_REDIRECT_THRESHOLD_TIME_DEFAULT, 0)); + let timeoutMinMillis = Number(getValueOrDefault(settings, 'blinkMinTimeout', BLINK_TIMEOUT_REDIRECT_THRESHOLD_TIME_DEFAULT, -1)); if (timeoutMinMillis > 0) { timeoutMinMillis += Date.now(); } - const blinkTimeout = getValueOrDefault(settings, 'blinkTimeout', BLINK_TIMEOUT_DEFAULT, 0); + const blinkTimeout = getValueOrDefault(settings, 'blinkTimeout', BLINK_TIMEOUT_DEFAULT, -1); const pageUpdateAllowance = getValueOrDefault(settings, 'allowedRedirect', BLINK_TIMEOUT_REDIRECT_COUNT_DEFAULT, 0); const stackData = { diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 8d1b39c..244ded0 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -159,6 +159,9 @@ kpxcEvent.onReconnect = function(callback, tab) { // Add a small timeout after reconnecting. Just to make sure. It's not pretty, I know :( setTimeout(() => { keepass.reconnect(callback, tab).then((configured) => { + browser.tabs.sendMessage(tab.id, { + action: 'redetect_fields' + }); kpxcEvent.showStatus(configured, tab, callback); }); }, 500); @@ -269,6 +272,14 @@ kpxcEvent.pageClearLogins = function(callback, tab, alreadyCalled) { callback(); }; +kpxcEvent.pageGetLoginId = function(callback, tab) { + callback(page.loginId); +}; + +kpxcEvent.pageSetLoginId = function(callback, tab, loginId) { + page.loginId = loginId; +}; + // all methods named in this object have to be declared BEFORE this! kpxcEvent.messageHandlers = { 'add_credentials': keepass.addCredentials, @@ -284,6 +295,8 @@ kpxcEvent.messageHandlers = { 'load_settings': kpxcEvent.onLoadSettings, 'lock-database': kpxcEvent.lockDatabase, 'page_clear_logins': kpxcEvent.pageClearLogins, + 'page_get_login_id': kpxcEvent.pageGetLoginId, + 'page_set_login_id': kpxcEvent.pageSetLoginId, 'pop_stack': kpxcEvent.onPopStack, 'popup_login': kpxcEvent.onLoginPopup, 'popup_multiple-fields': kpxcEvent.onMultipleFieldsPopup, diff --git a/keepassxc-browser/background/init.js b/keepassxc-browser/background/init.js index ce7fed7..0e015ce 100644 --- a/keepassxc-browser/background/init.js +++ b/keepassxc-browser/background/init.js @@ -16,7 +16,6 @@ keepass.migrateKeyRing().then(() => { // Milliseconds for intervall (e.g. to update browserAction) const _interval = 250; - /** * Generate information structure for created tab and invoke all needed * functions if tab is created in foreground @@ -71,7 +70,13 @@ browser.tabs.onActivated.addListener((activeInfo) => { * @param {object} changeInfo */ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + // If the tab URL has changed (e.g. logged in) clear credentials + if (changeInfo.url) { + page.clearLogins(tabId); + } + if (changeInfo.status === 'complete') { + browserAction.showDefault(null, tab); kpxcEvent.invoke(browserAction.removeRememberPopup, null, tabId, []); } }); diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index e8fe811..698bb36 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -185,6 +185,7 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password, } else if (response.error && response.errorCode) { keepass.handleError(tab, response.errorCode, response.error); + callback('error'); } else { browserAction.showDefault(null, tab); @@ -839,6 +840,17 @@ keepass.onNativeMessage = function(response) { keepass.testAssociation((associationResponse) => { keepass.isConfigured().then((configured) => { keepass.updatePopup(configured ? 'normal' : 'cross'); + + // Send message to content script + browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + if (tabs.length) { + browser.tabs.sendMessage(tabs[0].id, { + action: 'check_database_hash', + hash: {old: keepass.previousDatabaseHash, new: keepass.databaseHash} + }); + keepass.previousDatabaseHash = keepass.databaseHash; + } + }); }); }, null); } @@ -1041,7 +1053,9 @@ keepass.reconnect = function(callback, tab) { }; keepass.updatePopup = function(iconType) { - const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1]; - data.iconType = iconType; - browserAction.show(null, {'id': page.currentTabId}); + if (page && page.tabs.length > 0) { + const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1]; + data.iconType = iconType; + browserAction.show(null, {'id': page.currentTabId}); + } }; diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index 60be892..f56ebac 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -16,6 +16,7 @@ var page = {}; page.tabs = []; page.currentTabId = -1; page.blockedTabs = []; +page.loginId = -1; page.initSettings = function() { return new Promise((resolve, reject) => { @@ -104,6 +105,10 @@ page.clearCredentials = function(tabId, complete) { }; page.clearLogins = function(tabId) { + if (!page.tabs[tabId]) { + return; + } + page.tabs[tabId].loginList = []; }; diff --git a/keepassxc-browser/global.js b/keepassxc-browser/global.js index 55456cd..23cbd06 100755 --- a/keepassxc-browser/global.js +++ b/keepassxc-browser/global.js @@ -1,5 +1,13 @@ 'use strict'; +const IGNORE_NOTHING = 'ignoreNothing'; +const IGNORE_NORMAL = 'ignoreNormal'; +const IGNORE_FULL = 'ignoreFull'; + +var schemeSegment = '(\\*|http|https|ws|wss|file|ftp)'; +var hostSegment = '(\\*|(?:\\*\\.)?(?:[^/*]+))?'; +var pathSegment = '(.*)'; + var isFirefox = function() { if (!(/Chrome/.test(navigator.userAgent) && /Google/.test(navigator.vendor))) { return true; @@ -15,3 +23,71 @@ var showNotification = function(message) { 'message': message }); }; + +/** + * Transforms a valid match pattern into a regular expression + * which matches all URLs included by that pattern. + * + * @param {string} pattern The pattern to transform. + * @return {RegExp} The pattern's equivalent as a RegExp. + * @throws {TypeError} If the pattern is not a valid MatchPattern + * + * https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns + */ +var matchPatternToRegExp = function(pattern) { + if (pattern === '') { + return (/^(?:http|https|file|ftp|app):\/\//); + } + + const matchPatternRegExp = new RegExp( + `^${schemeSegment}://${hostSegment}/${pathSegment}$` + ); + + let match = matchPatternRegExp.exec(pattern); + if (!match) { + throw new TypeError(pattern + ' is not a valid MatchPattern'); + } + + let [, scheme, host, path] = match; + if (!host) { + throw new TypeError(pattern + ' does not have a valid host'); + } + + let regex = '^'; + + if (scheme === '*') { + regex += '(http|https)'; + } else { + regex += scheme; + } + + regex += '://'; + + if (host && host === '*') { + regex += '[^/]+?'; + } else if (host) { + if (host.match(/^\*\./)) { + regex += '[^/]*?'; + host = host.substring(2); + } + regex += host.replace(/\./g, '\\.'); + } + + if (path) { + if (path === '*') { + regex += '(/.*)?'; + } else if (path.charAt(0) !== '/') { + regex += '/'; + regex += path.replace(/\./g, '\\.').replace(/\*/g, '.*?'); + regex += '/?'; + } + } + + regex += '$'; + return new RegExp(regex); +}; + +var siteMatch = function(site, url) { + const rx = matchPatternToRegExp(site); + return url.match(rx); +}; \ No newline at end of file diff --git a/keepassxc-browser/keepassxc-browser.js b/keepassxc-browser/keepassxc-browser.js index 490f675..3d90c51 100755 --- a/keepassxc-browser/keepassxc-browser.js +++ b/keepassxc-browser/keepassxc-browser.js @@ -6,6 +6,8 @@ _called.retrieveCredentials = false; _called.clearLogins = false; _called.manualFillRequested = 'none'; let _loginId = -1; +let _singleInputEnabledForPage = false; +const _maximumInputs = 100; // Count of detected form fields on the page var _detectedFields = 0; @@ -21,12 +23,16 @@ browser.runtime.onMessage.addListener(function(req, sender, callback) { if (cip.u) { cip.setValueWithChange(cip.u, cip.credentials[req.id].login); combination = cipFields.getCombination('username', cip.u); - _loginId = req.id; + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [req.id] + }); cip.u.focus(); } if (cip.p) { cip.setValueWithChange(cip.p, cip.credentials[req.id].password); - _loginId = req.id; + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [req.id] + }); combination = cipFields.getCombination('password', cip.p); } @@ -497,7 +503,7 @@ cipForm.init = function(form, credentialFields) { // TODO: could be called multiple times --> update credentialFields // not already initialized && password-field is not null - if (!form.data('cipForm-initialized') && (credentialFields.password || credentialFields.username)) { + if (!form.data('cipForm-initialized') && (credentialFields.password || (_singleInputEnabledForPage && credentialFields.username))) { form.data('cipForm-initialized', true); cipForm.setInputFields(form, credentialFields); form.submit(cipForm.onSubmit); @@ -614,8 +620,8 @@ cipDefine.initDescription = function() { .addClass('btn').addClass('btn-primary') .css('margin-right', '15px') .click(function(e) { - if (!cip.settings['defined-credential-fields']) { - cip.settings['defined-credential-fields'] = {}; + if (!cip.settings['defined-custom-fields']) { + cip.settings['defined-custom-fields'] = {}; } if (cipDefine.selection.username) { @@ -632,7 +638,8 @@ cipDefine.initDescription = function() { fieldIds.push(cipFields.prepareId(i)); } - cip.settings['defined-credential-fields'][document.location.href] = { + const location = cip.getDocumentLocation(); + cip.settings['defined-custom-fields'][location] = { username: cipDefine.selection.username, password: cipDefine.selection.password, fields: fieldIds @@ -652,7 +659,8 @@ cipDefine.initDescription = function() { $description.append($btnAgain); $description.append($btnDismiss); - if (cip.settings['defined-credential-fields'] && cip.settings['defined-credential-fields'][document.location.href]) { + const location = cip.getDocumentLocation(); + if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { const $p = jQuery('

').html('For this page credential fields are already selected and will be overwritten.
'); const $btnDiscard = jQuery(' What is the maximum time (ms) the icon should blink after detecting new credentials
- Default: 7500 + Default: 7500, Infinite: -1

@@ -189,7 +189,7 @@
-

Connected Databases

+

Connected databases


The following KeePassXC databases are connected to KeePassXC-Browser. @@ -241,16 +241,16 @@

- -
-

Specified credential fields

+ +
+

Custom credential fields


If KeePassXC-Browser detects the wrong credential fields, you are able to specify the correct fields by yourself.
- Just go to the page and click on the KeePassXC-Browser-Icon, now select Choose own credential fields for this page. + Go to the page and click on the KeePassXC-Browser-Icon, now select Choose custom credential fields for this page.
- On this page you can manage theses specified credential fields. + On this page you can manage saved custom credential fields.

@@ -261,7 +261,7 @@ - + @@ -269,16 +269,16 @@
No specified credential fields found.No saved custom credential fields found.
-