From 5cbde7eee184c1359adbb1e7df48cd005bb38fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= Date: Sun, 18 Aug 2024 07:32:48 +0300 Subject: [PATCH] Allow content scripts on XHTML pages (#2314) Allow content scripts on XHTML pages --- .eslintrc | 2 ++ keepassxc-browser/common/global.js | 11 ++++++++++ keepassxc-browser/common/sites.js | 4 ++-- keepassxc-browser/content/fields.js | 15 +++++++++---- keepassxc-browser/content/fill.js | 4 ++-- keepassxc-browser/content/form.js | 22 +++++++++++++------ .../content/keepassxc-browser.js | 6 +++-- keepassxc-browser/content/observer-helper.js | 6 ++--- keepassxc-browser/content/ui.js | 2 +- tests/global.spec.ts | 12 ++++++++++ 10 files changed, 63 insertions(+), 21 deletions(-) diff --git a/.eslintrc b/.eslintrc index 5ad12c7..902adb2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -148,6 +148,7 @@ "logError": true, "kpxcPasskeysUtils": true, "ManualFill": true, + "matchesWithNodeName": true, "MAX_AUTOCOMPLETE_NAME_LEN": true, "MAX_OPACITY": true, "MAX_TOTP_INPUT_LENGTH": true, @@ -156,6 +157,7 @@ "MIN_INPUT_FIELD_WIDTH_PX": true, "MIN_OPACITY": true, "MIN_TOTP_INPUT_LENGTH": true, + "module": true, "nacl": true, "ORANGE_BUTTON": true, "page": true, diff --git a/keepassxc-browser/common/global.js b/keepassxc-browser/common/global.js index 5336f17..45fcbfb 100755 --- a/keepassxc-browser/common/global.js +++ b/keepassxc-browser/common/global.js @@ -56,6 +56,16 @@ const ManualFill = { BOTH: 2 }; +// Checks if element's nodeName matches +const matchesWithNodeName = function(elem, name) { + // Don't allow undefined element or 'name' + if (!elem || !name) { + return false; + } + + return elem?.nodeName?.toUpperCase() === name?.toUpperCase(); +}; + // Match hostname or path with wildcards const matchWithRegex = function(firstUrlPart, secondUrlPart, hostnameUsed = false) { if (firstUrlPart === secondUrlPart) { @@ -149,6 +159,7 @@ const getCurrentTab = async function() { // Exports for tests if (typeof module === 'object') { module.exports = { + matchesWithNodeName, siteMatch, slashNeededForUrl, trimURL, diff --git a/keepassxc-browser/common/sites.js b/keepassxc-browser/common/sites.js index e7fc41e..d27b9cf 100644 --- a/keepassxc-browser/common/sites.js +++ b/keepassxc-browser/common/sites.js @@ -102,7 +102,7 @@ kpxcSites.exceptionFound = function(identifier, field) { * @returns {boolean} True if an Element has a match with the needed indentfifiers and document location */ kpxcSites.totpExceptionFound = function(field) { - if (!field || field.nodeName !== 'INPUT') { + if (!field || !matchesWithNodeName(field.nodeName, 'INPUT')) { return false; } @@ -121,7 +121,7 @@ kpxcSites.totpExceptionFound = function(field) { * @returns {boolean} True if an Element has a match with the needed indentfifiers and document location */ kpxcSites.segmentedTotpExceptionFound = function(form) { - if (!form || form.nodeName !== 'FORM') { + if (!form || !matchesWithNodeName(form.nodeName, 'FORM')) { return false; } diff --git a/keepassxc-browser/content/fields.js b/keepassxc-browser/content/fields.js index a08d8a3..7c2fcfb 100644 --- a/keepassxc-browser/content/fields.js +++ b/keepassxc-browser/content/fields.js @@ -114,7 +114,11 @@ kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { const addTotpFieldsToCombination = function(inputFields, ignoreFieldCount = false) { const totpInputs = Array.from(inputFields).filter( - e => e.nodeName === 'INPUT' && e.type !== 'password' && e.type !== 'hidden' && e.type !== 'submit', + (e) => + matchesWithNodeName(e.nodeName, 'INPUT') && + e.type !== 'password' && + e.type !== 'hidden' && + e.type !== 'submit' ); if (areFieldsSegmentedTotp(totpInputs, ignoreFieldCount)) { @@ -149,9 +153,12 @@ kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) { } // 7 inputs with a button as the last one (e.g. PayPal uses this) - if (currentForm.length === 7 - && (currentForm.lastChild.nodeName === 'BUTTON' - || (currentForm.lastChild.nodeName === 'INPUT' && currentForm.lastChild.type === 'button'))) { + if ( + currentForm.length === 7 && + (matchesWithNodeName(currentForm.lastChild.nodeName, 'BUTTON') || + (matchesWithNodeName(currentForm.lastChild.nodeName, 'INPUT') && + currentForm.lastChild.type === 'button')) + ) { return true; } diff --git a/keepassxc-browser/content/fill.js b/keepassxc-browser/content/fill.js index fcb5266..33387c1 100644 --- a/keepassxc-browser/content/fill.js +++ b/keepassxc-browser/content/fill.js @@ -146,7 +146,7 @@ kpxcFill.fillTOTPFromUuid = async function(el, uuid) { if (user.totp?.length > 0) { // Retrieve a new TOTP value - const totp = await sendMessage('get_totp', [user.uuid, user.totp]); + const totp = await sendMessage('get_totp', [ user.uuid, user.totp ]); if (!totp) { kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound')); return; @@ -246,7 +246,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui } // Fill password - if (combination.password && combination.password.nodeName === 'INPUT') { + if (combination.password && matchesWithNodeName(combination.password.nodeName, 'INPUT')) { // Show a notification if password length exceeds the length defined in input if (combination.password.maxLength && combination.password.maxLength > 0 diff --git a/keepassxc-browser/content/form.js b/keepassxc-browser/content/form.js index 3e4c9bc..29b57c7 100644 --- a/keepassxc-browser/content/form.js +++ b/keepassxc-browser/content/form.js @@ -49,15 +49,17 @@ kpxcForm.getFormSubmitButton = function(form) { // Try to find another button. Select the last one. // If any formaction overriding the default action is set, ignore those buttons. - const buttons = Array.from(form.querySelectorAll(kpxcForm.formButtonQuery)).filter(b => !b.getAttribute('formAction')); + const buttons = Array.from(form.querySelectorAll(kpxcForm.formButtonQuery)).filter( + b => !b.getAttribute('formAction') + ); if (buttons.length > 0) { return buttons.at(-1); } // Try to find similar buttons outside the form which are added via 'form' property for (const e of form.elements) { - if ((e.nodeName === 'BUTTON' && (e.type === 'button' || e.type === 'submit' || e.type === '')) - || (e.nodeName === 'INPUT' && (e.type === 'button' || e.type === 'submit'))) { + if ((matchesWithNodeName(e.nodeName, 'BUTTON') && (e.type === 'button' || e.type === 'submit' || e.type === '')) + || (matchesWithNodeName(e.nodeName, 'INPUT') && (e.type === 'button' || e.type === 'submit'))) { return e; } } @@ -125,13 +127,15 @@ kpxcForm.onSubmit = async function(e) { kpxcForm.submitTriggered = true; const searchForm = f => { - if (f.nodeName === 'FORM') { + if (matchesWithNodeName(f.nodeName, 'FORM')) { return f; } }; // Traverse parents if the form is not found. - let form = this.nodeName === 'FORM' ? this : kpxcFields.traverseParents(this, searchForm, searchForm, () => null); + let form = matchesWithNodeName(this.nodeName, 'FORM') + ? this + : kpxcFields.traverseParents(this, searchForm, searchForm, () => null); // Check for extra forms from sites.js if (!form) { @@ -202,7 +206,11 @@ kpxcForm.saveForm = function(form, combination) { username: combination.username, password: combination.password, totp: combination.totp, - totpInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && kpxcTOTPIcons.isValid(e)), - passwordInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type === 'password') + totpInputs: Array.from(form.elements).filter( + e => matchesWithNodeName(e.nodeName, 'INPUT') && kpxcTOTPIcons.isValid(e), + ), + passwordInputs: Array.from(form.elements).filter( + e => matchesWithNodeName(e.nodeName, 'INPUT') && e.type === 'password', + ) }); }; diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 76dd42b..307834b 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -600,7 +600,7 @@ kpxc.rememberCredentialsFromContextMenu = async function() { } const el = document.activeElement; - if (el.nodeName !== 'INPUT') { + if (!matchesWithNodeName(el.nodeName, 'INPUT')) { return; } @@ -938,7 +938,9 @@ kpxc.enablePasskeys = function() { */ const initContentScript = async function() { try { - if (document?.documentElement?.ownerDocument?.contentType !== 'text/html') { + if (document?.documentElement?.ownerDocument?.contentType !== 'text/html' + && document?.documentElement?.ownerDocument?.contentType !== 'application/xhtml+xml' + ) { return; } diff --git a/keepassxc-browser/content/observer-helper.js b/keepassxc-browser/content/observer-helper.js index 58aa1b8..2f76a95 100644 --- a/keepassxc-browser/content/observer-helper.js +++ b/keepassxc-browser/content/observer-helper.js @@ -73,7 +73,7 @@ kpxcObserverHelper.initObserver = async function() { } } else if (mut.type === 'attributes' && (mut.attributeName === 'class' || mut.attributeName === 'style')) { // Only accept targets with forms - const forms = mut.target.nodeName === 'FORM' ? mut.target : mut.target.getElementsByTagName('form'); + const forms = matchesWithNodeName(mut.target.nodeName, 'FORM') ? mut.target : mut.target.getElementsByTagName('form'); if (forms.length === 0 && !kpxcSites.exceptionFound(mut.target.classList, mut.target)) { continue; } @@ -146,7 +146,7 @@ kpxcObserverHelper.getInputs = function(target, ignoreVisibility = false) { } }); - if (target.nodeName === 'INPUT') { + if (matchesWithNodeName(target.nodeName, 'INPUT')) { inputFields.push(target); } @@ -304,7 +304,7 @@ const traverseChildren = function(target, inputFields, depth = 1) { continue; } - if (child.nodeName === 'INPUT') { + if (matchesWithNodeName(child.nodeName, 'INPUT')) { inputFields.push(child); } diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index 7196da3..5f70b4e 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -133,7 +133,7 @@ kpxcUI.monitorIconPosition = function(iconClass) { }); window.addEventListener('transitionend', function(e) { - if (e.target?.nodeName === 'INPUT' || e.target?.nodeName === 'TEXTAREA') { + if (matchesWithNodeName(e.target?.nodeName, 'INPUT') || matchesWithNodeName(e.target?.nodeName, 'TEXTAREA')) { kpxcUI.updateIconPosition(iconClass); } }); diff --git a/tests/global.spec.ts b/tests/global.spec.ts index 4d40f21..1151be5 100644 --- a/tests/global.spec.ts +++ b/tests/global.spec.ts @@ -1,10 +1,22 @@ import { test, expect } from '@playwright/test'; import { + matchesWithNodeName, siteMatch, slashNeededForUrl, trimURL } from '../keepassxc-browser/common/global.js'; +test('Test matchesWithNodeName()', async ({ page }) => { + const elem1 = { nodeName: 'INPUT' }; + const elem2 = { nodeName: 'input' }; + expect(matchesWithNodeName(elem1, 'INPUT')).toBe(true); + expect(matchesWithNodeName(elem1, 'INPUT')).toBe(true); + expect(matchesWithNodeName(elem2, 'INPUT')).toBe(true); + expect(matchesWithNodeName(elem1, 'TEXT')).toBe(false); + expect(matchesWithNodeName(undefined, 'INPUT')).toBe(false); + expect(matchesWithNodeName(undefined, undefined)).toBe(false); +}); + test('Test siteMatch()', async ({ page }) => { expect(siteMatch('https://example.com/*', 'https://example.com/login_page')).toBe(true); expect(siteMatch('https://*.lexample.com/*', 'https://example.com/login_page')).toBe(false);