/**
 * Bug 1720294 - Testing disallow relaxing default referrer policy for
 *               cross-site requests.
 */

"use strict";

requestLongerTimeout(6);

const TEST_DOMAIN = "https://example.com/";
const TEST_SAME_SITE_DOMAIN = "https://test1.example.com/";
const TEST_SAME_SITE_DOMAIN_HTTP = "http://test1.example.com/";
const TEST_CROSS_SITE_DOMAIN = "https://test1.example.org/";
const TEST_CROSS_SITE_DOMAIN_HTTP = "http://test1.example.org/";

const TEST_PATH = "browser/dom/security/test/referrer-policy/";

const TEST_PAGE = `${TEST_DOMAIN}${TEST_PATH}referrer_page.sjs`;
const TEST_SAME_SITE_PAGE = `${TEST_SAME_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`;
const TEST_SAME_SITE_PAGE_HTTP = `${TEST_SAME_SITE_DOMAIN_HTTP}${TEST_PATH}referrer_page.sjs`;
const TEST_CROSS_SITE_PAGE = `${TEST_CROSS_SITE_DOMAIN}${TEST_PATH}referrer_page.sjs`;
const TEST_CROSS_SITE_PAGE_HTTP = `${TEST_CROSS_SITE_DOMAIN_HTTP}${TEST_PATH}referrer_page.sjs`;

const REFERRER_FULL = 0;
const REFERRER_ORIGIN = 1;
const REFERRER_NONE = 2;

function getExpectedReferrer(referrer, type) {
  let res;

  switch (type) {
    case REFERRER_FULL:
      res = referrer;
      break;
    case REFERRER_ORIGIN: {
      let url = new URL(referrer);
      res = `${url.origin}/`;
      break;
    }
    case REFERRER_NONE:
      res = "";
      break;
    default:
      ok(false, "unknown type");
  }

  return res;
}

async function verifyResultInPage(browser, expected) {
  await SpecialPowers.spawn(browser, [expected], value => {
    is(content.document.referrer, value, "The document.referrer is correct.");

    let result = content.document.getElementById("result");
    is(result.textContent, value, "The referer header is correct");
  });
}

function getExpectedConsoleMessage(expected, isPrefOn, url) {
  let msg;

  if (isPrefOn) {
    msg =
      "Referrer Policy: Ignoring the less restricted referrer policy “" +
      expected +
      "” for the cross-site request: " +
      url;
  } else {
    msg =
      "Referrer Policy: Less restricted policies, including " +
      "‘no-referrer-when-downgrade’, ‘origin-when-cross-origin’ and " +
      "‘unsafe-url’, will be ignored soon for the cross-site request: " +
      url;
  }

  return msg;
}

function createConsoleMessageVerificationPromise(expected, isPrefOn, url) {
  if (!expected) {
    return Promise.resolve();
  }

  return new Promise(resolve => {
    let listener = {
      observe(msg) {
        let message = msg.QueryInterface(Ci.nsIScriptError);

        if (message.category.startsWith("Security")) {
          is(
            message.errorMessage,
            getExpectedConsoleMessage(expected, isPrefOn, url),
            "The console message is correct."
          );
          Services.console.unregisterListener(listener);
          resolve();
        }
      },
    };

    Services.console.registerListener(listener);
  });
}

function verifyNoConsoleMessage() {
  // Verify that there is no referrer policy console message.
  let allMessages = Services.console.getMessageArray();

  for (let msg of allMessages) {
    let message = msg.QueryInterface(Ci.nsIScriptError);
    if (
      message.category.startsWith("Security") &&
      message.errorMessage.startsWith("Referrer Policy:")
    ) {
      ok(false, "There should be no console message for referrer policy.");
    }
  }
}

