//
// Bug 1724376 - Tests for the privilege requestStorageAccessForOrigin API.
//

/* import-globals-from storageAccessAPIHelpers.js */

"use strict";

const TEST_ANOTHER_TRACKER_DOMAIN = "https://itisatracker.org/";
const TEST_ANOTHER_TRACKER_PAGE =
  TEST_ANOTHER_TRACKER_DOMAIN + TEST_PATH + "3rdParty.html";
const TEST_ANOTHER_4TH_PARTY_DOMAIN = "https://test1.example.org/";
const TEST_ANOTHER_4TH_PARTY_PAGE =
  TEST_ANOTHER_4TH_PARTY_DOMAIN + TEST_PATH + "3rdParty.html";

// Insert an iframe with the given id into the content.
async function insertSubFrame(browser, url, id) {
  return SpecialPowers.spawn(browser, [url, id], async (url, id) => {
    let ifr = content.document.createElement("iframe");
    ifr.setAttribute("id", id);

    let loaded = ContentTaskUtils.waitForEvent(ifr, "load", false);
    content.document.body.appendChild(ifr);
    ifr.src = url;
    await loaded;
  });
}

// Run the given script in the iframe with the given id.
function runScriptInSubFrame(browser, id, script) {
  return SpecialPowers.spawn(
    browser,
    [{ callback: script.toString(), id }],
    async obj => {
      await new content.Promise(resolve => {
        let ifr = content.document.getElementById(obj.id);

        content.addEventListener("message", function msg(event) {
          if (event.data.type == "finish") {
            content.removeEventListener("message", msg);
            resolve();
            return;
          }

          if (event.data.type == "ok") {
            ok(event.data.what, event.data.msg);
            return;
          }

          if (event.data.type == "info") {
            info(event.data.msg);
            return;
          }

          ok(false, "Unknown message");
        });

        ifr.contentWindow.postMessage(obj.callback, "*");
      });
    }
  );
}

function waitStoragePermission(trackingOrigin) {
  return TestUtils.topicObserved("perm-changed", aSubject => {
    let permission = aSubject.QueryInterface(Ci.nsIPermission);
    let uri = Services.io.newURI(TEST_DOMAIN);
    return (
      permission.type == `3rdPartyStorage^${trackingOrigin}` &&
      permission.principal.equalsURI(uri)
    );
  });
}

function clearStoragePermission(trackingOrigin) {
  return SpecialPowers.removePermission(
    `3rdPartyStorage^${trackingOrigin}`,
    TEST_TOP_PAGE
  );
}

function triggerCommand(button) {
  let notifications = PopupNotifications.panel.children;
  let notification = notifications[0];
  EventUtils.synthesizeMouseAtCenter(notification[button], {});
}

function triggerMainCommand() {
  triggerCommand("button");
}

function triggerSecondaryCommand() {
  triggerCommand("secondaryButton");
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.storage_access.auto_grants", true],
      ["dom.storage_access.auto_grants.delayed", false],
      ["dom.storage_access.enabled", true],
      ["dom.storage_access.prompt.testing", false],
      ["privacy.trackingprotection.enabled", false],
      ["privacy.trackingprotection.pbmode.enabled", false],
      ["privacy.trackingprotection.annotate_channels", true],
      // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
      ["network.cookie.sameSite.laxByDefault", false],
      ["network.cookie.CHIPS.enabled", true],
    ],
  });

  await UrlClassifierTestUtils.addTestTrackers();

  registerCleanupFunction(() => {
    SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
    UrlClassifierTestUtils.cleanupTestTrackers();
  });
});

