'use strict';

function Scriptish_stringBundle(a) a;

const {Cc, Ci, Cu, Cr} = require("chrome");
var {Instances} = require("../chrome/instances");
var {XPCOMUtils} = require("../chrome/xpcom-utils");
var {NetUtil} = require("../chrome/net-utils");

const MIME_JSON = /^(application|text)\/(?:x-)?json/i;

/**
 * Abstract base class for (chained) request notification callback overrides
 *
 * Use such overrides sparely, as the individual request performance might
 * degrade quite a bit.
 *
 * @param req XMLHttpRequest (chrome)
 * @author Nils Maier
 */
function NotificationCallbacks(req) {
  throw new Error("trying to initiate an abstract NotificationCallbacks");
}
NotificationCallbacks.prototype = {
  init: function(req) {
    // rewrite notification callbacks
    this._channel = req.channel;
    this._notificationCallbacks = this._channel.notificationCallbacks;
    this._channel.notificationCallbacks = this;
  },
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
  getInterface: function(iid) {
    try {
      return this.query(iid);
    }
    catch (ex) {
      return this.queryOriginal(iid);
    }
  },
  queryOriginal: function(iid) {
    if (this._notificationCallbacks) {
      return this._notificationCallbacks.getInterface(iid);
    }
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}

/**
 * Ignore (specific) redirects
 * @param req XMLHttpRequest (chrome)
 * @author Nils Maier
 */
function IgnoreRedirect(req, ignoreFlags) {
  this.init(req);
  this.ignoreFlags = ignoreFlags;
}
IgnoreRedirect.prototype = {
  __proto__: NotificationCallbacks.prototype,
  query: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
  asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
    if (this.ignoreFlags & flags) {
      // must throw here, not call callback.onRedirectVerifyCallback,
      // or else it will completely cancel the request
      throw Cr.NS_ERROR_UNEXPECTED;
    }

    try {
      let ces = this.queryOriginal(Ci.nsIChannelEventSink);
      if (ces) {
        ces.asyncOnChannelRedirect(oldChannel, newChannel, flags, callback);
        return;
      }
    }
    catch (ex) {}

    callback.onRedirectVerifyCallback(Cr.NS_OK);
  }
};


function GM_xmlhttpRequester(unsafeContentWin, originUrl, aScript) {
  this.unsafeContentWin = unsafeContentWin;
  this.originUrl = originUrl;
  this.script = aScript;
}
exports.GM_xmlhttpRequester = GM_xmlhttpRequester;

// this function gets called by user scripts in content security scope to
// start a cross-domain xmlhttp request.
//
// details should look like:
// {method,url,onload,onerror,onreadystatechange,headers,data}
// headers should be in the form {name:value,name:value,etc}
// can't support mimetype because i think it's only used for forcing
// text/xml and we can't support that
GM_xmlhttpRequester.prototype.contentStartRequest = function(details) {
  try {
    // Validate and parse the (possibly relative) given URL.
    var uri = NetUtil.newURI(details.url, null, NetUtil.newURI(this.originUrl));
    var url = uri.spec;
  } catch (e) {
    // A malformed URL won't be parsed properly.
    //throw new Error(Scriptish_stringBundle("error.api.reqURL") + ": " + details.url);
    console.error(e);
  }

  // check if the script is allowed to access the url
  if (!this.script.matchesDomain(url))
    throw new Error(
        "User script is attempting access to restricted domain '" + uri.host + "'",
        this.script.fileURL);

  // This is important - without it, GM_xmlhttpRequest can be used to get
  // access to things like files and chrome. Careful.
  switch (uri.scheme) {
    case "http":
    case "https":
    case "ftp":
      var req = Instances.xhr;
      this.chromeStartRequest(url, details, req);
      break;
    default:
      throw new Error(Scriptish_stringBundle("error.api.reqURL.scheme") + ": " + details.url);
  }

  return {
    abort: function() {
      req.abort();
    }
  };
};

// this function is intended to be called in chrome's security context, so
// that it can access other domains without security warning
GM_xmlhttpRequester.prototype.chromeStartRequest =
    function(safeUrl, details, req) {
  this.setupRequestEvent(this.unsafeContentWin, req, "onload", details);
  this.setupRequestEvent(this.unsafeContentWin, req, "onerror", details);
  this.setupRequestEvent(
      this.unsafeContentWin, req, "onreadystatechange", details);

  if (details.mozBackgroundRequest) req.mozBackgroundRequest = true;

  req.open(
      details.method || "GET",
      safeUrl,
      true,
      details.user || "",
      details.password || ""
      );

  if (details.overrideMimeType) req.overrideMimeType(details.overrideMimeType);

  if (details.ignoreCache)
    req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; // bypass cache

  if (details.ignoreRedirect)
    new IgnoreRedirect(req,
      Ci.nsIChannelEventSink.REDIRECT_TEMPORARY | Ci.nsIChannelEventSink.REDIRECT_PERMANENT);
  if (details.ignoreTempRedirect)
    new IgnoreRedirect(req, Ci.nsIChannelEventSink.REDIRECT_TEMPORARY);
  if (details.ignorePermanentRedirect)
    new IgnoreRedirect(req, Ci.nsIChannelEventSink.REDIRECT_PERMANENT);

  let redirectionLimit = null;
  if (details.failOnRedirect) {
    redirectionLimit = 0;
  }
  if ("redirectionLimit" in details) {
    if (details.redirectionLimit < 0 || details.redirectionLimit > 10) {
      throw new Error("redirectionLimit must be within (0, 10), but it is " + details.redirectionLimit);
    }
    redirectionLimit = details.redirectionLimit;
  }
  if (redirectionLimit !== null && req.channel instanceof Ci.nsIHttpChannel) {
    req.channel.redirectionLimit = redirectionLimit;
  }

  if (details.headers) {
    var headers = details.headers;

    for (var prop in headers) {
      if (Object.prototype.hasOwnProperty.call(headers, prop))
        req.setRequestHeader(prop, headers[prop]);
    }
  }

  var body = details.data ? details.data : null;
  if (details.binary) req.sendAsBinary(body);
  else req.send(body);
}

// arranges for the specified 'event' on xmlhttprequest 'req' to call the
// method by the same name which is a property of 'details' in the content
// window's security context.
GM_xmlhttpRequester.prototype.setupRequestEvent =
    function(unsafeContentWin, req, event, details) {
  var origMimeType = details.overrideMimeType;
  var script = this.script;

  if (details[event]) {
    req[event] = function() {
      var responseState = {
        // can't support responseXML because security won't
        // let the browser call properties on it
        responseText: req.responseText,
        readyState: req.readyState,
        responseHeaders: null,
        status: null,
        statusText: null,
        finalUrl: null
      };
      if (4 == req.readyState && 'onerror' != event) {
        responseState.responseHeaders = req.getAllResponseHeaders();
        responseState.status = req.status;
        responseState.statusText = req.statusText;
        if (MIME_JSON.test(origMimeType)
            || MIME_JSON.test(details.overrideMimeType)
            || MIME_JSON.test(req.channel.contentType)) {
          try {
            responseState.responseJSON = JSON.parse(req.responseText);
          } catch (e) {
            responseState.responseJSON = {};
          }
        }
        responseState.finalUrl = req.channel.URI.spec;
      }

      GM_apiSafeCallback(
          unsafeContentWin, script, details, details[event], [responseState]);
    }
  }
}

// TODO: replace!!
function GM_apiSafeCallback(aWindow, aScript, aThis, aCb, aArgs) {
  aCb.apply(aThis, aArgs);
}
