tor-browser

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

remote-context-helper.js (27044B)


      1 'use strict';
      2 
      3 // Requires:
      4 // - /common/dispatcher/dispatcher.js
      5 // - /common/utils.js
      6 // - /common/get-host-info.sub.js if automagic conversion of origin names to
      7 // URLs is used.
      8 
      9 /**
     10 * This provides a more friendly interface to remote contexts in dispatches.js.
     11 * The goal is to make it easy to write multi-window/-frame/-worker tests where
     12 * the logic is entirely in 1 test file and there is no need to check in any
     13 * other file (although it is often helpful to check in files of JS helper
     14 * functions that are shared across remote context).
     15 *
     16 * So for example, to test that history traversal works, we create a new window,
     17 * navigate it to a new document, go back and then go forward.
     18 *
     19 * @example
     20 * promise_test(async t => {
     21 *   const rcHelper = new RemoteContextHelper();
     22 *   const rc1 = await rcHelper.addWindow();
     23 *   const rc2 = await rc1.navigateToNew();
     24 *   assert_equals(await rc2.executeScript(() => 'here'), 'here', 'rc2 is live');
     25 *   rc2.historyBack();
     26 *   assert_equals(await rc1.executeScript(() => 'here'), 'here', 'rc1 is live');
     27 *   rc1.historyForward();
     28 *   assert_equals(await rc2.executeScript(() => 'here'), 'here', 'rc2 is live');
     29 * });
     30 *
     31 * Note on the correspondence between remote contexts and
     32 * `RemoteContextWrapper`s. A remote context is entirely determined by its URL.
     33 * So navigating away from one and then back again will result in a remote
     34 * context that can be controlled by the same `RemoteContextWrapper` instance
     35 * before and after navigation. Messages sent to a remote context while it is
     36 * destroyed or in BFCache will be queued and processed if that that URL is
     37 * navigated back to.
     38 *
     39 * Navigation:
     40 * This framework does not keep track of the history of the frame tree and so it
     41 * is up to the test script to keep track of what remote contexts are currently
     42 * active and to keep references to the corresponding `RemoteContextWrapper`s.
     43 *
     44 * Any action that leads to navigation in the remote context must be executed
     45 * using
     46 * @see RemoteContextWrapper.navigate.
     47 */
     48 
     49 {
     50  const RESOURCES_PATH =
     51      '/html/browsers/browsing-the-web/remote-context-helper/resources';
     52  const WINDOW_EXECUTOR_PATH = `${RESOURCES_PATH}/executor-window.py`;
     53  const WORKER_EXECUTOR_PATH = `${RESOURCES_PATH}/executor-worker.js`;
     54 
     55  /**
     56   * Turns a string into an origin. If `origin` is null this will return the
     57   * current document's origin. If `origin` contains not '/', this will attempt
     58   * to use it as an index in `get_host_info()`. Otherwise returns the input
     59   * origin.
     60   * @private
     61   * @param {string|null} origin The input origin.
     62   * @return {string|null} The output origin.
     63   * @throws {RangeError} is `origin` cannot be found in
     64   *     `get_host_info()`.
     65   */
     66  function finalizeOrigin(origin) {
     67    if (!origin) {
     68      return location.origin;
     69    }
     70    if (!origin.includes('/')) {
     71      const origins = get_host_info();
     72      if (origin in origins) {
     73        return origins[origin];
     74      } else {
     75        throw new RangeError(
     76            `${origin} is not a key in the get_host_info() object`);
     77      }
     78    }
     79    return origin;
     80  }
     81 
     82  /**
     83   * @private
     84   * @param {string} url
     85   * @returns {string} Absolute url using `location` as the base.
     86   */
     87  function makeAbsolute(url) {
     88    return new URL(url, location).toString();
     89  }
     90 
     91  async function fetchText(url) {
     92    return fetch(url).then(r => r.text());
     93  }
     94 
     95  /**
     96   * Represents a configuration for a remote context executor.
     97   */
     98  class RemoteContextConfig {
     99    /**
    100     * @param {Object} [options]
    101     * @param {string} [options.origin] A URL or a key in `get_host_info()`.
    102     *                 @see finalizeOrigin for how origins are handled.
    103     * @param {string[]} [options.scripts]  A list of script URLs. The current
    104     *     document will be used as the base for relative URLs.
    105     * @param {[string, string][]} [options.headers]  A list of pairs of name
    106     *     and value. The executor will be served with these headers set.
    107     * @param {string} [options.startOn] If supplied, the executor will start
    108     *     when this event occurs, e.g. "pageshow",
    109     *     (@see window.addEventListener). This only makes sense for
    110     *     window-based executors, not worker-based.
    111     * @param {string} [options.status] If supplied, the executor will pass
    112     *     this value in the "status" parameter to the executor. The default
    113     *     executor will default to a status code of 200, if the parameter is
    114     *     not supplied.
    115     * @param {string} [options.urlType] Determines what kind of URL is used. Options:
    116     *     'origin', the URL will be based on the origin;
    117     *      'data' or 'blob', the URL will contains the document content in
    118     *      a 'data:' or 'blob:' URL; 'blank', the URL will be blank and the
    119     *     document content will be written to the initial empty document using
    120     *     `document.open()`, `document.write()`, and `document.close()`. If not
    121     *     supplied, the default is 'origin'.
    122     */
    123    constructor(
    124        {origin, scripts = [], headers = [], startOn, status, urlType} = {}) {
    125      this.origin = origin;
    126      this.scripts = scripts;
    127      this.headers = headers;
    128      this.startOn = startOn;
    129      this.status = status;
    130      this.urlType = urlType;
    131    }
    132 
    133    /**
    134     * If `config` is not already a `RemoteContextConfig`, one is constructed
    135     * using `config`.
    136     * @private
    137     * @param {object} [config]
    138     * @returns
    139     */
    140    static ensure(config) {
    141      if (!config) {
    142        return DEFAULT_CONTEXT_CONFIG;
    143      }
    144      return new RemoteContextConfig(config);
    145    }
    146 
    147    /**
    148     * Merges `this` with another `RemoteContextConfig` and to give a new
    149     * `RemoteContextConfig`. `origin` is replaced by the other if present,
    150     * `headers` and `scripts` are concatenated with `this`'s coming first.
    151     * @param {RemoteContextConfig} extraConfig
    152     * @returns {RemoteContextConfig}
    153     */
    154    merged(extraConfig) {
    155      let origin = this.origin;
    156      if (extraConfig.origin) {
    157        origin = extraConfig.origin;
    158      }
    159      let startOn = this.startOn;
    160      if (extraConfig.startOn) {
    161        startOn = extraConfig.startOn;
    162      }
    163      let status = this.status;
    164      if (extraConfig.status) {
    165        status = extraConfig.status;
    166      }
    167      let urlType = this.urlType;
    168      if (extraConfig.urlType) {
    169        urlType = extraConfig.urlType;
    170      }
    171      const headers = this.headers.concat(extraConfig.headers);
    172      const scripts = this.scripts.concat(extraConfig.scripts);
    173      return new RemoteContextConfig(
    174          {origin, headers, scripts, startOn, status, urlType});
    175    }
    176 
    177    /**
    178     * Creates a URL for an executor based on this config.
    179     * @param {string} uuid The unique ID of the executor.
    180     * @param {boolean} isWorker If true, the executor will be Worker. If false,
    181     * it will be a HTML document.
    182     * @returns {string|Blob|undefined}
    183     */
    184    async createExecutorUrl(uuid, isWorker) {
    185      const origin = finalizeOrigin(this.origin);
    186      const url = new URL(
    187          isWorker ? WORKER_EXECUTOR_PATH : WINDOW_EXECUTOR_PATH, origin);
    188 
    189      // UUID is needed for executor.
    190      url.searchParams.append('uuid', uuid);
    191 
    192      if (this.headers) {
    193        addHeaders(url, this.headers);
    194      }
    195      for (const script of this.scripts) {
    196        url.searchParams.append('script', makeAbsolute(script));
    197      }
    198 
    199      if (this.startOn) {
    200        url.searchParams.append('startOn', this.startOn);
    201      }
    202 
    203      if (this.status) {
    204        url.searchParams.append('status', this.status);
    205      }
    206 
    207      const urlType = this.urlType || 'origin';
    208      switch (urlType) {
    209        case 'origin':
    210        case 'blank':
    211          return url.href;
    212        case 'data':
    213          return `data:text/html;base64,${btoa(await fetchText(url.href))}`;
    214        case 'blob':
    215          return URL.createObjectURL(
    216              new Blob([await fetchText(url.href)], {type: 'text/html'}));
    217        default:
    218          throw TypeError(`Invalid urlType: ${urlType}`);
    219      };
    220    }
    221  }
    222 
    223  /**
    224   * The default `RemoteContextConfig` to use if none is supplied. It has no
    225   * origin, headers or scripts.
    226   * @constant {RemoteContextConfig}
    227   */
    228  const DEFAULT_CONTEXT_CONFIG = new RemoteContextConfig();
    229 
    230  /**
    231   * Attaches header to the URL. See
    232   * https://web-platform-tests.org/writing-tests/server-pipes.html#headers
    233   * @param {string} url the URL to which headers should be attached.
    234   * @param {[[string, string]]} headers a list of pairs of head-name,
    235   *     header-value.
    236   */
    237  function addHeaders(url, headers) {
    238    function escape(s) {
    239      return s.replace('(', '\\(').replace(')', '\\)').replace(',', '\\,');
    240    }
    241    const formattedHeaders = headers.map((header) => {
    242      return `header(${escape(header[0])}, ${escape(header[1])})`;
    243    });
    244    url.searchParams.append('pipe', formattedHeaders.join('|'));
    245  }
    246 
    247  function windowExecutorCreator(
    248    { target = '_blank', features } = {}, remoteContextWrapper) {
    249    let openWindow = (url, target, features, documentContent) => {
    250      const w = window.open(url, target, features);
    251      if (documentContent) {
    252        w.document.open();
    253        w.document.write(documentContent);
    254        w.document.close();
    255      }
    256    };
    257 
    258    return (url, documentContent) => {
    259      if (url && url.substring(0, 5) == 'data:') {
    260        throw new TypeError('Windows cannot use data: URLs.');
    261      }
    262 
    263      if (remoteContextWrapper) {
    264        return remoteContextWrapper.executeScript(
    265          openWindow, [url, target, features, documentContent]);
    266      } else {
    267        openWindow(url, target, features, documentContent);
    268      }
    269    };
    270  }
    271 
    272  function elementExecutorCreator(
    273      remoteContextWrapper, elementName, attributes) {
    274    return (url, documentContent) => {
    275      return remoteContextWrapper.executeScript(
    276          (url, elementName, attributes, documentContent) => {
    277            const el = document.createElement(elementName);
    278            for (const attribute in attributes) {
    279              el.setAttribute(attribute, attributes[attribute]);
    280            }
    281            if (url) {
    282              if (elementName == 'object') {
    283                el.data = url;
    284              } else {
    285                el.src = url;
    286              }
    287            }
    288            const parent =
    289                elementName == 'frame' ? findOrCreateFrameset() : document.body;
    290            parent.appendChild(el);
    291            if (documentContent) {
    292              el.contentDocument.open();
    293              el.contentDocument.write(documentContent);
    294              el.contentDocument.close();
    295            }
    296          },
    297          [url, elementName, attributes, documentContent]);
    298    };
    299  }
    300 
    301  function iframeSrcdocExecutorCreator(remoteContextWrapper, attributes) {
    302    return async (url) => {
    303      // `url` points to the content needed to run an `Executor` in the frame.
    304      // So we download the content and pass it via the `srcdoc` attribute,
    305      // setting the iframe's `src` to `undefined`.
    306      attributes['srcdoc'] = await fetchText(url);
    307 
    308      elementExecutorCreator(
    309          remoteContextWrapper, 'iframe', attributes)(undefined);
    310    };
    311  }
    312 
    313  function workerExecutorCreator(remoteContextWrapper, globalVariable) {
    314    return url => {
    315      return remoteContextWrapper.executeScript((url, globalVariable) => {
    316        const worker = new Worker(url);
    317        if (globalVariable) {
    318          window[globalVariable] = worker;
    319        }
    320      }, [url, globalVariable]);
    321    };
    322  }
    323 
    324  function navigateExecutorCreator(remoteContextWrapper) {
    325    return url => {
    326      return remoteContextWrapper.navigate((url) => {
    327        window.location = url;
    328      }, [url]);
    329    };
    330  }
    331 
    332  /**
    333   * This class represents a remote context running an executor (a
    334   * window/frame/worker that can receive commands). It is the interface for
    335   * scripts to control remote contexts.
    336   *
    337   * Instances are returned when new remote contexts are created (e.g.
    338   * `addFrame` or `navigateToNew`).
    339   */
    340  class RemoteContextWrapper {
    341    /**
    342     * This should only be constructed by `RemoteContextHelper`.
    343     * @private
    344     */
    345    constructor(context, helper, url) {
    346      this.context = context;
    347      this.helper = helper;
    348      this.url = url;
    349    }
    350 
    351    /**
    352     * Executes a script in the remote context.
    353     * @param {function} fn The script to execute.
    354     * @param {any[]} args An array of arguments to pass to the script.
    355     * @returns {Promise<any>} The return value of the script (after
    356     *     being serialized and deserialized).
    357     */
    358    async executeScript(fn, args) {
    359      return this.context.execute_script(fn, args);
    360    }
    361 
    362    /**
    363     * Adds a string of HTML to the executor's document.
    364     * @param {string} html
    365     * @returns {Promise<undefined>}
    366     */
    367    async addHTML(html) {
    368      return this.executeScript((htmlSource) => {
    369        document.body.insertAdjacentHTML('beforebegin', htmlSource);
    370      }, [html]);
    371    }
    372 
    373    /**
    374     * Adds scripts to the executor's document.
    375     * @param {string[]} urls A list of URLs. URLs are relative to the current
    376     *     document.
    377     * @returns {Promise<undefined>}
    378     */
    379    async addScripts(urls) {
    380      if (!urls) {
    381        return [];
    382      }
    383      return this.executeScript(urls => {
    384        return addScripts(urls);
    385      }, [urls.map(makeAbsolute)]);
    386    }
    387 
    388    /**
    389     * Adds an `iframe` with `src` attribute to the current document.
    390     * @param {RemoteContextConfig} [extraConfig]
    391     * @param {[string, string][]} [attributes] A list of pairs of strings
    392     *     of attribute name and value these will be set on the iframe element
    393     *     when added to the document.
    394     * @returns {Promise<RemoteContextWrapper>} The remote context.
    395     */
    396    addIframe(extraConfig, attributes = {}) {
    397      return this.helper.createContext({
    398        executorCreator: elementExecutorCreator(this, 'iframe', attributes),
    399        extraConfig,
    400      });
    401    }
    402 
    403    /**
    404     * Adds a `frame` with `src` attribute to the current document's first
    405     * `frameset` element.
    406     * @param {RemoteContextConfig} [extraConfig]
    407     * @param {[string, string][]} [attributes] A list of pairs of strings
    408     *     of attribute name and value these will be set on the frame element
    409     *     when added to the document.
    410     * @returns {Promise<RemoteContextWrapper>} The remote context.
    411     */
    412    addFrame(extraConfig, attributes = {}) {
    413      return this.helper.createContext({
    414        executorCreator: elementExecutorCreator(this, 'frame', attributes),
    415        extraConfig,
    416      });
    417    }
    418 
    419    /**
    420     * Adds an `embed` with `src` attribute to the current document.
    421     * @param {RemoteContextConfig} [extraConfig]
    422     * @param {[string, string][]} [attributes] A list of pairs of strings
    423     *     of attribute name and value these will be set on the embed element
    424     *     when added to the document.
    425     * @returns {Promise<RemoteContextWrapper>} The remote context.
    426     */
    427    addEmbed(extraConfig, attributes = {}) {
    428      return this.helper.createContext({
    429        executorCreator: elementExecutorCreator(this, 'embed', attributes),
    430        extraConfig,
    431      });
    432    }
    433 
    434    /**
    435     * Adds an `object` with `data` attribute to the current document.
    436     * @param {RemoteContextConfig} [extraConfig]
    437     * @param {[string, string][]} [attributes] A list of pairs of strings
    438     *     of attribute name and value these will be set on the object element
    439     *     when added to the document.
    440     * @returns {Promise<RemoteContextWrapper>} The remote context.
    441     */
    442    addObject(extraConfig, attributes = {}) {
    443      return this.helper.createContext({
    444        executorCreator: elementExecutorCreator(this, 'object', attributes),
    445        extraConfig,
    446      });
    447    }
    448 
    449    /**
    450     * Adds an iframe with `srcdoc` attribute to the current document
    451     * @param {RemoteContextConfig} [extraConfig]
    452     * @param {[string, string][]} [attributes] A list of pairs of strings
    453     *     of attribute name and value these will be set on the iframe element
    454     *     when added to the document.
    455     * @returns {Promise<RemoteContextWrapper>} The remote context.
    456     */
    457    addIframeSrcdoc(extraConfig, attributes = {}) {
    458      return this.helper.createContext({
    459        executorCreator: iframeSrcdocExecutorCreator(this, attributes),
    460        extraConfig,
    461      });
    462    }
    463 
    464    /**
    465     * Opens a window from the remote context. @see createContext for
    466     * @param {RemoteContextConfig|object} [extraConfig]
    467     * @param {Object} [options]
    468     * @param {string} [options.target] Passed to `window.open` as the
    469     *     2nd argument
    470     * @param {string} [options.features] Passed to `window.open` as the
    471     *     3rd argument
    472     * @returns {Promise<RemoteContextWrapper>} The remote context.
    473     */
    474    addWindow(extraConfig, options) {
    475      return this.helper.createContext({
    476        executorCreator: windowExecutorCreator(options, this),
    477        extraConfig,
    478      });
    479    }
    480 
    481    /**
    482     * Adds a dedicated worker to the current document.
    483     * @param {string|null} [globalVariable] The name of the global variable to
    484     *   which to assign the `Worker` object after construction. If `null`,
    485     *   then no assignment will take place.
    486     * @param {RemoteContextConfig} [extraConfig]
    487     * @returns {Promise<RemoteContextWrapper>} The remote context.
    488     */
    489    addWorker(globalVariable, extraConfig) {
    490      return this.helper.createContext({
    491        executorCreator: workerExecutorCreator(this, globalVariable),
    492        extraConfig,
    493        isWorker: true,
    494      });
    495    }
    496 
    497    /**
    498     * Gets a `Headers` object containing the request headers that were used
    499     * when the browser requested this document.
    500     *
    501     * Currently, this only works for `RemoteContextHelper`s representing
    502     * windows, not workers.
    503     * @returns {Promise<Headers>}
    504     */
    505    async getRequestHeaders() {
    506      // This only works in window environments for now. We could make it work
    507      // for workers too; if you have a need, just share or duplicate the code
    508      // that's in executor-window.py. Anyway, we explicitly use `window` in
    509      // the script so that we get a clear error if you try using it on a
    510      // worker.
    511 
    512      // We need to serialize and deserialize the `Headers` object manually.
    513      const asNestedArrays = await this.executeScript(() => [...window.__requestHeaders]);
    514      return new Headers(asNestedArrays);
    515    }
    516 
    517    /**
    518     * Executes a script in the remote context that will perform a navigation.
    519     * To do this safely, we must suspend the executor and wait for that to
    520     * complete before executing. This ensures that all outstanding requests are
    521     * completed and no more can start. It also ensures that the executor will
    522     * restart if the page goes into BFCache or it was a same-document
    523     * navigation. It does not return a value.
    524     *
    525     * NOTE: We cannot monitor whether and what navigations are happening. The
    526     * logic has been made as robust as possible but is not fool-proof.
    527     *
    528     * Foolproof rule:
    529     * - The script must perform exactly one navigation.
    530     * - If that navigation is a same-document history traversal, you must
    531     * `await` the result of `waitUntilLocationIs`. (Same-document non-traversal
    532     * navigations do not need this extra step.)
    533     *
    534     * More complex rules:
    535     * - The script must perform a navigation. If it performs no navigation,
    536     *   the remote context will be left in the suspended state.
    537     * - If the script performs a direct same-document navigation, it is not
    538     * necessary to use this function but it will work as long as it is the only
    539     *   navigation performed.
    540     * - If the script performs a same-document history navigation, you must
    541     * `await` the result of `waitUntilLocationIs`.
    542     *
    543     * @param {function} fn The script to execute.
    544     * @param {any[]} args An array of arguments to pass to the script.
    545     * @returns {Promise<undefined>}
    546     */
    547    navigate(fn, args) {
    548      return this.executeScript((fnText, args) => {
    549        executeScriptToNavigate(fnText, args);
    550      }, [fn.toString(), args]);
    551    }
    552 
    553    /**
    554     * Navigates to the given URL, by executing a script in the remote
    555     * context that will perform navigation with the `location.href`
    556     * setter.
    557     *
    558     * Be aware that performing a cross-document navigation using this
    559     * method will cause this `RemoteContextWrapper` to become dormant,
    560     * since the remote context it points to is no longer active and
    561     * able to receive messages. You also won't be able to reliably
    562     * tell when the navigation finishes; the returned promise will
    563     * fulfill when the script finishes running, not when the navigation
    564     * is done. As such, this is most useful for testing things like
    565     * unload behavior (where it doesn't matter) or prerendering (where
    566     * there is already a `RemoteContextWrapper` for the destination).
    567     * For other cases, using `navigateToNew()` will likely be better.
    568     *
    569     * @param {string|URL} url The URL to navigate to.
    570     * @returns {Promise<undefined>}
    571     */
    572    navigateTo(url) {
    573      return this.navigate(url => {
    574        location.href = url;
    575      }, [url.toString()]);
    576    }
    577 
    578    /**
    579     * Navigates the context to a new document running an executor.
    580     * @param {RemoteContextConfig} [extraConfig]
    581     * @returns {Promise<RemoteContextWrapper>} The remote context.
    582     */
    583    async navigateToNew(extraConfig) {
    584      return this.helper.createContext({
    585        executorCreator: navigateExecutorCreator(this),
    586        extraConfig,
    587      });
    588    }
    589 
    590    //////////////////////////////////////
    591    // Navigation Helpers.
    592    //
    593    // It is up to the test script to know which remote context will be
    594    // navigated to and which `RemoteContextWrapper` should be used after
    595    // navigation.
    596    //
    597    // NOTE: For a same-document history navigation, the caller use `await` a
    598    // call to `waitUntilLocationIs` in order to know that the navigation has
    599    // completed. For convenience the method below can return the promise to
    600    // wait on, if passed the expected location.
    601 
    602    async waitUntilLocationIs(expectedLocation) {
    603      return this.executeScript(async (expectedLocation) => {
    604        if (location.href === expectedLocation) {
    605          return;
    606        }
    607 
    608        // Wait until the location updates to the expected one.
    609        await new Promise(resolve => {
    610          const listener = addEventListener('hashchange', (event) => {
    611            if (event.newURL === expectedLocation) {
    612              removeEventListener(listener);
    613              resolve();
    614            }
    615          });
    616        });
    617      }, [expectedLocation]);
    618    }
    619 
    620    /**
    621     * Performs a history traversal.
    622     * @param {integer} n How many steps to traverse. @see history.go
    623     * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs.
    624     * @returns {Promise<undefined>}
    625     */
    626    async historyGo(n, expectedLocation) {
    627      await this.navigate((n) => {
    628        history.go(n);
    629      }, [n]);
    630      if (expectedLocation) {
    631        await this.waitUntilLocationIs(expectedLocation);
    632      }
    633    }
    634 
    635    /**
    636     * Performs a history traversal back.
    637     * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs.
    638     * @returns {Promise<undefined>}
    639     */
    640    async historyBack(expectedLocation) {
    641      await this.navigate(() => {
    642        history.back();
    643      });
    644      if (expectedLocation) {
    645        await this.waitUntilLocationIs(expectedLocation);
    646      }
    647    }
    648 
    649    /**
    650     * Performs a history traversal back.
    651     * @param {string} [expectedLocation] If supplied will be passed to @see waitUntilLocationIs.
    652     * @returns {Promise<undefined>}
    653     */
    654    async historyForward(expectedLocation) {
    655      await this.navigate(() => {
    656        history.forward();
    657      });
    658      if (expectedLocation) {
    659        await this.waitUntilLocationIs(expectedLocation);
    660      }
    661    }
    662  }
    663 
    664 
    665  /**
    666   * This class represents a configuration for creating remote contexts. This is
    667   * the entry-point
    668   * for creating remote contexts, providing @see addWindow .
    669   */
    670  class RemoteContextHelper {
    671    /**
    672     * The constructor to use when creating new remote context wrappers.
    673     * Can be overridden by subclasses.
    674     */
    675    static RemoteContextWrapper = RemoteContextWrapper;
    676 
    677    /**
    678     * @param {RemoteContextConfig|object} config The configuration
    679     *     for this remote context.
    680     */
    681    constructor(config) {
    682      this.config = RemoteContextConfig.ensure(config);
    683    }
    684 
    685    /**
    686     * Creates a new remote context and returns a `RemoteContextWrapper` giving
    687     * access to it.
    688     * @private
    689     * @param {Object} options
    690     * @param {(url: string) => Promise<undefined>} [options.executorCreator] A
    691     *     function that takes a URL and causes the browser to navigate some
    692     *     window to that URL, e.g. via an iframe or a new window. If this is
    693     *     not supplied, then the returned RemoteContextWrapper won't actually
    694     *     be communicating with something yet, and something will need to
    695     *     navigate to it using its `url` property, before communication can be
    696     *     established.
    697     * @param {RemoteContextConfig|object} [options.extraConfig] If supplied,
    698     *     extra configuration for this remote context to be merged with
    699     *     `this`'s existing config. If it's not a `RemoteContextConfig`, it
    700     *     will be used to construct a new one.
    701     * @returns {Promise<RemoteContextWrapper>}
    702     */
    703    async createContext({
    704      executorCreator,
    705      extraConfig,
    706      isWorker = false,
    707    }) {
    708      const config =
    709        this.config.merged(RemoteContextConfig.ensure(extraConfig));
    710 
    711      // UUID is needed for executor.
    712      const uuid = token();
    713      const url = await config.createExecutorUrl(uuid, isWorker);
    714 
    715      if (executorCreator) {
    716        if (config.urlType == 'blank') {
    717          await executorCreator(undefined, await fetchText(url));
    718        } else {
    719          await executorCreator(url, undefined);
    720        }
    721      }
    722 
    723      return new this.constructor.RemoteContextWrapper(new RemoteContext(uuid), this, url);
    724    }
    725 
    726    /**
    727     * Creates a window with a remote context. @see createContext for
    728     * @param {RemoteContextConfig|object} [extraConfig] Will be
    729     *     merged with `this`'s config.
    730     * @param {Object} [options]
    731     * @param {string} [options.target] Passed to `window.open` as the
    732     *     2nd argument
    733     * @param {string} [options.features] Passed to `window.open` as the
    734     *     3rd argument
    735     * @returns {Promise<RemoteContextWrapper>}
    736     */
    737    addWindow(extraConfig, options) {
    738      return this.createContext({
    739        executorCreator: windowExecutorCreator(options),
    740        extraConfig,
    741      });
    742    }
    743  }
    744  // Export this class.
    745  self.RemoteContextHelper = RemoteContextHelper;
    746 }