const TEST_CASES = [
  // Testing that the referrer policy can be overridden with less restricted
  // policy in the same-origin scenario.
  {
    policy: "unsafe-url",
    referrer: TEST_PAGE,
    test_url: TEST_PAGE,
    expect: REFERRER_FULL,
    original: REFERRER_FULL,
  },
  // Testing that the referrer policy can be overridden with less restricted
  // policy in the same-site scenario.
  {
    policy: "unsafe-url",
    referrer: TEST_PAGE,
    test_url: TEST_SAME_SITE_PAGE,
    expect: REFERRER_FULL,
    original: REFERRER_FULL,
  },
  {
    policy: "no-referrer-when-downgrade",
    referrer: TEST_PAGE,
    test_url: TEST_SAME_SITE_PAGE,
    expect: REFERRER_FULL,
    original: REFERRER_FULL,
  },
  {
    policy: "origin-when-cross-origin",
    referrer: TEST_PAGE,
    test_url: TEST_SAME_SITE_PAGE_HTTP,
    expect: REFERRER_ORIGIN,
    original: REFERRER_ORIGIN,
  },
  // Testing that the referrer policy cannot be overridden with less restricted
  // policy in the cross-site scenario.
  {
    policy: "unsafe-url",
    referrer: TEST_PAGE,
    test_url: TEST_CROSS_SITE_PAGE,
    expect: REFERRER_ORIGIN,
    expect_console: "unsafe-url",
    original: REFERRER_FULL,
  },
  {
    policy: "no-referrer-when-downgrade",
    referrer: TEST_PAGE,
    test_url: TEST_CROSS_SITE_PAGE,
    expect: REFERRER_ORIGIN,
    expect_console: "no-referrer-when-downgrade",
    original: REFERRER_FULL,
  },
  {
    policy: "origin-when-cross-origin",
    referrer: TEST_PAGE,
    test_url: TEST_CROSS_SITE_PAGE_HTTP,
    expect: REFERRER_NONE,
    expect_console: "origin-when-cross-origin",
    original: REFERRER_ORIGIN,
  },
  // Testing that the referrer policy can still be overridden with more
  // restricted policy in the cross-site scenario.
  {
    policy: "no-referrer",
    referrer: TEST_PAGE,
    test_url: TEST_CROSS_SITE_PAGE,
    expect: REFERRER_NONE,
    original: REFERRER_NONE,
  },
];

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      // Disable mixed content blocking to be able to test downgrade scenario.
      ["security.mixed_content.block_active_content", false],
      // Disable https-first since we are testing http and https referrers
      ["dom.security.https_first", false],
      ["browser.safebrowsing.only_top_level", false],
    ],
  });
});

async function runTestIniFrame(gBrowser, enabled, expectNoConsole) {
  await BrowserTestUtils.withNewTab(
    { gBrowser, url: "about:blank" },
    async browser => {
      for (let type of ["meta", "header"]) {
        for (let test of TEST_CASES) {
          info(`Test iframe: ${test.toSource()}`);
          let referrerURL = `${test.referrer}?${type}=${test.policy}`;
          let expected = enabled
            ? getExpectedReferrer(referrerURL, test.expect)
            : getExpectedReferrer(referrerURL, test.original);

          let expected_console = expectNoConsole
            ? undefined
            : test.expect_console;

          Services.console.reset();

          BrowserTestUtils.startLoadingURIString(browser, referrerURL);
          await BrowserTestUtils.browserLoaded(browser, false, referrerURL);

          let iframeURL = test.test_url + "?show";

          let consolePromise = createConsoleMessageVerificationPromise(
            expected_console,
            enabled,
            iframeURL
          );
          // Create an iframe and load the url.
          let bc = await SpecialPowers.spawn(
            browser,
            [iframeURL],
            async url => {
              let iframe = content.document.createElement("iframe");
              let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load");
              iframe.src = url;
              content.document.body.appendChild(iframe);

              await loadPromise;

              return iframe.browsingContext;
            }
          );

          await verifyResultInPage(bc, expected);
          await consolePromise;
          if (!expected_console) {
            verifyNoConsoleMessage();
          }
        }
      }
    }
  );
}

async function runTestForLinkClick(gBrowser, enabled, expectNoConsole) {
  await BrowserTestUtils.withNewTab(
    { gBrowser, url: "about:blank" },
    async browser => {
      for (let type of ["meta", "header"]) {
        for (let test of TEST_CASES) {
          info(`Test link click: ${test.toSource()}`);
          let referrerURL = `${test.referrer}?${type}=${test.policy}`;
          let expected = enabled
            ? getExpectedReferrer(referrerURL, test.expect)
            : getExpectedReferrer(referrerURL, test.original);

          let expected_console = expectNoConsole
            ? undefined
            : test.expect_console;

          Services.console.reset();

          BrowserTestUtils.startLoadingURIString(browser, referrerURL);
          await BrowserTestUtils.browserLoaded(browser, false, referrerURL);

          let linkURL = test.test_url + "?show";

          let consolePromise = createConsoleMessageVerificationPromise(
            expected_console,
            enabled,
            linkURL
          );

          // Create the promise to wait for the navigation finishes.
          let loadedPromise = BrowserTestUtils.browserLoaded(
            browser,
            false,
            linkURL
          );

          // Generate the link and click it to navigate.
          await SpecialPowers.spawn(browser, [linkURL], async url => {
            let link = content.document.createElement("a");
            link.textContent = "Link";
            link.setAttribute("href", url);

            content.document.body.appendChild(link);
            link.click();
          });

          await loadedPromise;

          await verifyResultInPage(browser, expected);
          await consolePromise;
          if (!expected_console) {
            verifyNoConsoleMessage();
          }
        }
      }
    }
  );
}

