passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
297 lines (273 loc) • 12.2 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 InFormCallToActionField from "./InFormCallToActionField";
import InFormFieldSelector from "./InFormFieldSelector";
import InFormMenuField from "./InformMenuField";
import {fireEvent} from "@testing-library/dom/dist/events";
import InFormCredentialsFormField from "./InFormCredentialsFormField";
import DomUtils from "../Dom/DomUtils";
/**
* Manages the in-form web integration including call-to-action and menu
*/
class InFormManager {
/**
* Default constructor
*/
constructor() {
/** In-form username and password callToActionFields in the target page*/
this.callToActionFields = [];
/** In-form menu menuField in the target page*/
this.menuField = null;
/** In-form form fields in the target page*/
this.credentialsFormFields = [];
/** Mutation observer to detect any change on the DOM */
this.mutationObserver = null;
this.bindCallbacks();
}
/**
* Initializes the in-form manager
*/
initialize() {
this.findAndSetAuthenticationFields();
this.handleDomChange();
this.handleInformCallToActionRepositionEvent();
this.handlePortDisconnectEvent();
this.handleInFormMenuInsertionEvent();
this.handleInFormMenuRemoveEvent();
this.handleInformCallToActionClickEvent();
this.handleGetLastCallToActionClickedInput();
this.handleGetCurrentCredentials();
this.handleFillCredentials();
this.handleFillPassword();
}
/**
* Binds the callbacks
*/
bindCallbacks() {
this.findAndSetAuthenticationFields = this.findAndSetAuthenticationFields.bind(this);
this.handleInformCallToActionClickEvent = this.handleInformCallToActionClickEvent.bind(this);
this.clean = this.clean.bind(this);
this.destroy = this.destroy.bind(this);
}
/**
* Find authentication fields in the document and set them as object properties
*/
findAndSetAuthenticationFields() {
this.findAndSetUsernameAndPasswordFields();
this.findAndSetCredentialsFormFields();
}
/**
* Find authentication callToActionFields in the document and set them as object properties
*/
findAndSetUsernameAndPasswordFields() {
/*
* We find the username / passwords DOM callToActionFields.
* If it was previously found, we reuse the same InformUsernameField, otherwise we create one
* Else we clean and reset callToActionFields
*/
const newUsernameFields = InFormCallToActionField.findAll(InFormFieldSelector.USERNAME_FIELD_SELECTOR);
const newPasswordFields = InFormCallToActionField.findAll(InFormFieldSelector.PASSWORD_FIELD_SELECTOR);
const newFields = newUsernameFields.concat(newPasswordFields);
if (newFields.length > 0) {
this.callToActionFields = newFields.map(newField => {
const matchField = fieldToMatch => callToActionField => callToActionField.field === fieldToMatch;
const existingField = this.callToActionFields.find(matchField(newField));
const fieldType = newField.matches(InFormFieldSelector.USERNAME_FIELD_SELECTOR) ? 'username' : 'password';
return existingField || new InFormCallToActionField(newField, fieldType);
});
} else {
this.clean();
this.callToActionFields = [];
}
}
/**
* Find authentication formFields in the document and set them as object properties
*/
findAndSetCredentialsFormFields() {
/*
* We find the form DOM formFields.
* If it was previously found, we reuse the same InformFormField, otherwise we create one
*/
const newCredentialsFormFields = InFormCredentialsFormField.findAll();
if (newCredentialsFormFields.length > 0) {
this.credentialsFormFields = newCredentialsFormFields.map(newField => {
const matchField = fieldToMatch => credentialsFormField => credentialsFormField.field === fieldToMatch;
const existingField = this.credentialsFormFields.find(matchField(newField));
const usernameField = this.callToActionFields.find(callToActionField => callToActionField.fieldType === 'username' && newField.contains(callToActionField.field));
const passwordField = this.callToActionFields.find(callToActionField => callToActionField.fieldType === 'password' && newField.contains(callToActionField.field));
return existingField || new InFormCredentialsFormField(newField, usernameField?.field, passwordField?.field);
});
} else {
this.credentialsFormFields = [];
}
}
/**
* Clean the DOM of in-form entities
*/
clean() {
this.callToActionFields.forEach(field => field.removeCallToActionIframe());
this.menuField?.removeMenuIframe();
}
/**
* Whenever the DOM changes
*/
handleDomChange() {
const updateAuthenticationFields = mutationsList => {
// Check if a child node has been added or removed with type
const mutationChildNodes = mutation => mutation.type === 'childList';
// Check if the mutation is an iframe added or removed by us
const isMutationInformIframe = mutation => mutationChildNodes(mutation) && this.isInformIframe(mutation);
// Check if our iframe is in the mutation list
const hasNotMutationFromInformIframe = !mutationsList.some(isMutationInformIframe);
if (hasNotMutationFromInformIframe) {
this.findAndSetAuthenticationFields();
this.handleInformCallToActionClickEvent();
}
};
// Search again for authentication callToActionFields to attach when the DOM changes
this.mutationObserver = new MutationObserver(updateAuthenticationFields);
this.mutationObserver.observe(document.body, {attributes: true, childList: true, subtree: true});
}
/**
* Check if the mutation is an iframe added or removed by us
* @param mutation
* @returns {boolean}
*/
isInformIframe(mutation) {
const nodeList = mutation.addedNodes.length > 0 ? mutation.addedNodes : mutation.removedNodes;
let isInformIframe = false;
if (this.callToActionFields.length > 0) {
const isIdPresent = iframe => Array.prototype.some.call(nodeList, node => iframe.iframeId === node.id);
isInformIframe = this.callToActionFields.some(isIdPresent);
}
if (!isInformIframe && this.menuField) {
isInformIframe = Array.prototype.some.call(nodeList, node => this.menuField.iframeId === node.id);
}
return isInformIframe;
}
/**
* Whenever the username / password callToActionFields change its position, reposition the call-to-action
*/
handleInformCallToActionRepositionEvent() {
window.addEventListener('resize', this.clean);
}
/**
* Whenever the user clicks on the in-form call-to-action, it inserts the in-form menu iframe
*/
handleInFormMenuInsertionEvent() {
port.on('passbolt.in-form-menu.open', () => {
this.menuField = new InFormMenuField(this.lastCallToActionFieldClicked.field);
});
}
/**
* Whenever the user clicks on the in-form menu, it removes the in-form menu iframe
*/
handleInFormMenuRemoveEvent() {
port.on('passbolt.in-form-menu.close', () => {
this.menuField.removeMenuIframe();
});
}
/**
* Handle the click on the in-form call-to-action (iframe)
*/
handleInformCallToActionClickEvent() {
const setLastCallToActionFieldClicked = callToActionField => callToActionField.onClick(() => { this.lastCallToActionFieldClicked = callToActionField; });
this.callToActionFields.forEach(setLastCallToActionFieldClicked);
}
/** Whenever one requires to get the type and value of the input attached to the last call-to-action performed */
handleGetLastCallToActionClickedInput() {
port.on('passbolt.web-integration.last-performed-call-to-action-input', requestId => {
port.emit(requestId, 'SUCCESS', {type: this.lastCallToActionFieldClicked.fieldType, value: this.lastCallToActionFieldClicked.field.value});
});
}
/** Whenever one requires to get the current credentials */
handleGetCurrentCredentials() {
port.on('passbolt.web-integration.get-credentials', requestId => {
const currentFieldType = this.lastCallToActionFieldClicked?.fieldType;
const isUsernameType = currentFieldType === 'username';
const isPasswordType = currentFieldType === 'password';
let username = null;
let password = null;
if (!isUsernameType) {
username = this.callToActionFields.find(field => field.fieldType === 'username')?.field.value || "";
password = this.lastCallToActionFieldClicked?.field.value;
}
if (!isPasswordType) {
username = this.lastCallToActionFieldClicked?.field.value;
password = this.callToActionFields.find(field => field.fieldType === 'password')?.field.value || "";
}
port.emit(requestId, 'SUCCESS', {username, password});
});
}
/**
* Whenever one requests to fill the current page form with given credentials
*/
handleFillCredentials() {
port.on('passbolt.web-integration.fill-credentials', ({username, password}) => {
const currentFieldType = this.lastCallToActionFieldClicked?.fieldType;
const isUsernameType = currentFieldType === 'username';
const isPasswordType = currentFieldType === 'password';
if (!isUsernameType) {
fireEvent.input(this.lastCallToActionFieldClicked.field, {target: {value: password}});
// Get username fields and find the one with the lowest common ancestor
const usernameFields = this.callToActionFields
.filter(callToActionField => callToActionField.fieldType === 'username');
const usernameField = DomUtils.getFieldWithLowestCommonAncestor(this.lastCallToActionFieldClicked.field, usernameFields);
if (usernameField) {
fireEvent.input(usernameField.field, {target: {value: username}});
}
} else if (!isPasswordType) {
fireEvent.input(this.lastCallToActionFieldClicked.field, {target: {value: username}});
// Get password fields and find the one with the lowest common ancestor
const passwordFields = this.callToActionFields
.filter(callToActionField => callToActionField.fieldType === 'password');
const passwordField = DomUtils.getFieldWithLowestCommonAncestor(this.lastCallToActionFieldClicked.field, passwordFields);
if (passwordField) {
fireEvent.input(passwordField.field, {target: {value: password}});
}
}
});
}
/**
* Whenever one requests to fill the current page form with a password
*/
handleFillPassword() {
port.on('passbolt.web-integration.fill-password', password => {
this.callToActionFields
.filter(callToActionField => callToActionField.fieldType === 'password')
.forEach(callToActionField => fireEvent.input(callToActionField.field, {target: {value: password}}));
this.menuField.removeMenuIframe();
// Listen the auto-save on the appropriate form field
const formField = this.credentialsFormFields.find(formField => formField.field.contains(this.lastCallToActionFieldClicked.field));
formField?.handleAutoSaveEvent();
});
}
/**
* Remove all event, observer and iframe
*/
destroy() {
this.mutationObserver.disconnect();
this.callToActionFields.forEach(field => field.destroy());
this.menuField?.destroy();
this.credentialsFormFields.forEach(field => field.destroy());
window.removeEventListener('resize', this.clean);
}
/**
* Whenever the port is disconnected due to an update of the extension
*/
handlePortDisconnectEvent() {
port._port.onDisconnect.addListener(this.destroy);
}
}
export default new InFormManager();