chromium/chrome/browser/resources/chromeos/accessibility/chromevox/background/injected_script_loader.ts

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Responsible for loading scripts into the inject context.
 */

export class InjectedScriptLoader {

  private code_: { [key: string]: string } = {};
  private constructor() { }

  static injectContentScriptForGoogleDocs(): void {
    // Build a regexp to match all allowed urls.
    let matches = [];
    try {
      matches = chrome.runtime.getManifest()['content_scripts'][0]['matches'];
    } catch (e) {
      throw new Error(
        'Unable to find content script matches entry in manifest.');
    }

    // Build one large regexp.
    const docsRe = new RegExp(matches.join('|'));

    // Inject the content script into all running tabs allowed by the
    // manifest. This block is still necessary because the extension system
    // doesn't re-inject content scripts into already running tabs.
    chrome.windows.getAll({ 'populate': true }, (windows: chrome.windows.Window[]) => {
      for (let i = 0; i < windows.length; i++) {
        // TODO(b/314203187): Determine if not null assertion is acceptable.
        const tabs = windows[i].tabs!.filter(tab => docsRe.test(tab.url!));
        InjectedScriptLoader.injectContentScript_(tabs);
      }
    });
  }

  /**
   * Loads a dictionary of file contents for Javascript files.
   * @param files A list of file names.
   * @private
   */
  private async fetchCode_(files: string[]): Promise<void> {
    Promise.all(files.map(file => this.loadScriptAsCode_(file)));
  }

  private async loadScriptAsCode_(fileName: string): Promise<void> {
    if (this.code_[fileName]) {
      return;
    }

    // Load the script by fetching its source and running 'eval' on it
    // directly, with a magic comment that makes Chrome treat it like it
    // loaded normally. Wait until it's fetched before loading the
    // next script.
    const url = chrome.extension.getURL(fileName) + '?' + new Date().getTime();
    const response = await fetch(url);
    if (response.ok) {
      let scriptText = await response.text();
      // Add a magic comment to the bottom of the file so that
      // Chrome knows the name of the script in the JavaScript debugger.
      const debugSrc = fileName.replace('closure/../', '');
      // The 'chromevox' id is only used in the DevTools instead of a long
      // extension id.
      scriptText +=
        '\n//# sourceURL= chrome-extension://chromevox/' + debugSrc + '\n';
      this.code_[fileName] = scriptText;
    } else {
      // Cause the promise created by this async function to reject.
      throw new Error(`${response.status}: ${response.statusText}`);
    }
  }

  private async executeCodeInAllTabs_(tabs: chrome.tabs.Tab[]): Promise<void> {
    for (const tab of tabs) {
      // Inject the ChromeVox content script code into the tab.
      await Promise.all(
        Object.values(this.code_).map(script => this.execute_(script, tab)));
    }
  }

  /**
   * Inject the content scripts into already existing tabs.
   * @param tabs The tabs where ChromeVox scripts should be
   *     injected.
   * @private
   */
  private static async injectContentScript_(tabs: chrome.tabs.Tab[]): Promise<void> {
    if (!InjectedScriptLoader.instance) {
      InjectedScriptLoader.instance = new InjectedScriptLoader();
    }
    await InjectedScriptLoader.instance.fetchCode_(contentScriptFiles);
    await InjectedScriptLoader.instance.executeCodeInAllTabs_(tabs);
  }

  private async execute_(code: string, tab: chrome.tabs.Tab): Promise<void> {
    await new Promise(
      resolve => chrome.tabs.executeScript(
        tab.id, { code, allFrames: true }, resolve));
    if (chrome.runtime.lastError) {
      console.error('Could not inject into tab', tab);
    }
  }
}

export namespace InjectedScriptLoader {
  export let instance: InjectedScriptLoader;
}

// Local to module.

const contentScriptFiles =
  chrome.runtime.getManifest()['content_scripts'][0]['js'];