async function toggleETPForPage(gBrowser, url, toggle) {
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);

  // First, Toggle ETP off for the test page.
  let browserLoadedPromise = BrowserTestUtils.browserLoaded(
    tab.linkedBrowser,
    false,
    url
  );

  if (toggle) {
    gProtectionsHandler.enableForCurrentPage();
  } else {
    gProtectionsHandler.disableForCurrentPage();
  }

  await browserLoadedPromise;
  BrowserTestUtils.removeTab(tab);
}

add_task(async function test_iframe() {
  for (let enabled of [true, false]) {
    await SpecialPowers.pushPrefEnv({
      set: [["network.http.referer.disallowCrossSiteRelaxingDefault", enabled]],
    });

    await runTestIniFrame(gBrowser, enabled);
  }
});

add_task(async function test_iframe_pbmode() {
  let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });

  for (let enabled of [true, false]) {
    await SpecialPowers.pushPrefEnv({
      set: [
        [
          "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode",
          enabled,
        ],
      ],
    });

    await runTestIniFrame(win.gBrowser, enabled);
  }

  await BrowserTestUtils.closeWindow(win);
});

add_task(async function test_link_click() {
  for (let enabled of [true, false]) {
    for (let enabled_top of [true, false]) {
      await SpecialPowers.pushPrefEnv({
        set: [
          ["network.http.referer.disallowCrossSiteRelaxingDefault", enabled],
          [
            "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation",
            enabled_top,
          ],
        ],
      });

      // We won't show the console message if the strict rule is disabled for
      // the top navigation.
      await runTestForLinkClick(gBrowser, enabled && enabled_top, !enabled_top);
    }
  }
});

add_task(async function test_link_click_pbmode() {
  let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });

  for (let enabled of [true, false]) {
    for (let enabled_top of [true, false]) {
      await SpecialPowers.pushPrefEnv({
        set: [
          [
            "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode",
            enabled,
          ],
          [
            "network.http.referer.disallowCrossSiteRelaxingDefault.pbmode.top_navigation",
            enabled_top,
          ],
          // Disable https first mode for private browsing mode to test downgrade
          // cases.
          ["dom.security.https_first_pbm", false],
        ],
      });

      // We won't show the console message if the strict rule is disabled for
      // the top navigation in the private browsing window.
      await runTestForLinkClick(
        win.gBrowser,
        enabled && enabled_top,
        !enabled_top
      );
    }
  }

  await BrowserTestUtils.closeWindow(win);
});

add_task(async function test_iframe_etp_toggle_off() {
  await SpecialPowers.pushPrefEnv({
    set: [["network.http.referer.disallowCrossSiteRelaxingDefault", true]],
  });

  // Open a new tab for the test page and toggle ETP off.
  await toggleETPForPage(gBrowser, TEST_PAGE, false);

  // Run the test to see if the protection is disabled. We won't send console
  // message if the protection was disabled by the ETP toggle.
  await runTestIniFrame(gBrowser, false, true);

  // toggle ETP on again.
  await toggleETPForPage(gBrowser, TEST_PAGE, true);

  // Run the test again to see if the protection is enabled.
  await runTestIniFrame(gBrowser, true);
});

add_task(async function test_link_click_etp_toggle_off() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["network.http.referer.disallowCrossSiteRelaxingDefault", true],
      [
        "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation",
        true,
      ],
    ],
  });

  // Toggle ETP off for the cross site. Note that the cross site is the place
  // where we test against the ETP permission for top navigation.
  await toggleETPForPage(gBrowser, TEST_CROSS_SITE_PAGE, false);

  // Run the test to see if the protection is disabled. We won't send console
  // message if the protection was disabled by the ETP toggle.
  await runTestForLinkClick(gBrowser, false, true);

  // toggle ETP on again.
  await toggleETPForPage(gBrowser, TEST_CROSS_SITE_PAGE, true);

  // Run the test again to see if the protection is enabled.
  await runTestForLinkClick(gBrowser, true);
});
