diff --git a/keepassxc-browser/background/browserAction.js b/keepassxc-browser/background/browserAction.js index dcfd8d5..4f6835d 100755 --- a/keepassxc-browser/background/browserAction.js +++ b/keepassxc-browser/background/browserAction.js @@ -15,8 +15,7 @@ browserAction.show = function(callback, tab) { if (!page.tabs[tab.id] || page.tabs[tab.id].stack.length === 0) { browserAction.showDefault(callback, tab); return; - } - else { + } else { data = page.tabs[tab.id].stack[page.tabs[tab.id].stack.length - 1]; } @@ -38,13 +37,13 @@ browserAction.update = function(interval) { return; } - let data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1]; + const data = page.tabs[page.currentTabId].stack[page.tabs[page.currentTabId].stack.length - 1]; if (data.visibleForMilliSeconds !== undefined && data.visibleForMilliSeconds !== -1) { if (data.visibleForMilliSeconds <= 0) { browserAction.stackPop(page.currentTabId); browserAction.disableLoop(); - browserAction.show(null, {'id': page.currentTabId}); + browserAction.show(null, { 'id': page.currentTabId }); page.clearCredentials(page.currentTabId); return; } @@ -72,7 +71,7 @@ browserAction.update = function(interval) { }; browserAction.showDefault = function(callback, tab) { - let stackData = { + const stackData = { level: 1, iconType: 'normal', popup: 'popup.html' @@ -100,7 +99,7 @@ browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visib level = 1; } - let stackData = { + const stackData = { level: level, icon: icon }; @@ -123,13 +122,12 @@ browserAction.stackAdd = function(callback, tab, icon, popup, level, push, visib if (push) { browserAction.stackPush(stackData, id); - } - else { + } else { browserAction.stackUnshift(stackData, id); } if (!dontShow) { - browserAction.show(null, {'id': id}); + browserAction.show(null, { 'id': id }); } }; @@ -142,17 +140,15 @@ browserAction.removeLevelFromStack = function(callback, tab, level, type, dontSh type = '<='; } - let newStack = []; + const newStack = []; for (const i of page.tabs[tab.id].stack) { - if ( - (type == '<' && i.level >= level) || - (type == '<=' && i.level > level) || - (type == '=' && i.level != level) || - (type == '==' && i.level != level) || - (type == '!=' && i.level == level) || - (type == '>' && i.level <= level) || - (type == '>=' && i.level < level) - ) { + if ((type === '<' && i.level >= level) || + (type === '<=' && i.level > level) || + (type === '=' && i.level !== level) || + (type === '==' && i.level !== level) || + (type === '!=' && i.level === level) || + (type === '>' && i.level <= level) || + (type === '>=' && i.level < level)) { newStack.push(i); } } @@ -199,10 +195,8 @@ browserAction.removeRememberPopup = function(callback, tab, removeImmediately) { browserAction.stackPop(tab.id); browserAction.show(null, {"id": tab.id}); page.clearCredentials(tab.id); - return; - } - else if (!isNaN(data.visibleForPageUpdates) && data.redirectOffset > 0 && currentMS >= data.redirectOffset) { - data.visibleForPageUpdates = data.visibleForPageUpdates - 1; + } else if (!isNaN(data.visibleForPageUpdates) && data.redirectOffset > 0 && currentMS >= data.redirectOffset) { + data.visibleForPageUpdates -= 1; } } }; @@ -256,17 +250,18 @@ browserAction.setRememberPopup = function(tabId, username, password, url, userna list: credentialsList }; - browserAction.show(null, {'id': id}); + browserAction.show(null, { 'id': id }); if (page.settings.showLoginNotifications) { const message = tr('rememberCredentialsPopup'); const buttons = [ - { - 'title': tr('popupButtonClose') - }, - { - 'title': tr('popupButtonIgnore') - }]; + { + 'title': tr('popupButtonClose') + }, + { + 'title': tr('popupButtonIgnore') + } + ]; browser.notifications.create({ 'type': 'basic', @@ -293,7 +288,7 @@ function getValueOrDefault(settings, key, defaultVal, min) { val = defaultVal; } return val; - } catch(e) { + } catch (e) { return defaultVal; } } @@ -312,16 +307,16 @@ browserAction.generateIconName = function(iconType, icon) { }; browserAction.ignoreSite = function(url) { - browser.windows.getCurrent().then((win) => { + browser.windows.getCurrent().then(() => { // Get current active window browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => { const tab = tabs[0]; // Send the message to the current tab's content script - browser.runtime.getBackgroundPage().then((global) => { + browser.runtime.getBackgroundPage().then(() => { browser.tabs.sendMessage(tab.id, { - action: 'ignore-site', - args: [url] + action: 'ignore_site', + args: [ url ] }); }); }); diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 2d6477b..921374f 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -40,7 +40,7 @@ kpxcEvent.invoke = function(handler, callback, senderTabId, args, secondTime) { page.createTabEntry(senderTabId); } - // remove information from no longer existing tabs + // Remove information from no longer existing tabs page.removePageInformationFromNotExistingTabs(); browser.tabs.get(senderTabId).then((tab) => { @@ -70,8 +70,7 @@ kpxcEvent.invoke = function(handler, callback, senderTabId, args, secondTime) { if (handler) { handler.apply(this, args); - } - else { + } else { console.log('undefined handler for tab ' + tab.id); } }).catch((e) => { @@ -113,12 +112,12 @@ kpxcEvent.onLoadSettings = function(callback, tab) { }; kpxcEvent.onLoadKeyRing = function(callback, tab) { - browser.storage.local.get({'keyRing': {}}).then(function(item) { + browser.storage.local.get({ 'keyRing': {} }).then(function(item) { keepass.keyRing = item.keyRing; if (keepass.isAssociated() && !keepass.keyRing[keepass.associated.hash]) { keepass.associated = { - "value": false, - "hash": null + 'value': false, + 'hash': null }; } callback(item.keyRing); @@ -128,7 +127,7 @@ kpxcEvent.onLoadKeyRing = function(callback, tab) { }; kpxcEvent.onSaveSettings = function(callback, tab, settings) { - browser.storage.local.set({'settings': settings}).then(function() { + browser.storage.local.set({ 'settings': settings }).then(function() { kpxcEvent.onLoadSettings(callback, tab); }); }; @@ -168,9 +167,9 @@ kpxcEvent.onReconnect = function(callback, tab) { }; kpxcEvent.lockDatabase = function(callback, tab) { - keepass.lockDatabase(tab).then((response => { + keepass.lockDatabase(tab).then(() => { kpxcEvent.showStatus(true, tab, callback); - })); + }); }; kpxcEvent.onPopStack = function(callback, tab) { @@ -191,7 +190,7 @@ kpxcEvent.onGetConnectedDatabase = function(callback, tab) { }; kpxcEvent.onGetKeePassXCVersions = function(callback, tab) { - if (keepass.currentKeePassXC == '') { + if (keepass.currentKeePassXC === '') { keepass.getDatabaseHash((res) => { callback({'current': keepass.currentKeePassXC, 'latest': keepass.latestKeePassXC.version}); }, tab); @@ -229,7 +228,7 @@ kpxcEvent.onSetRememberPopup = function(callback, tab, username, password, url, }; kpxcEvent.onLoginPopup = function(callback, tab, logins) { - let stackData = { + const stackData = { level: 1, iconType: 'questionmark', popup: 'popup_login.html' @@ -245,7 +244,7 @@ kpxcEvent.initHttpAuth = function(callback) { } kpxcEvent.onHTTPAuthPopup = function(callback, tab, data) { - let stackData = { + const stackData = { level: 1, iconType: 'questionmark', popup: 'popup_httpauth.html' @@ -256,7 +255,7 @@ kpxcEvent.onHTTPAuthPopup = function(callback, tab, data) { }; kpxcEvent.onMultipleFieldsPopup = function(callback, tab) { - let stackData = { + const stackData = { level: 1, iconType: 'normal', popup: 'popup_multiple-fields.html' @@ -280,7 +279,7 @@ kpxcEvent.pageSetLoginId = function(callback, tab, loginId) { page.loginId = loginId; }; -// all methods named in this object have to be declared BEFORE this! +// All methods named in this object have to be declared BEFORE this! kpxcEvent.messageHandlers = { 'add_credentials': keepass.addCredentials, 'associate': keepass.associate, diff --git a/keepassxc-browser/background/httpauth.js b/keepassxc-browser/background/httpauth.js index f1fbba0..cf9caef 100755 --- a/keepassxc-browser/background/httpauth.js +++ b/keepassxc-browser/background/httpauth.js @@ -31,7 +31,7 @@ httpAuth.init = function() { }; httpAuth.requestCompleted = function(details) { - let index = httpAuth.requests.indexOf(details.requestId); + const index = httpAuth.requests.indexOf(details.requestId); if (index >= 0) { httpAuth.requests.splice(index, 1); } @@ -57,7 +57,7 @@ httpAuth.retrieveCredentials = function(tabId, url, submitUrl, forceCallback) { httpAuth.processPendingCallbacks = async function(details, resolve, reject) { if (httpAuth.requests.indexOf(details.requestId) >= 0 || !page.tabs[details.tabId]) { - reject({cancel: false}); + reject({ cancel: false }); return; } @@ -74,7 +74,7 @@ httpAuth.processPendingCallbacks = async function(details, resolve, reject) { }; httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) { - // at least one login found --> use first to login + // At least one login found --> use first to login if (logins.length > 0 && page.settings.autoFillAndSend) { if (logins.length === 1) { resolve({ @@ -89,9 +89,7 @@ httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) { } kpxcEvent.onHTTPAuthPopup(null, { 'id': details.tabId }, { 'logins': logins, 'url': details.searchUrl, 'resolve': resolve }); } - } - // no logins found - else { - reject({cancel: false}); + } else { + reject({ cancel: false }); // No logins found } }; diff --git a/keepassxc-browser/background/init.js b/keepassxc-browser/background/init.js index 842217f..f4797c5 100644 --- a/keepassxc-browser/background/init.js +++ b/keepassxc-browser/background/init.js @@ -46,9 +46,9 @@ browser.tabs.onRemoved.addListener((tabId, removeInfo) => { * @param {object} activeInfo */ browser.tabs.onActivated.addListener((activeInfo) => { - // remove possible credentials from old tab information + // Remove possible credentials from old tab information page.clearCredentials(page.currentTabId, true); - browserAction.removeRememberPopup(null, {'id': page.currentTabId}, true); + browserAction.removeRememberPopup(null, { 'id': page.currentTabId }, true); browser.tabs.get(activeInfo.tabId).then((info) => { if (info && info.id) { @@ -81,14 +81,14 @@ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { browser.runtime.onMessage.addListener(kpxcEvent.onMessage); const contextMenuItems = [ - {title: tr('contextMenuFillUsernameAndPassword'), action: 'fill_user_pass'}, - {title: tr('contextMenuFillPassword'), action: 'fill_pass_only'}, + {title: tr('contextMenuFillUsernameAndPassword'), action: 'fill_username_password'}, + {title: tr('contextMenuFillPassword'), action: 'fill_password'}, {title: tr('contextMenuFillTOTP'), action: 'fill_totp'}, {title: tr('contextMenuShowPasswordGeneratorIcons'), action: 'activate_password_generator'}, {title: tr('contextMenuSaveCredentials'), action: 'remember_credentials'} ]; -let menuContexts = ['editable']; +const menuContexts = ['editable']; if (isFirefox()) { menuContexts.push('password'); @@ -102,33 +102,19 @@ for (const item of contextMenuItems) { onclick: (info, tab) => { browser.tabs.sendMessage(tab.id, { action: item.action - }).catch((e) => {console.log(e);}); + }).catch((e) => { + console.log(e); + }); } }); } // Listen for keyboard shortcuts specified by user browser.commands.onCommand.addListener((command) => { - if (command === 'fill-username-password') { + if (contextMenuItems.some(e => e.action === command)) { browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { if (tabs.length) { - browser.tabs.sendMessage(tabs[0].id, { action: 'fill_user_pass' }); - } - }); - } - - if (command === 'fill-password') { - browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - if (tabs.length) { - browser.tabs.sendMessage(tabs[0].id, { action: 'fill_pass_only' }); - } - }); - } - - if (command === 'fill-totp') { - browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - if (tabs.length) { - browser.tabs.sendMessage(tabs[0].id, { action: 'fill_totp' }); + browser.tabs.sendMessage(tabs[0].id, { action: command }); } }); } diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index f813c63..8549fbc 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -15,12 +15,12 @@ keepass.nativeHostName = 'org.keepassxc.keepassxc_browser'; keepass.nativePort = null; keepass.keySize = 24; keepass.latestVersionUrl = 'https://api.github.com/repos/keepassxreboot/keepassxc/releases/latest'; -keepass.cacheTimeout = 30 * 1000; // milliseconds +keepass.cacheTimeout = 30 * 1000; // Milliseconds keepass.databaseHash = ''; keepass.previousDatabaseHash = ''; keepass.keyId = 'keepassxc-browser-cryptokey-name'; keepass.keyBody = 'keepassxc-browser-key'; -keepass.messageTimeout = 500; // milliseconds +keepass.messageTimeout = 500; // Milliseconds keepass.nonce = nacl.util.encodeBase64(nacl.randomBytes(keepass.keySize)); const kpActions = { @@ -79,7 +79,7 @@ const kpErrors = { }; browser.storage.local.get({ - 'latestKeePassXC': {'version': '', 'lastChecked': null}, + 'latestKeePassXC': { 'version': '', 'lastChecked': null }, 'keyRing': {}}).then((item) => { keepass.latestKeePassXC = item.latestKeePassXC; keepass.keyRing = item.keyRing; @@ -88,11 +88,11 @@ browser.storage.local.get({ keepass.sendNativeMessage = function(request, enableTimeout = false) { return new Promise((resolve, reject) => { let timeout; - let action = request.action; - let ev = keepass.nativePort.onMessage; + const requestAction = request.action; + const ev = keepass.nativePort.onMessage; - let listener = ((port, action) => { - let handler = (msg) => { + const listener = ((port, action) => { + const handler = (msg) => { if (msg && msg.action === action) { port.removeListener(handler); if (enableTimeout) { @@ -102,7 +102,7 @@ keepass.sendNativeMessage = function(request, enableTimeout = false) { } }; return handler; - })(ev, action); + })(ev, requestAction); ev.addListener(listener); @@ -110,7 +110,7 @@ keepass.sendNativeMessage = function(request, enableTimeout = false) { if (enableTimeout) { timeout = setTimeout(() => { const errorMessage = { - action: action, + action: requestAction, error: kpErrors.getError(kpErrors.TIMEOUT_OR_NOT_CONNECTED), errorCode: kpErrors.TIMEOUT_OR_NOT_CONNECTED }; @@ -137,19 +137,19 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password, page.tabs[tab.id].errorMessage = null; } - keepass.testAssociation((response) => { - if (!response) { + keepass.testAssociation((taResponse) => { + if (!taResponse) { browserAction.showDefault(null, tab); callback([]); return; } const kpAction = kpActions.SET_LOGIN; - const {dbid} = keepass.getCryptoKey(); + const { dbid } = keepass.getCryptoKey(); const nonce = keepass.getNonce(); const incrementedNonce = keepass.incrementedNonce(nonce); - let messageData = { + const messageData = { action: kpAction, id: dbid, login: username, @@ -181,12 +181,10 @@ keepass.updateCredentials = function(callback, tab, entryId, username, password, const message = nacl.util.encodeUTF8(res); const parsed = JSON.parse(message); callback(keepass.verifyResponse(parsed, incrementedNonce) ? 'success' : 'error'); - } - else if (response.error && response.errorCode) { + } else if (response.error && response.errorCode) { keepass.handleError(tab, response.errorCode, response.error); callback('error'); - } - else { + } else { browserAction.showDefault(null, tab); } }); @@ -219,16 +217,16 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb const kpAction = kpActions.GET_LOGINS; const nonce = keepass.getNonce(); const incrementedNonce = keepass.incrementedNonce(nonce); - const {dbid} = keepass.getCryptoKey(); + const { dbid } = keepass.getCryptoKey(); - for (let keyHash in keepass.keyRing) { + for (const keyHash in keepass.keyRing) { keys.push({ id: keepass.keyRing[keyHash].id, key: keepass.keyRing[keyHash].key }); } - let messageData = { + const messageData = { action: kpAction, id: dbid, url: url, @@ -267,21 +265,18 @@ keepass.retrieveCredentials = function(callback, tab, url, submiturl, forceCallb entries = parsed.entries; keepass.updateLastUsed(keepass.databaseHash); if (entries.length === 0) { - // questionmark-icon is not triggered, so we have to trigger for the normal symbol + // Questionmark-icon is not triggered, so we have to trigger for the normal symbol browserAction.showDefault(null, tab); } callback(entries); - } - else { + } else { console.log('RetrieveCredentials for ' + url + ' rejected'); } page.debug('keepass.retrieveCredentials() => entries.length = {1}', entries.length); - } - else if (response.error && response.errorCode) { + } else if (response.error && response.errorCode) { keepass.handleError(tab, response.errorCode, response.error); callback([]); - } - else { + } else { browserAction.showDefault(null, tab); callback([]); } @@ -337,17 +332,14 @@ keepass.generatePassword = function(callback, tab, forceCallback) { if (parsed.entries) { passwords = parsed.entries; keepass.updateLastUsed(keepass.databaseHash); - } - else { + } else { console.log('No entries returned. Is KeePassXC up-to-date?'); } - } - else { + } else { console.log('GeneratePassword rejected'); } callback(passwords); - } - else if (response.error && response.errorCode) { + } else if (response.error && response.errorCode) { keepass.handleError(tab, response.errorCode, response.error); } }); @@ -406,8 +398,7 @@ keepass.associate = function(callback, tab) { if (!keepass.verifyResponse(parsed, incrementedNonce)) { keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); - } - else { + } else { // Use public key as identification key with older KeePassXC releases const savedKey = keepass.compareVersion('2.3.4', keepass.currentKeePassXC) ? idKey : key; keepass.setCryptoKey(id, savedKey); // Save the new identification public key as id key for the database @@ -416,8 +407,7 @@ keepass.associate = function(callback, tab) { } browserAction.show(callback, tab); - } - else if (response.error && response.errorCode) { + } else if (response.error && response.errorCode) { keepass.handleError(tab, response.errorCode, response.error); } }); @@ -456,7 +446,7 @@ keepass.testAssociation = function(callback, tab, enableTimeout = false, trigger const kpAction = kpActions.TEST_ASSOCIATE; const nonce = keepass.getNonce(); const incrementedNonce = keepass.incrementedNonce(nonce); - const {dbid, dbkey} = keepass.getCryptoKey(); + const { dbid, dbkey } = keepass.getCryptoKey(); if (dbkey === null || dbid === null) { if (tab && page.tabs[tab.id]) { @@ -500,17 +490,14 @@ keepass.testAssociation = function(callback, tab, enableTimeout = false, trigger keepass.handleError(tab, kpErrors.ENCRYPTION_KEY_UNRECOGNIZED); keepass.associated.value = false; keepass.associated.hash = null; - } - else if (!keepass.isAssociated()) { + } else if (!keepass.isAssociated()) { keepass.handleError(tab, kpErrors.ASSOCIATION_FAILED); - } - else { + } else { if (tab && page.tabs[tab.id]) { delete page.tabs[tab.id].errorMessage; } } - } - else if (response.error && response.errorCode) { + } else if (response.error && response.errorCode) { keepass.handleError(tab, response.errorCode, response.error); } callback(keepass.isAssociated()); @@ -539,9 +526,9 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false, trigger const encrypted = keepass.encrypt(messageData, nonce); if (encrypted.length <= 0) { - keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); - callback(keepass.databaseHash); - return; + keepass.handleError(tab, kpErrors.PUBLIC_KEY_NOT_FOUND); + callback(keepass.databaseHash); + return; } const request = { @@ -580,23 +567,20 @@ keepass.getDatabaseHash = function(callback, tab, enableTimeout = false, trigger keepass.isKeePassXCAvailable = true; callback(parsed.hash); return; - } - else if (parsed.errorCode) { + } else if (parsed.errorCode) { keepass.databaseHash = ''; keepass.isDatabaseClosed = true; keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); callback(keepass.databaseHash); return; } - } - else { + } else { keepass.databaseHash = ''; keepass.isDatabaseClosed = true; if (response.message && response.message === '') { keepass.isKeePassXCAvailable = false; keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); - } - else { + } else { keepass.handleError(tab, response.errorCode, response.error); } callback(keepass.databaseHash); @@ -633,8 +617,7 @@ keepass.changePublicKeys = function(tab, enableTimeout = false) { keepass.handleError(tab, kpErrors.KEY_CHANGE_FAILED); reject(false); } - } - else { + } else { keepass.isKeePassXCAvailable = true; console.log('Server public key: ' + nacl.util.encodeBase64(keepass.serverPublicKey)); } @@ -686,8 +669,7 @@ keepass.lockDatabase = function(tab) { keepass.handleError(tab, kpErrors.DATABASE_NOT_OPENED); resolve(true); } - } - else if (response.error && response.errorCode) { + } else if (response.error && response.errorCode) { keepass.isDatabaseClosed = true; keepass.handleError(tab, response.errorCode, response.error); } @@ -759,25 +741,24 @@ keepass.saveKey = function(hash, id, key) { created: new Date().valueOf(), lastUsed: new Date().valueOf() }; - } - else { + } else { keepass.keyRing[hash].id = id; keepass.keyRing[hash].key = key; keepass.keyRing[hash].hash = hash; } - browser.storage.local.set({'keyRing': keepass.keyRing}); + browser.storage.local.set({ 'keyRing': keepass.keyRing }); }; keepass.updateLastUsed = function(hash) { if ((hash in keepass.keyRing)) { keepass.keyRing[hash].lastUsed = new Date().valueOf(); - browser.storage.local.set({'keyRing': keepass.keyRing}); + browser.storage.local.set({ 'keyRing': keepass.keyRing }); } }; keepass.deleteKey = function(hash) { delete keepass.keyRing[hash]; - browser.storage.local.set({'keyRing': keepass.keyRing}); + browser.storage.local.set({ 'keyRing': keepass.keyRing }); }; keepass.setcurrentKeePassXCVersion = function(version) { @@ -789,7 +770,7 @@ keepass.setcurrentKeePassXCVersion = function(version) { keepass.keePassXCUpdateAvailable = function() { if (page.settings.checkUpdateKeePassXC && page.settings.checkUpdateKeePassXC > 0) { const lastChecked = (keepass.latestKeePassXC.lastChecked) ? new Date(keepass.latestKeePassXC.lastChecked) : new Date(1986, 11, 21); - const daysSinceLastCheck = Math.floor(((new Date()).getTime()-lastChecked.getTime())/86400000); + const daysSinceLastCheck = Math.floor(((new Date()).getTime() - lastChecked.getTime()) / 86400000); if (daysSinceLastCheck >= page.settings.checkUpdateKeePassXC) { keepass.checkForNewKeePassXCVersion(); } @@ -799,7 +780,7 @@ keepass.keePassXCUpdateAvailable = function() { }; keepass.checkForNewKeePassXCVersion = function() { - let xhr = new XMLHttpRequest(); + const xhr = new XMLHttpRequest(); let version = -1; xhr.onload = function(e) { @@ -973,7 +954,7 @@ keepass.getCryptoKey = function() { let dbkey = null; let dbid = null; if (!(keepass.databaseHash in keepass.keyRing)) { - return {dbid, dbkey}; + return { dbid, dbkey }; } dbid = keepass.keyRing[keepass.databaseHash].id; @@ -982,7 +963,7 @@ keepass.getCryptoKey = function() { dbkey = keepass.keyRing[keepass.databaseHash].key; } - return {dbid, dbkey}; + return { dbid, dbkey }; }; keepass.setCryptoKey = function(id, key) { @@ -1046,7 +1027,7 @@ keepass.updatePopup = function(iconType) { 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}); + browserAction.show(null, { 'id': page.currentTabId }); } }; diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index 87da6b3..95c8884 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -20,7 +20,7 @@ page.loginId = -1; page.initSettings = function() { return new Promise((resolve, reject) => { - browser.storage.local.get({'settings': {}}).then((item) => { + browser.storage.local.get({ 'settings': {} }).then((item) => { page.settings = item.settings; if (!('checkUpdateKeePassXC' in page.settings)) { page.settings.checkUpdateKeePassXC = defaultSettings.checkUpdateKeePassXC; @@ -62,14 +62,14 @@ page.initOpenedTabs = function() { page.createTabEntry(i.id); } - // set initial tab-ID - browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => { - if (tabs.length === 0) { + // Set initial tab-ID + browser.tabs.query({ 'active': true, 'currentWindow': true }).then((t) => { + if (t.length === 0) { resolve(); return; // For example: only the background devtools or a popup are opened } - page.currentTabId = tabs[0].id; - browserAction.show(null, tabs[0]); + page.currentTabId = t[0].id; + browserAction.show(null, t[0]); resolve(); }); }); @@ -84,7 +84,7 @@ page.isValidProtocol = function(url) { page.switchTab = function(callback, tab) { browserAction.showDefault(null, tab); - browser.tabs.sendMessage(tab.id, {action: 'activated_tab'}).catch((e) => {}); + browser.tabs.sendMessage(tab.id, { action: 'activated_tab' }).catch((e) => {}); }; page.clearCredentials = function(tabId, complete) { @@ -121,18 +121,18 @@ page.createTabEntry = function(tabId) { }; page.removePageInformationFromNotExistingTabs = function() { - let rand = Math.floor(Math.random()*1001); + const rand = Math.floor(Math.random() * 1001); if (rand === 28) { browser.tabs.query({}).then(function(tabs) { - let $tabIds = []; - const $infoIds = Object.keys(page.tabs); + const tabIds = []; + const infoIds = Object.keys(page.tabs); for (const t of tabs) { - $tabIds[t.id] = true; + tabIds[t.id] = true; } - for (const i of $infoIds) { - if (!(i in $tabIds)) { + for (const i of infoIds) { + if (!(i in tabIds)) { delete page.tabs[i]; } } @@ -143,8 +143,7 @@ page.removePageInformationFromNotExistingTabs = function() { page.debugConsole = function() { if (arguments.length > 1) { console.log(page.sprintf(arguments[0], arguments)); - } - else { + } else { console.log(arguments[0]); } }; @@ -163,8 +162,7 @@ page.setDebug = function(bool) { if (bool) { page.debug = page.debugConsole; return 'Debug mode enabled'; - } - else { + } else { page.debug = page.debugDummy; return 'Debug mode disabled'; } diff --git a/keepassxc-browser/keepassxc-browser.js b/keepassxc-browser/keepassxc-browser.js index d9dff0c..93160dd 100755 --- a/keepassxc-browser/keepassxc-browser.js +++ b/keepassxc-browser/keepassxc-browser.js @@ -1,2170 +1,2148 @@ -'use strict'; - -// contains already called method names -var _called = {}; -_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; - -// Element id's containing input fields detected by MutationObserver -var _observerIds = []; - -// Document URL -let _documentURL = document.location.href; - -function _f(fieldId) { - const field = (fieldId) ? jQuery('input[data-cip-id=\''+fieldId+'\']:first') : []; - return (field.length > 0) ? field : null; -} - -function _fs(fieldId) { - const field = (fieldId) ? jQuery('input[data-cip-id=\''+fieldId+'\']:first,select[data-cip-id=\''+fieldId+'\']:first').first() : []; - return (field.length > 0) ? field : null; -} - -browser.runtime.onMessage.addListener(function(req, sender, callback) { - if ('action' in req) { - if (req.action === 'fill_user_pass_with_specific_login') { - if (cip.credentials[req.id]) { - let combination = null; - if (cip.u) { - cip.setValueWithChange(cip.u, cip.credentials[req.id].login); - combination = cipFields.getCombination('username', cip.u); - 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); - browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [req.id] - }); - combination = cipFields.getCombination('password', cip.p); - } - - let list = []; - if (cip.fillInStringFields(combination.fields, cip.credentials[req.id].stringFields, list)) { - cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); - } - } - } else if (req.action === 'fill_user_pass') { - _called.manualFillRequested = 'both'; - cip.receiveCredentialsIfNecessary().then((response) => { - cip.fillInFromActiveElement(false); - }); - } else if (req.action === 'fill_pass_only') { - _called.manualFillRequested = 'pass'; - cip.receiveCredentialsIfNecessary().then((response) => { - cip.fillInFromActiveElement(false, true); // passOnly to true - }); - } else if (req.action === 'fill_totp') { - cip.receiveCredentialsIfNecessary().then((response) => { - cip.fillInFromActiveElementTOTPOnly(false); - }); - } else if (req.action === 'activate_password_generator') { - cip.initPasswordGenerator(cipFields.getAllFields()); - } else if (req.action === 'remember_credentials') { - cip.contextMenuRememberCredentials(); - } else if (req.action === 'choose_credential_fields') { - cipDefine.init(); - } else if (req.action === 'clear_credentials') { - cipEvents.clearCredentials(); - return Promise.resolve(); - } else if (req.action === 'activated_tab') { - cipEvents.triggerActivatedTab(); - return Promise.resolve(); - } else if (req.action === 'redetect_fields') { - browser.runtime.sendMessage({ - action: 'load_settings', - }).then((response) => { - cip.settings = response; - cip.initCredentialFields(true); - }); - } else if (req.action === 'ignore-site') { - cip.ignoreSite(req.args); - } - else if (req.action === 'check_database_hash' && 'hash' in req) { - cip.detectDatabaseChange(req.hash); - } - } -}); - - -var cipAutocomplete = {}; - -// objects of username + description for autocomplete -cipAutocomplete.elements = []; - -cipAutocomplete.init = function(field) { - if (cip.settings.autoFillSingleEntry && cip.credentials.length === 1 && field.hasClass('ui-autocomplete-input')) { - field.autocomplete('destroy'); - } - - let acMenu = jQuery('#kpxc-ac-menu'); - if (acMenu.length == 0) { - jQuery('
').html(tr('defineAlreadySelected') + ''); - const $btnDiscard = jQuery('') - .attr('id', 'btn-warning') - .text(tr('defineDiscard')) - .css('margin-top', '5px') - .addClass('btn') - .addClass('btn-sm') - .addClass('btn-danger') - .click(function(e) { - delete cip.settings['defined-custom-fields'][location]; - - browser.runtime.sendMessage({ - action: 'save_settings', - args: [cip.settings] - }); - - browser.runtime.sendMessage({ - action: 'load_settings' - }); - - jQuery(this).parent('p').remove(); - }); - $p.append($btnDiscard); - $description.append($p); - } - - jQuery('div#b2c-cipDefine-description').draggable(); -}; - -cipDefine.resetSelection = function() { - cipDefine.selection = { - username: null, - password: null, - fields: [] - }; -}; - -cipDefine.isFieldSelected = function($cipId) { - return ( - $cipId === cipDefine.selection.username || - $cipId === cipDefine.selection.password || - $cipId in cipDefine.selection.fields - ); -}; - -cipDefine.markAllUsernameFields = function($chooser) { - cipDefine.eventFieldClick = function(e) { - cipDefine.selection.username = jQuery(this).data('cip-id'); - jQuery(this).addClass('b2c-fixed-username-field').text(tr('username')).unbind('click'); - cipDefine.prepareStep2(); - cipDefine.markAllPasswordFields(jQuery('#b2c-cipDefine-fields')); - }; - cipDefine.markFields($chooser, cipFields.inputQueryPattern); -}; - -cipDefine.markAllPasswordFields = function($chooser, more) { - cipDefine.eventFieldClick = function(e) { - cipDefine.selection.password = jQuery(this).data('cip-id'); - jQuery(this).addClass('b2c-fixed-password-field').text(tr('password')).unbind('click'); - cipDefine.prepareStep3(); - cipDefine.markAllStringFields(jQuery('#b2c-cipDefine-fields')); - }; - if (more) { - cipDefine.markFields($chooser, cipFields.inputQueryPattern); - } else { - cipDefine.markFields($chooser, 'input[type=\'password\']'); - } -}; - -cipDefine.markAllStringFields = function($chooser) { - cipDefine.eventFieldClick = function(e) { - cipDefine.selection.fields[jQuery(this).data('cip-id')] = true; - const count = Object.keys(cipDefine.selection.fields).length; - jQuery(this).addClass('b2c-fixed-string-field').text(tr('defineStringField') + String(count)).unbind('click'); - jQuery('button#b2c-btn-confirm:first').addClass('b2c-btn-primary').attr('disabled', false); - }; - cipDefine.markFields($chooser, cipFields.inputQueryPattern + ', select'); -}; - -cipDefine.markFields = function ($chooser, $pattern) { - jQuery($pattern).each(function() { - if (cipDefine.isFieldSelected(jQuery(this).data('cip-id'))) { - return true; - } - - if (cipFields.isVisible(this)) { - const $field = jQuery('').addClass('b2c-fixed-field') - .css('top', jQuery(this).offset().top) - .css('left', jQuery(this).offset().left) - .css('width', jQuery(this).outerWidth()) - .css('height', jQuery(this).outerHeight()) - .attr('data-cip-id', jQuery(this).attr('data-cip-id')) - .click(cipDefine.eventFieldClick) - .hover(function() {jQuery(this).addClass('b2c-fixed-hover-field');}, function() {jQuery(this).removeClass('b2c-fixed-hover-field');}); - $chooser.append($field); - } - }); -}; - -cipDefine.prepareStep1 = function() { - jQuery('div#b2c-help').text('').css('margin-bottom', 0); - jQuery('div#b2c-cipDefine-fields').removeData('username'); - jQuery('div#b2c-cipDefine-fields').removeData('password'); - jQuery('div.b2c-fixed-field', jQuery('div#b2c-cipDefine-fields')).remove(); - jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineChooseUsername')); - jQuery('button#b2c-btn-skip:first').data('step', '1').show(); - jQuery('button#b2c-btn-confirm:first').hide(); - jQuery('button#b2c-btn-again:first').hide(); - jQuery('button#b2c-btn-more:first').hide(); -}; - -cipDefine.prepareStep2 = function() { - jQuery('div#b2c-help').text('').css('margin-bottom', 0); - jQuery('div.b2c-fixed-field:not(.b2c-fixed-username-field)', jQuery('div#b2c-cipDefine-fields')).remove(); - jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineChoosePassword')); - jQuery('button#b2c-btn-skip:first').data('step', '2'); - jQuery('button#b2c-btn-again:first').show(); - jQuery('button#b2c-btn-more:first').show(); -}; - -cipDefine.prepareStep3 = function() { - if (!cipDefine.selection.username && !cipDefine.selection.password) { - jQuery('button#b2c-btn-confirm:first').removeClass('b2c-btn-primary').attr('disabled', true); - } - - jQuery('div#b2c-help').html(tr('defineHelpText')).css('margin-bottom', '5px'); - jQuery('div.b2c-fixed-field:not(.b2c-fixed-password-field,.b2c-fixed-username-field)', jQuery('div#b2c-cipDefine-fields')).remove(); - jQuery('button#b2c-btn-confirm:first').show(); - jQuery('button#b2c-btn-skip:first').data('step', '3').hide(); - jQuery('button#b2c-btn-more:first').hide(); - jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineConfirmSelection')); -}; - - - -var cipFields = {}; - -cipFields.inputQueryPattern = 'input[type=\'text\'], input[type=\'email\'], input[type=\'password\'], input[type=\'tel\'], input[type=\'number\'], input:not([type])'; -// unique number as new IDs for input fields -cipFields.uniqueNumber = 342845638; -// objects with combination of username + password fields -cipFields.combinations = []; - -cipFields.setUniqueId = function(field) { - if (field && !field.attr('data-cip-id')) { - // use ID of field if it is unique - // yes, it should be, but there are many bad developers outside... - const fieldId = field.attr('id'); - if (fieldId) { - const foundIds = jQuery('input#' + cipFields.prepareId(fieldId)); - if (foundIds.length === 1) { - field.attr('data-cip-id', fieldId); - return; - } - } - - // create own ID if no ID is set for this field - cipFields.uniqueNumber += 1; - field.attr('data-cip-id', 'jQuery'+String(cipFields.uniqueNumber)); - } -}; - -cipFields.prepareId = function(id) { - return id.replace(/[:#.,\[\]\(\)' "]/g, function(m) { return '\\'+m; }); -}; - -/** - * Returns the first parent element satifying the {@code predicate} mapped by {@code resultFn} or else {@code defaultVal}. - * @param {HTMLElement} element The start element (excluded, starting with the parents) - * @param {function} predicate Matcher for the element to find, type (HTMLElement) => boolean - * @param {function} resultFn Callback function of type (HTMLElement) => {*} called for the first matching element - * @param {fun} defaultValFn Fallback return value supplier, if no element matching the predicate can be found - */ -cipFields.traverseParents = function(element, predicate, resultFn = () => true, defaultValFn = () => false) { - for (let f = element.parentElement; f !== null; f = f.parentElement) { - if (predicate(f)) { - return resultFn(f); - } - } - return defaultValFn(); -}; - -cipFields.getOverflowHidden = function(field) { - return cipFields.traverseParents(field, f => f.style.overflow === 'hidden'); -}; - - -// Checks if input field is a search field. Attributes or form action containing 'search', or parent element holding -// role="search" will be identified as a search field. -cipFields.isSearchField = function(target) { - const attributes = target.attributes; - - // Check element attributes - for (const attr of attributes) { - if ((attr.value && (attr.value.toLowerCase().includes('search')) || attr.value === 'q')) { - return true; - } - } - - // Check closest form - const closestForm = target.closest('form'); - if (closestForm) { - // Check form action - const formAction = closestForm.getAttribute('action'); - if (formAction && (formAction.toLowerCase().includes('search') && - !formAction.toLowerCase().includes('research'))) { - return true; - } - - // Check form class and id - const closestFormId = closestForm.getAttribute('id'); - const closestFormClass = closestForm.className; - if (closestFormClass && (closestForm.className.toLowerCase().includes('search') || - (closestFormId && closestFormId.toLowerCase().includes('search') && !closestFormId.toLowerCase().includes('research')))) { - return true; - } - } - - // Check parent elements for role="search" - const roleFunc = f => f.getAttribute('role'); - const roleValue = cipFields.traverseParents(target, roleFunc, roleFunc, () => null); - if (roleValue && roleValue === 'search') { - return true; - } - - return false; -}; - -cipFields.isVisible = function(field) { - const rect = field.getBoundingClientRect(); - - // Check CSS visibility - const fieldStyle = getComputedStyle(field); - if (fieldStyle.visibility && (fieldStyle.visibility === 'hidden' || fieldStyle.visibility === 'collapse')) { - return false; - } - - // Check element position and size - if (rect.x < 0 || rect.y < 0 || rect.width < 8 || rect.height < 8) { - return false; - } - - return true; -}; - -cipFields.getAllFields = function() { - let fields = []; - const inputs = cipObserverHelper.getInputs(document); - for (const i of inputs) { - if (cipFields.isVisible(i) && !cipFields.isSearchField(i)) { - cipFields.setUniqueId(jQuery(i)); - fields.push(jQuery(i)); - } - }; - - _detectedFields = fields.length; - return fields; -}; - -cipFields.prepareVisibleFieldsWithID = function($pattern) { - jQuery($pattern).each(function() { - if (cipFields.isVisible(this) && !cipFields.isSearchField(this)) { - cipFields.setUniqueId(jQuery(this)); - } - }); -}; - -cipFields.getAllCombinations = function(inputs) { - let fields = []; - let uField = null; - - for (const i of inputs) { - if (i) { - if (i.attr('type') && i.attr('type').toLowerCase() === 'password') { - const uId = (!uField || uField.length < 1) ? null : cipFields.prepareId(uField.attr('data-cip-id')); - - const combination = { - username: uId, - password: cipFields.prepareId(i.attr('data-cip-id')) - }; - fields.push(combination); - - // reset selected username field - uField = null; - } - else { - // username field - uField = i; - } - } - } - - if (_singleInputEnabledForPage && fields.length === 0 && uField) { - const combination = { - username: uField[0].getAttribute('data-cip-id'), - password: null - }; - fields.push(combination); - } - - return fields; -}; - -cipFields.getCombination = function(givenType, fieldId) { - if (cipFields.combinations.length === 0) { - if (cipFields.useDefinedCredentialFields()) { - return cipFields.combinations[0]; - } - } - // use defined credential fields (already loaded into combinations) - const location = cip.getDocumentLocation(); - if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { - return cipFields.combinations[0]; - } - - for (let c of cipFields.combinations) { - if (c[givenType] === fieldId) { - return c; - } - } - - // find new combination - let combination = { - username: null, - password: null - }; - - let newCombi = false; - if (givenType === 'username') { - const passwordField = cipFields.getPasswordField(fieldId, true); - let passwordId = null; - if (passwordField && passwordField.length > 0) { - passwordId = cipFields.prepareId(passwordField.attr('data-cip-id')); - } - combination = { - username: fieldId, - password: passwordId - }; - newCombi = true; - } - else if (givenType === 'password') { - const usernameField = cipFields.getUsernameField(fieldId, true); - let usernameId = null; - if (usernameField && usernameField.length > 0) { - usernameId = cipFields.prepareId(usernameField.attr('data-cip-id')); - } - combination = { - username: usernameId, - password: fieldId - }; - newCombi = true; - } - - if (combination.username || combination.password) { - cipFields.combinations.push(combination); - } - - if (combination.username) { - if (cip.credentials.length > 0) { - cip.preparePageForMultipleCredentials(cip.credentials); - } - } - - if (newCombi) { - combination.isNew = true; - } - return combination; -}; - -/** -* return the username field or null if it not exists -*/ -cipFields.getUsernameField = function(passwordId, checkDisabled) { - const passwordField = _f(passwordId); - if (!passwordField) { - return null; - } - - const form = passwordField.closest('form')[0]; - let usernameField = null; - - // search all inputs on this one form - if (form) { - jQuery(cipFields.inputQueryPattern, form).each(function() { - cipFields.setUniqueId(jQuery(this)); - if (jQuery(this).attr('data-cip-id') === passwordId) { - // break - return false; - } - - if (jQuery(this).attr('type') && jQuery(this).attr('type').toLowerCase() === 'password') { - // continue - return true; - } - - usernameField = jQuery(this); - }); - } - // search all inputs on page - else { - const inputs = cipFields.getAllFields(); - cip.initPasswordGenerator(inputs); - for (const i of inputs) { - if (i.attr('data-cip-id') === passwordId) { - break; - } - - if (i.attr('type') && i.attr('type').toLowerCase() === 'password') { - continue; - } - - usernameField = i; - } - } - - if (usernameField && !checkDisabled) { - const usernameId = usernameField.attr('data-cip-id'); - // check if usernameField is already used by another combination - for (const c of cipFields.combinations) { - if (c.username === usernameId) { - usernameField = null; - break; - } - } - } - - cipFields.setUniqueId(usernameField); - return usernameField; -}; - -/** -* return the password field or null if it not exists -*/ -cipFields.getPasswordField = function(usernameId, checkDisabled) { - const usernameField = _f(usernameId); - if (!usernameField) { - return null; - } - - const form = usernameField.closest('form')[0]; - let passwordField = null; - - // search all inputs on this one form - if (form) { - passwordField = jQuery('input[type=\'password\']:first', form); - if (passwordField && passwordField.length < 1) { - passwordField = null; - } - - if (cip.settings.usePasswordGenerator) { - cipPassword.init(); - cipPassword.initField(passwordField); - } - } - // search all inputs on page - else { - const inputs = cipFields.getAllFields(); - cip.initPasswordGenerator(inputs); - - let active = false; - for (const i of inputs) { - if (i.attr('data-cip-id') === usernameId) { - active = true; - } - if (active && jQuery(i).attr('type') && jQuery(i).attr('type').toLowerCase() === 'password') { - passwordField = i; - break; - } - } - } - - if (passwordField && !checkDisabled) { - const passwordId = passwordField.attr('data-cip-id'); - // check if passwordField is already used by another combination - for (const c of cipFields.combinations) { - if (c.password === passwordId) { - passwordField = null; - break; - } - } - } - - cipFields.setUniqueId(passwordField); - - return passwordField; -}; - -cipFields.prepareCombinations = function(combinations) { - for (const c of combinations) { - const pwField = _f(c.password); - // needed for auto-complete: don't overwrite manually filled-in password field - if (pwField && !pwField.data('cipFields-onChange')) { - pwField.data('cipFields-onChange', true); - pwField.change(function() { - jQuery(this).data('unchanged', false); - }); - } - - // initialize form-submit for remembering credentials - const fieldId = c.password || c.username; - const field = _f(fieldId); - if (field) { - const form = field.closest('form'); - if (form && form.length > 0) { - cipForm.init(form, c); - } - } - } -}; - -cipFields.useDefinedCredentialFields = function() { - const location = cip.getDocumentLocation(); - if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { - const creds = cip.settings['defined-custom-fields'][location]; - - let $found = _f(creds.username) || _f(creds.password); - for (const i of creds.fields) { - if (_fs(i)) { - $found = true; - break; - } - } - - if ($found) { - let fields = { - username: creds.username, - password: creds.password, - fields: creds.fields - }; - cipFields.combinations = []; - cipFields.combinations.push(fields); - - return true; - } - } - - return false; -}; - - -var cipObserverHelper = {}; -cipObserverHelper.inputTypes = [ - 'text', - 'email', - 'password', - 'tel', - 'number', - null // Input field can be without any type. Include these to the list. -]; - -// Ignores all nodes that doesn't contain elements -cipObserverHelper.ignoredNode = function(target) { - if (target.nodeType === Node.ATTRIBUTE_NODE || - target.nodeType === Node.TEXT_NODE || - target.nodeType === Node.CDATA_SECTION_NODE || - target.nodeType === Node.PROCESSING_INSTRUCTION_NODE || - target.nodeType === Node.COMMENT_NODE || - target.nodeType === Node.DOCUMENT_TYPE_NODE || - target.nodeType === Node.NOTATION_NODE) { - return true; - } - return false; -}; - -cipObserverHelper.getInputs = function(target) { - // Ignores target element if it's not an element node - if (cipObserverHelper.ignoredNode(target)) { - return []; - } - - // Filter out any input fields with type 'hidden' right away - let inputFields = []; - Array.from(target.getElementsByTagName('input')).forEach(e => { - if (e.type !== 'hidden') { - inputFields.push(e); - } - }); - - // Do not allow more visible inputs than _maximumInputs (default value: 100) - if (inputFields.length === 0 || inputFields.length > _maximumInputs) { - return []; - } - - // Only include input fields that match with cipObserverHelper.inputTypes - let inputs = []; - for (const i of inputFields) { - let type = i.getAttribute('type'); - if (type) { - type = type.toLowerCase(); - } - - if (cipObserverHelper.inputTypes.includes(type)) { - inputs.push(i); - } - } - return inputs; -}; - -cipObserverHelper.getId = function(target) { - return target.classList.length === 0 ? target.id : target.classList; -}; - -cipObserverHelper.ignoredElement = function(target) { - // Ignore elements that do not have a className (including SVG) - if (typeof target.className !== 'string') { - return true; - } - - // Ignore KeePassXC-Browser classes - if (target.className && target.className !== undefined && - (target.className.includes('kpxc') || target.className.includes('ui-helper'))) { - return true; - } - - return false; -}; - -cipObserverHelper.handleObserverAdd = function(target) { - if (cipObserverHelper.ignoredElement(target)) { - return; - } - - const inputs = cipObserverHelper.getInputs(target); - if (inputs.length === 0) { - return; - } - - const neededLength = _detectedFields === 1 ? 0 : 1; - const id = cipObserverHelper.getId(target); - if (inputs.length > neededLength && !_observerIds.includes(id)) { - // Save target element id for preventing multiple calls to initCredentialsFields() - _observerIds.push(id); - - // Sometimes the settings haven't been loaded before new input fields are detected - if (Object.keys(cip.settings).length === 0) { - cip.init(); - } else { - cip.initCredentialFields(true); - } - } -}; - -cipObserverHelper.handleObserverRemove = function(target) { - if (cipObserverHelper.ignoredElement(target)) { - return; - } - - const inputs = cipObserverHelper.getInputs(target); - if (inputs.length === 0) { - return; - } - - // Remove target element id from the list - const id = cipObserverHelper.getId(target); - if (_observerIds.includes(id)) { - const index = _observerIds.indexOf(id); - if (index >= 0) { - _observerIds.splice(index, 1); - } - } -}; - -cipObserverHelper.detectURLChange = function() { - if (_documentURL !== document.location.href) { - _documentURL = document.location.href; - cipEvents.clearCredentials(); - cip.initCredentialFields(true); - } -}; - -MutationObserver = window.MutationObserver || window.WebKitMutationObserver; - -// Detects DOM changes in the document -let observer = new MutationObserver(function(mutations, observer) { - if (document.visibilityState === 'hidden') { - return; - } - - for (const mut of mutations) { - // Skip text nodes - if (mut.target.nodeType === Node.TEXT_NODE) { - continue; - } - - // Check document URL change and detect new fields - cipObserverHelper.detectURLChange(); - - // Handle attributes only if CSS display is modified - if (mut.type === 'attributes') { - const newValue = mut.target.getAttribute(mut.attributeName); - if (newValue && (newValue.includes('display') || newValue.includes('z-index'))) { - if (mut.target.style.display !== 'none') { - cipObserverHelper.handleObserverAdd(mut.target); - } else { - cipObserverHelper.handleObserverRemove(mut.target); - } - } - } else if (mut.type === 'childList') { - cipObserverHelper.handleObserverAdd((mut.addedNodes.length > 0) ? mut.addedNodes[0] : mut.target); - cipObserverHelper.handleObserverRemove((mut.removedNodes.length > 0) ? mut.removedNodes[0] : mut.target); - } - } -}); - -// define what element should be observed by the observer -// and what types of mutations trigger the callback -observer.observe(document, { - subtree: true, - attributes: true, - childList: true, - characterData: true, - attributeFilter: ['style'] -}); - - -var cip = {}; -cip.settings = {}; -cip.u = null; -cip.p = null; -cip.url = null; -cip.submitUrl = null; -cip.credentials = []; - -jQuery(function() { - cip.init(); -}); - -cip.init = function() { - browser.runtime.sendMessage({ - action: 'load_settings', - }).then((response) => { - cip.settings = response; - cip.initCredentialFields(); - }); -}; - -// Switch credentials if database is changed or closed -cip.detectDatabaseChange = function(response) { - if (document.visibilityState !== 'hidden') { - if (response.new === '' && response.old !== '') { - cipEvents.clearCredentials(); - - browser.runtime.sendMessage({ - action: 'page_clear_logins' - }); - - // Switch back to default popup - browser.runtime.sendMessage({ - action: 'get_status', - args: [ true ] // Set polling to true, this is an internal function call - }); - } else if (response.new !== '' && response.new !== response.old) { - _called.retrieveCredentials = false; - browser.runtime.sendMessage({ - action: 'load_settings', - }).then((response) => { - cip.settings = response; - cip.initCredentialFields(true); - - // If user has requested a manual fill through context menu the actual credential filling - // is handled here when the opened database has been regognized. It's not a pretty hack. - if (_called.manualFillRequested && _called.manualFillRequested !== 'none') { - cip.fillInFromActiveElement(false, _called.manualFillRequested === 'pass'); - _called.manualFillRequested = 'none'; - } - }); - } - } -}; - -cip.initCredentialFields = function(forceCall) { - if (_called.initCredentialFields && !forceCall) { - return; - } - _called.initCredentialFields = true; - - browser.runtime.sendMessage({ 'action': 'page_clear_logins', args: [_called.clearLogins] }).then(() => { - _called.clearLogins = true; - - // Check site preferences - cip.initializeSitePreferences(); - if (cip.settings.sitePreferences) { - for (const site of cip.settings.sitePreferences) { - if (site.url === document.location.href || siteMatch(site.url, document.location.href)) { - if (site.ignore === IGNORE_FULL) { - return; - } - - _singleInputEnabledForPage = site.usernameOnly; - } - } - } - - const inputs = cipFields.getAllFields(); - if (inputs.length === 0) { - return; - } - - cipFields.prepareVisibleFieldsWithID('select'); - cip.initPasswordGenerator(inputs); - - if (!cipFields.useDefinedCredentialFields()) { - // get all combinations of username + password fields - cipFields.combinations = cipFields.getAllCombinations(inputs); - } - cipFields.prepareCombinations(cipFields.combinations); - - if (cipFields.combinations.length === 0 && inputs.length === 0) { - browser.runtime.sendMessage({ - action: 'show_default_browseraction' - }); - return; - } - - cip.url = document.location.origin; - cip.submitUrl = cip.getFormActionUrl(cipFields.combinations[0]); - - // Get submitUrl for a single input - if (_singleInputEnabledForPage && !cip.submitUrl && cipFields.combinations.length === 1 && inputs.length === 1) { - cip.submitUrl = cip.getFormActionUrlFromSingleInput(inputs[0]); - } - - if (cip.settings.autoRetrieveCredentials && _called.retrieveCredentials === false && (cip.url && cip.submitUrl)) { - browser.runtime.sendMessage({ - action: 'retrieve_credentials', - args: [ cip.url, cip.submitUrl ] - }).then(cip.retrieveCredentialsCallback).catch((e) => { - console.log(e); - }); - } else if (_singleInputEnabledForPage) { - cip.preparePageForMultipleCredentials(cip.credentials); - } - }); -}; - -cip.initPasswordGenerator = function(inputs) { - if (cip.settings.usePasswordGenerator) { - cipPassword.init(); - - for (let i = 0; i < inputs.length; i++) { - if (inputs[i] && inputs[i].attr('type') && inputs[i].attr('type').toLowerCase() === 'password') { - cipPassword.initField(inputs[i], inputs, i); - } - } - } -}; - -cip.receiveCredentialsIfNecessary = function() { - return new Promise((resolve, reject) => { - if (cip.credentials.length === 0 && _called.retrieveCredentials === false) { - browser.runtime.sendMessage({ - action: 'retrieve_credentials', - args: [ cip.url, cip.submitUrl, false, true ] // Sets triggerUnlock to true - }).then((credentials) => { - // If the database was locked, this is scope never met. In these cases the response is met at cip.detectDatabaseChange - _called.manualFillRequested = 'none'; - cip.retrieveCredentialsCallback(credentials, false); - resolve(credentials); - }); - } else { - resolve(cip.credentials); - } - }); -}; - -cip.retrieveCredentialsCallback = function(credentials, dontAutoFillIn) { - if (cipFields.combinations.length > 0) { - cip.u = _f(cipFields.combinations[0].username); - cip.p = _f(cipFields.combinations[0].password); - } - - if (credentials && credentials.length > 0) { - cip.credentials = credentials; - cip.prepareFieldsForCredentials(!Boolean(dontAutoFillIn)); - _called.retrieveCredentials = true; - } -}; - -cip.prepareFieldsForCredentials = function(autoFillInForSingle) { - // only one login for this site - if (autoFillInForSingle && cip.settings.autoFillSingleEntry && cip.credentials.length === 1) { - let combination = null; - if (!cip.p && !cip.u && cipFields.combinations.length > 0) { - cip.u = _f(cipFields.combinations[0].username); - cip.p = _f(cipFields.combinations[0].password); - combination = cipFields.combinations[0]; - } - if (cip.u) { - cip.setValueWithChange(cip.u, cip.credentials[0].login); - combination = cipFields.getCombination('username', cip.u); - } - if (cip.p) { - cip.setValueWithChange(cip.p, cip.credentials[0].password); - combination = cipFields.getCombination('password', cip.p); - } - - if (combination) { - let list = []; - if (cip.fillInStringFields(combination.fields, cip.credentials[0].stringFields, list)) { - cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); - } - } - - // generate popup-list of usernames + descriptions - browser.runtime.sendMessage({ - action: 'popup_login', - args: [[cip.credentials[0].login + ' (' + cip.credentials[0].name + ')']] - }); - } - //multiple logins for this site - else if (cip.credentials.length > 1 || (cip.credentials.length > 0 && (!cip.settings.autoFillSingleEntry || !autoFillInForSingle))) { - cip.preparePageForMultipleCredentials(cip.credentials); - } -}; - -cip.preparePageForMultipleCredentials = function(credentials) { - // add usernames + descriptions to autocomplete-list and popup-list - let usernames = []; - cipAutocomplete.elements = []; - let visibleLogin; - for (let i = 0; i < credentials.length; i++) { - visibleLogin = (credentials[i].login.length > 0) ? credentials[i].login : tr('credentialsNoUsername'); - usernames.push(visibleLogin + ' (' + credentials[i].name + ')'); - const item = { - label: visibleLogin + ' (' + credentials[i].name + ')', - value: credentials[i].login, - loginId: i - }; - cipAutocomplete.elements.push(item); - } - - // generate popup-list of usernames + descriptions - browser.runtime.sendMessage({ - action: 'popup_login', - args: [usernames] - }); - - // initialize autocomplete for username fields - if (cip.settings.autoCompleteUsernames) { - for (const i of cipFields.combinations) { - // Both username and password fields are visible - if (_detectedFields >= 2) { - if (_f(i.username)) { - cipAutocomplete.init(_f(i.username)); - } - } else if (_detectedFields == 1) { - if (_f(i.username)) { - cipAutocomplete.init(_f(i.username)); - } - if (_f(i.password)) { - cipAutocomplete.init(_f(i.password)); - } - } - } - } -}; - -cip.getFormActionUrl = function(combination) { - if (!combination) { - return null; - } - - const field = _f(combination.password) || _f(combination.username); - - if (field === null) { - return null; - } - - const form = field.closest('form'); - let action = null; - - if (form && form.length > 0) { - action = form[0].action; - } - - if (typeof(action) !== 'string' || action === '') { - action = document.location.origin + document.location.pathname; - } - - return action; -}; - -cip.getFormActionUrlFromSingleInput = function(field) { - if (!field) { - return null; - } - - let action = field.formAction; - - if (typeof(action) !== 'string' || action === '') { - action = document.location.origin + document.location.pathname; - } - - return action; -}; - -cip.fillInCredentials = function(combination, onlyPassword, suppressWarnings) { - const action = cip.getFormActionUrl(combination); - - const u = _f(combination.username); - const p = _f(combination.password); - - if (combination.isNew) { - // initialize form-submit for remembering credentials - const fieldId = combination.password || combination.username; - const field = _f(fieldId); - if (field) { - const form2 = field.closest('form'); - if (form2 && form2.length > 0) { - cipForm.init(form2, combination); - } - } - } - - if (u) { - cip.u = u; - } - if (p) { - cip.p = p; - } - - if (cip.url === document.location.origin && cip.submitUrl === action && cip.credentials.length > 0) { - cip.fillIn(combination, onlyPassword, suppressWarnings); - } - else { - cip.url = document.location.origin; - cip.submitUrl = action; - - browser.runtime.sendMessage({ - action: 'retrieve_credentials', - args: [ cip.url, cip.submitUrl, false, true ] - }).then((credentials) => { - cip.retrieveCredentialsCallback(credentials, true); - cip.fillIn(combination, onlyPassword, suppressWarnings); - }); - } -}; - -cip.fillInFromActiveElement = function(suppressWarnings, passOnly = false) { - const el = document.activeElement; - if (el.tagName.toLowerCase() !== 'input') { - if (cipFields.combinations.length > 0) { - cip.fillInCredentials(cipFields.combinations[0], false, suppressWarnings); - } - return; - } - - cipFields.setUniqueId(jQuery(el)); - const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); - let combination = null; - if ($(el).attr('type') === 'password') { - combination = cipFields.getCombination('password', fieldId); - } - else { - combination = cipFields.getCombination('username', fieldId); - } - - if (passOnly) { - if (!_f(combination.password)) { - const message = tr('fieldsNoPasswordField'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [message] - }); - return; - } - } - - delete combination.loginId; - - cip.fillInCredentials(combination, passOnly, suppressWarnings); -}; - -cip.fillInFromActiveElementTOTPOnly = function(suppressWarnings) { - const el = document.activeElement; - cipFields.setUniqueId(jQuery(el)); - const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); - - browser.runtime.sendMessage({ - action: 'page_get_login_id' - }).then((pos) => { - if (pos >= 0 && cip.credentials[pos]) { - // Check the value from stringFields (to be removed) - const currentField = _fs(fieldId); - if (cip.credentials[pos].stringFields && cip.credentials[pos].stringFields.length > 0) { - const stringFields = cip.credentials[pos].stringFields; - for (const s of stringFields) { - const val = s["KPH: {TOTP}"]; - if (val) { - cip.setValue(currentField, val); - } - } - } else if (cip.credentials[pos].totp && cip.credentials[pos].totp.length > 0) { - cip.setValue(currentField, cip.credentials[pos].totp); - } - } - }); -}; - -cip.setValue = function(field, value) { - if (field.is('select')) { - value = value.toLowerCase().trim(); - jQuery('option', field).each(function() { - if (jQuery(this).text().toLowerCase().trim() === value) { - cip.setValueWithChange(field, jQuery(this).val()); - return false; - } - }); - } - else { - cip.setValueWithChange(field, value); - field.trigger('input'); - } -}; - -cip.fillInStringFields = function(fields, stringFields, filledInFields) { - let filledIn = false; - - filledInFields.list = []; - if (fields && stringFields && fields.length > 0 && stringFields.length > 0) { - for (let i = 0; i < fields.length; i++) { - const currentField = _fs(fields[i]); - const stringFieldValue = Object.values(stringFields[i]); - if (currentField && stringFieldValue[0]) { - cip.setValue(currentField, stringFieldValue[0]); - filledInFields.list.push(fields[i]); - filledIn = true; - } - } - } - - return filledIn; -}; - -cip.setValueWithChange = function(field, value) { - if (cip.settings.respectMaxLength === true) { - const attribute_maxlength = field.attr('maxlength'); - if (attribute_maxlength && !isNaN(attribute_maxlength) && attribute_maxlength > 0) { - value = value.substr(0, attribute_maxlength); - } - } - - field.val(value); - field[0].dispatchEvent(new Event('input', {'bubbles': true})); - field[0].dispatchEvent(new Event('change', {'bubbles': true})); -}; - -cip.fillIn = function(combination, onlyPassword, suppressWarnings) { - // no credentials available - if (cip.credentials.length === 0 && !suppressWarnings) { - const message = tr('credentialsNoLoginsFound'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [message] - }); - return; - } - - const uField = _f(combination.username); - const pField = _f(combination.password); - - // exactly one pair of credentials available - if (cip.credentials.length === 1) { - let filledIn = false; - if (uField && (!onlyPassword || _singleInputEnabledForPage)) { - cip.setValueWithChange(uField, cip.credentials[0].login); - browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [0] - }); - filledIn = true; - } - if (pField) { - pField.attr('type', 'password'); - cip.setValueWithChange(pField, cip.credentials[0].password); - pField.data('unchanged', true); - browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [0] - }); - filledIn = true; - } - - let list = []; - if (cip.fillInStringFields(combination.fields, cip.credentials[0].stringFields, list)) { - cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); - filledIn = true; - } - - if (!filledIn) { - if (!suppressWarnings) { - const message = tr('fieldsFill'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [message] - }); - } - } - } - // specific login id given - else if (combination.loginId !== undefined && cip.credentials[combination.loginId]) { - let filledIn = false; - if (uField) { - cip.setValueWithChange(uField, cip.credentials[combination.loginId].login); - browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [combination.loginId] - }); - filledIn = true; - } - - if (pField) { - cip.setValueWithChange(pField, cip.credentials[combination.loginId].password); - pField.data('unchanged', true); - browser.runtime.sendMessage({ - action: 'page_set_login_id', args: [combination.loginId] - }); - filledIn = true; - } - - let list = []; - if (cip.fillInStringFields(combination.fields, cip.credentials[combination.loginId].stringFields, list)) { - cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); - filledIn = true; - } - - if (!filledIn) { - if (!suppressWarnings) { - const message = tr('fieldsFill'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [message] - }); - } - } - } - // multiple credentials available - else { - // check if only one password for given username exists - let countPasswords = 0; - - if (uField) { - let valPassword = ''; - let valUsername = ''; - let valStringFields = []; - const valQueryUsername = uField.val().toLowerCase(); - - // find passwords to given username (even those with empty username) - for (const c of cip.credentials) { - if (c.login.toLowerCase() === valQueryUsername) { - countPasswords += 1; - valPassword = c.password; - valUsername = c.login; - valStringFields = c.stringFields; - } - } - - // for the correct notification message: 0 = no logins, X > 1 = too many logins - if (countPasswords === 0) { - countPasswords = cip.credentials.length; - } - - // only one mapping username found - if (countPasswords === 1) { - if (!onlyPassword) { - cip.setValueWithChange(uField, valUsername); - } - - if (pField) { - cip.setValueWithChange(pField, valPassword); - pField.data('unchanged', true); - } - - let list = []; - if (cip.fillInStringFields(combination.fields, valStringFields, list)) { - cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); - } - } - - // user has to select correct credentials by himself - if (countPasswords > 1) { - if (!suppressWarnings) { - const $target = onlyPassword ? pField : uField; - cipAutocomplete.init($target); - $target.focus(); - jQuery($target).autocomplete('search', jQuery($target).val()); - } - } - else if (countPasswords < 1) { - if (!suppressWarnings) { - const message = tr('credentialsNoUsernameFound'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [message] - }); - } - } - } - else { - if (!suppressWarnings) { - const $target = onlyPassword ? pField : uField; - cipAutocomplete.init($target); - $target.focus(); - jQuery($target).autocomplete('search', jQuery($target).val()); - } - } - } -}; - -cip.contextMenuRememberCredentials = function() { - const el = document.activeElement; - if (el.tagName.toLowerCase() !== 'input') { - return; - } - - cipFields.setUniqueId(jQuery(el)); - const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); - let combination = null; - if ($(el).attr('type') === 'password') { - combination = cipFields.getCombination('password', fieldId); - } - else { - combination = cipFields.getCombination('username', fieldId); - } - - let usernameValue = ''; - let passwordValue = ''; - - const usernameField = _f(combination.username); - const passwordField = _f(combination.password); - - if (usernameField) { - usernameValue = usernameField.val(); - } - if (passwordField) { - passwordValue = passwordField.val(); - } - - if (!cip.rememberCredentials(usernameValue, passwordValue)) { - const message = tr('rememberNothingChanged'); - browser.runtime.sendMessage({ - action: 'show_notification', - args: [message] - }); - } -}; - -cip.rememberCredentials = function(usernameValue, passwordValue) { - // no password given or field cleaned by a site-running script - // --> no password to save - if (passwordValue === '') { - return false; - } - - let usernameExists = false; - let nothingChanged = false; - - for (const c of cip.credentials) { - if (c.login === usernameValue && c.password === passwordValue) { - nothingChanged = true; - break; - } - - if (c.login === usernameValue) { - usernameExists = true; - } - } - - if (!nothingChanged) { - if (!usernameExists) { - for (const c of cip.credentials) { - if (c.login === usernameValue) { - usernameExists = true; - break; - } - } - } - let credentialsList = []; - for (const c of cip.credentials) { - credentialsList.push({ - login: c.login, - name: c.name, - uuid: c.uuid - }); - } - - let url = jQuery(this)[0].action; - if (!url) { - url = cip.getDocumentLocation(); - if (url.indexOf('?') > 0) { - url = url.substring(0, url.indexOf('?')); - if (url.length < document.location.origin.length) { - url = document.location.origin; - } - } - } - - browser.runtime.sendMessage({ - action: 'set_remember_credentials', - args: [usernameValue, passwordValue, url, usernameExists, credentialsList] - }); - - return true; - } - - return false; -}; - -cip.ignoreSite = function(sites) { - if (!sites || sites.length === 0) { - return; - } - - let site = sites[0]; - cip.initializeSitePreferences(); - - if (slashNeededForUrl(site)) { - site += '/'; - } - - // Check if the site already exists - let siteExists = false; - for (const existingSite of cip.settings['sitePreferences']) { - if (existingSite.url === site) { - existingSite.ignore = IGNORE_NORMAL; - siteExists = true; - } - } - - if (!siteExists) { - cip.settings['sitePreferences'].push({ - url: site, - ignore: IGNORE_NORMAL, - usernameOnly: false - }); - } - - browser.runtime.sendMessage({ - action: 'save_settings', - args: [cip.settings] - }); -}; - - // Delete previously created Object if it exists. It will be replaced by an Array -cip.initializeSitePreferences = function() { - if (cip.settings['sitePreferences'] !== undefined && cip.settings['sitePreferences'].constructor === Object) { - delete cip.settings['sitePreferences']; - } - - if (!cip.settings['sitePreferences']) { - cip.settings['sitePreferences'] = []; - } -}; - -cip.getDocumentLocation = function() { - return cip.settings.saveDomainOnly ? document.location.origin : document.location.href; -}; - -var cipEvents = {}; - -cipEvents.clearCredentials = function() { - cip.credentials = []; - cipAutocomplete.elements = []; - _called.retrieveCredentials = false; - - if (cip.settings.autoCompleteUsernames) { - for (const c of cipFields.combinations) { - const uField = _f(c.username); - if (uField) { - if (uField.hasClass('ui-autocomplete-input')) { - uField.autocomplete('destroy'); - } - } - } - } -}; - -cipEvents.triggerActivatedTab = function() { - // doesn't run a second time because of _called.initCredentialFields set to true - cip.init(); - $(this.target).find('input').autocomplete(); - - // initCredentialFields calls also "retrieve_credentials", to prevent it - // check of init() was already called - if (_called.initCredentialFields && (cip.url && cip.submitUrl) && cip.settings.autoRetrieveCredentials) { - browser.runtime.sendMessage({ - action: 'retrieve_credentials', - args: [ cip.url, cip.submitUrl ] - }).then(cip.retrieveCredentialsCallback).catch((e) => { - console.log(e); - }); - } -}; +'use strict'; + +// contains already called method names +var _called = {}; +_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; + +// Element id's containing input fields detected by MutationObserver +var _observerIds = []; + +// Document URL +let _documentURL = document.location.href; + +function _f(fieldId) { + const field = (fieldId) ? jQuery('input[data-cip-id=\'' + fieldId + '\']:first') : []; + return (field.length > 0) ? field : null; +} + +function _fs(fieldId) { + const field = (fieldId) ? jQuery('input[data-cip-id=\'' + fieldId + '\']:first,select[data-cip-id=\'' + fieldId + '\']:first').first() : []; + return (field.length > 0) ? field : null; +} + +browser.runtime.onMessage.addListener(function(req, sender, callback) { + if ('action' in req) { + if (req.action === 'fill_user_pass_with_specific_login') { + if (cip.credentials[req.id]) { + let combination = null; + if (cip.u) { + cip.setValueWithChange(cip.u, cip.credentials[req.id].login); + combination = cipFields.getCombination('username', cip.u); + 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); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [req.id] + }); + combination = cipFields.getCombination('password', cip.p); + } + + let list = []; + if (cip.fillInStringFields(combination.fields, cip.credentials[req.id].stringFields, list)) { + cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); + } + } + } else if (req.action === 'fill_user_pass') { + _called.manualFillRequested = 'both'; + cip.receiveCredentialsIfNecessary().then((response) => { + cip.fillInFromActiveElement(false); + }); + } else if (req.action === 'fill_pass_only') { + _called.manualFillRequested = 'pass'; + cip.receiveCredentialsIfNecessary().then((response) => { + cip.fillInFromActiveElement(false, true); // passOnly to true + }); + } else if (req.action === 'fill_totp') { + cip.receiveCredentialsIfNecessary().then((response) => { + cip.fillInFromActiveElementTOTPOnly(false); + }); + } else if (req.action === 'activate_password_generator') { + cip.initPasswordGenerator(cipFields.getAllFields()); + } else if (req.action === 'remember_credentials') { + cip.contextMenuRememberCredentials(); + } else if (req.action === 'choose_credential_fields') { + cipDefine.init(); + } else if (req.action === 'clear_credentials') { + cipEvents.clearCredentials(); + return Promise.resolve(); + } else if (req.action === 'activated_tab') { + cipEvents.triggerActivatedTab(); + return Promise.resolve(); + } else if (req.action === 'redetect_fields') { + browser.runtime.sendMessage({ + action: 'load_settings', + }).then((response) => { + cip.settings = response; + cip.initCredentialFields(true); + }); + } else if (req.action === 'ignore-site') { + cip.ignoreSite(req.args); + } + else if (req.action === 'check_database_hash' && 'hash' in req) { + cip.detectDatabaseChange(req.hash); + } + } +}); + + +var cipAutocomplete = {}; + +// objects of username + description for autocomplete +cipAutocomplete.elements = []; + +cipAutocomplete.init = function(field) { + if (cip.settings.autoFillSingleEntry && cip.credentials.length === 1 && field.hasClass('ui-autocomplete-input')) { + field.autocomplete('destroy'); + } + + const acMenu = jQuery('#kpxc-ac-menu'); + if (acMenu.length == 0) { + jQuery('').appendTo('body'); + } + + field + .addClass('kpxc') + .autocomplete({ + appendTo: '#kpxc-ac-menu', + minLength: 0, + source: cipAutocomplete.onSource, + select: cipAutocomplete.onSelect, + open: cipAutocomplete.onOpen + }); + field + .click(cipAutocomplete.onClick) + .blur(cipAutocomplete.onBlur) + .focus(cipAutocomplete.onFocus); +}; + +cipAutocomplete.onClick = function() { + jQuery(this).autocomplete('search', jQuery(this).val()); +}; + +cipAutocomplete.onOpen = function(event, ui) { + jQuery('ul.ui-autocomplete.ui-menu').css('z-index', 2147483636); +}; + +cipAutocomplete.onSource = function(request, callback) { + const matches = jQuery.map(cipAutocomplete.elements, (tag) => { + if (tag.label.toUpperCase().indexOf(request.term.toUpperCase()) === 0) { + return tag; + } + }); + callback(matches); +}; + +cipAutocomplete.onSelect = function(e, ui) { + e.preventDefault(); + cip.setValueWithChange(jQuery(this), ui.item.value); + const fieldId = cipFields.prepareId(jQuery(this).attr('data-cip-id')); + const combination = cipFields.getCombination('username', fieldId); + combination.loginId = ui.item.loginId; + cip.fillInCredentials(combination, true, false); + jQuery(this).data('fetched', true); +}; + +cipAutocomplete.onBlur = function() { + if (jQuery(this).data('fetched') === true) { + jQuery(this).data('fetched', false); + } else { + const fieldId = cipFields.prepareId(jQuery(this).attr('data-cip-id')); + const fields = cipFields.getCombination('username', fieldId); + const fieldValue = jQuery(this).val(); + + // Check if the manually inserted value is one of the retrieved credentials + const fieldFound = cipAutocomplete.elements.some(e => e.value === fieldValue); + + if (_f(fields.password) && _f(fields.password).data('unchanged') !== true && fieldFound && _detectedFields > 1) { + cip.fillInCredentials(fields, true, true); + } + } +}; + +cipAutocomplete.onFocus = function() { + cip.u = jQuery(this); + + if (jQuery(this).val() === '') { + jQuery(this).autocomplete('search', ''); + } +}; + +var cipPassword = {}; +cipPassword.observedIcons = []; +cipPassword.observingLock = false; + +cipPassword.init = function() { + if ('initPasswordGenerator' in _called) { + return; + } + + _called.initPasswordGenerator = true; + + window.setInterval(function() { + cipPassword.checkObservedElements(); + }, 400); +}; + +cipPassword.initField = function(field, inputs, pos) { + if (!field || field.length !== 1) { + return; + } + if (field.data('cip-password-generator')) { + return; + } + + field.data('cip-password-generator', true); + + cipPassword.createIcon(field); + + let $found = false; + if (inputs) { + for (let i = pos + 1; i < inputs.length; i++) { + if (inputs[i] && inputs[i].attr('type') && inputs[i].attr('type').toLowerCase() === 'password') { + field.data('cip-genpw-next-field-id', inputs[i].data('cip-id')); + field.data('cip-genpw-next-is-password-field', (i === 0)); + $found = true; + break; + } + } + } + + field.data('cip-genpw-next-field-exists', $found); +}; + +cipPassword.createDialog = function() { + if ('passwordCreateDialog' in _called) { + return; + } + + _called.passwordCreateDialog = true; + + const $dialog = jQuery('') + .addClass('dialog-form') + .attr('id', 'cip-genpw-dialog'); + + const $inputDiv = jQuery('').addClass('form-group'); + const $inputGroup = jQuery('').addClass('genpw-input-group'); + const $textfieldPassword = jQuery('') + .attr('id', 'cip-genpw-textfield-password') + .attr('type', 'text') + .attr('aria-describedby', 'cip-genpw-quality') + .attr('placeholder', tr('passwordGeneratorPlaceholder')) + .addClass('genpw-text ui-widget-content ui-corner-all') + .on('change keypress paste textInput input', function() { + jQuery('#cip-genpw-btn-clipboard:first').removeClass('btn-success'); + }); + const $quality = jQuery('') + .addClass('genpw-input-group-addon') + .addClass('b2c-add-on') + .attr('id', 'cip-genpw-quality') + .text(tr('passwordGeneratorBits')); + $inputGroup.append($textfieldPassword).append($quality); + + const $checkGroup = jQuery('').addClass('genpw-input-group'); + const $checkboxNextField = jQuery('') + .attr('id', 'cip-genpw-checkbox-next-field') + .attr('type', 'checkbox') + .addClass('cip-genpw-checkbox'); + const $labelNextField = jQuery('') + .append($checkboxNextField) + .addClass('cip-genpw-label') + .append(tr('passwordGeneratorLabel')); + $checkGroup.append($labelNextField); + + $inputDiv.append($inputGroup).append($checkGroup); + $dialog.append($inputDiv); + + $dialog.hide(); + jQuery('body').append($dialog); + + const $container = jQuery('#kpxc-pw-dialog'); + if ($container.length === 0) { + jQuery('').appendTo('body'); + } + + $dialog.dialog({ + appendTo: '#kpxc-pw-dialog', + autoOpen: false, + modal: true, + resizable: false, + minWidth: 300, + minHeight: 80, + title: tr('passwordGeneratorTitle'), + classes: {'ui-dialog': 'ui-corner-all'}, + buttons: { + 'Generate': + { + text: tr('passwordGeneratorGenerate'), + id: 'cip-genpw-btn-generate', + click: (e) => { + e.preventDefault(); + browser.runtime.sendMessage({ + action: 'generate_password' + }).then(cipPassword.callbackGeneratedPassword).catch((err) => { + console.log(err); + }); + } + }, + 'Copy': + { + text: tr('passwordGeneratorCopy'), + id: 'cip-genpw-btn-clipboard', + click: (e) => { + e.preventDefault(); + cipPassword.copyPasswordToClipboard(); + } + }, + 'Fill & copy': + { + text: tr('passwordGeneratorFillAndCopy'), + id: 'cip-genpw-btn-fillin', + click: (e) => { + e.preventDefault(); + + const fieldId = jQuery('#cip-genpw-dialog:first').data('cip-genpw-field-id'); + const field = jQuery('input[data-cip-id=\'' + fieldId + '\']:first'); + if (field.length === 1) { + let $password = jQuery('input#cip-genpw-textfield-password:first').val(); + + if (field.attr('maxlength')) { + if ($password.length > field.attr('maxlength')) { + $password = $password.substring(0, field.attr('maxlength')); + jQuery('input#cip-genpw-textfield-password:first').val($password); + jQuery('#cip-genpw-btn-clipboard:first').removeClass('b2c-btn-success'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ tr('passwordGeneratorErrorTooLong') ] + }); + } + } + + field.val($password); + if (jQuery('input#cip-genpw-checkbox-next-field:checked').length === 1) { + if (field.data('cip-genpw-next-field-exists')) { + const nextFieldId = field.data('cip-genpw-next-field-id'); + const nextField = jQuery('input[data-cip-id=\'' + nextFieldId + '\']:first'); + if (nextField.length === 1) { + nextField.val($password); + } + } + } + + cipPassword.copyPasswordToClipboard(); + } + } + } + }, + open: function(event, ui) { + jQuery('.ui-widget-overlay').click(function() { + jQuery('#cip-genpw-dialog:first').dialog('close'); + jQuery('span').remove('.kpxc'); + }); + + if (jQuery('input#cip-genpw-textfield-password:first').val() === '') { + jQuery('button#cip-genpw-btn-generate:first').click(); + } + } + }); +}; + +cipPassword.createIcon = function(field) { + const $className = (isFirefox() ? 'key-moz' : 'key'); + const $size = (field.outerHeight() > 28) ? 24 : 16; + let $offset = Math.floor((field.outerHeight() - $size) / 3); + $offset = ($offset < 0) ? 0 : $offset; + + const $icon = jQuery('') + .addClass('kpxc') + .addClass('cip-genpw-icon') + .addClass($className) + .attr('title', tr('passwordGeneratorGenerateText')) + .css('z-index', '9999') + .css('width', $size) + .css('height', $size) + .data('size', $size) + .data('offset', $offset) + .data('cip-genpw-field-id', field.data('cip-id')); + cipPassword.setIconPosition($icon, field); + $icon.click(function(e) { + e.preventDefault(); + + if (!field.is(':visible')) { + $icon.remove(); + field.removeData('cip-password-generator'); + return; + } + + cipPassword.createDialog(); + const $dialog = jQuery('#cip-genpw-dialog'); + if ($dialog.dialog('isOpen')) { + $dialog.dialog('close'); + } + + $dialog.dialog('option', 'position', { my: 'left-10px top', at: 'center bottom', of: jQuery(this) }); + $dialog.data('cip-genpw-field-id', field.data('cip-id')); + $dialog.data('cip-genpw-next-field-id', field.data('cip-genpw-next-field-id')); + $dialog.data('cip-genpw-next-is-password-field', field.data('cip-genpw-next-is-password-field')); + + const $bool = Boolean(field.data('cip-genpw-next-field-exists')); + jQuery('input#cip-genpw-checkbox-next-field:first') + .attr('checked', $bool) + .attr('disabled', !$bool); + + $dialog.dialog('open'); + }); + + cipPassword.observedIcons.push($icon); + jQuery('body').append($icon); +}; + +cipPassword.setIconPosition = function($icon, $field) { + $icon.css('top', $field.offset().top + $icon.data('offset') + 1) + .css('left', $field.offset().left + $field.outerWidth() - $icon.data('size') - $icon.data('offset')); +}; + +cipPassword.copyPasswordToClipboard = function(e) { + if (e) { + e.preventDefault(); + } + + const input = jQuery('input#cip-genpw-textfield-password'); + input.select(); + try { + const success = document.execCommand('copy'); + if (success) { + jQuery('#cip-genpw-btn-clipboard').addClass('b2c-btn-success'); + } + jQuery('#cip-genpw-dialog').select(); + input.value = ''; + } + catch (err) { + console.log('Could not copy password to clipboard: ' + err); + } +}; + +cipPassword.callbackPasswordCopied = function(bool) { + if (bool) { + jQuery('#cip-genpw-btn-clipboard').addClass('btn-success'); + } +}; + +cipPassword.callbackGeneratedPassword = function(entries) { + if (entries && entries.length >= 1) { + jQuery('#cip-genpw-btn-clipboard:first').removeClass('btn-success'); + jQuery('input#cip-genpw-textfield-password:first').val(entries[0].password); + if (isNaN(entries[0].login)) { + jQuery('#cip-genpw-quality:first').text('??? Bits'); + } else { + jQuery('#cip-genpw-quality:first').text(entries[0].login + ' Bits'); + } + } else { + if (jQuery('div#cip-genpw-error:first').length === 0) { + jQuery('button#cip-genpw-btn-generate:first').after('' + tr('passwordGeneratorError') + ''); + jQuery('input#cip-genpw-textfield-password:first').parent().hide(); + jQuery('input#cip-genpw-checkbox-next-field:first').parent('label').hide(); + jQuery('button#cip-genpw-btn-generate').hide(); + jQuery('button#cip-genpw-btn-clipboard').hide(); + jQuery('button#cip-genpw-btn-fillin').hide(); + } + } +}; + +cipPassword.onRequestPassword = function() { + browser.runtime.sendMessage({ + action: 'generate_password' + }).then(cipPassword.callbackGeneratedPassword); +}; + +cipPassword.checkObservedElements = function() { + if (cipPassword.observingLock) { + return; + } + + cipPassword.observingLock = true; + jQuery.each(cipPassword.observedIcons, (index, iconField) => { + if (iconField && iconField.length === 1) { + const fieldId = iconField.data('cip-genpw-field-id'); + const field = jQuery('input[data-cip-id=\'' + fieldId + '\']:first'); + if (!field || field.length !== 1) { + iconField.remove(); + cipPassword.observedIcons.splice(index, 1); + } else if (!field.is(':visible')) { + iconField.hide(); + } else if (field.is(':visible')) { + iconField.show(); + cipPassword.setIconPosition(iconField, field); + field.data('cip-password-generator', true); + } + + + } else { + cipPassword.observedIcons.splice(index, 1); + } + }); + cipPassword.observingLock = false; +}; + + + +var cipForm = {}; + +cipForm.init = function(form, credentialFields) { + // Not already initialized && password-field is not null + if (!form.data('cipForm-initialized') && (credentialFields.password || (_singleInputEnabledForPage && credentialFields.username))) { + form.data('cipForm-initialized', true); + cipForm.setInputFields(form, credentialFields); + form.submit(cipForm.onSubmit); + } +}; + +cipForm.destroy = function(form, credentialFields) { + if (form === false && credentialFields) { + const field = _f(credentialFields.password) || _f(credentialFields.username); + if (field) { + form = field.closest('form'); + } + } + + if (form && jQuery(form).length > 0) { + jQuery(form).unbind('submit', cipForm.onSubmit); + } +}; + +cipForm.setInputFields = function(form, credentialFields) { + form.data('cipUsername', credentialFields.username); + form.data('cipPassword', credentialFields.password); +}; + +cipForm.onSubmit = function() { + const usernameId = jQuery(this).data('cipUsername'); + const passwordId = jQuery(this).data('cipPassword'); + + let usernameValue = ''; + let passwordValue = ''; + + const usernameField = _f(usernameId); + const passwordField = _f(passwordId); + + if (usernameField) { + usernameValue = usernameField.val(); + } + if (passwordField) { + passwordValue = passwordField.val(); + } + + cip.rememberCredentials(usernameValue, passwordValue); +}; + + + +var cipDefine = {}; + +cipDefine.selection = { + username: null, + password: null, + fields: [] +}; +cipDefine.eventFieldClick = null; + +cipDefine.init = function() { + const $backdrop = jQuery('').attr('id', 'b2c-backdrop').addClass('b2c-modal-backdrop'); + jQuery('body').append($backdrop); + + const $chooser = jQuery('').attr('id', 'b2c-cipDefine-fields'); + jQuery('body').append($chooser); + + const $description = jQuery('').attr('id', 'b2c-cipDefine-description'); + $backdrop.append($description); + + cipFields.getAllFields(); + cipFields.prepareVisibleFieldsWithID('select'); + + cipDefine.initDescription(); + + cipDefine.resetSelection(); + cipDefine.prepareStep1(); + cipDefine.markAllUsernameFields($chooser); +}; + +cipDefine.initDescription = function() { + const $description = jQuery('div#b2c-cipDefine-description'); + const $h1 = jQuery('').addClass('b2c-chooser-headline'); + $description.append($h1); + const $help = jQuery('').addClass('b2c-chooser-help').attr('id', 'b2c-help'); + $description.append($help); + + const $btnDismiss = jQuery('').text(tr('defineDismiss')).attr('id', 'b2c-btn-dismiss') + .addClass('btn') + .addClass('btn-danger') + .click(function(e) { + jQuery('div#b2c-backdrop').remove(); + jQuery('div#b2c-cipDefine-fields').remove(); + }); + const $btnSkip = jQuery('').text(tr('defineSkip')).attr('id', 'b2c-btn-skip') + .addClass('btn') + .addClass('btn-info') + .css('margin-right', '5px') + .click(function() { + if (jQuery(this).data('step') === '1') { + cipDefine.selection.username = null; + cipDefine.prepareStep2(); + cipDefine.markAllPasswordFields(jQuery('#b2c-cipDefine-fields'), false); + } else if (jQuery(this).data('step') === '2') { + cipDefine.selection.password = null; + cipDefine.prepareStep3(); + cipDefine.markAllStringFields(jQuery('#b2c-cipDefine-fields')); + } + }); + const $btnMore = jQuery('').text(tr('defineMore')).attr('id', 'b2c-btn-more') + .addClass('btn') + .addClass('btn-info') + .css('margin-right', '5px') + .css('margin-left', '5px') + .click(function() { + cipDefine.prepareStep2(); + cipDefine.markAllPasswordFields(jQuery('#b2c-cipDefine-fields'), true); + }); + const $btnAgain = jQuery('').text(tr('defineAgain')).attr('id', 'b2c-btn-again') + .addClass('btn') + .addClass('btn-warning') + .css('margin-right', '5px') + .click(function(e) { + cipDefine.resetSelection(); + cipDefine.prepareStep1(); + cipDefine.markAllUsernameFields(jQuery('#b2c-cipDefine-fields')); + }) + .hide(); + const $btnConfirm = jQuery('').text(tr('defineConfirm')).attr('id', 'b2c-btn-confirm') + .addClass('btn') + .addClass('btn-primary') + .css('margin-right', '15px') + .click(function(e) { + if (!cip.settings['defined-custom-fields']) { + cip.settings['defined-custom-fields'] = {}; + } + + if (cipDefine.selection.username) { + cipDefine.selection.username = cipFields.prepareId(cipDefine.selection.username); + } + + if (cipDefine.selection.password) { + cipDefine.selection.password = cipFields.prepareId(cipDefine.selection.password); + } + + const fieldIds = []; + const fieldKeys = Object.keys(cipDefine.selection.fields); + for (const i of fieldKeys) { + fieldIds.push(cipFields.prepareId(i)); + } + + const location = cip.getDocumentLocation(); + cip.settings['defined-custom-fields'][location] = { + username: cipDefine.selection.username, + password: cipDefine.selection.password, + fields: fieldIds + }; + + browser.runtime.sendMessage({ + action: 'save_settings', + args: [ cip.settings ] + }); + + jQuery('button#b2c-btn-dismiss').click(); + }) + .hide(); + + $description.append($btnConfirm); + $description.append($btnSkip); + $description.append($btnAgain); + $description.append($btnDismiss); + $description.append($btnMore); + + const location = cip.getDocumentLocation(); + if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { + const $p = jQuery('').html(tr('defineAlreadySelected') + ''); + const $btnDiscard = jQuery('') + .attr('id', 'btn-warning') + .text(tr('defineDiscard')) + .css('margin-top', '5px') + .addClass('btn') + .addClass('btn-sm') + .addClass('btn-danger') + .click(function(e) { + delete cip.settings['defined-custom-fields'][location]; + + browser.runtime.sendMessage({ + action: 'save_settings', + args: [ cip.settings ] + }); + + browser.runtime.sendMessage({ + action: 'load_settings' + }); + + jQuery(this).parent('p').remove(); + }); + $p.append($btnDiscard); + $description.append($p); + } + + jQuery('div#b2c-cipDefine-description').draggable(); +}; + +cipDefine.resetSelection = function() { + cipDefine.selection = { + username: null, + password: null, + fields: [] + }; +}; + +cipDefine.isFieldSelected = function($cipId) { + return ( + $cipId === cipDefine.selection.username || + $cipId === cipDefine.selection.password || + $cipId in cipDefine.selection.fields + ); +}; + +cipDefine.markAllUsernameFields = function($chooser) { + cipDefine.eventFieldClick = function(e) { + cipDefine.selection.username = jQuery(this).data('cip-id'); + jQuery(this).addClass('b2c-fixed-username-field').text(tr('username')).unbind('click'); + cipDefine.prepareStep2(); + cipDefine.markAllPasswordFields(jQuery('#b2c-cipDefine-fields')); + }; + cipDefine.markFields($chooser, cipFields.inputQueryPattern); +}; + +cipDefine.markAllPasswordFields = function($chooser, more) { + cipDefine.eventFieldClick = function(e) { + cipDefine.selection.password = jQuery(this).data('cip-id'); + jQuery(this).addClass('b2c-fixed-password-field').text(tr('password')).unbind('click'); + cipDefine.prepareStep3(); + cipDefine.markAllStringFields(jQuery('#b2c-cipDefine-fields')); + }; + if (more) { + cipDefine.markFields($chooser, cipFields.inputQueryPattern); + } else { + cipDefine.markFields($chooser, 'input[type=\'password\']'); + } +}; + +cipDefine.markAllStringFields = function($chooser) { + cipDefine.eventFieldClick = function(e) { + cipDefine.selection.fields[jQuery(this).data('cip-id')] = true; + const count = Object.keys(cipDefine.selection.fields).length; + jQuery(this).addClass('b2c-fixed-string-field').text(tr('defineStringField') + String(count)).unbind('click'); + jQuery('button#b2c-btn-confirm:first').addClass('b2c-btn-primary').attr('disabled', false); + }; + cipDefine.markFields($chooser, cipFields.inputQueryPattern + ', select'); +}; + +cipDefine.markFields = function($chooser, $pattern) { + jQuery($pattern).each(function() { + if (cipDefine.isFieldSelected(jQuery(this).data('cip-id'))) { + return true; + } + + if (cipFields.isVisible(this)) { + const $field = jQuery('').addClass('b2c-fixed-field') + .css('top', jQuery(this).offset().top) + .css('left', jQuery(this).offset().left) + .css('width', jQuery(this).outerWidth()) + .css('height', jQuery(this).outerHeight()) + .attr('data-cip-id', jQuery(this).attr('data-cip-id')) + .click(cipDefine.eventFieldClick) + .hover(function() {jQuery(this).addClass('b2c-fixed-hover-field');}, function() {jQuery(this).removeClass('b2c-fixed-hover-field');}); + $chooser.append($field); + } + }); +}; + +cipDefine.prepareStep1 = function() { + jQuery('div#b2c-help').text('').css('margin-bottom', 0); + jQuery('div#b2c-cipDefine-fields').removeData('username'); + jQuery('div#b2c-cipDefine-fields').removeData('password'); + jQuery('div.b2c-fixed-field', jQuery('div#b2c-cipDefine-fields')).remove(); + jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineChooseUsername')); + jQuery('button#b2c-btn-skip:first').data('step', '1').show(); + jQuery('button#b2c-btn-confirm:first').hide(); + jQuery('button#b2c-btn-again:first').hide(); + jQuery('button#b2c-btn-more:first').hide(); +}; + +cipDefine.prepareStep2 = function() { + jQuery('div#b2c-help').text('').css('margin-bottom', 0); + jQuery('div.b2c-fixed-field:not(.b2c-fixed-username-field)', jQuery('div#b2c-cipDefine-fields')).remove(); + jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineChoosePassword')); + jQuery('button#b2c-btn-skip:first').data('step', '2'); + jQuery('button#b2c-btn-again:first').show(); + jQuery('button#b2c-btn-more:first').show(); +}; + +cipDefine.prepareStep3 = function() { + if (!cipDefine.selection.username && !cipDefine.selection.password) { + jQuery('button#b2c-btn-confirm:first').removeClass('b2c-btn-primary').attr('disabled', true); + } + + jQuery('div#b2c-help').html(tr('defineHelpText')).css('margin-bottom', '5px'); + jQuery('div.b2c-fixed-field:not(.b2c-fixed-password-field,.b2c-fixed-username-field)', jQuery('div#b2c-cipDefine-fields')).remove(); + jQuery('button#b2c-btn-confirm:first').show(); + jQuery('button#b2c-btn-skip:first').data('step', '3').hide(); + jQuery('button#b2c-btn-more:first').hide(); + jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineConfirmSelection')); +}; + + + +var cipFields = {}; + +cipFields.inputQueryPattern = 'input[type=\'text\'], input[type=\'email\'], input[type=\'password\'], input[type=\'tel\'], input[type=\'number\'], input:not([type])'; +// Unique number as new IDs for input fields +cipFields.uniqueNumber = 342845638; +// Objects with combination of username + password fields +cipFields.combinations = []; + +cipFields.setUniqueId = function(field) { + if (field && !field.attr('data-cip-id')) { + // Use ID of field if it is unique + const fieldId = field.attr('id'); + if (fieldId) { + const foundIds = jQuery('input#' + cipFields.prepareId(fieldId)); + if (foundIds.length === 1) { + field.attr('data-cip-id', fieldId); + return; + } + } + + // Create own ID if no ID is set for this field + cipFields.uniqueNumber += 1; + field.attr('data-cip-id', 'jQuery' + String(cipFields.uniqueNumber)); + } +}; + +cipFields.prepareId = function(id) { + return id.replace(/[:#.,\[\]\(\)' "]/g, function(m) { return '\\' + m; }); +}; + +/** + * Returns the first parent element satifying the {@code predicate} mapped by {@code resultFn} or else {@code defaultVal}. + * @param {HTMLElement} element The start element (excluded, starting with the parents) + * @param {function} predicate Matcher for the element to find, type (HTMLElement) => boolean + * @param {function} resultFn Callback function of type (HTMLElement) => {*} called for the first matching element + * @param {fun} defaultValFn Fallback return value supplier, if no element matching the predicate can be found + */ +cipFields.traverseParents = function(element, predicate, resultFn = () => true, defaultValFn = () => false) { + for (let f = element.parentElement; f !== null; f = f.parentElement) { + if (predicate(f)) { + return resultFn(f); + } + } + return defaultValFn(); +}; + +cipFields.getOverflowHidden = function(field) { + return cipFields.traverseParents(field, f => f.style.overflow === 'hidden'); +}; + + +// Checks if input field is a search field. Attributes or form action containing 'search', or parent element holding +// role="search" will be identified as a search field. +cipFields.isSearchField = function(target) { + const attributes = target.attributes; + + // Check element attributes + for (const attr of attributes) { + if ((attr.value && (attr.value.toLowerCase().includes('search')) || attr.value === 'q')) { + return true; + } + } + + // Check closest form + const closestForm = target.closest('form'); + if (closestForm) { + // Check form action + const formAction = closestForm.getAttribute('action'); + if (formAction && (formAction.toLowerCase().includes('search') && + !formAction.toLowerCase().includes('research'))) { + return true; + } + + // Check form class and id + const closestFormId = closestForm.getAttribute('id'); + const closestFormClass = closestForm.className; + if (closestFormClass && (closestForm.className.toLowerCase().includes('search') || + (closestFormId && closestFormId.toLowerCase().includes('search') && !closestFormId.toLowerCase().includes('research')))) { + return true; + } + } + + // Check parent elements for role="search" + const roleFunc = f => f.getAttribute('role'); + const roleValue = cipFields.traverseParents(target, roleFunc, roleFunc, () => null); + if (roleValue && roleValue === 'search') { + return true; + } + + return false; +}; + +cipFields.isVisible = function(field) { + const rect = field.getBoundingClientRect(); + + // Check CSS visibility + const fieldStyle = getComputedStyle(field); + if (fieldStyle.visibility && (fieldStyle.visibility === 'hidden' || fieldStyle.visibility === 'collapse')) { + return false; + } + + // Check element position and size + if (rect.x < 0 || rect.y < 0 || rect.width < 8 || rect.height < 8) { + return false; + } + + return true; +}; + +cipFields.getAllFields = function() { + const fields = []; + const inputs = cipObserverHelper.getInputs(document); + for (const i of inputs) { + if (cipFields.isVisible(i) && !cipFields.isSearchField(i)) { + cipFields.setUniqueId(jQuery(i)); + fields.push(jQuery(i)); + } + }; + + _detectedFields = fields.length; + return fields; +}; + +cipFields.prepareVisibleFieldsWithID = function($pattern) { + jQuery($pattern).each(function() { + if (cipFields.isVisible(this) && !cipFields.isSearchField(this)) { + cipFields.setUniqueId(jQuery(this)); + } + }); +}; + +cipFields.getAllCombinations = function(inputs) { + const fields = []; + let uField = null; + + for (const i of inputs) { + if (i) { + if (i.attr('type') && i.attr('type').toLowerCase() === 'password') { + const uId = (!uField || uField.length < 1) ? null : cipFields.prepareId(uField.attr('data-cip-id')); + + const combination = { + username: uId, + password: cipFields.prepareId(i.attr('data-cip-id')) + }; + fields.push(combination); + + // Reset selected username field + uField = null; + } else { + // Username field + uField = i; + } + } + } + + if (_singleInputEnabledForPage && fields.length === 0 && uField) { + const combination = { + username: uField[0].getAttribute('data-cip-id'), + password: null + }; + fields.push(combination); + } + + return fields; +}; + +cipFields.getCombination = function(givenType, fieldId) { + if (cipFields.combinations.length === 0) { + if (cipFields.useDefinedCredentialFields()) { + return cipFields.combinations[0]; + } + } + // Use defined credential fields (already loaded into combinations) + const location = cip.getDocumentLocation(); + if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { + return cipFields.combinations[0]; + } + + for (let c of cipFields.combinations) { + if (c[givenType] === fieldId) { + return c; + } + } + + // Find new combination + let combination = { + username: null, + password: null + }; + + let newCombi = false; + if (givenType === 'username') { + const passwordField = cipFields.getPasswordField(fieldId, true); + let passwordId = null; + if (passwordField && passwordField.length > 0) { + passwordId = cipFields.prepareId(passwordField.attr('data-cip-id')); + } + combination = { + username: fieldId, + password: passwordId + }; + newCombi = true; + } else if (givenType === 'password') { + const usernameField = cipFields.getUsernameField(fieldId, true); + let usernameId = null; + if (usernameField && usernameField.length > 0) { + usernameId = cipFields.prepareId(usernameField.attr('data-cip-id')); + } + combination = { + username: usernameId, + password: fieldId + }; + newCombi = true; + } + + if (combination.username || combination.password) { + cipFields.combinations.push(combination); + } + + if (combination.username) { + if (cip.credentials.length > 0) { + cip.preparePageForMultipleCredentials(cip.credentials); + } + } + + if (newCombi) { + combination.isNew = true; + } + return combination; +}; + +/** +* Return the username field or null if it not exists +*/ +cipFields.getUsernameField = function(passwordId, checkDisabled) { + const passwordField = _f(passwordId); + if (!passwordField) { + return null; + } + + const form = passwordField.closest('form')[0]; + let usernameField = null; + + // Search all inputs on this one form + if (form) { + jQuery(cipFields.inputQueryPattern, form).each(function() { + cipFields.setUniqueId(jQuery(this)); + if (jQuery(this).attr('data-cip-id') === passwordId) { + return false; // Break + } + + if (jQuery(this).attr('type') && jQuery(this).attr('type').toLowerCase() === 'password') { + return true; // Continue + } + + usernameField = jQuery(this); + }); + } else { + // Search all inputs on page + const inputs = cipFields.getAllFields(); + cip.initPasswordGenerator(inputs); + for (const i of inputs) { + if (i.attr('data-cip-id') === passwordId) { + break; + } + + if (i.attr('type') && i.attr('type').toLowerCase() === 'password') { + continue; + } + + usernameField = i; + } + } + + if (usernameField && !checkDisabled) { + const usernameId = usernameField.attr('data-cip-id'); + // Check if usernameField is already used by another combination + for (const c of cipFields.combinations) { + if (c.username === usernameId) { + usernameField = null; + break; + } + } + } + + cipFields.setUniqueId(usernameField); + return usernameField; +}; + +/** +* Return the password field or null if it not exists +*/ +cipFields.getPasswordField = function(usernameId, checkDisabled) { + const usernameField = _f(usernameId); + if (!usernameField) { + return null; + } + + const form = usernameField.closest('form')[0]; + let passwordField = null; + + // Search all inputs on this one form + if (form) { + passwordField = jQuery('input[type=\'password\']:first', form); + if (passwordField && passwordField.length < 1) { + passwordField = null; + } + + if (cip.settings.usePasswordGenerator) { + cipPassword.init(); + cipPassword.initField(passwordField); + } + } else { + // Search all inputs on page + const inputs = cipFields.getAllFields(); + cip.initPasswordGenerator(inputs); + + let active = false; + for (const i of inputs) { + if (i.attr('data-cip-id') === usernameId) { + active = true; + } + if (active && jQuery(i).attr('type') && jQuery(i).attr('type').toLowerCase() === 'password') { + passwordField = i; + break; + } + } + } + + if (passwordField && !checkDisabled) { + const passwordId = passwordField.attr('data-cip-id'); + // Check if passwordField is already used by another combination + for (const c of cipFields.combinations) { + if (c.password === passwordId) { + passwordField = null; + break; + } + } + } + + cipFields.setUniqueId(passwordField); + + return passwordField; +}; + +cipFields.prepareCombinations = function(combinations) { + for (const c of combinations) { + const pwField = _f(c.password); + // Needed for auto-complete: don't overwrite manually filled-in password field + if (pwField && !pwField.data('cipFields-onChange')) { + pwField.data('cipFields-onChange', true); + pwField.change(function() { + jQuery(this).data('unchanged', false); + }); + } + + // Initialize form-submit for remembering credentials + const fieldId = c.password || c.username; + const field = _f(fieldId); + if (field) { + const form = field.closest('form'); + if (form && form.length > 0) { + cipForm.init(form, c); + } + } + } +}; + +cipFields.useDefinedCredentialFields = function() { + const location = cip.getDocumentLocation(); + if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { + const creds = cip.settings['defined-custom-fields'][location]; + + let $found = _f(creds.username) || _f(creds.password); + for (const i of creds.fields) { + if (_fs(i)) { + $found = true; + break; + } + } + + if ($found) { + const fields = { + username: creds.username, + password: creds.password, + fields: creds.fields + }; + cipFields.combinations = []; + cipFields.combinations.push(fields); + + return true; + } + } + + return false; +}; + + +var cipObserverHelper = {}; +cipObserverHelper.inputTypes = [ + 'text', + 'email', + 'password', + 'tel', + 'number', + null // Input field can be without any type. Include these to the list. +]; + +// Ignores all nodes that doesn't contain elements +cipObserverHelper.ignoredNode = function(target) { + if (target.nodeType === Node.ATTRIBUTE_NODE || + target.nodeType === Node.TEXT_NODE || + target.nodeType === Node.CDATA_SECTION_NODE || + target.nodeType === Node.PROCESSING_INSTRUCTION_NODE || + target.nodeType === Node.COMMENT_NODE || + target.nodeType === Node.DOCUMENT_TYPE_NODE || + target.nodeType === Node.NOTATION_NODE) { + return true; + } + return false; +}; + +cipObserverHelper.getInputs = function(target) { + // Ignores target element if it's not an element node + if (cipObserverHelper.ignoredNode(target)) { + return []; + } + + // Filter out any input fields with type 'hidden' right away + const inputFields = []; + Array.from(target.getElementsByTagName('input')).forEach((e) => { + if (e.type !== 'hidden') { + inputFields.push(e); + } + }); + + // Do not allow more visible inputs than _maximumInputs (default value: 100) + if (inputFields.length === 0 || inputFields.length > _maximumInputs) { + return []; + } + + // Only include input fields that match with cipObserverHelper.inputTypes + const inputs = []; + for (const i of inputFields) { + let type = i.getAttribute('type'); + if (type) { + type = type.toLowerCase(); + } + + if (cipObserverHelper.inputTypes.includes(type)) { + inputs.push(i); + } + } + return inputs; +}; + +cipObserverHelper.getId = function(target) { + return target.classList.length === 0 ? target.id : target.classList; +}; + +cipObserverHelper.ignoredElement = function(target) { + // Ignore elements that do not have a className (including SVG) + if (typeof target.className !== 'string') { + return true; + } + + // Ignore KeePassXC-Browser classes + if (target.className && target.className !== undefined && + (target.className.includes('kpxc') || target.className.includes('ui-helper'))) { + return true; + } + + return false; +}; + +cipObserverHelper.handleObserverAdd = function(target) { + if (cipObserverHelper.ignoredElement(target)) { + return; + } + + const inputs = cipObserverHelper.getInputs(target); + if (inputs.length === 0) { + return; + } + + const neededLength = _detectedFields === 1 ? 0 : 1; + const id = cipObserverHelper.getId(target); + if (inputs.length > neededLength && !_observerIds.includes(id)) { + // Save target element id for preventing multiple calls to initCredentialsFields() + _observerIds.push(id); + + // Sometimes the settings haven't been loaded before new input fields are detected + if (Object.keys(cip.settings).length === 0) { + cip.init(); + } else { + cip.initCredentialFields(true); + } + } +}; + +cipObserverHelper.handleObserverRemove = function(target) { + if (cipObserverHelper.ignoredElement(target)) { + return; + } + + const inputs = cipObserverHelper.getInputs(target); + if (inputs.length === 0) { + return; + } + + // Remove target element id from the list + const id = cipObserverHelper.getId(target); + if (_observerIds.includes(id)) { + const index = _observerIds.indexOf(id); + if (index >= 0) { + _observerIds.splice(index, 1); + } + } +}; + +cipObserverHelper.detectURLChange = function() { + if (_documentURL !== document.location.href) { + _documentURL = document.location.href; + cipEvents.clearCredentials(); + cip.initCredentialFields(true); + } +}; + +MutationObserver = window.MutationObserver || window.WebKitMutationObserver; + +// Detects DOM changes in the document +let observer = new MutationObserver(function(mutations, observer) { + if (document.visibilityState === 'hidden') { + return; + } + + for (const mut of mutations) { + // Skip text nodes + if (mut.target.nodeType === Node.TEXT_NODE) { + continue; + } + + // Check document URL change and detect new fields + cipObserverHelper.detectURLChange(); + + // Handle attributes only if CSS display is modified + if (mut.type === 'attributes') { + const newValue = mut.target.getAttribute(mut.attributeName); + if (newValue && (newValue.includes('display') || newValue.includes('z-index'))) { + if (mut.target.style.display !== 'none') { + cipObserverHelper.handleObserverAdd(mut.target); + } else { + cipObserverHelper.handleObserverRemove(mut.target); + } + } + } else if (mut.type === 'childList') { + cipObserverHelper.handleObserverAdd((mut.addedNodes.length > 0) ? mut.addedNodes[0] : mut.target); + cipObserverHelper.handleObserverRemove((mut.removedNodes.length > 0) ? mut.removedNodes[0] : mut.target); + } + } +}); + +// Define what element should be observed by the observer +// and what types of mutations trigger the callback +observer.observe(document, { + subtree: true, + attributes: true, + childList: true, + characterData: true, + attributeFilter: ['style'] +}); + + +var cip = {}; +cip.settings = {}; +cip.u = null; +cip.p = null; +cip.url = null; +cip.submitUrl = null; +cip.credentials = []; + +jQuery(function() { + cip.init(); +}); + +cip.init = function() { + browser.runtime.sendMessage({ + action: 'load_settings', + }).then((response) => { + cip.settings = response; + cip.initCredentialFields(); + }); +}; + +// Switch credentials if database is changed or closed +cip.detectDatabaseChange = function(response) { + if (document.visibilityState !== 'hidden') { + if (response.new === '' && response.old !== '') { + cipEvents.clearCredentials(); + + browser.runtime.sendMessage({ + action: 'page_clear_logins' + }); + + // Switch back to default popup + browser.runtime.sendMessage({ + action: 'get_status', + args: [ true ] // Set polling to true, this is an internal function call + }); + } else if (response.new !== '' && response.new !== response.old) { + _called.retrieveCredentials = false; + browser.runtime.sendMessage({ + action: 'load_settings', + }).then((settings) => { + cip.settings = settings; + cip.initCredentialFields(true); + + // If user has requested a manual fill through context menu the actual credential filling + // is handled here when the opened database has been regognized. It's not a pretty hack. + if (_called.manualFillRequested && _called.manualFillRequested !== 'none') { + cip.fillInFromActiveElement(false, _called.manualFillRequested === 'pass'); + _called.manualFillRequested = 'none'; + } + }); + } + } +}; + +cip.initCredentialFields = function(forceCall) { + if (_called.initCredentialFields && !forceCall) { + return; + } + _called.initCredentialFields = true; + + browser.runtime.sendMessage({ 'action': 'page_clear_logins', args: [ _called.clearLogins ] }).then(() => { + _called.clearLogins = true; + + // Check site preferences + cip.initializeSitePreferences(); + if (cip.settings.sitePreferences) { + for (const site of cip.settings.sitePreferences) { + if (site.url === document.location.href || siteMatch(site.url, document.location.href)) { + if (site.ignore === IGNORE_FULL) { + return; + } + + _singleInputEnabledForPage = site.usernameOnly; + } + } + } + + const inputs = cipFields.getAllFields(); + if (inputs.length === 0) { + return; + } + + cipFields.prepareVisibleFieldsWithID('select'); + cip.initPasswordGenerator(inputs); + + if (!cipFields.useDefinedCredentialFields()) { + // Get all combinations of username + password fields + cipFields.combinations = cipFields.getAllCombinations(inputs); + } + cipFields.prepareCombinations(cipFields.combinations); + + if (cipFields.combinations.length === 0 && inputs.length === 0) { + browser.runtime.sendMessage({ + action: 'show_default_browseraction' + }); + return; + } + + cip.url = document.location.origin; + cip.submitUrl = cip.getFormActionUrl(cipFields.combinations[0]); + + // Get submitUrl for a single input + if (_singleInputEnabledForPage && !cip.submitUrl && cipFields.combinations.length === 1 && inputs.length === 1) { + cip.submitUrl = cip.getFormActionUrlFromSingleInput(inputs[0]); + } + + if (cip.settings.autoRetrieveCredentials && _called.retrieveCredentials === false && (cip.url && cip.submitUrl)) { + _called.retrieveCredentials = true; + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl ] + }).then(cip.retrieveCredentialsCallback).catch((e) => { + console.log(e); + }); + } else if (_singleInputEnabledForPage) { + cip.preparePageForMultipleCredentials(cip.credentials); + } + }); +}; + +cip.initPasswordGenerator = function(inputs) { + if (cip.settings.usePasswordGenerator) { + cipPassword.init(); + + for (let i = 0; i < inputs.length; i++) { + if (inputs[i] && inputs[i].attr('type') && inputs[i].attr('type').toLowerCase() === 'password') { + cipPassword.initField(inputs[i], inputs, i); + } + } + } +}; + +cip.receiveCredentialsIfNecessary = function() { + return new Promise((resolve, reject) => { + if (cip.credentials.length === 0 && _called.retrieveCredentials === false) { + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl, false, true ] // Sets triggerUnlock to true + }).then((credentials) => { + // If the database was locked, this is scope never met. In these cases the response is met at cip.detectDatabaseChange + _called.manualFillRequested = 'none'; + cip.retrieveCredentialsCallback(credentials, false); + resolve(credentials); + }); + } else { + resolve(cip.credentials); + } + }); +}; + +cip.retrieveCredentialsCallback = function(credentials, dontAutoFillIn) { + if (cipFields.combinations.length > 0) { + cip.u = _f(cipFields.combinations[0].username); + cip.p = _f(cipFields.combinations[0].password); + } + + if (credentials && credentials.length > 0) { + cip.credentials = credentials; + cip.prepareFieldsForCredentials(!Boolean(dontAutoFillIn)); + _called.retrieveCredentials = true; + } +}; + +cip.prepareFieldsForCredentials = function(autoFillInForSingle) { + // Only one login for this site + if (autoFillInForSingle && cip.settings.autoFillSingleEntry && cip.credentials.length === 1) { + let combination = null; + if (!cip.p && !cip.u && cipFields.combinations.length > 0) { + cip.u = _f(cipFields.combinations[0].username); + cip.p = _f(cipFields.combinations[0].password); + combination = cipFields.combinations[0]; + } + if (cip.u) { + cip.setValueWithChange(cip.u, cip.credentials[0].login); + combination = cipFields.getCombination('username', cip.u); + } + if (cip.p) { + cip.setValueWithChange(cip.p, cip.credentials[0].password); + combination = cipFields.getCombination('password', cip.p); + } + + if (combination) { + const list = []; + if (cip.fillInStringFields(combination.fields, cip.credentials[0].stringFields, list)) { + cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); + } + } + + // Generate popup-list of usernames + descriptions + browser.runtime.sendMessage({ + action: 'popup_login', + args: [ [cip.credentials[0].login + ' (' + cip.credentials[0].name + ')'] ] + }); + } else if (cip.credentials.length > 1 || (cip.credentials.length > 0 && (!cip.settings.autoFillSingleEntry || !autoFillInForSingle))) { + // Multiple logins for this site + cip.preparePageForMultipleCredentials(cip.credentials); + } +}; + +cip.preparePageForMultipleCredentials = function(credentials) { + // Add usernames + descriptions to autocomplete-list and popup-list + const usernames = []; + cipAutocomplete.elements = []; + let visibleLogin; + for (let i = 0; i < credentials.length; i++) { + visibleLogin = (credentials[i].login.length > 0) ? credentials[i].login : tr('credentialsNoUsername'); + usernames.push(visibleLogin + ' (' + credentials[i].name + ')'); + const item = { + label: visibleLogin + ' (' + credentials[i].name + ')', + value: credentials[i].login, + loginId: i + }; + cipAutocomplete.elements.push(item); + } + + // Generate popup-list of usernames + descriptions + browser.runtime.sendMessage({ + action: 'popup_login', + args: [ usernames ] + }); + + // Initialize autocomplete for username fields + if (cip.settings.autoCompleteUsernames) { + for (const i of cipFields.combinations) { + // Both username and password fields are visible + if (_detectedFields >= 2) { + if (_f(i.username)) { + cipAutocomplete.init(_f(i.username)); + } + } else if (_detectedFields == 1) { + if (_f(i.username)) { + cipAutocomplete.init(_f(i.username)); + } + if (_f(i.password)) { + cipAutocomplete.init(_f(i.password)); + } + } + } + } +}; + +cip.getFormActionUrl = function(combination) { + if (!combination) { + return null; + } + + const field = _f(combination.password) || _f(combination.username); + + if (field === null) { + return null; + } + + const form = field.closest('form'); + let action = null; + + if (form && form.length > 0) { + action = form[0].action; + } + + if (typeof(action) !== 'string' || action === '') { + action = document.location.origin + document.location.pathname; + } + + return action; +}; + +cip.getFormActionUrlFromSingleInput = function(field) { + if (!field) { + return null; + } + + let action = field.formAction; + + if (typeof(action) !== 'string' || action === '') { + action = document.location.origin + document.location.pathname; + } + + return action; +}; + +cip.fillInCredentials = function(combination, onlyPassword, suppressWarnings) { + const action = cip.getFormActionUrl(combination); + + const u = _f(combination.username); + const p = _f(combination.password); + + if (combination.isNew) { + // Initialize form-submit for remembering credentials + const fieldId = combination.password || combination.username; + const field = _f(fieldId); + if (field) { + const form2 = field.closest('form'); + if (form2 && form2.length > 0) { + cipForm.init(form2, combination); + } + } + } + + if (u) { + cip.u = u; + } + if (p) { + cip.p = p; + } + + if (cip.url === document.location.origin && cip.submitUrl === action && cip.credentials.length > 0) { + cip.fillIn(combination, onlyPassword, suppressWarnings); + } else { + cip.url = document.location.origin; + cip.submitUrl = action; + + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl, false, true ] + }).then((credentials) => { + cip.retrieveCredentialsCallback(credentials, true); + cip.fillIn(combination, onlyPassword, suppressWarnings); + }); + } +}; + +cip.fillInFromActiveElement = function(suppressWarnings, passOnly = false) { + const el = document.activeElement; + if (el.tagName.toLowerCase() !== 'input') { + if (cipFields.combinations.length > 0) { + cip.fillInCredentials(cipFields.combinations[0], false, suppressWarnings); + } + return; + } + + cipFields.setUniqueId(jQuery(el)); + const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); + let combination = null; + if ($(el).attr('type') === 'password') { + combination = cipFields.getCombination('password', fieldId); + } else { + combination = cipFields.getCombination('username', fieldId); + } + + if (passOnly) { + if (!_f(combination.password)) { + const message = tr('fieldsNoPasswordField'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + return; + } + } + + delete combination.loginId; + + cip.fillInCredentials(combination, passOnly, suppressWarnings); +}; + +cip.fillInFromActiveElementTOTPOnly = function(suppressWarnings) { + const el = document.activeElement; + cipFields.setUniqueId(jQuery(el)); + const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); + + browser.runtime.sendMessage({ + action: 'page_get_login_id' + }).then((pos) => { + if (pos >= 0 && cip.credentials[pos]) { + // Check the value from stringFields (to be removed) + const currentField = _fs(fieldId); + if (cip.credentials[pos].stringFields && cip.credentials[pos].stringFields.length > 0) { + const stringFields = cip.credentials[pos].stringFields; + for (const s of stringFields) { + const val = s['KPH: {TOTP}']; + if (val) { + cip.setValue(currentField, val); + } + } + } else if (cip.credentials[pos].totp && cip.credentials[pos].totp.length > 0) { + cip.setValue(currentField, cip.credentials[pos].totp); + } + } + }); +}; + +cip.setValue = function(field, value) { + if (field.is('select')) { + value = value.toLowerCase().trim(); + jQuery('option', field).each(function() { + if (jQuery(this).text().toLowerCase().trim() === value) { + cip.setValueWithChange(field, jQuery(this).val()); + return false; + } + }); + } else { + cip.setValueWithChange(field, value); + field.trigger('input'); + } +}; + +cip.fillInStringFields = function(fields, stringFields, filledInFields) { + let filledIn = false; + + filledInFields.list = []; + if (fields && stringFields && fields.length > 0 && stringFields.length > 0) { + for (let i = 0; i < fields.length; i++) { + const currentField = _fs(fields[i]); + const stringFieldValue = Object.values(stringFields[i]); + if (currentField && stringFieldValue[0]) { + cip.setValue(currentField, stringFieldValue[0]); + filledInFields.list.push(fields[i]); + filledIn = true; + } + } + } + + return filledIn; +}; + +cip.setValueWithChange = function(field, value) { + if (cip.settings.respectMaxLength === true) { + const attributeMaxLength = field.attr('maxlength'); + if (attributeMaxLength && !isNaN(attributeMaxLength) && attributeMaxLength > 0) { + value = value.substr(0, attributeMaxLength); + } + } + + field.val(value); + field[0].dispatchEvent(new Event('input', { 'bubbles': true })); + field[0].dispatchEvent(new Event('change', { 'bubbles': true })); +}; + +cip.fillIn = function(combination, onlyPassword, suppressWarnings) { + // No credentials available + if (cip.credentials.length === 0 && !suppressWarnings) { + const message = tr('credentialsNoLoginsFound'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + return; + } + + const uField = _f(combination.username); + const pField = _f(combination.password); + + // Exactly one pair of credentials available + if (cip.credentials.length === 1) { + let filledIn = false; + if (uField && (!onlyPassword || _singleInputEnabledForPage)) { + cip.setValueWithChange(uField, cip.credentials[0].login); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ 0 ] + }); + filledIn = true; + } + if (pField) { + pField.attr('type', 'password'); + cip.setValueWithChange(pField, cip.credentials[0].password); + pField.data('unchanged', true); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ 0 ] + }); + filledIn = true; + } + + const list = []; + if (cip.fillInStringFields(combination.fields, cip.credentials[0].stringFields, list)) { + cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); + filledIn = true; + } + + if (!filledIn) { + if (!suppressWarnings) { + const message = tr('fieldsFill'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } + } + } else if (combination.loginId !== undefined && cip.credentials[combination.loginId]) { + // Specific login id given + let filledIn = false; + if (uField) { + cip.setValueWithChange(uField, cip.credentials[combination.loginId].login); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ combination.loginId ] + }); + filledIn = true; + } + + if (pField) { + cip.setValueWithChange(pField, cip.credentials[combination.loginId].password); + pField.data('unchanged', true); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ combination.loginId ] + }); + filledIn = true; + } + + let list = []; + if (cip.fillInStringFields(combination.fields, cip.credentials[combination.loginId].stringFields, list)) { + cipForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); + filledIn = true; + } + + if (!filledIn) { + if (!suppressWarnings) { + const message = tr('fieldsFill'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } + } + } else { // Multiple credentials available + // Check if only one password for given username exists + let countPasswords = 0; + + if (uField) { + let valPassword = ''; + let valUsername = ''; + let valStringFields = []; + const valQueryUsername = uField.val().toLowerCase(); + + // Find passwords to given username (even those with empty username) + for (const c of cip.credentials) { + if (c.login.toLowerCase() === valQueryUsername) { + countPasswords += 1; + valPassword = c.password; + valUsername = c.login; + valStringFields = c.stringFields; + } + } + + // For the correct notification message: 0 = no logins, X > 1 = too many logins + if (countPasswords === 0) { + countPasswords = cip.credentials.length; + } + + // Only one mapping username found + if (countPasswords === 1) { + if (!onlyPassword) { + cip.setValueWithChange(uField, valUsername); + } + + if (pField) { + cip.setValueWithChange(pField, valPassword); + pField.data('unchanged', true); + } + + let list = []; + if (cip.fillInStringFields(combination.fields, valStringFields, list)) { + cipForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); + } + } + + // User has to select correct credentials by himself + if (countPasswords > 1) { + if (!suppressWarnings) { + const $target = onlyPassword ? pField : uField; + cipAutocomplete.init($target); + $target.focus(); + jQuery($target).autocomplete('search', jQuery($target).val()); + } + } else if (countPasswords < 1) { + if (!suppressWarnings) { + const message = tr('credentialsNoUsernameFound'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } + } + } else { + if (!suppressWarnings) { + const $target = onlyPassword ? pField : uField; + cipAutocomplete.init($target); + $target.focus(); + jQuery($target).autocomplete('search', jQuery($target).val()); + } + } + } +}; + +cip.contextMenuRememberCredentials = function() { + const el = document.activeElement; + if (el.tagName.toLowerCase() !== 'input') { + return; + } + + cipFields.setUniqueId(jQuery(el)); + const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); + let combination = null; + if ($(el).attr('type') === 'password') { + combination = cipFields.getCombination('password', fieldId); + } else { + combination = cipFields.getCombination('username', fieldId); + } + + let usernameValue = ''; + let passwordValue = ''; + + const usernameField = _f(combination.username); + const passwordField = _f(combination.password); + + if (usernameField) { + usernameValue = usernameField.val(); + } + if (passwordField) { + passwordValue = passwordField.val(); + } + + if (!cip.rememberCredentials(usernameValue, passwordValue)) { + const message = tr('rememberNothingChanged'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } +}; + +cip.rememberCredentials = function(usernameValue, passwordValue) { + // No password given or field cleaned by a site-running script + // --> no password to save + if (passwordValue === '') { + return false; + } + + let usernameExists = false; + let nothingChanged = false; + + for (const c of cip.credentials) { + if (c.login === usernameValue && c.password === passwordValue) { + nothingChanged = true; + break; + } + + if (c.login === usernameValue) { + usernameExists = true; + } + } + + if (!nothingChanged) { + if (!usernameExists) { + for (const c of cip.credentials) { + if (c.login === usernameValue) { + usernameExists = true; + break; + } + } + } + const credentialsList = []; + for (const c of cip.credentials) { + credentialsList.push({ + login: c.login, + name: c.name, + uuid: c.uuid + }); + } + + let url = jQuery(this)[0].action; + if (!url) { + url = cip.getDocumentLocation(); + if (url.indexOf('?') > 0) { + url = url.substring(0, url.indexOf('?')); + if (url.length < document.location.origin.length) { + url = document.location.origin; + } + } + } + + browser.runtime.sendMessage({ + action: 'set_remember_credentials', + args: [ usernameValue, passwordValue, url, usernameExists, credentialsList ] + }); + + return true; + } + + return false; +}; + +cip.ignoreSite = function(sites) { + if (!sites || sites.length === 0) { + return; + } + + let site = sites[0]; + cip.initializeSitePreferences(); + + if (slashNeededForUrl(site)) { + site += '/'; + } + + // Check if the site already exists + let siteExists = false; + for (const existingSite of cip.settings['sitePreferences']) { + if (existingSite.url === site) { + existingSite.ignore = IGNORE_NORMAL; + siteExists = true; + } + } + + if (!siteExists) { + cip.settings['sitePreferences'].push({ + url: site, + ignore: IGNORE_NORMAL, + usernameOnly: false + }); + } + + browser.runtime.sendMessage({ + action: 'save_settings', + args: [ cip.settings ] + }); +}; + +// Delete previously created Object if it exists. It will be replaced by an Array +cip.initializeSitePreferences = function() { + if (cip.settings['sitePreferences'] !== undefined && cip.settings['sitePreferences'].constructor === Object) { + delete cip.settings['sitePreferences']; + } + + if (!cip.settings['sitePreferences']) { + cip.settings['sitePreferences'] = []; + } +}; + +cip.getDocumentLocation = function() { + return cip.settings.saveDomainOnly ? document.location.origin : document.location.href; +}; + +var cipEvents = {}; + +cipEvents.clearCredentials = function() { + cip.credentials = []; + cipAutocomplete.elements = []; + _called.retrieveCredentials = false; + + if (cip.settings.autoCompleteUsernames) { + for (const c of cipFields.combinations) { + const uField = _f(c.username); + if (uField) { + if (uField.hasClass('ui-autocomplete-input')) { + uField.autocomplete('destroy'); + } + } + } + } +}; + +cipEvents.triggerActivatedTab = function() { + // Doesn't run a second time because of _called.initCredentialFields set to true + cip.init(); + $(this.target).find('input').autocomplete(); + + // InitCredentialFields calls also "retrieve_credentials", to prevent it + // check of init() was already called + if (_called.initCredentialFields && (cip.url && cip.submitUrl) && cip.settings.autoRetrieveCredentials) { + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl ] + }).then(cip.retrieveCredentialsCallback).catch((e) => { + console.log(e); + }); + } +}; diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index 0ac3cc5..a96ba4a 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -61,21 +61,21 @@ } ], "commands": { - "fill-username-password": { + "fill_username_password": { "description": "__MSG_contextMenuFillUsernameAndPassword__", "suggested_key": { "default": "Alt+Shift+U", "mac": "MacCtrl+Shift+U" } }, - "fill-password": { + "fill_password": { "description": "__MSG_contextMenuFillPassword__", "suggested_key": { "default": "Alt+Shift+I", "mac": "MacCtrl+Shift+I" } }, - "fill-totp": { + "fill_totp": { "description": "__MSG_contextMenuFillTOTP__", "suggested_key": { "default": "Alt+Shift+T", diff --git a/keepassxc-browser/options/options.html b/keepassxc-browser/options/options.html index 389d259..e364619 100644 --- a/keepassxc-browser/options/options.html +++ b/keepassxc-browser/options/options.html @@ -344,7 +344,7 @@ × - + diff --git a/keepassxc-browser/options/options.js b/keepassxc-browser/options/options.js index 65bf2e9..69a5a56 100644 --- a/keepassxc-browser/options/options.js +++ b/keepassxc-browser/options/options.js @@ -41,7 +41,7 @@ options.initMenu = function() { options.saveSettingsPromise = function() { return new Promise((resolve, reject) => { - browser.storage.local.set({'settings': options.settings}).then((item) => { + browser.storage.local.set({ 'settings': options.settings }).then((item) => { browser.runtime.sendMessage({ action: 'load_settings' }).then((settings) => { @@ -56,21 +56,21 @@ options.saveSetting = function(name) { $(id).closest('.control-group').removeClass('error').addClass('success'); setTimeout(() => { $(id).closest('.control-group').removeClass('success'); }, 2500); - browser.storage.local.set({'settings': options.settings}); + browser.storage.local.set({ 'settings': options.settings }); browser.runtime.sendMessage({ action: 'load_settings' }); }; options.saveSettings = function() { - browser.storage.local.set({'settings': options.settings}); + browser.storage.local.set({ 'settings': options.settings }); browser.runtime.sendMessage({ action: 'load_settings' }); }; options.saveKeyRing = function() { - browser.storage.local.set({'keyRing': options.keyRing}); + browser.storage.local.set({ 'keyRing': options.keyRing }); browser.runtime.sendMessage({ action: 'load_keyring' }); @@ -86,7 +86,7 @@ options.initGeneralSettings = function() { options.settings[name] = $(this).is(':checked'); options.saveSettingsPromise().then((x) => { if (name === 'autoFillAndSend') { - browser.runtime.sendMessage({action: 'init_http_auth'}); + browser.runtime.sendMessage({ action: 'init_http_auth' }); } }); }); @@ -128,11 +128,11 @@ options.initGeneralSettings = function() { $('#configureCommands').click(function() { browser.tabs.create({ - url: isFirefox() ? browser.runtime.getURL("options/shortcuts.html") : 'chrome://extensions/configureCommands' + url: isFirefox() ? browser.runtime.getURL('options/shortcuts.html') : 'chrome://extensions/configureCommands' }); }); - $('#blinkTimeoutButton').click(function(){ + $('#blinkTimeoutButton').click(function() { const blinkTimeout = $.trim($('#blinkTimeout').val()); const blinkTimeoutval = blinkTimeout !== '' ? Number(blinkTimeout) : defaultSettings.blinkTimeout; @@ -140,7 +140,7 @@ options.initGeneralSettings = function() { options.saveSetting('blinkTimeout'); }); - $('#blinkMinTimeoutButton').click(function(){ + $('#blinkMinTimeoutButton').click(function() { const blinkMinTimeout = $.trim($('#blinkMinTimeout').val()); const blinkMinTimeoutval = blinkMinTimeout !== '' ? Number(blinkMinTimeout) : defaultSettings.redirectOffset; @@ -148,7 +148,7 @@ options.initGeneralSettings = function() { options.saveSetting('blinkMinTimeout'); }); - $('#allowedRedirectButton').click(function(){ + $('#allowedRedirectButton').click(function() { const allowedRedirect = $.trim($('#allowedRedirect').val()); const allowedRedirectval = allowedRedirect !== '' ? Number(allowedRedirect) : defaultSettings.redirectAllowance; @@ -175,7 +175,7 @@ options.getPartiallyHiddenKey = function(key) { }; options.initConnectedDatabases = function() { - $('#dialogDeleteConnectedDatabase').modal({keyboard: true, show: false, backdrop: true}); + $('#dialogDeleteConnectedDatabase').modal({ keyboard: true, show: false, backdrop: true }); $('#tab-connected-databases tr.clone:first button.delete:first').click(function(e) { e.preventDefault(); $('#dialogDeleteConnectedDatabase').data('hash', $(this).closest('tr').data('hash')); @@ -233,7 +233,7 @@ options.initConnectedDatabases = function() { }; options.initCustomCredentialFields = function() { - $('#dialogDeleteCustomCredentialFields').modal({keyboard: true, show: false, backdrop: true}); + $('#dialogDeleteCustomCredentialFields').modal({ keyboard: true, show: false, backdrop: true }); $('#tab-custom-fields tr.clone:first button.delete:first').click(function(e) { e.preventDefault(); $('#dialogDeleteCustomCredentialFields').data('url', $(this).closest('tr').data('url')); @@ -262,7 +262,7 @@ options.initCustomCredentialFields = function() { const trClone = $('#tab-custom-fields table tr.clone:first').clone(true); trClone.removeClass('clone'); let counter = 1; - for (let url in options.settings['defined-custom-fields']) { + for (const url in options.settings['defined-custom-fields']) { const tr = trClone.clone(true); tr.data('url', url); tr.attr('id', 'tr-scf' + counter); @@ -280,7 +280,7 @@ options.initCustomCredentialFields = function() { }; options.initSitePreferences = function() { - $('#dialogDeleteSite').modal({keyboard: true, show: false, backdrop: true}); + $('#dialogDeleteSite').modal({ keyboard: true, show: false, backdrop: true }); $('#tab-site-preferences tr.clone:first button.delete:first').click(function(e) { e.preventDefault(); $('#dialogDeleteSite').data('url', $(this).closest('tr').data('url')); @@ -291,7 +291,7 @@ options.initSitePreferences = function() { $('#tab-site-preferences tr.clone:first input[type=checkbox]:first').change(function() { const url = $(this).closest('tr').data('url'); - for (let site of options.settings['sitePreferences']) { + for (const site of options.settings['sitePreferences']) { if (site.url === url) { site.usernameOnly = $(this).is(':checked'); } @@ -301,7 +301,7 @@ options.initSitePreferences = function() { $('#tab-site-preferences tr.clone:first select:first').change(function() { const url = $(this).closest('tr').data('url'); - for (let site of options.settings['sitePreferences']) { + for (const site of options.settings['sitePreferences']) { if (site.url === url) { site.ignore = $(this).val(); } @@ -309,9 +309,9 @@ options.initSitePreferences = function() { options.saveSettings(); }); - $("#manualUrl").keyup(function(event) { + $('#manualUrl').keyup(function(event) { if (event.keyCode === 13) { - $("#sitePreferencesManualAdd").click(); + $('#sitePreferencesManualAdd').click(); } }); @@ -340,7 +340,7 @@ options.initSitePreferences = function() { $('#tab-site-preferences table tbody:first').append(tr); $('#tab-site-preferences table tbody:first tr.empty:first').hide(); - options.settings['sitePreferences'].push({url: value, ignore: IGNORE_NOTHING, usernameOnly: false}); + options.settings['sitePreferences'].push({ url: value, ignore: IGNORE_NOTHING, usernameOnly: false }); options.saveSettings(); $('#manualUrl').val(''); @@ -371,7 +371,7 @@ options.initSitePreferences = function() { const trClone = $('#tab-site-preferences table tr.clone:first').clone(true); trClone.removeClass('clone'); let counter = 1; - if (options.settings['sitePreferences']){ + if (options.settings['sitePreferences']) { for (let site of options.settings['sitePreferences']) { const tr = trClone.clone(true); tr.data('url', site.url); @@ -396,7 +396,7 @@ options.initAbout = function() { $('#tab-about em.versionCIP').text(browser.runtime.getManifest().version); // Hides keyboard shortcut configure button if Firefox version is < 60 (API is not compatible) - if (isFirefox() && Number(navigator.userAgent.substr(navigator.userAgent.lastIndexOf('/')+1, 2)) < 60) { + if (isFirefox() && Number(navigator.userAgent.substr(navigator.userAgent.lastIndexOf('/') + 1, 2)) < 60) { $('#chrome-only').remove(); } }; diff --git a/keepassxc-browser/options/shortcuts.js b/keepassxc-browser/options/shortcuts.js index e37dce1..3ac8519 100644 --- a/keepassxc-browser/options/shortcuts.js +++ b/keepassxc-browser/options/shortcuts.js @@ -10,14 +10,14 @@ document.querySelectorAll('input').forEach((b) => { const saveButtons = document.querySelectorAll('.btn-primary'); for (const b of saveButtons) { - b.addEventListener('click', e => { + b.addEventListener('click', (e) => { updateShortcut(b.parentElement.children[1].getAttribute('id')) }); } const resetButtons = document.querySelectorAll('.btn-danger'); for (const b of resetButtons) { - b.addEventListener('click', e => { + b.addEventListener('click', (e) => { resetShortcut(b.parentElement.children[1].getAttribute('id')) }); } @@ -69,12 +69,12 @@ async function updateKeys() { async function updateShortcut(shortcut) { try { - await browser.commands.update({ + await browser.commands.update({ name: shortcut, shortcut: document.querySelector('#' + shortcut).value }); createBanner('success', shortcut); - } catch(e) { + } catch (e) { console.log('Cannot change shortcut: ' + e); createBanner('danger', shortcut); } @@ -105,7 +105,7 @@ function createBanner(type, shortcut) { } else { return; } - + document.body.appendChild(banner); // Destroy the banner after five seconds diff --git a/keepassxc-browser/popups/popup.js b/keepassxc-browser/popups/popup.js index a22b3b3..1447778 100644 --- a/keepassxc-browser/popups/popup.js +++ b/keepassxc-browser/popups/popup.js @@ -1,6 +1,6 @@ 'use strict'; -function status_response(r) { +function statusResponse(r) { $('#initial-state').hide(); $('#error-encountered').hide(); $('#need-reconfigure').hide(); @@ -12,27 +12,21 @@ function status_response(r) { if (!r.keePassXCAvailable) { $('#error-message').html(r.error); $('#error-encountered').show(); - } - else if (r.keePassXCAvailable && r.databaseClosed) { + } else if (r.keePassXCAvailable && r.databaseClosed) { $('#database-error-message').html(r.error); $('#database-not-opened').show(); - } - else if (!r.configured) { + } else if (!r.configured) { $('#not-configured').show(); - } - else if (r.encryptionKeyUnrecognized) { + } else if (r.encryptionKeyUnrecognized) { $('#need-reconfigure').show(); $('#need-reconfigure-message').html(r.error); - } - else if (!r.associated) { + } else if (!r.associated) { $('#need-reconfigure').show(); $('#need-reconfigure-message').html(r.error); - } - else if (r.error !== null) { + } else if (r.error !== null) { $('#error-encountered').show(); $('#error-message').html(r.error); - } - else { + } else { $('#configured-and-associated').show(); $('#associated-identifier').html(r.identifier); $('#lock-database-button').show(); @@ -57,22 +51,22 @@ $(function() { $('#reload-status-button').click(function() { browser.runtime.sendMessage({ action: 'reconnect' - }).then(status_response); + }).then(statusResponse); }); $('#reopen-database-button').click(function() { browser.runtime.sendMessage({ action: 'get_status', args: [ false, true ] // Set forcePopup to true - }).then(status_response); + }).then(statusResponse); }); $('#redetect-fields-button').click(function() { - browser.tabs.query({"active": true, "currentWindow": true}).then(function(tabs) { + browser.tabs.query({ 'active': true, 'currentWindow': true }).then(function(tabs) { if (tabs.length === 0) { return; // For example: only the background devtools or a popup are opened } - let tab = tabs[0]; + const tab = tabs[0]; browser.tabs.sendMessage(tab.id, { action: 'redetect_fields' @@ -83,10 +77,10 @@ $(function() { $('#lock-database-button').click(function() { browser.runtime.sendMessage({ action: 'lock-database' - }).then(status_response); + }).then(statusResponse); }); browser.runtime.sendMessage({ - action: "get_status" - }).then(status_response); + action: 'get_status' + }).then(statusResponse); }); diff --git a/keepassxc-browser/popups/popup_httpauth.js b/keepassxc-browser/popups/popup_httpauth.js index 8ff0e13..04732cd 100644 --- a/keepassxc-browser/popups/popup_httpauth.js +++ b/keepassxc-browser/popups/popup_httpauth.js @@ -3,7 +3,7 @@ const getLoginData = function() { return new Promise((resolve, reject) => { browser.runtime.getBackgroundPage().then((global) => { - browser.tabs.query({'active': true, 'currentWindow': true}).then((tabs) => { + browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => { resolve(global.page.tabs[tabs[0].id].loginList); }); }); @@ -12,13 +12,13 @@ const getLoginData = function() { $(function() { getLoginData().then((data) => { - let ll = document.getElementById('login-list'); + const ll = document.getElementById('login-list'); for (let i = 0; i < data.logins.length; ++i) { const a = document.createElement('a'); a.setAttribute('class', 'list-group-item'); - a.textContent = data.logins[i].login + " (" + data.logins[i].name + ")"; + a.textContent = data.logins[i].login + ' (' + data.logins[i].name + ')'; $(a).data('creds', data.logins[i]); - $(a).click(function () { + $(a).click(function() { if (data.resolve) { const creds = $(this).data('creds'); data.resolve({ @@ -37,8 +37,8 @@ $(function() { $('#lock-database-button').click(function() { browser.runtime.sendMessage({ action: 'lock-database' - }).then(status_response); - }); + }).then(statusResponse); + }); $('#btn-dismiss').click(function() { getLoginData().then((data) => { diff --git a/keepassxc-browser/popups/popup_login.js b/keepassxc-browser/popups/popup_login.js index adc3d36..3412237 100644 --- a/keepassxc-browser/popups/popup_login.js +++ b/keepassxc-browser/popups/popup_login.js @@ -2,14 +2,14 @@ $(function() { browser.runtime.getBackgroundPage().then((global) => { - browser.tabs.query({'active': true, 'currentWindow': true}).then((tabs) => { + browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => { if (tabs.length === 0) { return; // For example: only the background devtools or a popup are opened } const tab = tabs[0]; const logins = global.page.tabs[tab.id].loginList; - let ll = document.getElementById('login-list'); + const ll = document.getElementById('login-list'); for (let i = 0; i < logins.length; i++) { const a = document.createElement('a'); a.textContent = logins[i]; @@ -25,17 +25,17 @@ $(function() { }); ll.appendChild(a); } - + if (logins.length > 1) { document.getElementById('filter-block').style = ''; - let filter = document.getElementById('login-filter'); + const filter = document.getElementById('login-filter'); filter.addEventListener('keyup', (e) => { - let val = filter.value; - let re = new RegExp(val, 'i'); - let links = ll.getElementsByTagName('a'); - for (let i in links) { + const val = filter.value; + const re = new RegExp(val, 'i'); + const links = ll.getElementsByTagName('a'); + for (const i in links) { if (links.hasOwnProperty(i)) { - let found = String(links[i].textContent).match(re) !== null; + const found = String(links[i].textContent).match(re) !== null; links[i].style = found ? '' : 'display: none;'; } } @@ -57,7 +57,7 @@ $(function() { $('#reopen-database-button').click(function() { browser.runtime.sendMessage({ action: 'get_status', - args: [ false, true ] // Set forcePopup to true + args: [ false, true ] // Set forcePopup to true }); }); }); diff --git a/keepassxc-browser/popups/popup_remember.js b/keepassxc-browser/popups/popup_remember.js index f189205..5131ef8 100644 --- a/keepassxc-browser/popups/popup_remember.js +++ b/keepassxc-browser/popups/popup_remember.js @@ -37,7 +37,7 @@ function _initialize(tab) { e.preventDefault(); // Only one entry which could be updated - if(_tab.credentials.list.length === 1) { + if (_tab.credentials.list.length === 1) { // Use the current username if it's empty if (!_tab.credentials.username) { _tab.credentials.username = _tab.credentials.list[0].login; @@ -47,22 +47,20 @@ function _initialize(tab) { action: 'update_credentials', args: [_tab.credentials.list[0].uuid, _tab.credentials.username, _tab.credentials.password, _tab.credentials.url] }).then(_verifyResult); - } - else { + } else { $('.credentials:first .username-new:first strong:first').text(_tab.credentials.username); $('.credentials:first .username-exists:first strong:first').text(_tab.credentials.username); if (_tab.credentials.usernameExists) { $('.credentials:first .username-new:first').hide(); $('.credentials:first .username-exists:first').show(); - } - else { + } else { $('.credentials:first .username-new:first').show(); $('.credentials:first .username-exists:first').hide(); } for (let i = 0; i < _tab.credentials.list.length; i++) { - let $a = $('') + const $a = $('') .attr('href', '#') .text(_tab.credentials.list[i].login + ' (' + _tab.credentials.list[i].name + ')') .data('entryId', i) @@ -84,7 +82,7 @@ function _initialize(tab) { _verifyResult('error'); return; } - + // Show a notification if the user tries to update credentials using the old password if (credentials[entryId].password === _tab.credentials.password) { showNotification('Error: Credentials not updated. The password has not been changed.'); @@ -122,8 +120,8 @@ function _initialize(tab) { const tab = tabs[0]; browser.runtime.getBackgroundPage().then((global) => { browser.tabs.sendMessage(tab.id, { - action: 'ignore-site', - args: [_tab.credentials.url] + action: 'ignore_site', + args: [ _tab.credentials.url ] }); _close(); }); @@ -132,12 +130,11 @@ function _initialize(tab) { }); } -function _connected_database(db) { +function _connectedDatabase(db) { if (db.count > 1 && db.identifier) { $('.connected-database:first em:first').text(db.identifier); $('.connected-database:first').show(); - } - else { + } else { $('.connected-database:first').hide(); } } @@ -164,7 +161,7 @@ function _close() { $(function() { browser.runtime.sendMessage({ action: 'stack_add', - args: ['icon_remember_red_background_19x19.png', 'popup_remember.html', 10, true, 0] + args: [ 'icon_remember_red_background_19x19.png', 'popup_remember.html', 10, true, 0 ] }); browser.runtime.sendMessage({ @@ -173,5 +170,5 @@ $(function() { browser.runtime.sendMessage({ action: 'get_connected_database' - }).then(_connected_database); + }).then(_connectedDatabase); }); diff --git a/keepassxc-browser/translate.js b/keepassxc-browser/translate.js index 69c25e8..e8713dc 100644 --- a/keepassxc-browser/translate.js +++ b/keepassxc-browser/translate.js @@ -1,7 +1,7 @@ -'use strict' +'use strict'; const items = document.querySelectorAll('[data-i18n]'); -for (let item of items) { +for (const item of items) { const key = item.getAttribute('data-i18n'); if (key) { const placeholder = item.getAttribute('i18n-placeholder');
').html(tr('defineAlreadySelected') + ''); + const $btnDiscard = jQuery('') + .attr('id', 'btn-warning') + .text(tr('defineDiscard')) + .css('margin-top', '5px') + .addClass('btn') + .addClass('btn-sm') + .addClass('btn-danger') + .click(function(e) { + delete cip.settings['defined-custom-fields'][location]; + + browser.runtime.sendMessage({ + action: 'save_settings', + args: [ cip.settings ] + }); + + browser.runtime.sendMessage({ + action: 'load_settings' + }); + + jQuery(this).parent('p').remove(); + }); + $p.append($btnDiscard); + $description.append($p); + } + + jQuery('div#b2c-cipDefine-description').draggable(); +}; + +cipDefine.resetSelection = function() { + cipDefine.selection = { + username: null, + password: null, + fields: [] + }; +}; + +cipDefine.isFieldSelected = function($cipId) { + return ( + $cipId === cipDefine.selection.username || + $cipId === cipDefine.selection.password || + $cipId in cipDefine.selection.fields + ); +}; + +cipDefine.markAllUsernameFields = function($chooser) { + cipDefine.eventFieldClick = function(e) { + cipDefine.selection.username = jQuery(this).data('cip-id'); + jQuery(this).addClass('b2c-fixed-username-field').text(tr('username')).unbind('click'); + cipDefine.prepareStep2(); + cipDefine.markAllPasswordFields(jQuery('#b2c-cipDefine-fields')); + }; + cipDefine.markFields($chooser, cipFields.inputQueryPattern); +}; + +cipDefine.markAllPasswordFields = function($chooser, more) { + cipDefine.eventFieldClick = function(e) { + cipDefine.selection.password = jQuery(this).data('cip-id'); + jQuery(this).addClass('b2c-fixed-password-field').text(tr('password')).unbind('click'); + cipDefine.prepareStep3(); + cipDefine.markAllStringFields(jQuery('#b2c-cipDefine-fields')); + }; + if (more) { + cipDefine.markFields($chooser, cipFields.inputQueryPattern); + } else { + cipDefine.markFields($chooser, 'input[type=\'password\']'); + } +}; + +cipDefine.markAllStringFields = function($chooser) { + cipDefine.eventFieldClick = function(e) { + cipDefine.selection.fields[jQuery(this).data('cip-id')] = true; + const count = Object.keys(cipDefine.selection.fields).length; + jQuery(this).addClass('b2c-fixed-string-field').text(tr('defineStringField') + String(count)).unbind('click'); + jQuery('button#b2c-btn-confirm:first').addClass('b2c-btn-primary').attr('disabled', false); + }; + cipDefine.markFields($chooser, cipFields.inputQueryPattern + ', select'); +}; + +cipDefine.markFields = function($chooser, $pattern) { + jQuery($pattern).each(function() { + if (cipDefine.isFieldSelected(jQuery(this).data('cip-id'))) { + return true; + } + + if (cipFields.isVisible(this)) { + const $field = jQuery('').addClass('b2c-fixed-field') + .css('top', jQuery(this).offset().top) + .css('left', jQuery(this).offset().left) + .css('width', jQuery(this).outerWidth()) + .css('height', jQuery(this).outerHeight()) + .attr('data-cip-id', jQuery(this).attr('data-cip-id')) + .click(cipDefine.eventFieldClick) + .hover(function() {jQuery(this).addClass('b2c-fixed-hover-field');}, function() {jQuery(this).removeClass('b2c-fixed-hover-field');}); + $chooser.append($field); + } + }); +}; + +cipDefine.prepareStep1 = function() { + jQuery('div#b2c-help').text('').css('margin-bottom', 0); + jQuery('div#b2c-cipDefine-fields').removeData('username'); + jQuery('div#b2c-cipDefine-fields').removeData('password'); + jQuery('div.b2c-fixed-field', jQuery('div#b2c-cipDefine-fields')).remove(); + jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineChooseUsername')); + jQuery('button#b2c-btn-skip:first').data('step', '1').show(); + jQuery('button#b2c-btn-confirm:first').hide(); + jQuery('button#b2c-btn-again:first').hide(); + jQuery('button#b2c-btn-more:first').hide(); +}; + +cipDefine.prepareStep2 = function() { + jQuery('div#b2c-help').text('').css('margin-bottom', 0); + jQuery('div.b2c-fixed-field:not(.b2c-fixed-username-field)', jQuery('div#b2c-cipDefine-fields')).remove(); + jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineChoosePassword')); + jQuery('button#b2c-btn-skip:first').data('step', '2'); + jQuery('button#b2c-btn-again:first').show(); + jQuery('button#b2c-btn-more:first').show(); +}; + +cipDefine.prepareStep3 = function() { + if (!cipDefine.selection.username && !cipDefine.selection.password) { + jQuery('button#b2c-btn-confirm:first').removeClass('b2c-btn-primary').attr('disabled', true); + } + + jQuery('div#b2c-help').html(tr('defineHelpText')).css('margin-bottom', '5px'); + jQuery('div.b2c-fixed-field:not(.b2c-fixed-password-field,.b2c-fixed-username-field)', jQuery('div#b2c-cipDefine-fields')).remove(); + jQuery('button#b2c-btn-confirm:first').show(); + jQuery('button#b2c-btn-skip:first').data('step', '3').hide(); + jQuery('button#b2c-btn-more:first').hide(); + jQuery('div:first', jQuery('div#b2c-cipDefine-description')).text(tr('defineConfirmSelection')); +}; + + + +var cipFields = {}; + +cipFields.inputQueryPattern = 'input[type=\'text\'], input[type=\'email\'], input[type=\'password\'], input[type=\'tel\'], input[type=\'number\'], input:not([type])'; +// Unique number as new IDs for input fields +cipFields.uniqueNumber = 342845638; +// Objects with combination of username + password fields +cipFields.combinations = []; + +cipFields.setUniqueId = function(field) { + if (field && !field.attr('data-cip-id')) { + // Use ID of field if it is unique + const fieldId = field.attr('id'); + if (fieldId) { + const foundIds = jQuery('input#' + cipFields.prepareId(fieldId)); + if (foundIds.length === 1) { + field.attr('data-cip-id', fieldId); + return; + } + } + + // Create own ID if no ID is set for this field + cipFields.uniqueNumber += 1; + field.attr('data-cip-id', 'jQuery' + String(cipFields.uniqueNumber)); + } +}; + +cipFields.prepareId = function(id) { + return id.replace(/[:#.,\[\]\(\)' "]/g, function(m) { return '\\' + m; }); +}; + +/** + * Returns the first parent element satifying the {@code predicate} mapped by {@code resultFn} or else {@code defaultVal}. + * @param {HTMLElement} element The start element (excluded, starting with the parents) + * @param {function} predicate Matcher for the element to find, type (HTMLElement) => boolean + * @param {function} resultFn Callback function of type (HTMLElement) => {*} called for the first matching element + * @param {fun} defaultValFn Fallback return value supplier, if no element matching the predicate can be found + */ +cipFields.traverseParents = function(element, predicate, resultFn = () => true, defaultValFn = () => false) { + for (let f = element.parentElement; f !== null; f = f.parentElement) { + if (predicate(f)) { + return resultFn(f); + } + } + return defaultValFn(); +}; + +cipFields.getOverflowHidden = function(field) { + return cipFields.traverseParents(field, f => f.style.overflow === 'hidden'); +}; + + +// Checks if input field is a search field. Attributes or form action containing 'search', or parent element holding +// role="search" will be identified as a search field. +cipFields.isSearchField = function(target) { + const attributes = target.attributes; + + // Check element attributes + for (const attr of attributes) { + if ((attr.value && (attr.value.toLowerCase().includes('search')) || attr.value === 'q')) { + return true; + } + } + + // Check closest form + const closestForm = target.closest('form'); + if (closestForm) { + // Check form action + const formAction = closestForm.getAttribute('action'); + if (formAction && (formAction.toLowerCase().includes('search') && + !formAction.toLowerCase().includes('research'))) { + return true; + } + + // Check form class and id + const closestFormId = closestForm.getAttribute('id'); + const closestFormClass = closestForm.className; + if (closestFormClass && (closestForm.className.toLowerCase().includes('search') || + (closestFormId && closestFormId.toLowerCase().includes('search') && !closestFormId.toLowerCase().includes('research')))) { + return true; + } + } + + // Check parent elements for role="search" + const roleFunc = f => f.getAttribute('role'); + const roleValue = cipFields.traverseParents(target, roleFunc, roleFunc, () => null); + if (roleValue && roleValue === 'search') { + return true; + } + + return false; +}; + +cipFields.isVisible = function(field) { + const rect = field.getBoundingClientRect(); + + // Check CSS visibility + const fieldStyle = getComputedStyle(field); + if (fieldStyle.visibility && (fieldStyle.visibility === 'hidden' || fieldStyle.visibility === 'collapse')) { + return false; + } + + // Check element position and size + if (rect.x < 0 || rect.y < 0 || rect.width < 8 || rect.height < 8) { + return false; + } + + return true; +}; + +cipFields.getAllFields = function() { + const fields = []; + const inputs = cipObserverHelper.getInputs(document); + for (const i of inputs) { + if (cipFields.isVisible(i) && !cipFields.isSearchField(i)) { + cipFields.setUniqueId(jQuery(i)); + fields.push(jQuery(i)); + } + }; + + _detectedFields = fields.length; + return fields; +}; + +cipFields.prepareVisibleFieldsWithID = function($pattern) { + jQuery($pattern).each(function() { + if (cipFields.isVisible(this) && !cipFields.isSearchField(this)) { + cipFields.setUniqueId(jQuery(this)); + } + }); +}; + +cipFields.getAllCombinations = function(inputs) { + const fields = []; + let uField = null; + + for (const i of inputs) { + if (i) { + if (i.attr('type') && i.attr('type').toLowerCase() === 'password') { + const uId = (!uField || uField.length < 1) ? null : cipFields.prepareId(uField.attr('data-cip-id')); + + const combination = { + username: uId, + password: cipFields.prepareId(i.attr('data-cip-id')) + }; + fields.push(combination); + + // Reset selected username field + uField = null; + } else { + // Username field + uField = i; + } + } + } + + if (_singleInputEnabledForPage && fields.length === 0 && uField) { + const combination = { + username: uField[0].getAttribute('data-cip-id'), + password: null + }; + fields.push(combination); + } + + return fields; +}; + +cipFields.getCombination = function(givenType, fieldId) { + if (cipFields.combinations.length === 0) { + if (cipFields.useDefinedCredentialFields()) { + return cipFields.combinations[0]; + } + } + // Use defined credential fields (already loaded into combinations) + const location = cip.getDocumentLocation(); + if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { + return cipFields.combinations[0]; + } + + for (let c of cipFields.combinations) { + if (c[givenType] === fieldId) { + return c; + } + } + + // Find new combination + let combination = { + username: null, + password: null + }; + + let newCombi = false; + if (givenType === 'username') { + const passwordField = cipFields.getPasswordField(fieldId, true); + let passwordId = null; + if (passwordField && passwordField.length > 0) { + passwordId = cipFields.prepareId(passwordField.attr('data-cip-id')); + } + combination = { + username: fieldId, + password: passwordId + }; + newCombi = true; + } else if (givenType === 'password') { + const usernameField = cipFields.getUsernameField(fieldId, true); + let usernameId = null; + if (usernameField && usernameField.length > 0) { + usernameId = cipFields.prepareId(usernameField.attr('data-cip-id')); + } + combination = { + username: usernameId, + password: fieldId + }; + newCombi = true; + } + + if (combination.username || combination.password) { + cipFields.combinations.push(combination); + } + + if (combination.username) { + if (cip.credentials.length > 0) { + cip.preparePageForMultipleCredentials(cip.credentials); + } + } + + if (newCombi) { + combination.isNew = true; + } + return combination; +}; + +/** +* Return the username field or null if it not exists +*/ +cipFields.getUsernameField = function(passwordId, checkDisabled) { + const passwordField = _f(passwordId); + if (!passwordField) { + return null; + } + + const form = passwordField.closest('form')[0]; + let usernameField = null; + + // Search all inputs on this one form + if (form) { + jQuery(cipFields.inputQueryPattern, form).each(function() { + cipFields.setUniqueId(jQuery(this)); + if (jQuery(this).attr('data-cip-id') === passwordId) { + return false; // Break + } + + if (jQuery(this).attr('type') && jQuery(this).attr('type').toLowerCase() === 'password') { + return true; // Continue + } + + usernameField = jQuery(this); + }); + } else { + // Search all inputs on page + const inputs = cipFields.getAllFields(); + cip.initPasswordGenerator(inputs); + for (const i of inputs) { + if (i.attr('data-cip-id') === passwordId) { + break; + } + + if (i.attr('type') && i.attr('type').toLowerCase() === 'password') { + continue; + } + + usernameField = i; + } + } + + if (usernameField && !checkDisabled) { + const usernameId = usernameField.attr('data-cip-id'); + // Check if usernameField is already used by another combination + for (const c of cipFields.combinations) { + if (c.username === usernameId) { + usernameField = null; + break; + } + } + } + + cipFields.setUniqueId(usernameField); + return usernameField; +}; + +/** +* Return the password field or null if it not exists +*/ +cipFields.getPasswordField = function(usernameId, checkDisabled) { + const usernameField = _f(usernameId); + if (!usernameField) { + return null; + } + + const form = usernameField.closest('form')[0]; + let passwordField = null; + + // Search all inputs on this one form + if (form) { + passwordField = jQuery('input[type=\'password\']:first', form); + if (passwordField && passwordField.length < 1) { + passwordField = null; + } + + if (cip.settings.usePasswordGenerator) { + cipPassword.init(); + cipPassword.initField(passwordField); + } + } else { + // Search all inputs on page + const inputs = cipFields.getAllFields(); + cip.initPasswordGenerator(inputs); + + let active = false; + for (const i of inputs) { + if (i.attr('data-cip-id') === usernameId) { + active = true; + } + if (active && jQuery(i).attr('type') && jQuery(i).attr('type').toLowerCase() === 'password') { + passwordField = i; + break; + } + } + } + + if (passwordField && !checkDisabled) { + const passwordId = passwordField.attr('data-cip-id'); + // Check if passwordField is already used by another combination + for (const c of cipFields.combinations) { + if (c.password === passwordId) { + passwordField = null; + break; + } + } + } + + cipFields.setUniqueId(passwordField); + + return passwordField; +}; + +cipFields.prepareCombinations = function(combinations) { + for (const c of combinations) { + const pwField = _f(c.password); + // Needed for auto-complete: don't overwrite manually filled-in password field + if (pwField && !pwField.data('cipFields-onChange')) { + pwField.data('cipFields-onChange', true); + pwField.change(function() { + jQuery(this).data('unchanged', false); + }); + } + + // Initialize form-submit for remembering credentials + const fieldId = c.password || c.username; + const field = _f(fieldId); + if (field) { + const form = field.closest('form'); + if (form && form.length > 0) { + cipForm.init(form, c); + } + } + } +}; + +cipFields.useDefinedCredentialFields = function() { + const location = cip.getDocumentLocation(); + if (cip.settings['defined-custom-fields'] && cip.settings['defined-custom-fields'][location]) { + const creds = cip.settings['defined-custom-fields'][location]; + + let $found = _f(creds.username) || _f(creds.password); + for (const i of creds.fields) { + if (_fs(i)) { + $found = true; + break; + } + } + + if ($found) { + const fields = { + username: creds.username, + password: creds.password, + fields: creds.fields + }; + cipFields.combinations = []; + cipFields.combinations.push(fields); + + return true; + } + } + + return false; +}; + + +var cipObserverHelper = {}; +cipObserverHelper.inputTypes = [ + 'text', + 'email', + 'password', + 'tel', + 'number', + null // Input field can be without any type. Include these to the list. +]; + +// Ignores all nodes that doesn't contain elements +cipObserverHelper.ignoredNode = function(target) { + if (target.nodeType === Node.ATTRIBUTE_NODE || + target.nodeType === Node.TEXT_NODE || + target.nodeType === Node.CDATA_SECTION_NODE || + target.nodeType === Node.PROCESSING_INSTRUCTION_NODE || + target.nodeType === Node.COMMENT_NODE || + target.nodeType === Node.DOCUMENT_TYPE_NODE || + target.nodeType === Node.NOTATION_NODE) { + return true; + } + return false; +}; + +cipObserverHelper.getInputs = function(target) { + // Ignores target element if it's not an element node + if (cipObserverHelper.ignoredNode(target)) { + return []; + } + + // Filter out any input fields with type 'hidden' right away + const inputFields = []; + Array.from(target.getElementsByTagName('input')).forEach((e) => { + if (e.type !== 'hidden') { + inputFields.push(e); + } + }); + + // Do not allow more visible inputs than _maximumInputs (default value: 100) + if (inputFields.length === 0 || inputFields.length > _maximumInputs) { + return []; + } + + // Only include input fields that match with cipObserverHelper.inputTypes + const inputs = []; + for (const i of inputFields) { + let type = i.getAttribute('type'); + if (type) { + type = type.toLowerCase(); + } + + if (cipObserverHelper.inputTypes.includes(type)) { + inputs.push(i); + } + } + return inputs; +}; + +cipObserverHelper.getId = function(target) { + return target.classList.length === 0 ? target.id : target.classList; +}; + +cipObserverHelper.ignoredElement = function(target) { + // Ignore elements that do not have a className (including SVG) + if (typeof target.className !== 'string') { + return true; + } + + // Ignore KeePassXC-Browser classes + if (target.className && target.className !== undefined && + (target.className.includes('kpxc') || target.className.includes('ui-helper'))) { + return true; + } + + return false; +}; + +cipObserverHelper.handleObserverAdd = function(target) { + if (cipObserverHelper.ignoredElement(target)) { + return; + } + + const inputs = cipObserverHelper.getInputs(target); + if (inputs.length === 0) { + return; + } + + const neededLength = _detectedFields === 1 ? 0 : 1; + const id = cipObserverHelper.getId(target); + if (inputs.length > neededLength && !_observerIds.includes(id)) { + // Save target element id for preventing multiple calls to initCredentialsFields() + _observerIds.push(id); + + // Sometimes the settings haven't been loaded before new input fields are detected + if (Object.keys(cip.settings).length === 0) { + cip.init(); + } else { + cip.initCredentialFields(true); + } + } +}; + +cipObserverHelper.handleObserverRemove = function(target) { + if (cipObserverHelper.ignoredElement(target)) { + return; + } + + const inputs = cipObserverHelper.getInputs(target); + if (inputs.length === 0) { + return; + } + + // Remove target element id from the list + const id = cipObserverHelper.getId(target); + if (_observerIds.includes(id)) { + const index = _observerIds.indexOf(id); + if (index >= 0) { + _observerIds.splice(index, 1); + } + } +}; + +cipObserverHelper.detectURLChange = function() { + if (_documentURL !== document.location.href) { + _documentURL = document.location.href; + cipEvents.clearCredentials(); + cip.initCredentialFields(true); + } +}; + +MutationObserver = window.MutationObserver || window.WebKitMutationObserver; + +// Detects DOM changes in the document +let observer = new MutationObserver(function(mutations, observer) { + if (document.visibilityState === 'hidden') { + return; + } + + for (const mut of mutations) { + // Skip text nodes + if (mut.target.nodeType === Node.TEXT_NODE) { + continue; + } + + // Check document URL change and detect new fields + cipObserverHelper.detectURLChange(); + + // Handle attributes only if CSS display is modified + if (mut.type === 'attributes') { + const newValue = mut.target.getAttribute(mut.attributeName); + if (newValue && (newValue.includes('display') || newValue.includes('z-index'))) { + if (mut.target.style.display !== 'none') { + cipObserverHelper.handleObserverAdd(mut.target); + } else { + cipObserverHelper.handleObserverRemove(mut.target); + } + } + } else if (mut.type === 'childList') { + cipObserverHelper.handleObserverAdd((mut.addedNodes.length > 0) ? mut.addedNodes[0] : mut.target); + cipObserverHelper.handleObserverRemove((mut.removedNodes.length > 0) ? mut.removedNodes[0] : mut.target); + } + } +}); + +// Define what element should be observed by the observer +// and what types of mutations trigger the callback +observer.observe(document, { + subtree: true, + attributes: true, + childList: true, + characterData: true, + attributeFilter: ['style'] +}); + + +var cip = {}; +cip.settings = {}; +cip.u = null; +cip.p = null; +cip.url = null; +cip.submitUrl = null; +cip.credentials = []; + +jQuery(function() { + cip.init(); +}); + +cip.init = function() { + browser.runtime.sendMessage({ + action: 'load_settings', + }).then((response) => { + cip.settings = response; + cip.initCredentialFields(); + }); +}; + +// Switch credentials if database is changed or closed +cip.detectDatabaseChange = function(response) { + if (document.visibilityState !== 'hidden') { + if (response.new === '' && response.old !== '') { + cipEvents.clearCredentials(); + + browser.runtime.sendMessage({ + action: 'page_clear_logins' + }); + + // Switch back to default popup + browser.runtime.sendMessage({ + action: 'get_status', + args: [ true ] // Set polling to true, this is an internal function call + }); + } else if (response.new !== '' && response.new !== response.old) { + _called.retrieveCredentials = false; + browser.runtime.sendMessage({ + action: 'load_settings', + }).then((settings) => { + cip.settings = settings; + cip.initCredentialFields(true); + + // If user has requested a manual fill through context menu the actual credential filling + // is handled here when the opened database has been regognized. It's not a pretty hack. + if (_called.manualFillRequested && _called.manualFillRequested !== 'none') { + cip.fillInFromActiveElement(false, _called.manualFillRequested === 'pass'); + _called.manualFillRequested = 'none'; + } + }); + } + } +}; + +cip.initCredentialFields = function(forceCall) { + if (_called.initCredentialFields && !forceCall) { + return; + } + _called.initCredentialFields = true; + + browser.runtime.sendMessage({ 'action': 'page_clear_logins', args: [ _called.clearLogins ] }).then(() => { + _called.clearLogins = true; + + // Check site preferences + cip.initializeSitePreferences(); + if (cip.settings.sitePreferences) { + for (const site of cip.settings.sitePreferences) { + if (site.url === document.location.href || siteMatch(site.url, document.location.href)) { + if (site.ignore === IGNORE_FULL) { + return; + } + + _singleInputEnabledForPage = site.usernameOnly; + } + } + } + + const inputs = cipFields.getAllFields(); + if (inputs.length === 0) { + return; + } + + cipFields.prepareVisibleFieldsWithID('select'); + cip.initPasswordGenerator(inputs); + + if (!cipFields.useDefinedCredentialFields()) { + // Get all combinations of username + password fields + cipFields.combinations = cipFields.getAllCombinations(inputs); + } + cipFields.prepareCombinations(cipFields.combinations); + + if (cipFields.combinations.length === 0 && inputs.length === 0) { + browser.runtime.sendMessage({ + action: 'show_default_browseraction' + }); + return; + } + + cip.url = document.location.origin; + cip.submitUrl = cip.getFormActionUrl(cipFields.combinations[0]); + + // Get submitUrl for a single input + if (_singleInputEnabledForPage && !cip.submitUrl && cipFields.combinations.length === 1 && inputs.length === 1) { + cip.submitUrl = cip.getFormActionUrlFromSingleInput(inputs[0]); + } + + if (cip.settings.autoRetrieveCredentials && _called.retrieveCredentials === false && (cip.url && cip.submitUrl)) { + _called.retrieveCredentials = true; + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl ] + }).then(cip.retrieveCredentialsCallback).catch((e) => { + console.log(e); + }); + } else if (_singleInputEnabledForPage) { + cip.preparePageForMultipleCredentials(cip.credentials); + } + }); +}; + +cip.initPasswordGenerator = function(inputs) { + if (cip.settings.usePasswordGenerator) { + cipPassword.init(); + + for (let i = 0; i < inputs.length; i++) { + if (inputs[i] && inputs[i].attr('type') && inputs[i].attr('type').toLowerCase() === 'password') { + cipPassword.initField(inputs[i], inputs, i); + } + } + } +}; + +cip.receiveCredentialsIfNecessary = function() { + return new Promise((resolve, reject) => { + if (cip.credentials.length === 0 && _called.retrieveCredentials === false) { + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl, false, true ] // Sets triggerUnlock to true + }).then((credentials) => { + // If the database was locked, this is scope never met. In these cases the response is met at cip.detectDatabaseChange + _called.manualFillRequested = 'none'; + cip.retrieveCredentialsCallback(credentials, false); + resolve(credentials); + }); + } else { + resolve(cip.credentials); + } + }); +}; + +cip.retrieveCredentialsCallback = function(credentials, dontAutoFillIn) { + if (cipFields.combinations.length > 0) { + cip.u = _f(cipFields.combinations[0].username); + cip.p = _f(cipFields.combinations[0].password); + } + + if (credentials && credentials.length > 0) { + cip.credentials = credentials; + cip.prepareFieldsForCredentials(!Boolean(dontAutoFillIn)); + _called.retrieveCredentials = true; + } +}; + +cip.prepareFieldsForCredentials = function(autoFillInForSingle) { + // Only one login for this site + if (autoFillInForSingle && cip.settings.autoFillSingleEntry && cip.credentials.length === 1) { + let combination = null; + if (!cip.p && !cip.u && cipFields.combinations.length > 0) { + cip.u = _f(cipFields.combinations[0].username); + cip.p = _f(cipFields.combinations[0].password); + combination = cipFields.combinations[0]; + } + if (cip.u) { + cip.setValueWithChange(cip.u, cip.credentials[0].login); + combination = cipFields.getCombination('username', cip.u); + } + if (cip.p) { + cip.setValueWithChange(cip.p, cip.credentials[0].password); + combination = cipFields.getCombination('password', cip.p); + } + + if (combination) { + const list = []; + if (cip.fillInStringFields(combination.fields, cip.credentials[0].stringFields, list)) { + cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); + } + } + + // Generate popup-list of usernames + descriptions + browser.runtime.sendMessage({ + action: 'popup_login', + args: [ [cip.credentials[0].login + ' (' + cip.credentials[0].name + ')'] ] + }); + } else if (cip.credentials.length > 1 || (cip.credentials.length > 0 && (!cip.settings.autoFillSingleEntry || !autoFillInForSingle))) { + // Multiple logins for this site + cip.preparePageForMultipleCredentials(cip.credentials); + } +}; + +cip.preparePageForMultipleCredentials = function(credentials) { + // Add usernames + descriptions to autocomplete-list and popup-list + const usernames = []; + cipAutocomplete.elements = []; + let visibleLogin; + for (let i = 0; i < credentials.length; i++) { + visibleLogin = (credentials[i].login.length > 0) ? credentials[i].login : tr('credentialsNoUsername'); + usernames.push(visibleLogin + ' (' + credentials[i].name + ')'); + const item = { + label: visibleLogin + ' (' + credentials[i].name + ')', + value: credentials[i].login, + loginId: i + }; + cipAutocomplete.elements.push(item); + } + + // Generate popup-list of usernames + descriptions + browser.runtime.sendMessage({ + action: 'popup_login', + args: [ usernames ] + }); + + // Initialize autocomplete for username fields + if (cip.settings.autoCompleteUsernames) { + for (const i of cipFields.combinations) { + // Both username and password fields are visible + if (_detectedFields >= 2) { + if (_f(i.username)) { + cipAutocomplete.init(_f(i.username)); + } + } else if (_detectedFields == 1) { + if (_f(i.username)) { + cipAutocomplete.init(_f(i.username)); + } + if (_f(i.password)) { + cipAutocomplete.init(_f(i.password)); + } + } + } + } +}; + +cip.getFormActionUrl = function(combination) { + if (!combination) { + return null; + } + + const field = _f(combination.password) || _f(combination.username); + + if (field === null) { + return null; + } + + const form = field.closest('form'); + let action = null; + + if (form && form.length > 0) { + action = form[0].action; + } + + if (typeof(action) !== 'string' || action === '') { + action = document.location.origin + document.location.pathname; + } + + return action; +}; + +cip.getFormActionUrlFromSingleInput = function(field) { + if (!field) { + return null; + } + + let action = field.formAction; + + if (typeof(action) !== 'string' || action === '') { + action = document.location.origin + document.location.pathname; + } + + return action; +}; + +cip.fillInCredentials = function(combination, onlyPassword, suppressWarnings) { + const action = cip.getFormActionUrl(combination); + + const u = _f(combination.username); + const p = _f(combination.password); + + if (combination.isNew) { + // Initialize form-submit for remembering credentials + const fieldId = combination.password || combination.username; + const field = _f(fieldId); + if (field) { + const form2 = field.closest('form'); + if (form2 && form2.length > 0) { + cipForm.init(form2, combination); + } + } + } + + if (u) { + cip.u = u; + } + if (p) { + cip.p = p; + } + + if (cip.url === document.location.origin && cip.submitUrl === action && cip.credentials.length > 0) { + cip.fillIn(combination, onlyPassword, suppressWarnings); + } else { + cip.url = document.location.origin; + cip.submitUrl = action; + + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl, false, true ] + }).then((credentials) => { + cip.retrieveCredentialsCallback(credentials, true); + cip.fillIn(combination, onlyPassword, suppressWarnings); + }); + } +}; + +cip.fillInFromActiveElement = function(suppressWarnings, passOnly = false) { + const el = document.activeElement; + if (el.tagName.toLowerCase() !== 'input') { + if (cipFields.combinations.length > 0) { + cip.fillInCredentials(cipFields.combinations[0], false, suppressWarnings); + } + return; + } + + cipFields.setUniqueId(jQuery(el)); + const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); + let combination = null; + if ($(el).attr('type') === 'password') { + combination = cipFields.getCombination('password', fieldId); + } else { + combination = cipFields.getCombination('username', fieldId); + } + + if (passOnly) { + if (!_f(combination.password)) { + const message = tr('fieldsNoPasswordField'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + return; + } + } + + delete combination.loginId; + + cip.fillInCredentials(combination, passOnly, suppressWarnings); +}; + +cip.fillInFromActiveElementTOTPOnly = function(suppressWarnings) { + const el = document.activeElement; + cipFields.setUniqueId(jQuery(el)); + const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); + + browser.runtime.sendMessage({ + action: 'page_get_login_id' + }).then((pos) => { + if (pos >= 0 && cip.credentials[pos]) { + // Check the value from stringFields (to be removed) + const currentField = _fs(fieldId); + if (cip.credentials[pos].stringFields && cip.credentials[pos].stringFields.length > 0) { + const stringFields = cip.credentials[pos].stringFields; + for (const s of stringFields) { + const val = s['KPH: {TOTP}']; + if (val) { + cip.setValue(currentField, val); + } + } + } else if (cip.credentials[pos].totp && cip.credentials[pos].totp.length > 0) { + cip.setValue(currentField, cip.credentials[pos].totp); + } + } + }); +}; + +cip.setValue = function(field, value) { + if (field.is('select')) { + value = value.toLowerCase().trim(); + jQuery('option', field).each(function() { + if (jQuery(this).text().toLowerCase().trim() === value) { + cip.setValueWithChange(field, jQuery(this).val()); + return false; + } + }); + } else { + cip.setValueWithChange(field, value); + field.trigger('input'); + } +}; + +cip.fillInStringFields = function(fields, stringFields, filledInFields) { + let filledIn = false; + + filledInFields.list = []; + if (fields && stringFields && fields.length > 0 && stringFields.length > 0) { + for (let i = 0; i < fields.length; i++) { + const currentField = _fs(fields[i]); + const stringFieldValue = Object.values(stringFields[i]); + if (currentField && stringFieldValue[0]) { + cip.setValue(currentField, stringFieldValue[0]); + filledInFields.list.push(fields[i]); + filledIn = true; + } + } + } + + return filledIn; +}; + +cip.setValueWithChange = function(field, value) { + if (cip.settings.respectMaxLength === true) { + const attributeMaxLength = field.attr('maxlength'); + if (attributeMaxLength && !isNaN(attributeMaxLength) && attributeMaxLength > 0) { + value = value.substr(0, attributeMaxLength); + } + } + + field.val(value); + field[0].dispatchEvent(new Event('input', { 'bubbles': true })); + field[0].dispatchEvent(new Event('change', { 'bubbles': true })); +}; + +cip.fillIn = function(combination, onlyPassword, suppressWarnings) { + // No credentials available + if (cip.credentials.length === 0 && !suppressWarnings) { + const message = tr('credentialsNoLoginsFound'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + return; + } + + const uField = _f(combination.username); + const pField = _f(combination.password); + + // Exactly one pair of credentials available + if (cip.credentials.length === 1) { + let filledIn = false; + if (uField && (!onlyPassword || _singleInputEnabledForPage)) { + cip.setValueWithChange(uField, cip.credentials[0].login); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ 0 ] + }); + filledIn = true; + } + if (pField) { + pField.attr('type', 'password'); + cip.setValueWithChange(pField, cip.credentials[0].password); + pField.data('unchanged', true); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ 0 ] + }); + filledIn = true; + } + + const list = []; + if (cip.fillInStringFields(combination.fields, cip.credentials[0].stringFields, list)) { + cipForm.destroy(false, {'password': list.list[0], 'username': list.list[1]}); + filledIn = true; + } + + if (!filledIn) { + if (!suppressWarnings) { + const message = tr('fieldsFill'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } + } + } else if (combination.loginId !== undefined && cip.credentials[combination.loginId]) { + // Specific login id given + let filledIn = false; + if (uField) { + cip.setValueWithChange(uField, cip.credentials[combination.loginId].login); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ combination.loginId ] + }); + filledIn = true; + } + + if (pField) { + cip.setValueWithChange(pField, cip.credentials[combination.loginId].password); + pField.data('unchanged', true); + browser.runtime.sendMessage({ + action: 'page_set_login_id', args: [ combination.loginId ] + }); + filledIn = true; + } + + let list = []; + if (cip.fillInStringFields(combination.fields, cip.credentials[combination.loginId].stringFields, list)) { + cipForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); + filledIn = true; + } + + if (!filledIn) { + if (!suppressWarnings) { + const message = tr('fieldsFill'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } + } + } else { // Multiple credentials available + // Check if only one password for given username exists + let countPasswords = 0; + + if (uField) { + let valPassword = ''; + let valUsername = ''; + let valStringFields = []; + const valQueryUsername = uField.val().toLowerCase(); + + // Find passwords to given username (even those with empty username) + for (const c of cip.credentials) { + if (c.login.toLowerCase() === valQueryUsername) { + countPasswords += 1; + valPassword = c.password; + valUsername = c.login; + valStringFields = c.stringFields; + } + } + + // For the correct notification message: 0 = no logins, X > 1 = too many logins + if (countPasswords === 0) { + countPasswords = cip.credentials.length; + } + + // Only one mapping username found + if (countPasswords === 1) { + if (!onlyPassword) { + cip.setValueWithChange(uField, valUsername); + } + + if (pField) { + cip.setValueWithChange(pField, valPassword); + pField.data('unchanged', true); + } + + let list = []; + if (cip.fillInStringFields(combination.fields, valStringFields, list)) { + cipForm.destroy(false, { 'password': list.list[0], 'username': list.list[1] }); + } + } + + // User has to select correct credentials by himself + if (countPasswords > 1) { + if (!suppressWarnings) { + const $target = onlyPassword ? pField : uField; + cipAutocomplete.init($target); + $target.focus(); + jQuery($target).autocomplete('search', jQuery($target).val()); + } + } else if (countPasswords < 1) { + if (!suppressWarnings) { + const message = tr('credentialsNoUsernameFound'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } + } + } else { + if (!suppressWarnings) { + const $target = onlyPassword ? pField : uField; + cipAutocomplete.init($target); + $target.focus(); + jQuery($target).autocomplete('search', jQuery($target).val()); + } + } + } +}; + +cip.contextMenuRememberCredentials = function() { + const el = document.activeElement; + if (el.tagName.toLowerCase() !== 'input') { + return; + } + + cipFields.setUniqueId(jQuery(el)); + const fieldId = cipFields.prepareId(jQuery(el).attr('data-cip-id')); + let combination = null; + if ($(el).attr('type') === 'password') { + combination = cipFields.getCombination('password', fieldId); + } else { + combination = cipFields.getCombination('username', fieldId); + } + + let usernameValue = ''; + let passwordValue = ''; + + const usernameField = _f(combination.username); + const passwordField = _f(combination.password); + + if (usernameField) { + usernameValue = usernameField.val(); + } + if (passwordField) { + passwordValue = passwordField.val(); + } + + if (!cip.rememberCredentials(usernameValue, passwordValue)) { + const message = tr('rememberNothingChanged'); + browser.runtime.sendMessage({ + action: 'show_notification', + args: [ message ] + }); + } +}; + +cip.rememberCredentials = function(usernameValue, passwordValue) { + // No password given or field cleaned by a site-running script + // --> no password to save + if (passwordValue === '') { + return false; + } + + let usernameExists = false; + let nothingChanged = false; + + for (const c of cip.credentials) { + if (c.login === usernameValue && c.password === passwordValue) { + nothingChanged = true; + break; + } + + if (c.login === usernameValue) { + usernameExists = true; + } + } + + if (!nothingChanged) { + if (!usernameExists) { + for (const c of cip.credentials) { + if (c.login === usernameValue) { + usernameExists = true; + break; + } + } + } + const credentialsList = []; + for (const c of cip.credentials) { + credentialsList.push({ + login: c.login, + name: c.name, + uuid: c.uuid + }); + } + + let url = jQuery(this)[0].action; + if (!url) { + url = cip.getDocumentLocation(); + if (url.indexOf('?') > 0) { + url = url.substring(0, url.indexOf('?')); + if (url.length < document.location.origin.length) { + url = document.location.origin; + } + } + } + + browser.runtime.sendMessage({ + action: 'set_remember_credentials', + args: [ usernameValue, passwordValue, url, usernameExists, credentialsList ] + }); + + return true; + } + + return false; +}; + +cip.ignoreSite = function(sites) { + if (!sites || sites.length === 0) { + return; + } + + let site = sites[0]; + cip.initializeSitePreferences(); + + if (slashNeededForUrl(site)) { + site += '/'; + } + + // Check if the site already exists + let siteExists = false; + for (const existingSite of cip.settings['sitePreferences']) { + if (existingSite.url === site) { + existingSite.ignore = IGNORE_NORMAL; + siteExists = true; + } + } + + if (!siteExists) { + cip.settings['sitePreferences'].push({ + url: site, + ignore: IGNORE_NORMAL, + usernameOnly: false + }); + } + + browser.runtime.sendMessage({ + action: 'save_settings', + args: [ cip.settings ] + }); +}; + +// Delete previously created Object if it exists. It will be replaced by an Array +cip.initializeSitePreferences = function() { + if (cip.settings['sitePreferences'] !== undefined && cip.settings['sitePreferences'].constructor === Object) { + delete cip.settings['sitePreferences']; + } + + if (!cip.settings['sitePreferences']) { + cip.settings['sitePreferences'] = []; + } +}; + +cip.getDocumentLocation = function() { + return cip.settings.saveDomainOnly ? document.location.origin : document.location.href; +}; + +var cipEvents = {}; + +cipEvents.clearCredentials = function() { + cip.credentials = []; + cipAutocomplete.elements = []; + _called.retrieveCredentials = false; + + if (cip.settings.autoCompleteUsernames) { + for (const c of cipFields.combinations) { + const uField = _f(c.username); + if (uField) { + if (uField.hasClass('ui-autocomplete-input')) { + uField.autocomplete('destroy'); + } + } + } + } +}; + +cipEvents.triggerActivatedTab = function() { + // Doesn't run a second time because of _called.initCredentialFields set to true + cip.init(); + $(this.target).find('input').autocomplete(); + + // InitCredentialFields calls also "retrieve_credentials", to prevent it + // check of init() was already called + if (_called.initCredentialFields && (cip.url && cip.submitUrl) && cip.settings.autoRetrieveCredentials) { + browser.runtime.sendMessage({ + action: 'retrieve_credentials', + args: [ cip.url, cip.submitUrl ] + }).then(cip.retrieveCredentialsCallback).catch((e) => { + console.log(e); + }); + } +}; diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index 0ac3cc5..a96ba4a 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -61,21 +61,21 @@ } ], "commands": { - "fill-username-password": { + "fill_username_password": { "description": "__MSG_contextMenuFillUsernameAndPassword__", "suggested_key": { "default": "Alt+Shift+U", "mac": "MacCtrl+Shift+U" } }, - "fill-password": { + "fill_password": { "description": "__MSG_contextMenuFillPassword__", "suggested_key": { "default": "Alt+Shift+I", "mac": "MacCtrl+Shift+I" } }, - "fill-totp": { + "fill_totp": { "description": "__MSG_contextMenuFillTOTP__", "suggested_key": { "default": "Alt+Shift+T", diff --git a/keepassxc-browser/options/options.html b/keepassxc-browser/options/options.html index 389d259..e364619 100644 --- a/keepassxc-browser/options/options.html +++ b/keepassxc-browser/options/options.html @@ -344,7 +344,7 @@ × - + diff --git a/keepassxc-browser/options/options.js b/keepassxc-browser/options/options.js index 65bf2e9..69a5a56 100644 --- a/keepassxc-browser/options/options.js +++ b/keepassxc-browser/options/options.js @@ -41,7 +41,7 @@ options.initMenu = function() { options.saveSettingsPromise = function() { return new Promise((resolve, reject) => { - browser.storage.local.set({'settings': options.settings}).then((item) => { + browser.storage.local.set({ 'settings': options.settings }).then((item) => { browser.runtime.sendMessage({ action: 'load_settings' }).then((settings) => { @@ -56,21 +56,21 @@ options.saveSetting = function(name) { $(id).closest('.control-group').removeClass('error').addClass('success'); setTimeout(() => { $(id).closest('.control-group').removeClass('success'); }, 2500); - browser.storage.local.set({'settings': options.settings}); + browser.storage.local.set({ 'settings': options.settings }); browser.runtime.sendMessage({ action: 'load_settings' }); }; options.saveSettings = function() { - browser.storage.local.set({'settings': options.settings}); + browser.storage.local.set({ 'settings': options.settings }); browser.runtime.sendMessage({ action: 'load_settings' }); }; options.saveKeyRing = function() { - browser.storage.local.set({'keyRing': options.keyRing}); + browser.storage.local.set({ 'keyRing': options.keyRing }); browser.runtime.sendMessage({ action: 'load_keyring' }); @@ -86,7 +86,7 @@ options.initGeneralSettings = function() { options.settings[name] = $(this).is(':checked'); options.saveSettingsPromise().then((x) => { if (name === 'autoFillAndSend') { - browser.runtime.sendMessage({action: 'init_http_auth'}); + browser.runtime.sendMessage({ action: 'init_http_auth' }); } }); }); @@ -128,11 +128,11 @@ options.initGeneralSettings = function() { $('#configureCommands').click(function() { browser.tabs.create({ - url: isFirefox() ? browser.runtime.getURL("options/shortcuts.html") : 'chrome://extensions/configureCommands' + url: isFirefox() ? browser.runtime.getURL('options/shortcuts.html') : 'chrome://extensions/configureCommands' }); }); - $('#blinkTimeoutButton').click(function(){ + $('#blinkTimeoutButton').click(function() { const blinkTimeout = $.trim($('#blinkTimeout').val()); const blinkTimeoutval = blinkTimeout !== '' ? Number(blinkTimeout) : defaultSettings.blinkTimeout; @@ -140,7 +140,7 @@ options.initGeneralSettings = function() { options.saveSetting('blinkTimeout'); }); - $('#blinkMinTimeoutButton').click(function(){ + $('#blinkMinTimeoutButton').click(function() { const blinkMinTimeout = $.trim($('#blinkMinTimeout').val()); const blinkMinTimeoutval = blinkMinTimeout !== '' ? Number(blinkMinTimeout) : defaultSettings.redirectOffset; @@ -148,7 +148,7 @@ options.initGeneralSettings = function() { options.saveSetting('blinkMinTimeout'); }); - $('#allowedRedirectButton').click(function(){ + $('#allowedRedirectButton').click(function() { const allowedRedirect = $.trim($('#allowedRedirect').val()); const allowedRedirectval = allowedRedirect !== '' ? Number(allowedRedirect) : defaultSettings.redirectAllowance; @@ -175,7 +175,7 @@ options.getPartiallyHiddenKey = function(key) { }; options.initConnectedDatabases = function() { - $('#dialogDeleteConnectedDatabase').modal({keyboard: true, show: false, backdrop: true}); + $('#dialogDeleteConnectedDatabase').modal({ keyboard: true, show: false, backdrop: true }); $('#tab-connected-databases tr.clone:first button.delete:first').click(function(e) { e.preventDefault(); $('#dialogDeleteConnectedDatabase').data('hash', $(this).closest('tr').data('hash')); @@ -233,7 +233,7 @@ options.initConnectedDatabases = function() { }; options.initCustomCredentialFields = function() { - $('#dialogDeleteCustomCredentialFields').modal({keyboard: true, show: false, backdrop: true}); + $('#dialogDeleteCustomCredentialFields').modal({ keyboard: true, show: false, backdrop: true }); $('#tab-custom-fields tr.clone:first button.delete:first').click(function(e) { e.preventDefault(); $('#dialogDeleteCustomCredentialFields').data('url', $(this).closest('tr').data('url')); @@ -262,7 +262,7 @@ options.initCustomCredentialFields = function() { const trClone = $('#tab-custom-fields table tr.clone:first').clone(true); trClone.removeClass('clone'); let counter = 1; - for (let url in options.settings['defined-custom-fields']) { + for (const url in options.settings['defined-custom-fields']) { const tr = trClone.clone(true); tr.data('url', url); tr.attr('id', 'tr-scf' + counter); @@ -280,7 +280,7 @@ options.initCustomCredentialFields = function() { }; options.initSitePreferences = function() { - $('#dialogDeleteSite').modal({keyboard: true, show: false, backdrop: true}); + $('#dialogDeleteSite').modal({ keyboard: true, show: false, backdrop: true }); $('#tab-site-preferences tr.clone:first button.delete:first').click(function(e) { e.preventDefault(); $('#dialogDeleteSite').data('url', $(this).closest('tr').data('url')); @@ -291,7 +291,7 @@ options.initSitePreferences = function() { $('#tab-site-preferences tr.clone:first input[type=checkbox]:first').change(function() { const url = $(this).closest('tr').data('url'); - for (let site of options.settings['sitePreferences']) { + for (const site of options.settings['sitePreferences']) { if (site.url === url) { site.usernameOnly = $(this).is(':checked'); } @@ -301,7 +301,7 @@ options.initSitePreferences = function() { $('#tab-site-preferences tr.clone:first select:first').change(function() { const url = $(this).closest('tr').data('url'); - for (let site of options.settings['sitePreferences']) { + for (const site of options.settings['sitePreferences']) { if (site.url === url) { site.ignore = $(this).val(); } @@ -309,9 +309,9 @@ options.initSitePreferences = function() { options.saveSettings(); }); - $("#manualUrl").keyup(function(event) { + $('#manualUrl').keyup(function(event) { if (event.keyCode === 13) { - $("#sitePreferencesManualAdd").click(); + $('#sitePreferencesManualAdd').click(); } }); @@ -340,7 +340,7 @@ options.initSitePreferences = function() { $('#tab-site-preferences table tbody:first').append(tr); $('#tab-site-preferences table tbody:first tr.empty:first').hide(); - options.settings['sitePreferences'].push({url: value, ignore: IGNORE_NOTHING, usernameOnly: false}); + options.settings['sitePreferences'].push({ url: value, ignore: IGNORE_NOTHING, usernameOnly: false }); options.saveSettings(); $('#manualUrl').val(''); @@ -371,7 +371,7 @@ options.initSitePreferences = function() { const trClone = $('#tab-site-preferences table tr.clone:first').clone(true); trClone.removeClass('clone'); let counter = 1; - if (options.settings['sitePreferences']){ + if (options.settings['sitePreferences']) { for (let site of options.settings['sitePreferences']) { const tr = trClone.clone(true); tr.data('url', site.url); @@ -396,7 +396,7 @@ options.initAbout = function() { $('#tab-about em.versionCIP').text(browser.runtime.getManifest().version); // Hides keyboard shortcut configure button if Firefox version is < 60 (API is not compatible) - if (isFirefox() && Number(navigator.userAgent.substr(navigator.userAgent.lastIndexOf('/')+1, 2)) < 60) { + if (isFirefox() && Number(navigator.userAgent.substr(navigator.userAgent.lastIndexOf('/') + 1, 2)) < 60) { $('#chrome-only').remove(); } }; diff --git a/keepassxc-browser/options/shortcuts.js b/keepassxc-browser/options/shortcuts.js index e37dce1..3ac8519 100644 --- a/keepassxc-browser/options/shortcuts.js +++ b/keepassxc-browser/options/shortcuts.js @@ -10,14 +10,14 @@ document.querySelectorAll('input').forEach((b) => { const saveButtons = document.querySelectorAll('.btn-primary'); for (const b of saveButtons) { - b.addEventListener('click', e => { + b.addEventListener('click', (e) => { updateShortcut(b.parentElement.children[1].getAttribute('id')) }); } const resetButtons = document.querySelectorAll('.btn-danger'); for (const b of resetButtons) { - b.addEventListener('click', e => { + b.addEventListener('click', (e) => { resetShortcut(b.parentElement.children[1].getAttribute('id')) }); } @@ -69,12 +69,12 @@ async function updateKeys() { async function updateShortcut(shortcut) { try { - await browser.commands.update({ + await browser.commands.update({ name: shortcut, shortcut: document.querySelector('#' + shortcut).value }); createBanner('success', shortcut); - } catch(e) { + } catch (e) { console.log('Cannot change shortcut: ' + e); createBanner('danger', shortcut); } @@ -105,7 +105,7 @@ function createBanner(type, shortcut) { } else { return; } - + document.body.appendChild(banner); // Destroy the banner after five seconds diff --git a/keepassxc-browser/popups/popup.js b/keepassxc-browser/popups/popup.js index a22b3b3..1447778 100644 --- a/keepassxc-browser/popups/popup.js +++ b/keepassxc-browser/popups/popup.js @@ -1,6 +1,6 @@ 'use strict'; -function status_response(r) { +function statusResponse(r) { $('#initial-state').hide(); $('#error-encountered').hide(); $('#need-reconfigure').hide(); @@ -12,27 +12,21 @@ function status_response(r) { if (!r.keePassXCAvailable) { $('#error-message').html(r.error); $('#error-encountered').show(); - } - else if (r.keePassXCAvailable && r.databaseClosed) { + } else if (r.keePassXCAvailable && r.databaseClosed) { $('#database-error-message').html(r.error); $('#database-not-opened').show(); - } - else if (!r.configured) { + } else if (!r.configured) { $('#not-configured').show(); - } - else if (r.encryptionKeyUnrecognized) { + } else if (r.encryptionKeyUnrecognized) { $('#need-reconfigure').show(); $('#need-reconfigure-message').html(r.error); - } - else if (!r.associated) { + } else if (!r.associated) { $('#need-reconfigure').show(); $('#need-reconfigure-message').html(r.error); - } - else if (r.error !== null) { + } else if (r.error !== null) { $('#error-encountered').show(); $('#error-message').html(r.error); - } - else { + } else { $('#configured-and-associated').show(); $('#associated-identifier').html(r.identifier); $('#lock-database-button').show(); @@ -57,22 +51,22 @@ $(function() { $('#reload-status-button').click(function() { browser.runtime.sendMessage({ action: 'reconnect' - }).then(status_response); + }).then(statusResponse); }); $('#reopen-database-button').click(function() { browser.runtime.sendMessage({ action: 'get_status', args: [ false, true ] // Set forcePopup to true - }).then(status_response); + }).then(statusResponse); }); $('#redetect-fields-button').click(function() { - browser.tabs.query({"active": true, "currentWindow": true}).then(function(tabs) { + browser.tabs.query({ 'active': true, 'currentWindow': true }).then(function(tabs) { if (tabs.length === 0) { return; // For example: only the background devtools or a popup are opened } - let tab = tabs[0]; + const tab = tabs[0]; browser.tabs.sendMessage(tab.id, { action: 'redetect_fields' @@ -83,10 +77,10 @@ $(function() { $('#lock-database-button').click(function() { browser.runtime.sendMessage({ action: 'lock-database' - }).then(status_response); + }).then(statusResponse); }); browser.runtime.sendMessage({ - action: "get_status" - }).then(status_response); + action: 'get_status' + }).then(statusResponse); }); diff --git a/keepassxc-browser/popups/popup_httpauth.js b/keepassxc-browser/popups/popup_httpauth.js index 8ff0e13..04732cd 100644 --- a/keepassxc-browser/popups/popup_httpauth.js +++ b/keepassxc-browser/popups/popup_httpauth.js @@ -3,7 +3,7 @@ const getLoginData = function() { return new Promise((resolve, reject) => { browser.runtime.getBackgroundPage().then((global) => { - browser.tabs.query({'active': true, 'currentWindow': true}).then((tabs) => { + browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => { resolve(global.page.tabs[tabs[0].id].loginList); }); }); @@ -12,13 +12,13 @@ const getLoginData = function() { $(function() { getLoginData().then((data) => { - let ll = document.getElementById('login-list'); + const ll = document.getElementById('login-list'); for (let i = 0; i < data.logins.length; ++i) { const a = document.createElement('a'); a.setAttribute('class', 'list-group-item'); - a.textContent = data.logins[i].login + " (" + data.logins[i].name + ")"; + a.textContent = data.logins[i].login + ' (' + data.logins[i].name + ')'; $(a).data('creds', data.logins[i]); - $(a).click(function () { + $(a).click(function() { if (data.resolve) { const creds = $(this).data('creds'); data.resolve({ @@ -37,8 +37,8 @@ $(function() { $('#lock-database-button').click(function() { browser.runtime.sendMessage({ action: 'lock-database' - }).then(status_response); - }); + }).then(statusResponse); + }); $('#btn-dismiss').click(function() { getLoginData().then((data) => { diff --git a/keepassxc-browser/popups/popup_login.js b/keepassxc-browser/popups/popup_login.js index adc3d36..3412237 100644 --- a/keepassxc-browser/popups/popup_login.js +++ b/keepassxc-browser/popups/popup_login.js @@ -2,14 +2,14 @@ $(function() { browser.runtime.getBackgroundPage().then((global) => { - browser.tabs.query({'active': true, 'currentWindow': true}).then((tabs) => { + browser.tabs.query({ 'active': true, 'currentWindow': true }).then((tabs) => { if (tabs.length === 0) { return; // For example: only the background devtools or a popup are opened } const tab = tabs[0]; const logins = global.page.tabs[tab.id].loginList; - let ll = document.getElementById('login-list'); + const ll = document.getElementById('login-list'); for (let i = 0; i < logins.length; i++) { const a = document.createElement('a'); a.textContent = logins[i]; @@ -25,17 +25,17 @@ $(function() { }); ll.appendChild(a); } - + if (logins.length > 1) { document.getElementById('filter-block').style = ''; - let filter = document.getElementById('login-filter'); + const filter = document.getElementById('login-filter'); filter.addEventListener('keyup', (e) => { - let val = filter.value; - let re = new RegExp(val, 'i'); - let links = ll.getElementsByTagName('a'); - for (let i in links) { + const val = filter.value; + const re = new RegExp(val, 'i'); + const links = ll.getElementsByTagName('a'); + for (const i in links) { if (links.hasOwnProperty(i)) { - let found = String(links[i].textContent).match(re) !== null; + const found = String(links[i].textContent).match(re) !== null; links[i].style = found ? '' : 'display: none;'; } } @@ -57,7 +57,7 @@ $(function() { $('#reopen-database-button').click(function() { browser.runtime.sendMessage({ action: 'get_status', - args: [ false, true ] // Set forcePopup to true + args: [ false, true ] // Set forcePopup to true }); }); }); diff --git a/keepassxc-browser/popups/popup_remember.js b/keepassxc-browser/popups/popup_remember.js index f189205..5131ef8 100644 --- a/keepassxc-browser/popups/popup_remember.js +++ b/keepassxc-browser/popups/popup_remember.js @@ -37,7 +37,7 @@ function _initialize(tab) { e.preventDefault(); // Only one entry which could be updated - if(_tab.credentials.list.length === 1) { + if (_tab.credentials.list.length === 1) { // Use the current username if it's empty if (!_tab.credentials.username) { _tab.credentials.username = _tab.credentials.list[0].login; @@ -47,22 +47,20 @@ function _initialize(tab) { action: 'update_credentials', args: [_tab.credentials.list[0].uuid, _tab.credentials.username, _tab.credentials.password, _tab.credentials.url] }).then(_verifyResult); - } - else { + } else { $('.credentials:first .username-new:first strong:first').text(_tab.credentials.username); $('.credentials:first .username-exists:first strong:first').text(_tab.credentials.username); if (_tab.credentials.usernameExists) { $('.credentials:first .username-new:first').hide(); $('.credentials:first .username-exists:first').show(); - } - else { + } else { $('.credentials:first .username-new:first').show(); $('.credentials:first .username-exists:first').hide(); } for (let i = 0; i < _tab.credentials.list.length; i++) { - let $a = $('') + const $a = $('') .attr('href', '#') .text(_tab.credentials.list[i].login + ' (' + _tab.credentials.list[i].name + ')') .data('entryId', i) @@ -84,7 +82,7 @@ function _initialize(tab) { _verifyResult('error'); return; } - + // Show a notification if the user tries to update credentials using the old password if (credentials[entryId].password === _tab.credentials.password) { showNotification('Error: Credentials not updated. The password has not been changed.'); @@ -122,8 +120,8 @@ function _initialize(tab) { const tab = tabs[0]; browser.runtime.getBackgroundPage().then((global) => { browser.tabs.sendMessage(tab.id, { - action: 'ignore-site', - args: [_tab.credentials.url] + action: 'ignore_site', + args: [ _tab.credentials.url ] }); _close(); }); @@ -132,12 +130,11 @@ function _initialize(tab) { }); } -function _connected_database(db) { +function _connectedDatabase(db) { if (db.count > 1 && db.identifier) { $('.connected-database:first em:first').text(db.identifier); $('.connected-database:first').show(); - } - else { + } else { $('.connected-database:first').hide(); } } @@ -164,7 +161,7 @@ function _close() { $(function() { browser.runtime.sendMessage({ action: 'stack_add', - args: ['icon_remember_red_background_19x19.png', 'popup_remember.html', 10, true, 0] + args: [ 'icon_remember_red_background_19x19.png', 'popup_remember.html', 10, true, 0 ] }); browser.runtime.sendMessage({ @@ -173,5 +170,5 @@ $(function() { browser.runtime.sendMessage({ action: 'get_connected_database' - }).then(_connected_database); + }).then(_connectedDatabase); }); diff --git a/keepassxc-browser/translate.js b/keepassxc-browser/translate.js index 69c25e8..e8713dc 100644 --- a/keepassxc-browser/translate.js +++ b/keepassxc-browser/translate.js @@ -1,7 +1,7 @@ -'use strict' +'use strict'; const items = document.querySelectorAll('[data-i18n]'); -for (let item of items) { +for (const item of items) { const key = item.getAttribute('data-i18n'); if (key) { const placeholder = item.getAttribute('i18n-placeholder');