browserpass-extension/src/inject.js
2021-01-01 01:51:13 +13:00

503 lines
17 KiB
JavaScript

(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 === undefined) {
// undefined values should not be filled, but are always considered successful
return true;
}
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,
};
})();