(function () { const FORM_MARKERS = ["login", "log-in", "log_in", "signin", "sign-in", "sign_in"]; const OPENID_FIELDS = { selectors: ["input[name*=openid i]", "input[id*=openid i]", "input[class*=openid i]"], types: ["text"], }; const USERNAME_FIELDS = { selectors: [ "input[autocomplete=username i]", "input[name=login i]", "input[name=user i]", "input[name=username i]", "input[name=email i]", "input[name=alias i]", "input[id=login i]", "input[id=user i]", "input[id=username i]", "input[id=email i]", "input[id=alias i]", "input[class=login i]", "input[class=user i]", "input[class=username i]", "input[class=email i]", "input[class=alias i]", "input[name*=login i]", "input[name*=user i]", "input[name*=email i]", "input[name*=alias i]", "input[id*=login i]", "input[id*=user i]", "input[id*=email i]", "input[id*=alias i]", "input[class*=login i]", "input[class*=user i]", "input[class*=email i]", "input[class*=alias i]", "input[type=email i]", "input[autocomplete=email i]", "input[type=text i]", "input[type=tel i]", ], types: ["email", "text", "tel"], }; const PASSWORD_FIELDS = { selectors: [ "input[type=password i][autocomplete=current-password i]", "input[type=password i]", ], }; const INPUT_FIELDS = { selectors: PASSWORD_FIELDS.selectors .concat(USERNAME_FIELDS.selectors) .concat(OPENID_FIELDS.selectors), }; const SUBMIT_FIELDS = { selectors: [ "[type=submit i]", "button[name=login i]", "button[name=log-in i]", "button[name=log_in i]", "button[name=signin i]", "button[name=sign-in i]", "button[name=sign_in i]", "button[id=login i]", "button[id=log-in i]", "button[id=log_in i]", "button[id=signin i]", "button[id=sign-in i]", "button[id=sign_in i]", "button[class=login i]", "button[class=log-in i]", "button[class=log_in i]", "button[class=signin i]", "button[class=sign-in i]", "button[class=sign_in i]", "input[type=button i][name=login i]", "input[type=button i][name=log-in i]", "input[type=button i][name=log_in i]", "input[type=button i][name=signin i]", "input[type=button i][name=sign-in i]", "input[type=button i][name=sign_in i]", "input[type=button i][id=login i]", "input[type=button i][id=log-in i]", "input[type=button i][id=log_in i]", "input[type=button i][id=signin i]", "input[type=button i][id=sign-in i]", "input[type=button i][id=sign_in i]", "input[type=button i][class=login i]", "input[type=button i][class=log-in i]", "input[type=button i][class=log_in i]", "input[type=button i][class=signin i]", "input[type=button i][class=sign-in i]", "input[type=button i][class=sign_in i]", "button[name*=login i]", "button[name*=log-in i]", "button[name*=log_in i]", "button[name*=signin i]", "button[name*=sign-in i]", "button[name*=sign_in i]", "button[id*=login i]", "button[id*=log-in i]", "button[id*=log_in i]", "button[id*=signin i]", "button[id*=sign-in i]", "button[id*=sign_in i]", "button[class*=login i]", "button[class*=log-in i]", "button[class*=log_in i]", "button[class*=signin i]", "button[class*=sign-in i]", "button[class*=sign_in i]", "input[type=button i][name*=login i]", "input[type=button i][name*=log-in i]", "input[type=button i][name*=log_in i]", "input[type=button i][name*=signin i]", "input[type=button i][name*=sign-in i]", "input[type=button i][name*=sign_in i]", "input[type=button i][id*=login i]", "input[type=button i][id*=log-in i]", "input[type=button i][id*=log_in i]", "input[type=button i][id*=signin i]", "input[type=button i][id*=sign-in i]", "input[type=button i][id*=sign_in i]", "input[type=button i][class*=login i]", "input[type=button i][class*=log-in i]", "input[type=button i][class*=log_in i]", "input[type=button i][class*=signin i]", "input[type=button i][class*=sign-in i]", "input[type=button i][class*=sign_in i]", ], }; /** * Fill password * * @since 3.0.0 * * @param object request Form fill request * @return object result of filling a form */ function fillLogin(request) { var result = { filledFields: [], foreignFill: undefined, }; // get the login form let loginForm = undefined; if (request.fields.includes("openid")) { // this is an attempt to fill a form containing only openid field loginForm = form(OPENID_FIELDS); } else { // this is an attempt to fill a regular login form loginForm = form(INPUT_FIELDS); } // don't attempt to fill non-secret forms unless non-secret filling is allowed if (!request.allowNoSecret && !find(PASSWORD_FIELDS, loginForm)) { return result; } // ensure the origin is the same, or ask the user for permissions to continue if (window.location.origin !== request.origin) { if (!request.allowForeign || request.foreignFills[window.location.origin] === false) { return result; } var message = "You have requested to fill login credentials into an embedded document from a " + "different origin than the main document in this tab. Do you wish to proceed?\n\n" + `Tab origin: ${request.origin}\n` + `Embedded origin: ${window.location.origin}`; if (request.foreignFills[window.location.origin] !== true) { result.foreignOrigin = window.location.origin; result.foreignFill = confirm(message); if (!result.foreignFill) { return result; } } } // fill login field if ( request.fields.includes("login") && update(USERNAME_FIELDS, request.login.fields.login, loginForm) ) { result.filledFields.push("login"); } // fill secret field if ( request.fields.includes("secret") && update(PASSWORD_FIELDS, request.login.fields.secret, loginForm) ) { result.filledFields.push("secret"); } // fill openid field if ( request.fields.includes("openid") && update(OPENID_FIELDS, request.login.fields.openid, loginForm) ) { result.filledFields.push("openid"); } // finished filling things successfully return result; } /** * Focus submit button, and maybe click on it (based on user settings) * * @since 3.0.0 * * @param object request Form fill request * @return object result of focusing or submitting a form */ function focusOrSubmit(request) { var result = {}; // get the login form let loginForm = undefined; if (request.filledFields.includes("openid")) { // this is an attempt to focus or submit a form containing only openid field loginForm = form(OPENID_FIELDS); } else { // this is an attempt to focus or submit a regular login form loginForm = form(INPUT_FIELDS); } // ensure the origin is the same or allowed if (window.location.origin !== request.origin) { if (!request.allowForeign || request.foreignFills[window.location.origin] === false) { return; } } // check for multiple password fields in the login form var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm); if (password_inputs.length > 1) { // There is likely a field asking for OTP code, so do not submit form just yet password_inputs[1].select(); } else { // try to locate the submit button var submit = find(SUBMIT_FIELDS, loginForm); // Try to submit the form, or focus on the submit button (based on user settings) if (submit) { if (request.autoSubmit) { submit.click(); } else { submit.focus(); } } else { // We need to keep focus somewhere within the form, so that Enter hopefully submits the form. for (let selectors of [OPENID_FIELDS, PASSWORD_FIELDS, USERNAME_FIELDS]) { let field = find(selectors, loginForm); if (field) { field.focus(); break; } } } } return result; } /** * Query all visible elements * * @since 3.0.0 * * @param DOMElement parent Parent element to query * @param object field Field to search for * @param DOMElement form Search only within this form * @return array List of search results */ function queryAllVisible(parent, field, form) { const result = []; for (let i = 0; i < field.selectors.length; i++) { let elems = parent.querySelectorAll(field.selectors[i]); for (let j = 0; j < elems.length; j++) { let elem = elems[j]; // Select only elements from specified form if (form && form != elem.form) { continue; } // Ignore disabled fields if (elem.disabled) { continue; } // Elem or its parent has a style 'display: none', // or it is just too narrow to be a real field (a trap for spammers?). if (elem.offsetWidth < 30 || elem.offsetHeight < 10) { continue; } // We may have a whitelist of acceptable field types. If so, skip elements of a different type. if (field.types && field.types.indexOf(elem.type.toLowerCase()) < 0) { continue; } // Elem takes space on the screen, but it or its parent is hidden with a visibility style. let style = window.getComputedStyle(elem); if (style.visibility == "hidden") { continue; } // Elem is outside of the boundaries of the visible viewport. let rect = elem.getBoundingClientRect(); if ( rect.x + rect.width < 0 || rect.y + rect.height < 0 || rect.x > window.innerWidth || rect.y > window.innerHeight ) { continue; } // Elem is hidden by its or or its parent's opacity rules const OPACITY_LIMIT = 0.1; let opacity = 1; for ( let testElem = elem; opacity >= OPACITY_LIMIT && testElem && testElem.nodeType === Node.ELEMENT_NODE; testElem = testElem.parentNode ) { let style = window.getComputedStyle(testElem); if (style.opacity) { opacity *= parseFloat(style.opacity); } } if (opacity < OPACITY_LIMIT) { continue; } // This element is visible, will use it. result.push(elem); } } return result; } /** * Query first visible element * * @since 3.0.0 * * @param DOMElement parent Parent element to query * @param object field Field to search for * @param DOMElement form Search only within this form * @return array First search result */ function queryFirstVisible(parent, field, form) { var elems = queryAllVisible(parent, field, form); return elems.length > 0 ? elems[0] : undefined; } /** * Detect the login form * * @since 3.0.0 * * @param array selectors Selectors to use to find the right form * @return The login form */ function form(selectors) { const elems = queryAllVisible(document, selectors, undefined); const forms = []; for (let elem of elems) { const form = elem.form; if (form && forms.indexOf(form) < 0) { forms.push(form); } } // Try to filter only forms that have some identifying marker const markedForms = []; for (let form of forms) { const props = ["id", "name", "class", "action"]; for (let marker of FORM_MARKERS) { for (let prop of props) { let propValue = form.getAttribute(prop) || ""; if (propValue.toLowerCase().indexOf(marker) > -1) { markedForms.push(form); } } } } // Try to filter only forms that have a password field const formsWithPassword = []; for (let form of markedForms) { if (find(PASSWORD_FIELDS, form)) { formsWithPassword.push(form); } } // Give up and return the first available form, if any if (formsWithPassword.length > 0) { return formsWithPassword[0]; } if (markedForms.length > 0) { return markedForms[0]; } if (forms.length > 0) { return forms[0]; } return undefined; } /** * Find a form field * * @since 3.0.0 * * @param object field Field to search for * @param DOMElement form Form to search in * @return DOMElement First matching form field */ function find(field, form) { return queryFirstVisible(document, field, form); } /** * Update a form field value * * @since 3.0.0 * * @param object field Field to update * @param string value Value to set * @param DOMElement form Form for which to set the given field * @return bool Whether the update succeeded */ function update(field, value, form) { if (!value.length) { return false; } // Focus the input element first let el = find(field, form); if (!el) { return false; } for (let eventName of ["click", "focus"]) { el.dispatchEvent(new Event(eventName, { bubbles: true })); } // Focus may have triggered unvealing a true input, find it again el = find(field, form); if (!el) { return false; } // Focus the potentially new element again for (let eventName of ["click", "focus"]) { el.dispatchEvent(new Event(eventName, { bubbles: true })); } // Send some keyboard events indicating that value modification has started (no associated keycode) for (let eventName of ["keydown", "keypress", "keyup", "input", "change"]) { el.dispatchEvent(new Event(eventName, { bubbles: true })); } // truncate the value if required by the field if (el.maxLength > 0) { value = value.substr(0, el.maxLength); } // Set the field value let initialValue = el.value || el.getAttribute("value"); el.setAttribute("value", value); el.value = value; // Send the keyboard events again indicating that value modification has finished (no associated keycode) for (let eventName of ["keydown", "keypress", "keyup", "input", "change"]) { el.dispatchEvent(new Event(eventName, { bubbles: true })); } // re-set value if unchanged after firing post-fill events // (in case of sabotage by the site's own event handlers) if ((el.value || el.getAttribute("value")) === initialValue) { el.setAttribute("value", value); el.value = value; } // Finally unfocus the element el.dispatchEvent(new Event("blur", { bubbles: true })); return true; } // set window object window.browserpass = { fillLogin: fillLogin, focusOrSubmit: focusOrSubmit, }; })();