/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const TEST_PATH = getRootDirectory(gTestPath).replace(
  "chrome://mochitests/content",
  "http://example.com"
);

const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view";
const nullP = Services.scriptSecurityManager.createNullPrincipal({});

// We need 3 levels of nesting just to get to run something against a content
// page, so the devtools limit of 4 levels of nesting don't help:
/* eslint max-nested-callbacks: 0 */

/**
 * Check that we don't expose a JSONView object on data: URI windows where
 * we block the load.
 */
add_task(async function test_blocked_data_exposure() {
  await SpecialPowers.pushPrefEnv({
    set: [["security.data_uri.block_toplevel_data_uri_navigations", true]],
  });
  await BrowserTestUtils.withNewTab(TEST_PATH + "empty.html", async browser => {
    const tabCount = gBrowser.tabs.length;
    await SpecialPowers.spawn(browser, [], function() {
      content.w = content.window.open(
        "data:application/vnd.mozilla.json.view,1",
        "_blank"
      );
      ok(
        !Cu.waiveXrays(content.w).JSONView,
        "Should not have created a JSON View object"
      );
      // We need to wait for the JSON view machinery to actually have a chance to run.
      // We have no way to detect that it has or hasn't, so a setTimeout is the best we
      // can do, unfortunately.
      return new Promise(resolve => {
        content.setTimeout(function() {
          // Putting the resolve before the check to avoid JS errors potentially causing
          // the test to time out.
          resolve();
          ok(
            !Cu.waiveXrays(content.w).JSONView,
            "Should still not have a JSON View object"
          );
        }, 1000);
      });
    });
    // Without this, if somehow the data: protocol blocker stops working, the
    // test would just keep passing.
    is(
      tabCount,
      gBrowser.tabs.length,
      "Haven't actually opened a new window/tab"
    );
  });
});

/**
 * Check that aborted channels also abort sending data from the stream converter.
 */
add_task(async function test_converter_abort_should_stop_data_sending() {
  const loadInfo = NetUtil.newChannel({
    uri: Services.io.newURI("data:text/plain,"),
    loadingPrincipal: nullP,
    securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
  }).loadInfo;
  // Stub all the things.
  const chan = {
    QueryInterface: ChromeUtils.generateQI([
      "nsIChannel",
      "nsIWritablePropertyBag",
    ]),
    URI: Services.io.newURI("data:application/json,{}"),
    // loadinfo is builtinclass, need to actually have one:
    loadInfo,
    notificationCallbacks: {
      QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
      getInterface() {
        // We want a loadcontext here, which is also builtinclass, can't stub.
        return docShell;
      },
    },
    status: Cr.NS_OK,
    setProperty() {},
  };
  let onStartFired = false;
  const listener = {
    QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
    onStartRequest() {
      onStartFired = true;
      // This should force the converter to abort, too:
      chan.status = Cr.NS_BINDING_ABORTED;
    },
    onDataAvailable() {
      ok(false, "onDataAvailable should never fire");
    },
  };
  const conv = Cc[
    "@mozilla.org/streamconv;1?from=" + JSON_VIEW_MIME_TYPE + "&to=*/*"
  ].createInstance(Ci.nsIStreamConverter);
  conv.asyncConvertData(
    "application/vnd.mozilla.json.view",
    "text/html",
    listener,
    null
  );
  conv.onStartRequest(chan);
  ok(onStartFired, "Should have fired onStartRequest");
});

/**
 * Check that principal mismatches break things. Note that we're stubbing
 * the window associated with the channel to be a browser window; the
 * converter should be bailing out because the window's principal won't
 * match the null principal to which the converter tries to reset the
 * inheriting principal of the channel.
 */
add_task(async function test_converter_principal_needs_matching() {
  const loadInfo = NetUtil.newChannel({
    uri: Services.io.newURI("data:text/plain,"),
    loadingPrincipal: nullP,
    securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
  }).loadInfo;
  // Stub all the things.
  const chan = {
    QueryInterface: ChromeUtils.generateQI([
      "nsIChannel",
      "nsIWritablePropertyBag",
    ]),
    URI: Services.io.newURI("data:application/json,{}"),
    // loadinfo is builtinclass, need to actually have one:
    loadInfo,
    notificationCallbacks: {
      QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
      getInterface() {
        // We want a loadcontext here, which is also builtinclass, can't stub.
        return docShell;
      },
    },
    status: Cr.NS_OK,
    setProperty() {},
    cancel(arg) {
      this.status = arg;
    },
  };
  let onStartFired = false;
  const listener = {
    QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
    onStartRequest() {
      onStartFired = true;
    },
    onDataAvailable() {
      ok(false, "onDataAvailable should never fire");
    },
  };
  const conv = Cc[
    "@mozilla.org/streamconv;1?from=" + JSON_VIEW_MIME_TYPE + "&to=*/*"
  ].createInstance(Ci.nsIStreamConverter);
  conv.asyncConvertData(
    "application/vnd.mozilla.json.view",
    "text/html",
    listener,
    null
  );
  conv.onStartRequest(chan);
  ok(onStartFired, "Should have fired onStartRequest");
  is(chan.status, Cr.NS_BINDING_ABORTED, "Should have been aborted.");
});
