tor-browser

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

Realm.ts (11950B)


      1 /**
      2 * @license
      3 * Copyright 2024 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
      7 
      8 import type {JSHandle} from '../api/JSHandle.js';
      9 import {Realm} from '../api/Realm.js';
     10 import {ARIAQueryHandler} from '../cdp/AriaQueryHandler.js';
     11 import {LazyArg} from '../common/LazyArg.js';
     12 import {scriptInjector} from '../common/ScriptInjector.js';
     13 import type {TimeoutSettings} from '../common/TimeoutSettings.js';
     14 import type {EvaluateFunc, HandleFor} from '../common/types.js';
     15 import {
     16  debugError,
     17  getSourcePuppeteerURLIfAvailable,
     18  getSourceUrlComment,
     19  isString,
     20  PuppeteerURL,
     21  SOURCE_URL_REGEX,
     22 } from '../common/util.js';
     23 import type PuppeteerUtil from '../injected/injected.js';
     24 import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
     25 import {stringifyFunction} from '../util/Function.js';
     26 
     27 import type {
     28  Realm as BidiRealmCore,
     29  DedicatedWorkerRealm,
     30  SharedWorkerRealm,
     31 } from './core/Realm.js';
     32 import type {WindowRealm} from './core/Realm.js';
     33 import {BidiDeserializer} from './Deserializer.js';
     34 import {BidiElementHandle} from './ElementHandle.js';
     35 import {ExposableFunction} from './ExposedFunction.js';
     36 import type {BidiFrame} from './Frame.js';
     37 import {BidiJSHandle} from './JSHandle.js';
     38 import {BidiSerializer} from './Serializer.js';
     39 import {createEvaluationError} from './util.js';
     40 import type {BidiWebWorker} from './WebWorker.js';
     41 
     42 /**
     43 * @internal
     44 */
     45 export abstract class BidiRealm extends Realm {
     46  readonly realm: BidiRealmCore;
     47 
     48  constructor(realm: BidiRealmCore, timeoutSettings: TimeoutSettings) {
     49    super(timeoutSettings);
     50    this.realm = realm;
     51  }
     52 
     53  protected initialize(): void {
     54    this.realm.on('destroyed', ({reason}) => {
     55      this.taskManager.terminateAll(new Error(reason));
     56      this.dispose();
     57    });
     58    this.realm.on('updated', () => {
     59      this.internalPuppeteerUtil = undefined;
     60      void this.taskManager.rerunAll();
     61    });
     62  }
     63 
     64  protected internalPuppeteerUtil?: Promise<BidiJSHandle<PuppeteerUtil>>;
     65  get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> {
     66    const promise = Promise.resolve() as Promise<unknown>;
     67    scriptInjector.inject(script => {
     68      if (this.internalPuppeteerUtil) {
     69        void this.internalPuppeteerUtil.then(handle => {
     70          void handle.dispose();
     71        });
     72      }
     73      this.internalPuppeteerUtil = promise.then(() => {
     74        return this.evaluateHandle(script) as Promise<
     75          BidiJSHandle<PuppeteerUtil>
     76        >;
     77      });
     78    }, !this.internalPuppeteerUtil);
     79    return this.internalPuppeteerUtil as Promise<BidiJSHandle<PuppeteerUtil>>;
     80  }
     81 
     82  override async evaluateHandle<
     83    Params extends unknown[],
     84    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
     85  >(
     86    pageFunction: Func | string,
     87    ...args: Params
     88  ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
     89    return await this.#evaluate(false, pageFunction, ...args);
     90  }
     91 
     92  override async evaluate<
     93    Params extends unknown[],
     94    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
     95  >(
     96    pageFunction: Func | string,
     97    ...args: Params
     98  ): Promise<Awaited<ReturnType<Func>>> {
     99    return await this.#evaluate(true, pageFunction, ...args);
    100  }
    101 
    102  async #evaluate<
    103    Params extends unknown[],
    104    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    105  >(
    106    returnByValue: true,
    107    pageFunction: Func | string,
    108    ...args: Params
    109  ): Promise<Awaited<ReturnType<Func>>>;
    110  async #evaluate<
    111    Params extends unknown[],
    112    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    113  >(
    114    returnByValue: false,
    115    pageFunction: Func | string,
    116    ...args: Params
    117  ): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
    118  async #evaluate<
    119    Params extends unknown[],
    120    Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
    121  >(
    122    returnByValue: boolean,
    123    pageFunction: Func | string,
    124    ...args: Params
    125  ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
    126    const sourceUrlComment = getSourceUrlComment(
    127      getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
    128        PuppeteerURL.INTERNAL_URL,
    129    );
    130 
    131    let responsePromise;
    132    const resultOwnership = returnByValue
    133      ? Bidi.Script.ResultOwnership.None
    134      : Bidi.Script.ResultOwnership.Root;
    135    const serializationOptions: Bidi.Script.SerializationOptions = returnByValue
    136      ? {}
    137      : {
    138          maxObjectDepth: 0,
    139          maxDomDepth: 0,
    140        };
    141    if (isString(pageFunction)) {
    142      const expression = SOURCE_URL_REGEX.test(pageFunction)
    143        ? pageFunction
    144        : `${pageFunction}\n${sourceUrlComment}\n`;
    145 
    146      responsePromise = this.realm.evaluate(expression, true, {
    147        resultOwnership,
    148        userActivation: true,
    149        serializationOptions,
    150      });
    151    } else {
    152      let functionDeclaration = stringifyFunction(pageFunction);
    153      functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
    154        ? functionDeclaration
    155        : `${functionDeclaration}\n${sourceUrlComment}\n`;
    156      responsePromise = this.realm.callFunction(
    157        functionDeclaration,
    158        /* awaitPromise= */ true,
    159        {
    160          // LazyArgs are used only internally and should not affect the order
    161          // evaluate calls for the public APIs.
    162          arguments: args.some(arg => {
    163            return arg instanceof LazyArg;
    164          })
    165            ? await Promise.all(
    166                args.map(arg => {
    167                  return this.serializeAsync(arg);
    168                }),
    169              )
    170            : args.map(arg => {
    171                return this.serialize(arg);
    172              }),
    173          resultOwnership,
    174          userActivation: true,
    175          serializationOptions,
    176        },
    177      );
    178    }
    179 
    180    const result = await responsePromise;
    181 
    182    if ('type' in result && result.type === 'exception') {
    183      throw createEvaluationError(result.exceptionDetails);
    184    }
    185 
    186    if (returnByValue) {
    187      return BidiDeserializer.deserialize(result.result);
    188    }
    189 
    190    return this.createHandle(result.result) as unknown as HandleFor<
    191      Awaited<ReturnType<Func>>
    192    >;
    193  }
    194 
    195  createHandle(
    196    result: Bidi.Script.RemoteValue,
    197  ): BidiJSHandle<unknown> | BidiElementHandle<Node> {
    198    if (
    199      (result.type === 'node' || result.type === 'window') &&
    200      this instanceof BidiFrameRealm
    201    ) {
    202      return BidiElementHandle.from(result, this);
    203    }
    204    return BidiJSHandle.from(result, this);
    205  }
    206 
    207  async serializeAsync(arg: unknown): Promise<Bidi.Script.LocalValue> {
    208    if (arg instanceof LazyArg) {
    209      arg = await arg.get(this);
    210    }
    211    return this.serialize(arg);
    212  }
    213 
    214  serialize(arg: unknown): Bidi.Script.LocalValue {
    215    if (arg instanceof BidiJSHandle || arg instanceof BidiElementHandle) {
    216      if (arg.realm !== this) {
    217        if (
    218          !(arg.realm instanceof BidiFrameRealm) ||
    219          !(this instanceof BidiFrameRealm)
    220        ) {
    221          throw new Error(
    222            "Trying to evaluate JSHandle from different global types. Usually this means you're using a handle from a worker in a page or vice versa.",
    223          );
    224        }
    225        if (arg.realm.environment !== this.environment) {
    226          throw new Error(
    227            "Trying to evaluate JSHandle from different frames. Usually this means you're using a handle from a page on a different page.",
    228          );
    229        }
    230      }
    231      if (arg.disposed) {
    232        throw new Error('JSHandle is disposed!');
    233      }
    234      return arg.remoteValue() as Bidi.Script.RemoteReference;
    235    }
    236 
    237    return BidiSerializer.serialize(arg);
    238  }
    239 
    240  async destroyHandles(handles: Array<BidiJSHandle<unknown>>): Promise<void> {
    241    if (this.disposed) {
    242      return;
    243    }
    244 
    245    const handleIds = handles
    246      .map(({id}) => {
    247        return id;
    248      })
    249      .filter((id): id is string => {
    250        return id !== undefined;
    251      });
    252 
    253    if (handleIds.length === 0) {
    254      return;
    255    }
    256 
    257    await this.realm.disown(handleIds).catch(error => {
    258      // Exceptions might happen in case of a page been navigated or closed.
    259      // Swallow these since they are harmless and we don't leak anything in this case.
    260      debugError(error);
    261    });
    262  }
    263 
    264  override async adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T> {
    265    return (await this.evaluateHandle(node => {
    266      return node;
    267    }, handle)) as unknown as T;
    268  }
    269 
    270  override async transferHandle<T extends JSHandle<Node>>(
    271    handle: T,
    272  ): Promise<T> {
    273    if (handle.realm === this) {
    274      return handle;
    275    }
    276    const transferredHandle = this.adoptHandle(handle);
    277    await handle.dispose();
    278    return await transferredHandle;
    279  }
    280 }
    281 
    282 /**
    283 * @internal
    284 */
    285 export class BidiFrameRealm extends BidiRealm {
    286  static from(realm: WindowRealm, frame: BidiFrame): BidiFrameRealm {
    287    const frameRealm = new BidiFrameRealm(realm, frame);
    288    frameRealm.#initialize();
    289    return frameRealm;
    290  }
    291  declare readonly realm: WindowRealm;
    292 
    293  readonly #frame: BidiFrame;
    294 
    295  private constructor(realm: WindowRealm, frame: BidiFrame) {
    296    super(realm, frame.timeoutSettings);
    297    this.#frame = frame;
    298  }
    299 
    300  #initialize() {
    301    super.initialize();
    302 
    303    // This should run first.
    304    this.realm.on('updated', () => {
    305      this.environment.clearDocumentHandle();
    306      this.#bindingsInstalled = false;
    307    });
    308  }
    309 
    310  #bindingsInstalled = false;
    311  override get puppeteerUtil(): Promise<BidiJSHandle<PuppeteerUtil>> {
    312    let promise = Promise.resolve() as Promise<unknown>;
    313    if (!this.#bindingsInstalled) {
    314      promise = Promise.all([
    315        ExposableFunction.from(
    316          this.environment as BidiFrame,
    317          '__ariaQuerySelector',
    318          ARIAQueryHandler.queryOne,
    319          !!this.sandbox,
    320        ),
    321        ExposableFunction.from(
    322          this.environment as BidiFrame,
    323          '__ariaQuerySelectorAll',
    324          async (
    325            element: BidiElementHandle<Node>,
    326            selector: string,
    327          ): Promise<JSHandle<Node[]>> => {
    328            const results = ARIAQueryHandler.queryAll(element, selector);
    329            return await element.realm.evaluateHandle(
    330              (...elements) => {
    331                return elements;
    332              },
    333              ...(await AsyncIterableUtil.collect(results)),
    334            );
    335          },
    336          !!this.sandbox,
    337        ),
    338      ]);
    339      this.#bindingsInstalled = true;
    340    }
    341    return promise.then(() => {
    342      return super.puppeteerUtil;
    343    });
    344  }
    345 
    346  get sandbox(): string | undefined {
    347    return this.realm.sandbox;
    348  }
    349 
    350  override get environment(): BidiFrame {
    351    return this.#frame;
    352  }
    353 
    354  override async adoptBackendNode(
    355    backendNodeId?: number | undefined,
    356  ): Promise<JSHandle<Node>> {
    357    const {object} = await this.#frame.client.send('DOM.resolveNode', {
    358      backendNodeId,
    359      executionContextId: await this.realm.resolveExecutionContextId(),
    360    });
    361    using handle = BidiElementHandle.from(
    362      {
    363        handle: object.objectId,
    364        type: 'node',
    365      },
    366      this,
    367    );
    368    // We need the sharedId, so we perform the following to obtain it.
    369    return await handle.evaluateHandle(element => {
    370      return element;
    371    });
    372  }
    373 }
    374 
    375 /**
    376 * @internal
    377 */
    378 export class BidiWorkerRealm extends BidiRealm {
    379  static from(
    380    realm: DedicatedWorkerRealm | SharedWorkerRealm,
    381    worker: BidiWebWorker,
    382  ): BidiWorkerRealm {
    383    const workerRealm = new BidiWorkerRealm(realm, worker);
    384    workerRealm.initialize();
    385    return workerRealm;
    386  }
    387  declare readonly realm: DedicatedWorkerRealm | SharedWorkerRealm;
    388 
    389  readonly #worker: BidiWebWorker;
    390 
    391  private constructor(
    392    realm: DedicatedWorkerRealm | SharedWorkerRealm,
    393    frame: BidiWebWorker,
    394  ) {
    395    super(realm, frame.timeoutSettings);
    396    this.#worker = frame;
    397  }
    398 
    399  override get environment(): BidiWebWorker {
    400    return this.#worker;
    401  }
    402 
    403  override async adoptBackendNode(): Promise<JSHandle<Node>> {
    404    throw new Error('Cannot adopt DOM nodes into a worker.');
    405  }
    406 }