tor-browser

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

GeckoViewUtils.sys.mjs (17267B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import { Log } from "resource://gre/modules/Log.sys.mjs";
      7 
      8 const lazy = {};
      9 
     10 ChromeUtils.defineESModuleGetters(lazy, {
     11  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
     12  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     13  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     14 });
     15 
     16 if (AppConstants.platform == "android") {
     17  ChromeUtils.defineESModuleGetters(lazy, {
     18    AndroidAppender: "resource://gre/modules/AndroidLog.sys.mjs",
     19  });
     20 }
     21 
     22 export var GeckoViewUtils = {
     23  /**
     24   * Define a lazy getter that loads an object from external code, and
     25   * optionally handles observer and/or message manager notifications for the
     26   * object, so the object only loads when a notification is received.
     27   *
     28   * @param scope     Scope for holding the loaded object.
     29   * @param name      Name of the object to load.
     30   * @param service   If specified, load the object from a JS component; the
     31   *                  component must include the line
     32   *                  "this.wrappedJSObject = this;" in its constructor.
     33   * @param module    If specified, load the object from a JS module.
     34   * @param init      Optional post-load initialization function.
     35   * @param observers If specified, listen to specified observer notifications.
     36   * @param ppmm      If specified, listen to specified process messages.
     37   * @param mm        If specified, listen to specified frame messages.
     38   * @param ged       If specified, listen to specified global EventDispatcher events.
     39   * @param once      if true, only listen to the specified
     40   *                  events/messages/notifications once.
     41   */
     42  addLazyGetter(
     43    scope,
     44    name,
     45    { service, module, handler, observers, ppmm, mm, ged, init, once }
     46  ) {
     47    ChromeUtils.defineLazyGetter(scope, name, _ => {
     48      let ret = undefined;
     49      if (module) {
     50        ret = ChromeUtils.importESModule(module)[name];
     51      } else if (service) {
     52        ret = Cc[service].getService(Ci.nsISupports).wrappedJSObject;
     53      } else if (typeof handler === "function") {
     54        ret = {
     55          handleEvent: handler,
     56          observe: handler,
     57          onEvent: handler,
     58          receiveMessage: handler,
     59        };
     60      } else if (handler) {
     61        ret = handler;
     62      }
     63      if (ret && init) {
     64        init.call(scope, ret);
     65      }
     66      return ret;
     67    });
     68 
     69    if (observers) {
     70      const observer = (subject, topic, data) => {
     71        Services.obs.removeObserver(observer, topic);
     72        if (!once) {
     73          Services.obs.addObserver(scope[name], topic);
     74        }
     75        scope[name].observe(subject, topic, data); // Explicitly notify new observer
     76      };
     77      observers.forEach(topic => Services.obs.addObserver(observer, topic));
     78    }
     79 
     80    if (!this.IS_PARENT_PROCESS) {
     81      // ppmm, mm, and ged are only available in the parent process.
     82      return;
     83    }
     84 
     85    const addMMListener = (target, names) => {
     86      const listener = msg => {
     87        target.removeMessageListener(msg.name, listener);
     88        if (!once) {
     89          target.addMessageListener(msg.name, scope[name]);
     90        }
     91        scope[name].receiveMessage(msg);
     92      };
     93      names.forEach(msg => target.addMessageListener(msg, listener));
     94    };
     95    if (ppmm) {
     96      addMMListener(Services.ppmm, ppmm);
     97    }
     98    if (mm) {
     99      addMMListener(Services.mm, mm);
    100    }
    101 
    102    if (ged) {
    103      const listener = (event, data, callback) => {
    104        lazy.EventDispatcher.instance.unregisterListener(listener, event);
    105        if (!once) {
    106          lazy.EventDispatcher.instance.registerListener(scope[name], event);
    107        }
    108        scope[name].onEvent(event, data, callback);
    109      };
    110      lazy.EventDispatcher.instance.registerListener(listener, ged);
    111    }
    112  },
    113 
    114  _addLazyListeners(events, handler, scope, name, addFn, handleFn) {
    115    if (!handler) {
    116      handler = _ =>
    117        Array.isArray(name) ? name.map(n => scope[n]) : scope[name];
    118    }
    119    const listener = (...args) => {
    120      let handlers = handler(...args);
    121      if (!handlers) {
    122        return;
    123      }
    124      if (!Array.isArray(handlers)) {
    125        handlers = [handlers];
    126      }
    127      handleFn(handlers, listener, args);
    128    };
    129    if (Array.isArray(events)) {
    130      addFn(events, listener);
    131    } else {
    132      addFn([events], listener);
    133    }
    134  },
    135 
    136  /**
    137   * Add lazy event listeners that only load the actual handler when an event
    138   * is being handled.
    139   *
    140   * @param target  Event target for the event listeners.
    141   * @param events  Event name as a string or array.
    142   * @param handler If specified, function that, for a given event, returns the
    143   *                actual event handler as an object or an array of objects.
    144   *                If handler is not specified, the actual event handler is
    145   *                specified using the scope and name pair.
    146   * @param scope   See handler.
    147   * @param name    See handler.
    148   * @param options Options for addEventListener.
    149   */
    150  addLazyEventListener(target, events, { handler, scope, name, options }) {
    151    this._addLazyListeners(
    152      events,
    153      handler,
    154      scope,
    155      name,
    156      (events, listener) => {
    157        events.forEach(event =>
    158          target.addEventListener(event, listener, options)
    159        );
    160      },
    161      (handlers, listener, args) => {
    162        if (!options || !options.once) {
    163          target.removeEventListener(args[0].type, listener, options);
    164          handlers.forEach(handler =>
    165            target.addEventListener(args[0].type, handler, options)
    166          );
    167        }
    168        handlers.forEach(handler => handler.handleEvent(args[0]));
    169      }
    170    );
    171  },
    172 
    173  /**
    174   * Add lazy pref observers, and only load the actual handler once the pref
    175   * value changes from default, and every time the pref value changes
    176   * afterwards.
    177   *
    178   * @param aPrefs  Prefs as an object or array. Each pref object has fields
    179   *                "name" and "default", indicating the name and default value
    180   *                of the pref, respectively.
    181   * @param handler If specified, function that, for a given pref, returns the
    182   *                actual event handler as an object or an array of objects.
    183   *                If handler is not specified, the actual event handler is
    184   *                specified using the scope and name pair.
    185   * @param scope   See handler.
    186   * @param name    See handler.
    187   * @param once    If true, only observe the specified prefs once.
    188   */
    189  addLazyPrefObserver(aPrefs, { handler, scope, name, once }) {
    190    this._addLazyListeners(
    191      aPrefs,
    192      handler,
    193      scope,
    194      name,
    195      (prefs, observer) => {
    196        prefs.forEach(pref => Services.prefs.addObserver(pref.name, observer));
    197        prefs.forEach(pref => {
    198          if (pref.default === undefined) {
    199            return;
    200          }
    201          let value;
    202          switch (typeof pref.default) {
    203            case "string":
    204              value = Services.prefs.getCharPref(pref.name, pref.default);
    205              break;
    206            case "number":
    207              value = Services.prefs.getIntPref(pref.name, pref.default);
    208              break;
    209            case "boolean":
    210              value = Services.prefs.getBoolPref(pref.name, pref.default);
    211              break;
    212          }
    213          if (pref.default !== value) {
    214            // Notify observer if value already changed from default.
    215            observer(Services.prefs, "nsPref:changed", pref.name);
    216          }
    217        });
    218      },
    219      (handlers, observer, args) => {
    220        if (!once) {
    221          Services.prefs.removeObserver(args[2], observer);
    222          handlers.forEach(() => Services.prefs.addObserver(args[2], observer));
    223        }
    224        handlers.forEach(handler => handler.observe(...args));
    225      }
    226    );
    227  },
    228 
    229  getRootDocShell(aWin) {
    230    if (!aWin) {
    231      return null;
    232    }
    233    let docShell;
    234    try {
    235      docShell = aWin.QueryInterface(Ci.nsIDocShell);
    236    } catch (e) {
    237      docShell = aWin.docShell;
    238    }
    239    return docShell.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor);
    240  },
    241 
    242  /**
    243   * Return the outermost chrome DOM window (the XUL window) for a given DOM
    244   * window, in the parent process.
    245   *
    246   * @param aWin a DOM window.
    247   */
    248  getChromeWindow(aWin) {
    249    const docShell = this.getRootDocShell(aWin);
    250    return docShell && docShell.domWindow;
    251  },
    252 
    253  /**
    254   * Return the content frame message manager (aka the frame script global
    255   * object) for a given DOM window, in a child process.
    256   *
    257   * @param aWin a DOM window.
    258   */
    259  getContentFrameMessageManager(aWin) {
    260    const docShell = this.getRootDocShell(aWin);
    261    return docShell && docShell.getInterface(Ci.nsIBrowserChild).messageManager;
    262  },
    263 
    264  /**
    265   * Return the per-nsWindow EventDispatcher for a given DOM window, in either
    266   * the parent process or a child process.
    267   *
    268   * @param aWin a DOM window.
    269   */
    270  getDispatcherForWindow(aWin) {
    271    try {
    272      if (!this.IS_PARENT_PROCESS) {
    273        const mm = this.getContentFrameMessageManager(aWin.top || aWin);
    274        return mm && lazy.EventDispatcher.forMessageManager(mm);
    275      }
    276      const win = this.getChromeWindow(aWin.top || aWin);
    277      if (!win.closed) {
    278        return win.WindowEventDispatcher || lazy.EventDispatcher.for(win);
    279      }
    280    } catch (e) {}
    281    return null;
    282  },
    283 
    284  /**
    285   * Return promise for waiting for finishing PanZoomState.
    286   *
    287   * @param aWindow a DOM window.
    288   * @return promise
    289   */
    290  waitForPanZoomState(aWindow) {
    291    return new Promise((resolve, reject) => {
    292      if (
    293        !aWindow?.windowUtils.asyncPanZoomEnabled ||
    294        !Services.prefs.getBoolPref("apz.zoom-to-focused-input.enabled")
    295      ) {
    296        // No zoomToFocusedInput.
    297        resolve();
    298        return;
    299      }
    300 
    301      let timerId = 0;
    302 
    303      const panZoomState = (aSubject, aTopic, aData) => {
    304        if (timerId != 0) {
    305          // aWindow may be dead object now.
    306          try {
    307            lazy.clearTimeout(timerId);
    308          } catch (e) {}
    309          timerId = 0;
    310        }
    311 
    312        if (aData === "NOTHING") {
    313          Services.obs.removeObserver(panZoomState, "PanZoom:StateChange");
    314          resolve();
    315        }
    316      };
    317 
    318      Services.obs.addObserver(panZoomState, "PanZoom:StateChange");
    319 
    320      // "GeckoView:ZoomToInput" has the timeout as 500ms when window isn't
    321      // resized (it means on-screen-keyboard is already shown).
    322      // So after up to 500ms, APZ event is sent. So we need to wait for more
    323      // 500ms.
    324      timerId = lazy.setTimeout(() => {
    325        // PanZoom state isn't changed. zoomToFocusedInput will return error.
    326        Services.obs.removeObserver(panZoomState, "PanZoom:StateChange");
    327        reject();
    328      }, 600);
    329    });
    330  },
    331 
    332  /**
    333   * Add logging functions to the specified scope that forward to the given
    334   * Log.sys.mjs logger. Currently "debug" and "warn" functions are supported. To
    335   * log something, call the function through a template literal:
    336   *
    337   *   function foo(bar, baz) {
    338   *     debug `hello world`;
    339   *     debug `foo called with ${bar} as bar`;
    340   *     warn `this is a warning for ${baz}`;
    341   *   }
    342   *
    343   * An inline format can also be used for logging:
    344   *
    345   *   let bar = 42;
    346   *   do_something(bar); // No log.
    347   *   do_something(debug.foo = bar); // Output "foo = 42" to the log.
    348   *
    349   * @param aTag Name of the Log.sys.mjs logger to forward logs to.
    350   * @param aScope Scope to add the logging functions to.
    351   */
    352  initLogging(aTag, aScope) {
    353    aScope = aScope || {};
    354    const tag = "GeckoView." + aTag.replace(/^GeckoView\.?/, "");
    355 
    356    // Only provide two levels for simplicity.
    357    // For "info", use "debug" instead.
    358    // For "error", throw an actual JS error instead.
    359    for (const level of ["DEBUG", "WARN"]) {
    360      const log = (strings, ...exprs) =>
    361        this._log(log.logger, level, strings, exprs);
    362 
    363      ChromeUtils.defineLazyGetter(log, "logger", _ => {
    364        const logger = Log.repository.getLogger(tag);
    365        logger.parent = this.rootLogger;
    366        return logger;
    367      });
    368 
    369      aScope[level.toLowerCase()] = new Proxy(log, {
    370        set: (obj, prop, value) => obj([prop + " = ", ""], value) || true,
    371      });
    372    }
    373    return aScope;
    374  },
    375 
    376  get rootLogger() {
    377    if (!this._rootLogger) {
    378      this._rootLogger = Log.repository.getLogger("GeckoView");
    379      // On Android, we'll log to the native android logcat output using
    380      // __android_log_write. On iOS, fall back to a dump appender.
    381      if (AppConstants.platform == "android") {
    382        this._rootLogger.addAppender(new lazy.AndroidAppender());
    383      } else {
    384        this._rootLogger.addAppender(new Log.DumpAppender());
    385      }
    386      this._rootLogger.manageLevelFromPref("geckoview.logging");
    387    }
    388    return this._rootLogger;
    389  },
    390 
    391  _log(aLogger, aLevel, aStrings, aExprs) {
    392    if (!Array.isArray(aStrings)) {
    393      const [, file, line] = new Error().stack.match(/.*\n.*\n.*@(.*):(\d+):/);
    394      throw Error(
    395        `Expecting template literal: ${aLevel} \`foo \${bar}\``,
    396        file,
    397        +line
    398      );
    399    }
    400 
    401    if (aLogger.level > Log.Level.Numbers[aLevel]) {
    402      // Log disabled.
    403      return;
    404    }
    405 
    406    // Do some GeckoView-specific formatting:
    407    // * Remove newlines so long log lines can be put into multiple lines:
    408    //   debug `foo=${foo}
    409    //          bar=${bar}`;
    410    const strs = Array.from(aStrings);
    411    const regex = /\n\s*/g;
    412    for (let i = 0; i < strs.length; i++) {
    413      strs[i] = strs[i].replace(regex, " ");
    414    }
    415 
    416    // * Heuristically format flags as hex.
    417    // * Heuristically format nsresult as string name or hex.
    418    for (let i = 0; i < aExprs.length; i++) {
    419      const expr = aExprs[i];
    420      switch (typeof expr) {
    421        case "number":
    422          if (expr > 0 && /\ba?[fF]lags?[\s=:]+$/.test(strs[i])) {
    423            // Likely a flag; display in hex.
    424            aExprs[i] = `0x${expr.toString(0x10)}`;
    425          } else if (expr >= 0 && /\b(a?[sS]tatus|rv)[\s=:]+$/.test(strs[i])) {
    426            // Likely an nsresult; display in name or hex.
    427            aExprs[i] = `0x${expr.toString(0x10)}`;
    428            for (const name in Cr) {
    429              if (expr === Cr[name]) {
    430                aExprs[i] = name;
    431                break;
    432              }
    433            }
    434          }
    435          break;
    436      }
    437    }
    438 
    439    aLogger[aLevel.toLowerCase()](strs, ...aExprs);
    440  },
    441 
    442  /**
    443   * Checks whether the principal is supported for permissions.
    444   *
    445   * @param {nsIPrincipal} principal
    446   *        The principal to check.
    447   *
    448   * @return {boolean} if the principal is supported.
    449   */
    450  isSupportedPermissionsPrincipal(principal) {
    451    if (!principal) {
    452      return false;
    453    }
    454    if (!(principal instanceof Ci.nsIPrincipal)) {
    455      throw new Error(
    456        "Argument passed as principal is not an instance of Ci.nsIPrincipal"
    457      );
    458    }
    459    return this.isSupportedPermissionsScheme(principal.scheme);
    460  },
    461 
    462  /**
    463   * Checks whether we support managing permissions for a specific scheme.
    464   *
    465   * @param {string} scheme - Scheme to test.
    466   * @returns {boolean} Whether the scheme is supported.
    467   */
    468  isSupportedPermissionsScheme(scheme) {
    469    return ["http", "https", "moz-extension", "file"].includes(scheme);
    470  },
    471 
    472  /**
    473   * Attach nsIOpenWindowInfo when opening GeckoSession
    474   *
    475   * @param {string} aSessionId A session id
    476   * @param {nsIOpenWindowInfo} aOpenWindowInfo Attached nsIOpendWindowInfo
    477   * @param {string} aName A window name
    478   * @returns {Promise} resolved when nsIOpenWindowInfo is attached
    479   */
    480  waitAndSetupWindow(aSessionId, aOpenWindowInfo, aName) {
    481    if (!aSessionId) {
    482      return Promise.reject();
    483    }
    484 
    485    return new Promise((resolve, reject) => {
    486      const handler = {
    487        observe(aSubject, aTopic) {
    488          if (
    489            aTopic === "geckoview-window-created" &&
    490            aSubject.name === aSessionId
    491          ) {
    492            // This value will be read by nsFrameLoader while it is being initialized.
    493            aSubject.browser.openWindowInfo = aOpenWindowInfo;
    494 
    495            // Gecko will use this attribute to set the name of the opened window.
    496            if (aName) {
    497              aSubject.browser.setAttribute("name", aName);
    498            }
    499 
    500            if (
    501              !aOpenWindowInfo.isRemote &&
    502              aSubject.browser.hasAttribute("remote")
    503            ) {
    504              // We cannot start in remote mode when we have an opener.
    505              aSubject.browser.setAttribute("remote", "false");
    506              aSubject.browser.removeAttribute("remoteType");
    507            }
    508            Services.obs.removeObserver(handler, "geckoview-window-created");
    509            if (!aSubject) {
    510              reject();
    511              return;
    512            }
    513            resolve(aSubject);
    514          }
    515        },
    516      };
    517 
    518      // This event is emitted from createBrowser() in geckoview.js
    519      Services.obs.addObserver(handler, "geckoview-window-created");
    520    });
    521  },
    522 };
    523 
    524 ChromeUtils.defineLazyGetter(
    525  GeckoViewUtils,
    526  "IS_PARENT_PROCESS",
    527  _ => Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT
    528 );