tor-browser

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

helper_localStorage.js (10196B)


      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 // Simple tab wrapper abstracting our messaging mechanism;
      6 class KnownTab {
      7  constructor(name, tab) {
      8    this.name = name;
      9    this.tab = tab;
     10  }
     11 
     12  cleanup() {
     13    this.tab = null;
     14  }
     15 }
     16 
     17 // Simple data structure class to help us track opened tabs and their pids.
     18 class KnownTabs {
     19  constructor() {
     20    this.byPid = new Map();
     21    this.byName = new Map();
     22  }
     23 
     24  cleanup() {
     25    for (let key of this.byPid.keys()) {
     26      this.byPid[key] = null;
     27    }
     28    this.byPid = null;
     29    this.byName = null;
     30  }
     31 }
     32 
     33 /**
     34 * Open our helper page in a tab in its own content process, asserting that it
     35 * really is in its own process.  We initially load and wait for about:blank to
     36 * load, and only then loadURI to our actual page.  This is to ensure that
     37 * LocalStorageManager has had an opportunity to be created and populate
     38 * mOriginsHavingData.
     39 *
     40 * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of
     41 * the unconditional call to nsGlobalWindow::PreloadLocalStorage.  This will
     42 * reliably create the StorageDBChild instance, and its corresponding
     43 * StorageDBParent will send the set of origins when it is constructed.)
     44 */
     45 async function openTestTab(
     46  helperPageUrl,
     47  name,
     48  knownTabs,
     49  shouldLoadInNewProcess
     50 ) {
     51  let realUrl = helperPageUrl + "?" + encodeURIComponent(name);
     52  // Load and wait for about:blank.
     53  let tab = await BrowserTestUtils.openNewForegroundTab({
     54    gBrowser,
     55    opening: "about:blank",
     56    forceNewProcess: true,
     57  });
     58  ok(!knownTabs.byName.has(name), "tab needs its own name: " + name);
     59 
     60  let knownTab = new KnownTab(name, tab);
     61  knownTabs.byName.set(name, knownTab);
     62 
     63  // Now trigger the actual load of our page.
     64  BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, realUrl);
     65  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
     66 
     67  let pid = tab.linkedBrowser.frameLoader.remoteTab.osPid;
     68  if (shouldLoadInNewProcess) {
     69    ok(
     70      !knownTabs.byPid.has(pid),
     71      "tab should be loaded in new process, pid: " + pid
     72    );
     73  } else {
     74    ok(
     75      knownTabs.byPid.has(pid),
     76      "tab should be loaded in the same process, new pid: " + pid
     77    );
     78  }
     79 
     80  if (knownTabs.byPid.has(pid)) {
     81    knownTabs.byPid.get(pid).set(name, knownTab);
     82  } else {
     83    let pidMap = new Map();
     84    pidMap.set(name, knownTab);
     85    knownTabs.byPid.set(pid, pidMap);
     86  }
     87 
     88  return knownTab;
     89 }
     90 
     91 /**
     92 * Close all the tabs we opened.
     93 */
     94 async function cleanupTabs(knownTabs) {
     95  for (let knownTab of knownTabs.byName.values()) {
     96    BrowserTestUtils.removeTab(knownTab.tab);
     97    knownTab.cleanup();
     98  }
     99  knownTabs.cleanup();
    100 }
    101 
    102 /**
    103 * Wait for a LocalStorage flush to occur.  This notification can occur as a
    104 * result of any of:
    105 * - The normal, hardcoded 5-second flush timer.
    106 * - InsertDBOp seeing a preload op for an origin with outstanding changes.
    107 * - Us generating a "domstorage-test-flush-force" observer notification.
    108 */
    109 function waitForLocalStorageFlush() {
    110  if (Services.domStorageManager.nextGenLocalStorageEnabled) {
    111    return new Promise(resolve => executeSoon(resolve));
    112  }
    113 
    114  return new Promise(function (resolve) {
    115    let observer = {
    116      observe() {
    117        SpecialPowers.removeObserver(observer, "domstorage-test-flushed");
    118        resolve();
    119      },
    120    };
    121    SpecialPowers.addObserver(observer, "domstorage-test-flushed");
    122  });
    123 }
    124 
    125 /**
    126 * Trigger and wait for a flush.  This is only necessary for forcing
    127 * mOriginsHavingData to be updated.  Normal operations exposed to content know
    128 * to automatically flush when necessary for correctness.
    129 *
    130 * The notification we're waiting for to verify flushing is fundamentally
    131 * ambiguous (see waitForLocalStorageFlush), so we actually trigger the flush
    132 * twice and wait twice.  In the event there was a race, there will be 3 flush
    133 * notifications, but correctness is guaranteed after the second notification.
    134 */
    135 function triggerAndWaitForLocalStorageFlush() {
    136  if (Services.domStorageManager.nextGenLocalStorageEnabled) {
    137    return new Promise(resolve => executeSoon(resolve));
    138  }
    139 
    140  SpecialPowers.notifyObservers(null, "domstorage-test-flush-force");
    141  // This first wait is ambiguous...
    142  return waitForLocalStorageFlush().then(function () {
    143    // So issue a second flush and wait for that.
    144    SpecialPowers.notifyObservers(null, "domstorage-test-flush-force");
    145    return waitForLocalStorageFlush();
    146  });
    147 }
    148 
    149 /**
    150 * Clear the origin's storage so that "OriginsHavingData" will return false for
    151 * our origin.  Note that this is only the case for AsyncClear() which is
    152 * explicitly issued against a cache, or AsyncClearAll() which we can trigger
    153 * by wiping all storage.  However, the more targeted domain clearings that
    154 * we can trigger via observer, AsyncClearMatchingOrigin and
    155 * AsyncClearMatchingOriginAttributes will not clear the hashtable entry for
    156 * the origin.
    157 *
    158 * So we explicitly access the cache here in the parent for the origin and issue
    159 * an explicit clear.  Clearing all storage might be a little easier but seems
    160 * like asking for intermittent failures.
    161 */
    162 function clearOriginStorageEnsuringNoPreload(origin) {
    163  let principal =
    164    Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
    165 
    166  if (Services.domStorageManager.nextGenLocalStorageEnabled) {
    167    let request = Services.qms.clearStoragesForClient(
    168      principal,
    169      "ls",
    170      "default"
    171    );
    172    let promise = new Promise(resolve => {
    173      request.callback = () => {
    174        resolve();
    175      };
    176    });
    177    return promise;
    178  }
    179 
    180  // We want to use createStorage to force the cache to be created so we can
    181  // issue the clear.  It's possible for getStorage to return false but for the
    182  // origin preload hash to still have our origin in it.
    183  let storage = Services.domStorageManager.createStorage(
    184    null,
    185    principal,
    186    principal,
    187    ""
    188  );
    189  storage.clear();
    190 
    191  // We also need to trigger a flush os that mOriginsHavingData gets updated.
    192  // The inherent flush race is fine here because
    193  return triggerAndWaitForLocalStorageFlush();
    194 }
    195 
    196 async function verifyTabPreload(knownTab, expectStorageExists, origin) {
    197  let storageExists = await SpecialPowers.spawn(
    198    knownTab.tab.linkedBrowser,
    199    [origin],
    200    function (origin) {
    201      let principal =
    202        Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
    203      if (Services.domStorageManager.nextGenLocalStorageEnabled) {
    204        return Services.domStorageManager.isPreloaded(principal);
    205      }
    206      return !!Services.domStorageManager.getStorage(
    207        null,
    208        principal,
    209        principal
    210      );
    211    }
    212  );
    213  is(storageExists, expectStorageExists, "Storage existence === preload");
    214 }
    215 
    216 /**
    217 * Instruct the given tab to execute the given series of mutations.  For
    218 * simplicity, the mutations representation matches the expected events rep.
    219 */
    220 async function mutateTabStorage(knownTab, mutations, sentinelValue) {
    221  await SpecialPowers.spawn(
    222    knownTab.tab.linkedBrowser,
    223    [{ mutations, sentinelValue }],
    224    function (args) {
    225      return content.wrappedJSObject.mutateStorage(Cu.cloneInto(args, content));
    226    }
    227  );
    228 }
    229 
    230 /**
    231 * Instruct the given tab to add a "storage" event listener and record all
    232 * received events.  verifyTabStorageEvents is the corresponding method to
    233 * check and assert the recorded events.
    234 */
    235 async function recordTabStorageEvents(knownTab, sentinelValue) {
    236  await SpecialPowers.spawn(
    237    knownTab.tab.linkedBrowser,
    238    [sentinelValue],
    239    function (sentinelValue) {
    240      return content.wrappedJSObject.listenForStorageEvents(sentinelValue);
    241    }
    242  );
    243 }
    244 
    245 /**
    246 * Retrieve the current localStorage contents perceived by the tab and assert
    247 * that they match the provided expected state.
    248 *
    249 * If maybeSentinel is non-null, it's assumed to be a string that identifies the
    250 * value we should be waiting for the sentinel key to take on.  This is
    251 * necessary because we cannot make any assumptions about when state will be
    252 * propagated to the given process.  See the comments in
    253 * page_localstorage_e10s.js for more context.  In general, a sentinel value is
    254 * required for correctness unless the process in question is the one where the
    255 * writes were performed or verifyTabStorageEvents was used.
    256 */
    257 async function verifyTabStorageState(knownTab, expectedState, maybeSentinel) {
    258  let actualState = await SpecialPowers.spawn(
    259    knownTab.tab.linkedBrowser,
    260    [maybeSentinel],
    261    function (maybeSentinel) {
    262      return content.wrappedJSObject.getStorageState(maybeSentinel);
    263    }
    264  );
    265 
    266  for (let [expectedKey, expectedValue] of Object.entries(expectedState)) {
    267    ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey);
    268    is(actualState[expectedKey], expectedValue, "value correct");
    269  }
    270  for (let actualKey of Object.keys(actualState)) {
    271    if (!expectedState.hasOwnProperty(actualKey)) {
    272      ok(false, "actual state has key it shouldn't have: " + actualKey);
    273    }
    274  }
    275 }
    276 
    277 /**
    278 * Retrieve and clear the storage events recorded by the tab and assert that
    279 * they match the provided expected events.  For simplicity, the expected events
    280 * representation is the same as that used by mutateTabStorage.
    281 *
    282 * Note that by convention for test readability we are passed a 3rd argument of
    283 * the sentinel value, but we don't actually care what it is.
    284 */
    285 async function verifyTabStorageEvents(knownTab, expectedEvents) {
    286  let actualEvents = await SpecialPowers.spawn(
    287    knownTab.tab.linkedBrowser,
    288    [],
    289    function () {
    290      return content.wrappedJSObject.returnAndClearStorageEvents();
    291    }
    292  );
    293 
    294  is(actualEvents.length, expectedEvents.length, "right number of events");
    295  for (let i = 0; i < actualEvents.length; i++) {
    296    let [actualKey, actualNewValue, actualOldValue] = actualEvents[i];
    297    let [expectedKey, expectedNewValue, expectedOldValue] = expectedEvents[i];
    298    is(actualKey, expectedKey, "keys match");
    299    is(actualNewValue, expectedNewValue, "new values match");
    300    is(actualOldValue, expectedOldValue, "old values match");
    301  }
    302 }