add_task(async function test_api_only_available_in_privilege_scope() {
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );
  let browser = tab.linkedBrowser;

  await SpecialPowers.spawn(browser, [], async _ => {
    ok(
      content.document.requestStorageAccessForOrigin,
      "The privilege API is available in system privilege code."
    );
  });

  // Open an iframe and check if the privilege is not available in content code.
  await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");
  await runScriptInSubFrame(browser, "test", async function check() {
    ok(
      !document.requestStorageAccessForOrigin,
      "The privilege API is not available in content code."
    );
  });

  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_privilege_api_with_reject_tracker() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "network.cookie.cookieBehavior",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
      ],
      [
        "network.cookie.cookieBehavior.pbmode",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
      ],
    ],
  });

  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );
  let browser = tab.linkedBrowser;

  await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");

  // Verify that the third party tracker doesn't have storage access at
  // beginning.
  await runScriptInSubFrame(browser, "test", async _ => {
    await noStorageAccessInitially();

    is(document.cookie, "", "No cookies for me");
    document.cookie = "name=value";
    is(document.cookie, "", "Setting cookie is blocked");
  });

  let storagePermissionPromise = waitStoragePermission(
    "https://tracking.example.org"
  );

  // Verify if the prompt has been shown.
  let shownPromise = BrowserTestUtils.waitForEvent(
    PopupNotifications.panel,
    "popupshown"
  );

  // Call the privilege API.
  let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => {
    // The privilege API requires user activation. So, we set the user
    // activation flag before we call the API.
    content.document.notifyUserGestureActivation();

    try {
      await content.document.requestStorageAccessForOrigin(
        "https://tracking.example.org/"
      );
    } catch (e) {
      ok(false, "The API shouldn't throw.");
    }

    content.document.clearUserGestureActivation();
  });

  await shownPromise;

  // Accept the prompt
  triggerMainCommand();

  await callAPIPromise;

  // Verify if the storage access permission is set correctly.
  await storagePermissionPromise;

  // Verify if the existing third-party tracker iframe gains the storage
  // access.
  await runScriptInSubFrame(browser, "test", async _ => {
    await hasStorageAccessInitially();

    is(document.cookie, "", "Still no cookies for me");
    document.cookie = "name=value";
    is(document.cookie, "name=value", "Successfully set cookies.");
  });

  // Insert another third-party tracker iframe and check if it has storage access.
  await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test2");
  await runScriptInSubFrame(browser, "test2", async _ => {
    await hasStorageAccessInitially();

    is(document.cookie, "name=value", "Some cookies for me");
  });

  // Insert another iframe with different third-party tracker and check it has
  // no storage access.
  await insertSubFrame(browser, TEST_ANOTHER_TRACKER_PAGE, "test3");
  await runScriptInSubFrame(browser, "test3", async _ => {
    await noStorageAccessInitially();

    is(document.cookie, "", "No cookies for me");
    document.cookie = "name=value";
    is(document.cookie, "", "Setting cookie is blocked for another tracker.");
  });

  await clearStoragePermission("https://tracking.example.org");
  Services.cookies.removeAll();
  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_privilege_api_with_dFPI() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "network.cookie.cookieBehavior",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
      ],
      [
        "network.cookie.cookieBehavior.pbmode",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
      ],
    ],
  });

  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );
  let browser = tab.linkedBrowser;

  await insertSubFrame(browser, TEST_4TH_PARTY_PAGE_HTTPS, "test");

  // Verify that the third-party context doesn't have storage access at
  // beginning.
  await runScriptInSubFrame(browser, "test", async _ => {
    await noStorageAccessInitially();

    is(document.cookie, "", "No cookies for me");
    document.cookie = "name=partitioned; SameSite=None; Secure; Partitioned;";
    is(
      document.cookie,
      "name=partitioned",
      "Setting cookie in partitioned context."
    );
  });

  let storagePermissionPromise = waitStoragePermission(
    "https://not-tracking.example.com"
  );

  // Verify if the prompt has been shown.
  let shownPromise = BrowserTestUtils.waitForEvent(
    PopupNotifications.panel,
    "popupshown"
  );

  // Call the privilege API.
  let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => {
    // The privilege API requires a user gesture. So, we set the user handling
    // flag before we call the API.
    content.document.notifyUserGestureActivation();

    try {
      await content.document.requestStorageAccessForOrigin(
        "https://not-tracking.example.com/"
      );
    } catch (e) {
      ok(false, "The API shouldn't throw.");
    }

    content.document.clearUserGestureActivation();
  });

  await shownPromise;

  // Accept the prompt
  triggerMainCommand();

  await callAPIPromise;

  // Verify if the storage access permission is set correctly.
  await storagePermissionPromise;

  // Verify if the existing third-party iframe gains the storage access.
  await runScriptInSubFrame(browser, "test", async _ => {
    await hasStorageAccessInitially();

    is(
      document.cookie,
      "name=partitioned",
      "partitioned cookies should be available"
    );
    document.cookie = "name=unpartitioned";
    is(
      document.cookie,
      "name=partitioned; name=unpartitioned",
      "Successfully set cookies. Both partitioned and unpartitioned cookies should be available"
    );
  });

  // Insert another third-party content iframe and check if it has storage access.
  await insertSubFrame(browser, TEST_4TH_PARTY_PAGE_HTTPS, "test2");
  await runScriptInSubFrame(browser, "test2", async _ => {
    await hasStorageAccessInitially();

    is(
      document.cookie,
      "name=partitioned; name=unpartitioned",
      "Some cookies for unpartitioned context"
    );
  });

  // Insert another iframe with different third-party content and check it has
  // no storage access.
  await insertSubFrame(browser, TEST_ANOTHER_4TH_PARTY_PAGE, "test3");
  await runScriptInSubFrame(browser, "test3", async _ => {
    await noStorageAccessInitially();

    is(document.cookie, "", "No cookies for me");
    document.cookie = "name=value; SameSite=None; Secure; Partitioned;";
    is(document.cookie, "name=value", "Setting cookie to partitioned context.");
  });

  await clearStoragePermission("https://not-tracking.example.com");
  Services.cookies.removeAll();
  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_prompt() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.storage_access.auto_grants", false],
      [
        "network.cookie.cookieBehavior",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
      ],
      [
        "network.cookie.cookieBehavior.pbmode",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
      ],
    ],
  });

  for (const allow of [false, true]) {
    let tab = await BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TEST_TOP_PAGE
    );
    let browser = tab.linkedBrowser;

    // Verify if the prompt has been shown.
    let shownPromise = BrowserTestUtils.waitForEvent(
      PopupNotifications.panel,
      "popupshown"
    );

    let hiddenPromise = BrowserTestUtils.waitForEvent(
      PopupNotifications.panel,
      "popuphidden"
    );

    // Call the privilege API.
    let callAPIPromise = SpecialPowers.spawn(browser, [allow], async allow => {
      // The privilege API requires a user gesture. So, we set the user handling
      // flag before we call the API.
      content.document.notifyUserGestureActivation();
      let isThrown = false;

      try {
        await content.document.requestStorageAccessForOrigin(
          "https://tracking.example.org"
        );
      } catch (e) {
        isThrown = true;
      }

      is(isThrown, !allow, `The API ${allow ? "shouldn't" : "should"} throw.`);

      content.document.clearUserGestureActivation();
    });

    await shownPromise;

    let notification = await TestUtils.waitForCondition(_ =>
      PopupNotifications.getNotification("storage-access", browser)
    );
    ok(notification, "Should have gotten the notification");

    // Click the popup button.
    if (allow) {
      triggerMainCommand();
    } else {
      triggerSecondaryCommand();
    }

    // Wait until the popup disappears.
    await hiddenPromise;

    // Wait until the API finishes.
    await callAPIPromise;

    await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");

    if (allow) {
      await runScriptInSubFrame(browser, "test", async _ => {
        await hasStorageAccessInitially();

        is(document.cookie, "", "Still no cookies for me");
        document.cookie = "name=value";
        is(document.cookie, "name=value", "Successfully set cookies.");
      });
    } else {
      await runScriptInSubFrame(browser, "test", async _ => {
        await noStorageAccessInitially();

        is(document.cookie, "", "Still no cookies for me");
        document.cookie = "name=value";
        is(document.cookie, "", "No cookie after setting.");
      });
    }

    BrowserTestUtils.removeTab(tab);
  }

  await clearStoragePermission("https://tracking.example.org");
  Services.cookies.removeAll();
});

