/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// This test make sure that the favicon of the private browsing is isolated.

const TEST_SITE = "https://example.com";
const TEST_CACHE_SITE = "https://test1.example.com";
const TEST_DIRECTORY =
  "/browser/browser/components/privatebrowsing/test/browser/";

const TEST_PAGE = TEST_SITE + TEST_DIRECTORY + "file_favicon.html";
const TEST_CACHE_PAGE = TEST_CACHE_SITE + TEST_DIRECTORY + "file_favicon.html";
const FAVICON_URI = TEST_SITE + TEST_DIRECTORY + "file_favicon.png";
const FAVICON_CACHE_URI = TEST_CACHE_SITE + TEST_DIRECTORY + "file_favicon.png";

let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();

function clearAllImageCaches() {
  let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(
    SpecialPowers.Ci.imgITools
  );
  let imageCache = tools.getImgCacheForDocument(window.document);
  imageCache.clearCache(true); // true=chrome
  imageCache.clearCache(false); // false=content
}

function clearAllPlacesFavicons() {
  let faviconService = Cc["@mozilla.org/browser/favicon-service;1"].getService(
    Ci.nsIFaviconService
  );

  return new Promise(resolve => {
    let observer = {
      observe(aSubject, aTopic) {
        if (aTopic === "places-favicons-expired") {
          resolve();
          Services.obs.removeObserver(observer, "places-favicons-expired");
        }
      },
    };

    Services.obs.addObserver(observer, "places-favicons-expired");
    faviconService.expireAllFavicons();
  });
}

function observeFavicon(aIsPrivate, aExpectedCookie, aPageURI) {
  let attr = {};

  if (aIsPrivate) {
    attr.privateBrowsingId = 1;
  }

  let expectedPrincipal = Services.scriptSecurityManager.createContentPrincipal(
    aPageURI,
    attr
  );

  return new Promise(resolve => {
    let observer = {
      observe(aSubject, aTopic) {
        // Make sure that the topic is 'http-on-modify-request'.
        if (aTopic === "http-on-modify-request") {
          // We check the privateBrowsingId for the originAttributes of the loading
          // channel. All requests for the favicon should contain the correct
          // privateBrowsingId. There are two requests for a favicon loading, one
          // from the Places library and one from the XUL image. The difference
          // of them is the loading principal. The Places will use the content
          // principal and the XUL image will use the system principal.

          let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
          let reqLoadInfo = httpChannel.loadInfo;
          let loadingPrincipal = reqLoadInfo.loadingPrincipal;

          // Make sure this is a favicon request.
          if (httpChannel.URI.spec !== FAVICON_URI) {
            return;
          }

          // Check the privateBrowsingId.
          if (aIsPrivate) {
            is(
              reqLoadInfo.originAttributes.privateBrowsingId,
              1,
              "The loadInfo has correct privateBrowsingId"
            );
          } else {
            is(
              reqLoadInfo.originAttributes.privateBrowsingId,
              0,
              "The loadInfo has correct privateBrowsingId"
            );
          }

          ok(
            loadingPrincipal.equals(expectedPrincipal),
            "The loadingPrincipal of favicon loading from Places should be the content prinicpal"
          );

          let faviconCookie = httpChannel.getRequestHeader("cookie");

          is(
            faviconCookie,
            aExpectedCookie,
            "The cookie of the favicon loading is correct."
          );
        } else {
          ok(false, "Received unexpected topic: ", aTopic);
        }

        resolve();
        Services.obs.removeObserver(observer, "http-on-modify-request");
      },
    };

    Services.obs.addObserver(observer, "http-on-modify-request");
  });
}

function waitOnFaviconResponse(aFaviconURL) {
  return new Promise(resolve => {
    let observer = {
      observe(aSubject, aTopic) {
        if (
          aTopic === "http-on-examine-response" ||
          aTopic === "http-on-examine-cached-response"
        ) {
          let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
          let loadInfo = httpChannel.loadInfo;

          if (httpChannel.URI.spec !== aFaviconURL) {
            return;
          }

          let result = {
            topic: aTopic,
            privateBrowsingId: loadInfo.originAttributes.privateBrowsingId,
          };

          resolve(result);
          Services.obs.removeObserver(observer, "http-on-examine-response");
          Services.obs.removeObserver(
            observer,
            "http-on-examine-cached-response"
          );
        }
      },
    };

    Services.obs.addObserver(observer, "http-on-examine-response");
    Services.obs.addObserver(observer, "http-on-examine-cached-response");
  });
}

function waitOnFaviconLoaded(aFaviconURL) {
  return PlacesTestUtils.waitForNotification("favicon-changed", events =>
    events.some(e => e.faviconUrl == aFaviconURL)
  );
}

