passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
353 lines (322 loc) • 12.8 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 2021 Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) 2021 Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 3.3.0
*/
import {fireEvent} from "@testing-library/dom/dist/events";
const PASSWORD_INPUT_SELECTOR = "input[type='password']:not([hidden]):not([disabled]), input[type='Password']:not([hidden]):not([disabled]), input[type='PASSWORD']:not([hidden]):not([disabled])";
const USERNAME_INPUT_SELECTOR = "input[type='text']:not([hidden]):not([disabled]), input[type='Text']:not([hidden]):not([disabled]), input[type='TEXT']:not([hidden]):not([disabled]), input[type='email']:not([hidden]):not([disabled]), input[type='Email']:not([hidden]):not([disabled]), input[type='EMAIL']:not([hidden]):not([disabled]), input:not([type]):not([hidden]):not([disabled])";
/**
* Fill the login form.
*
* @param {Object} formData
* - {string} loginUsername The username to use
* - {string} secret The password to use
* - {string} url to get domain
*/
const fillForm = function(formData) {
try {
// Validate the fillForm parameters
validateData(formData);
// Check the requested document, current active document is initiated from same origin
if (!isRequestInitiatedFromSameOrigin(formData.url, document.location.origin)) {
throw new Error('The request is not initiated from same origin');
}
// Get password element
const passwordElement = getPasswordElement(formData);
let usernameElement = null;
/*
* If password element exists
* Get username element by using `password's` parent element as reference
*/
if (passwordElement !== null) {
usernameElement = getUsernameElementBasedOnPasswordElement(formData, passwordElement.parentElement);
// If username element exists, fill username
if (usernameElement !== null) {
fillInputField(usernameElement, formData.username);
}
// Fill password
fillInputField(passwordElement, formData.secret);
} else {
/*
* When no password element found on the page
* Check for the username element by giving `document` as reference
*/
usernameElement = getUsernameElement(formData, document);
// If username element exists, fill username
if (usernameElement !== null) {
fillInputField(usernameElement, formData.username);
} else {
throw new Error('Unable to find the username element on this page.');
}
}
// Throw an error when no password and username elements found on the page
if (passwordElement === null && usernameElement === null) {
throw new Error('Unable to find the input elements on this page.');
} else {
// Success message
port.emit(formData.requestId, 'SUCCESS');
}
} catch (error) {
console.error(error);
port.emit(formData.requestId, 'ERROR', {name: "Error", message: error.message});
}
};
/**
* Check the requested document, top document and an iframe form is initiated from same domain.
*
* @param {string} requestedUrl The requested document url
* @param {string} documentUrl The current active document url
* @return {Boolean} true
*/
const isRequestInitiatedFromSameOrigin = function(requestedUrl, documentUrl) {
// requestedUrl - from quickaccess
const parsedRequestedUrl = new URL(requestedUrl);
// Request initiated document origin
const requestedOrigin = parsedRequestedUrl.origin;
// documentUrl - from current active page
const parsedDocumentUrl = new URL(documentUrl);
// Top level document/an iframe document origin
const documentOrigin = parsedDocumentUrl.origin;
// Requested document and top/iframe document origin is same
return requestedOrigin === documentOrigin;
};
/**
* Validate the fillForm parameters
* @param {object} formData
* - {string} username The autofill request username parameter
* - {string} secret The autofill request secret parameter
* - {url} url The autofill request url parameter
*/
const validateData = function(formData) {
const {username, secret, url} = formData;
if (typeof username !== 'string') {
throw new Error('The parameter username is not valid');
}
if (typeof secret !== 'string') {
throw new Error('The parameter secret is not valid');
}
if (typeof url !== 'string') {
throw new Error('The parameter url is not valid');
}
};
/**
* Fill form field.
* @param {DomElement} element The element to fill
* @param {string} value The value to use
*/
const fillInputField = function(element, value) {
/*
* In order to ensure a high level of compatibility with most forms (even ones
* controlled by javascript), the process needs to simulate how a user will
* interact with the form:
* 1. Focus the element by clicking on it;
* 2. Once focused, trigger an input event to change the value of the field.
*/
if (element || '') {
element.addEventListener('click', function clickHandler(event) {
fireEvent.input(element, {target: {value}});
event.target.removeEventListener(event.type, clickHandler); // Remove the event listener after it has fired
}, false);
fireEvent.click(element, {button: 0});
}
};
/**
* Get input elements from an iframe
* @param {string} type - either `password` or `username` to find elements
* @param {Object} formData - to check same origin request
*/
const getInputElementFromIframe = function(type, formData) {
const iframes = document.querySelectorAll("iframe");
let inputElement = null;
for (const i in iframes) {
// Get accessible iframe document
const contentDocument = getAccessedIframeContentDocument(iframes[i]);
if (!contentDocument) {
/*
* The iframe document is not accessible.
* It is the case when the iframe is protected by CSP.
*/
continue;
} else {
/*
* Proceed to search input elements in the iframe document
* When it's accessible cross check whether the iframe is requested from same origin.
*/
if (isRequestInitiatedFromSameOrigin(formData.url, contentDocument.location.origin)) {
inputElement = findInputElementInIframe(type, contentDocument);
if (inputElement || '') {
break;
}
}
}
}
return inputElement;
};
/**
* Returns an accessible iframe document in the page
* @param {DomElement} iframe found on the page
* @return {DomElement} iframe document
*/
const getAccessedIframeContentDocument = function(iframe) {
let iframeContentDocument = null;
try {
iframeContentDocument = iframe.contentDocument;
} catch (error) {
console.error(error);
}
return iframeContentDocument;
};
/**
* Returns an input element in the iframe
* @param {string} type - either `password` or `username` to find elements
* @param {DomElement} iframe document to start the search.
* @return {DomElement} iframe document
*/
const findInputElementInIframe = function(type, iframeDocument) {
let inputElement = null;
if (type === 'password') {
inputElement = iframeDocument.querySelectorAll(PASSWORD_INPUT_SELECTOR);
// Password element has been found.
if (inputElement.length) {
return inputElement[0];
}
} else if (type === 'username') {
inputElement = iframeDocument.querySelectorAll(USERNAME_INPUT_SELECTOR);
if (inputElement.length) {
// When username element found, extract it from an array of dom elements.
inputElement = extractUsernameElementWithFallback(inputElement);
// Username element has been found.
if (inputElement) {
return inputElement;
}
}
}
return null;
};
/**
* Find the password element on the page.
* @return {DomElement/null}
*/
const getPasswordElement = function(formData) {
const passwordElements = document.querySelectorAll(PASSWORD_INPUT_SELECTOR);
// A password element has been found.
if (passwordElements.length) {
for (const passwordElement of passwordElements) {
if (passwordElement.offsetWidth > 0) {
return passwordElement;
}
}
// If all passwords are hidden return null to autofill only the username input (PB-20173)
return null;
} else {
/*
* If no password element found on the page, the login form could be served by an iframe.
* Search the password element in the page iframes. By instance reddit.com login page serves its login
* form in an iframe.
*/
return getInputElementFromIframe('password', formData);
}
};
/**
* Find the username element on the page based on password's parent as reference element.
* @param {DomElement} referenceElement The element reference to start the search.
* @return {DomElement/null}
*/
const getUsernameElementBasedOnPasswordElement = function(formData, referenceElement) {
// No parent element found.
if (referenceElement || '') {
const parentElement = referenceElement.parentElement;
if (!parentElement) {
return null;
}
}
let usernameElement = null;
// The username field can be an input field of type email or text.
const elements = referenceElement.querySelectorAll(USERNAME_INPUT_SELECTOR);
/*
* No input fields found in the reference element.
* Search in the parent.
*/
if (!elements.length) {
return getUsernameElementBasedOnPasswordElement(formData, referenceElement.parentElement);
} else {
// When username element found, extract it from an array of dom elements.
usernameElement = extractUsernameElementWithFallback(elements);
}
/*
* If no username/email element found on the page, the login form could be served by an iframe.
* Search the username/email element in the page iframes. By instance reddit.com signup page serves its login
* form in an iframe.
*/
if (!usernameElement) {
usernameElement = getInputElementFromIframe('username', formData);
}
// A username element has been found.
return usernameElement;
};
/**
* Find the username element on the page.
* @param {DomElement} fallbackUsernameElement The element reference to start the search.
* @return {DomElement/null}
*/
const getUsernameElement = function(formData, fallbackUsernameElement) {
let usernameElement = null;
// The username field can be an input field of type email or text.
const elements = fallbackUsernameElement.querySelectorAll(USERNAME_INPUT_SELECTOR);
// When username element found, extract it from an array of dom elements.
if (elements.length) {
usernameElement = extractUsernameElementWithFallback(elements);
} else {
/*
* If no username/email element found on the page, the login form could be served by an iframe.
* Search the username/email element in the page iframes. By instance reddit.com signup page serves its login
* form in an iframe.
*/
usernameElement = getInputElementFromIframe('username', formData);
}
// A username element has been found.
return usernameElement;
};
/**
* Extract the username element from an array of dom elements.
* @param {array} elements An array of dom elements
* @return {DomElement/null}
*/
const extractUsernameElementWithFallback = function(elements) {
let usernameElement = null;
// Filter elements to find the field that has the highest odd to be the username field.
const inputAttributes = ['id', 'class', 'name', 'placeholder'];
// @todo Translations should be added in order to increase the algorithm success.
const inputAttrValues = ['user', 'email', 'name', 'login'];
// Iterate over visible input list to find the matching input based on filter elements.
foundBreakPoint: {
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
for (let j = 0; j < inputAttributes.length; j++) {
for (let k = 0; k < inputAttrValues.length; k++) {
const matchedInput = element.querySelectorAll(`input[${inputAttributes[j]}*='${inputAttrValues[k]}' i]`);
if (matchedInput.length) {
usernameElement = matchedInput[0];
break foundBreakPoint;
}
}
}
}
}
// When filters fail to find matched elements on the page, use first element from an array of dom elements as username element
if (!usernameElement) {
usernameElement = elements[0];
}
// Return either matched username element based on filters or fallback element
return usernameElement;
};
export const Autofill = {fillForm};