// Tests that the priviledged rSA method should show a prompt when auto grants
// are enabled, but we don't have user activation. When requiring user
// activation, rSA should still reject.
add_task(async function test_prompt_no_user_activation() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.storage_access.auto_grants", true],
      [
        "network.cookie.cookieBehavior",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
      ],
      [
        "network.cookie.cookieBehavior.pbmode",
        Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER,
      ],
    ],
  });

  for (let requireUserActivation of [false, true]) {
    let tab = await BrowserTestUtils.openNewForegroundTab(
      gBrowser,
      TEST_TOP_PAGE
    );
    let browser = tab.linkedBrowser;

    let shownPromise, hiddenPromise;

    // Verify if the prompt has been shown.
    if (!requireUserActivation) {
      shownPromise = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popupshown"
      );

      hiddenPromise = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
    }

    // Call the privilege API.
    let callAPIPromise = SpecialPowers.spawn(
      browser,
      [requireUserActivation],
      async requireUserActivation => {
        let isThrown = false;

        try {
          await content.document.requestStorageAccessForOrigin(
            "https://tracking.example.org",
            requireUserActivation
          );
        } catch (e) {
          isThrown = true;
        }

        is(
          isThrown,
          requireUserActivation,
          `The API ${requireUserActivation ? "shouldn't" : "should"} throw.`
        );
      }
    );

    if (!requireUserActivation) {
      await shownPromise;

      let notification = await TestUtils.waitForCondition(_ =>
        PopupNotifications.getNotification("storage-access", browser)
      );
      ok(notification, "Should have gotten the notification");

      // Click the popup button.
      triggerMainCommand();

      // Wait until the popup disappears.
      await hiddenPromise;
    }

    // Wait until the API finishes.
    await callAPIPromise;

    await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test");

    if (!requireUserActivation) {
      await runScriptInSubFrame(browser, "test", async _ => {
        await hasStorageAccessInitially();

        is(document.cookie, "", "Still no cookies for me");
        document.cookie = "name=value";
        is(document.cookie, "name=value", "Successfully set cookies.");
      });
    } else {
      await runScriptInSubFrame(browser, "test", async _ => {
        await noStorageAccessInitially();

        is(document.cookie, "", "Still no cookies for me");
        document.cookie = "name=value";
        is(document.cookie, "", "No cookie after setting.");
      });
    }

    BrowserTestUtils.removeTab(tab);
    await clearStoragePermission("https://tracking.example.org");
    Services.cookies.removeAll();
  }
});

