tor-browser

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

head.js (9708B)


      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 "use strict";
      6 
      7 const { NavigationManager } = ChromeUtils.importESModule(
      8  "chrome://remote/content/shared/NavigationManager.sys.mjs"
      9 );
     10 
     11 const numberRegex = /[0-9]+/i;
     12 const uuidRegex =
     13  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
     14 
     15 /**
     16 * Add a new tab in a given browser, pointing to a given URL and automatically
     17 * register the cleanup function to remove it at the end of the test.
     18 *
     19 * @param {Browser} browser
     20 *     The browser element where the tab should be added.
     21 * @param {string} url
     22 *     The URL for the tab.
     23 * @param {object=} options
     24 *     Options object to forward to BrowserTestUtils.addTab.
     25 * @returns {Tab}
     26 *     The created tab.
     27 */
     28 function addTab(browser, url, options) {
     29  info("Add a new tab for url: " + url);
     30  const tab = BrowserTestUtils.addTab(browser, url, options);
     31  registerCleanupFunction(() => browser.removeTab(tab));
     32  return tab;
     33 }
     34 
     35 /**
     36 * Add a new tab and wait until the navigation manager emitted the corresponding
     37 * navigation-stopped event. Same arguments as addTab.
     38 *
     39 * @param {Browser} browser
     40 *     The browser element where the tab should be added.
     41 * @param {string} url
     42 *     The URL for the tab.
     43 * @param {object=} options
     44 *     Options object to forward to BrowserTestUtils.addTab.
     45 * @returns {Tab}
     46 *     The created tab.
     47 */
     48 async function addTabAndWaitForNavigated(browser, url, options) {
     49  // Setup navigation manager and promises.
     50  const { promise: waitForNavigation, resolve } = Promise.withResolvers();
     51  const onNavigationStopped = (name, data) => {
     52    if (data?.url === url) {
     53      resolve();
     54    }
     55  };
     56  const navigationManager = new NavigationManager();
     57  navigationManager.on("navigation-stopped", onNavigationStopped);
     58  navigationManager.startMonitoring();
     59 
     60  // Add the new tab.
     61  const tab = addTab(browser, url, options);
     62 
     63  // See Bug 1971329, browserLoaded on its own might still miss the STATE_STOP
     64  // notification.
     65  info("Wait for BrowserTestUtils.browserLoaded for url: " + url);
     66  await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
     67 
     68  info("Wait for navigation-stopped for url: " + url);
     69  await waitForNavigation;
     70 
     71  // Wait for tick to allow other callbacks listening for STATE_STOP to be
     72  // processed correctly.
     73  await TestUtils.waitForTick();
     74 
     75  navigationManager.stopMonitoring();
     76  navigationManager.off("navigation-stopped", onNavigationStopped);
     77  navigationManager.destroy();
     78 
     79  return tab;
     80 }
     81 
     82 /**
     83 * Check if a given navigation is valid and has the expected url.
     84 *
     85 * @param {object} navigation
     86 *     The navigation to validate.
     87 * @param {string} expectedUrl
     88 *     The expected url for the navigation.
     89 */
     90 function assertNavigation(navigation, expectedUrl) {
     91  ok(!!navigation, "Retrieved a navigation");
     92  is(navigation.url, expectedUrl, "Navigation has the expected URL");
     93  is(
     94    typeof navigation.navigationId,
     95    "string",
     96    "Navigation has a string navigationId"
     97  );
     98 }
     99 
    100 /**
    101 * Check a pair of navigation events have the expected URL, navigation id and
    102 * navigable id. The pair is expected to be ordered as follows: navigation-started
    103 * and then navigation-stopped.
    104 *
    105 * @param {Array<object>} events
    106 *     The pair of events to validate.
    107 * @param {string} url
    108 *     The expected url for the navigation.
    109 * @param {string} navigationId
    110 *     The expected navigation id.
    111 * @param {string} navigableId
    112 *     The expected navigable id.
    113 * @param {boolean} isSameDocument
    114 *     If the navigation should be a same document navigation.
    115 */
    116 function assertNavigationEvents(
    117  events,
    118  url,
    119  navigationId,
    120  navigableId,
    121  isSameDocument
    122 ) {
    123  const expectedEvents = isSameDocument ? 1 : 2;
    124 
    125  const navigationEvents = events.filter(
    126    e => e.data.navigationId == navigationId
    127  );
    128  is(
    129    navigationEvents.length,
    130    expectedEvents,
    131    `Found ${expectedEvents} events for navigationId ${navigationId}`
    132  );
    133 
    134  const sameDocumentEvents = ["fragment-navigated", "same-document-changed"];
    135 
    136  if (isSameDocument) {
    137    // Check there are no navigation-started/stopped events.
    138    ok(!navigationEvents.some(e => e.name === "navigation-started"));
    139    ok(!navigationEvents.some(e => e.name === "navigation-stopped"));
    140 
    141    const locationChanged = navigationEvents.find(e =>
    142      sameDocumentEvents.includes(e.name)
    143    );
    144    ok(
    145      sameDocumentEvents.includes(locationChanged.name),
    146      "event has the expected name"
    147    );
    148    is(locationChanged.data.url, url, "event has the expected url");
    149    is(
    150      locationChanged.data.navigableId,
    151      navigableId,
    152      "event has the expected navigable"
    153    );
    154  } else {
    155    // Check there is no fragment-navigated/same-document-changed event.
    156    ok(!navigationEvents.some(e => sameDocumentEvents.includes(e.name)));
    157 
    158    const started = navigationEvents.find(e => e.name === "navigation-started");
    159    const stopped = navigationEvents.find(e => e.name === "navigation-stopped");
    160 
    161    // Check navigation-started
    162    is(started.name, "navigation-started", "event has the expected name");
    163    is(started.data.url, url, "event has the expected url");
    164    is(
    165      started.data.navigableId,
    166      navigableId,
    167      "event has the expected navigable"
    168    );
    169 
    170    // Check navigation-stopped
    171    is(stopped.name, "navigation-stopped", "event has the expected name");
    172    is(stopped.data.url, url, "event has the expected url");
    173    is(
    174      stopped.data.navigableId,
    175      navigableId,
    176      "event has the expected navigable"
    177    );
    178  }
    179 }
    180 
    181 /**
    182 * Assert that the given navigations all have unique/different ids.
    183 *
    184 * @param {Array<object>} navigations
    185 *     The navigations to validate.
    186 */
    187 function assertUniqueNavigationIds(...navigations) {
    188  const ids = navigations.map(navigation => navigation.navigationId);
    189  is(new Set(ids).size, ids.length, "Navigation ids are all different");
    190 }
    191 
    192 /**
    193 * Create a document-builder based page with an iframe served by a given domain.
    194 *
    195 * @param {string} domain
    196 *     The domain which should serve the page.
    197 * @returns {string}
    198 *     The URI for the page.
    199 */
    200 function createFrame(domain) {
    201  return createFrameForUri(
    202    `https://${domain}/document-builder.sjs?html=frame-${domain}`
    203  );
    204 }
    205 
    206 /**
    207 * Create the markup for an iframe pointing to a given URI.
    208 *
    209 * @param {string} uri
    210 *     The uri for the iframe.
    211 * @returns {string}
    212 *     The iframe markup.
    213 */
    214 function createFrameForUri(uri) {
    215  return `<iframe src="${encodeURI(uri)}"></iframe>`;
    216 }
    217 
    218 /**
    219 * Create the URL for a test page containing nested iframes
    220 *
    221 * @returns {string}
    222 *     The test page url.
    223 */
    224 function createTestPageWithFrames() {
    225  // Create the markup for an example.net frame nested in an example.com frame.
    226  const NESTED_FRAME_MARKUP = createFrameForUri(
    227    `https://example.org/document-builder.sjs?html=${createFrame(
    228      "example.net"
    229    )}`
    230  );
    231 
    232  // Combine the nested frame markup created above with an example.com frame.
    233  const TEST_URI_MARKUP = `${NESTED_FRAME_MARKUP}${createFrame("example.com")}`;
    234 
    235  // Create the test page URI on example.org.
    236  return `https://example.org/document-builder.sjs?html=${encodeURI(
    237    TEST_URI_MARKUP
    238  )}`;
    239 }
    240 
    241 /**
    242 * Load the provided url in an existing browser.
    243 *
    244 * @param {Browser} browser
    245 *     The browser element where the URL should be loaded.
    246 * @param {string} url
    247 *     The URL to load.
    248 * @param {object=} options
    249 * @param {boolean} options.includeSubFrames
    250 *     Whether we should monitor load of sub frames. Defaults to false.
    251 * @param {boolean} options.maybeErrorPage
    252 *     Whether we might reach an error page or not. Defaults to false.
    253 * @returns {Promise}
    254 *     Promise which will resolve when the page is loaded with the expected url.
    255 */
    256 async function loadURL(browser, url, options = {}) {
    257  const { includeSubFrames = false, maybeErrorPage = false } = options;
    258  const loaded = BrowserTestUtils.browserLoaded(
    259    browser,
    260    includeSubFrames,
    261    url,
    262    maybeErrorPage
    263  );
    264  BrowserTestUtils.startLoadingURIString(browser, url);
    265  return loaded;
    266 }
    267 
    268 /**
    269 * For a support file resolve its relative path to the absolute path.
    270 *
    271 * @param {string} path
    272 *     The path or a filename of a support file.
    273 * @returns {string}
    274 *     Absolute path of the support file.
    275 */
    276 function getSupportFilePath(path) {
    277  let absolutePath = getChromeDir(getResolvedURI(gTestPath));
    278 
    279  for (const part of path.split("/")) {
    280    if (part === "..") {
    281      absolutePath = absolutePath.parent;
    282    } else {
    283      absolutePath.append(part);
    284    }
    285  }
    286 
    287  if (!absolutePath.exists()) {
    288    throw new Error(`${absolutePath.path} does not exist`);
    289  }
    290 
    291  return absolutePath.path;
    292 }
    293 
    294 /**
    295 * Reads file from provided path and returns its contents encoded with base64
    296 *
    297 * @param {string} path
    298 *     The Path to load.
    299 * @returns {Promise}
    300 *     Promise which will resolved when the file finished loading.
    301 */
    302 async function readFileAsBase64(path) {
    303  const file = new FileUtils.File(path);
    304 
    305  const contents = await new Promise((resolve, reject) => {
    306    NetUtil.asyncFetch(
    307      {
    308        uri: file,
    309        loadUsingSystemPrincipal: true,
    310      },
    311      (inputStream, status) => {
    312        if (!Components.isSuccessCode(status)) {
    313          reject(new Error("Failed to read file; status = " + status));
    314          return;
    315        }
    316 
    317        const fileContents = NetUtil.readInputStreamToString(
    318          inputStream,
    319          inputStream.available()
    320        );
    321        inputStream.close();
    322 
    323        resolve(fileContents);
    324      }
    325    );
    326  });
    327 
    328  return btoa(contents);
    329 }