tor-browser

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

ExecutionContext.ts (16542B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {Protocol} from 'devtools-protocol';
      8 
      9 import {CDPSessionEvent, type CDPSession} from '../api/CDPSession.js';
     10 import type {ElementHandle} from '../api/ElementHandle.js';
     11 import type {JSHandle} from '../api/JSHandle.js';
     12 import {EventEmitter} from '../common/EventEmitter.js';
     13 import {LazyArg} from '../common/LazyArg.js';
     14 import {scriptInjector} from '../common/ScriptInjector.js';
     15 import type {BindingPayload, EvaluateFunc, HandleFor} from '../common/types.js';
     16 import {
     17  PuppeteerURL,
     18  SOURCE_URL_REGEX,
     19  debugError,
     20  getSourcePuppeteerURLIfAvailable,
     21  getSourceUrlComment,
     22  isString,
     23 } from '../common/util.js';
     24 import type PuppeteerUtil from '../injected/injected.js';
     25 import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
     26 import {DisposableStack, disposeSymbol} from '../util/disposable.js';
     27 import {stringifyFunction} from '../util/Function.js';
     28 import {Mutex} from '../util/Mutex.js';
     29 
     30 import {ARIAQueryHandler} from './AriaQueryHandler.js';
     31 import {Binding} from './Binding.js';
     32 import {CdpElementHandle} from './ElementHandle.js';
     33 import type {IsolatedWorld} from './IsolatedWorld.js';
     34 import {CdpJSHandle} from './JSHandle.js';
     35 import {
     36  addPageBinding,
     37  CDP_BINDING_PREFIX,
     38  createEvaluationError,
     39  valueFromRemoteObject,
     40 } from './utils.js';
     41 
     42 const ariaQuerySelectorBinding = new Binding(
     43  '__ariaQuerySelector',
     44  ARIAQueryHandler.queryOne as (...args: unknown[]) => unknown,
     45  '', // custom init
     46 );
     47 
     48 const ariaQuerySelectorAllBinding = new Binding(
     49  '__ariaQuerySelectorAll',
     50  (async (
     51    element: ElementHandle<Node>,
     52    selector: string,
     53  ): Promise<JSHandle<Node[]>> => {
     54    const results = ARIAQueryHandler.queryAll(element, selector);
     55    return await element.realm.evaluateHandle(
     56      (...elements) => {
     57        return elements;
     58      },
     59      ...(await AsyncIterableUtil.collect(results)),
     60    );
     61  }) as (...args: unknown[]) => unknown,
     62  '', // custom init
     63 );
     64 
     65 /**
     66 * @internal
     67 */
     68 export class ExecutionContext
     69  extends EventEmitter<{
     70    /** Emitted when this execution context is disposed. */
     71    disposed: undefined;
     72    consoleapicalled: Protocol.Runtime.ConsoleAPICalledEvent;
     73    /** Emitted when a binding that is not installed by the ExecutionContext is called. */
     74    bindingcalled: Protocol.Runtime.BindingCalledEvent;
     75  }>
     76  implements Disposable
     77 {
     78  #client: CDPSession;
     79  #world: IsolatedWorld;
     80  #id: number;
     81  #name?: string;
     82 
     83  readonly #disposables = new DisposableStack();
     84 
     85  constructor(
     86    client: CDPSession,
     87    contextPayload: Protocol.Runtime.ExecutionContextDescription,
     88    world: IsolatedWorld,
     89  ) {
     90    super();
     91    this.#client = client;
     92    this.#world = world;
     93    this.#id = contextPayload.id;
     94    if (contextPayload.name) {
     95      this.#name = contextPayload.name;
     96    }
     97    const clientEmitter = this.#disposables.use(new EventEmitter(this.#client));
     98    clientEmitter.on('Runtime.bindingCalled', this.#onBindingCalled.bind(this));
     99    clientEmitter.on('Runtime.executionContextDestroyed', async event => {
    100      if (event.executionContextId === this.#id) {
    101        this[disposeSymbol]();
    102      }
    103    });
    104    clientEmitter.on('Runtime.executionContextsCleared', async () => {
    105      this[disposeSymbol]();
    106    });
    107    clientEmitter.on('Runtime.consoleAPICalled', this.#onConsoleAPI.bind(this));
    108    clientEmitter.on(CDPSessionEvent.Disconnected, () => {
    109      this[disposeSymbol]();
    110    });
    111  }
    112 
    113  // Contains mapping from functions that should be bound to Puppeteer functions.
    114  #bindings = new Map<string, Binding>();
    115 
    116  // If multiple waitFor are set up asynchronously, we need to wait for the
    117  // first one to set up the binding in the page before running the others.
    118  #mutex = new Mutex();
    119  async #addBinding(binding: Binding): Promise<void> {
    120    if (this.#bindings.has(binding.name)) {
    121      return;
    122    }
    123 
    124    using _ = await this.#mutex.acquire();
    125    try {
    126      await this.#client.send(
    127        'Runtime.addBinding',
    128        this.#name
    129          ? {
    130              name: CDP_BINDING_PREFIX + binding.name,
    131              executionContextName: this.#name,
    132            }
    133          : {
    134              name: CDP_BINDING_PREFIX + binding.name,
    135              executionContextId: this.#id,
    136            },
    137      );
    138 
    139      await this.evaluate(
    140        addPageBinding,
    141        'internal',
    142        binding.name,
    143        CDP_BINDING_PREFIX,
    144      );
    145 
    146      this.#bindings.set(binding.name, binding);
    147    } catch (error) {
    148      // We could have tried to evaluate in a context which was already
    149      // destroyed. This happens, for example, if the page is navigated while
    150      // we are trying to add the binding
    151      if (error instanceof Error) {
    152        // Destroyed context.
    153        if (error.message.includes('Execution context was destroyed')) {
    154          return;
    155        }
    156        // Missing context.
    157        if (error.message.includes('Cannot find context with specified id')) {
    158          return;
    159        }
    160      }
    161 
    162      debugError(error);
    163    }
    164  }
    165 
    166  async #onBindingCalled(
    167    event: Protocol.Runtime.BindingCalledEvent,
    168  ): Promise<void> {
    169    if (event.executionContextId !== this.#id) {
    170      return;
    171    }
    172 
    173    let payload: BindingPayload;
    174    try {
    175      payload = JSON.parse(event.payload);
    176    } catch {
    177      // The binding was either called by something in the page or it was
    178      // called before our wrapper was initialized.
    179      return;
    180    }
    181    const {type, name, seq, args, isTrivial} = payload;
    182    if (type !== 'internal') {
    183      this.emit('bindingcalled', event);
    184      return;
    185    }
    186    if (!this.#bindings.has(name)) {
    187      this.emit('bindingcalled', event);
    188      return;
    189    }
    190 
    191    try {
    192      const binding = this.#bindings.get(name);
    193      await binding?.run(this, seq, args, isTrivial);
    194    } catch (err) {
    195      debugError(err);
    196    }
    197  }
    198 
    199  get id(): number {
    200    return this.#id;
    201  }
    202 
    203  #onConsoleAPI(event: Protocol.Runtime.ConsoleAPICalledEvent): void {
    204    if (event.executionContextId !== this.#id) {
    205      return;
    206    }
    207    this.emit('consoleapicalled', event);
    208  }
    209 
    210  #bindingsInstalled = false;
    211  #puppeteerUtil?: Promise<JSHandle<PuppeteerUtil>>;
    212  get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
    213    let promise = Promise.resolve() as Promise<unknown>;
    214    if (!this.#bindingsInstalled) {
    215      promise = Promise.all([
    216        this.#addBindingWithoutThrowing(ariaQuerySelectorBinding),
    217        this.#addBindingWithoutThrowing(ariaQuerySelectorAllBinding),
    218      ]);
    219      this.#bindingsInstalled = true;
    220    }
    221    scriptInjector.inject(script => {
    222      if (this.#puppeteerUtil) {
    223        void this.#puppeteerUtil.then(handle => {
    224          void handle.dispose();
    225        });
    226      }
    227      this.#puppeteerUtil = promise.then(() => {
    228        return this.evaluateHandle(script) as Promise<JSHandle<PuppeteerUtil>>;
    229      });
    230    }, !this.#puppeteerUtil);
    231    return this.#puppeteerUtil as Promise<JSHandle<PuppeteerUtil>>;
    232  }
    233 
    234  async #addBindingWithoutThrowing(binding: Binding) {
    235    try {
    236      await this.#addBinding(binding);
    237    } catch (err) {
    238      // If the binding cannot be added, the context is broken. We cannot
    239      // recover so we ignore the error.
    240      debugError(err);
    241    }
    242  }
    243 
    244  /**
    245   * Evaluates the given function.
    246   *
    247   * @example
    248   *
    249   * ```ts
    250   * const executionContext = await page.mainFrame().executionContext();
    251   * const result = await executionContext.evaluate(() => Promise.resolve(8 * 7))* ;
    252   * console.log(result); // prints "56"
    253   * ```
    254   *
    255   * @example
    256   * A string can also be passed in instead of a function:
    257   *
    258   * ```ts
    259   * console.log(await executionContext.evaluate('1 + 2')); // prints "3"
    260   * ```
    261   *
    262   * @example
    263   * Handles can also be passed as `args`. They resolve to their referenced object:
    264   *
    265   * ```ts
    266   * const oneHandle = await executionContext.evaluateHandle(() => 1);
    267   * const twoHandle = await executionContext.evaluateHandle(() => 2);
    268   * const result = await executionContext.evaluate(
    269   *   (a, b) => a + b,
    270   *   oneHandle,
    271   *   twoHandle,
    272   * );
    273   * await oneHandle.dispose();
    274   * await twoHandle.dispose();
    275   * console.log(result); // prints '3'.
    276   * ```
    277   *
    278   * @param pageFunction - The function to evaluate.
    279   * @param args - Additional arguments to pass into the function.
    280   * @returns The result of evaluating the function. If the result is an object,
    281   * a vanilla object containing the serializable properties of the result is
    282   * returned.
    283   */
    284  async evaluate<
    285    Params extends unknown[],
    286    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    287  >(
    288    pageFunction: Func | string,
    289    ...args: Params
    290  ): Promise<Awaited<ReturnType<Func>>> {
    291    return await this.#evaluate(true, pageFunction, ...args);
    292  }
    293 
    294  /**
    295   * Evaluates the given function.
    296   *
    297   * Unlike {@link ExecutionContext.evaluate | evaluate}, this method returns a
    298   * handle to the result of the function.
    299   *
    300   * This method may be better suited if the object cannot be serialized (e.g.
    301   * `Map`) and requires further manipulation.
    302   *
    303   * @example
    304   *
    305   * ```ts
    306   * const context = await page.mainFrame().executionContext();
    307   * const handle: JSHandle<typeof globalThis> = await context.evaluateHandle(
    308   *   () => Promise.resolve(self),
    309   * );
    310   * ```
    311   *
    312   * @example
    313   * A string can also be passed in instead of a function.
    314   *
    315   * ```ts
    316   * const handle: JSHandle<number> = await context.evaluateHandle('1 + 2');
    317   * ```
    318   *
    319   * @example
    320   * Handles can also be passed as `args`. They resolve to their referenced object:
    321   *
    322   * ```ts
    323   * const bodyHandle: ElementHandle<HTMLBodyElement> =
    324   *   await context.evaluateHandle(() => {
    325   *     return document.body;
    326   *   });
    327   * const stringHandle: JSHandle<string> = await context.evaluateHandle(
    328   *   body => body.innerHTML,
    329   *   body,
    330   * );
    331   * console.log(await stringHandle.jsonValue()); // prints body's innerHTML
    332   * // Always dispose your garbage! :)
    333   * await bodyHandle.dispose();
    334   * await stringHandle.dispose();
    335   * ```
    336   *
    337   * @param pageFunction - The function to evaluate.
    338   * @param args - Additional arguments to pass into the function.
    339   * @returns A {@link JSHandle | handle} to the result of evaluating the
    340   * function. If the result is a `Node`, then this will return an
    341   * {@link ElementHandle | element handle}.
    342   */
    343  async evaluateHandle<
    344    Params extends unknown[],
    345    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    346  >(
    347    pageFunction: Func | string,
    348    ...args: Params
    349  ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
    350    return await this.#evaluate(false, pageFunction, ...args);
    351  }
    352 
    353  async #evaluate<
    354    Params extends unknown[],
    355    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    356  >(
    357    returnByValue: true,
    358    pageFunction: Func | string,
    359    ...args: Params
    360  ): Promise<Awaited<ReturnType<Func>>>;
    361  async #evaluate<
    362    Params extends unknown[],
    363    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    364  >(
    365    returnByValue: false,
    366    pageFunction: Func | string,
    367    ...args: Params
    368  ): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
    369  async #evaluate<
    370    Params extends unknown[],
    371    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    372  >(
    373    returnByValue: boolean,
    374    pageFunction: Func | string,
    375    ...args: Params
    376  ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
    377    const sourceUrlComment = getSourceUrlComment(
    378      getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
    379        PuppeteerURL.INTERNAL_URL,
    380    );
    381 
    382    if (isString(pageFunction)) {
    383      const contextId = this.#id;
    384      const expression = pageFunction;
    385      const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
    386        ? expression
    387        : `${expression}\n${sourceUrlComment}\n`;
    388 
    389      const {exceptionDetails, result: remoteObject} = await this.#client
    390        .send('Runtime.evaluate', {
    391          expression: expressionWithSourceUrl,
    392          contextId,
    393          returnByValue,
    394          awaitPromise: true,
    395          userGesture: true,
    396        })
    397        .catch(rewriteError);
    398 
    399      if (exceptionDetails) {
    400        throw createEvaluationError(exceptionDetails);
    401      }
    402 
    403      if (returnByValue) {
    404        return valueFromRemoteObject(remoteObject);
    405      }
    406 
    407      return this.#world.createCdpHandle(remoteObject) as HandleFor<
    408        Awaited<ReturnType<Func>>
    409      >;
    410    }
    411 
    412    const functionDeclaration = stringifyFunction(pageFunction);
    413    const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test(
    414      functionDeclaration,
    415    )
    416      ? functionDeclaration
    417      : `${functionDeclaration}\n${sourceUrlComment}\n`;
    418    let callFunctionOnPromise;
    419    try {
    420      callFunctionOnPromise = this.#client.send('Runtime.callFunctionOn', {
    421        functionDeclaration: functionDeclarationWithSourceUrl,
    422        executionContextId: this.#id,
    423        // LazyArgs are used only internally and should not affect the order
    424        // evaluate calls for the public APIs.
    425        arguments: args.some(arg => {
    426          return arg instanceof LazyArg;
    427        })
    428          ? await Promise.all(
    429              args.map(arg => {
    430                return convertArgumentAsync(this, arg);
    431              }),
    432            )
    433          : args.map(arg => {
    434              return convertArgument(this, arg);
    435            }),
    436        returnByValue,
    437        awaitPromise: true,
    438        userGesture: true,
    439      });
    440    } catch (error) {
    441      if (
    442        error instanceof TypeError &&
    443        error.message.startsWith('Converting circular structure to JSON')
    444      ) {
    445        error.message += ' Recursive objects are not allowed.';
    446      }
    447      throw error;
    448    }
    449    const {exceptionDetails, result: remoteObject} =
    450      await callFunctionOnPromise.catch(rewriteError);
    451    if (exceptionDetails) {
    452      throw createEvaluationError(exceptionDetails);
    453    }
    454 
    455    if (returnByValue) {
    456      return valueFromRemoteObject(remoteObject);
    457    }
    458 
    459    return this.#world.createCdpHandle(remoteObject) as HandleFor<
    460      Awaited<ReturnType<Func>>
    461    >;
    462 
    463    async function convertArgumentAsync(
    464      context: ExecutionContext,
    465      arg: unknown,
    466    ) {
    467      if (arg instanceof LazyArg) {
    468        arg = await arg.get(context);
    469      }
    470      return convertArgument(context, arg);
    471    }
    472 
    473    function convertArgument(
    474      context: ExecutionContext,
    475      arg: unknown,
    476    ): Protocol.Runtime.CallArgument {
    477      if (typeof arg === 'bigint') {
    478        return {unserializableValue: `${arg.toString()}n`};
    479      }
    480      if (Object.is(arg, -0)) {
    481        return {unserializableValue: '-0'};
    482      }
    483      if (Object.is(arg, Infinity)) {
    484        return {unserializableValue: 'Infinity'};
    485      }
    486      if (Object.is(arg, -Infinity)) {
    487        return {unserializableValue: '-Infinity'};
    488      }
    489      if (Object.is(arg, NaN)) {
    490        return {unserializableValue: 'NaN'};
    491      }
    492      const objectHandle =
    493        arg && (arg instanceof CdpJSHandle || arg instanceof CdpElementHandle)
    494          ? arg
    495          : null;
    496      if (objectHandle) {
    497        if (objectHandle.realm !== context.#world) {
    498          throw new Error(
    499            'JSHandles can be evaluated only in the context they were created!',
    500          );
    501        }
    502        if (objectHandle.disposed) {
    503          throw new Error('JSHandle is disposed!');
    504        }
    505        if (objectHandle.remoteObject().unserializableValue) {
    506          return {
    507            unserializableValue:
    508              objectHandle.remoteObject().unserializableValue,
    509          };
    510        }
    511        if (!objectHandle.remoteObject().objectId) {
    512          return {value: objectHandle.remoteObject().value};
    513        }
    514        return {objectId: objectHandle.remoteObject().objectId};
    515      }
    516      return {value: arg};
    517    }
    518  }
    519 
    520  override [disposeSymbol](): void {
    521    this.#disposables.dispose();
    522    this.emit('disposed', undefined);
    523  }
    524 }
    525 
    526 const rewriteError = (error: Error): Protocol.Runtime.EvaluateResponse => {
    527  if (error.message.includes('Object reference chain is too long')) {
    528    return {result: {type: 'undefined'}};
    529  }
    530  if (error.message.includes("Object couldn't be returned by value")) {
    531    return {result: {type: 'undefined'}};
    532  }
    533 
    534  if (
    535    error.message.endsWith('Cannot find context with specified id') ||
    536    error.message.endsWith('Inspected target navigated or closed')
    537  ) {
    538    throw new Error(
    539      'Execution context was destroyed, most likely because of a navigation.',
    540    );
    541  }
    542  throw error;
    543 };