tor-browser

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

Frame.ts (11857B)


      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 type {CDPSession} from '../api/CDPSession.js';
     10 import type {ElementHandle} from '../api/ElementHandle.js';
     11 import type {WaitForOptions} from '../api/Frame.js';
     12 import {Frame, FrameEvent, throwIfDetached} from '../api/Frame.js';
     13 import type {HTTPResponse} from '../api/HTTPResponse.js';
     14 import type {WaitTimeoutOptions} from '../api/Page.js';
     15 import {UnsupportedOperation} from '../common/Errors.js';
     16 import {debugError} from '../common/util.js';
     17 import {Deferred} from '../util/Deferred.js';
     18 import {disposeSymbol} from '../util/disposable.js';
     19 import {isErrorLike} from '../util/ErrorLike.js';
     20 
     21 import {Accessibility} from './Accessibility.js';
     22 import type {Binding} from './Binding.js';
     23 import type {CdpPreloadScript} from './CdpPreloadScript.js';
     24 import type {
     25  DeviceRequestPrompt,
     26  DeviceRequestPromptManager,
     27 } from './DeviceRequestPrompt.js';
     28 import type {FrameManager} from './FrameManager.js';
     29 import {FrameManagerEvent} from './FrameManagerEvents.js';
     30 import type {IsolatedWorldChart} from './IsolatedWorld.js';
     31 import {IsolatedWorld} from './IsolatedWorld.js';
     32 import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
     33 import {
     34  LifecycleWatcher,
     35  type PuppeteerLifeCycleEvent,
     36 } from './LifecycleWatcher.js';
     37 import type {CdpPage} from './Page.js';
     38 import {CDP_BINDING_PREFIX} from './utils.js';
     39 
     40 /**
     41 * @internal
     42 */
     43 export class CdpFrame extends Frame {
     44  #url = '';
     45  #detached = false;
     46  #client: CDPSession;
     47 
     48  _frameManager: FrameManager;
     49  _loaderId = '';
     50  _lifecycleEvents = new Set<string>();
     51 
     52  override _id: string;
     53  override _parentId?: string;
     54  override accessibility: Accessibility;
     55 
     56  worlds: IsolatedWorldChart;
     57 
     58  constructor(
     59    frameManager: FrameManager,
     60    frameId: string,
     61    parentFrameId: string | undefined,
     62    client: CDPSession,
     63  ) {
     64    super();
     65    this._frameManager = frameManager;
     66    this.#url = '';
     67    this._id = frameId;
     68    this._parentId = parentFrameId;
     69    this.#detached = false;
     70    this.#client = client;
     71 
     72    this._loaderId = '';
     73    this.worlds = {
     74      [MAIN_WORLD]: new IsolatedWorld(this, this._frameManager.timeoutSettings),
     75      [PUPPETEER_WORLD]: new IsolatedWorld(
     76        this,
     77        this._frameManager.timeoutSettings,
     78      ),
     79    };
     80 
     81    this.accessibility = new Accessibility(this.worlds[MAIN_WORLD], frameId);
     82 
     83    this.on(FrameEvent.FrameSwappedByActivation, () => {
     84      // Emulate loading process for swapped frames.
     85      this._onLoadingStarted();
     86      this._onLoadingStopped();
     87    });
     88 
     89    this.worlds[MAIN_WORLD].emitter.on(
     90      'consoleapicalled',
     91      this.#onMainWorldConsoleApiCalled.bind(this),
     92    );
     93    this.worlds[MAIN_WORLD].emitter.on(
     94      'bindingcalled',
     95      this.#onMainWorldBindingCalled.bind(this),
     96    );
     97  }
     98 
     99  #onMainWorldConsoleApiCalled(
    100    event: Protocol.Runtime.ConsoleAPICalledEvent,
    101  ): void {
    102    this._frameManager.emit(FrameManagerEvent.ConsoleApiCalled, [
    103      this.worlds[MAIN_WORLD],
    104      event,
    105    ]);
    106  }
    107 
    108  #onMainWorldBindingCalled(event: Protocol.Runtime.BindingCalledEvent) {
    109    this._frameManager.emit(FrameManagerEvent.BindingCalled, [
    110      this.worlds[MAIN_WORLD],
    111      event,
    112    ]);
    113  }
    114 
    115  /**
    116   * This is used internally in DevTools.
    117   *
    118   * @internal
    119   */
    120  _client(): CDPSession {
    121    return this.#client;
    122  }
    123 
    124  /**
    125   * Updates the frame ID with the new ID. This happens when the main frame is
    126   * replaced by a different frame.
    127   */
    128  updateId(id: string): void {
    129    this._id = id;
    130  }
    131 
    132  updateClient(client: CDPSession): void {
    133    this.#client = client;
    134  }
    135 
    136  override page(): CdpPage {
    137    return this._frameManager.page();
    138  }
    139 
    140  @throwIfDetached
    141  override async goto(
    142    url: string,
    143    options: {
    144      referer?: string;
    145      referrerPolicy?: string;
    146      timeout?: number;
    147      waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
    148    } = {},
    149  ): Promise<HTTPResponse | null> {
    150    const {
    151      referer = this._frameManager.networkManager.extraHTTPHeaders()['referer'],
    152      referrerPolicy = this._frameManager.networkManager.extraHTTPHeaders()[
    153        'referer-policy'
    154      ],
    155      waitUntil = ['load'],
    156      timeout = this._frameManager.timeoutSettings.navigationTimeout(),
    157    } = options;
    158 
    159    let ensureNewDocumentNavigation = false;
    160    const watcher = new LifecycleWatcher(
    161      this._frameManager.networkManager,
    162      this,
    163      waitUntil,
    164      timeout,
    165    );
    166    let error = await Deferred.race([
    167      navigate(
    168        this.#client,
    169        url,
    170        referer,
    171        referrerPolicy as Protocol.Page.ReferrerPolicy,
    172        this._id,
    173      ),
    174      watcher.terminationPromise(),
    175    ]);
    176    if (!error) {
    177      error = await Deferred.race([
    178        watcher.terminationPromise(),
    179        ensureNewDocumentNavigation
    180          ? watcher.newDocumentNavigationPromise()
    181          : watcher.sameDocumentNavigationPromise(),
    182      ]);
    183    }
    184 
    185    try {
    186      if (error) {
    187        throw error;
    188      }
    189      return await watcher.navigationResponse();
    190    } finally {
    191      watcher.dispose();
    192    }
    193 
    194    async function navigate(
    195      client: CDPSession,
    196      url: string,
    197      referrer: string | undefined,
    198      referrerPolicy: Protocol.Page.ReferrerPolicy | undefined,
    199      frameId: string,
    200    ): Promise<Error | null> {
    201      try {
    202        const response = await client.send('Page.navigate', {
    203          url,
    204          referrer,
    205          frameId,
    206          referrerPolicy,
    207        });
    208        ensureNewDocumentNavigation = !!response.loaderId;
    209        if (response.errorText === 'net::ERR_HTTP_RESPONSE_CODE_FAILURE') {
    210          return null;
    211        }
    212        return response.errorText
    213          ? new Error(`${response.errorText} at ${url}`)
    214          : null;
    215      } catch (error) {
    216        if (isErrorLike(error)) {
    217          return error;
    218        }
    219        throw error;
    220      }
    221    }
    222  }
    223 
    224  @throwIfDetached
    225  override async waitForNavigation(
    226    options: WaitForOptions = {},
    227  ): Promise<HTTPResponse | null> {
    228    const {
    229      waitUntil = ['load'],
    230      timeout = this._frameManager.timeoutSettings.navigationTimeout(),
    231      signal,
    232    } = options;
    233    const watcher = new LifecycleWatcher(
    234      this._frameManager.networkManager,
    235      this,
    236      waitUntil,
    237      timeout,
    238      signal,
    239    );
    240    const error = await Deferred.race([
    241      watcher.terminationPromise(),
    242      ...(options.ignoreSameDocumentNavigation
    243        ? []
    244        : [watcher.sameDocumentNavigationPromise()]),
    245      watcher.newDocumentNavigationPromise(),
    246    ]);
    247    try {
    248      if (error) {
    249        throw error;
    250      }
    251      const result = await Deferred.race<
    252        Error | HTTPResponse | null | undefined
    253      >([watcher.terminationPromise(), watcher.navigationResponse()]);
    254      if (result instanceof Error) {
    255        throw error;
    256      }
    257      return result || null;
    258    } finally {
    259      watcher.dispose();
    260    }
    261  }
    262 
    263  override get client(): CDPSession {
    264    return this.#client;
    265  }
    266 
    267  override mainRealm(): IsolatedWorld {
    268    return this.worlds[MAIN_WORLD];
    269  }
    270 
    271  override isolatedRealm(): IsolatedWorld {
    272    return this.worlds[PUPPETEER_WORLD];
    273  }
    274 
    275  @throwIfDetached
    276  override async setContent(
    277    html: string,
    278    options: {
    279      timeout?: number;
    280      waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
    281    } = {},
    282  ): Promise<void> {
    283    const {
    284      waitUntil = ['load'],
    285      timeout = this._frameManager.timeoutSettings.navigationTimeout(),
    286    } = options;
    287 
    288    // We rely upon the fact that document.open() will reset frame lifecycle with "init"
    289    // lifecycle event. @see https://crrev.com/608658
    290    await this.setFrameContent(html);
    291 
    292    const watcher = new LifecycleWatcher(
    293      this._frameManager.networkManager,
    294      this,
    295      waitUntil,
    296      timeout,
    297    );
    298    const error = await Deferred.race<void | Error | undefined>([
    299      watcher.terminationPromise(),
    300      watcher.lifecyclePromise(),
    301    ]);
    302    watcher.dispose();
    303    if (error) {
    304      throw error;
    305    }
    306  }
    307 
    308  override url(): string {
    309    return this.#url;
    310  }
    311 
    312  override parentFrame(): CdpFrame | null {
    313    return this._frameManager._frameTree.parentFrame(this._id) || null;
    314  }
    315 
    316  override childFrames(): CdpFrame[] {
    317    return this._frameManager._frameTree.childFrames(this._id);
    318  }
    319 
    320  #deviceRequestPromptManager(): DeviceRequestPromptManager {
    321    return this._frameManager._deviceRequestPromptManager(this.#client);
    322  }
    323 
    324  @throwIfDetached
    325  async addPreloadScript(preloadScript: CdpPreloadScript): Promise<void> {
    326    const parentFrame = this.parentFrame();
    327    if (parentFrame && this.#client === parentFrame.client) {
    328      return;
    329    }
    330    if (preloadScript.getIdForFrame(this)) {
    331      return;
    332    }
    333    const {identifier} = await this.#client.send(
    334      'Page.addScriptToEvaluateOnNewDocument',
    335      {
    336        source: preloadScript.source,
    337      },
    338    );
    339    preloadScript.setIdForFrame(this, identifier);
    340  }
    341 
    342  @throwIfDetached
    343  async addExposedFunctionBinding(binding: Binding): Promise<void> {
    344    // If a frame has not started loading, it might never start. Rely on
    345    // addScriptToEvaluateOnNewDocument in that case.
    346    if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) {
    347      return;
    348    }
    349    await Promise.all([
    350      this.#client.send('Runtime.addBinding', {
    351        name: CDP_BINDING_PREFIX + binding.name,
    352      }),
    353      this.evaluate(binding.initSource).catch(debugError),
    354    ]);
    355  }
    356 
    357  @throwIfDetached
    358  async removeExposedFunctionBinding(binding: Binding): Promise<void> {
    359    // If a frame has not started loading, it might never start. Rely on
    360    // addScriptToEvaluateOnNewDocument in that case.
    361    if (this !== this._frameManager.mainFrame() && !this._hasStartedLoading) {
    362      return;
    363    }
    364    await Promise.all([
    365      this.#client.send('Runtime.removeBinding', {
    366        name: CDP_BINDING_PREFIX + binding.name,
    367      }),
    368      this.evaluate(name => {
    369        // Removes the dangling Puppeteer binding wrapper.
    370        // @ts-expect-error: In a different context.
    371        globalThis[name] = undefined;
    372      }, binding.name).catch(debugError),
    373    ]);
    374  }
    375 
    376  @throwIfDetached
    377  override async waitForDevicePrompt(
    378    options: WaitTimeoutOptions = {},
    379  ): Promise<DeviceRequestPrompt> {
    380    return await this.#deviceRequestPromptManager().waitForDevicePrompt(
    381      options,
    382    );
    383  }
    384 
    385  _navigated(framePayload: Protocol.Page.Frame): void {
    386    this._name = framePayload.name;
    387    this.#url = `${framePayload.url}${framePayload.urlFragment || ''}`;
    388  }
    389 
    390  _navigatedWithinDocument(url: string): void {
    391    this.#url = url;
    392  }
    393 
    394  _onLifecycleEvent(loaderId: string, name: string): void {
    395    if (name === 'init') {
    396      this._loaderId = loaderId;
    397      this._lifecycleEvents.clear();
    398    }
    399    this._lifecycleEvents.add(name);
    400  }
    401 
    402  _onLoadingStopped(): void {
    403    this._lifecycleEvents.add('DOMContentLoaded');
    404    this._lifecycleEvents.add('load');
    405  }
    406 
    407  _onLoadingStarted(): void {
    408    this._hasStartedLoading = true;
    409  }
    410 
    411  override get detached(): boolean {
    412    return this.#detached;
    413  }
    414 
    415  override [disposeSymbol](): void {
    416    if (this.#detached) {
    417      return;
    418    }
    419    this.#detached = true;
    420    this.worlds[MAIN_WORLD][disposeSymbol]();
    421    this.worlds[PUPPETEER_WORLD][disposeSymbol]();
    422  }
    423 
    424  exposeFunction(): never {
    425    throw new UnsupportedOperation();
    426  }
    427 
    428  override async frameElement(): Promise<ElementHandle<HTMLIFrameElement> | null> {
    429    const parent = this.parentFrame();
    430    if (!parent) {
    431      return null;
    432    }
    433    const {backendNodeId} = await parent.client.send('DOM.getFrameOwner', {
    434      frameId: this._id,
    435    });
    436    return (await parent
    437      .mainRealm()
    438      .adoptBackendNode(backendNodeId)) as ElementHandle<HTMLIFrameElement>;
    439  }
    440 }