/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { FrontClassWithSpec, registerFront, } = require("resource://devtools/shared/protocol.js"); const { accessibleSpec, accessibleWalkerSpec, accessibilitySpec, parentAccessibilitySpec, simulatorSpec, } = require("resource://devtools/shared/specs/accessibility.js"); const events = require("resource://devtools/shared/event-emitter.js"); class AccessibleFront extends FrontClassWithSpec(accessibleSpec) { constructor(client, targetFront, parentFront) { super(client, targetFront, parentFront); this.before("audited", this.audited.bind(this)); this.before("name-change", this.nameChange.bind(this)); this.before("value-change", this.valueChange.bind(this)); this.before("description-change", this.descriptionChange.bind(this)); this.before("shortcut-change", this.shortcutChange.bind(this)); this.before("reorder", this.reorder.bind(this)); this.before("text-change", this.textChange.bind(this)); this.before("index-in-parent-change", this.indexInParentChange.bind(this)); this.before("states-change", this.statesChange.bind(this)); this.before("actions-change", this.actionsChange.bind(this)); this.before("attributes-change", this.attributesChange.bind(this)); } marshallPool() { return this.getParent(); } get useChildTargetToFetchChildren() { return this._form.useChildTargetToFetchChildren; } get role() { return this._form.role; } get name() { return this._form.name; } get value() { return this._form.value; } get description() { return this._form.description; } get keyboardShortcut() { return this._form.keyboardShortcut; } get childCount() { return this._form.childCount; } get domNodeType() { return this._form.domNodeType; } get indexInParent() { return this._form.indexInParent; } get states() { return this._form.states; } get actions() { return this._form.actions; } get attributes() { return this._form.attributes; } get checks() { return this._form.checks; } form(form) { this.actorID = form.actor; this._form = this._form || {}; Object.assign(this._form, form); } nameChange(name, parent) { this._form.name = name; // Name change event affects the tree rendering, we fire this event on // accessibility walker as the point of interaction for UI. const accessibilityWalkerFront = this.getParent(); if (accessibilityWalkerFront) { events.emit(accessibilityWalkerFront, "name-change", this, parent); } } valueChange(value) { this._form.value = value; } descriptionChange(description) { this._form.description = description; } shortcutChange(keyboardShortcut) { this._form.keyboardShortcut = keyboardShortcut; } reorder(childCount) { this._form.childCount = childCount; // Reorder event affects the tree rendering, we fire this event on // accessibility walker as the point of interaction for UI. const accessibilityWalkerFront = this.getParent(); if (accessibilityWalkerFront) { events.emit(accessibilityWalkerFront, "reorder", this); } } textChange() { // Text event affects the tree rendering, we fire this event on // accessibility walker as the point of interaction for UI. const accessibilityWalkerFront = this.getParent(); if (accessibilityWalkerFront) { events.emit(accessibilityWalkerFront, "text-change", this); } } indexInParentChange(indexInParent) { this._form.indexInParent = indexInParent; } statesChange(states) { this._form.states = states; } actionsChange(actions) { this._form.actions = actions; } attributesChange(attributes) { this._form.attributes = attributes; } audited(checks) { this._form.checks = this._form.checks || {}; Object.assign(this._form.checks, checks); } hydrate() { return super.hydrate().then(properties => { Object.assign(this._form, properties); }); } async children() { if (!this.useChildTargetToFetchChildren) { return super.children(); } const { walker: domWalkerFront } = await this.targetFront.getFront( "inspector" ); const node = await domWalkerFront.getNodeFromActor(this.actorID, [ "rawAccessible", "DOMNode", ]); // We are using DOM inspector/walker API here because we want to keep both // the accessiblity tree and the DOM tree in sync. This is necessary for // several features that the accessibility panel provides such as inspecting // a corresponding DOM node or any other functionality that requires DOM // node ancestries to be resolved all the way up to the top level document. const { nodes: [documentNodeFront], } = await domWalkerFront.children(node); const accessibilityFront = await documentNodeFront.targetFront.getFront( "accessibility" ); return accessibilityFront.accessibleWalkerFront.children(); } /** * Helper function that helps with building a complete snapshot of * accessibility tree starting at the level of current accessible front. It * accumulates subtrees from possible out of process frames that are children * of the current accessible front. * @param {JSON} snapshot * Snapshot of the current accessible front or one of its in process * children when recursing. * * @return {JSON} * Complete snapshot of current accessible front. */ async _accumulateSnapshot(snapshot) { const { childCount, useChildTargetToFetchChildren } = snapshot; // No children, we are done. if (childCount === 0) { return snapshot; } // If current accessible is not a remote frame, continue accumulating inside // its children. if (!useChildTargetToFetchChildren) { const childSnapshots = []; for (const childSnapshot of snapshot.children) { childSnapshots.push(this._accumulateSnapshot(childSnapshot)); } await Promise.all(childSnapshots); return snapshot; } // When we have a remote frame, we need to obtain an accessible front for a // remote frame document and retrieve its snapshot. const inspectorFront = await this.targetFront.getFront("inspector"); const frameNodeFront = await inspectorFront.getNodeActorFromContentDomReference( snapshot.contentDOMReference ); // Remove contentDOMReference and useChildTargetToFetchChildren properties. delete snapshot.contentDOMReference; delete snapshot.useChildTargetToFetchChildren; if (!frameNodeFront) { return snapshot; } // Remote frame lives in the same process as the current accessible // front we can retrieve the accessible front directly. const frameAccessibleFront = await this.parentFront.getAccessibleFor( frameNodeFront ); if (!frameAccessibleFront) { return snapshot; } const [docAccessibleFront] = await frameAccessibleFront.children(); const childSnapshot = await docAccessibleFront.snapshot(); snapshot.children.push(childSnapshot); return snapshot; } /** * Retrieves a complete JSON snapshot for an accessible subtree of a given * accessible front (inclduing OOP frames). */ async snapshot() { const snapshot = await super.snapshot(); await this._accumulateSnapshot(snapshot); return snapshot; } } class AccessibleWalkerFront extends FrontClassWithSpec(accessibleWalkerSpec) { constructor(client, targetFront, parentFront) { super(client, targetFront, parentFront); this.documentReady = this.documentReady.bind(this); this.on("document-ready", this.documentReady); } destroy() { this.off("document-ready", this.documentReady); super.destroy(); } form(json) { this.actorID = json.actor; } documentReady() { if (this.targetFront.isTopLevel) { this.emit("top-level-document-ready"); } } pick(doFocus) { if (doFocus) { return this.pickAndFocus(); } return super.pick(); } /** * Get the accessible object ancestry starting from the given accessible to * the top level document. The top level document is in the top level content process. * @param {Object} accessible * Accessible front to determine the ancestry for. * * @return {Array} ancestry * List of ancestry objects which consist of an accessible with its * children. */ async getAncestry(accessible) { const ancestry = await super.getAncestry(accessible); const parentTarget = await this.targetFront.getParentTarget(); if (!parentTarget) { return ancestry; } // Get an accessible front for the parent frame. We go through the // inspector's walker to keep both inspector and accessibility trees in // sync. const { walker: domWalkerFront } = await this.targetFront.getFront( "inspector" ); const frameNodeFront = (await domWalkerFront.getRootNode()).parentNode(); const accessibilityFront = await parentTarget.getFront("accessibility"); const { accessibleWalkerFront } = accessibilityFront; const frameAccessibleFront = await accessibleWalkerFront.getAccessibleFor( frameNodeFront ); if (!frameAccessibleFront) { // Most likely we are inside a hidden frame. return Promise.reject( `Can't get the ancestry for an accessible front ${accessible.actorID}. It is in the detached tree.` ); } // Compose the final ancestry out of ancestry for the given accessible in // the current process and recursively get the ancestry for the frame // accessible. ancestry.push( { accessible: frameAccessibleFront, children: await frameAccessibleFront.children(), }, ...(await accessibleWalkerFront.getAncestry(frameAccessibleFront)) ); return ancestry; } /** * Run an accessibility audit for a document that accessibility walker is * responsible for (in process). In addition to plainly running an audit (in * cases when the document is in the OOP frame), this method also updates * relative ancestries of audited accessible objects all the way up to the top * level document for the toolbox. * @param {Object} options * - {Array} types * types of the accessibility issues to audit for * - {Function} onProgress * callback function for a progress audit-event * - {Boolean} retrieveAncestries (defaults to true) * Set to false to _not_ retrieve ancestries of audited accessible objects. * This is used when a specific document is selected in the iframe picker * and we want to treat it as the root of the accessibility panel tree. */ async audit({ types, onProgress, retrieveAncestries = true }) { const onAudit = new Promise(resolve => { const auditEventHandler = ({ type, ancestries, progress }) => { switch (type) { case "error": this.off("audit-event", auditEventHandler); resolve({ error: true }); break; case "completed": this.off("audit-event", auditEventHandler); resolve({ ancestries }); break; case "progress": onProgress(progress); break; default: break; } }; this.on("audit-event", auditEventHandler); super.startAudit({ types }); }); const audit = await onAudit; // If audit resulted in an error, if there's nothing to report or if the callsite // explicitly asked to not retrieve ancestries, we are done. // (no need to check for ancestry across the remote frame hierarchy). // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1641551 why the rest of // the code path is only supported when content toolbox fission is enabled. if (audit.error || audit.ancestries.length === 0 || !retrieveAncestries) { return audit; } const parentTarget = await this.targetFront.getParentTarget(); // If there is no parent target, we do not need to update ancestries as we // are in the top level document. if (!parentTarget) { return audit; } // Retrieve an ancestry (cross process) for a current root document and make // audit report ancestries relative to it. const [docAccessibleFront] = await this.children(); let docAccessibleAncestry; try { docAccessibleAncestry = await this.getAncestry(docAccessibleFront); } catch (e) { // We are in a detached subtree. We do not consider this an error, instead // we need to ignore the audit for this frame and return an empty report. return { ancestries: [] }; } for (const ancestry of audit.ancestries) { // Compose the final ancestries out of the ones in the audit report // relative to this document and the ancestry of the document itself // (cross process). ancestry.push(...docAccessibleAncestry); } return audit; } /** * A helper wrapper function to show tabbing order overlay for a given target. * The only additional work done is resolving domnode front from a * ContentDOMReference received from a remote target. * * @param {Object} startElm * domnode front to be used as the starting point for generating the * tabbing order. * @param {Number} startIndex * Starting index for the tabbing order. */ async _showTabbingOrder(startElm, startIndex) { const { contentDOMReference, index } = await super.showTabbingOrder( startElm, startIndex ); let elm; if (contentDOMReference) { const inspectorFront = await this.targetFront.getFront("inspector"); elm = await inspectorFront.getNodeActorFromContentDomReference( contentDOMReference ); } return { elm, index }; } /** * Show tabbing order overlay for a given target. * * @param {Object} startElm * domnode front to be used as the starting point for generating the * tabbing order. * @param {Number} startIndex * Starting index for the tabbing order. * * @return {JSON} * Tabbing order information for the last element in the tabbing * order. It includes a domnode front and a tabbing index. If we are * at the end of the tabbing order for the top level content document, * the domnode front will be null. If focus manager discovered a * remote IFRAME, then the domnode front is for the IFRAME itself. */ async showTabbingOrder(startElm, startIndex) { let { elm: currentElm, index: currentIndex } = await this._showTabbingOrder( startElm, startIndex ); // If no remote frames were found, currentElm will be null. while (currentElm) { // Safety check to ensure that the currentElm is a remote frame. if (currentElm.useChildTargetToFetchChildren) { const { walker: domWalkerFront } = await currentElm.targetFront.getFront("inspector"); const { nodes: [childDocumentNodeFront], } = await domWalkerFront.children(currentElm); const { accessibleWalkerFront } = await childDocumentNodeFront.targetFront.getFront("accessibility"); // Show tabbing order in the remote target, while updating the tabbing // index. ({ index: currentIndex } = await accessibleWalkerFront.showTabbingOrder( childDocumentNodeFront, currentIndex )); } // Finished with the remote frame, continue in tabbing order, from the // remote frame. ({ elm: currentElm, index: currentIndex } = await this._showTabbingOrder( currentElm, currentIndex )); } return { elm: currentElm, index: currentIndex }; } } class AccessibilityFront extends FrontClassWithSpec(accessibilitySpec) { constructor(client, targetFront, parentFront) { super(client, targetFront, parentFront); this.before("init", this.init.bind(this)); this.before("shutdown", this.shutdown.bind(this)); // Attribute name from which to retrieve the actorID out of the target // actor's form this.formAttributeName = "accessibilityActor"; } async initialize() { this.accessibleWalkerFront = await super.getWalker(); this.simulatorFront = await super.getSimulator(); const { enabled } = await super.bootstrap(); this.enabled = enabled; try { this._traits = await this.getTraits(); } catch (e) { // @backward-compat { version 84 } getTraits isn't available on older server. this._traits = {}; } } get traits() { return this._traits; } init() { this.enabled = true; } shutdown() { this.enabled = false; } } class ParentAccessibilityFront extends FrontClassWithSpec( parentAccessibilitySpec ) { constructor(client, targetFront, parentFront) { super(client, targetFront, parentFront); this.before("can-be-enabled-change", this.canBeEnabled.bind(this)); this.before("can-be-disabled-change", this.canBeDisabled.bind(this)); // Attribute name from which to retrieve the actorID out of the target // actor's form this.formAttributeName = "parentAccessibilityActor"; } async initialize() { ({ canBeEnabled: this.canBeEnabled, canBeDisabled: this.canBeDisabled } = await super.bootstrap()); } canBeEnabled(canBeEnabled) { this.canBeEnabled = canBeEnabled; } canBeDisabled(canBeDisabled) { this.canBeDisabled = canBeDisabled; } } const SimulatorFront = FrontClassWithSpec(simulatorSpec); exports.AccessibleFront = AccessibleFront; registerFront(AccessibleFront); exports.AccessibleWalkerFront = AccessibleWalkerFront; registerFront(AccessibleWalkerFront); exports.AccessibilityFront = AccessibilityFront; registerFront(AccessibilityFront); exports.ParentAccessibilityFront = ParentAccessibilityFront; registerFront(ParentAccessibilityFront); exports.SimulatorFront = SimulatorFront; registerFront(SimulatorFront);