tor-browser

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

util.ts (11162B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {OperatorFunction} from '../../third_party/rxjs/rxjs.js';
      8 import {
      9  filter,
     10  from,
     11  fromEvent,
     12  map,
     13  mergeMap,
     14  NEVER,
     15  Observable,
     16  timer,
     17 } from '../../third_party/rxjs/rxjs.js';
     18 import type {CDPSession} from '../api/CDPSession.js';
     19 import {environment} from '../environment.js';
     20 import {packageVersion} from '../generated/version.js';
     21 import {assert} from '../util/assert.js';
     22 import {mergeUint8Arrays, stringToTypedArray} from '../util/encoding.js';
     23 
     24 import {debug} from './Debug.js';
     25 import {TimeoutError} from './Errors.js';
     26 import type {EventEmitter, EventType} from './EventEmitter.js';
     27 import type {
     28  LowerCasePaperFormat,
     29  ParsedPDFOptions,
     30  PDFOptions,
     31 } from './PDFOptions.js';
     32 import {paperFormats} from './PDFOptions.js';
     33 
     34 /**
     35 * @internal
     36 */
     37 export const debugError = debug('puppeteer:error');
     38 
     39 /**
     40 * @internal
     41 */
     42 export const DEFAULT_VIEWPORT = Object.freeze({width: 800, height: 600});
     43 
     44 /**
     45 * @internal
     46 */
     47 const SOURCE_URL = Symbol('Source URL for Puppeteer evaluation scripts');
     48 
     49 /**
     50 * @internal
     51 */
     52 export class PuppeteerURL {
     53  static INTERNAL_URL = 'pptr:internal';
     54 
     55  static fromCallSite(
     56    functionName: string,
     57    site: NodeJS.CallSite,
     58  ): PuppeteerURL {
     59    const url = new PuppeteerURL();
     60    url.#functionName = functionName;
     61    url.#siteString = site.toString();
     62    return url;
     63  }
     64 
     65  static parse = (url: string): PuppeteerURL => {
     66    url = url.slice('pptr:'.length);
     67    const [functionName = '', siteString = ''] = url.split(';');
     68    const puppeteerUrl = new PuppeteerURL();
     69    puppeteerUrl.#functionName = functionName;
     70    puppeteerUrl.#siteString = decodeURIComponent(siteString);
     71    return puppeteerUrl;
     72  };
     73 
     74  static isPuppeteerURL = (url: string): boolean => {
     75    return url.startsWith('pptr:');
     76  };
     77 
     78  #functionName!: string;
     79  #siteString!: string;
     80 
     81  get functionName(): string {
     82    return this.#functionName;
     83  }
     84 
     85  get siteString(): string {
     86    return this.#siteString;
     87  }
     88 
     89  toString(): string {
     90    return `pptr:${[
     91      this.#functionName,
     92      encodeURIComponent(this.#siteString),
     93    ].join(';')}`;
     94  }
     95 }
     96 
     97 /**
     98 * @internal
     99 */
    100 export const withSourcePuppeteerURLIfNone = <T extends NonNullable<unknown>>(
    101  functionName: string,
    102  object: T,
    103 ): T => {
    104  if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
    105    return object;
    106  }
    107  const original = Error.prepareStackTrace;
    108  Error.prepareStackTrace = (_, stack) => {
    109    // First element is the function.
    110    // Second element is the caller of this function.
    111    // Third element is the caller of the caller of this function
    112    // which is precisely what we want.
    113    return stack[2];
    114  };
    115  const site = new Error().stack as unknown as NodeJS.CallSite;
    116  Error.prepareStackTrace = original;
    117  return Object.assign(object, {
    118    [SOURCE_URL]: PuppeteerURL.fromCallSite(functionName, site),
    119  });
    120 };
    121 
    122 /**
    123 * @internal
    124 */
    125 export const getSourcePuppeteerURLIfAvailable = <
    126  T extends NonNullable<unknown>,
    127 >(
    128  object: T,
    129 ): PuppeteerURL | undefined => {
    130  if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
    131    return object[SOURCE_URL as keyof T] as PuppeteerURL;
    132  }
    133  return undefined;
    134 };
    135 
    136 /**
    137 * @internal
    138 */
    139 export const isString = (obj: unknown): obj is string => {
    140  return typeof obj === 'string' || obj instanceof String;
    141 };
    142 
    143 /**
    144 * @internal
    145 */
    146 export const isNumber = (obj: unknown): obj is number => {
    147  return typeof obj === 'number' || obj instanceof Number;
    148 };
    149 
    150 /**
    151 * @internal
    152 */
    153 export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
    154  return typeof obj === 'object' && obj?.constructor === Object;
    155 };
    156 
    157 /**
    158 * @internal
    159 */
    160 export const isRegExp = (obj: unknown): obj is RegExp => {
    161  return typeof obj === 'object' && obj?.constructor === RegExp;
    162 };
    163 
    164 /**
    165 * @internal
    166 */
    167 export const isDate = (obj: unknown): obj is Date => {
    168  return typeof obj === 'object' && obj?.constructor === Date;
    169 };
    170 
    171 /**
    172 * @internal
    173 */
    174 export function evaluationString(
    175  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
    176  fun: Function | string,
    177  ...args: unknown[]
    178 ): string {
    179  if (isString(fun)) {
    180    assert(args.length === 0, 'Cannot evaluate a string with arguments');
    181    return fun;
    182  }
    183 
    184  function serializeArgument(arg: unknown): string {
    185    if (Object.is(arg, undefined)) {
    186      return 'undefined';
    187    }
    188    return JSON.stringify(arg);
    189  }
    190 
    191  return `(${fun})(${args.map(serializeArgument).join(',')})`;
    192 }
    193 
    194 /**
    195 * @internal
    196 */
    197 export async function getReadableAsTypedArray(
    198  readable: ReadableStream<Uint8Array>,
    199  path?: string,
    200 ): Promise<Uint8Array | null> {
    201  const buffers: Uint8Array[] = [];
    202  const reader = readable.getReader();
    203  if (path) {
    204    const fileHandle = await environment.value.fs.promises.open(path, 'w+');
    205    try {
    206      while (true) {
    207        const {done, value} = await reader.read();
    208        if (done) {
    209          break;
    210        }
    211        buffers.push(value);
    212        await fileHandle.writeFile(value);
    213      }
    214    } finally {
    215      await fileHandle.close();
    216    }
    217  } else {
    218    while (true) {
    219      const {done, value} = await reader.read();
    220      if (done) {
    221        break;
    222      }
    223      buffers.push(value);
    224    }
    225  }
    226  try {
    227    const concat = mergeUint8Arrays(buffers);
    228    if (concat.length === 0) {
    229      return null;
    230    }
    231    return concat;
    232  } catch (error) {
    233    debugError(error);
    234    return null;
    235  }
    236 }
    237 
    238 /**
    239 * @internal
    240 */
    241 
    242 /**
    243 * @internal
    244 */
    245 export async function getReadableFromProtocolStream(
    246  client: CDPSession,
    247  handle: string,
    248 ): Promise<ReadableStream<Uint8Array>> {
    249  return new ReadableStream({
    250    async pull(controller) {
    251      const {data, base64Encoded, eof} = await client.send('IO.read', {
    252        handle,
    253      });
    254 
    255      controller.enqueue(stringToTypedArray(data, base64Encoded ?? false));
    256      if (eof) {
    257        await client.send('IO.close', {handle});
    258        controller.close();
    259      }
    260    },
    261  });
    262 }
    263 
    264 /**
    265 * @internal
    266 */
    267 export function validateDialogType(
    268  type: string,
    269 ): 'alert' | 'confirm' | 'prompt' | 'beforeunload' {
    270  let dialogType = null;
    271  const validDialogTypes = new Set([
    272    'alert',
    273    'confirm',
    274    'prompt',
    275    'beforeunload',
    276  ]);
    277 
    278  if (validDialogTypes.has(type)) {
    279    dialogType = type;
    280  }
    281  assert(dialogType, `Unknown javascript dialog type: ${type}`);
    282  return dialogType as 'alert' | 'confirm' | 'prompt' | 'beforeunload';
    283 }
    284 
    285 /**
    286 * @internal
    287 */
    288 export function timeout(ms: number, cause?: Error): Observable<never> {
    289  return ms === 0
    290    ? NEVER
    291    : timer(ms).pipe(
    292        map(() => {
    293          throw new TimeoutError(`Timed out after waiting ${ms}ms`, {cause});
    294        }),
    295      );
    296 }
    297 
    298 /**
    299 * @internal
    300 */
    301 export const UTILITY_WORLD_NAME =
    302  '__puppeteer_utility_world__' + packageVersion;
    303 
    304 /**
    305 * @internal
    306 */
    307 export const SOURCE_URL_REGEX =
    308  /^[\x20\t]*\/\/[@#] sourceURL=\s{0,10}(\S*?)\s{0,10}$/m;
    309 /**
    310 * @internal
    311 */
    312 export function getSourceUrlComment(url: string): string {
    313  return `//# sourceURL=${url}`;
    314 }
    315 
    316 /**
    317 * @internal
    318 */
    319 export const NETWORK_IDLE_TIME = 500;
    320 
    321 /**
    322 * @internal
    323 */
    324 export function parsePDFOptions(
    325  options: PDFOptions = {},
    326  lengthUnit: 'in' | 'cm' = 'in',
    327 ): ParsedPDFOptions {
    328  const defaults: Omit<ParsedPDFOptions, 'width' | 'height' | 'margin'> = {
    329    scale: 1,
    330    displayHeaderFooter: false,
    331    headerTemplate: '',
    332    footerTemplate: '',
    333    printBackground: false,
    334    landscape: false,
    335    pageRanges: '',
    336    preferCSSPageSize: false,
    337    omitBackground: false,
    338    outline: false,
    339    tagged: true,
    340    waitForFonts: true,
    341  };
    342 
    343  let width = 8.5;
    344  let height = 11;
    345  if (options.format) {
    346    const format =
    347      paperFormats[options.format.toLowerCase() as LowerCasePaperFormat][
    348        lengthUnit
    349      ];
    350    assert(format, 'Unknown paper format: ' + options.format);
    351    width = format.width;
    352    height = format.height;
    353  } else {
    354    width = convertPrintParameterToInches(options.width, lengthUnit) ?? width;
    355    height =
    356      convertPrintParameterToInches(options.height, lengthUnit) ?? height;
    357  }
    358 
    359  const margin = {
    360    top: convertPrintParameterToInches(options.margin?.top, lengthUnit) || 0,
    361    left: convertPrintParameterToInches(options.margin?.left, lengthUnit) || 0,
    362    bottom:
    363      convertPrintParameterToInches(options.margin?.bottom, lengthUnit) || 0,
    364    right:
    365      convertPrintParameterToInches(options.margin?.right, lengthUnit) || 0,
    366  };
    367 
    368  // Quirk https://bugs.chromium.org/p/chromium/issues/detail?id=840455#c44
    369  if (options.outline) {
    370    options.tagged = true;
    371  }
    372 
    373  return {
    374    ...defaults,
    375    ...options,
    376    width,
    377    height,
    378    margin,
    379  };
    380 }
    381 
    382 /**
    383 * @internal
    384 */
    385 export const unitToPixels = {
    386  px: 1,
    387  in: 96,
    388  cm: 37.8,
    389  mm: 3.78,
    390 };
    391 
    392 function convertPrintParameterToInches(
    393  parameter?: string | number,
    394  lengthUnit: 'in' | 'cm' = 'in',
    395 ): number | undefined {
    396  if (typeof parameter === 'undefined') {
    397    return undefined;
    398  }
    399  let pixels;
    400  if (isNumber(parameter)) {
    401    // Treat numbers as pixel values to be aligned with phantom's paperSize.
    402    pixels = parameter;
    403  } else if (isString(parameter)) {
    404    const text = parameter;
    405    let unit = text.substring(text.length - 2).toLowerCase();
    406    let valueText = '';
    407    if (unit in unitToPixels) {
    408      valueText = text.substring(0, text.length - 2);
    409    } else {
    410      // In case of unknown unit try to parse the whole parameter as number of pixels.
    411      // This is consistent with phantom's paperSize behavior.
    412      unit = 'px';
    413      valueText = text;
    414    }
    415    const value = Number(valueText);
    416    assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
    417    pixels = value * unitToPixels[unit as keyof typeof unitToPixels];
    418  } else {
    419    throw new Error(
    420      'page.pdf() Cannot handle parameter type: ' + typeof parameter,
    421    );
    422  }
    423  return pixels / unitToPixels[lengthUnit];
    424 }
    425 
    426 /**
    427 * @internal
    428 */
    429 export function fromEmitterEvent<
    430  Events extends Record<EventType, unknown>,
    431  Event extends keyof Events,
    432 >(emitter: EventEmitter<Events>, eventName: Event): Observable<Events[Event]> {
    433  return new Observable(subscriber => {
    434    const listener = (event: Events[Event]) => {
    435      subscriber.next(event);
    436    };
    437    emitter.on(eventName, listener);
    438    return () => {
    439      emitter.off(eventName, listener);
    440    };
    441  });
    442 }
    443 
    444 /**
    445 * @internal
    446 */
    447 export function fromAbortSignal(
    448  signal?: AbortSignal,
    449  cause?: Error,
    450 ): Observable<never> {
    451  return signal
    452    ? fromEvent(signal, 'abort').pipe(
    453        map(() => {
    454          if (signal.reason instanceof Error) {
    455            signal.reason.cause = cause;
    456            throw signal.reason;
    457          }
    458 
    459          throw new Error(signal.reason, {cause});
    460        }),
    461      )
    462    : NEVER;
    463 }
    464 
    465 /**
    466 * @internal
    467 */
    468 export function filterAsync<T>(
    469  predicate: (value: T) => boolean | PromiseLike<boolean>,
    470 ): OperatorFunction<T, T> {
    471  return mergeMap<T, Observable<T>>((value): Observable<T> => {
    472    return from(Promise.resolve(predicate(value))).pipe(
    473      filter(isMatch => {
    474        return isMatch;
    475      }),
    476      map(() => {
    477        return value;
    478      }),
    479    );
    480  });
    481 }