From a42df70f6805eeaeb974484c95bc932b94a7eeca Mon Sep 17 00:00:00 2001 From: varjolintu Date: Mon, 22 Feb 2021 21:58:30 +0200 Subject: [PATCH 1/3] Use XPath for element ID creation --- .../content/keepassxc-browser.js | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index b1f8d4e..8a31936 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -430,17 +430,34 @@ kpxcFields.getCombination = async function(field, givenType) { return undefined; }; -// Gets of generates an unique ID for the element +// Returns an unique ID for the element kpxcFields.getId = function(target) { - if (target.classList.length > 0) { - return `${target.nodeName} ${target.type} ${target.classList.value} ${target.name} ${target.placeholder}`; + let xpath = ''; + let pos; + let temp; + + while (target !== document.documentElement) { + pos = 0; + temp = target; + while (temp) { + if (temp.nodeType === 1 && temp.nodeName === target.nodeName) { + pos += 1; + } + + temp = temp.previousSibling; + } + + xpath = `${target.nodeName.toLowerCase()}${(pos > 1 ? `[${pos}]/` : '/')}${xpath}`; + target = target.parentNode; } - if (target.id && target.id !== '') { - return `${target.nodeName} ${target.type} ${kpxcFields.prepareId(target.id)} ${target.name} ${target.placeholder}`; - } + xpath = `/${document.documentElement.nodeName.toLowerCase()}/${xpath}`; + xpath = xpath.replace(/\/$/, ''); + return xpath; +}; - return `kpxc ${target.type} ${target.clientTop}${target.clientLeft}${target.clientWidth}${target.clientHeight}${target.offsetTop}${target.offsetLeft}`; +kpxcFields.getElementFromId = function(id) { + return (new XPathEvaluator()).evaluate(id, document.documentElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; }; // Check for new password via autocomplete attribute @@ -558,7 +575,7 @@ kpxcFields.useCustomLoginFields = async function() { // Finds the input field based on the stored ID const findInputField = async function(inputFields, id) { if (id) { - const input = inputFields.find(e => kpxcFields.getId(e) === id); + const input = inputFields.find(e => e === kpxcFields.getElementFromId(id)); if (input) { return input; } From 2785f1655581f34a9102d7b748dee3d1fc9aa80e Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sat, 27 Feb 2021 09:55:55 +0200 Subject: [PATCH 2/3] Generate XPath and fallback IDs --- keepassxc-browser/content/define.js | 8 ++-- .../content/keepassxc-browser.js | 46 ++++++++++++++++--- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/keepassxc-browser/content/define.js b/keepassxc-browser/content/define.js index c1da93c..80e4216 100644 --- a/keepassxc-browser/content/define.js +++ b/keepassxc-browser/content/define.js @@ -382,20 +382,20 @@ kpxcDefine.confirm = async function() { } if (kpxcDefine.selection.username) { - kpxcDefine.selection.username = kpxcFields.getId(kpxcDefine.selection.username.originalElement); + kpxcDefine.selection.username = kpxcFields.setId(kpxcDefine.selection.username.originalElement); } if (kpxcDefine.selection.password) { - kpxcDefine.selection.password = kpxcFields.getId(kpxcDefine.selection.password.originalElement); + kpxcDefine.selection.password = kpxcFields.setId(kpxcDefine.selection.password.originalElement); } if (kpxcDefine.selection.totp) { - kpxcDefine.selection.totp = kpxcFields.getId(kpxcDefine.selection.totp.originalElement); + kpxcDefine.selection.totp = kpxcFields.setId(kpxcDefine.selection.totp.originalElement); } const fieldIds = []; for (const i of kpxcDefine.selection.fields) { - fieldIds.push(kpxcFields.getId(i)); + fieldIds.push(kpxcFields.setId(i)); } const location = kpxc.getDocumentLocation(); diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 8a31936..94315fc 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -430,8 +430,24 @@ kpxcFields.getCombination = async function(field, givenType) { return undefined; }; -// Returns an unique ID for the element -kpxcFields.getId = function(target) { +// Sets and returns unique ID's for the element +kpxcFields.setId = function(target) { + return [ kpxcFields.getIdFromXPath(target), kpxcFields.getIdFromProperties(target) ]; +}; + +// Returns generated unique ID's for the element. If XPath ID fails, return the fallback one. +kpxcFields.getId = function(idArray, inputField) { + if (!idArray || idArray.length < 2) { + return ''; + } + + const elementFromXPath = kpxcFields.getElementFromXPathId(idArray[0]); + const fallbackId = kpxcFields.getIdFromProperties(inputField); + + return elementFromXPath || (fallbackId === idArray[1] ? inputField : ''); +}; + +kpxcFields.getIdFromXPath = function(target) { let xpath = ''; let pos; let temp; @@ -456,8 +472,24 @@ kpxcFields.getId = function(target) { return xpath; }; -kpxcFields.getElementFromId = function(id) { - return (new XPathEvaluator()).evaluate(id, document.documentElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; +kpxcFields.getIdFromProperties = function(target) { + if (target.name) { + return `${target.nodeName} ${target.type} ${target.name} ${target.placeholder}`; + } + + if (target.classList && target.classList.length > 0) { + return `${target.nodeName} ${target.type} ${target.classList.value} ${target.placeholder}`; + } + + if (target.id && target.id !== '') { + return `${target.nodeName} ${target.type} ${kpxcFields.prepareId(target.id)} ${target.placeholder}`; + } + + return `kpxc ${target.type} ${target.clientTop}${target.clientLeft}${target.clientWidth}${target.clientHeight}${target.offsetTop}${target.offsetLeft}`; +} + +kpxcFields.getElementFromXPathId = function(xpath) { + return (new XPathEvaluator()).evaluate(xpath, document.documentElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; }; // Check for new password via autocomplete attribute @@ -573,9 +605,9 @@ kpxcFields.useCustomLoginFields = async function() { } // Finds the input field based on the stored ID - const findInputField = async function(inputFields, id) { - if (id) { - const input = inputFields.find(e => e === kpxcFields.getElementFromId(id)); + const findInputField = async function(inputFields, idArray) { + if (idArray) { + const input = inputFields.find(e => e === kpxcFields.getId(idArray, e)); if (input) { return input; } From de938259b93187171fa7b08647fefa0606923783 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sun, 7 Mar 2021 10:46:06 +0200 Subject: [PATCH 3/3] Show notification when fields are not found --- .../content/keepassxc-browser.js | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 94315fc..9ca3cee 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -437,16 +437,24 @@ kpxcFields.setId = function(target) { // Returns generated unique ID's for the element. If XPath ID fails, return the fallback one. kpxcFields.getId = function(idArray, inputField) { - if (!idArray || idArray.length < 2) { + if (!idArray) { return ''; } + // Legacy ID is used. Convert it to the new one if possible + if (!Array.isArray(idArray) && idArray.length > 0) { + if (idArray === kpxcFields.getLegacyId(inputField)) { + idArray = kpxcFields.setId(inputField); + } + } + const elementFromXPath = kpxcFields.getElementFromXPathId(idArray[0]); const fallbackId = kpxcFields.getIdFromProperties(inputField); return elementFromXPath || (fallbackId === idArray[1] ? inputField : ''); }; +// Returns element XPath kpxcFields.getIdFromXPath = function(target) { let xpath = ''; let pos; @@ -472,6 +480,7 @@ kpxcFields.getIdFromXPath = function(target) { return xpath; }; +// Generate uniqe ID from properties (new method) kpxcFields.getIdFromProperties = function(target) { if (target.name) { return `${target.nodeName} ${target.type} ${target.name} ${target.placeholder}`; @@ -486,7 +495,20 @@ kpxcFields.getIdFromProperties = function(target) { } return `kpxc ${target.type} ${target.clientTop}${target.clientLeft}${target.clientWidth}${target.clientHeight}${target.offsetTop}${target.offsetLeft}`; -} +}; + +// Legacy unique ID generation for converting +kpxcFields.getLegacyId = function(target) { + if (target.classList.length > 0) { + return `${target.nodeName} ${target.type} ${target.classList.value} ${target.name} ${target.placeholder}`; + } + + if (target.id && target.id !== '') { + return `${target.nodeName} ${target.type} ${kpxcFields.prepareId(target.id)} ${target.name} ${target.placeholder}`; + } + + return `kpxc ${target.type} ${target.clientTop}${target.clientLeft}${target.clientWidth}${target.clientHeight}${target.offsetTop}${target.offsetLeft}`; +}; kpxcFields.getElementFromXPathId = function(xpath) { return (new XPathEvaluator()).evaluate(xpath, document.documentElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; @@ -645,6 +667,12 @@ kpxcFields.useCustomLoginFields = async function() { kpxcTOTPIcons.newIcon(totp, kpxc.databaseState, true); } + // 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, @@ -1161,10 +1189,15 @@ kpxc.initCombinations = async function(inputs = []) { return []; } - const combinations = kpxcFields.isCustomLoginFieldsUsed() + const isCustomLoginFieldsUsed = kpxcFields.isCustomLoginFieldsUsed(); + const combinations = isCustomLoginFieldsUsed ? await kpxcFields.useCustomLoginFields() : await kpxcFields.getAllCombinations(inputs); if (!combinations || combinations.length === 0) { + if (isCustomLoginFieldsUsed) { + kpxcUI.createNotification('warning', tr('optionsCustomFieldsNotFound')); + } + return []; }