/* vim:set ts=2 sw=2 sts=2 et: */
/*
 * Software License Agreement (BSD License)
 *
 * Copyright (c) 2007, Parakey Inc.
 * All rights reserved.
 *
 * Redistribution and use of this software in source and binary forms,
 * with or without modification, are permitted provided that the
 * following conditions are met:
 *
 * * Redistributions of source code must retain the above
 *   copyright notice, this list of conditions and the
 *   following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the
 *   following disclaimer in the documentation and/or other
 *   materials provided with the distribution.
 *
 * * Neither the name of Parakey Inc. nor the names of its
 *   contributors may be used to endorse or promote products
 *   derived from this software without specific prior
 *   written permission of Parakey Inc.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * Creator:
 *  Joe Hewitt
 * Contributors
 *  John J. Barton (IBM Almaden)
 *  Jan Odvarko (Mozilla Corp.)
 *  Max Stepanov (Aptana Inc.)
 *  Rob Campbell (Mozilla Corp.)
 *  Hans Hillen (Paciello Group, Mozilla)
 *  Curtis Bartley (Mozilla Corp.)
 *  Mike Collins (IBM Almaden)
 *  Kevin Decker
 *  Mike Ratcliffe (Comartis AG)
 *  Hernan Rodríguez Colmeiro
 *  Austin Andrews
 *  Christoph Dorn
 *  Steven Roussey (AppCenter Inc, Network54)
 *  Mihai Sucan (Mozilla Corp.)
 */

"use strict";

const {components, Cc, Ci} = require("chrome");
loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const Services = require("Services");

// The cache used in the `nsIURL` function.
const gNSURLStore = new Map();

/**
 * Helper object for networking stuff.
 *
 * Most of the following functions have been taken from the Firebug source. They
 * have been modified to match the Firefox coding rules.
 */