add_task(async function test_invalid_input() {
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    TEST_TOP_PAGE
  );
  let browser = tab.linkedBrowser;

  await SpecialPowers.spawn(browser, [], async _ => {
    let isThrown = false;
    try {
      await content.document.requestStorageAccessForOrigin(
        "https://tracking.example.org"
      );
    } catch (e) {
      isThrown = true;
    }
    ok(isThrown, "The API should throw without user gesture.");

    content.document.notifyUserGestureActivation();
    isThrown = false;
    try {
      await content.document.requestStorageAccessForOrigin();
    } catch (e) {
      isThrown = true;
    }
    ok(isThrown, "The API should throw with no input.");

    content.document.notifyUserGestureActivation();
    isThrown = false;
    try {
      await content.document.requestStorageAccessForOrigin("");
    } catch (e) {
      isThrown = true;
      is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url");
    }
    ok(isThrown, "The API should throw with empty string.");

    content.document.notifyUserGestureActivation();
    isThrown = false;
    try {
      await content.document.requestStorageAccessForOrigin("invalid url");
    } catch (e) {
      isThrown = true;
      is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url");
    }
    ok(isThrown, "The API should throw with invalid url.");

    content.document.clearUserGestureActivation();
  });

  BrowserTestUtils.removeTab(tab);
});
