tor-browser

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

head.js (18991B)


      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 const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
      6 
      7 const TAB_STATE_NEEDS_RESTORE = 1;
      8 const TAB_STATE_RESTORING = 2;
      9 
     10 const ROOT = getRootDirectory(gTestPath);
     11 const HTTPROOT = ROOT.replace(
     12  "chrome://mochitests/content/",
     13  "http://example.com/"
     14 );
     15 const HTTPSROOT = ROOT.replace(
     16  "chrome://mochitests/content/",
     17  "https://example.com/"
     18 );
     19 
     20 const { SessionSaver } = ChromeUtils.importESModule(
     21  "resource:///modules/sessionstore/SessionSaver.sys.mjs"
     22 );
     23 const { SessionFile } = ChromeUtils.importESModule(
     24  "resource:///modules/sessionstore/SessionFile.sys.mjs"
     25 );
     26 const { TabState } = ChromeUtils.importESModule(
     27  "resource:///modules/sessionstore/TabState.sys.mjs"
     28 );
     29 const { TabStateFlusher } = ChromeUtils.importESModule(
     30  "resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
     31 );
     32 const { SessionStoreTestUtils } = ChromeUtils.importESModule(
     33  "resource://testing-common/SessionStoreTestUtils.sys.mjs"
     34 );
     35 
     36 const { PageWireframes } = ChromeUtils.importESModule(
     37  "resource:///modules/sessionstore/PageWireframes.sys.mjs"
     38 );
     39 
     40 const ss = SessionStore;
     41 SessionStoreTestUtils.init(this, window);
     42 
     43 // Some tests here assume that all restored tabs are loaded without waiting for
     44 // the user to bring them to the foreground. We ensure this by resetting the
     45 // related preference (see the "firefox.js" defaults file for details).
     46 Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
     47 registerCleanupFunction(function () {
     48  Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
     49 });
     50 
     51 // Obtain access to internals
     52 Services.prefs.setBoolPref("browser.sessionstore.debug", true);
     53 registerCleanupFunction(function () {
     54  Services.prefs.clearUserPref("browser.sessionstore.debug");
     55 });
     56 
     57 // This kicks off the search service used on about:home and allows the
     58 // session restore tests to be run standalone without triggering errors.
     59 Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs;
     60 
     61 function provideWindow(aCallback, aURL, aFeatures) {
     62  function callbackSoon(aWindow) {
     63    executeSoon(function executeCallbackSoon() {
     64      aCallback(aWindow);
     65    });
     66  }
     67 
     68  let win = openDialog(
     69    AppConstants.BROWSER_CHROME_URL,
     70    "",
     71    aFeatures || "chrome,all,dialog=no",
     72    aURL || "about:blank"
     73  );
     74  whenWindowLoaded(win, function onWindowLoaded(aWin) {
     75    if (!aURL) {
     76      info("Loaded a blank window.");
     77      callbackSoon(aWin);
     78      return;
     79    }
     80 
     81    aWin.gBrowser.selectedBrowser.addEventListener(
     82      "load",
     83      function () {
     84        callbackSoon(aWin);
     85      },
     86      { capture: true, once: true }
     87    );
     88  });
     89 }
     90 
     91 // This assumes that tests will at least have some state/entries
     92 function waitForBrowserState(aState, aSetStateCallback) {
     93  return SessionStoreTestUtils.waitForBrowserState(aState, aSetStateCallback);
     94 }
     95 
     96 function promiseBrowserState(aState) {
     97  return SessionStoreTestUtils.promiseBrowserState(aState);
     98 }
     99 
    100 function promiseTabState(tab, state) {
    101  if (typeof state != "string") {
    102    state = JSON.stringify(state);
    103  }
    104 
    105  let promise = promiseTabRestored(tab);
    106  ss.setTabState(tab, state);
    107  return promise;
    108 }
    109 
    110 function promiseWindowRestoring(win) {
    111  return new Promise(resolve =>
    112    win.addEventListener("SSWindowRestoring", resolve, { once: true })
    113  );
    114 }
    115 
    116 function promiseWindowRestored(win) {
    117  return new Promise(resolve =>
    118    win.addEventListener("SSWindowRestored", resolve, { once: true })
    119  );
    120 }
    121 
    122 async function setBrowserState(state, win = window) {
    123  ss.setBrowserState(typeof state != "string" ? JSON.stringify(state) : state);
    124  await promiseWindowRestored(win);
    125 }
    126 
    127 async function setWindowState(win, state, overwrite = false) {
    128  ss.setWindowState(
    129    win,
    130    typeof state != "string" ? JSON.stringify(state) : state,
    131    overwrite
    132  );
    133  await promiseWindowRestored(win);
    134 }
    135 
    136 function waitForTopic(aTopic, aTimeout, aCallback) {
    137  let observing = false;
    138  function removeObserver() {
    139    if (!observing) {
    140      return;
    141    }
    142    Services.obs.removeObserver(observer, aTopic);
    143    observing = false;
    144  }
    145 
    146  let timeout = setTimeout(function () {
    147    removeObserver();
    148    aCallback(false);
    149  }, aTimeout);
    150 
    151  function observer() {
    152    removeObserver();
    153    timeout = clearTimeout(timeout);
    154    executeSoon(() => aCallback(true));
    155  }
    156 
    157  registerCleanupFunction(function () {
    158    removeObserver();
    159    if (timeout) {
    160      clearTimeout(timeout);
    161    }
    162  });
    163 
    164  observing = true;
    165  Services.obs.addObserver(observer, aTopic);
    166 }
    167 
    168 /**
    169 * Wait until session restore has finished collecting its data and is
    170 * has written that data ("sessionstore-state-write-complete").
    171 *
    172 * @param {function} aCallback If sessionstore-state-write-complete is sent
    173 * within buffering interval + 100 ms, the callback is passed |true|,
    174 * otherwise, it is passed |false|.
    175 */
    176 function waitForSaveState(aCallback) {
    177  let timeout =
    178    100 + Services.prefs.getIntPref("browser.sessionstore.interval");
    179  return waitForTopic("sessionstore-state-write-complete", timeout, aCallback);
    180 }
    181 function promiseSaveState() {
    182  return new Promise((resolve, reject) => {
    183    waitForSaveState(isSuccessful => {
    184      if (!isSuccessful) {
    185        reject(new Error("Save state timeout"));
    186      } else {
    187        resolve();
    188      }
    189    });
    190  });
    191 }
    192 function forceSaveState() {
    193  return SessionSaver.run();
    194 }
    195 
    196 function promiseRecoveryFileContents() {
    197  let promise = forceSaveState();
    198  return promise.then(function () {
    199    return IOUtils.readUTF8(SessionFile.Paths.recovery, {
    200      decompress: true,
    201    });
    202  });
    203 }
    204 
    205 var promiseForEachSessionRestoreFile = async function (cb) {
    206  for (let key of SessionFile.Paths.loadOrder) {
    207    let data = "";
    208    try {
    209      data = await IOUtils.readUTF8(SessionFile.Paths[key], {
    210        decompress: true,
    211      });
    212    } catch (ex) {
    213      // Ignore missing files
    214      if (!(DOMException.isInstance(ex) && ex.name == "NotFoundError")) {
    215        throw ex;
    216      }
    217    }
    218    cb(data, key);
    219  }
    220 };
    221 
    222 function promiseBrowserLoaded(
    223  aBrowser,
    224  ignoreSubFrames = true,
    225  wantLoad = null
    226 ) {
    227  return BrowserTestUtils.browserLoaded(aBrowser, !ignoreSubFrames, wantLoad);
    228 }
    229 
    230 function whenWindowLoaded(aWindow, aCallback) {
    231  aWindow.addEventListener(
    232    "load",
    233    function () {
    234      executeSoon(function executeWhenWindowLoaded() {
    235        aCallback(aWindow);
    236      });
    237    },
    238    { once: true }
    239  );
    240 }
    241 function promiseWindowLoaded(aWindow) {
    242  return new Promise(resolve => whenWindowLoaded(aWindow, resolve));
    243 }
    244 
    245 var gUniqueCounter = 0;
    246 function r() {
    247  return Date.now() + "-" + ++gUniqueCounter;
    248 }
    249 
    250 function* BrowserWindowIterator() {
    251  for (let currentWindow of Services.wm.getEnumerator("navigator:browser")) {
    252    if (!currentWindow.closed) {
    253      yield currentWindow;
    254    }
    255  }
    256 }
    257 
    258 var gWebProgressListener = {
    259  _callback: null,
    260 
    261  setCallback(aCallback) {
    262    if (!this._callback) {
    263      window.gBrowser.addTabsProgressListener(this);
    264    }
    265    this._callback = aCallback;
    266  },
    267 
    268  unsetCallback() {
    269    if (this._callback) {
    270      this._callback = null;
    271      window.gBrowser.removeTabsProgressListener(this);
    272    }
    273  },
    274 
    275  onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, _aStatus) {
    276    if (
    277      aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
    278      aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
    279      aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW
    280    ) {
    281      this._callback(aBrowser);
    282    }
    283  },
    284 };
    285 
    286 registerCleanupFunction(function () {
    287  gWebProgressListener.unsetCallback();
    288 });
    289 
    290 var gProgressListener = {
    291  _callback: null,
    292 
    293  setCallback(callback) {
    294    Services.obs.addObserver(this, "sessionstore-debug-tab-restored");
    295    this._callback = callback;
    296  },
    297 
    298  unsetCallback() {
    299    if (this._callback) {
    300      this._callback = null;
    301      Services.obs.removeObserver(this, "sessionstore-debug-tab-restored");
    302    }
    303  },
    304 
    305  observe(browser) {
    306    gProgressListener.onRestored(browser);
    307  },
    308 
    309  onRestored(browser) {
    310    if (ss.getInternalObjectState(browser) == TAB_STATE_RESTORING) {
    311      let args = [browser].concat(gProgressListener._countTabs());
    312      gProgressListener._callback.apply(gProgressListener, args);
    313    }
    314  },
    315 
    316  _countTabs() {
    317    let needsRestore = 0,
    318      isRestoring = 0,
    319      wasRestored = 0;
    320 
    321    for (let win of BrowserWindowIterator()) {
    322      for (let i = 0; i < win.gBrowser.tabs.length; i++) {
    323        let browser = win.gBrowser.tabs[i].linkedBrowser;
    324        let state = ss.getInternalObjectState(browser);
    325        if (browser.isConnected && !state) {
    326          wasRestored++;
    327        } else if (state == TAB_STATE_RESTORING) {
    328          isRestoring++;
    329        } else if (state == TAB_STATE_NEEDS_RESTORE || !browser.isConnected) {
    330          needsRestore++;
    331        }
    332      }
    333    }
    334    return [needsRestore, isRestoring, wasRestored];
    335  },
    336 };
    337 
    338 registerCleanupFunction(function () {
    339  gProgressListener.unsetCallback();
    340 });
    341 
    342 // Close all but our primary window.
    343 function promiseAllButPrimaryWindowClosed() {
    344  let windows = [];
    345  for (let win of BrowserWindowIterator()) {
    346    if (win != window) {
    347      windows.push(win);
    348    }
    349  }
    350 
    351  return Promise.all(windows.map(BrowserTestUtils.closeWindow));
    352 }
    353 
    354 // Forget all closed windows.
    355 function forgetClosedWindows() {
    356  while (ss.getClosedWindowCount() > 0) {
    357    ss.forgetClosedWindow(0);
    358  }
    359 }
    360 
    361 // Forget all closed tabs for a window
    362 function forgetClosedTabs(win) {
    363  const closedTabCount = ss.getClosedTabCountForWindow(win);
    364  for (let i = 0; i < closedTabCount; i++) {
    365    try {
    366      ss.forgetClosedTab(win, 0);
    367    } catch (err) {
    368      // This will fail if there are tab groups in here
    369    }
    370  }
    371 }
    372 
    373 function forgetSavedTabGroups() {
    374  const tabGroups = ss.getSavedTabGroups();
    375  tabGroups.forEach(tabGroup => ss.forgetSavedTabGroup(tabGroup.id));
    376 }
    377 
    378 function forgetClosedTabGroups(win) {
    379  const tabGroups = ss.getClosedTabGroups(win);
    380  tabGroups.forEach(tabGroup => ss.forgetClosedTabGroup(win, tabGroup.id));
    381 }
    382 
    383 /**
    384 * When opening a new window it is not sufficient to wait for its load event.
    385 * We need to use whenDelayedStartupFinshed() here as the browser window's
    386 * delayedStartup() routine is executed one tick after the window's load event
    387 * has been dispatched. browser-delayed-startup-finished might be deferred even
    388 * further if parts of the window's initialization process take more time than
    389 * expected (e.g. reading a big session state from disk).
    390 */
    391 function whenNewWindowLoaded(aOptions, aCallback) {
    392  let features = "";
    393  let url = "about:blank";
    394 
    395  if ((aOptions && aOptions.private) || false) {
    396    features = ",private";
    397    url = "about:privatebrowsing";
    398  }
    399 
    400  let win = openDialog(
    401    AppConstants.BROWSER_CHROME_URL,
    402    "",
    403    "chrome,all,dialog=no" + features,
    404    url
    405  );
    406  let delayedStartup = promiseDelayedStartupFinished(win);
    407 
    408  let browserLoaded = new Promise(resolve => {
    409    if (url == "about:blank") {
    410      resolve();
    411      return;
    412    }
    413 
    414    win.addEventListener(
    415      "load",
    416      function () {
    417        let browser = win.gBrowser.selectedBrowser;
    418        promiseBrowserLoaded(browser).then(resolve);
    419      },
    420      { once: true }
    421    );
    422  });
    423 
    424  Promise.all([delayedStartup, browserLoaded]).then(() => aCallback(win));
    425 }
    426 function promiseNewWindowLoaded(aOptions) {
    427  return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve));
    428 }
    429 
    430 /**
    431 * This waits for the browser-delayed-startup-finished notification of a given
    432 * window. It indicates that the windows has loaded completely and is ready to
    433 * be used for testing.
    434 */
    435 function whenDelayedStartupFinished(aWindow, aCallback) {
    436  Services.obs.addObserver(function observer(aSubject, aTopic) {
    437    if (aWindow == aSubject) {
    438      Services.obs.removeObserver(observer, aTopic);
    439      executeSoon(aCallback);
    440    }
    441  }, "browser-delayed-startup-finished");
    442 }
    443 function promiseDelayedStartupFinished(aWindow) {
    444  return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve));
    445 }
    446 
    447 function promiseTabRestored(tab) {
    448  return BrowserTestUtils.waitForEvent(tab, "SSTabRestored");
    449 }
    450 
    451 function promiseTabRestoring(tab) {
    452  return BrowserTestUtils.waitForEvent(tab, "SSTabRestoring");
    453 }
    454 
    455 // Removes the given tab immediately and returns a promise that resolves when
    456 // all pending status updates (messages) of the closing tab have been received.
    457 function promiseRemoveTabAndSessionState(tab) {
    458  let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
    459  BrowserTestUtils.removeTab(tab);
    460  return sessionUpdatePromise;
    461 }
    462 
    463 // Write DOMSessionStorage data to the given browser.
    464 function modifySessionStorage(browser, storageData, storageOptions = {}) {
    465  let browsingContext = browser.browsingContext;
    466  if (storageOptions && "frameIndex" in storageOptions) {
    467    browsingContext = browsingContext.children[storageOptions.frameIndex];
    468  }
    469 
    470  return SpecialPowers.spawn(
    471    browsingContext,
    472    [[storageData, storageOptions]],
    473    async function ([data]) {
    474      let frame = content;
    475      let keys = new Set(Object.keys(data));
    476      let isClearing = !keys.size;
    477      let storage = frame.sessionStorage;
    478 
    479      return new Promise(resolve => {
    480        docShell.chromeEventHandler.addEventListener(
    481          "MozSessionStorageChanged",
    482          function onStorageChanged(event) {
    483            if (event.storageArea == storage) {
    484              keys.delete(event.key);
    485            }
    486 
    487            if (keys.size == 0) {
    488              docShell.chromeEventHandler.removeEventListener(
    489                "MozSessionStorageChanged",
    490                onStorageChanged,
    491                true
    492              );
    493              resolve();
    494            }
    495          },
    496          true
    497        );
    498 
    499        if (isClearing) {
    500          storage.clear();
    501        } else {
    502          for (let key of keys) {
    503            frame.sessionStorage[key] = data[key];
    504          }
    505        }
    506      });
    507    }
    508  );
    509 }
    510 
    511 function pushPrefs(...aPrefs) {
    512  return SpecialPowers.pushPrefEnv({ set: aPrefs });
    513 }
    514 
    515 function popPrefs() {
    516  return SpecialPowers.popPrefEnv();
    517 }
    518 
    519 function setScrollPosition(bc, x, y) {
    520  return SpecialPowers.spawn(bc, [x, y], (childX, childY) => {
    521    return new Promise(resolve => {
    522      content.addEventListener(
    523        "mozvisualscroll",
    524        function onScroll(event) {
    525          if (content.document.ownerGlobal.visualViewport == event.target) {
    526            content.removeEventListener("mozvisualscroll", onScroll, {
    527              mozSystemGroup: true,
    528            });
    529            resolve();
    530          }
    531        },
    532        { mozSystemGroup: true }
    533      );
    534      content.scrollTo(childX, childY);
    535    });
    536  });
    537 }
    538 
    539 async function checkScroll(tab, expected, msg) {
    540  let browser = tab.linkedBrowser;
    541  await TabStateFlusher.flush(browser);
    542 
    543  let scroll = JSON.parse(ss.getTabState(tab)).scroll || null;
    544  is(JSON.stringify(scroll), JSON.stringify(expected), msg);
    545 }
    546 
    547 function whenDomWindowClosedHandled(aCallback) {
    548  Services.obs.addObserver(function observer(aSubject, aTopic) {
    549    Services.obs.removeObserver(observer, aTopic);
    550    aCallback();
    551  }, "sessionstore-debug-domwindowclosed-handled");
    552 }
    553 
    554 function getPropertyOfFormField(browserContext, selector, propName) {
    555  return SpecialPowers.spawn(
    556    browserContext,
    557    [selector, propName],
    558    (selectorChild, propNameChild) => {
    559      return content.document.querySelector(selectorChild)[propNameChild];
    560    }
    561  );
    562 }
    563 
    564 function setPropertyOfFormField(browserContext, selector, propName, newValue) {
    565  return SpecialPowers.spawn(
    566    browserContext,
    567    [selector, propName, newValue],
    568    (selectorChild, propNameChild, newValueChild) => {
    569      let node = content.document.querySelector(selectorChild);
    570      node[propNameChild] = newValueChild;
    571 
    572      let event = node.ownerDocument.createEvent("UIEvents");
    573      event.initUIEvent("input", true, true, node.ownerGlobal, 0);
    574      node.dispatchEvent(event);
    575    }
    576  );
    577 }
    578 
    579 function promiseOnHistoryReplaceEntry(browser) {
    580  return new Promise(resolve => {
    581    let sessionHistory = browser.browsingContext?.sessionHistory;
    582    if (sessionHistory) {
    583      var historyListener = {
    584        OnHistoryNewEntry() {},
    585        OnHistoryGotoIndex() {},
    586        OnHistoryPurge() {},
    587        OnHistoryReload() {
    588          return true;
    589        },
    590 
    591        OnHistoryReplaceEntry() {
    592          resolve();
    593        },
    594 
    595        QueryInterface: ChromeUtils.generateQI([
    596          "nsISHistoryListener",
    597          "nsISupportsWeakReference",
    598        ]),
    599      };
    600 
    601      sessionHistory.addSHistoryListener(historyListener);
    602    }
    603  });
    604 }
    605 
    606 function loadTestSubscript(filePath) {
    607  Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
    608 }
    609 
    610 function addCoopTask(aFile, aTest, aUrlRoot) {
    611  async function taskToBeAdded() {
    612    info(`File ${aFile} has COOP headers enabled`);
    613    let filePath = `browser/browser/components/sessionstore/test/${aFile}`;
    614    let url = aUrlRoot + `coopHeaderCommon.sjs?fileRoot=${filePath}`;
    615    await aTest(url);
    616  }
    617  Object.defineProperty(taskToBeAdded, "name", { value: aTest.name });
    618  add_task(taskToBeAdded);
    619 }
    620 
    621 function addNonCoopTask(aFile, aTest, aUrlRoot) {
    622  async function taskToBeAdded() {
    623    await aTest(aUrlRoot + aFile);
    624  }
    625  Object.defineProperty(taskToBeAdded, "name", { value: aTest.name });
    626  add_task(taskToBeAdded);
    627 }
    628 
    629 function openAndCloseTab(window, url) {
    630  return SessionStoreTestUtils.openAndCloseTab(window, url);
    631 }
    632 
    633 /**
    634 * This is regrettable, but when `promiseBrowserState` resolves, we're still
    635 * midway through loading the tabs. To avoid race conditions in URLs for tabs
    636 * being available, wait for all the loads to finish:
    637 */
    638 function promiseSessionStoreLoads(numberOfLoads) {
    639  let loadsSeen = 0;
    640  return new Promise(resolve => {
    641    Services.obs.addObserver(function obs(browser) {
    642      loadsSeen++;
    643      if (loadsSeen == numberOfLoads) {
    644        resolve();
    645      }
    646      // The typeof check is here to avoid one test messing with everything else by
    647      // keeping the observer indefinitely.
    648      if (typeof info == "undefined" || loadsSeen >= numberOfLoads) {
    649        Services.obs.removeObserver(obs, "sessionstore-debug-tab-restored");
    650      }
    651      info("Saw load for " + browser.currentURI.spec);
    652    }, "sessionstore-debug-tab-restored");
    653  });
    654 }
    655 
    656 function triggerClickOn(target, options) {
    657  let promise = BrowserTestUtils.waitForEvent(target, "click");
    658  if (AppConstants.platform == "macosx") {
    659    options.metaKey = options.ctrlKey;
    660    delete options.ctrlKey;
    661  }
    662  EventUtils.synthesizeMouseAtCenter(target, options);
    663  return promise;
    664 }
    665 
    666 async function openTabMenuFor(tab) {
    667  let tabMenu = tab.ownerDocument.getElementById("tabContextMenu");
    668 
    669  let tabMenuShown = BrowserTestUtils.waitForEvent(tabMenu, "popupshown");
    670  EventUtils.synthesizeMouseAtCenter(
    671    tab,
    672    { type: "contextmenu" },
    673    tab.ownerGlobal
    674  );
    675  await tabMenuShown;
    676 
    677  return tabMenu;
    678 }