tor-browser

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

ext-windows.js (19394B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  HomePage: "resource:///modules/HomePage.sys.mjs",
     11  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     12 });
     13 
     14 var { ExtensionError, promiseObserved } = ExtensionUtils;
     15 
     16 function sanitizePositionParams(params, window = null, positionOffset = 0) {
     17  if (params.left === null && params.top === null) {
     18    return;
     19  }
     20 
     21  if (params.left === null) {
     22    const baseLeft = window ? window.screenX : 0;
     23    params.left = baseLeft + positionOffset;
     24  }
     25  if (params.top === null) {
     26    const baseTop = window ? window.screenY : 0;
     27    params.top = baseTop + positionOffset;
     28  }
     29 
     30  // boundary check: don't put window out of visible area
     31  const baseWidth = window ? window.outerWidth : 0;
     32  const baseHeight = window ? window.outerHeight : 0;
     33  // Secure minimum size of an window should be same to the one
     34  // defined at nsGlobalWindowOuter::CheckSecurityWidthAndHeight.
     35  const minWidth = 100;
     36  const minHeight = 100;
     37  const width = Math.max(
     38    minWidth,
     39    params.width !== null ? params.width : baseWidth
     40  );
     41  const height = Math.max(
     42    minHeight,
     43    params.height !== null ? params.height : baseHeight
     44  );
     45  const screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
     46    Ci.nsIScreenManager
     47  );
     48  const screen = screenManager.screenForRect(
     49    params.left,
     50    params.top,
     51    width,
     52    height
     53  );
     54  const availDeviceLeft = {};
     55  const availDeviceTop = {};
     56  const availDeviceWidth = {};
     57  const availDeviceHeight = {};
     58  screen.GetAvailRect(
     59    availDeviceLeft,
     60    availDeviceTop,
     61    availDeviceWidth,
     62    availDeviceHeight
     63  );
     64  const slopX = window?.screenEdgeSlopX || 0;
     65  const slopY = window?.screenEdgeSlopY || 0;
     66  const factor = screen.defaultCSSScaleFactor;
     67  const availLeft = Math.floor(availDeviceLeft.value / factor) - slopX;
     68  const availTop = Math.floor(availDeviceTop.value / factor) - slopY;
     69  const availWidth = Math.floor(availDeviceWidth.value / factor) + slopX;
     70  const availHeight = Math.floor(availDeviceHeight.value / factor) + slopY;
     71  params.left = Math.min(
     72    availLeft + availWidth - width,
     73    Math.max(availLeft, params.left)
     74  );
     75  params.top = Math.min(
     76    availTop + availHeight - height,
     77    Math.max(availTop, params.top)
     78  );
     79 }
     80 
     81 this.windows = class extends ExtensionAPIPersistent {
     82  windowEventRegistrar(event, listener) {
     83    let { extension } = this;
     84    return ({ fire }) => {
     85      let listener2 = (window, ...args) => {
     86        if (extension.canAccessWindow(window)) {
     87          listener(fire, window, ...args);
     88        }
     89      };
     90 
     91      windowTracker.addListener(event, listener2);
     92      return {
     93        unregister() {
     94          windowTracker.removeListener(event, listener2);
     95        },
     96        convert(_fire) {
     97          fire = _fire;
     98        },
     99      };
    100    };
    101  }
    102 
    103  PERSISTENT_EVENTS = {
    104    onCreated: this.windowEventRegistrar("domwindowopened", (fire, window) => {
    105      fire.async(this.extension.windowManager.convert(window));
    106    }),
    107    onRemoved: this.windowEventRegistrar("domwindowclosed", (fire, window) => {
    108      fire.async(windowTracker.getId(window));
    109    }),
    110    onFocusChanged({ fire }) {
    111      let { extension } = this;
    112      // Keep track of the last windowId used to fire an onFocusChanged event
    113      let lastOnFocusChangedWindowId;
    114 
    115      let listener = () => {
    116        // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
    117        // event when switching focus between two Firefox windows.
    118        Promise.resolve().then(() => {
    119          let windowId = Window.WINDOW_ID_NONE;
    120          let window = Services.focus.activeWindow;
    121          if (window && extension.canAccessWindow(window)) {
    122            windowId = windowTracker.getId(window);
    123          }
    124          if (windowId !== lastOnFocusChangedWindowId) {
    125            fire.async(windowId);
    126            lastOnFocusChangedWindowId = windowId;
    127          }
    128        });
    129      };
    130      windowTracker.addListener("focus", listener);
    131      windowTracker.addListener("blur", listener);
    132      return {
    133        unregister() {
    134          windowTracker.removeListener("focus", listener);
    135          windowTracker.removeListener("blur", listener);
    136        },
    137        convert(_fire) {
    138          fire = _fire;
    139        },
    140      };
    141    },
    142  };
    143 
    144  getAPI(context) {
    145    let { extension } = context;
    146 
    147    const { windowManager } = extension;
    148 
    149    return {
    150      windows: {
    151        onCreated: new EventManager({
    152          context,
    153          module: "windows",
    154          event: "onCreated",
    155          extensionApi: this,
    156        }).api(),
    157 
    158        onRemoved: new EventManager({
    159          context,
    160          module: "windows",
    161          event: "onRemoved",
    162          extensionApi: this,
    163        }).api(),
    164 
    165        onFocusChanged: new EventManager({
    166          context,
    167          module: "windows",
    168          event: "onFocusChanged",
    169          extensionApi: this,
    170        }).api(),
    171 
    172        get: function (windowId, getInfo) {
    173          let window = windowTracker.getWindow(windowId, context);
    174          if (!window || !context.canAccessWindow(window)) {
    175            return Promise.reject({
    176              message: `Invalid window ID: ${windowId}`,
    177            });
    178          }
    179          return Promise.resolve(windowManager.convert(window, getInfo));
    180        },
    181 
    182        getCurrent: function (getInfo) {
    183          let window = context.currentWindow || windowTracker.topWindow;
    184          if (!context.canAccessWindow(window)) {
    185            return Promise.reject({ message: `Invalid window` });
    186          }
    187          return Promise.resolve(windowManager.convert(window, getInfo));
    188        },
    189 
    190        getLastFocused: function (getInfo) {
    191          let window = windowTracker.topWindow;
    192          if (!context.canAccessWindow(window)) {
    193            return Promise.reject({ message: `Invalid window` });
    194          }
    195          return Promise.resolve(windowManager.convert(window, getInfo));
    196        },
    197 
    198        getAll: function (getInfo) {
    199          let doNotCheckTypes =
    200            getInfo === null || getInfo.windowTypes === null;
    201          let windows = [];
    202          // incognito access is checked in getAll
    203          for (let win of windowManager.getAll()) {
    204            if (doNotCheckTypes || getInfo.windowTypes.includes(win.type)) {
    205              windows.push(win.convert(getInfo));
    206            }
    207          }
    208          return windows;
    209        },
    210 
    211        create: async function (createData) {
    212          let needResize =
    213            createData.left !== null ||
    214            createData.top !== null ||
    215            createData.width !== null ||
    216            createData.height !== null;
    217          if (createData.incognito && !context.privateBrowsingAllowed) {
    218            throw new ExtensionError(
    219              "Extension does not have permission for incognito mode"
    220            );
    221          }
    222 
    223          if (needResize) {
    224            if (createData.state !== null && createData.state != "normal") {
    225              throw new ExtensionError(
    226                `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"`
    227              );
    228            }
    229            createData.state = "normal";
    230          }
    231 
    232          function mkstr(s) {
    233            let result = Cc["@mozilla.org/supports-string;1"].createInstance(
    234              Ci.nsISupportsString
    235            );
    236            result.data = s;
    237            return result;
    238          }
    239 
    240          let args = Cc["@mozilla.org/array;1"].createInstance(
    241            Ci.nsIMutableArray
    242          );
    243 
    244          // Whether there is only one URL to load, and it is a moz-extension:-URL.
    245          let isOnlyMozExtensionUrl = false;
    246 
    247          // Creating a new window allows one single triggering principal for all tabs that
    248          // are created in the window.  Due to that, if we need a browser principal to load
    249          // some urls, we fallback to using a content principal like we do in the tabs api.
    250          // Throws if url is an array and any url can't be loaded by the extension principal.
    251          let principal = context.principal;
    252          function setContentTriggeringPrincipal(url) {
    253            principal = Services.scriptSecurityManager.createContentPrincipal(
    254              Services.io.newURI(url),
    255              {
    256                // Note: privateBrowsingAllowed was already checked before.
    257                privateBrowsingId: createData.incognito ? 1 : 0,
    258              }
    259            );
    260          }
    261 
    262          if (createData.tabId !== null) {
    263            if (createData.url !== null) {
    264              throw new ExtensionError(
    265                "`tabId` may not be used in conjunction with `url`"
    266              );
    267            }
    268 
    269            if (createData.allowScriptsToClose) {
    270              throw new ExtensionError(
    271                "`tabId` may not be used in conjunction with `allowScriptsToClose`"
    272              );
    273            }
    274 
    275            let tab = tabTracker.getTab(createData.tabId);
    276            if (!context.canAccessWindow(tab.ownerGlobal)) {
    277              throw new ExtensionError(`Invalid tab ID: ${createData.tabId}`);
    278            }
    279            // Private browsing tabs can only be moved to private browsing
    280            // windows.
    281            let incognito = PrivateBrowsingUtils.isBrowserPrivate(
    282              tab.linkedBrowser
    283            );
    284            if (
    285              createData.incognito !== null &&
    286              createData.incognito != incognito
    287            ) {
    288              throw new ExtensionError(
    289                "`incognito` property must match the incognito state of tab"
    290              );
    291            }
    292            createData.incognito = incognito;
    293 
    294            if (
    295              createData.cookieStoreId &&
    296              createData.cookieStoreId !==
    297                getCookieStoreIdForTab(createData, tab)
    298            ) {
    299              throw new ExtensionError(
    300                "`cookieStoreId` must match the tab's cookieStoreId"
    301              );
    302            }
    303 
    304            args.appendElement(tab);
    305          } else if (createData.url !== null) {
    306            if (Array.isArray(createData.url)) {
    307              let array = Cc["@mozilla.org/array;1"].createInstance(
    308                Ci.nsIMutableArray
    309              );
    310              for (let url of createData.url.map(u => context.uri.resolve(u))) {
    311                // We can only provide a single triggering principal when
    312                // opening a window, so if the extension cannot normally
    313                // access a url, we fail.  This includes about and moz-ext
    314                // urls.
    315                if (!context.checkLoadURL(url, { dontReportErrors: true })) {
    316                  return Promise.reject({ message: `Illegal URL: ${url}` });
    317                }
    318                array.appendElement(mkstr(url));
    319              }
    320              args.appendElement(array);
    321              // TODO bug 1780583: support multiple triggeringPrincipals to
    322              // avoid having to use the system principal here.
    323              principal = Services.scriptSecurityManager.getSystemPrincipal();
    324            } else {
    325              let url = context.uri.resolve(createData.url);
    326              args.appendElement(mkstr(url));
    327              isOnlyMozExtensionUrl = ExtensionUtils.isExtensionUrl(url);
    328              if (!context.checkLoadURL(url, { dontReportErrors: true })) {
    329                if (isOnlyMozExtensionUrl) {
    330                  // For backwards-compatibility (also in tabs APIs), we allow
    331                  // extensions to open other moz-extension:-URLs even if that
    332                  // other resource is not listed in web_accessible_resources.
    333                  setContentTriggeringPrincipal(url);
    334                } else {
    335                  throw new ExtensionError(`Illegal URL: ${url}`);
    336                }
    337              }
    338            }
    339          } else {
    340            let url =
    341              createData.incognito &&
    342              !PrivateBrowsingUtils.permanentPrivateBrowsing
    343                ? "about:privatebrowsing"
    344                : HomePage.get().split("|", 1)[0];
    345            args.appendElement(mkstr(url));
    346            isOnlyMozExtensionUrl = ExtensionUtils.isExtensionUrl(url);
    347 
    348            if (!context.checkLoadURL(url, { dontReportErrors: true })) {
    349              // The extension principal cannot directly load about:-URLs,
    350              // except for about:blank, or other moz-extension:-URLs that are
    351              // not in web_accessible_resources. Ensure any page set as a home
    352              // page will load by using a content principal.
    353              setContentTriggeringPrincipal(url);
    354            }
    355          }
    356 
    357          args.appendElement(null); // extraOptions
    358          args.appendElement(null); // referrerInfo
    359          args.appendElement(null); // postData
    360          args.appendElement(null); // allowThirdPartyFixup
    361 
    362          if (createData.cookieStoreId) {
    363            let userContextIdSupports = Cc[
    364              "@mozilla.org/supports-PRUint32;1"
    365            ].createInstance(Ci.nsISupportsPRUint32);
    366            // May throw if validation fails.
    367            userContextIdSupports.data = getUserContextIdForCookieStoreId(
    368              extension,
    369              createData.cookieStoreId,
    370              createData.incognito
    371            );
    372 
    373            args.appendElement(userContextIdSupports); // userContextId
    374          } else {
    375            args.appendElement(null);
    376          }
    377 
    378          args.appendElement(context.principal); // originPrincipal - not important.
    379          args.appendElement(context.principal); // originStoragePrincipal - not important.
    380          args.appendElement(principal); // triggeringPrincipal
    381          args.appendElement(
    382            Cc["@mozilla.org/supports-PRBool;1"].createInstance(
    383              Ci.nsISupportsPRBool
    384            )
    385          ); // allowInheritPrincipal
    386          // There is no CSP associated with this extension, hence we explicitly pass null as the CSP argument.
    387          args.appendElement(null); // csp
    388 
    389          let features = ["chrome"];
    390 
    391          if (createData.type === null || createData.type == "normal") {
    392            features.push("dialog=no", "all");
    393          } else {
    394            // All other types create "popup"-type windows by default.
    395            features.push(
    396              "dialog",
    397              "resizable",
    398              "minimizable",
    399              "titlebar",
    400              "close"
    401            );
    402            if (createData.left === null && createData.top === null) {
    403              features.push("centerscreen");
    404            }
    405          }
    406 
    407          if (createData.incognito !== null) {
    408            if (createData.incognito) {
    409              if (!PrivateBrowsingUtils.enabled) {
    410                throw new ExtensionError(
    411                  "`incognito` cannot be used if incognito mode is disabled"
    412                );
    413              }
    414              features.push("private");
    415            } else {
    416              features.push("non-private");
    417            }
    418          }
    419 
    420          const baseWindow = windowTracker.getTopNormalWindow(context);
    421          // 10px offset is same to Chromium
    422          sanitizePositionParams(createData, baseWindow, 10);
    423 
    424          if (createData.width !== null) {
    425            features.push("outerWidth=" + createData.width);
    426          }
    427          if (createData.height !== null) {
    428            features.push("outerHeight=" + createData.height);
    429          }
    430          if (createData.left !== null) {
    431            features.push("left=" + createData.left);
    432          }
    433          if (createData.top !== null) {
    434            features.push("top=" + createData.top);
    435          }
    436 
    437          let window = Services.ww.openWindow(
    438            null,
    439            AppConstants.BROWSER_CHROME_URL,
    440            "_blank",
    441            features.join(","),
    442            args
    443          );
    444 
    445          let win = windowManager.getWrapper(window);
    446 
    447          // TODO: focused, type
    448 
    449          const contentLoaded = new Promise(resolve => {
    450            window.addEventListener(
    451              "DOMContentLoaded",
    452              function () {
    453                let { allowScriptsToClose } = createData;
    454                if (allowScriptsToClose === null && isOnlyMozExtensionUrl) {
    455                  allowScriptsToClose = true;
    456                }
    457                if (allowScriptsToClose) {
    458                  window.gBrowserAllowScriptsToCloseInitialTabs = true;
    459                }
    460                resolve();
    461              },
    462              { once: true }
    463            );
    464          });
    465 
    466          const startupFinished = promiseObserved(
    467            "browser-delayed-startup-finished",
    468            win => win == window
    469          );
    470 
    471          await contentLoaded;
    472          await startupFinished;
    473 
    474          if (
    475            [
    476              "minimized",
    477              "fullscreen",
    478              "docked",
    479              "normal",
    480              "maximized",
    481            ].includes(createData.state)
    482          ) {
    483            await win.setState(createData.state);
    484          }
    485 
    486          if (createData.titlePreface !== null) {
    487            win.setTitlePreface(createData.titlePreface);
    488          }
    489          return win.convert({ populate: true });
    490        },
    491 
    492        update: async function (windowId, updateInfo) {
    493          if (updateInfo.state !== null && updateInfo.state != "normal") {
    494            if (
    495              updateInfo.left !== null ||
    496              updateInfo.top !== null ||
    497              updateInfo.width !== null ||
    498              updateInfo.height !== null
    499            ) {
    500              throw new ExtensionError(
    501                `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`
    502              );
    503            }
    504          }
    505 
    506          let win = windowManager.get(windowId, context);
    507          if (!win) {
    508            throw new ExtensionError(`Invalid window ID: ${windowId}`);
    509          }
    510          if (updateInfo.focused) {
    511            win.window.focus();
    512          }
    513 
    514          if (updateInfo.state !== null) {
    515            await win.setState(updateInfo.state);
    516          }
    517 
    518          if (updateInfo.drawAttention) {
    519            // Bug 1257497 - Firefox can't cancel attention actions.
    520            win.window.getAttention();
    521          }
    522 
    523          sanitizePositionParams(updateInfo, win.window);
    524          win.updateGeometry(updateInfo);
    525 
    526          if (updateInfo.titlePreface !== null) {
    527            win.setTitlePreface(updateInfo.titlePreface);
    528            win.window.gBrowser.updateTitlebar();
    529          }
    530 
    531          // TODO: All the other properties, focused=false...
    532 
    533          return win.convert();
    534        },
    535 
    536        remove: function (windowId) {
    537          let window = windowTracker.getWindow(windowId, context);
    538          if (!context.canAccessWindow(window)) {
    539            return Promise.reject({
    540              message: `Invalid window ID: ${windowId}`,
    541            });
    542          }
    543          window.close();
    544 
    545          return new Promise(resolve => {
    546            let listener = () => {
    547              windowTracker.removeListener("domwindowclosed", listener);
    548              resolve();
    549            };
    550            windowTracker.addListener("domwindowclosed", listener);
    551          });
    552        },
    553      },
    554    };
    555  }
    556 };