tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

accessibility.js (18164B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const {
      8  FrontClassWithSpec,
      9  registerFront,
     10 } = require("resource://devtools/shared/protocol.js");
     11 const {
     12  accessibleSpec,
     13  accessibleWalkerSpec,
     14  accessibilitySpec,
     15  parentAccessibilitySpec,
     16  simulatorSpec,
     17 } = require("resource://devtools/shared/specs/accessibility.js");
     18 
     19 class AccessibleFront extends FrontClassWithSpec(accessibleSpec) {
     20  constructor(client, targetFront, parentFront) {
     21    super(client, targetFront, parentFront);
     22 
     23    this.before("audited", this.audited.bind(this));
     24    this.before("name-change", this.nameChange.bind(this));
     25    this.before("value-change", this.valueChange.bind(this));
     26    this.before("description-change", this.descriptionChange.bind(this));
     27    this.before("shortcut-change", this.shortcutChange.bind(this));
     28    this.before("reorder", this.reorder.bind(this));
     29    this.before("text-change", this.textChange.bind(this));
     30    this.before("index-in-parent-change", this.indexInParentChange.bind(this));
     31    this.before("states-change", this.statesChange.bind(this));
     32    this.before("actions-change", this.actionsChange.bind(this));
     33    this.before("attributes-change", this.attributesChange.bind(this));
     34  }
     35 
     36  marshallPool() {
     37    return this.getParent();
     38  }
     39 
     40  get useChildTargetToFetchChildren() {
     41    return this._form.useChildTargetToFetchChildren;
     42  }
     43 
     44  get role() {
     45    return this._form.role;
     46  }
     47 
     48  get name() {
     49    return this._form.name;
     50  }
     51 
     52  get value() {
     53    return this._form.value;
     54  }
     55 
     56  get description() {
     57    return this._form.description;
     58  }
     59 
     60  get keyboardShortcut() {
     61    return this._form.keyboardShortcut;
     62  }
     63 
     64  get childCount() {
     65    return this._form.childCount;
     66  }
     67 
     68  get domNodeType() {
     69    return this._form.domNodeType;
     70  }
     71 
     72  get indexInParent() {
     73    return this._form.indexInParent;
     74  }
     75 
     76  get states() {
     77    return this._form.states;
     78  }
     79 
     80  get actions() {
     81    return this._form.actions;
     82  }
     83 
     84  get attributes() {
     85    return this._form.attributes;
     86  }
     87 
     88  get checks() {
     89    return this._form.checks;
     90  }
     91 
     92  form(form) {
     93    this.actorID = form.actor;
     94    this._form = this._form || {};
     95    Object.assign(this._form, form);
     96  }
     97 
     98  nameChange(name, parent) {
     99    this._form.name = name;
    100    // Name change event affects the tree rendering, we fire this event on
    101    // accessibility walker as the point of interaction for UI.
    102    const accessibilityWalkerFront = this.getParent();
    103    if (accessibilityWalkerFront) {
    104      accessibilityWalkerFront.emit("name-change", this, parent);
    105    }
    106  }
    107 
    108  valueChange(value) {
    109    this._form.value = value;
    110  }
    111 
    112  descriptionChange(description) {
    113    this._form.description = description;
    114  }
    115 
    116  shortcutChange(keyboardShortcut) {
    117    this._form.keyboardShortcut = keyboardShortcut;
    118  }
    119 
    120  reorder(childCount) {
    121    this._form.childCount = childCount;
    122    // Reorder event affects the tree rendering, we fire this event on
    123    // accessibility walker as the point of interaction for UI.
    124    const accessibilityWalkerFront = this.getParent();
    125    if (accessibilityWalkerFront) {
    126      accessibilityWalkerFront.emit("reorder", this);
    127    }
    128  }
    129 
    130  textChange() {
    131    // Text event affects the tree rendering, we fire this event on
    132    // accessibility walker as the point of interaction for UI.
    133    const accessibilityWalkerFront = this.getParent();
    134    if (accessibilityWalkerFront) {
    135      accessibilityWalkerFront.emit("text-change", this);
    136    }
    137  }
    138 
    139  indexInParentChange(indexInParent) {
    140    this._form.indexInParent = indexInParent;
    141  }
    142 
    143  statesChange(states) {
    144    this._form.states = states;
    145  }
    146 
    147  actionsChange(actions) {
    148    this._form.actions = actions;
    149  }
    150 
    151  attributesChange(attributes) {
    152    this._form.attributes = attributes;
    153  }
    154 
    155  audited(checks) {
    156    this._form.checks = this._form.checks || {};
    157    Object.assign(this._form.checks, checks);
    158  }
    159 
    160  hydrate() {
    161    return super.hydrate().then(properties => {
    162      Object.assign(this._form, properties);
    163    });
    164  }
    165 
    166  async children() {
    167    if (!this.useChildTargetToFetchChildren) {
    168      return super.children();
    169    }
    170 
    171    const { walker: domWalkerFront } =
    172      await this.targetFront.getFront("inspector");
    173    const node = await domWalkerFront.getNodeFromActor(this.actorID, [
    174      "rawAccessible",
    175      "DOMNode",
    176    ]);
    177    // We are using DOM inspector/walker API here because we want to keep both
    178    // the accessiblity tree and the DOM tree in sync. This is necessary for
    179    // several features that the accessibility panel provides such as inspecting
    180    // a corresponding DOM node or any other functionality that requires DOM
    181    // node ancestries to be resolved all the way up to the top level document.
    182    const {
    183      nodes: [documentNodeFront],
    184    } = await domWalkerFront.children(node);
    185    const accessibilityFront =
    186      await documentNodeFront.targetFront.getFront("accessibility");
    187 
    188    return accessibilityFront.accessibleWalkerFront.children();
    189  }
    190 
    191  /**
    192   * Helper function that helps with building a complete snapshot of
    193   * accessibility tree starting at the level of current accessible front. It
    194   * accumulates subtrees from possible out of process frames that are children
    195   * of the current accessible front.
    196   *
    197   * @param  {JSON} snapshot
    198   *         Snapshot of the current accessible front or one of its in process
    199   *         children when recursing.
    200   *
    201   * @return {JSON}
    202   *         Complete snapshot of current accessible front.
    203   */
    204  async _accumulateSnapshot(snapshot) {
    205    const { childCount, useChildTargetToFetchChildren } = snapshot;
    206    // No children, we are done.
    207    if (childCount === 0) {
    208      return snapshot;
    209    }
    210 
    211    // If current accessible is not a remote frame, continue accumulating inside
    212    // its children.
    213    if (!useChildTargetToFetchChildren) {
    214      const childSnapshots = [];
    215      for (const childSnapshot of snapshot.children) {
    216        childSnapshots.push(this._accumulateSnapshot(childSnapshot));
    217      }
    218      await Promise.all(childSnapshots);
    219      return snapshot;
    220    }
    221 
    222    // When we have a remote frame, we need to obtain an accessible front for a
    223    // remote frame document and retrieve its snapshot.
    224    const inspectorFront = await this.targetFront.getFront("inspector");
    225    const frameNodeFront =
    226      await inspectorFront.getNodeActorFromContentDomReference(
    227        snapshot.contentDOMReference
    228      );
    229    // Remove contentDOMReference and useChildTargetToFetchChildren properties.
    230    delete snapshot.contentDOMReference;
    231    delete snapshot.useChildTargetToFetchChildren;
    232    if (!frameNodeFront) {
    233      return snapshot;
    234    }
    235 
    236    // Remote frame lives in the same process as the current accessible
    237    // front we can retrieve the accessible front directly.
    238    const frameAccessibleFront =
    239      await this.parentFront.getAccessibleFor(frameNodeFront);
    240    if (!frameAccessibleFront) {
    241      return snapshot;
    242    }
    243 
    244    const [docAccessibleFront] = await frameAccessibleFront.children();
    245    const childSnapshot = await docAccessibleFront.snapshot();
    246    snapshot.children.push(childSnapshot);
    247 
    248    return snapshot;
    249  }
    250 
    251  /**
    252   * Retrieves a complete JSON snapshot for an accessible subtree of a given
    253   * accessible front (inclduing OOP frames).
    254   */
    255  async snapshot() {
    256    const snapshot = await super.snapshot();
    257    await this._accumulateSnapshot(snapshot);
    258    return snapshot;
    259  }
    260 }
    261 
    262 class AccessibleWalkerFront extends FrontClassWithSpec(accessibleWalkerSpec) {
    263  constructor(client, targetFront, parentFront) {
    264    super(client, targetFront, parentFront);
    265 
    266    this.documentReady = this.documentReady.bind(this);
    267    this.on("document-ready", this.documentReady);
    268  }
    269 
    270  destroy() {
    271    this.off("document-ready", this.documentReady);
    272    super.destroy();
    273  }
    274 
    275  form(json) {
    276    this.actorID = json.actor;
    277  }
    278 
    279  documentReady() {
    280    if (this.targetFront.isTopLevel) {
    281      this.emit("top-level-document-ready");
    282    }
    283  }
    284 
    285  pick(doFocus) {
    286    if (doFocus) {
    287      return this.pickAndFocus();
    288    }
    289 
    290    return super.pick();
    291  }
    292 
    293  /**
    294   * Get the accessible object ancestry starting from the given accessible to
    295   * the top level document. The top level document is in the top level content process.
    296   *
    297   * @param  {object} accessible
    298   *         Accessible front to determine the ancestry for.
    299   *
    300   * @return {Array}  ancestry
    301   *         List of ancestry objects which consist of an accessible with its
    302   *         children.
    303   */
    304  async getAncestry(accessible) {
    305    const ancestry = await super.getAncestry(accessible);
    306 
    307    const parentTarget = await this.targetFront.getParentTarget();
    308    if (!parentTarget) {
    309      return ancestry;
    310    }
    311 
    312    // Get an accessible front for the parent frame. We go through the
    313    // inspector's walker to keep both inspector and accessibility trees in
    314    // sync.
    315    const { walker: domWalkerFront } =
    316      await this.targetFront.getFront("inspector");
    317    const frameNodeFront = (await domWalkerFront.getRootNode()).parentNode();
    318    const accessibilityFront = await parentTarget.getFront("accessibility");
    319    const { accessibleWalkerFront } = accessibilityFront;
    320    const frameAccessibleFront =
    321      await accessibleWalkerFront.getAccessibleFor(frameNodeFront);
    322 
    323    if (!frameAccessibleFront) {
    324      // Most likely we are inside a hidden frame.
    325      return Promise.reject(
    326        `Can't get the ancestry for an accessible front ${accessible.actorID}. It is in the detached tree.`
    327      );
    328    }
    329 
    330    // Compose the final ancestry out of ancestry for the given accessible in
    331    // the current process and recursively get the ancestry for the frame
    332    // accessible.
    333    ancestry.push(
    334      {
    335        accessible: frameAccessibleFront,
    336        children: await frameAccessibleFront.children(),
    337      },
    338      ...(await accessibleWalkerFront.getAncestry(frameAccessibleFront))
    339    );
    340 
    341    return ancestry;
    342  }
    343 
    344  /**
    345   * Run an accessibility audit for a document that accessibility walker is
    346   * responsible for (in process). In addition to plainly running an audit (in
    347   * cases when the document is in the OOP frame), this method also updates
    348   * relative ancestries of audited accessible objects all the way up to the top
    349   * level document for the toolbox.
    350   *
    351   * @param {object} options
    352   *                 - {Array}    types
    353   *                   types of the accessibility issues to audit for
    354   *                 - {Function} onProgress
    355   *                   callback function for a progress audit-event
    356   *                 - {Boolean} retrieveAncestries (defaults to true)
    357   *                   Set to false to _not_ retrieve ancestries of audited accessible objects.
    358   *                   This is used when a specific document is selected in the iframe picker
    359   *                   and we want to treat it as the root of the accessibility panel tree.
    360   */
    361  async audit({ types, onProgress, retrieveAncestries = true }) {
    362    const onAudit = new Promise(resolve => {
    363      const auditEventHandler = ({ type, ancestries, progress }) => {
    364        switch (type) {
    365          case "error":
    366            this.off("audit-event", auditEventHandler);
    367            resolve({ error: true });
    368            break;
    369          case "completed":
    370            this.off("audit-event", auditEventHandler);
    371            resolve({ ancestries });
    372            break;
    373          case "progress":
    374            onProgress(progress);
    375            break;
    376          default:
    377            break;
    378        }
    379      };
    380 
    381      this.on("audit-event", auditEventHandler);
    382      super.startAudit({ types });
    383    });
    384 
    385    const audit = await onAudit;
    386    // If audit resulted in an error, if there's nothing to report or if the callsite
    387    // explicitly asked to not retrieve ancestries, we are done.
    388    // (no need to check for ancestry across the remote frame hierarchy).
    389    // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1641551 why the rest of
    390    // the code path is only supported when content toolbox fission is enabled.
    391    if (audit.error || audit.ancestries.length === 0 || !retrieveAncestries) {
    392      return audit;
    393    }
    394 
    395    const parentTarget = await this.targetFront.getParentTarget();
    396    // If there is no parent target, we do not need to update ancestries as we
    397    // are in the top level document.
    398    if (!parentTarget) {
    399      return audit;
    400    }
    401 
    402    // Retrieve an ancestry (cross process) for a current root document and make
    403    // audit report ancestries relative to it.
    404    const [docAccessibleFront] = await this.children();
    405    let docAccessibleAncestry;
    406    try {
    407      docAccessibleAncestry = await this.getAncestry(docAccessibleFront);
    408    } catch (e) {
    409      // We are in a detached subtree. We do not consider this an error, instead
    410      // we need to ignore the audit for this frame and return an empty report.
    411      return { ancestries: [] };
    412    }
    413    for (const ancestry of audit.ancestries) {
    414      // Compose the final ancestries out of the ones in the audit report
    415      // relative to this document and the ancestry of the document itself
    416      // (cross process).
    417      ancestry.push(...docAccessibleAncestry);
    418    }
    419 
    420    return audit;
    421  }
    422 
    423  /**
    424   * A helper wrapper function to show tabbing order overlay for a given target.
    425   * The only additional work done is resolving domnode front from a
    426   * ContentDOMReference received from a remote target.
    427   *
    428   * @param  {object} startElm
    429   *         domnode front to be used as the starting point for generating the
    430   *         tabbing order.
    431   * @param  {number} startIndex
    432   *         Starting index for the tabbing order.
    433   */
    434  async _showTabbingOrder(startElm, startIndex) {
    435    const { contentDOMReference, index } = await super.showTabbingOrder(
    436      startElm,
    437      startIndex
    438    );
    439    let elm;
    440    if (contentDOMReference) {
    441      const inspectorFront = await this.targetFront.getFront("inspector");
    442      elm =
    443        await inspectorFront.getNodeActorFromContentDomReference(
    444          contentDOMReference
    445        );
    446    }
    447 
    448    return { elm, index };
    449  }
    450 
    451  /**
    452   * Show tabbing order overlay for a given target.
    453   *
    454   * @param  {object} startElm
    455   *         domnode front to be used as the starting point for generating the
    456   *         tabbing order.
    457   * @param  {number} startIndex
    458   *         Starting index for the tabbing order.
    459   *
    460   * @return {JSON}
    461   *         Tabbing order information for the last element in the tabbing
    462   *         order. It includes a domnode front and a tabbing index. If we are
    463   *         at the end of the tabbing order for the top level content document,
    464   *         the domnode front will be null. If focus manager discovered a
    465   *         remote IFRAME, then the domnode front is for the IFRAME itself.
    466   */
    467  async showTabbingOrder(startElm, startIndex) {
    468    let { elm: currentElm, index: currentIndex } = await this._showTabbingOrder(
    469      startElm,
    470      startIndex
    471    );
    472 
    473    // If no remote frames were found, currentElm will be null.
    474    while (currentElm) {
    475      // Safety check to ensure that the currentElm is a remote frame.
    476      if (currentElm.useChildTargetToFetchChildren) {
    477        const { walker: domWalkerFront } =
    478          await currentElm.targetFront.getFront("inspector");
    479        const {
    480          nodes: [childDocumentNodeFront],
    481        } = await domWalkerFront.children(currentElm);
    482        const { accessibleWalkerFront } =
    483          await childDocumentNodeFront.targetFront.getFront("accessibility");
    484        // Show tabbing order in the remote target, while updating the tabbing
    485        // index.
    486        ({ index: currentIndex } = await accessibleWalkerFront.showTabbingOrder(
    487          childDocumentNodeFront,
    488          currentIndex
    489        ));
    490      }
    491 
    492      // Finished with the remote frame, continue in tabbing order, from the
    493      // remote frame.
    494      ({ elm: currentElm, index: currentIndex } = await this._showTabbingOrder(
    495        currentElm,
    496        currentIndex
    497      ));
    498    }
    499 
    500    return { elm: currentElm, index: currentIndex };
    501  }
    502 }
    503 
    504 class AccessibilityFront extends FrontClassWithSpec(accessibilitySpec) {
    505  constructor(client, targetFront, parentFront) {
    506    super(client, targetFront, parentFront);
    507 
    508    this.before("init", this.init.bind(this));
    509    this.before("shutdown", this.shutdown.bind(this));
    510 
    511    // Attribute name from which to retrieve the actorID out of the target
    512    // actor's form
    513    this.formAttributeName = "accessibilityActor";
    514  }
    515 
    516  async initialize() {
    517    this.accessibleWalkerFront = await super.getWalker();
    518    this.simulatorFront = await super.getSimulator();
    519    const { enabled } = await super.bootstrap();
    520    this.enabled = enabled;
    521 
    522    try {
    523      this._traits = await this.getTraits();
    524    } catch (e) {
    525      // @backward-compat { version 84 } getTraits isn't available on older server.
    526      this._traits = {};
    527    }
    528  }
    529 
    530  get traits() {
    531    return this._traits;
    532  }
    533 
    534  init() {
    535    this.enabled = true;
    536  }
    537 
    538  shutdown() {
    539    this.enabled = false;
    540  }
    541 }
    542 
    543 class ParentAccessibilityFront extends FrontClassWithSpec(
    544  parentAccessibilitySpec
    545 ) {
    546  constructor(client, targetFront, parentFront) {
    547    super(client, targetFront, parentFront);
    548    this.before("can-be-enabled-change", this.canBeEnabled.bind(this));
    549    this.before("can-be-disabled-change", this.canBeDisabled.bind(this));
    550 
    551    // Attribute name from which to retrieve the actorID out of the target
    552    // actor's form
    553    this.formAttributeName = "parentAccessibilityActor";
    554  }
    555 
    556  async initialize() {
    557    ({ canBeEnabled: this.canBeEnabled, canBeDisabled: this.canBeDisabled } =
    558      await super.bootstrap());
    559  }
    560 
    561  canBeEnabled(canBeEnabled) {
    562    this.canBeEnabled = canBeEnabled;
    563  }
    564 
    565  canBeDisabled(canBeDisabled) {
    566    this.canBeDisabled = canBeDisabled;
    567  }
    568 }
    569 
    570 const SimulatorFront = FrontClassWithSpec(simulatorSpec);
    571 
    572 registerFront(AccessibleFront);
    573 registerFront(AccessibleWalkerFront);
    574 registerFront(AccessibilityFront);
    575 registerFront(ParentAccessibilityFront);
    576 registerFront(SimulatorFront);