async function assignCookies(aBrowser, aURL, aCookieValue) {
  let tabInfo = await openTab(aBrowser, aURL);

  await SpecialPowers.spawn(
    tabInfo.browser,
    [aCookieValue],
    async function (value) {
      content.document.cookie = value;
    }
  );

  BrowserTestUtils.removeTab(tabInfo.tab);
}

async function openTab(aBrowser, aURL) {
  let tab = BrowserTestUtils.addTab(aBrowser, aURL);

  // Select tab and make sure its browser is focused.
  aBrowser.selectedTab = tab;
  tab.ownerGlobal.focus();

  let browser = aBrowser.getBrowserForTab(tab);
  await BrowserTestUtils.browserLoaded(browser);
  return { tab, browser };
}

registerCleanupFunction(async () => {
  Services.cookies.removeAll();
  clearAllImageCaches();
  Services.cache2.clear();
  await PlacesUtils.history.clear();
  await PlacesUtils.bookmarks.eraseEverything();
});

add_task(async function test_favicon_privateBrowsing() {
  // Clear all image caches before running the test.
  clearAllImageCaches();
  // Clear all favicons in Places.
  await clearAllPlacesFavicons();

  // Create a private browsing window.
  let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  let pageURI = Services.io.newURI(TEST_PAGE);

  // Generate two random cookies for non-private window and private window
  // respectively.
  let cookies = [];
  cookies.push(Math.random().toString());
  cookies.push(Math.random().toString());

  // Open a tab in private window and add a cookie into it.
  await assignCookies(privateWindow.gBrowser, TEST_SITE, cookies[0]);

  // Open a tab in non-private window and add a cookie into it.
  await assignCookies(gBrowser, TEST_SITE, cookies[1]);

  // Add the observer earlier in case we don't capture events in time.
  let promiseObserveFavicon = observeFavicon(true, cookies[0], pageURI);

  // The page must be bookmarked for favicon requests to go through in PB mode.
  await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    url: TEST_PAGE,
  });

  // Open a tab for the private window.
  let tabInfo = await openTab(privateWindow.gBrowser, TEST_PAGE);

  info("Waiting until favicon requests are all made in private window.");
  await promiseObserveFavicon;

  // Close the tab.
  BrowserTestUtils.removeTab(tabInfo.tab);
  // FIXME: We need to wait for the next event tick here to avoid observing
  //        the previous tab info in the next step (bug 1446725).
  await new Promise(executeSoon);

  // Add the observer earlier in case we don't capture events in time.
  promiseObserveFavicon = observeFavicon(false, cookies[1], pageURI);

  // Open a tab for the non-private window.
  tabInfo = await openTab(gBrowser, TEST_PAGE);

  info("Waiting until favicon requests are all made in non-private window.");
  await promiseObserveFavicon;

  // Close the tab.
  BrowserTestUtils.removeTab(tabInfo.tab);
  await BrowserTestUtils.closeWindow(privateWindow);
});

add_task(async function test_favicon_cache_privateBrowsing() {
  // Clear all image caches and network cache before running the test.
  clearAllImageCaches();

  Services.cache2.clear();

  // Clear all favicons in Places.
  await clearAllPlacesFavicons();

  // Add an observer for making sure the favicon has been loaded and cached.
  let promiseFaviconLoaded = waitOnFaviconLoaded(FAVICON_CACHE_URI);
  let promiseFaviconResponse = waitOnFaviconResponse(FAVICON_CACHE_URI);

  // Open a tab for the non-private window.
  let tabInfoNonPrivate = await openTab(gBrowser, TEST_CACHE_PAGE);

  let response = await promiseFaviconResponse;

  await promiseFaviconLoaded;

  // Check that the favicon response has come from the network and it has the
  // correct privateBrowsingId.
  is(
    response.topic,
    "http-on-examine-response",
    "The favicon image should be loaded through network."
  );
  is(
    response.privateBrowsingId,
    0,
    "We should observe the network response for the non-private tab."
  );

  // Create a private browsing window.
  let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });

  // The page must be bookmarked for favicon requests to go through in PB mode.
  await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    url: TEST_CACHE_PAGE,
  });

  promiseFaviconResponse = waitOnFaviconResponse(FAVICON_CACHE_URI);

  // Open a tab for the private window.
  let tabInfoPrivate = await openTab(privateWindow.gBrowser, TEST_CACHE_PAGE);

  // Wait for the favicon response of the private tab.
  response = await promiseFaviconResponse;

  // Make sure the favicon is loaded through the network and its privateBrowsingId is correct.
  is(
    response.topic,
    "http-on-examine-response",
    "The favicon image should be loaded through the network again."
  );
  is(
    response.privateBrowsingId,
    1,
    "We should observe the network response for the private tab."
  );

  BrowserTestUtils.removeTab(tabInfoPrivate.tab);
  BrowserTestUtils.removeTab(tabInfoNonPrivate.tab);
  await BrowserTestUtils.closeWindow(privateWindow);
});
