UNPKG

passbolt-styleguide

Version:

Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.

379 lines (337 loc) 12.9 kB
/** * 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 UserEventsService from "../lib/User/UserEventsService"; import InFormFieldSelector from "../lib/InForm/InFormFieldSelector"; import InFormCallToActionField from "../lib/InForm/InFormCallToActionField"; import { TotpCodeGeneratorService } from "../../shared/services/otp/TotpCodeGeneratorService"; /** * 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"); } const otpElement = getOTPElement(); const passwordElement = getPasswordElement(); 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) { UserEventsService.autofill(usernameElement, formData.username); } // Fill password UserEventsService.autofill(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) { UserEventsService.autofill(usernameElement, formData.username); } } /** * If OTP element exists * Generate the OTP code and fill it */ if (otpElement !== null) { const otp = TotpCodeGeneratorService.generate(formData.otp); if (typeof otp !== "string") { throw new TypeError("Error while generating the TOTP."); } UserEventsService.autofill(otpElement, otp); } // Throw an error when no password and username elements found on the page if (passwordElement === null && usernameElement === null && otpElement === null) { throw new Error("Unable to find the input elements on this page."); } // 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) { try { // 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; } catch (error) { console.error(error); // Empty url or about:blank should not block all the process of autofill return false; } }; /** * Validate the fillForm parameters * @param {object} formData * - {string} username The autofill request username parameter * - {string} secret The autofill request secret parameter * - {string} otp The autofill request otp parameter * - {url} url The autofill request url parameter */ const validateData = function (formData) { const { username, secret, url, otp } = formData; if (username || secret) { 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 (otp) { if (typeof otp !== "object" || typeof otp.secret_key !== "string") { throw new Error("The parameter otp is not valid"); } } if (typeof url !== "string") { throw new Error("The parameter url is not valid"); } if (!username && !secret && !otp) { throw new Error("Either otp or username/secret parameters are required"); } }; /** * 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 iframe of iframes) { // Get accessible iframe document const contentDocument = getAccessedIframeContentDocument(iframe); 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(InFormFieldSelector.PASSWORD_FIELD_SELECTOR); // Password element has been found. if (inputElement.length) { return inputElement[0]; } } else if (type === "username") { inputElement = iframeDocument.querySelectorAll(InFormFieldSelector.USERNAME_FIELD_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 {HTMLInputElement} */ const getPasswordElement = function () { const passwordElements = InFormCallToActionField.findAll(InFormFieldSelector.PASSWORD_FIELD_SELECTOR); for (const passwordElement of passwordElements) { if (passwordElement.offsetWidth > 0) { return passwordElement; } } // No visible password element found return null; }; /** * Find the OTP element on the page. * @return {HTMLInputElement} */ const getOTPElement = function () { const otpElements = InFormCallToActionField.findAll(InFormFieldSelector.OTP_FIELD_SELECTOR); for (const otpElement of otpElements) { if (otpElement.offsetWidth > 0) { return otpElement; } } // No visible OTP element found return null; }; /** * 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 {HTMLInputElement} */ 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(InFormFieldSelector.USERNAME_FIELD_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 {HTMLInputElement} */ 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(InFormFieldSelector.USERNAME_FIELD_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"]; /* * Score each candidate element and keep the one most likely to be the username field. * The score formula (k * attributeCount + j) ensures keyword priority takes precedence * over attribute priority: an element matching "user" (k=0) in any attribute always wins * over one matching "login" (k=3) even in a higher-priority attribute. */ let bestScore = Infinity; 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++) { if (element.matches(`input[${inputAttributes[j]}*='${inputAttrValues[k]}' i]`)) { const score = k * inputAttributes.length + j; if (score < bestScore) { bestScore = score; usernameElement = element; } } } } } // 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 };