mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Allow content scripts on XHTML pages (#2314)
Allow content scripts on XHTML pages
This commit is contained in:
parent
368117fad1
commit
5cbde7eee1
10 changed files with 63 additions and 21 deletions
|
|
@ -148,6 +148,7 @@
|
||||||
"logError": true,
|
"logError": true,
|
||||||
"kpxcPasskeysUtils": true,
|
"kpxcPasskeysUtils": true,
|
||||||
"ManualFill": true,
|
"ManualFill": true,
|
||||||
|
"matchesWithNodeName": true,
|
||||||
"MAX_AUTOCOMPLETE_NAME_LEN": true,
|
"MAX_AUTOCOMPLETE_NAME_LEN": true,
|
||||||
"MAX_OPACITY": true,
|
"MAX_OPACITY": true,
|
||||||
"MAX_TOTP_INPUT_LENGTH": true,
|
"MAX_TOTP_INPUT_LENGTH": true,
|
||||||
|
|
@ -156,6 +157,7 @@
|
||||||
"MIN_INPUT_FIELD_WIDTH_PX": true,
|
"MIN_INPUT_FIELD_WIDTH_PX": true,
|
||||||
"MIN_OPACITY": true,
|
"MIN_OPACITY": true,
|
||||||
"MIN_TOTP_INPUT_LENGTH": true,
|
"MIN_TOTP_INPUT_LENGTH": true,
|
||||||
|
"module": true,
|
||||||
"nacl": true,
|
"nacl": true,
|
||||||
"ORANGE_BUTTON": true,
|
"ORANGE_BUTTON": true,
|
||||||
"page": true,
|
"page": true,
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,16 @@ const ManualFill = {
|
||||||
BOTH: 2
|
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
|
// Match hostname or path with wildcards
|
||||||
const matchWithRegex = function(firstUrlPart, secondUrlPart, hostnameUsed = false) {
|
const matchWithRegex = function(firstUrlPart, secondUrlPart, hostnameUsed = false) {
|
||||||
if (firstUrlPart === secondUrlPart) {
|
if (firstUrlPart === secondUrlPart) {
|
||||||
|
|
@ -149,6 +159,7 @@ const getCurrentTab = async function() {
|
||||||
// Exports for tests
|
// Exports for tests
|
||||||
if (typeof module === 'object') {
|
if (typeof module === 'object') {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
matchesWithNodeName,
|
||||||
siteMatch,
|
siteMatch,
|
||||||
slashNeededForUrl,
|
slashNeededForUrl,
|
||||||
trimURL,
|
trimURL,
|
||||||
|
|
|
||||||
|
|
@ -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
|
* @returns {boolean} True if an Element has a match with the needed indentfifiers and document location
|
||||||
*/
|
*/
|
||||||
kpxcSites.totpExceptionFound = function(field) {
|
kpxcSites.totpExceptionFound = function(field) {
|
||||||
if (!field || field.nodeName !== 'INPUT') {
|
if (!field || !matchesWithNodeName(field.nodeName, 'INPUT')) {
|
||||||
return false;
|
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
|
* @returns {boolean} True if an Element has a match with the needed indentfifiers and document location
|
||||||
*/
|
*/
|
||||||
kpxcSites.segmentedTotpExceptionFound = function(form) {
|
kpxcSites.segmentedTotpExceptionFound = function(form) {
|
||||||
if (!form || form.nodeName !== 'FORM') {
|
if (!form || !matchesWithNodeName(form.nodeName, 'FORM')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,11 @@ kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) {
|
||||||
|
|
||||||
const addTotpFieldsToCombination = function(inputFields, ignoreFieldCount = false) {
|
const addTotpFieldsToCombination = function(inputFields, ignoreFieldCount = false) {
|
||||||
const totpInputs = Array.from(inputFields).filter(
|
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)) {
|
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)
|
// 7 inputs with a button as the last one (e.g. PayPal uses this)
|
||||||
if (currentForm.length === 7
|
if (
|
||||||
&& (currentForm.lastChild.nodeName === 'BUTTON'
|
currentForm.length === 7 &&
|
||||||
|| (currentForm.lastChild.nodeName === 'INPUT' && currentForm.lastChild.type === 'button'))) {
|
(matchesWithNodeName(currentForm.lastChild.nodeName, 'BUTTON') ||
|
||||||
|
(matchesWithNodeName(currentForm.lastChild.nodeName, 'INPUT') &&
|
||||||
|
currentForm.lastChild.type === 'button'))
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ kpxcFill.fillTOTPFromUuid = async function(el, uuid) {
|
||||||
|
|
||||||
if (user.totp?.length > 0) {
|
if (user.totp?.length > 0) {
|
||||||
// Retrieve a new TOTP value
|
// 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) {
|
if (!totp) {
|
||||||
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
|
kpxcUI.createNotification('warning', tr('credentialsNoTOTPFound'));
|
||||||
return;
|
return;
|
||||||
|
|
@ -246,7 +246,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill password
|
// 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
|
// Show a notification if password length exceeds the length defined in input
|
||||||
if (combination.password.maxLength
|
if (combination.password.maxLength
|
||||||
&& combination.password.maxLength > 0
|
&& combination.password.maxLength > 0
|
||||||
|
|
|
||||||
|
|
@ -49,15 +49,17 @@ kpxcForm.getFormSubmitButton = function(form) {
|
||||||
|
|
||||||
// Try to find another button. Select the last one.
|
// Try to find another button. Select the last one.
|
||||||
// If any formaction overriding the default action is set, ignore those buttons.
|
// 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) {
|
if (buttons.length > 0) {
|
||||||
return buttons.at(-1);
|
return buttons.at(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find similar buttons outside the form which are added via 'form' property
|
// Try to find similar buttons outside the form which are added via 'form' property
|
||||||
for (const e of form.elements) {
|
for (const e of form.elements) {
|
||||||
if ((e.nodeName === 'BUTTON' && (e.type === 'button' || e.type === 'submit' || e.type === ''))
|
if ((matchesWithNodeName(e.nodeName, 'BUTTON') && (e.type === 'button' || e.type === 'submit' || e.type === ''))
|
||||||
|| (e.nodeName === 'INPUT' && (e.type === 'button' || e.type === 'submit'))) {
|
|| (matchesWithNodeName(e.nodeName, 'INPUT') && (e.type === 'button' || e.type === 'submit'))) {
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,13 +127,15 @@ kpxcForm.onSubmit = async function(e) {
|
||||||
kpxcForm.submitTriggered = true;
|
kpxcForm.submitTriggered = true;
|
||||||
|
|
||||||
const searchForm = f => {
|
const searchForm = f => {
|
||||||
if (f.nodeName === 'FORM') {
|
if (matchesWithNodeName(f.nodeName, 'FORM')) {
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Traverse parents if the form is not found.
|
// 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
|
// Check for extra forms from sites.js
|
||||||
if (!form) {
|
if (!form) {
|
||||||
|
|
@ -202,7 +206,11 @@ kpxcForm.saveForm = function(form, combination) {
|
||||||
username: combination.username,
|
username: combination.username,
|
||||||
password: combination.password,
|
password: combination.password,
|
||||||
totp: combination.totp,
|
totp: combination.totp,
|
||||||
totpInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && kpxcTOTPIcons.isValid(e)),
|
totpInputs: Array.from(form.elements).filter(
|
||||||
passwordInputs: Array.from(form.elements).filter(e => e.nodeName === 'INPUT' && e.type === 'password')
|
e => matchesWithNodeName(e.nodeName, 'INPUT') && kpxcTOTPIcons.isValid(e),
|
||||||
|
),
|
||||||
|
passwordInputs: Array.from(form.elements).filter(
|
||||||
|
e => matchesWithNodeName(e.nodeName, 'INPUT') && e.type === 'password',
|
||||||
|
)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -600,7 +600,7 @@ kpxc.rememberCredentialsFromContextMenu = async function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = document.activeElement;
|
const el = document.activeElement;
|
||||||
if (el.nodeName !== 'INPUT') {
|
if (!matchesWithNodeName(el.nodeName, 'INPUT')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -938,7 +938,9 @@ kpxc.enablePasskeys = function() {
|
||||||
*/
|
*/
|
||||||
const initContentScript = async function() {
|
const initContentScript = async function() {
|
||||||
try {
|
try {
|
||||||
if (document?.documentElement?.ownerDocument?.contentType !== 'text/html') {
|
if (document?.documentElement?.ownerDocument?.contentType !== 'text/html'
|
||||||
|
&& document?.documentElement?.ownerDocument?.contentType !== 'application/xhtml+xml'
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ kpxcObserverHelper.initObserver = async function() {
|
||||||
}
|
}
|
||||||
} else if (mut.type === 'attributes' && (mut.attributeName === 'class' || mut.attributeName === 'style')) {
|
} else if (mut.type === 'attributes' && (mut.attributeName === 'class' || mut.attributeName === 'style')) {
|
||||||
// Only accept targets with forms
|
// 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)) {
|
if (forms.length === 0 && !kpxcSites.exceptionFound(mut.target.classList, mut.target)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +146,7 @@ kpxcObserverHelper.getInputs = function(target, ignoreVisibility = false) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (target.nodeName === 'INPUT') {
|
if (matchesWithNodeName(target.nodeName, 'INPUT')) {
|
||||||
inputFields.push(target);
|
inputFields.push(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,7 +304,7 @@ const traverseChildren = function(target, inputFields, depth = 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.nodeName === 'INPUT') {
|
if (matchesWithNodeName(child.nodeName, 'INPUT')) {
|
||||||
inputFields.push(child);
|
inputFields.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ kpxcUI.monitorIconPosition = function(iconClass) {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('transitionend', function(e) {
|
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);
|
kpxcUI.updateIconPosition(iconClass);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import {
|
import {
|
||||||
|
matchesWithNodeName,
|
||||||
siteMatch,
|
siteMatch,
|
||||||
slashNeededForUrl,
|
slashNeededForUrl,
|
||||||
trimURL
|
trimURL
|
||||||
} from '../keepassxc-browser/common/global.js';
|
} 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 }) => {
|
test('Test siteMatch()', async ({ page }) => {
|
||||||
expect(siteMatch('https://example.com/*', 'https://example.com/login_page')).toBe(true);
|
expect(siteMatch('https://example.com/*', 'https://example.com/login_page')).toBe(true);
|
||||||
expect(siteMatch('https://*.lexample.com/*', 'https://example.com/login_page')).toBe(false);
|
expect(siteMatch('https://*.lexample.com/*', 'https://example.com/login_page')).toBe(false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue