author | Luke Chang <lchang@mozilla.com> |
Mon, 07 Aug 2017 18:34:27 +0800 | |
changeset 648832 | 49ae18b8d224c7cf0b417695f1116439e81bb252 |
parent 648831 | f9ab71432c920ef8842746a143b3a457083e6f80 |
child 648833 | 737d420bf61dda43f405434086515216e628586b |
child 649647 | 597ca8c2c0f98f0041f0d01a47ddf666f4911844 |
child 649751 | 2b0895148593f43b1773d2a1e4d69327b5efd3a4 |
push id | 74896 |
push user | schung@mozilla.com |
push date | Fri, 18 Aug 2017 10:48:05 +0000 |
reviewers | MattN |
bugs | 1390433, 1387988 |
milestone | 56.0 |
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm +++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm @@ -3,17 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * Form Autofill field heuristics. */ "use strict"; -this.EXPORTED_SYMBOLS = ["FormAutofillHeuristics"]; +this.EXPORTED_SYMBOLS = ["FormAutofillHeuristics", "LabelUtils"]; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://formautofill/FormAutofillUtils.jsm"); this.log = null; @@ -177,16 +177,94 @@ class FieldScanner { return this.fieldDetails.filter(f => f.fieldName && !f._duplicated); } elementExisting(index) { return index < this._elements.length; } } +this.LabelUtils = { + // The tag name list is from Chromium except for "STYLE": + // eslint-disable-next-line max-len + // https://cs.chromium.org/chromium/src/components/autofill/content/renderer/form_autofill_util.cc?l=216&rcl=d33a171b7c308a64dc3372fac3da2179c63b419e + EXCLUDED_TAGS: ["SCRIPT", "NOSCRIPT", "OPTION", "STYLE"], + /** + * Extract all strings of an element's children to an array. + * "element.textContent" is a string which is merged of all children nodes, + * and this function provides an array of the strings contains in an element. + * + * @param {Object} element + * A DOM element to be extracted. + * @returns {Array} + * All strings in an element. + */ + extractLabelStrings(element) { + let strings = []; + let _extractLabelStrings = (el) => { + if (this.EXCLUDED_TAGS.includes(el.tagName)) { + return; + } + + if (el.nodeType == Ci.nsIDOMNode.TEXT_NODE || + el.childNodes.length == 0) { + let trimmedText = el.textContent.trim(); + if (trimmedText) { + strings.push(trimmedText); + } + return; + } + + for (let node of el.childNodes) { + if (node.nodeType != Ci.nsIDOMNode.ELEMENT_NODE && + node.nodeType != Ci.nsIDOMNode.TEXT_NODE) { + continue; + } + _extractLabelStrings(node); + } + }; + _extractLabelStrings(element); + return strings; + }, + + findLabelElements(element) { + let document = element.ownerDocument; + let labels = []; + // TODO: querySelectorAll is inefficient here. However, bug 1339726 is for + // a more efficient implementation from DOM API perspective. This function + // should be refined after input.labels API landed. + for (let label of document.querySelectorAll("label[for]")) { + if (element.id == label.htmlFor) { + labels.push(label); + } + } + + if (labels.length > 0) { + log.debug("Label found by ID", element.id); + return labels; + } + + let parent = element.parentNode; + if (!parent) { + return []; + } + do { + if (parent.tagName == "LABEL" && + parent.control == element && + !parent.hasAttribute("for")) { + log.debug("Label found in input's parent or ancestor."); + return [parent]; + } + parent = parent.parentNode; + } while (parent); + + return []; + }, +}; + /** * Returns the autocomplete information of fields according to heuristics. */ this.FormAutofillHeuristics = { RULES: null, /** * Try to match the telephone related fields to the grammar @@ -365,19 +443,19 @@ this.FormAutofillHeuristics = { let labelStrings; let getElementStrings = {}; getElementStrings[Symbol.iterator] = function* () { yield element.id; yield element.name; if (!labelStrings) { labelStrings = []; - let labels = FormAutofillUtils.findLabelElements(element); + let labels = LabelUtils.findLabelElements(element); for (let label of labels) { - labelStrings.push(...FormAutofillUtils.extractLabelStrings(label)); + labelStrings.push(...LabelUtils.extractLabelStrings(label)); } } yield *labelStrings; }; for (let regexp of regexps) { for (let string of getElementStrings) { // The original regexp "(?<!united )state|county|region|province" for
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm +++ b/browser/extensions/formautofill/FormAutofillUtils.jsm @@ -123,92 +123,16 @@ this.FormAutofillUtils = { } } else if (!(element instanceof Ci.nsIDOMHTMLSelectElement)) { return false; } return true; }, - // The tag name list is from Chromium except for "STYLE": - // eslint-disable-next-line max-len - // https://cs.chromium.org/chromium/src/components/autofill/content/renderer/form_autofill_util.cc?l=216&rcl=d33a171b7c308a64dc3372fac3da2179c63b419e - EXCLUDED_TAGS: ["SCRIPT", "NOSCRIPT", "OPTION", "STYLE"], - /** - * Extract all strings of an element's children to an array. - * "element.textContent" is a string which is merged of all children nodes, - * and this function provides an array of the strings contains in an element. - * - * @param {Object} element - * A DOM element to be extracted. - * @returns {Array} - * All strings in an element. - */ - extractLabelStrings(element) { - let strings = []; - let _extractLabelStrings = (el) => { - if (this.EXCLUDED_TAGS.includes(el.tagName)) { - return; - } - - if (el.nodeType == Ci.nsIDOMNode.TEXT_NODE || - el.childNodes.length == 0) { - let trimmedText = el.textContent.trim(); - if (trimmedText) { - strings.push(trimmedText); - } - return; - } - - for (let node of el.childNodes) { - if (node.nodeType != Ci.nsIDOMNode.ELEMENT_NODE && - node.nodeType != Ci.nsIDOMNode.TEXT_NODE) { - continue; - } - _extractLabelStrings(node); - } - }; - _extractLabelStrings(element); - return strings; - }, - - findLabelElements(element) { - let document = element.ownerDocument; - let labels = []; - // TODO: querySelectorAll is inefficient here. However, bug 1339726 is for - // a more efficient implementation from DOM API perspective. This function - // should be refined after input.labels API landed. - for (let label of document.querySelectorAll("label[for]")) { - if (element.id == label.htmlFor) { - labels.push(label); - } - } - - if (labels.length > 0) { - log.debug("Label found by ID", element.id); - return labels; - } - - let parent = element.parentNode; - if (!parent) { - return []; - } - do { - if (parent.tagName == "LABEL" && - parent.control == element && - !parent.hasAttribute("for")) { - log.debug("Label found in input's parent or ancestor."); - return [parent]; - } - parent = parent.parentNode; - } while (parent); - - return []; - }, - loadDataFromScript(url, sandbox = {}) { let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader); scriptLoader.loadSubScript(url, sandbox, "utf-8"); return sandbox; }, /**
--- a/browser/extensions/formautofill/test/unit/test_extractLabelStrings.js +++ b/browser/extensions/formautofill/test/unit/test_extractLabelStrings.js @@ -1,11 +1,11 @@ "use strict"; -Cu.import("resource://formautofill/FormAutofillUtils.jsm"); +Cu.import("resource://formautofill/FormAutofillHeuristics.jsm"); const TESTCASES = [ { description: "A label element contains one input element.", document: `<label id="typeA"> label type A <!-- This comment should not be extracted. --> <input type="text"> <script>FOO</script> @@ -54,13 +54,13 @@ const TESTCASES = [ TESTCASES.forEach(testcase => { add_task(async function() { do_print("Starting testcase: " + testcase.description); let doc = MockDocument.createTestDocument( "http://localhost:8080/test/", testcase.document); let element = doc.getElementById(testcase.inputId); - let strings = FormAutofillUtils.extractLabelStrings(element); + let strings = LabelUtils.extractLabelStrings(element); Assert.deepEqual(strings, testcase.expectedStrings); }); });
--- a/browser/extensions/formautofill/test/unit/test_findLabelElements.js +++ b/browser/extensions/formautofill/test/unit/test_findLabelElements.js @@ -1,11 +1,11 @@ "use strict"; -Cu.import("resource://formautofill/FormAutofillUtils.jsm"); +Cu.import("resource://formautofill/FormAutofillHeuristics.jsm"); const TESTCASES = [ { description: "Input contains in a label element.", document: `<form> <label id="labelA"> label type A <input id="typeA" type="text"> </label> @@ -78,13 +78,13 @@ const TESTCASES = [ TESTCASES.forEach(testcase => { add_task(async function() { do_print("Starting testcase: " + testcase.description); let doc = MockDocument.createTestDocument( "http://localhost:8080/test/", testcase.document); let input = doc.getElementById(testcase.inputId); - let labels = FormAutofillUtils.findLabelElements(input); + let labels = LabelUtils.findLabelElements(input); Assert.deepEqual(labels.map(l => l.id), testcase.expectedLabelIds); }); });
--- a/tools/lint/eslint/modules.json +++ b/tools/lint/eslint/modules.json @@ -72,16 +72,17 @@ "fakeservices.js": ["FakeCryptoService", "FakeFilesystemService", "FakeGUIDService", "fakeSHA256HMAC"], "file_expandosharing.jsm": ["checkFromJSM"], "file_stringencoding.jsm": ["checkFromJSM"], "file_url.jsm": ["checkFromJSM"], "file_worker_url.jsm": ["checkFromJSM"], "Finder.jsm": ["Finder", "GetClipboardSearchString"], "forms.js": ["FormEngine", "FormRec", "FormValidator"], "forms.jsm": ["FormData"], + "FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"], "FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"], "frame.js": ["Collector", "Runner", "events", "runTestFile", "log", "timers", "persisted", "shutdownApplication"], "FrameScriptManager.jsm": ["getNewLoaderID"], "fxa_utils.js": ["initializeIdentityWithTokenServerResponse"], "fxaccounts.jsm": ["Authentication"], "FxAccounts.jsm": ["fxAccounts", "FxAccounts"], "FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"], "FxAccountsOAuthGrantClient.jsm": ["FxAccountsOAuthGrantClient", "FxAccountsOAuthGrantClientError"],