tor-browser

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

accessibility-proxy.js (18697B)


      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 loader.lazyRequireGetter(
      8  this,
      9  "CombinedProgress",
     10  "resource://devtools/client/accessibility/utils/audit.js",
     11  true
     12 );
     13 
     14 const {
     15  accessibility: { AUDIT_TYPE },
     16 } = require("resource://devtools/shared/constants.js");
     17 const {
     18  FILTERS,
     19 } = require("resource://devtools/client/accessibility/constants.js");
     20 
     21 /**
     22 * Component responsible for tracking all Accessibility fronts in parent and
     23 * content processes.
     24 */
     25 class AccessibilityProxy {
     26  #panel;
     27  #initialized;
     28  constructor(commands, panel) {
     29    this.commands = commands;
     30    this.#panel = panel;
     31 
     32    this.#initialized = false;
     33    this._accessibilityWalkerFronts = new Set();
     34    this.lifecycleEvents = new Map();
     35    this.accessibilityEvents = new Map();
     36 
     37    this.audit = this.audit.bind(this);
     38    this.enableAccessibility = this.enableAccessibility.bind(this);
     39    this.getAccessibilityTreeRoot = this.getAccessibilityTreeRoot.bind(this);
     40    this.resetAccessiblity = this.resetAccessiblity.bind(this);
     41    this.startListeningForAccessibilityEvents =
     42      this.startListeningForAccessibilityEvents.bind(this);
     43    this.startListeningForLifecycleEvents =
     44      this.startListeningForLifecycleEvents.bind(this);
     45    this.startListeningForParentLifecycleEvents =
     46      this.startListeningForParentLifecycleEvents.bind(this);
     47    this.stopListeningForAccessibilityEvents =
     48      this.stopListeningForAccessibilityEvents.bind(this);
     49    this.stopListeningForLifecycleEvents =
     50      this.stopListeningForLifecycleEvents.bind(this);
     51    this.stopListeningForParentLifecycleEvents =
     52      this.stopListeningForParentLifecycleEvents.bind(this);
     53    this.highlightAccessible = this.highlightAccessible.bind(this);
     54    this.unhighlightAccessible = this.unhighlightAccessible.bind(this);
     55    this.onTargetAvailable = this.onTargetAvailable.bind(this);
     56    this.onTargetDestroyed = this.onTargetDestroyed.bind(this);
     57    this.onTargetSelected = this.onTargetSelected.bind(this);
     58    this.onAccessibilityFrontAvailable =
     59      this.onAccessibilityFrontAvailable.bind(this);
     60    this.onAccessibilityFrontDestroyed =
     61      this.onAccessibilityFrontDestroyed.bind(this);
     62    this.onAccessibleWalkerFrontAvailable =
     63      this.onAccessibleWalkerFrontAvailable.bind(this);
     64    this.onAccessibleWalkerFrontDestroyed =
     65      this.onAccessibleWalkerFrontDestroyed.bind(this);
     66    this.unhighlightBeforeCalling = this.unhighlightBeforeCalling.bind(this);
     67    this.toggleDisplayTabbingOrder = this.toggleDisplayTabbingOrder.bind(this);
     68  }
     69 
     70  get enabled() {
     71    return this.accessibilityFront && this.accessibilityFront.enabled;
     72  }
     73 
     74  /**
     75   * Indicates whether the accessibility service is enabled.
     76   */
     77  get canBeEnabled() {
     78    return this.parentAccessibilityFront.canBeEnabled;
     79  }
     80 
     81  get currentTarget() {
     82    return this.commands.targetCommand.selectedTargetFront;
     83  }
     84 
     85  /**
     86   * Perform an audit for a given filter.
     87   *
     88   * @param  {string} filter
     89   *         Type of an audit to perform.
     90   * @param  {Function} onProgress
     91   *         Audit progress callback.
     92   *
     93   * @return {Promise}
     94   *         Resolves when the audit for every document, that each of the frame
     95   *         accessibility walkers traverse, completes.
     96   */
     97  async audit(filter, onProgress) {
     98    const types = filter === FILTERS.ALL ? Object.values(AUDIT_TYPE) : [filter];
     99 
    100    const targetTypes = [this.commands.targetCommand.TYPES.FRAME];
    101    const targets =
    102      await this.commands.targetCommand.getAllTargetsInSelectedTargetTree(
    103        targetTypes
    104      );
    105 
    106    const progress = new CombinedProgress({
    107      onProgress,
    108      totalFrames: targets.length,
    109    });
    110    const audits = await this.withAllAccessibilityWalkerFronts(
    111      async accessibleWalkerFront =>
    112        accessibleWalkerFront.audit({
    113          types,
    114          onProgress: progress.onProgressForWalker.bind(
    115            progress,
    116            accessibleWalkerFront
    117          ),
    118          // If a frame was selected in the iframe picker, we don't want to retrieve the
    119          // ancestries at it would mess with the tree structure and would make it misbehave.
    120          retrieveAncestries:
    121            this.commands.targetCommand.isTopLevelTargetSelected(),
    122        })
    123    );
    124 
    125    // Accumulate all audits into a single structure.
    126    const combinedAudit = { ancestries: [] };
    127    for (const audit of audits) {
    128      // If any of the audits resulted in an error, no need to continue.
    129      if (audit.error) {
    130        return audit;
    131      }
    132 
    133      combinedAudit.ancestries.push(...audit.ancestries);
    134    }
    135 
    136    return combinedAudit;
    137  }
    138 
    139  async toggleDisplayTabbingOrder(displayTabbingOrder) {
    140    if (displayTabbingOrder) {
    141      const { walker: domWalkerFront } =
    142        await this.currentTarget.getFront("inspector");
    143      await this.accessibilityFront.accessibleWalkerFront.showTabbingOrder(
    144        await domWalkerFront.getRootNode(),
    145        0
    146      );
    147    } else {
    148      // we don't want to use withAllAccessibilityWalkerFronts as it only acts on selected
    149      // target tree, and we want to hide _all_ highlighters.
    150      const accessibilityFronts =
    151        await this.commands.targetCommand.getAllFronts(
    152          [this.commands.targetCommand.TYPES.FRAME],
    153          "accessibility"
    154        );
    155      await Promise.all(
    156        accessibilityFronts.map(accessibilityFront =>
    157          accessibilityFront.accessibleWalkerFront.hideTabbingOrder()
    158        )
    159      );
    160    }
    161  }
    162 
    163  async enableAccessibility() {
    164    // Accessibility service is initialized using the parent accessibility
    165    // front. That, in turn, initializes accessibility service in all content
    166    // processes. We need to wait until that happens to be sure platform
    167    // accessibility is fully enabled.
    168    const enabled = this.accessibilityFront.once("init");
    169    await this.parentAccessibilityFront.enable();
    170    await enabled;
    171  }
    172 
    173  /**
    174   * Return the topmost level accessibility walker to be used as the root of
    175   * the accessibility tree view.
    176   *
    177   * @return {object}
    178   *         Topmost accessibility walker.
    179   */
    180  getAccessibilityTreeRoot() {
    181    return this.accessibilityFront.accessibleWalkerFront;
    182  }
    183 
    184  /**
    185   * Look up accessibility fronts (get an existing one or create a new one) for
    186   * all existing target fronts and run a task with each one of them.
    187   *
    188   * @param {Function} task
    189   *        Function to execute with each accessiblity front.
    190   */
    191  async withAllAccessibilityFronts(taskFn) {
    192    const accessibilityFronts = await this.commands.targetCommand.getAllFronts(
    193      [this.commands.targetCommand.TYPES.FRAME],
    194      "accessibility",
    195      {
    196        // only get the fronts for the selected frame tree, in case a specific document
    197        // is selected in the iframe picker (if not, the top-level target is considered
    198        // as the selected target)
    199        onlyInSelectedTargetTree: true,
    200      }
    201    );
    202    const tasks = [];
    203    for (const accessibilityFront of accessibilityFronts) {
    204      tasks.push(taskFn(accessibilityFront));
    205    }
    206 
    207    return Promise.all(tasks);
    208  }
    209 
    210  /**
    211   * Look up accessibility walker fronts (get an existing one or create a new
    212   * one using accessibility front) for all existing target fronts and run a
    213   * task with each one of them.
    214   *
    215   * @param {Function} task
    216   *        Function to execute with each accessiblity walker front.
    217   */
    218  withAllAccessibilityWalkerFronts(taskFn) {
    219    return this.withAllAccessibilityFronts(async accessibilityFront =>
    220      taskFn(accessibilityFront.accessibleWalkerFront)
    221    );
    222  }
    223 
    224  /**
    225   * Unhighlight previous accessible object if we switched between processes and
    226   * call the appropriate event handler.
    227   */
    228  unhighlightBeforeCalling(listener) {
    229    return async accessible => {
    230      if (accessible) {
    231        const accessibleWalkerFront = accessible.getParent();
    232        if (this._currentAccessibleWalkerFront !== accessibleWalkerFront) {
    233          if (this._currentAccessibleWalkerFront) {
    234            await this._currentAccessibleWalkerFront.unhighlight();
    235          }
    236 
    237          this._currentAccessibleWalkerFront = accessibleWalkerFront;
    238        }
    239      }
    240 
    241      await listener(accessible);
    242    };
    243  }
    244 
    245  /**
    246   * Start picking and add walker listeners.
    247   *
    248   * @param  {boolean} doFocus
    249   *         If true, move keyboard focus into content.
    250   */
    251  pick(doFocus, onHovered, onPicked, onPreviewed, onCanceled) {
    252    return this.withAllAccessibilityWalkerFronts(
    253      async accessibleWalkerFront => {
    254        this.startListening(accessibleWalkerFront, {
    255          events: {
    256            "picker-accessible-hovered":
    257              this.unhighlightBeforeCalling(onHovered),
    258            "picker-accessible-picked": this.unhighlightBeforeCalling(onPicked),
    259            "picker-accessible-previewed":
    260              this.unhighlightBeforeCalling(onPreviewed),
    261            "picker-accessible-canceled":
    262              this.unhighlightBeforeCalling(onCanceled),
    263          },
    264          // Only register listeners once (for top level), no need to register
    265          // them for all walkers again and again.
    266          register: accessibleWalkerFront.targetFront.isTopLevel,
    267        });
    268        await accessibleWalkerFront.pick(
    269          // Only pass doFocus to the top level accessibility walker front.
    270          doFocus && accessibleWalkerFront.targetFront.isTopLevel
    271        );
    272      }
    273    );
    274  }
    275 
    276  /**
    277   * Stop picking and remove all walker listeners.
    278   */
    279  async cancelPick() {
    280    this._currentAccessibleWalkerFront = null;
    281    return this.withAllAccessibilityWalkerFronts(
    282      async accessibleWalkerFront => {
    283        await accessibleWalkerFront.cancelPick();
    284        this.stopListening(accessibleWalkerFront, {
    285          events: {
    286            "picker-accessible-hovered": null,
    287            "picker-accessible-picked": null,
    288            "picker-accessible-previewed": null,
    289            "picker-accessible-canceled": null,
    290          },
    291          // Only unregister listeners once (for top level), no need to
    292          // unregister them for all walkers again and again.
    293          unregister: accessibleWalkerFront.targetFront.isTopLevel,
    294        });
    295      }
    296    );
    297  }
    298 
    299  async resetAccessiblity() {
    300    const { enabled } = this.accessibilityFront;
    301    const { canBeEnabled, canBeDisabled } = this.parentAccessibilityFront;
    302    return { enabled, canBeDisabled, canBeEnabled };
    303  }
    304 
    305  startListening(front, { events, register = false } = {}) {
    306    for (const [type, listener] of Object.entries(events)) {
    307      front.on(type, listener);
    308      if (register) {
    309        this.registerEvent(front, type, listener);
    310      }
    311    }
    312  }
    313 
    314  stopListening(front, { events, unregister = false } = {}) {
    315    for (const [type, listener] of Object.entries(events)) {
    316      front.off(type, listener);
    317      if (unregister) {
    318        this.unregisterEvent(front, type, listener);
    319      }
    320    }
    321  }
    322 
    323  startListeningForAccessibilityEvents(events) {
    324    for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) {
    325      this.startListening(accessibleWalkerFront, {
    326        events,
    327        // Only register listeners once (for top level), no need to register
    328        // them for all walkers again and again.
    329        register: accessibleWalkerFront.targetFront.isTopLevel,
    330      });
    331    }
    332  }
    333 
    334  stopListeningForAccessibilityEvents(events) {
    335    for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) {
    336      this.stopListening(accessibleWalkerFront, {
    337        events,
    338        // Only unregister listeners once (for top level), no need to unregister
    339        // them for all walkers again and again.
    340        unregister: accessibleWalkerFront.targetFront.isTopLevel,
    341      });
    342    }
    343  }
    344 
    345  startListeningForLifecycleEvents(events) {
    346    this.startListening(this.accessibilityFront, { events, register: true });
    347  }
    348 
    349  stopListeningForLifecycleEvents(events) {
    350    this.stopListening(this.accessibilityFront, { events, unregister: true });
    351  }
    352 
    353  startListeningForParentLifecycleEvents(events) {
    354    this.startListening(this.parentAccessibilityFront, {
    355      events,
    356      register: false,
    357    });
    358  }
    359 
    360  stopListeningForParentLifecycleEvents(events) {
    361    this.stopListening(this.parentAccessibilityFront, {
    362      events,
    363      unregister: false,
    364    });
    365  }
    366 
    367  highlightAccessible(accessibleFront, options) {
    368    if (!accessibleFront) {
    369      return;
    370    }
    371 
    372    const accessibleWalkerFront = accessibleFront.getParent();
    373    if (!accessibleWalkerFront) {
    374      return;
    375    }
    376 
    377    accessibleWalkerFront
    378      .highlightAccessible(accessibleFront, options)
    379      .catch(error => {
    380        // Only report an error where there's still a commands instance.
    381        // Ignore cases where toolbox is already destroyed.
    382        if (this.commands) {
    383          console.error(error);
    384        }
    385      });
    386  }
    387 
    388  unhighlightAccessible(accessibleFront) {
    389    if (!accessibleFront) {
    390      return;
    391    }
    392 
    393    const accessibleWalkerFront = accessibleFront.getParent();
    394    if (!accessibleWalkerFront) {
    395      return;
    396    }
    397 
    398    accessibleWalkerFront.unhighlight().catch(error => {
    399      // Only report an error where there's still a commands instance.
    400      // Ignore cases where toolbox is already destroyed.
    401      if (this.commands) {
    402        console.error(error);
    403      }
    404    });
    405  }
    406 
    407  async initialize() {
    408    // Initialize it first as it may be used on target selection when calling watchTargets
    409    this.parentAccessibilityFront =
    410      await this.commands.targetCommand.rootFront.getFront(
    411        "parentaccessibility"
    412      );
    413 
    414    await this.commands.targetCommand.watchTargets({
    415      types: [this.commands.targetCommand.TYPES.FRAME],
    416      onAvailable: this.onTargetAvailable,
    417      onSelected: this.onTargetSelected,
    418      onDestroyed: this.onTargetDestroyed,
    419    });
    420 
    421    // Enable accessibility service if necessary.
    422    if (this.canBeEnabled && !this.enabled) {
    423      await this.enableAccessibility();
    424    }
    425    this.#initialized = true;
    426  }
    427 
    428  get supports() {
    429    // Retrieve backward compatibility traits.
    430    // New API's must be described in the "getTraits" method of the AccessibilityActor.
    431    return this.accessibilityFront.traits;
    432  }
    433 
    434  destroy() {
    435    this.commands.targetCommand.unwatchTargets({
    436      types: [this.commands.targetCommand.TYPES.FRAME],
    437      onAvailable: this.onTargetAvailable,
    438      onSelected: this.onTargetSelected,
    439      onDestroyed: this.onTargetDestroyed,
    440    });
    441 
    442    this.lifecycleEvents.clear();
    443    this.accessibilityEvents.clear();
    444 
    445    this.accessibilityFront = null;
    446    this.parentAccessibilityFront = null;
    447    this.simulatorFront = null;
    448    this.simulate = null;
    449    this.commands = null;
    450  }
    451 
    452  _getEvents(front) {
    453    return front.typeName === "accessiblewalker"
    454      ? this.accessibilityEvents
    455      : this.lifecycleEvents;
    456  }
    457 
    458  registerEvent(front, type, listener) {
    459    const events = this._getEvents(front);
    460    if (events.has(type)) {
    461      events.get(type).add(listener);
    462    } else {
    463      events.set(type, new Set([listener]));
    464    }
    465  }
    466 
    467  unregisterEvent(front, type, listener) {
    468    const events = this._getEvents(front);
    469    if (!events.has(type)) {
    470      return;
    471    }
    472 
    473    if (!listener) {
    474      events.delete(type);
    475      return;
    476    }
    477 
    478    const listeners = events.get(type);
    479    if (listeners.has(listener)) {
    480      listeners.delete(listener);
    481    }
    482 
    483    if (!listeners.size) {
    484      events.delete(type);
    485    }
    486  }
    487 
    488  onAccessibilityFrontAvailable(accessibilityFront) {
    489    accessibilityFront.watchFronts(
    490      "accessiblewalker",
    491      this.onAccessibleWalkerFrontAvailable,
    492      this.onAccessibleWalkerFrontDestroyed
    493    );
    494  }
    495 
    496  onAccessibilityFrontDestroyed(accessibilityFront) {
    497    accessibilityFront.unwatchFronts(
    498      "accessiblewalker",
    499      this.onAccessibleWalkerFrontAvailable,
    500      this.onAccessibleWalkerFrontDestroyed
    501    );
    502  }
    503 
    504  onAccessibleWalkerFrontAvailable(accessibleWalkerFront) {
    505    this._accessibilityWalkerFronts.add(accessibleWalkerFront);
    506    // Apply all existing accessible walker front event listeners to the new
    507    // front.
    508    for (const [type, listeners] of this.accessibilityEvents.entries()) {
    509      for (const listener of listeners) {
    510        accessibleWalkerFront.on(type, listener);
    511      }
    512    }
    513  }
    514 
    515  onAccessibleWalkerFrontDestroyed(accessibleWalkerFront) {
    516    this._accessibilityWalkerFronts.delete(accessibleWalkerFront);
    517    // Remove all existing accessible walker front event listeners from the
    518    // destroyed front.
    519    for (const [type, listeners] of this.accessibilityEvents.entries()) {
    520      for (const listener of listeners) {
    521        accessibleWalkerFront.off(type, listener);
    522      }
    523    }
    524  }
    525 
    526  async onTargetAvailable({ targetFront }) {
    527    targetFront.watchFronts(
    528      "accessibility",
    529      this.onAccessibilityFrontAvailable,
    530      this.onAccessibilityFrontDestroyed
    531    );
    532 
    533    if (!targetFront.isTopLevel) {
    534      return;
    535    }
    536 
    537    // Clear all the fronts collected by `watchFronts` on the previous set of targets/documents.
    538    this._accessibilityWalkerFronts.clear();
    539  }
    540 
    541  async onTargetDestroyed({ targetFront }) {
    542    targetFront.unwatchFronts(
    543      "accessibility",
    544      this.onAccessibilityFrontAvailable,
    545      this.onAccessibilityFrontDestroyed
    546    );
    547  }
    548 
    549  async onTargetSelected({ targetFront }) {
    550    this.accessibilityFront = await targetFront.getFront("accessibility");
    551 
    552    this.simulatorFront = this.accessibilityFront.simulatorFront;
    553    if (this.simulatorFront) {
    554      this.simulate = types => this.simulatorFront.simulate({ types });
    555 
    556      // Re-apply a potential existing simulation
    557      const { simulation } = this.#panel.panelWin.view.store.getState();
    558      const simulationType = Object.keys(simulation).find(
    559        name => simulation[name]
    560      );
    561      if (simulationType) {
    562        await this.simulate([simulationType]);
    563      }
    564    } else {
    565      this.simulate = null;
    566    }
    567 
    568    await this.toggleDisplayTabbingOrder(false);
    569 
    570    // Move accessibility front lifecycle event listeners to a new top level
    571    // front.
    572    for (const [type, listeners] of this.lifecycleEvents.entries()) {
    573      for (const listener of listeners.values()) {
    574        this.accessibilityFront.on(type, listener);
    575      }
    576    }
    577 
    578    // Hold on refreshing the view on initialization.
    579    // This will be done by the Panel class after everything is setup.
    580    // (we especially need to wait for the a11y service to be started)
    581    if (this.#initialized) {
    582      await this.#panel.forceRefresh();
    583    }
    584  }
    585 }
    586 
    587 exports.AccessibilityProxy = AccessibilityProxy;