tor-browser

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

accessible.js (17160B)


      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 { Actor } = require("resource://devtools/shared/protocol.js");
      8 const {
      9  accessibleSpec,
     10 } = require("resource://devtools/shared/specs/accessibility.js");
     11 
     12 const {
     13  accessibility: { AUDIT_TYPE },
     14 } = require("resource://devtools/shared/constants.js");
     15 
     16 loader.lazyRequireGetter(
     17  this,
     18  "getContrastRatioFor",
     19  "resource://devtools/server/actors/accessibility/audit/contrast.js",
     20  true
     21 );
     22 loader.lazyRequireGetter(
     23  this,
     24  "auditKeyboard",
     25  "resource://devtools/server/actors/accessibility/audit/keyboard.js",
     26  true
     27 );
     28 loader.lazyRequireGetter(
     29  this,
     30  "auditTextLabel",
     31  "resource://devtools/server/actors/accessibility/audit/text-label.js",
     32  true
     33 );
     34 loader.lazyRequireGetter(
     35  this,
     36  "isDefunct",
     37  "resource://devtools/server/actors/utils/accessibility.js",
     38  true
     39 );
     40 loader.lazyRequireGetter(
     41  this,
     42  "findCssSelector",
     43  "resource://devtools/shared/inspector/css-logic.js",
     44  true
     45 );
     46 loader.lazyRequireGetter(
     47  this,
     48  "getBounds",
     49  "resource://devtools/server/actors/highlighters/utils/accessibility.js",
     50  true
     51 );
     52 loader.lazyRequireGetter(
     53  this,
     54  "isFrameWithChildTarget",
     55  "resource://devtools/shared/layout/utils.js",
     56  true
     57 );
     58 const lazy = {};
     59 loader.lazyGetter(
     60  lazy,
     61  "ContentDOMReference",
     62  () =>
     63    ChromeUtils.importESModule(
     64      "resource://gre/modules/ContentDOMReference.sys.mjs",
     65      // ContentDOMReference needs to be retrieved from the shared global
     66      // since it is a shared singleton.
     67      { global: "shared" }
     68    ).ContentDOMReference
     69 );
     70 
     71 const RELATIONS_TO_IGNORE = new Set([
     72  Ci.nsIAccessibleRelation.RELATION_CONTAINING_APPLICATION,
     73  Ci.nsIAccessibleRelation.RELATION_CONTAINING_TAB_PANE,
     74  Ci.nsIAccessibleRelation.RELATION_CONTAINING_WINDOW,
     75  Ci.nsIAccessibleRelation.RELATION_PARENT_WINDOW_OF,
     76  Ci.nsIAccessibleRelation.RELATION_SUBWINDOW_OF,
     77 ]);
     78 
     79 const nsIAccessibleRole = Ci.nsIAccessibleRole;
     80 const TEXT_ROLES = new Set([
     81  nsIAccessibleRole.ROLE_TEXT_LEAF,
     82  nsIAccessibleRole.ROLE_STATICTEXT,
     83 ]);
     84 
     85 const STATE_DEFUNCT = Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT;
     86 const CSS_TEXT_SELECTOR = "#text";
     87 
     88 /**
     89 * Get node inforamtion such as nodeType and the unique CSS selector for the node.
     90 *
     91 * @param  {DOMNode} node
     92 *         Node for which to get the information.
     93 * @return {object}
     94 *         Information about the type of the node and how to locate it.
     95 */
     96 function getNodeDescription(node) {
     97  if (!node || Cu.isDeadWrapper(node)) {
     98    return { nodeType: undefined, nodeCssSelector: "" };
     99  }
    100 
    101  const { nodeType } = node;
    102  return {
    103    nodeType,
    104    // If node is a text node, we find a unique CSS selector for its parent and add a
    105    // CSS_TEXT_SELECTOR postfix to indicate that it's a text node.
    106    nodeCssSelector:
    107      nodeType === Node.TEXT_NODE
    108        ? `${findCssSelector(node.parentNode)}${CSS_TEXT_SELECTOR}`
    109        : findCssSelector(node),
    110  };
    111 }
    112 
    113 /**
    114 * Get a snapshot of the nsIAccessible object including its subtree. None of the subtree
    115 * queried here is cached via accessible walker's refMap.
    116 *
    117 * @param  {nsIAccessible} acc
    118 *         Accessible object to take a snapshot of.
    119 * @param  {nsIAccessibilityService} a11yService
    120 *         Accessibility service instance in the current process, used to get localized
    121 *         string representation of various accessible properties.
    122 * @param  {WindowGlobalTargetActor} targetActor
    123 * @return {JSON}
    124 *         JSON snapshot of the accessibility tree with root at current accessible.
    125 */
    126 function getSnapshot(acc, a11yService, targetActor) {
    127  if (isDefunct(acc)) {
    128    return {
    129      states: [a11yService.getStringStates(0, STATE_DEFUNCT)],
    130    };
    131  }
    132 
    133  const actions = [];
    134  for (let i = 0; i < acc.actionCount; i++) {
    135    actions.push(acc.getActionDescription(i));
    136  }
    137 
    138  const attributes = {};
    139  if (acc.attributes) {
    140    for (const { key, value } of acc.attributes.enumerate()) {
    141      attributes[key] = value;
    142    }
    143  }
    144 
    145  const state = {};
    146  const extState = {};
    147  acc.getState(state, extState);
    148  const states = [...a11yService.getStringStates(state.value, extState.value)];
    149 
    150  const children = [];
    151  for (let child = acc.firstChild; child; child = child.nextSibling) {
    152    // Ignore children from different documents when we have targets for every documents.
    153    if (
    154      targetActor.ignoreSubFrames &&
    155      child.DOMNode.ownerDocument !== targetActor.contentDocument
    156    ) {
    157      continue;
    158    }
    159    children.push(getSnapshot(child, a11yService, targetActor));
    160  }
    161 
    162  const { nodeType, nodeCssSelector } = getNodeDescription(acc.DOMNode);
    163  const snapshot = {
    164    name: acc.name,
    165    role: getStringRole(acc, a11yService),
    166    actions,
    167    value: acc.value,
    168    nodeCssSelector,
    169    nodeType,
    170    description: acc.description,
    171    keyboardShortcut: acc.accessKey || acc.keyboardShortcut,
    172    childCount: acc.childCount,
    173    indexInParent: acc.indexInParent,
    174    states,
    175    children,
    176    attributes,
    177  };
    178  const useChildTargetToFetchChildren =
    179    acc.role === Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME &&
    180    isFrameWithChildTarget(targetActor, acc.DOMNode);
    181  if (useChildTargetToFetchChildren) {
    182    snapshot.useChildTargetToFetchChildren = useChildTargetToFetchChildren;
    183    snapshot.childCount = 1;
    184    snapshot.contentDOMReference = lazy.ContentDOMReference.get(acc.DOMNode);
    185  }
    186 
    187  return snapshot;
    188 }
    189 
    190 /**
    191 * Get a string indicating the role of the nsIAccessible object.
    192 * An ARIA role token will be returned unless the role can't be mapped to an
    193 * ARIA role (e.g. <iframe>), in which case a Gecko role string will be
    194 * returned.
    195 *
    196 * @param  {nsIAccessible} acc
    197 *         Accessible object to take a snapshot of.
    198 * @param  {nsIAccessibilityService} a11yService
    199 *         Accessibility service instance in the current process, used to get localized
    200 *         string representation of various accessible properties.
    201 * @return String
    202 */
    203 function getStringRole(acc, a11yService) {
    204  let role = acc.computedARIARole;
    205  if (!role) {
    206    // We couldn't map to an ARIA role, so use a Gecko role string.
    207    role = a11yService.getStringRole(acc.role);
    208  }
    209  return role;
    210 }
    211 
    212 /**
    213 * The AccessibleActor provides information about a given accessible object: its
    214 * role, name, states, etc.
    215 */
    216 class AccessibleActor extends Actor {
    217  constructor(walker, rawAccessible) {
    218    super(walker.conn, accessibleSpec);
    219    this.walker = walker;
    220    this.rawAccessible = rawAccessible;
    221 
    222    /**
    223     * Indicates if the raw accessible is no longer alive.
    224     *
    225     * @return Boolean
    226     */
    227    Object.defineProperty(this, "isDefunct", {
    228      get() {
    229        const defunct = isDefunct(this.rawAccessible);
    230        if (defunct) {
    231          delete this.isDefunct;
    232          this.isDefunct = true;
    233          return this.isDefunct;
    234        }
    235 
    236        return defunct;
    237      },
    238      configurable: true,
    239    });
    240  }
    241 
    242  destroy() {
    243    super.destroy();
    244    this.walker = null;
    245    this.rawAccessible = null;
    246  }
    247 
    248  get role() {
    249    if (this.isDefunct) {
    250      return null;
    251    }
    252    return getStringRole(this.rawAccessible, this.walker.a11yService);
    253  }
    254 
    255  get name() {
    256    if (this.isDefunct) {
    257      return null;
    258    }
    259    return this.rawAccessible.name;
    260  }
    261 
    262  get value() {
    263    if (this.isDefunct) {
    264      return null;
    265    }
    266    return this.rawAccessible.value;
    267  }
    268 
    269  get description() {
    270    if (this.isDefunct) {
    271      return null;
    272    }
    273    return this.rawAccessible.description;
    274  }
    275 
    276  get keyboardShortcut() {
    277    if (this.isDefunct) {
    278      return null;
    279    }
    280    // Gecko accessibility exposes two key bindings: Accessible::AccessKey and
    281    // Accessible::KeyboardShortcut. The former is used for accesskey, where the latter
    282    // is used for global shortcuts defined by XUL menu items, etc. Here - do what the
    283    // Windows implementation does: try AccessKey first, and if that's empty, use
    284    // KeyboardShortcut.
    285    return this.rawAccessible.accessKey || this.rawAccessible.keyboardShortcut;
    286  }
    287 
    288  get childCount() {
    289    if (this.isDefunct) {
    290      return 0;
    291    }
    292    // In case of a remote frame declare at least one child (the #document
    293    // element) so that they can be expanded.
    294    if (this.useChildTargetToFetchChildren) {
    295      return 1;
    296    }
    297 
    298    return this.rawAccessible.childCount;
    299  }
    300 
    301  get domNodeType() {
    302    if (this.isDefunct) {
    303      return 0;
    304    }
    305    return this.rawAccessible.DOMNode ? this.rawAccessible.DOMNode.nodeType : 0;
    306  }
    307 
    308  get parentAcc() {
    309    if (this.isDefunct) {
    310      return null;
    311    }
    312    return this.walker.addRef(this.rawAccessible.parent);
    313  }
    314 
    315  children() {
    316    const children = [];
    317    if (this.isDefunct) {
    318      return children;
    319    }
    320 
    321    for (
    322      let child = this.rawAccessible.firstChild;
    323      child;
    324      child = child.nextSibling
    325    ) {
    326      children.push(this.walker.addRef(child));
    327    }
    328    return children;
    329  }
    330 
    331  get indexInParent() {
    332    if (this.isDefunct) {
    333      return -1;
    334    }
    335 
    336    try {
    337      return this.rawAccessible.indexInParent;
    338    } catch (e) {
    339      // Accessible is dead.
    340      return -1;
    341    }
    342  }
    343 
    344  get actions() {
    345    const actions = [];
    346    if (this.isDefunct) {
    347      return actions;
    348    }
    349 
    350    for (let i = 0; i < this.rawAccessible.actionCount; i++) {
    351      actions.push(this.rawAccessible.getActionDescription(i));
    352    }
    353    return actions;
    354  }
    355 
    356  get states() {
    357    if (this.isDefunct) {
    358      return [];
    359    }
    360 
    361    const state = {};
    362    const extState = {};
    363    this.rawAccessible.getState(state, extState);
    364    return [
    365      ...this.walker.a11yService.getStringStates(state.value, extState.value),
    366    ];
    367  }
    368 
    369  get attributes() {
    370    if (this.isDefunct || !this.rawAccessible.attributes) {
    371      return {};
    372    }
    373 
    374    const attributes = {};
    375    for (const { key, value } of this.rawAccessible.attributes.enumerate()) {
    376      attributes[key] = value;
    377    }
    378 
    379    return attributes;
    380  }
    381 
    382  get bounds() {
    383    if (this.isDefunct) {
    384      return null;
    385    }
    386 
    387    let x = {},
    388      y = {},
    389      w = {},
    390      h = {};
    391    try {
    392      this.rawAccessible.getBoundsInCSSPixels(x, y, w, h);
    393      x = x.value;
    394      y = y.value;
    395      w = w.value;
    396      h = h.value;
    397    } catch (e) {
    398      return null;
    399    }
    400 
    401    // Check if accessible bounds are invalid.
    402    const left = x,
    403      right = x + w,
    404      top = y,
    405      bottom = y + h;
    406    if (left === right || top === bottom) {
    407      return null;
    408    }
    409 
    410    return { x, y, w, h };
    411  }
    412 
    413  async getRelations() {
    414    const relationObjects = [];
    415    if (this.isDefunct) {
    416      return relationObjects;
    417    }
    418 
    419    const relations = [
    420      ...this.rawAccessible.getRelations().enumerate(Ci.nsIAccessibleRelation),
    421    ];
    422    if (relations.length === 0) {
    423      return relationObjects;
    424    }
    425 
    426    const doc = await this.walker.getDocument();
    427    if (this.isDestroyed()) {
    428      // This accessible actor is destroyed.
    429      return relationObjects;
    430    }
    431    relations.forEach(relation => {
    432      if (RELATIONS_TO_IGNORE.has(relation.relationType)) {
    433        return;
    434      }
    435 
    436      const type = this.walker.a11yService.getStringRelationType(
    437        relation.relationType
    438      );
    439      const targets = [...relation.getTargets().enumerate(Ci.nsIAccessible)];
    440      let relationObject;
    441      for (const target of targets) {
    442        let targetAcc;
    443        try {
    444          targetAcc = this.walker.attachAccessible(target, doc.rawAccessible);
    445        } catch (e) {
    446          // Target is not available.
    447        }
    448 
    449        if (targetAcc) {
    450          if (!relationObject) {
    451            relationObject = { type, targets: [] };
    452          }
    453 
    454          relationObject.targets.push(targetAcc);
    455        }
    456      }
    457 
    458      if (relationObject) {
    459        relationObjects.push(relationObject);
    460      }
    461    });
    462 
    463    return relationObjects;
    464  }
    465 
    466  get useChildTargetToFetchChildren() {
    467    if (this.isDefunct) {
    468      return false;
    469    }
    470 
    471    return (
    472      this.rawAccessible.role === Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME &&
    473      isFrameWithChildTarget(
    474        this.walker.targetActor,
    475        this.rawAccessible.DOMNode
    476      )
    477    );
    478  }
    479 
    480  form() {
    481    return {
    482      actor: this.actorID,
    483      role: this.role,
    484      name: this.name,
    485      useChildTargetToFetchChildren: this.useChildTargetToFetchChildren,
    486      childCount: this.childCount,
    487      checks: this._lastAudit,
    488    };
    489  }
    490 
    491  /**
    492   * Provide additional (full) information about the accessible object that is
    493   * otherwise missing from the form.
    494   *
    495   * @return {object}
    496   *         Object that contains accessible object information such as states,
    497   *         actions, attributes, etc.
    498   */
    499  hydrate() {
    500    return {
    501      value: this.value,
    502      description: this.description,
    503      keyboardShortcut: this.keyboardShortcut,
    504      domNodeType: this.domNodeType,
    505      indexInParent: this.indexInParent,
    506      states: this.states,
    507      actions: this.actions,
    508      attributes: this.attributes,
    509    };
    510  }
    511 
    512  _isValidTextLeaf(rawAccessible) {
    513    return (
    514      !isDefunct(rawAccessible) &&
    515      TEXT_ROLES.has(rawAccessible.role) &&
    516      rawAccessible.name &&
    517      !!rawAccessible.name.trim().length
    518    );
    519  }
    520 
    521  /**
    522   * Calculate the contrast ratio of the given accessible.
    523   */
    524  async _getContrastRatio() {
    525    if (!this._isValidTextLeaf(this.rawAccessible)) {
    526      return null;
    527    }
    528 
    529    const { bounds } = this;
    530    if (!bounds) {
    531      return null;
    532    }
    533 
    534    const { DOMNode: rawNode } = this.rawAccessible;
    535    const win = rawNode.ownerGlobal;
    536 
    537    // Keep the reference to the walker actor in case the actor gets destroyed
    538    // during the colour contrast ratio calculation.
    539    const { walker } = this;
    540    await walker.clearStyles(win);
    541    const contrastRatio = await getContrastRatioFor(rawNode.parentNode, {
    542      bounds: getBounds(win, bounds),
    543      win,
    544      appliedColorMatrix: this.walker.colorMatrix,
    545    });
    546 
    547    if (this.isDestroyed()) {
    548      // This accessible actor is destroyed.
    549      return null;
    550    }
    551    await walker.restoreStyles(win);
    552 
    553    return contrastRatio;
    554  }
    555 
    556  /**
    557   * Run an accessibility audit for a given audit type.
    558   *
    559   * @param {string} type
    560   *        Type of an audit (Check AUDIT_TYPE in devtools/shared/constants
    561   *        to see available audit types).
    562   *
    563   * @return {null | object}
    564   *         Object that contains accessible audit data for a given type or null
    565   *         if there's nothing to report for this accessible.
    566   */
    567  _getAuditByType(type) {
    568    switch (type) {
    569      case AUDIT_TYPE.CONTRAST:
    570        return this._getContrastRatio();
    571      case AUDIT_TYPE.KEYBOARD:
    572        // Determine if keyboard accessibility is lacking where it is necessary.
    573        return auditKeyboard(this.rawAccessible);
    574      case AUDIT_TYPE.TEXT_LABEL:
    575        // Determine if text alternative is missing for an accessible where it
    576        // is necessary.
    577        return auditTextLabel(this.rawAccessible);
    578      default:
    579        return null;
    580    }
    581  }
    582 
    583  /**
    584   * Audit the state of the accessible object.
    585   *
    586   * @param  {object} options
    587   *         Options for running audit, may include:
    588   *         - types: Array of audit types to be performed during audit.
    589   *
    590   * @return {object | null}
    591   *         Audit results for the accessible object.
    592   */
    593  audit(options = {}) {
    594    if (this._auditing) {
    595      return this._auditing;
    596    }
    597 
    598    const { types } = options;
    599    let auditTypes = Object.values(AUDIT_TYPE);
    600    if (types && types.length) {
    601      auditTypes = auditTypes.filter(auditType => types.includes(auditType));
    602    }
    603 
    604    this._auditing = (async () => {
    605      const results = [];
    606      for (const auditType of auditTypes) {
    607        // For some reason keyboard checks for focus styling affect values (that are
    608        // used by other types of checks (text names and values)) returned by
    609        // accessible objects. This happens only when multiple checks are run at the
    610        // same time (asynchronously) and the audit might return unexpected
    611        // failures. We thus run checks sequentially to avoid this.
    612        // See bug 1594743 for more detail.
    613        const audit = await this._getAuditByType(auditType);
    614        results.push(audit);
    615      }
    616      return results;
    617    })()
    618      .then(results => {
    619        if (this.isDefunct || this.isDestroyed()) {
    620          return null;
    621        }
    622 
    623        const audit = results.reduce((auditResults, result, index) => {
    624          auditResults[auditTypes[index]] = result;
    625          return auditResults;
    626        }, {});
    627        this._lastAudit = this._lastAudit || {};
    628        Object.assign(this._lastAudit, audit);
    629        this.emit("audited", audit);
    630 
    631        return audit;
    632      })
    633      .catch(error => {
    634        if (!this.isDefunct && !this.isDestroyed()) {
    635          throw error;
    636        }
    637        return null;
    638      })
    639      .finally(() => {
    640        this._auditing = null;
    641      });
    642 
    643    return this._auditing;
    644  }
    645 
    646  snapshot() {
    647    return getSnapshot(
    648      this.rawAccessible,
    649      this.walker.a11yService,
    650      this.walker.targetActor
    651    );
    652  }
    653 }
    654 
    655 exports.AccessibleActor = AccessibleActor;