diff options
author | Sami Vänttinen <sami.vanttinen@protonmail.com> | 2022-06-22 07:21:05 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-22 07:21:05 +0300 |
commit | ba5a7141fba456ee409446ef32da2b19c416cee3 (patch) | |
tree | 5f88938f91fec875a3d1daf76bf9b6c57faec45f | |
parent | 32ee5105650ed3917b3d03f25c9a7fc333132b71 (diff) |
Custom Login Fields banner selector (#1390)
New Custom Login Fields banner selector.
-rw-r--r-- | .eslintrc | 2 | ||||
-rw-r--r-- | keepassxc-browser/_locales/en/messages.json | 44 | ||||
-rw-r--r-- | keepassxc-browser/content/banner.js | 1 | ||||
-rw-r--r-- | keepassxc-browser/content/custom-fields-banner.js | 767 | ||||
-rw-r--r-- | keepassxc-browser/content/define.js | 496 | ||||
-rw-r--r-- | keepassxc-browser/content/fields.js | 31 | ||||
-rw-r--r-- | keepassxc-browser/content/fill.js | 11 | ||||
-rwxr-xr-x | keepassxc-browser/content/keepassxc-browser.js | 13 | ||||
-rw-r--r-- | keepassxc-browser/content/ui.js | 40 | ||||
-rw-r--r-- | keepassxc-browser/content/username-field.js | 4 | ||||
-rw-r--r-- | keepassxc-browser/css/banner.css | 21 | ||||
-rw-r--r-- | keepassxc-browser/css/button.css | 8 | ||||
-rw-r--r-- | keepassxc-browser/css/define.css | 116 | ||||
-rw-r--r-- | keepassxc-browser/icons/help.svg | 1 | ||||
-rwxr-xr-x | keepassxc-browser/manifest.json | 3 |
15 files changed, 922 insertions, 636 deletions
@@ -99,7 +99,7 @@ "kpxc": true, "kpActions": true, "kpxcBanner": true, - "kpxcDefine": true, + "kpxcCustomLoginFieldsBanner": true, "kpxcFields": true, "kpxcFill": true, "kpxcForm": true, diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index e4d4214..a38d888 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -183,6 +183,10 @@ "message": "Username field icon", "description": "Username field icon text." }, + "totp": { + "message": "TOTP", + "description": "TOTP text." + }, "totpFieldText": { "message": "Fill TOTP from KeePassXC", "description": "OTP field icon hover text." @@ -191,9 +195,9 @@ "message": "TOTP field icon", "description": "OTP field icon text." }, - "defineDismiss": { - "message": "Dismiss", - "description": "Dismiss button text when choosing custom login fields." + "defineClose": { + "message": "Close", + "description": "Close button text when choosing custom login fields." }, "defineSkip": { "message": "Skip", @@ -203,9 +207,9 @@ "message": "Show more", "description": "More button text when choosing custom login fields." }, - "defineAgain": { - "message": "Again", - "description": "Again button text when choosing custom login fields." + "defineReset": { + "message": "Reset", + "description": "Reset button text when choosing custom login fields." }, "defineConfirm": { "message": "Confirm", @@ -215,29 +219,29 @@ "message": "Login fields for this page are already selected and will be overwritten.", "description": "A text shown when custom credentials fields are already set for the page." }, - "defineDiscard": { - "message": "Discard selection", - "description": "Discard selection button text when choosing custom login fields." + "defineClearData": { + "message": "Clear saved data", + "description": "Clear save data button text when choosing custom login fields." }, "defineStringField": { - "message": "String field #", + "message": "String field", "description": "Text for string field." }, "defineChooseUsername": { - "message": "1. Choose a username field", + "message": "Choose a username field", "description": "Choosing a username field text when choosing custom login fields." }, "defineChoosePassword": { - "message": "2. Now choose a password field", + "message": "Choose a password field", "description": "Choosing a password field text when choosing custom login fields." }, "defineChooseTOTP": { - "message": "3. Choose a TOTP field", + "message": "Choose a TOTP field", "description": "Choosing a TOTP field text when choosing custom login fields." }, - "defineConfirmSelection": { - "message": "4. Confirm selection", - "description": "Confirm a selection text when choosing custom login fields." + "defineChooseStringFields": { + "message": "Choose String Fields", + "description": "Choose String Fields a selection text when choosing custom login fields." }, "defineHelpText": { "message": "Please confirm your selection or choose more fields as String fields.", @@ -247,6 +251,10 @@ "message": "You can also use the numbers to choose the input fields from keyboard.", "description": "Help text when choosing custom login fields." }, + "defineChooseCustomLoginFieldText": { + "message": "Choose a Custom Login Field", + "description": "Help text for Custom Login Field banner." + }, "username": { "message": "Username", "description": "General text for username." @@ -255,6 +263,10 @@ "message": "Password", "description": "General text for password." }, + "stringFields": { + "message": "String Fields", + "description": "General text for a String Fields." + }, "credentialsNoUsername": { "message": "- no username -", "description": "Shown when no username is set in the credentials." diff --git a/keepassxc-browser/content/banner.js b/keepassxc-browser/content/banner.js index e98e664..cfb1f66 100644 --- a/keepassxc-browser/content/banner.js +++ b/keepassxc-browser/content/banner.js @@ -126,7 +126,6 @@ kpxcBanner.create = async function(credentials = {}) { const colorStyleSheet = createStylesheet('css/colors.css'); const wrapper = document.createElement('div'); - wrapper.setAttribute('id', 'kpxc-banner'); this.shadowRoot = wrapper.attachShadow({ mode: 'closed' }); this.shadowRoot.append(colorStyleSheet); this.shadowRoot.append(styleSheet); diff --git a/keepassxc-browser/content/custom-fields-banner.js b/keepassxc-browser/content/custom-fields-banner.js new file mode 100644 index 0000000..f51fd20 --- /dev/null +++ b/keepassxc-browser/content/custom-fields-banner.js @@ -0,0 +1,767 @@ +'use strict'; + +const STEP_NONE = 0; +const STEP_SELECT_USERNAME = 1; +const STEP_SELECT_PASSWORD = 2; +const STEP_SELECT_TOTP = 3; +const STEP_SELECT_STRING_FIELDS = 4; + +const BLUE_BUTTON = 'kpxc-button kpxc-blue-button'; +const GREEN_BUTTON = 'kpxc-button kpxc-green-button'; +const ORANGE_BUTTON = 'kpxc-button kpxc-orange-button'; +const RED_BUTTON = 'kpxc-button kpxc-red-button'; +const GRAY_BUTTON_CLASS = 'kpxc-gray-button'; + +const DEFINED_CUSTOM_FIELDS = 'defined-custom-fields'; +const FIXED_FIELD_CLASS = 'kpxcDefine-fixed-field'; +const DARK_FIXED_FIELD_CLASS = 'kpxcDefine-fixed-field-dark'; +const HOVER_FIELD_CLASS = 'kpxcDefine-fixed-hover-field'; +const DARK_HOVER_FIELD_CLASS = 'kpxcDefine-fixed-hover-field-dark'; +const DARK_TEXT_CLASS = 'kpxcDefine-dark-text'; +const USERNAME_FIELD_CLASS = 'kpxcDefine-fixed-username-field'; +const PASSWORD_FIELD_CLASS = 'kpxcDefine-fixed-password-field'; +const TOTP_FIELD_CLASS = 'kpxcDefine-fixed-totp-field'; +const STRING_FIELD_CLASS = 'kpxcDefine-fixed-string-field'; + +const kpxcCustomLoginFieldsBanner = {}; +kpxcCustomLoginFieldsBanner.banner = undefined; +kpxcCustomLoginFieldsBanner.chooser = undefined; +kpxcCustomLoginFieldsBanner.created = false; +kpxcCustomLoginFieldsBanner.dataStep = STEP_NONE; +kpxcCustomLoginFieldsBanner.infoText = undefined; +kpxcCustomLoginFieldsBanner.wrapper = undefined; +kpxcCustomLoginFieldsBanner.inputQueryPattern = 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=file]):not([type=hidden]):not([type=image]):not([type=month]):not([type=range]):not([type=reset]):not([type=submit]):not([type=time]):not([type=week]), select, textarea'; +kpxcCustomLoginFieldsBanner.markedFields = []; +kpxcCustomLoginFieldsBanner.nonSelectedElementsPattern = `div.${FIXED_FIELD_CLASS}:not(.${USERNAME_FIELD_CLASS}):not(.${PASSWORD_FIELD_CLASS}):not(.${TOTP_FIELD_CLASS}):not(.${STRING_FIELD_CLASS})`; + +kpxcCustomLoginFieldsBanner.selection = { + username: undefined, + usernameElement: undefined, + password: undefined, + passwordElement: undefined, + totp: undefined, + totpElement: undefined, + fields: [], + fieldElements: [], +}; + +kpxcCustomLoginFieldsBanner.buttons = { + reset: undefined, + confirm: undefined, + clearData: undefined, + close: undefined, +}; + +kpxcCustomLoginFieldsBanner.destroy = async function() { + if (!kpxcCustomLoginFieldsBanner.created) { + return; + } + + kpxcCustomLoginFieldsBanner.resetSelection(); + kpxcCustomLoginFieldsBanner.created = false; + kpxcCustomLoginFieldsBanner.close(); + + if (kpxcCustomLoginFieldsBanner.wrapper && window.self.document.body.contains(kpxcCustomLoginFieldsBanner.wrapper)) { + window.self.document.body.removeChild(kpxcCustomLoginFieldsBanner.wrapper); + } else { + window.self.document.body.removeChild(window.parent.document.body.querySelector('#kpxc-banner')); + } +}; + +kpxcCustomLoginFieldsBanner.close = function() { + kpxcCustomLoginFieldsBanner.chooser.remove(); + document.removeEventListener('keydown', kpxcCustomLoginFieldsBanner.keyDown); +}; + +kpxcCustomLoginFieldsBanner.create = async function() { + if (await kpxc.siteIgnored() || kpxcCustomLoginFieldsBanner.created) { + return; + } + + const banner = kpxcUI.createElement('div', 'kpxc-banner', { 'id': 'container' }); + banner.style.zIndex = '2147483646'; + kpxcCustomLoginFieldsBanner.chooser = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-fields' }); + + const bannerInfo = kpxcUI.createElement('div', 'banner-info'); + const bannerButtons = kpxcUI.createElement('div', 'banner-buttons'); + + const iconClassName = isFirefox() ? 'kpxc-banner-icon-moz' : 'kpxc-banner-icon'; + const icon = kpxcUI.createElement('span', iconClassName); + const infoText = kpxcUI.createElement('span', '', {}, tr('defineChooseCustomLoginFieldText')); + const separator = kpxcUI.createElement('div', 'kpxc-separator'); + const secondSeparator = kpxcUI.createElement('div', 'kpxc-separator'); + + const resetButton = kpxcUI.createButton(BLUE_BUTTON, tr('defineReset'), kpxcCustomLoginFieldsBanner.reset); + const usernameButton = kpxcUI.createButton(ORANGE_BUTTON, tr('username'), kpxcCustomLoginFieldsBanner.usernameButtonClicked); + const passwordButton = kpxcUI.createButton(RED_BUTTON, tr('password'), kpxcCustomLoginFieldsBanner.passwordButtonClicked); + const totpButton = kpxcUI.createButton(GREEN_BUTTON, 'TOTP', kpxcCustomLoginFieldsBanner.totpButtonClicked); + const stringFieldsButton = kpxcUI.createButton(BLUE_BUTTON, tr('stringFields'), kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked); + const clearDataButton = kpxcUI.createButton(RED_BUTTON, tr('defineClearData'), kpxcCustomLoginFieldsBanner.clearData); + const confirmButton = kpxcUI.createButton(GREEN_BUTTON, tr('defineConfirm'), kpxcCustomLoginFieldsBanner.confirm); + const closeButton = kpxcUI.createButton(RED_BUTTON, tr('defineClose'), kpxcCustomLoginFieldsBanner.closeButtonClicked); + closeButton.style.minWidth = Pixels(64); + + confirmButton.disabled = true; + kpxcCustomLoginFieldsBanner.banner = banner; + kpxcCustomLoginFieldsBanner.infoText = infoText; + kpxcCustomLoginFieldsBanner.buttons.reset = resetButton; + kpxcCustomLoginFieldsBanner.buttons.clearData = clearDataButton; + kpxcCustomLoginFieldsBanner.buttons.confirm = confirmButton; + kpxcCustomLoginFieldsBanner.buttons.close = closeButton; + kpxcCustomLoginFieldsBanner.buttons.username = usernameButton; + kpxcCustomLoginFieldsBanner.buttons.password = passwordButton; + kpxcCustomLoginFieldsBanner.buttons.totp = totpButton; + kpxcCustomLoginFieldsBanner.buttons.stringFields = stringFieldsButton; + + bannerInfo.appendMultiple(icon, infoText); + bannerButtons.appendMultiple(resetButton, separator, usernameButton, + passwordButton, totpButton, stringFieldsButton, secondSeparator, clearDataButton, confirmButton, closeButton); + banner.appendMultiple(bannerInfo, bannerButtons); + + const location = kpxc.getDocumentLocation(); + kpxcCustomLoginFieldsBanner.buttons.clearData.style.display + = kpxc.settings[DEFINED_CUSTOM_FIELDS] && kpxc.settings[DEFINED_CUSTOM_FIELDS][location] + ? 'inline-block' : 'none'; + if (window.self !== window.top && kpxcCustomLoginFieldsBanner.buttons.clearData.style.display === 'inline-block') { + sendMessageToParent('enable_clear_data_button'); + } + + initColorTheme(banner); + + const bannerStyleSheet = createStylesheet('css/banner.css'); + const defineStyleSheet = createStylesheet('css/define.css'); + const buttonStyleSheet = createStylesheet('css/button.css'); + const colorStyleSheet = createStylesheet('css/colors.css'); + + const wrapper = document.createElement('div'); + this.shadowRoot = wrapper.attachShadow({ mode: 'closed' }); + this.shadowRoot.append(colorStyleSheet); + this.shadowRoot.append(bannerStyleSheet); + this.shadowRoot.append(defineStyleSheet); + this.shadowRoot.append(buttonStyleSheet); + + // Only create the banner to top window + if (window.self === window.top) { + this.shadowRoot.append(banner); + } + + this.shadowRoot.append(kpxcCustomLoginFieldsBanner.chooser); + kpxcCustomLoginFieldsBanner.wrapper = wrapper; + + if (!kpxcCustomLoginFieldsBanner.created) { + window.self.document.body.appendChild(wrapper); + kpxcCustomLoginFieldsBanner.created = true; + + if (window.self === window.top) { + // Listen messages from iframes + window.addEventListener('message', handleTopWindowMessage, false); + } else { + // Listen messages from top window + window.addEventListener('message', handleParentWindowMessage, false); + } + } + + document.addEventListener('keydown', kpxcCustomLoginFieldsBanner.keyDown); +}; + +kpxcCustomLoginFieldsBanner.removeSelection = function(selection, fieldClass) { + const inputField = kpxcFields.getElementFromXPathId(selection[0]); + const index = kpxcCustomLoginFieldsBanner.markedFields.indexOf(inputField); + if (index >= 0) { + removeContent(fieldClass); + kpxcCustomLoginFieldsBanner.markedFields.splice(index, 1); + } +}; + +kpxcCustomLoginFieldsBanner.enableAllButtons = function() { + for (const button of Object.values(kpxcCustomLoginFieldsBanner.buttons)) { + button.classList.remove(GRAY_BUTTON_CLASS); + } +}; + +kpxcCustomLoginFieldsBanner.usernameButtonClicked = function(e) { + // Cancel the current selection if button is clicked again + if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_USERNAME) { + kpxcCustomLoginFieldsBanner.backToStart(); + return; + } + + // Reset username field selection if already set + if (kpxcCustomLoginFieldsBanner.selection.username) { + kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.username, `div.${USERNAME_FIELD_CLASS}`); + kpxcCustomLoginFieldsBanner.selection.username = undefined; + } + + kpxcCustomLoginFieldsBanner.prepareUsernameSelection(); + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true; + + sendMessageToFrames('username_button_clicked'); +}; + +kpxcCustomLoginFieldsBanner.passwordButtonClicked = function(e) { + if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_PASSWORD) { + kpxcCustomLoginFieldsBanner.backToStart(); + return; + } + + // Reset password field selection if already set + if (kpxcCustomLoginFieldsBanner.selection.password) { + kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.password, `div.${PASSWORD_FIELD_CLASS}`); + kpxcCustomLoginFieldsBanner.selection.password = undefined; + } + + kpxcCustomLoginFieldsBanner.preparePasswordSelection(); + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true; + + sendMessageToFrames('password_button_clicked'); +}; + +kpxcCustomLoginFieldsBanner.totpButtonClicked = function(e) { + if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_TOTP) { + kpxcCustomLoginFieldsBanner.backToStart(); + return; + } + + // Reset TOTP field selection if already set + if (kpxcCustomLoginFieldsBanner.selection.totp) { + kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.totp, `div.${TOTP_FIELD_CLASS}`); + kpxcCustomLoginFieldsBanner.selection.totp = undefined; + } + + kpxcCustomLoginFieldsBanner.prepareTOTPSelection(); + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true; + + sendMessageToFrames('totp_button_clicked'); +}; + +kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked = function(e) { + if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) { + kpxcCustomLoginFieldsBanner.backToStart(); + return; + } + + // Reset String Field selection if already set + if (kpxcCustomLoginFieldsBanner.selection.fields.length > 0) { + for (const field of kpxcCustomLoginFieldsBanner.selection.fields) { + kpxcCustomLoginFieldsBanner.removeSelection(field, `div.${STRING_FIELD_CLASS}`); + } + + kpxcCustomLoginFieldsBanner.selection.fields = []; + } + + kpxcCustomLoginFieldsBanner.prepareStringFieldSelection(); + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true; + + sendMessageToFrames('string_field_button_clicked'); +}; + +kpxcCustomLoginFieldsBanner.closeButtonClicked = function(e) { + if (!e.isTrusted) { + return; + } + + kpxcCustomLoginFieldsBanner.destroy(); + + sendMessageToFrames('close_button_clicked'); +}; + +// Updates the possible selections if the page content has been changed +kpxcCustomLoginFieldsBanner.updateFieldSelections = function() { + if (kpxcCustomLoginFieldsBanner.dataStep === STEP_NONE && kpxcCustomLoginFieldsBanner.markedFields.length === 0) { + return; + } + + kpxcCustomLoginFieldsBanner.removeMarkedFields(); + + if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_USERNAME) { + kpxcCustomLoginFieldsBanner.prepareUsernameSelection(); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_PASSWORD) { + kpxcCustomLoginFieldsBanner.preparePasswordSelection(); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_TOTP) { + kpxcCustomLoginFieldsBanner.prepareTOTPSelection(); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) { + kpxcCustomLoginFieldsBanner.prepareStringFieldSelection(); + } +}; + +// Reset selections +kpxcCustomLoginFieldsBanner.reset = function() { + kpxcCustomLoginFieldsBanner.resetSelection(); + + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true; + kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseCustomLoginFieldText'); + kpxcCustomLoginFieldsBanner.buttons.close.textContent = tr('defineClose'); + kpxcCustomLoginFieldsBanner.dataStep = STEP_NONE; + + kpxcCustomLoginFieldsBanner.enableAllButtons(); + + sendMessageToFrames('reset_button_clicked'); +}; + +// Confirm and save the selections +kpxcCustomLoginFieldsBanner.confirm = async function() { + if (!kpxc.settings[DEFINED_CUSTOM_FIELDS]) { + kpxc.settings[DEFINED_CUSTOM_FIELDS] = {}; + } + + // If the new selection is already used in some other field, clear it + const clearIdenticalField = function(path, location) { + const currentSite = kpxc.settings[DEFINED_CUSTOM_FIELDS][location]; + if (currentSite.username && currentSite.username[0] === path[0]) { + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].username = undefined; + } else if (currentSite.password && currentSite.password[0] === path[0]) { + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].password = undefined; + } else if (currentSite.totp && currentSite.totp[0] === path[0]) { + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].totp = undefined; + } + }; + + const usernamePath = kpxcCustomLoginFieldsBanner.selection.username; + const passwordPath = kpxcCustomLoginFieldsBanner.selection.password; + const totpPath = kpxcCustomLoginFieldsBanner.selection.totp; + const stringFieldsPaths = kpxcCustomLoginFieldsBanner.selection.fields; + const location = kpxc.getDocumentLocation(); + const currentSettings = kpxc.settings[DEFINED_CUSTOM_FIELDS][location]; + + if (currentSettings) { + // Update the single selection to current settings + if (usernamePath) { + clearIdenticalField(usernamePath, location); + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].username = usernamePath; + } + + if (passwordPath) { + clearIdenticalField(passwordPath, location); + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].password = passwordPath; + } + + if (totpPath) { + clearIdenticalField(totpPath, location); + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].totp = totpPath; + } + + if (stringFieldsPaths.length > 0) { + kpxc.settings[DEFINED_CUSTOM_FIELDS][location].fields = stringFieldsPaths; + } + } else { + // Override all fields (default) + kpxc.settings[DEFINED_CUSTOM_FIELDS][location] = { + username: usernamePath, + password: passwordPath, + totp: totpPath, + fields: stringFieldsPaths + }; + } + + await sendMessage('save_settings', kpxc.settings); + kpxcCustomLoginFieldsBanner.destroy(); + + sendMessageToFrames('confirm_button_clicked'); +}; + +// Clears the previously saved data from settings +kpxcCustomLoginFieldsBanner.clearData = async function() { + const location = kpxc.getDocumentLocation(); + delete kpxc.settings[DEFINED_CUSTOM_FIELDS][location]; + + await sendMessage('save_settings', kpxc.settings); + await sendMessage('load_settings'); + + kpxcCustomLoginFieldsBanner.buttons.clearData.style.display = 'none'; + + sendMessageToFrames('clear_data_button_clicked'); +}; + +// Resets all selections and marked fields +kpxcCustomLoginFieldsBanner.resetSelection = function() { + kpxcCustomLoginFieldsBanner.selection = { + username: undefined, + password: undefined, + totp: undefined, + fields: [] + }; + + kpxcCustomLoginFieldsBanner.removeMarkedFields(); +}; + +kpxcCustomLoginFieldsBanner.removeMarkedFields = function() { + while (kpxcCustomLoginFieldsBanner.chooser.firstChild) { + kpxcCustomLoginFieldsBanner.chooser.firstChild.remove(); + } + + kpxcCustomLoginFieldsBanner.markedFields = []; +}; + +kpxcCustomLoginFieldsBanner.prepareUsernameSelection = function() { + kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseUsername'); + kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_USERNAME; + kpxcCustomLoginFieldsBanner.buttons.username.classList.remove(GRAY_BUTTON_CLASS); + kpxcCustomLoginFieldsBanner.selectField('username'); +}; + +kpxcCustomLoginFieldsBanner.preparePasswordSelection = function() { + kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChoosePassword'); + kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_PASSWORD; + kpxcCustomLoginFieldsBanner.buttons.password.classList.remove(GRAY_BUTTON_CLASS); + kpxcCustomLoginFieldsBanner.selectField('password'); +}; + +kpxcCustomLoginFieldsBanner.prepareTOTPSelection = function() { + kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseTOTP'); + kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_TOTP; + kpxcCustomLoginFieldsBanner.buttons.totp.classList.remove(GRAY_BUTTON_CLASS); + kpxcCustomLoginFieldsBanner.selectField('totp'); +}; + +kpxcCustomLoginFieldsBanner.prepareStringFieldSelection = function() { + kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseStringFields'); + kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_STRING_FIELDS; + kpxcCustomLoginFieldsBanner.buttons.stringFields.classList.remove(GRAY_BUTTON_CLASS); + kpxcCustomLoginFieldsBanner.selectStringFields(); +}; + +kpxcCustomLoginFieldsBanner.isFieldSelected = function(field) { + const currentFieldId = kpxcFields.setId(field); + + if (kpxcCustomLoginFieldsBanner.markedFields.some(f => f === field)) { + return ( + (kpxcCustomLoginFieldsBanner.selection.username && kpxcCustomLoginFieldsBanner.selection.usernameElement === field) + || (kpxcCustomLoginFieldsBanner.selection.password && kpxcCustomLoginFieldsBanner.selection.passwordElement === field) + || (kpxcCustomLoginFieldsBanner.selection.totp && kpxcCustomLoginFieldsBanner.selection.totpElement === field) + || kpxcCustomLoginFieldsBanner.selection.fields.some(f => f[0] === currentFieldId[0]) + ); + } + + return false; +}; + +kpxcCustomLoginFieldsBanner.getSelectedField = function(e, elem) { + if (!e.isTrusted) { + return undefined; + } + + const field = elem || e.currentTarget; + if (kpxcCustomLoginFieldsBanner.markedFields.includes(field.originalElement)) { + return undefined; + } + + return field; +}; + +kpxcCustomLoginFieldsBanner.setSelectedField = function(elem) { + if (elem) { + kpxcCustomLoginFieldsBanner.markedFields.push(elem); + } + + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = false; + kpxcCustomLoginFieldsBanner.buttons.close.textContent = tr('optionsButtonCancel'); +}; + +// Expects 'username', 'password' or 'totp' +kpxcCustomLoginFieldsBanner.selectField = function(fieldType) { + kpxcCustomLoginFieldsBanner.eventFieldClick = function(e) { + const field = kpxcCustomLoginFieldsBanner.getSelectedField(e); + if (!field) { + return; + } + + if (isLightThemeBackground(field.originalElement)) { + field.classList.add(DARK_TEXT_CLASS); + } + + field.classList.add(`kpxcDefine-fixed-${fieldType}-field`); + field.textContent = tr(fieldType); + field.onclick = undefined; + + kpxcCustomLoginFieldsBanner.selection[fieldType] = kpxcFields.setId(field.originalElement); + kpxcCustomLoginFieldsBanner.selection[`${fieldType}Element`] = field.originalElement; + kpxcCustomLoginFieldsBanner.setSelectedField(field.originalElement); + kpxcCustomLoginFieldsBanner.backToStart(); + + kpxcCustomLoginFieldsBanner.buttons[fieldType].classList.add(GRAY_BUTTON_CLASS); + sendMessageToParent(`${fieldType}_selected`, kpxcCustomLoginFieldsBanner.selection[fieldType]); + }; + + kpxcCustomLoginFieldsBanner.markFields(); +}; + +kpxcCustomLoginFieldsBanner.selectStringFields = function() { + kpxcCustomLoginFieldsBanner.eventFieldClick = function(e) { + const field = kpxcCustomLoginFieldsBanner.getSelectedField(e); + if (!field) { + return; + } + + if (isLightThemeBackground(field.originalElement)) { + field.classList.add(DARK_TEXT_CLASS); + } + + kpxcCustomLoginFieldsBanner.selection.fields.push(kpxcFields.setId(field.originalElement)); + kpxcCustomLoginFieldsBanner.setSelectedField(field.originalElement); + + field.classList.add(STRING_FIELD_CLASS); + field.textContent = `${tr('defineStringField')} #${String(kpxcCustomLoginFieldsBanner.selection.fields.length)}`; + field.onclick = undefined; + + kpxcCustomLoginFieldsBanner.buttons.stringFields.classList.add(GRAY_BUTTON_CLASS); + sendMessageToParent('string_field_selected', kpxcCustomLoginFieldsBanner.selection.fields); + }; + + kpxcCustomLoginFieldsBanner.markFields(); +}; + +kpxcCustomLoginFieldsBanner.markFields = function() { + let index = 1; + let firstInput; + const inputs = document.querySelectorAll(kpxcCustomLoginFieldsBanner.inputQueryPattern); + + for (const i of inputs) { + if (kpxcCustomLoginFieldsBanner.isFieldSelected(i) || inputFieldIsSelected(i)) { + // Switch texts to current selection type + for (const selection of kpxcCustomLoginFieldsBanner.getNonSelectedElements()) { + selection.textContent = dataStepToString(); + } + + continue; + } + + const field = kpxcUI.createElement('div', FIXED_FIELD_CLASS); + field.originalElement = i; + + const rect = i.getBoundingClientRect(); + field.style.top = Pixels(rect.top); + field.style.left = Pixels(rect.left); + field.style.width = Pixels(rect.width); + field.style.height = Pixels(rect.height); + field.textContent = dataStepToString(); + + // Change selection theme if needed + const isLightTheme = isLightThemeBackground(i); + if (isLightTheme) { + field.classList.add(DARK_FIXED_FIELD_CLASS); + } + + field.addEventListener('click', function(e) { + kpxcCustomLoginFieldsBanner.eventFieldClick(e); + }); + + field.addEventListener('mouseenter', function() { + field.classList.add(isLightTheme ? DARK_HOVER_FIELD_CLASS : HOVER_FIELD_CLASS); + }); + + field.addEventListener('mouseleave', function() { + field.classList.remove(HOVER_FIELD_CLASS, DARK_HOVER_FIELD_CLASS); + }); + + i.addEventListener('focus', function() { + field.classList.add(isLightTheme ? DARK_HOVER_FIELD_CLASS : HOVER_FIELD_CLASS); + }); + + i.addEventListener('blur', function() { + field.classList.remove(HOVER_FIELD_CLASS, DARK_HOVER_FIELD_CLASS); + }); + + if (kpxcCustomLoginFieldsBanner.chooser) { + kpxcCustomLoginFieldsBanner.setSelectionPosition(field); + kpxcCustomLoginFieldsBanner.monitorSelectionPosition(field); + + kpxcCustomLoginFieldsBanner.chooser.append(field); + firstInput = field; + ++index; + } + } + + if (firstInput) { + firstInput.focus(); + } +}; + +// Returns to start after a single selection +kpxcCustomLoginFieldsBanner.backToStart = function() { + removeContent(kpxcCustomLoginFieldsBanner.nonSelectedElementsPattern); + kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseCustomLoginFieldText'); + kpxcCustomLoginFieldsBanner.dataStep = STEP_NONE; + + kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = kpxcCustomLoginFieldsBanner.markedFields.length === 0; +}; + +// Handle keyboard events +kpxcCustomLoginFieldsBanner.keyDown = function(e) { + if (!e.isTrusted) { + return; + } + + // Works as a cancel when selection process is active + if (e.key === 'Escape') { + if (kpxcCustomLoginFieldsBanner.dataStep === STEP_NONE) { + kpxcCustomLoginFieldsBanner.destroy(); + } else { + kpxcCustomLoginFieldsBanner.backToStart(); + } + } +}; + +// Detect page scroll or resize changes +kpxcCustomLoginFieldsBanner.monitorSelectionPosition = function(selection) { + // Handle icon position on resize + window.addEventListener('resize', function(e) { + kpxcCustomLoginFieldsBanner.setSelectionPosition(selection); + }); + + // Handle icon position on scroll + window.addEventListener('scroll', function(e) { + kpxcCustomLoginFieldsBanner.setSelectionPosition(selection); + }); +}; + +// Set selection input field position dynamically including the scroll position +kpxcCustomLoginFieldsBanner.setSelectionPosition = function(field) { + const rect = field.originalElement.getBoundingClientRect(); + const left = kpxcUI.getRelativeLeftPosition(rect); + const top = kpxcUI.getRelativeTopPosition(rect); + const scrollTop = document.scrollingElement ? document.scrollingElement.scrollTop : 0; + const scrollLeft = document.scrollingElement ? document.scrollingElement.scrollLeft : 0; + + field.style.top = Pixels(top + scrollTop); + field.style.left = Pixels(left + scrollLeft); +}; + +kpxcCustomLoginFieldsBanner.getNonSelectedElements = function() { + return kpxcCustomLoginFieldsBanner.chooser.querySelectorAll(kpxcCustomLoginFieldsBanner.nonSelectedElementsPattern); +}; + +const removeContent = function(pattern) { + const elems = kpxcCustomLoginFieldsBanner.chooser.querySelectorAll(pattern); + for (const e of elems) { + e.remove(); + } +}; + +const inputFieldIsSelected = function(field) { + for (const child of kpxcCustomLoginFieldsBanner.chooser.children) { + if (child.originalElement === field) { + return true; + } + } + + return false; +}; + +// Checks if an element has a light background, using luminance. Luminance with >= 127 is considered 'light'. +const isLightThemeBackground = function(elem) { + const inputStyle = getComputedStyle(elem); + const bgColor = inputStyle.backgroundColor.match(/[\.\d]+/g).map(e => Number(e)); + if (bgColor.length < 3) { + return false; + } + + const luminance = 0.299 * bgColor[0] + 0.587 * bgColor[1] + 0.114 * bgColor[2]; + return luminance >= 128; +}; + +const dataStepToString = function() { + if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_USERNAME) { + return tr('username'); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_PASSWORD) { + return tr('password'); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_TOTP) { + return tr('totp'); + } else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) { + return tr('defineStringField'); + } +} + +//-------------------------------------------------------------------------- +// IFrame support +//-------------------------------------------------------------------------- + +// A simple check for top-level-domain +const topLevelDomainMatches = function(host) { + if (!host) { + return false; + } + + const originUrl = new URL(host); + const frameUrl = new URL(window.self.document.location.origin); + const urlParts = originUrl.host.split('.'); + const dotCount = urlParts.length - 1; + + // Simple host like google.com, check directly + if (dotCount < 1) { + return false; + } else if (dotCount === 1) { + return frameUrl.host.includes(originUrl.host); + } + + // Get the top-level-domain using counts of '.' but backwards, max 3. + // A basic host is like idmsa.apple.com, a more complex one like www.bbva.com.ar. + const index = Math.min(dotCount, 3); + const subDomain = `${urlParts[dotCount - index]}.`; + const topLevelDomain = originUrl.host.substring(originUrl.host.indexOf(subDomain) + subDomain.length); + + return frameUrl.host.includes(topLevelDomain); +}; + +// Handles messages sent from iframes to the top window +const handleTopWindowMessage = function(e) { + if (!topLevelDomainMatches(e.origin)) { + return; + } + + if (e.data.message === 'username_selected') { + kpxcCustomLoginFieldsBanner.selection.username = e.data.selection; + kpxcCustomLoginFieldsBanner.setSelectedField(); + } else if (e.data.message === 'password_selected') { + kpxcCustomLoginFieldsBanner.selection.password = e.data.selection; + kpxcCustomLoginFieldsBanner.setSelectedField(); + } else if (e.data.message === 'totp_selected') { + kpxcCustomLoginFieldsBanner.selection.totp = e.data.selection; + kpxcCustomLoginFieldsBanner.setSelectedField(); + } else if (e.data.message === 'string_field_selected') { + kpxcCustomLoginFieldsBanner.selection.stringFields = e.data.selection; + kpxcCustomLoginFieldsBanner.setSelectedField(); + } else if (e.data.message === 'enable_clear_data_button') { + kpxcCustomLoginFieldsBanner.buttons.clearData.style.display = 'inline-block'; + } +}; + +// Handle Banner button clicks from the top window +const handleParentWindowMessage = function(e) { + if (!topLevelDomainMatches(e.origin)) { + return; + } + + if (e.data === 'username_button_clicked') { + kpxcCustomLoginFieldsBanner.usernameButtonClicked(e); + } else if (e.data === 'password_button_clicked') { + kpxcCustomLoginFieldsBanner.passwordButtonClicked(e); + } else if (e.data === 'totp_button_clicked') { + kpxcCustomLoginFieldsBanner.totpButtonClicked(e); + } else if (e.data === 'string_field_button_clicked') { + kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked(e); + } else if (e.data === 'reset_button_clicked') { + kpxcCustomLoginFieldsBanner.reset(); + } else if (e.data === 'close_button_clicked') { + kpxcCustomLoginFieldsBanner.closeButtonClicked(e); + } else if (e.data === 'confirm_button_clicked') { + kpxcCustomLoginFieldsBanner.confirm(); + } else if (e.data === 'clear_data_button_clicked') { + kpxcCustomLoginFieldsBanner.clearData(); + } +}; + +// Sends messages to all iframes. Works only from the top window. +const sendMessageToFrames = function(message) { + if (window.self === window.top) { + for (var i = 0; i < window.frames.length; i++) { + frames[i].postMessage(message, '*'); + } + } +}; + +// Sends message to parent window. Works only from iframes. +const sendMessageToParent = function(message, selection) { + if (window.self !== window.top) { + window.top.postMessage({ message, selection }, '*'); + } +} diff --git a/keepassxc-browser/content/define.js b/keepassxc-browser/content/define.js deleted file mode 100644 index 20cd1ea..0000000 --- a/keepassxc-browser/content/define.js +++ /dev/null @@ -1,496 +0,0 @@ -'use strict'; - -var kpxcDefine = {}; - -kpxcDefine.selection = { - username: null, - password: null, - totp: null, - fields: [] -}; - -kpxcDefine.buttons = { - again: undefined, - confirm: undefined, - discard: undefined, - dismiss: undefined, - more: undefined, - skip: undefined -}; - -kpxcDefine.backdrop = undefined; -kpxcDefine.chooser = undefined; -kpxcDefine.dialog = undefined; -kpxcDefine.diffX = 0; -kpxcDefine.diffY = 0; -kpxcDefine.discardSection = undefined; -kpxcDefine.eventFieldClick = undefined; -kpxcDefine.headline = undefined; -kpxcDefine.help = undefined; -kpxcDefine.inputQueryPattern = 'input[type=email], input[type=number], input[type=password], input[type=tel], input[type=text], input[type=username], input:not([type])'; -kpxcDefine.keyboardSelectorPattern = 'div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field):not(.kpxcDefine-fixed-password-field):not(.kpxcDefine-fixed-totp-field)'; -kpxcDefine.moreInputQueryPattern = 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=file]):not([type=hidden]):not([type=image]):not([type=month]):not([type=range]):not([type=reset]):not([type=submit]):not([type=time]):not([type=week]), select, textarea'; -kpxcDefine.markedFields = []; -kpxcDefine.keyDown = undefined; -kpxcDefine.startPosX = 0; -kpxcDefine.startPosY = 0; - -kpxcDefine.init = async function() { - kpxcDefine.backdrop = kpxcUI.createElement('div', 'kpxcDefine-modal-backdrop', { 'id': 'kpxcDefine-backdrop' }); - kpxcDefine.chooser = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-fields' }); - kpxcDefine.dialog = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-description' }); - kpxcDefine.backdrop.append(kpxcDefine.dialog); - - const styleSheet = createStylesheet('css/define.css'); - const buttonStyleSheet = createStylesheet('css/button.css'); - const wrapper = document.createElement('div'); - - this.shadowRoot = wrapper.attachShadow({ mode: 'closed' }); - this.shadowRoot.append(styleSheet); - this.shadowRoot.append(buttonStyleSheet); - this.shadowRoot.append(kpxcDefine.backdrop); - this.shadowRoot.append(kpxcDefine.chooser); - document.body.append(wrapper); - - kpxcDefine.initDescription(); - kpxcDefine.resetSelection(); - kpxcDefine.prepareStep1(); - kpxcDefine.markAllUsernameFields(); - - kpxcDefine.dialog.onmousedown = function(e) { - kpxcDefine.mouseDown(e); - }; - - document.addEventListener('keydown', kpxcDefine.keyDown); -}; - -kpxcDefine.close = function() { - kpxcDefine.backdrop.remove(); - kpxcDefine.chooser.remove(); - document.removeEventListener('keydown', kpxcDefine.keyDown); -}; - -kpxcDefine.mouseDown = function(e) { - kpxcDefine.selected = kpxcDefine.dialog; - kpxcDefine.startPosX = e.clientX; - kpxcDefine.startPosY = e.clientY; - kpxcDefine.diffX = kpxcDefine.startPosX - kpxcDefine.dialog.offsetLeft; - kpxcDefine.diffY = kpxcDefine.startPosY - kpxcDefine.dialog.offsetTop; - return false; -}; - -kpxcDefine.initDescription = function() { - const description = kpxcDefine.dialog; - kpxcDefine.headline = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-chooser-headline' }); - kpxcDefine.help = kpxcUI.createElement('div', 'kpxcDefine-chooser-help', { 'id': 'kpxcDefine-help' }); - - // Show keyboard shortcuts help text - const keyboardHelp = kpxcUI.createElement('div', 'kpxcDefine-keyboardHelp', {}, `${tr('optionsKeyboardShortcutsHeader')}:`); - keyboardHelp.style.marginBottom = '5px'; - keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'Escape'), ' ' + tr('defineDismiss')); - keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'S'), ' ' + tr('defineSkip')); - keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'A'), ' ' + tr('defineAgain')); - keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'C'), ' ' + tr('defineConfirm')); - keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'M'), ' ' + tr('defineMore')); - keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'D'), ' ' + tr('defineDiscard')); - - description.appendMultiple(kpxcDefine.headline, kpxcDefine.help, keyboardHelp); - - const buttonDismiss = kpxcUI.createElement('button', 'kpxc-button kpxc-red-button', { 'id': 'kpxcDefine-btn-dismiss' }, tr('defineDismiss')); - buttonDismiss.addEventListener('click', kpxcDefine.close); - - const buttonSkip = kpxcUI.createElement('button', 'kpxc-button kpxc-orange-button', { 'id': 'kpxcDefine-btn-skip' }, tr('defineSkip')); - buttonSkip.style.marginRight = '5px'; - buttonSkip.addEventListener('click', kpxcDefine.skip); - - const buttonMore = kpxcUI.createElement('button', 'kpxc-button kpxc-orange-button', { 'id': 'kpxcDefine-btn-more' }, tr('defineMore')); - buttonMore.style.marginRight = '5px'; - buttonMore.style.marginLeft = '5px'; - buttonMore.addEventListener('click', kpxcDefine.more); - - const buttonAgain = kpxcUI.createElement('button', 'kpxc-button kpxc-blue-button', { 'id': 'kpxcDefine-btn-again' }, tr('defineAgain')); - buttonAgain.style.marginRight = '5px'; - buttonAgain.addEventListener('click', kpxcDefine.again); - - const buttonConfirm = kpxcUI.createElement('button', 'kpxc-button kpxc-green-button', { 'id': 'kpxcDefine-btn-confirm' }, tr('defineConfirm')); - buttonConfirm.style.marginRight = '15px'; - buttonConfirm.style.display = 'none'; - buttonConfirm.addEventListener('click', kpxcDefine.confirm); - - kpxcDefine.buttons.again = buttonAgain; - kpxcDefine.buttons.confirm = buttonConfirm; - kpxcDefine.buttons.dismiss = buttonDismiss; - kpxcDefine.buttons.more = buttonMore; - kpxcDefine.buttons.skip = buttonSkip; - description.appendMultiple(buttonConfirm, buttonSkip, buttonMore, buttonAgain, buttonDismiss); - - const location = kpxc.getDocumentLocation(); - if (kpxc.settings['defined-custom-fields'] && kpxc.settings['defined-custom-fields'][location]) { - const div = kpxcUI.createElement('div', 'alreadySelected', {}); - const defineDiscard = kpxcUI.createElement('p', '', {}, tr('defineAlreadySelected')); - const buttonDiscard = kpxcUI.createElement('button', 'kpxc-button kpxc-red-button', { 'id': 'kpxcDefine-btn-discard' }, tr('defineDiscard')); - buttonDiscard.style.marginTop = '5px'; - buttonDiscard.addEventListener('click', kpxcDefine.discard); - kpxcDefine.buttons.discard = buttonSkip; - kpxcDefine.discardSection = div; - - div.appendMultiple(defineDiscard, buttonDiscard); - description.append(div); - } -}; - -kpxcDefine.resetSelection = function() { - kpxcDefine.selection = { - username: null, - password: null, - totp: null, - fields: [] - }; - - kpxcDefine.markedFields = []; - - if (kpxcDefine.chooser) { - kpxcDefine.chooser.textContent = ''; - } -}; - -kpxcDefine.isFieldSelected = function(field) { - if (kpxcDefine.markedFields.some(f => f === field)) { - return ( - (kpxcDefine.selection.username && kpxcDefine.selection.username.originalElement === field) - || (kpxcDefine.selection.password && kpxcDefine.selection.password.originalElement === field) - || (kpxcDefine.selection.totp && kpxcDefine.selection.totp.originalElement === field) - || kpxcDefine.selection.fields.includes(field) - ); - } - - return false; -}; - -kpxcDefine.markAllUsernameFields = function() { - kpxcDefine.eventFieldClick = function(e, elem) { - if (!e.isTrusted) { - return; - } - - const field = elem || e.currentTarget; - field.classList.add('kpxcDefine-fixed-username-field'); - field.textContent = tr('username'); - field.onclick = null; - kpxcDefine.selection.username = field; - kpxcDefine.markedFields.push(field.originalElement); - - kpxcDefine.prepareStep2(); - kpxcDefine.markAllPasswordFields(); - }; - - kpxcDefine.markFields(kpxcDefine.inputQueryPattern); -}; - -kpxcDefine.markAllPasswordFields = function() { - kpxcDefine.eventFieldClick = function(e, elem) { - if (!e.isTrusted) { - return; - } - - const field = elem || e.currentTarget; - field.classList.add('kpxcDefine-fixed-password-field'); - field.textContent = tr('password'); - field.onclick = null; - kpxcDefine.selection.password = field; - kpxcDefine.markedFields.push(field.originalElement); - - kpxcDefine.prepareStep3(); - kpxcDefine.markAllTOTPFields(); - }; - - kpxcDefine.markFields('input[type=\'password\']'); -}; - -kpxcDefine.markAllStringFields = function() { - kpxcDefine.eventFieldClick = function(e, elem) { - if (!e.isTrusted) { - return; - } - - const field = elem || e.currentTarget; - if (kpxcDefine.isFieldSelected(field.originalElement)) { - return; - } - - kpxcDefine.selection.fields.push(field.originalElement); - kpxcDefine.markedFields.push(field.originalElement); - - field.classList.add('kpxcDefine-fixed-string-field'); - field.textContent = tr('defineStringField') + String(kpxcDefine.selection.fields.length); - field.onclick = null; - }; - - kpxcDefine.markFields(kpxcDefine.inputQueryPattern + ', select'); -}; - -kpxcDefine.markAllTOTPFields = function() { - kpxcDefine.eventFieldClick = function(e, elem) { - if (!e.isTrusted) { - return; - } - - const field = elem || e.currentTarget; - field.classList.add('kpxcDefine-fixed-totp-field'); - field.textContent = 'TOTP'; - field.onclick = null; - kpxcDefine.selection.totp = field; - kpxcDefine.markedFields.push(field.originalElement); - - kpxcDefine.prepareStep4(); - kpxcDefine.markAllStringFields(); - }; - - kpxcDefine.markFields(kpxcDefine.inputQueryPattern); -}; - -kpxcDefine.markFields = function(pattern) { - let index = 1; - let firstInput = null; - const inputs = document.querySelectorAll(pattern); - - for (const i of inputs) { - if (kpxcDefine.isFieldSelected(i)) { - continue; - } - - if (!kpxcFields.isVisible(i)) { - continue; - } - - const field = kpxcUI.createElement('div', 'kpxcDefine-fixed-field'); - field.originalElement = i; - - const rect = i.getBoundingClientRect(); - field.style.top = Pixels(rect.top); - field.style.left = Pixels(rect.left); - field.style.width = Pixels(rect.width); - field.style.height = Pixels(rect.height); - field.textContent = String(index); - - field.addEventListener('click', function(e) { - kpxcDefine.eventFieldClick(e); - }); - - field.addEventListener('mouseenter', function() { - field.classList.add('kpxcDefine-fixed-hover-field'); - }); - - field.addEventListener('mouseleave', function() { - field.classList.remove('kpxcDefine-fixed-hover-field'); - }); - - i.addEventListener('focus', function() { - field.classList.add('kpxcDefine-fixed-hover-field'); - }); - - i.addEventListener('blur', function() { - field.classList.remove('kpxcDefine-fixed-hover-field'); - }); - - if (kpxcDefine.chooser) { - kpxcDefine.chooser.append(field); - firstInput = field; - ++index; - } - } - - if (firstInput) { - firstInput.focus(); - } -}; - -kpxcDefine.prepareStep1 = function() { - kpxcDefine.help.style.marginBottom = '10px'; - kpxcDefine.help.textContent = tr('defineKeyboardText'); - - removeContent('div#kpxcDefine-fixed-field'); - kpxcDefine.headline.textContent = tr('defineChooseUsername'); - kpxcDefine.dataStep = 1; - - kpxcDefine.buttons.skip.style.display = 'inline-block'; - kpxcDefine.buttons.confirm.style.display = 'none'; - kpxcDefine.buttons.again.style.display = 'none'; - kpxcDefine.buttons.more.style.display = 'none'; -}; - -kpxcDefine.prepareStep2 = function() { - const help = kpxcDefine.help; - help.style.marginBottom = '10px'; - help.textContent = tr('defineKeyboardText'); - - removeContent('div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field)'); - removeContent('div.kpxcDefine-fixed.field'); - kpxcDefine.headline.textContent = tr('defineChoosePassword'); - kpxcDefine.dataStep = 2; - kpxcDefine.buttons.again.style.display = 'inline-block'; - kpxcDefine.buttons.more.style.display = 'inline-block'; -}; - -kpxcDefine.prepareStep3 = function() { - kpxcDefine.help.style.marginBottom = '10px'; - kpxcDefine.help.textContent = tr('defineHelpText'); - - removeContent('div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field):not(.kpxcDefine-fixed-password-field)'); - kpxcDefine.headline.textContent = tr('defineChooseTOTP'); - kpxcDefine.dataStep = 3; - kpxcDefine.buttons.skip.style.display = 'inline-block'; - kpxcDefine.buttons.again.style.display = 'inline-block'; - kpxcDefine.buttons.more.style.display = 'none'; - kpxcDefine.buttons.confirm.style.display = 'none'; -}; - -kpxcDefine.prepareStep4 = function() { - kpxcDefine.help.style.marginBottom = '10px'; - kpxcDefine.help.textContent = tr('defineHelpText'); - - removeContent('div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field):not(.kpxcDefine-fixed-password-field):not(.kpxcDefine-fixed-totp-field):not(.kpxcDefine-fixed-string-field)'); - kpxcDefine.headline.textContent = tr('defineConfirmSelection'); - kpxcDefine.dataStep = 4; - kpxcDefine.buttons.skip.style.display = 'none'; - kpxcDefine.buttons.more.style.display = 'none'; - kpxcDefine.buttons.again.style.display = 'inline-block'; - kpxcDefine.buttons.confirm.style.display = 'inline-block'; -}; - -kpxcDefine.skip = function() { - if (kpxcDefine.dataStep === 1) { - kpxcDefine.selection.username = null; - kpxcDefine.prepareStep2(); - kpxcDefine.markAllPasswordFields(); - } else if (kpxcDefine.dataStep === 2) { - kpxcDefine.selection.password = null; - kpxcDefine.prepareStep3(); - kpxcDefine.markAllTOTPFields(); - } else if (kpxcDefine.dataStep === 3) { - kpxcDefine.selection.totp = null; - kpxcDefine.prepareStep4(); - kpxcDefine.markAllStringFields(); - } -}; - -kpxcDefine.again = function() { - kpxcDefine.resetSelection(); - kpxcDefine.prepareStep1(); - kpxcDefine.markAllUsernameFields(); -}; - -kpxcDefine.more = function() { - if (kpxcDefine.dataStep === 1) { - kpxcDefine.prepareStep1(); - - // Reset previous marked fields when no usernames have been selected - if (kpxcDefine.markedFields.length === 0) { - kpxcDefine.resetSelection(); - } - } else if (kpxcDefine.dataStep === 2) { - kpxcDefine.prepareStep2(); - } else if (kpxcDefine.dataStep === 3) { - kpxcDefine.prepareStep3(); - } else if (kpxcDefine.dataStep === 4) { - kpxcDefine.prepareStep4(); - } - - kpxcDefine.markFields(kpxcDefine.moreInputQueryPattern); -}; - -kpxcDefine.confirm = async function() { - if (kpxcDefine.dataStep !== 4) { - return; - } - - if (!kpxc.settings['defined-custom-fields']) { - kpxc.settings['defined-custom-fields'] = {}; - } - - if (kpxcDefine.selection.username) { - kpxcDefine.selection.username = kpxcFields.setId(kpxcDefine.selection.username.originalElement); - } - - if (kpxcDefine.selection.password) { - kpxcDefine.selection.password = kpxcFields.setId(kpxcDefine.selection.password.originalElement); - } - - if (kpxcDefine.selection.totp) { - kpxcDefine.selection.totp = kpxcFields.setId(kpxcDefine.selection.totp.originalElement); - } - - const fieldIds = []; - for (const i of kpxcDefine.selection.fields) { - fieldIds.push(kpxcFields.setId(i)); - } - - const location = kpxc.getDocumentLocation(); - kpxc.settings['defined-custom-fields'][location] = { - username: kpxcDefine.selection.username, - password: kpxcDefine.selection.password, - totp: kpxcDefine.selection.totp, - fields: fieldIds - }; - - await sendMessage('save_settings', kpxc.settings); - kpxcDefine.close(); -}; - -kpxcDefine.discard = async function() { - if (!kpxcDefine.buttons.discard) { - return; - } - - const location = kpxc.getDocumentLocation(); - delete kpxc.settings['defined-custom-fields'][location]; - - await sendMessage('save_settings', kpxc.settings); - await sendMessage('load_settings'); - - kpxcDefine.discardSection.remove(); -}; - -// Handle keyboard events -kpxcDefine.keyDown = function(e) { - if (!e.isTrusted) { - return; - } - - if (e.key === 'Escape') { - kpxcDefine.close(); - } else if (e.key === 'Enter') { - e.preventDefault(); - } else if (e.keyCode >= 49 && e.keyCode <= 57) { - // Select input field by number - e.preventDefault(); - const index = e.keyCode - 48; - const inputFields = document.querySelectorAll(kpxcDefine.keyboardSelectorPattern); - - if (inputFields.length >= index) { - kpxcDefine.eventFieldClick(e, inputFields[index - 1]); - } - } else if (e.key === 's') { - e.preventDefault(); - kpxcDefine.skip(); - } else if (e.key === 'a') { - e.preventDefault(); - kpxcDefine.again(); - } else if (e.key === 'c') { - e.preventDefault(); - kpxcDefine.confirm(); - } else if (e.key === 'm') { - e.preventDefault(); - kpxcDefine.more(); - } else if (e.key === 'd') { - e.preventDefault(); - kpxcDefine.discard(); - } -}; - -const removeContent = function(pattern) { - const elems = kpxcDefine.chooser.querySelectorAll(pattern); - for (const e of elems) { - e.remove(); - } -}; diff --git a/keepassxc-browser/content/fields.js b/keepassxc-browser/content/fields.js index d51b6d7..7e75afc 100644 --- a/keepassxc-browser/content/fields.js +++ b/keepassxc-browser/content/fields.js @@ -62,6 +62,31 @@ kpxcFields.getAllCombinations = async function(inputs) { return combinations; }; +// If there are multiple combinations, return the first one where input field can be found inside the document. +// Used with Custom Login Fields where selected input fields might not be visible on the page yet, +// and there's an extra combination for those. Only used from popup fill. +kpxcFields.getCombinationFromAllInputs = function() { + const inputs = kpxcObserverHelper.getInputs(document.body); + + for (const combination of kpxc.combinations) { + for (const value of Object.values(combination)) { + if (Array.isArray(value)) { + for (const v of value) { + if (inputs.some(i => i === v)) { + return combination; + } + } + } else { + if (inputs.some(i => i === value)) { + return combination; + } + } + } + } + + return kpxc.combinations[0]; +}; + // Adds segmented TOTP fields to the combination if found kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { if (!kpxc.settings.showOTPIcon) { @@ -397,12 +422,6 @@ kpxcFields.useCustomLoginFields = async function() { kpxcTOTPIcons.newIcon(totp, kpxc.databaseState); } - // If not all expected fields are identified, return an empty combination - if ((creds.username && !username) || (creds.password && !password) || (creds.totp && !totp) - || (creds.fields.length !== stringFields.length)) { - return []; - } - const combinations = []; combinations.push({ username: username, diff --git a/keepassxc-browser/content/fill.js b/keepassxc-browser/content/fill.js index 198f80c..345a4f5 100644 --- a/keepassxc-browser/content/fill.js +++ b/keepassxc-browser/content/fill.js @@ -111,7 +111,8 @@ kpxcFill.fillFromPopup = async function(id, uuid) { combination = kpxc.combinations[1]; } - kpxcFill.fillInCredentials(combination, selectedCredentials.login, uuid); + const foundCombination = kpxcFields.getCombinationFromAllInputs(); + kpxcFill.fillInCredentials(foundCombination, selectedCredentials.login, uuid); kpxcUserAutocomplete.closeList(); }; @@ -222,7 +223,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui return; } - if (!combination || (!combination.username && !combination.password)) { + if (!combination) { logDebug('Error: Empty login combination.'); return; } @@ -286,8 +287,12 @@ kpxcFill.fillInStringFields = function(fields, stringFields) { const filledInFields = []; if (fields && stringFields && fields.length > 0 && stringFields.length > 0) { for (let i = 0; i < fields.length; i++) { - const currentField = fields[i]; + if (i >= stringFields.length) { + continue; + } + const stringFieldValue = Object.values(stringFields[i]); + const currentField = fields[i]; if (currentField && stringFieldValue[0]) { kpxc.setValue(currentField, stringFieldValue[0]); diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index eb1889c..a13598e 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -255,11 +255,11 @@ kpxc.initAutocomplete = function() { // Looks for any username & password combinations from the detected input fields kpxc.initCombinations = async function(inputs = []) { - if (inputs.length === 0) { + const isCustomLoginFieldsUsed = kpxcFields.isCustomLoginFieldsUsed(); + if (inputs.length === 0 && !isCustomLoginFieldsUsed) { return []; } - const isCustomLoginFieldsUsed = kpxcFields.isCustomLoginFieldsUsed(); const combinations = isCustomLoginFieldsUsed ? await kpxcFields.useCustomLoginFields() : await kpxcFields.getAllCombinations(inputs); @@ -285,6 +285,11 @@ kpxc.initCombinations = async function(inputs = []) { } } + // Update the fields in Custom Login Fields banner if it's open + if (kpxcCustomLoginFieldsBanner.created) { + kpxcCustomLoginFieldsBanner.updateFieldSelections(); + } + logDebug('Login field combinations identified:', combinations); return combinations; }; @@ -296,7 +301,7 @@ kpxc.initCredentialFields = async function() { // Search all remaining inputs from the page, ignore the previous input fields const pageInputs = await kpxcFields.getAllPageInputs(formInputs); - if (formInputs.length === 0 && pageInputs.length === 0) { + if (formInputs.length === 0 && pageInputs.length === 0 && !kpxcFields.isCustomLoginFieldsUsed()) { // Run 'redetect_credentials' manually if no fields are found after a page load setTimeout(async function() { if (_called.automaticRedetectCompleted) { @@ -821,7 +826,7 @@ browser.runtime.onMessage.addListener(async function(req, sender) { } else if (req.action === 'check_database_hash' && 'hash' in req) { kpxc.detectDatabaseChange(req); } else if (req.action === 'choose_credential_fields') { - kpxcDefine.init(); + kpxcCustomLoginFieldsBanner.create(); } else if (req.action === 'clear_credentials') { kpxc.clearAllFromPage(); } else if (req.action === 'fill_user_pass_with_specific_login') { diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index 508a160..b4d7d12 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -7,15 +7,15 @@ const MIN_INPUT_FIELD_OFFSET_WIDTH = 60; const MIN_OPACITY = 0.7; const MAX_OPACITY = 1; -let notificationWrapper; -let notificationTimeout; - const DatabaseState = { DISCONNECTED: 0, LOCKED: 1, UNLOCKED: 2 }; +let notificationWrapper; +let notificationTimeout; + // jQuery style wrapper for querySelector() const $ = function(elem) { return document.querySelector(elem); @@ -122,8 +122,8 @@ kpxcUI.setIconPosition = function(icon, field, rtl = false, segmented = false) { const rect = field.getBoundingClientRect(); const size = Number(icon.getAttribute('size')); const offset = kpxcUI.calculateIconOffset(field, size); - let left = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left; - let top = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.top - kpxcUI.bodyRect.top : rect.top; + let left = kpxcUI.getRelativeLeftPosition(rect); + let top = kpxcUI.getRelativeTopPosition(rect); // Add more space for the icon to show it at the right side of the field if TOTP fields are segmented if (segmented) { @@ -145,6 +145,14 @@ kpxcUI.setIconPosition = function(icon, field, rtl = false, segmented = false) { : Pixels(left + scrollLeft + field.offsetWidth - size - offset); }; +kpxcUI.getRelativeLeftPosition = function(rect) { + return kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left; +}; + +kpxcUI.getRelativeTopPosition = function(rect) { + return kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.top - kpxcUI.bodyRect.top : rect.top; +}; + kpxcUI.deleteHiddenIcons = function(iconList, attr) { const deletedIcons = []; for (const icon of iconList) { @@ -260,6 +268,12 @@ kpxcUI.createNotification = function(type, message) { }, 5000); }; +kpxcUI.createButton = function(color, textContent, callback) { + const button = kpxcUI.createElement('button', color, {}, textContent); + button.addEventListener('click', callback); + return button; +}; + const DOMRectToArray = function(domRect) { return [ domRect.bottom, domRect.height, domRect.left, domRect.right, domRect.top, domRect.width, domRect.x, domRect.y ]; }; @@ -267,8 +281,11 @@ const DOMRectToArray = function(domRect) { const initColorTheme = function(elem) { const colorTheme = kpxc.settings['colorTheme']; - if (colorTheme === undefined || colorTheme === 'system') { + if (colorTheme === undefined) { elem.removeAttribute('data-color-theme'); + } else if (colorTheme === 'system') { + const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + elem.setAttribute('data-color-theme', theme); } else { elem.setAttribute('data-color-theme', colorTheme); } @@ -302,16 +319,6 @@ document.addEventListener('mousemove', function(e) { kpxcPasswordDialog.dialog.style.top = Pixels(yPos); } } - - if (kpxcDefine.selected === kpxcDefine.dialog) { - const xPos = e.clientX - kpxcDefine.diffX; - const yPos = e.clientY - kpxcDefine.diffY; - - if (kpxcDefine.selected && kpxcDefine.dialog) { - kpxcDefine.dialog.style.left = Pixels(xPos); - kpxcDefine.dialog.style.top = Pixels(yPos); - } - } }); document.addEventListener('mousedown', function(e) { @@ -328,7 +335,6 @@ document.addEventListener('mouseup', function(e) { } kpxcPasswordDialog.selected = null; - kpxcDefine.selected = null; kpxcUI.mouseDown = false; }); diff --git a/keepassxc-browser/content/username-field.js b/keepassxc-browser/content/username-field.js index dc69427..0a17c44 100644 --- a/keepassxc-browser/content/username-field.js +++ b/keepassxc-browser/content/username-field.js @@ -21,7 +21,7 @@ kpxcUsernameIcons.isValid = function(field) { || field.offsetWidth < MIN_INPUT_FIELD_OFFSET_WIDTH || field.readOnly || kpxcIcons.hasIcon(field) - || !kpxcFields.isVisible(field)) { + || (!kpxcFields.isCustomLoginFieldsUsed() && !kpxcFields.isVisible(field))) { return false; } @@ -117,7 +117,7 @@ UsernameFieldIcon.prototype.createIcon = function(field) { }; const iconClicked = async function(field, icon) { - if (!kpxcFields.isVisible(field)) { + if (!kpxcFields.isCustomLoginFieldsUsed() && !kpxcFields.isVisible(field)) { icon.parentNode.removeChild(icon); field.removeAttribute('kpxc-username-field'); return; diff --git a/keepassxc-browser/css/banner.css b/keepassxc-browser/css/banner.css index ea3e3d7..47c082d 100644 --- a/keepassxc-browser/css/banner.css +++ b/keepassxc-browser/css/banner.css @@ -64,12 +64,33 @@ div.kpxc-banner .kpxc-banner-icon-moz { background-size: contain; } +div.kpxc-banner .kpxc-help-icon { + width: 24px; + height: 24px; + overflow: hidden; + background: url('chrome-extension://__MSG_@@extension_id__/icons/help.svg') right no-repeat; + background-size: contain; +} + +div.kpxc-banner .kpxc-help-icon-moz { + width: 24px; + height: 24px; + overflow: hidden; + background: url('moz-extension://__MSG_@@extension_id__/icons/help.svg') right no-repeat; + background-size: contain; +} + .kpxc-separator { border-left: 1px solid #ccc; height: 100% !important; margin: 10px !important; } +.kpxc-pick-info-text { + margin-left: 8px; + margin-right: 8px; +} + div.kpxc-banner .kpxc-checkbox { margin: 2px !important; } diff --git a/keepassxc-browser/css/button.css b/keepassxc-browser/css/button.css index 79df313..e3b75de 100644 --- a/keepassxc-browser/css/button.css +++ b/keepassxc-browser/css/button.css @@ -46,10 +46,12 @@ transition: all .15s; } -.kpxc-button:disabled { - border-color: #ccc !important; +.kpxc-button:disabled, .kpxc-gray-button { + background-color: #777777 !important; + border-color: #444444 !important; + color: #fff !important; } .kpxc-button:disabled:hover { - background-color: #fff !important; + background-color: #777777 !important; } diff --git a/keepassxc-browser/css/define.css b/keepassxc-browser/css/define.css index a5b1a9f..23921fd 100644 --- a/keepassxc-browser/css/define.css +++ b/keepassxc-browser/css/define.css @@ -1,117 +1,61 @@ -.kpxcDefine-modal-backdrop { - bottom: 0; - left: 0; - position: fixed; - right: 0; - top: 0; - z-index: 2147483645; -} - -.kpxcDefine-modal-backdrop:after { - background-color: #000000; - bottom: 0; - content: ''; - filter: alpha(opacity=50); - left: 0; - opacity: 0.5; - position: fixed; - right: 0; - top: 0; -} - #kpxcDefine-fields { z-index: 2147483646; } -#kpxcDefine-description { - background-color: rgba(0,0,0,0.8); - border: 2px solid #555555; - color: #efefef; - cursor: pointer; - font-size: 15px; - height: auto !important; - left: 150px; - padding: 7px 5px; - position: absolute; - text-align: left; - top: 100px; - user-select: none; - z-index: 2147483646; -} - -#kpxcDefine-description div:first-of-type { - color: #efefef; - font-weight: bold; - font-size: 120%; - margin-top: 0; - padding-bottom: 8px; - padding-top: 0; - text-align: left; -} - -#kpxcDefine-description p { - border-top: 2px solid #666666; - color: #efefef; - line-height: 110%; - margin-top: 10px; - padding-top: 10px; -} - -#kpxcDefine-help { - margin-bottom: 3px; -} - -.kpxcDefine-keyboardHelp { - font-size: 0.75em; - height: auto !important; -} - -.kpxcDefine-keyboardHelp kbd { - background-color: #eee; - border-radius: 3px; - border: 1px solid #b4b4b4; - color: #333; - display: inline-block; - height: auto !important; - line-height: 1; - padding: 2px 4px; - white-space: nowrap; -} - -.kpxcDefine-chooser-help { - height: auto !important; -} - .kpxcDefine-fixed-field { + align-items: center; background-color: rgba(239,239,239,0.4); border: 2px solid #efefef; color: #fff; cursor: pointer; + display: flex; font-weight: bold; + justify-content: center; position: absolute; - text-align: center; z-index: 2147483646; } +.kpxcDefine-fixed-field-dark { + background-color: rgba(45, 45, 45, 0.4); + border: 2px solid #0f0f0f; + color: #fff; +} + .kpxcDefine-fixed-hover-field { + background-color: rgba(255, 165, 238, 0.631); border: 2px solid orange; - background-color: rgba(255,165,239,0.4); +} + +.kpxcDefine-fixed-hover-field-dark { + background-color: rgba(255, 165, 238, 0.599); + border: 2px solid orange; + color: #505050; } .kpxcDefine-fixed-password-field { - border: 2px solid red; background-color: rgba(255,0,0,0.4); + border: 2px solid red; color: #efefef; } .kpxcDefine-fixed-username-field { - border: 2px solid limegreen; + background-color: rgba(232, 252, 3, 0.4); + border: 2px solid yellow; + color: #efefef; +} + +.kpxcDefine-fixed-totp-field { background-color: rgba(50,205,50,0.4); + border: 2px solid limegreen; color: #efefef; } -.kpxcDefine-fixed-string-field, .kpxcDefine-fixed-totp-field { - border: 2px solid deepskyblue; +.kpxcDefine-fixed-string-field { background-color: rgba(30,144,255,0.4); + border: 2px solid deepskyblue; color: #efefef; } + +.kpxcDefine-dark-text { + color: #505050; +} diff --git a/keepassxc-browser/icons/help.svg b/keepassxc-browser/icons/help.svg new file mode 100644 index 0000000..6a7bf81 --- /dev/null +++ b/keepassxc-browser/icons/help.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><g transform="matrix(0.983029,0,0,0.983029,-2.15274,-4.69129)"><circle cx="26.604" cy="29.187" r="20.345" style="fill:#0090ff"/></g><g transform="matrix(31.8224,0,0,31.8224,16.3008,35.374)"><path d="M.169-.218C.169-.263.175-.3.186-.327.197-.354.217-.38.247-.407.276-.433.296-.454.306-.471.315-.487.32-.505.32-.523.32-.578.295-.605.244-.605.22-.605.201-.598.186-.583.172-.568.164-.548.164-.522H.022C.023-.584.043-.633.082-.668.122-.703.176-.721.244-.721.313-.721.367-.704.405-.671.443-.637.462-.59.462-.529.462-.501.456-.475.443-.451.431-.426.409-.399.378-.369L.339-.331C.314-.307.3-.28.296-.248l-.002.03H.169zm-.014.15C.155-.09.163-.108.177-.122.192-.136.211-.143.234-.143S.276-.136.291-.122c.015.014.022.032.022.054C.313-.047.306-.029.292-.015.277-.001.258.006.234.006.211.006.191-.001.177-.015.163-.029.155-.047.155-.068z" style="fill:#fff;fill-rule:nonzero"/></g></svg>
\ No newline at end of file diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index cb74d70..8591052 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -58,7 +58,7 @@ "content/banner.js", "content/autocomplete.js", "content/credential-autocomplete.js", - "content/define.js", + "content/custom-fields-banner.js", "content/fields.js", "content/fill.js", "content/form.js", @@ -124,6 +124,7 @@ }, "web_accessible_resources": [ "icons/disconnected.svg", + "icons/help.svg", "icons/keepassxc.svg", "icons/key.svg", "icons/locked.svg", |