1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 00:33:44 +00:00

[PS-1918] Make autofill doc-scanner traverse into ShadowRoot (#4119)

* This commit implements the following main changes:

- Query elements by using a TreeWalker instead of `document.querySelector[All]`. The reason for this is that `querySelector[All]` doesn't traverse into elements with ShadowRoot.
- Recursively traverse into elements with `openOrClosedShadowRoot` or `Element.shadowRoot` (depending on browser support) inside TreeWalker loop.
- Use new query logic everywhere inside `autofill.js`. This also means we need to use filter functions to find elements with specific nodeNames and/or attributes instead of CSS selector strings.
- Add two new `instanceof Element` checks to prevent `Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'." errors`.

This change is fully backward compatible. If `openOrClosedShadowRoot` is not available it will always return undefined and we will never traverse into ShadowRoots just as the behavior was before this change.

* refactor: outsource recursive logic to accumulatingQueryDocAll

We don't want the `els` argument on the `queryDocAll` function because it's never used from outside the function itself. Thus the recursive logic is moved to `accumulatingQueryDocAll`.
Now `queryDocAll` creates an empty array and passes it to `accumulatingQueryDocAll` which recursively walks the document and all ShadowRoots and pushes all found nodes directly to the referenced array.

The decision to use a directly mutated array instead of `Array.concat(els)` or `Array.push(...els)` is for performance reasons. Pushing to the referenced array was 74% faster than using `Array.push` with spread operator and even 90% faster than using `Array.concat`.

Co-authored-by: Chad Miller <64046472+chadm-sq@users.noreply.github.com>

* refactor: extract input field relevance check into own function

Addresses CodeScene analysis violation "Bumpy Road Ahead" where conditional logic is checked for a nesting of 2 or deeper.

* refactor: use proper element attribute handling

- use el.type attribute instead of el.attribute.type on input elements. This makes sure we also get 'text' when type attribute is not explicitly specified
- use el.htmlFor attribute instead of el.attribute.for on label elements
- use `hasAttribute` and `getAttribute` methods instead of `attributes[]` which is discouraged by https://quirksmode.org/dom/core/#attributes
- improve readability of `isRelevantInputField`

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Chad Miller <64046472+chadm-sq@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
Rafael Kraut
2023-02-19 23:43:18 +01:00
committed by GitHub
parent a348c78a79
commit 208be8dfbf

View File

@@ -31,21 +31,115 @@
/* /*
MODIFICATIONS FROM ORIGINAL MODIFICATIONS FROM ORIGINAL
1. Populate isFirefox 1. Populate isFirefox
2. Remove isChrome and isSafari since they are not used. 2. Remove isChrome and isSafari since they are not used.
3. Unminify and format to meet Mozilla review requirements. 3. Unminify and format to meet Mozilla review requirements.
4. Remove unnecessary input types from getFormElements query selector and limit number of elements returned. 4. Remove unnecessary input types from getFormElements query selector and limit number of elements returned.
5. Remove fakeTested prop. 5. Remove fakeTested prop.
6. Rename com.agilebits.* stuff to com.bitwarden.* 6. Rename com.agilebits.* stuff to com.bitwarden.*
7. Remove "some useful globals" on window 7. Remove "some useful globals" on window
8. Add ability to autofill span[data-bwautofill] elements 8. Add ability to autofill span[data-bwautofill] elements
9. Add new handler, for new command that responds with page details in response callback 9. Add new handler, for new command that responds with page details in response callback
10. Handle sandbox iframe and sandbox rule in CSP 10. Handle sandbox iframe and sandbox rule in CSP
11. Work on array of saved urls instead of just one to determine if we should autofill non-https sites 11. Work on array of saved urls instead of just one to determine if we should autofill non-https sites
12. Remove setting of attribute com.browser.browser.userEdited on user-inputs 12. Remove setting of attribute com.browser.browser.userEdited on user-inputs
13. Handle null value URLs in urlNotSecure 13. Handle null value URLs in urlNotSecure
14. Implement new HTML element query logic to be able to traverse into ShadowRoot
*/ */
/*
* `openOrClosedShadowRoot` is only available to WebExtensions.
* We need to use the correct implementation based on browser.
*/
// START MODIFICATION
var getShadowRoot;
if (chrome.dom && chrome.dom.openOrClosedShadowRoot) {
// Chromium 88+
// https://developer.chrome.com/docs/extensions/reference/dom/
getShadowRoot = function (element) {
if (!(element instanceof HTMLElement)) {
return null;
}
return chrome.dom.openOrClosedShadowRoot(element);
};
} else {
getShadowRoot = function (element) {
// `openOrClosedShadowRoot` is currently available for Firefox 63+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/openOrClosedShadowRoot
// Fallback to usual shadowRoot if it doesn't exist, which will only find open ShadowRoots, not closed ones.
// https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot#browser_compatibility
return element.openOrClosedShadowRoot || element.shadowRoot;
};
}
/*
* Returns elements like Document.querySelectorAll does, but traverses the document and shadow
* roots, yielding a visited node only if it passes the predicate in filterCallback.
*/
function queryDocAll(doc, rootEl, filterCallback) {
var accumulatedNodes = [];
// mutates accumulatedNodes
accumulatingQueryDocAll(doc, rootEl, filterCallback, accumulatedNodes);
return accumulatedNodes;
}
function accumulatingQueryDocAll(doc, rootEl, filterCallback, accumulatedNodes) {
var treeWalker = doc.createTreeWalker(rootEl, NodeFilter.SHOW_ELEMENT);
var node;
while (node = treeWalker.nextNode()) {
if (filterCallback(node)) {
accumulatedNodes.push(node);
}
// If node contains a ShadowRoot we want to step into it and also traverse all child nodes inside.
var nodeShadowRoot = getShadowRoot(node);
if (!nodeShadowRoot) {
continue;
}
// recursively traverse into ShadowRoot
accumulatingQueryDocAll(doc, nodeShadowRoot, filterCallback, accumulatedNodes);
}
}
/*
* Returns an element like Document.querySelector does, but traverses the document and shadow
* roots, yielding a visited node only if it passes the predicate in filterCallback.
*/
function queryDoc(doc, rootEl, filterCallback) {
var treeWalker = doc.createTreeWalker(rootEl, NodeFilter.SHOW_ELEMENT);
var node;
while (node = treeWalker.nextNode()) {
if (filterCallback(node)) {
return node;
}
// If node contains a ShadowRoot we want to step into it and also traverse all child nodes inside.
var nodeShadowRoot = getShadowRoot(node);
if (!nodeShadowRoot) {
continue;
}
// recursively traverse into ShadowRoot
var subQueryResult = queryDoc(doc, nodeShadowRoot, filterCallback);
if (subQueryResult) {
return subQueryResult;
}
}
return null;
}
// END MODIFICATION
function collect(document, undefined) { function collect(document, undefined) {
// START MODIFICATION // START MODIFICATION
var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Gecko/') !== -1; var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Gecko/') !== -1;
@@ -58,8 +152,8 @@
/** /**
* For a given element `el`, returns the value of the attribute `attrName`. * For a given element `el`, returns the value of the attribute `attrName`.
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {string} attrName * @param {string} attrName
* @returns {string} The value of the attribute * @returns {string} The value of the attribute
*/ */
function getElementAttrValue(el, attrName) { function getElementAttrValue(el, attrName) {
@@ -96,7 +190,7 @@
/** /**
* Returns the value of the given element. * Returns the value of the given element.
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {any} Value of the element * @returns {any} Value of the element
*/ */
function getElementValue(el) { function getElementValue(el) {
@@ -124,7 +218,7 @@
/** /**
* If `el` is a `<select>` element, return an array of all of the options' `text` properties. * If `el` is a `<select>` element, return an array of all of the options' `text` properties.
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {string[]} An array of options for the given `<select>` element * @returns {string[]} An array of options for the given `<select>` element
*/ */
function getSelectElementOptions(el) { function getSelectElementOptions(el) {
@@ -147,7 +241,7 @@
/** /**
* If `el` is in a data table, get the label in the row directly above it * If `el` is in a data table, get the label in the row directly above it
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {string} A string containing the label, or null if not found * @returns {string} A string containing the label, or null if not found
*/ */
function getLabelTop(el) { function getLabelTop(el) {
@@ -187,7 +281,7 @@
/** /**
* Get the contents of the elements that are labels for `el` * Get the contents of the elements that are labels for `el`
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {string} A string containing all of the `innerText` or `textContent` values for all elements that are labels for `el` * @returns {string} A string containing all of the `innerText` or `textContent` values for all elements that are labels for `el`
*/ */
function getLabelTag(el) { function getLabelTag(el) {
@@ -198,12 +292,22 @@
theLabels = Array.prototype.slice.call(el.labels); theLabels = Array.prototype.slice.call(el.labels);
} else { } else {
if (el.id) { if (el.id) {
theLabels = theLabels.concat(Array.prototype.slice.call( // START MODIFICATION
queryDoc(theDoc, 'label[for=' + JSON.stringify(el.id) + ']'))); var elId = JSON.stringify(el.id);
var labelsByReferencedId = queryDocAll(theDoc, theDoc.body, function (node) {
return node.nodeName === 'LABEL' && node.htmlFor === elId;
});
theLabels = theLabels.concat(labelsByReferencedId);
// END MODIFICATION
} }
if (el.name) { if (el.name) {
docLabel = queryDoc(theDoc, 'label[for=' + JSON.stringify(el.name) + ']'); // START MODIFICATION
var elName = JSON.stringify(el.name);
docLabel = queryDocAll(theDoc, theDoc.body, function (node) {
return node.nodeName === 'LABEL' && node.htmlFor === elName;
});
// END MODIFICATION
for (var labelIndex = 0; labelIndex < docLabel.length; labelIndex++) { for (var labelIndex = 0; labelIndex < docLabel.length; labelIndex++) {
if (-1 === theLabels.indexOf(docLabel[labelIndex])) { if (-1 === theLabels.indexOf(docLabel[labelIndex])) {
@@ -239,10 +343,10 @@
/** /**
* Add property `prop` with value `val` to the object `obj` * Add property `prop` with value `val` to the object `obj`
* @param {object} obj * @param {object} obj
* @param {string} prop * @param {string} prop
* @param {any} val * @param {any} val
* @param {*} d * @param {*} d
*/ */
function addProp(obj, prop, val, d) { function addProp(obj, prop, val, d) {
if (0 !== d && d === val || null === val || void 0 === val) { if (0 !== d && d === val || null === val || void 0 === val) {
@@ -254,34 +358,27 @@
/** /**
* Converts the string `s` to lowercase * Converts the string `s` to lowercase
* @param {string} s * @param {string} s
* @returns Lowercase string * @returns Lowercase string
*/ */
function toLowerString(s) { function toLowerString(s) {
return 'string' === typeof s ? s.toLowerCase() : ('' + s).toLowerCase(); return 'string' === typeof s ? s.toLowerCase() : ('' + s).toLowerCase();
} }
// START MODIFICATION
/** // renamed queryDoc to queryDocAll and moved to top
* Query the document `doc` for elements matching the selector `selector` // END MODIFICATION
* @param {Document} doc
* @param {string} query
* @returns {HTMLElement[]} An array of elements matching the selector
*/
function queryDoc(doc, query) {
var els = [];
try {
els = doc.querySelectorAll(query);
} catch (e) { }
return els;
}
// end helpers // end helpers
var theView = theDoc.defaultView ? theDoc.defaultView : window, var theView = theDoc.defaultView ? theDoc.defaultView : window,
passwordRegEx = RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|(\\\\b|_|-)passe(\\\\b|_|-)|contraseña|senha|密码|adgangskode|hasło|wachtwoord)', 'i'); passwordRegEx = RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|(\\\\b|_|-)passe(\\\\b|_|-)|contraseña|senha|密码|adgangskode|hasło|wachtwoord)', 'i');
// get all the docs // get all the docs
var theForms = Array.prototype.slice.call(queryDoc(theDoc, 'form')).map(function (formEl, elIndex) { // START MODIFICATION
var formNodes = queryDocAll(theDoc, theDoc.body, function (el) {
return el.nodeName === 'FORM';
});
var theForms = formNodes.map(function (formEl, elIndex) {
// END MODIFICATION
var op = {}, var op = {},
formOpId = '__form__' + elIndex; formOpId = '__form__' + elIndex;
@@ -440,7 +537,11 @@
}; };
// get proper page title. maybe they are using the special meta tag? // get proper page title. maybe they are using the special meta tag?
var theTitle = document.querySelector('[data-onepassword-title]') // START MODIFICATION
var theTitle = queryDoc(theDoc, theDoc, function (node) {
return node.hasAttribute('data-onepassword-title');
});
// END MODIFICATION
if (theTitle && theTitle.dataset[DISPLAY_TITLE_ATTRIBUE]) { if (theTitle && theTitle.dataset[DISPLAY_TITLE_ATTRIBUE]) {
pageDetails.displayTitle = theTitle.dataset.onepasswordTitle; pageDetails.displayTitle = theTitle.dataset.onepasswordTitle;
} }
@@ -453,8 +554,8 @@
/** /**
* Do the event on the element. * Do the event on the element.
* @param {HTMLElement} kedol The element to do the event on * @param {HTMLElement} kedol The element to do the event on
* @param {string} fonor The event name * @param {string} fonor The event name
* @returns * @returns
*/ */
function doEventOnElement(kedol, fonor) { function doEventOnElement(kedol, fonor) {
var quebo; var quebo;
@@ -466,7 +567,7 @@
/** /**
* Clean up the string `s` to remove non-printable characters and whitespace. * Clean up the string `s` to remove non-printable characters and whitespace.
* @param {string} s * @param {string} s
* @returns {string} Clean text * @returns {string} Clean text
*/ */
function cleanText(s) { function cleanText(s) {
@@ -478,7 +579,7 @@
/** /**
* If `el` is a text node, add the node's text to `arr`. * If `el` is a text node, add the node's text to `arr`.
* If `el` is an element node, add the element's `textContent or `innerText` to `arr`. * If `el` is an element node, add the element's `textContent or `innerText` to `arr`.
* @param {string[]} arr An array of `textContent` or `innerText` values * @param {string[]} arr An array of `textContent` or `innerText` values
* @param {HTMLElement} el The element to push to the array * @param {HTMLElement} el The element to push to the array
*/ */
function checkNodeType(arr, el) { function checkNodeType(arr, el) {
@@ -512,9 +613,9 @@
/** /**
* Recursively gather all of the text values from the elements preceding `el` in the DOM * Recursively gather all of the text values from the elements preceding `el` in the DOM
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {string[]} arr An array of `textContent` or `innerText` values * @param {string[]} arr An array of `textContent` or `innerText` values
* @param {number} steps The number of steps to take up the DOM tree * @param {number} steps The number of steps to take up the DOM tree
*/ */
function shiftForLeftLabel(el, arr, steps) { function shiftForLeftLabel(el, arr, steps) {
var sib; var sib;
@@ -545,7 +646,7 @@
/** /**
* Determine if the element is visible. * Determine if the element is visible.
* Visible is define as not having `display: none` or `visibility: hidden`. * Visible is define as not having `display: none` or `visibility: hidden`.
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {boolean} Returns `true` if the element is visible and `false` otherwise * @returns {boolean} Returns `true` if the element is visible and `false` otherwise
*/ */
function isElementVisible(el) { function isElementVisible(el) {
@@ -556,7 +657,10 @@
// walk the dom tree until we reach the top // walk the dom tree until we reach the top
for (var elStyle; theEl && theEl !== document;) { for (var elStyle; theEl && theEl !== document;) {
// Calculate the style of the element // Calculate the style of the element
elStyle = el.getComputedStyle ? el.getComputedStyle(theEl, null) : theEl.style; // START MODIFICATION
elStyle = el.getComputedStyle && theEl instanceof Element ? el.getComputedStyle(theEl, null) : theEl.style;
// END MODIFICATION
// If there's no computed style at all, we're done, as we know that it's not hidden // If there's no computed style at all, we're done, as we know that it's not hidden
if (!elStyle) { if (!elStyle) {
return true; return true;
@@ -577,7 +681,7 @@
/** /**
* Determine if the element is "viewable" on the screen. * Determine if the element is "viewable" on the screen.
* "Viewable" is defined as being visible in the DOM and being within the confines of the viewport. * "Viewable" is defined as being visible in the DOM and being within the confines of the viewport.
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {boolean} Returns `true` if the element is viewable and `false` otherwise * @returns {boolean} Returns `true` if the element is viewable and `false` otherwise
*/ */
function isElementViewable(el) { function isElementViewable(el) {
@@ -616,7 +720,7 @@
// If the right side of the bounding rectangle is outside the viewport, the x coordinate of the center point is the window width (minus offset) divided by 2. // If the right side of the bounding rectangle is outside the viewport, the x coordinate of the center point is the window width (minus offset) divided by 2.
// If the right side of the bounding rectangle is inside the viewport, the x coordinate of the center point is the width of the bounding rectangle divided by 2. // If the right side of the bounding rectangle is inside the viewport, the x coordinate of the center point is the width of the bounding rectangle divided by 2.
// If the bottom of the bounding rectangle is outside the viewport, the y coordinate of the center point is the window height (minus offset) divided by 2. // If the bottom of the bounding rectangle is outside the viewport, the y coordinate of the center point is the window height (minus offset) divided by 2.
// If the bottom side of the bounding rectangle is inside the viewport, the y coordinate of the center point is the height of the bounding rectangle divided by // If the bottom side of the bounding rectangle is inside the viewport, the y coordinate of the center point is the height of the bounding rectangle divided by
// We then use elementFromPoint to find the element at that point. // We then use elementFromPoint to find the element at that point.
for (var pointEl = el.ownerDocument.elementFromPoint(leftOffset + (rect.right > window.innerWidth ? (window.innerWidth - leftOffset) / 2 : rect.width / 2), topOffset + (rect.bottom > window.innerHeight ? (window.innerHeight - topOffset) / 2 : rect.height / 2)); pointEl && pointEl !== el && pointEl !== document;) { for (var pointEl = el.ownerDocument.elementFromPoint(leftOffset + (rect.right > window.innerWidth ? (window.innerWidth - leftOffset) / 2 : rect.width / 2), topOffset + (rect.bottom > window.innerHeight ? (window.innerHeight - topOffset) / 2 : rect.height / 2)); pointEl && pointEl !== el && pointEl !== document;) {
// If the element we found is a label, and the element we're checking has labels // If the element we found is a label, and the element we're checking has labels
@@ -638,7 +742,7 @@
/** /**
* Retrieve the element from the document with the specified `opid` property * Retrieve the element from the document with the specified `opid` property
* @param {number} opId * @param {number} opId
* @returns {HTMLElement} The element with the specified `opiId`, or `null` if no such element exists * @returns {HTMLElement} The element with the specified `opiId`, or `null` if no such element exists
*/ */
function getElementForOPID(opId) { function getElementForOPID(opId) {
@@ -666,6 +770,28 @@
} }
} }
var ignoredInputTypes = {
hidden: true,
submit: true,
reset: true,
button: true,
image: true,
file: true,
};
/*
* inputEl MUST BE an instanceof HTMLInputElement, else inputEl.type.toLowerCase will throw an error
*/
function isRelevantInputField(inputEl) {
if (inputEl.hasAttribute('data-bwignore')) {
return false;
}
const isIgnoredInputType = ignoredInputTypes.hasOwnProperty(inputEl.type.toLowerCase());
return !isIgnoredInputType;
}
/** /**
* Query `theDoc` for form elements that we can use for autofill, ranked by importance and limited by `limit` * Query `theDoc` for form elements that we can use for autofill, ranked by importance and limited by `limit`
* @param {Document} theDoc The Document to query * @param {Document} theDoc The Document to query
@@ -674,13 +800,19 @@
*/ */
function getFormElements(theDoc, limit) { function getFormElements(theDoc, limit) {
// START MODIFICATION // START MODIFICATION
var els = [];
try { var els = queryDocAll(theDoc, theDoc.body, function (el) {
var elsList = theDoc.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="reset"])' + switch (el.nodeName) {
':not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), select, ' + case 'SELECT':
'span[data-bwautofill]'); return true;
els = Array.prototype.slice.call(elsList); case 'SPAN':
} catch (e) { } return el.hasAttribute('data-bwautofill');
case 'INPUT':
return isRelevantInputField(el);
default:
return false;
}
});
if (!limit || els.length <= limit) { if (!limit || els.length <= limit) {
return els; return els;
@@ -710,12 +842,12 @@
} }
return returnEls; return returnEls;
// END MODIFICATION
} }
// END MODIFICATION
/** /**
* Focus the element `el` and optionally restore its original value * Focus the element `el` and optionally restore its original value
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {boolean} setVal Set the value of the element to its original value * @param {boolean} setVal Set the value of the element to its original value
*/ */
function focusElement(el, setVal) { function focusElement(el, setVal) {
@@ -740,6 +872,12 @@
var markTheFilling = true, var markTheFilling = true,
animateTheFilling = true; animateTheFilling = true;
function queryPasswordInputs() {
return queryDocAll(document, document.body, function (el) {
return el.nodeName === 'INPUT' && el.type.toLowerCase() === 'password';
})
}
// Check if URL is not secure when the original saved one was // Check if URL is not secure when the original saved one was
function urlNotSecure(savedURLs) { function urlNotSecure(savedURLs) {
var passwordInputs = null; var passwordInputs = null;
@@ -747,7 +885,7 @@
return false; return false;
} }
return savedURLs.some(url => url?.indexOf('https://') === 0) && 'http:' === document.location.protocol && (passwordInputs = document.querySelectorAll('input[type=password]'), return savedURLs.some(url => url?.indexOf('https://') === 0) && 'http:' === document.location.protocol && (passwordInputs = queryPasswordInputs(),
0 < passwordInputs.length && (confirmResult = confirm('Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page.\n\nDo you still wish to fill this login?'), 0 < passwordInputs.length && (confirmResult = confirm('Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page.\n\nDo you still wish to fill this login?'),
0 == confirmResult)) ? true : false; 0 == confirmResult)) ? true : false;
} }
@@ -871,8 +1009,8 @@
/** /**
* Find all elements matching `query` and fill them using the value `op` from the fill script * Find all elements matching `query` and fill them using the value `op` from the fill script
* @param {string} query * @param {string} query
* @param {string} op * @param {string} op
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
function doFillByQuery(query, op) { function doFillByQuery(query, op) {
@@ -885,8 +1023,8 @@
/** /**
* Assign `valueToSet` to all elements in the DOM that match `query`. * Assign `valueToSet` to all elements in the DOM that match `query`.
* @param {string} query * @param {string} query
* @param {string} valueToSet * @param {string} valueToSet
* @returns {Array} Array of elements that were set. * @returns {Array} Array of elements that were set.
*/ */
function doSimpleSetByQuery(query, valueToSet) { function doSimpleSetByQuery(query, valueToSet) {
@@ -900,8 +1038,8 @@
/** /**
* Do a a click and focus on the element with the given `opId`. * Do a a click and focus on the element with the given `opId`.
* @param {number} opId * @param {number} opId
* @returns * @returns
*/ */
function doFocusByOpId(opId) { function doFocusByOpId(opId) {
var el = getElementByOpId(opId) var el = getElementByOpId(opId)
@@ -915,8 +1053,8 @@
/** /**
* Do a click on the element with the given `opId`. * Do a click on the element with the given `opId`.
* @param {number} opId * @param {number} opId
* @returns * @returns
*/ */
function doClickByOpId(opId) { function doClickByOpId(opId) {
var el = getElementByOpId(opId); var el = getElementByOpId(opId);
@@ -924,9 +1062,9 @@
} }
/** /**
* Do a `click` and `focus` on all elements that match the query. * Do a `click` and `focus` on all elements that match the query.
* @param {string} query * @param {string} query
* @returns * @returns
*/ */
function doClickByQuery(query) { function doClickByQuery(query) {
query = selectAllFromDoc(query); query = selectAllFromDoc(query);
@@ -949,8 +1087,8 @@
/** /**
* Fll an element `el` using the value `op` from the fill script * Fll an element `el` using the value `op` from the fill script
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {string} op * @param {string} op
*/ */
function fillTheElement(el, op) { function fillTheElement(el, op) {
var shouldCheck; var shouldCheck;
@@ -982,7 +1120,7 @@
/** /**
* Do all the fill operations needed on the element `el`. * Do all the fill operations needed on the element `el`.
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {*} afterValSetFunc The function to perform after the operations are complete. * @param {*} afterValSetFunc The function to perform after the operations are complete.
*/ */
function doAllFillOperations(el, afterValSetFunc) { function doAllFillOperations(el, afterValSetFunc) {
@@ -1006,8 +1144,8 @@
/** /**
* Normalize the event based on API support * Normalize the event based on API support
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {string} eventName * @param {string} eventName
* @returns {Event} A normalized event * @returns {Event} A normalized event
*/ */
function normalizeEvent(el, eventName) { function normalizeEvent(el, eventName) {
@@ -1034,7 +1172,7 @@
/** /**
* Simulate the entry of a value into an element. * Simulate the entry of a value into an element.
* Clicks the element, focuses it, and then fires a keydown, keypress, and keyup event. * Clicks the element, focuses it, and then fires a keydown, keypress, and keyup event.
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
function setValueForElement(el) { function setValueForElement(el) {
var valueToSet = el.value; var valueToSet = el.value;
@@ -1049,7 +1187,7 @@
/** /**
* Simulate the entry of a value into an element by using events. * Simulate the entry of a value into an element by using events.
* Dispatches a keydown, keypress, and keyup event, then fires the `input` and `change` events before removing focus. * Dispatches a keydown, keypress, and keyup event, then fires the `input` and `change` events before removing focus.
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
function setValueForElementByEvent(el) { function setValueForElementByEvent(el) {
var valueToSet = el.value, var valueToSet = el.value,
@@ -1069,7 +1207,7 @@
/** /**
* Click on an element `el` * Click on an element `el`
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {boolean} Returns true if the element was clicked and false if it was not able to be clicked * @returns {boolean} Returns true if the element was clicked and false if it was not able to be clicked
*/ */
function clickElement(el) { function clickElement(el) {
@@ -1086,9 +1224,12 @@
*/ */
function getAllFields() { function getAllFields() {
var r = RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|passe|contraseña|senha|密码|adgangskode|hasło|wachtwoord)', 'i'); var r = RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|passe|contraseña|senha|密码|adgangskode|hasło|wachtwoord)', 'i');
return Array.prototype.slice.call(selectAllFromDoc("input[type='text']")).filter(function (el) { return queryDocAll(document, document.body, function (el) {
return el.value && r.test(el.value); return el.nodeName === 'INPUT' &&
}, this); el.type.toLowerCase() === 'text' &&
el.value &&
r.test(el.value);
});
} }
/** /**
@@ -1104,7 +1245,7 @@
/** /**
* Determine if we can apply styling to `el` to indicate that it was filled. * Determine if we can apply styling to `el` to indicate that it was filled.
* @param {HTMLElement} el * @param {HTMLElement} el
* @returns {boolean} Returns true if we can see the element to apply styling. * @returns {boolean} Returns true if we can see the element to apply styling.
*/ */
function canSeeElementToStyle(el) { function canSeeElementToStyle(el) {
@@ -1113,7 +1254,9 @@
a: { a: {
currentEl = el; currentEl = el;
for (var owner = el.ownerDocument, owner = owner ? owner.defaultView : {}, theStyle; currentEl && currentEl !== document;) { for (var owner = el.ownerDocument, owner = owner ? owner.defaultView : {}, theStyle; currentEl && currentEl !== document;) {
theStyle = owner.getComputedStyle ? owner.getComputedStyle(currentEl, null) : currentEl.style; // START MODIFICATION
theStyle = owner.getComputedStyle && currentEl instanceof Element ? owner.getComputedStyle(currentEl, null) : currentEl.style;
// END MODIFICATION
if (!theStyle) { if (!theStyle) {
currentEl = true; currentEl = true;
break a; break a;
@@ -1137,7 +1280,7 @@
/** /**
* Find the element for the given `opid`. * Find the element for the given `opid`.
* @param {number} theOpId * @param {number} theOpId
* @returns {HTMLElement} The element for the given `opid`, or `null` if not found. * @returns {HTMLElement} The element for the given `opid`, or `null` if not found.
*/ */
function getElementByOpId(theOpId) { function getElementByOpId(theOpId) {
@@ -1147,12 +1290,19 @@
} }
try { try {
// START MODIFICATION // START MODIFICATION
var elements = Array.prototype.slice.call(selectAllFromDoc('input, select, button, ' + var filteredElements = queryDocAll(document, document.body, function (el) {
'span[data-bwautofill]')); switch (el.nodeName) {
// END MODIFICATION case 'INPUT':
var filteredElements = elements.filter(function (o) { case 'SELECT':
return o.opid == theOpId; case 'BUTTON':
return el.opid === theOpId;
case 'SPAN':
return el.hasAttribute('data-bwautofill') && el.opid === theOpId;
}
return false;
}); });
// END MODIFICATION
if (0 < filteredElements.length) { if (0 < filteredElements.length) {
theElement = filteredElements[0], theElement = filteredElements[0],
1 < filteredElements.length && console.warn('More than one element found with opid ' + theOpId); 1 < filteredElements.length && console.warn('More than one element found with opid ' + theOpId);
@@ -1169,20 +1319,20 @@
/** /**
* Helper for doc.querySelectorAll * Helper for doc.querySelectorAll
* @param {string} theSelector * @param {string} theSelector
* @returns * @returns
*/ */
function selectAllFromDoc(theSelector) { function selectAllFromDoc(theSelector) {
var d = document, elements = []; // START MODIFICATION
try { return queryDocAll(document, document, function(node) {
elements = d.querySelectorAll(theSelector); return node.matches(theSelector);
} catch (e) { } });
return elements; // END MODIFICATION
} }
/** /**
* Focus an element and optionally re-set its value after focusing * Focus an element and optionally re-set its value after focusing
* @param {HTMLElement} el * @param {HTMLElement} el
* @param {boolean} setValue Re-set the value after focusing * @param {boolean} setValue Re-set the value after focusing
*/ */
function doFocusElement(el, setValue) { function doFocusElement(el, setValue) {