var NetworkHelper = {
  /**
   * Converts text with a given charset to unicode.
   *
   * @param string text
   *        Text to convert.
   * @param string charset
   *        Charset to convert the text to.
   * @returns string
   *          Converted text.
   */
  convertToUnicode: function(text, charset) {
    let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
        .createInstance(Ci.nsIScriptableUnicodeConverter);
    try {
      conv.charset = charset || "UTF-8";
      return conv.ConvertToUnicode(text);
    } catch (ex) {
      return text;
    }
  },

  /**
   * Reads all available bytes from stream and converts them to charset.
   *
   * @param nsIInputStream stream
   * @param string charset
   * @returns string
   *          UTF-16 encoded string based on the content of stream and charset.
   */
  readAndConvertFromStream: function(stream, charset) {
    let text = null;
    try {
      text = NetUtil.readInputStreamToString(stream, stream.available());
      return this.convertToUnicode(text, charset);
    } catch (err) {
      return text;
    }
  },

   /**
   * Reads the posted text from request.
   *
   * @param nsIHttpChannel request
   * @param string charset
   *        The content document charset, used when reading the POSTed data.
   * @returns string or null
   *          Returns the posted string if it was possible to read from request
   *          otherwise null.
   */
  readPostTextFromRequest: function(request, charset) {
    if (request instanceof Ci.nsIUploadChannel) {
      let iStream = request.uploadStream;

      let isSeekableStream = false;
      if (iStream instanceof Ci.nsISeekableStream) {
        isSeekableStream = true;
      }

      let prevOffset;
      if (isSeekableStream) {
        prevOffset = iStream.tell();
        iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
      }

      // Read data from the stream.
      let text = this.readAndConvertFromStream(iStream, charset);

      // Seek locks the file, so seek to the beginning only if necko hasn't
      // read it yet, since necko doesn't seek to 0 before reading (at lest
      // not till 459384 is fixed).
      if (isSeekableStream && prevOffset == 0) {
        iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
      }
      return text;
    }
    return null;
  },

  /**
   * Reads the posted text from the page's cache.
   *
   * @param nsIDocShell docShell
   * @param string charset
   * @returns string or null
   *          Returns the posted string if it was possible to read from
   *          docShell otherwise null.
   */
  readPostTextFromPage: function(docShell, charset) {
    let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
    return this.readPostTextFromPageViaWebNav(webNav, charset);
  },

  /**
   * Reads the posted text from the page's cache, given an nsIWebNavigation
   * object.
   *
   * @param nsIWebNavigation webNav
   * @param string charset
   * @returns string or null
   *          Returns the posted string if it was possible to read from
   *          webNav, otherwise null.
   */
  readPostTextFromPageViaWebNav: function(webNav, charset) {
    if (webNav instanceof Ci.nsIWebPageDescriptor) {
      let descriptor = webNav.currentDescriptor;

      if (descriptor instanceof Ci.nsISHEntry && descriptor.postData &&
          descriptor instanceof Ci.nsISeekableStream) {
        descriptor.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);

        return this.readAndConvertFromStream(descriptor, charset);
      }
    }
    return null;
  },

  /**
   * Gets the web appId that is associated with request.
   *
   * @param nsIHttpChannel request
   * @returns number|null
   *          The appId for the given request, if available.
   */
  getAppIdForRequest: function(request) {
    try {
      return this.getRequestLoadContext(request).appId;
    } catch (ex) {
      // request loadContent is not always available.
    }
    return null;
  },

  /**
   * Gets the topFrameElement that is associated with request. This
   * works in single-process and multiprocess contexts. It may cross
   * the content/chrome boundary.
   *
   * @param nsIHttpChannel request
   * @returns nsIDOMElement|null
   *          The top frame element for the given request.
   */
  getTopFrameForRequest: function(request) {
    try {
      return this.getRequestLoadContext(request).topFrameElement;
    } catch (ex) {
      // request loadContent is not always available.
    }
    return null;
  },

  /**
   * Gets the nsIDOMWindow that is associated with request.
   *
   * @param nsIHttpChannel request
   * @returns nsIDOMWindow or null
   */
  getWindowForRequest: function(request) {
    try {
      return this.getRequestLoadContext(request).associatedWindow;
    } catch (ex) {
      // TODO: bug 802246 - getWindowForRequest() throws on b2g: there is no
      // associatedWindow property.
    }
    return null;
  },

  /**
   * Gets the nsILoadContext that is associated with request.
   *
   * @param nsIHttpChannel request
   * @returns nsILoadContext or null
   */
  getRequestLoadContext: function(request) {
    try {
      return request.notificationCallbacks.getInterface(Ci.nsILoadContext);
    } catch (ex) {
      // Ignore.
    }

    try {
      return request.loadGroup.notificationCallbacks
        .getInterface(Ci.nsILoadContext);
    } catch (ex) {
      // Ignore.
    }

    return null;
  },

  /**
   * Determines whether the request has been made for the top level document.
   *
   * @param nsIHttpChannel request
   * @returns Boolean True if the request represents the top level document.
   */
  isTopLevelLoad: function(request) {
    if (request instanceof Ci.nsIChannel) {
      let loadInfo = request.loadInfo;
      if (loadInfo && loadInfo.parentOuterWindowID == loadInfo.outerWindowID) {
        return (request.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI);
      }
    }

    return false;
  },

  /**
   * Loads the content of url from the cache.
   *
   * @param string url
   *        URL to load the cached content for.
   * @param string charset
   *        Assumed charset of the cached content. Used if there is no charset
   *        on the channel directly.
   * @param function callback
   *        Callback that is called with the loaded cached content if available
   *        or null if something failed while getting the cached content.
   */
  loadFromCache: function(url, charset, callback) {
    let channel = NetUtil.newChannel({uri: url,
                                      loadUsingSystemPrincipal: true});

    // Ensure that we only read from the cache and not the server.
    channel.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE |
      Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
      Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;

    NetUtil.asyncFetch(
      channel,
      (inputStream, statusCode, request) => {
        if (!components.isSuccessCode(statusCode)) {
          callback(null);
          return;
        }

        // Try to get the encoding from the channel. If there is none, then use
        // the passed assumed charset.
        let requestChannel = request.QueryInterface(Ci.nsIChannel);
        let contentCharset = requestChannel.contentCharset || charset;

        // Read the content of the stream using contentCharset as encoding.
        callback(this.readAndConvertFromStream(inputStream, contentCharset));
      });
  },

  /**
   * Parse a raw Cookie header value.
   *
   * @param string header
   *        The raw Cookie header value.
   * @return array
   *         Array holding an object for each cookie. Each object holds the
   *         following properties: name and value.
   */
  parseCookieHeader: function(header) {
    let cookies = header.split(";");
    let result = [];

    cookies.forEach(function(cookie) {
      let equal = cookie.indexOf("=");
      let name = cookie.substr(0, equal);
      let value = cookie.substr(equal + 1);
      result.push({name: unescape(name.trim()),
                   value: unescape(value.trim())});
    });

    return result;
  },

  /**
   * Parse a raw Set-Cookie header value.
   *
   * @param string header
   *        The raw Set-Cookie header value.
   * @return array
   *         Array holding an object for each cookie. Each object holds the
   *         following properties: name, value, secure (boolean), httpOnly
   *         (boolean), path, domain and expires (ISO date string).
   */
  parseSetCookieHeader: function(header) {
    let rawCookies = header.split(/\r\n|\n|\r/);
    let cookies = [];

    rawCookies.forEach(function(cookie) {
      let equal = cookie.indexOf("=");
      let name = unescape(cookie.substr(0, equal).trim());
      let parts = cookie.substr(equal + 1).split(";");
      let value = unescape(parts.shift().trim());

      cookie = {name: name, value: value};

      parts.forEach(function(part) {
        part = part.trim();
        if (part.toLowerCase() == "secure") {
          cookie.secure = true;
        } else if (part.toLowerCase() == "httponly") {
          cookie.httpOnly = true;
        } else if (part.indexOf("=") > -1) {
          let pair = part.split("=");
          pair[0] = pair[0].toLowerCase();
          if (pair[0] == "path" || pair[0] == "domain") {
            cookie[pair[0]] = pair[1];
          } else if (pair[0] == "expires") {
            try {
              pair[1] = pair[1].replace(/-/g, " ");
              cookie.expires = new Date(pair[1]).toISOString();
            } catch (ex) {
              // Ignore.
            }
          }
        }
      });

      cookies.push(cookie);
    });

    return cookies;
  },

  // This is a list of all the mime category maps jviereck could find in the
  // firebug code base.
  mimeCategoryMap: {
    "text/plain": "txt",
    "text/html": "html",
    "text/xml": "xml",
    "text/xsl": "txt",
    "text/xul": "txt",
    "text/css": "css",
    "text/sgml": "txt",
    "text/rtf": "txt",
    "text/x-setext": "txt",
    "text/richtext": "txt",
    "text/javascript": "js",
    "text/jscript": "txt",
    "text/tab-separated-values": "txt",
    "text/rdf": "txt",
    "text/xif": "txt",
    "text/ecmascript": "js",
    "text/vnd.curl": "txt",
    "text/x-json": "json",
    "text/x-js": "txt",
    "text/js": "txt",
    "text/vbscript": "txt",
    "view-source": "txt",
    "view-fragment": "txt",
    "application/xml": "xml",
    "application/xhtml+xml": "xml",
    "application/atom+xml": "xml",
    "application/rss+xml": "xml",
    "application/vnd.mozilla.maybe.feed": "xml",
    "application/vnd.mozilla.xul+xml": "xml",
    "application/javascript": "js",
    "application/x-javascript": "js",
    "application/x-httpd-php": "txt",
    "application/rdf+xml": "xml",
    "application/ecmascript": "js",
    "application/http-index-format": "txt",
    "application/json": "json",
    "application/x-js": "txt",
    "application/x-mpegurl": "txt",
    "application/vnd.apple.mpegurl": "txt",
    "multipart/mixed": "txt",
    "multipart/x-mixed-replace": "txt",
    "image/svg+xml": "svg",
    "application/octet-stream": "bin",
    "image/jpeg": "image",
    "image/jpg": "image",
    "image/gif": "image",
    "image/png": "image",
    "image/bmp": "image",
    "application/x-shockwave-flash": "flash",
    "video/x-flv": "flash",
    "audio/mpeg3": "media",
    "audio/x-mpeg-3": "media",
    "video/mpeg": "media",
    "video/x-mpeg": "media",
    "video/vnd.mpeg.dash.mpd": "xml",
    "audio/ogg": "media",
    "application/ogg": "media",
    "application/x-ogg": "media",
    "application/x-midi": "media",
    "audio/midi": "media",
    "audio/x-mid": "media",
    "audio/x-midi": "media",
    "music/crescendo": "media",
    "audio/wav": "media",
    "audio/x-wav": "media",
    "text/json": "json",
    "application/x-json": "json",
    "application/json-rpc": "json",
    "application/x-web-app-manifest+json": "json",
    "application/manifest+json": "json"
  },

  /**
   * Check if the given MIME type is a text-only MIME type.
   *
   * @param string mimeType
   * @return boolean
   */
  isTextMimeType: function(mimeType) {
    if (mimeType.indexOf("text/") == 0) {
      return true;
    }

    // XML and JSON often come with custom MIME types, so in addition to the
    // standard "application/xml" and "application/json", we also look for
    // variants like "application/x-bigcorp+xml". For JSON we allow "+json" and
    // "-json" as suffixes.
    if (/^application\/\w+(?:[\.-]\w+)*(?:\+xml|[-+]json)$/.test(mimeType)) {
      return true;
    }

    let category = this.mimeCategoryMap[mimeType] || null;
    switch (category) {
      case "txt":
      case "js":
      case "json":
      case "css":
      case "html":
      case "svg":
      case "xml":
        return true;

      default:
        return false;
    }
  },

  /**
   * Takes a securityInfo object of nsIRequest, the nsIRequest itself and
   * extracts security information from them.
   *
   * @param object securityInfo
   *        The securityInfo object of a request. If null channel is assumed
   *        to be insecure.
   * @param object httpActivity
   *        The httpActivity object for the request with at least members
   *        { private, hostname }.
   *
   * @return object
   *         Returns an object containing following members:
   *          - state: The security of the connection used to fetch this
   *                   request. Has one of following string values:
   *                    * "insecure": the connection was not secure (only http)
   *                    * "weak": the connection has minor security issues
   *                    * "broken": secure connection failed (e.g. expired cert)
   *                    * "secure": the connection was properly secured.
   *          If state == broken:
   *            - errorMessage: full error message from
   *                            nsITransportSecurityInfo.
   *          If state == secure:
   *            - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2.
   *            - cipherSuite: the cipher suite used in this connection.
   *            - cert: information about certificate used in this connection.
   *                    See parseCertificateInfo for the contents.
   *            - hsts: true if host uses Strict Transport Security,
   *                    false otherwise
   *            - hpkp: true if host uses Public Key Pinning, false otherwise
   *          If state == weak: Same as state == secure and
   *            - weaknessReasons: list of reasons that cause the request to be
   *                               considered weak. See getReasonsForWeakness.
   */
  parseSecurityInfo: function(securityInfo, httpActivity) {
    const info = {
      state: "insecure",
    };

    // The request did not contain any security info.
    if (!securityInfo) {
      return info;
    }

    /**
     * Different scenarios to consider here and how they are handled:
     * - request is HTTP, the connection is not secure
     *   => securityInfo is null
     *      => state === "insecure"
     *
     * - request is HTTPS, the connection is secure
     *   => .securityState has STATE_IS_SECURE flag
     *      => state === "secure"
     *
     * - request is HTTPS, the connection has security issues
     *   => .securityState has STATE_IS_INSECURE flag
     *   => .errorCode is an NSS error code.
     *      => state === "broken"
     *
     * - request is HTTPS, the connection was terminated before the security
     *   could be validated
     *   => .securityState has STATE_IS_INSECURE flag
     *   => .errorCode is NOT an NSS error code.
     *   => .errorMessage is not available.
     *      => state === "insecure"
     *
     * - request is HTTPS but it uses a weak cipher or old protocol, see
     *   http://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
     *   security/manager/ssl/nsNSSCallbacks.cpp#l1233
     * - request is mixed content (which makes no sense whatsoever)
     *   => .securityState has STATE_IS_BROKEN flag
     *   => .errorCode is NOT an NSS error code
     *   => .errorMessage is not available
     *      => state === "weak"
     */

    securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
    securityInfo.QueryInterface(Ci.nsISSLStatusProvider);

    const wpl = Ci.nsIWebProgressListener;
    const NSSErrorsService = Cc["@mozilla.org/nss_errors_service;1"]
                               .getService(Ci.nsINSSErrorsService);
    const SSLStatus = securityInfo.SSLStatus;
    if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
      const state = securityInfo.securityState;

      let uri = null;
      if (httpActivity.channel && httpActivity.channel.URI) {
        uri = httpActivity.channel.URI;
      }
      if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
        // it is not enough to look at the transport security info -
        // schemes other than https and wss are subject to
        // downgrade/etc at the scheme level and should always be
        // considered insecure
        info.state = "insecure";
      } else if (state & wpl.STATE_IS_SECURE) {
        // The connection is secure if the scheme is sufficient
        info.state = "secure";
      } else if (state & wpl.STATE_IS_BROKEN) {
        // The connection is not secure, there was no error but there's some
        // minor security issues.
        info.state = "weak";
        info.weaknessReasons = this.getReasonsForWeakness(state);
      } else if (state & wpl.STATE_IS_INSECURE) {
        // This was most likely an https request that was aborted before
        // validation. Return info as info.state = insecure.
        return info;
      } else {
        DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo",
          "Security state " + state + " has no known STATE_IS_* flags.");
        return info;
      }

      // Cipher suite.
      info.cipherSuite = SSLStatus.cipherName;

      // Protocol version.
      info.protocolVersion =
        this.formatSecurityProtocol(SSLStatus.protocolVersion);

      // Certificate.
      info.cert = this.parseCertificateInfo(SSLStatus.serverCert);

      // HSTS and HPKP if available.
      if (httpActivity.hostname) {
        const sss = Cc["@mozilla.org/ssservice;1"]
                      .getService(Ci.nsISiteSecurityService);

        // SiteSecurityService uses different storage if the channel is
        // private. Thus we must give isSecureHost correct flags or we
        // might get incorrect results.
        let flags = (httpActivity.private) ?
                      Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;

        let host = httpActivity.hostname;

        info.hsts = sss.isSecureHost(sss.HEADER_HSTS, host, flags);
        info.hpkp = sss.isSecureHost(sss.HEADER_HPKP, host, flags);
      } else {
        DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo",
          "Could not get HSTS/HPKP status as hostname is not available.");
        info.hsts = false;
        info.hpkp = false;
      }
    } else {
      // The connection failed.
      info.state = "broken";
      info.errorMessage = securityInfo.errorMessage;
    }

    return info;
  },

  /**
   * Takes an nsIX509Cert and returns an object with certificate information.
   *
   * @param nsIX509Cert cert
   *        The certificate to extract the information from.
   * @return object
   *         An object with following format:
   *           {
   *             subject: { commonName, organization, organizationalUnit },
   *             issuer: { commonName, organization, organizationUnit },
   *             validity: { start, end },
   *             fingerprint: { sha1, sha256 }
   *           }
   */
  parseCertificateInfo: function(cert) {
    let info = {};
    if (cert) {
      info.subject = {
        commonName: cert.commonName,
        organization: cert.organization,
        organizationalUnit: cert.organizationalUnit,
      };

      info.issuer = {
        commonName: cert.issuerCommonName,
        organization: cert.issuerOrganization,
        organizationUnit: cert.issuerOrganizationUnit,
      };

      info.validity = {
        start: cert.validity.notBeforeLocalDay,
        end: cert.validity.notAfterLocalDay,
      };

      info.fingerprint = {
        sha1: cert.sha1Fingerprint,
        sha256: cert.sha256Fingerprint,
      };
    } else {
      DevToolsUtils.reportException("NetworkHelper.parseCertificateInfo",
        "Secure connection established without certificate.");
    }

    return info;
  },

  /**
   * Takes protocolVersion of SSLStatus object and returns human readable
   * description.
   *
   * @param Number version
   *        One of nsISSLStatus version constants.
   * @return string
   *         One of TLSv1, TLSv1.1, TLSv1.2 if @param version is valid,
   *         Unknown otherwise.
   */
  formatSecurityProtocol: function(version) {
    switch (version) {
      case Ci.nsISSLStatus.TLS_VERSION_1:
        return "TLSv1";
      case Ci.nsISSLStatus.TLS_VERSION_1_1:
        return "TLSv1.1";
      case Ci.nsISSLStatus.TLS_VERSION_1_2:
        return "TLSv1.2";
      default:
        DevToolsUtils.reportException("NetworkHelper.formatSecurityProtocol",
          "protocolVersion " + version + " is unknown.");
        return "Unknown";
    }
  },

  /**
   * Takes the securityState bitfield and returns reasons for weak connection
   * as an array of strings.
   *
   * @param Number state
   *        nsITransportSecurityInfo.securityState.
   *
   * @return Array[String]
   *         List of weakness reasons. A subset of { cipher } where
   *         * cipher: The cipher suite is consireded to be weak (RC4).
   */
  getReasonsForWeakness: function(state) {
    const wpl = Ci.nsIWebProgressListener;

    // If there's non-fatal security issues the request has STATE_IS_BROKEN
    // flag set. See http://hg.mozilla.org/mozilla-central/file/44344099d119
    // /security/manager/ssl/nsNSSCallbacks.cpp#l1233
    let reasons = [];

    if (state & wpl.STATE_IS_BROKEN) {
      let isCipher = state & wpl.STATE_USES_WEAK_CRYPTO;

      if (isCipher) {
        reasons.push("cipher");
      }

      if (!isCipher) {
        DevToolsUtils.reportException("NetworkHelper.getReasonsForWeakness",
          "STATE_IS_BROKEN without a known reason. Full state was: " + state);
      }
    }

    return reasons;
  },

  /**
   * Parse a url's query string into its components
   *
   * @param string queryString
   *        The query part of a url
   * @return array
   *         Array of query params {name, value}
   */
  parseQueryString: function(queryString) {
    // Make sure there's at least one param available.
    // Be careful here, params don't necessarily need to have values, so
    // no need to verify the existence of a "=".
    if (!queryString) {
      return null;
    }

    // Turn the params string into an array containing { name: value } tuples.
    let paramsArray = queryString.replace(/^[?&]/, "").split("&").map(e => {
      let param = e.split("=");
      return {
        name: param[0] ?
          NetworkHelper.convertToUnicode(unescape(param[0])) : "",
        value: param[1] ?
          NetworkHelper.convertToUnicode(unescape(param[1])) : ""
      };
    });

    return paramsArray;
  },

  /**
   * Helper for getting an nsIURL instance out of a string.
   */
  nsIURL: function(url, store = gNSURLStore) {
    if (store.has(url)) {
      return store.get(url);
    }

    let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
    store.set(url, uri);
    return uri;
  }
};

for (let prop of Object.getOwnPropertyNames(NetworkHelper)) {
  exports[prop] = NetworkHelper[prop];
}
