/* 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, Cu} = require("chrome");
loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");

/**
 * 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.
 */
let NetworkHelper = {
  /**
   * Converts aText with a given aCharset to unicode.
   *
   * @param string aText
   *        Text to convert.
   * @param string aCharset
   *        Charset to convert the text to.
   * @returns string
   *          Converted text.
   */
  convertToUnicode: function NH_convertToUnicode(aText, aCharset)
  {
    let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
               createInstance(Ci.nsIScriptableUnicodeConverter);
    try {
      conv.charset = aCharset || "UTF-8";
      return conv.ConvertToUnicode(aText);
    }
    catch (ex) {
      return aText;
    }
  },

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

   /**
   * Reads the posted text from aRequest.
   *
   * @param nsIHttpChannel aRequest
   * @param string aCharset
   *        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 aRequest
   *          otherwise null.
   */
  readPostTextFromRequest: function NH_readPostTextFromRequest(aRequest, aCharset)
  {
    if (aRequest instanceof Ci.nsIUploadChannel) {
      let iStream = aRequest.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, aCharset);

      // 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 aDocShell
   * @param string aCharset
   * @returns string or null
   *          Returns the posted string if it was possible to read from
   *          aDocShell otherwise null.
   */
  readPostTextFromPage: function NH_readPostTextFromPage(aDocShell, aCharset)
  {
    let webNav = aDocShell.QueryInterface(Ci.nsIWebNavigation);
    return this.readPostTextFromPageViaWebNav(webNav, aCharset);
  },

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

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

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

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

  /**
   * Gets the topFrameElement that is associated with aRequest.
   *
   * @param nsIHttpChannel aRequest
   * @returns nsIDOMElement|null
   *          The top frame element for the given request, if available.
   */
  getTopFrameForRequest: function NH_getTopFrameForRequest(aRequest)
  {
    try {
      return this.getRequestLoadContext(aRequest).topFrameElement;
    } catch (ex) {
      // request loadContent is not always available.
    }
    return null;
  },

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

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

    try {
      return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
    } catch (ex) { }

    return null;
  },

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

    // 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, (aInputStream, aStatusCode, aRequest) => {
      if (!components.isSuccessCode(aStatusCode)) {
        aCallback(null);
        return;
      }

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

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

  /**
   * Parse a raw Cookie header value.
   *
   * @param string aHeader
   *        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 NH_parseCookieHeader(aHeader)
  {
    let cookies = aHeader.split(";");
    let result = [];

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

    return result;
  },

  /**
   * Parse a raw Set-Cookie header value.
   *
   * @param string aHeader
   *        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 NH_parseSetCookieHeader(aHeader)
  {
    let rawCookies = aHeader.split(/\r\n|\n|\r/);
    let cookies = [];

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

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

      parts.forEach(function(aPart) {
        let part = aPart.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) { }
          }
        }
      });

      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",
    "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",
    "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",
  },

  /**
   * Check if the given MIME type is a text-only MIME type.
   *
   * @param string aMimeType
   * @return boolean
   */
  isTextMimeType: function NH_isTextMimeType(aMimeType)
  {
    if (aMimeType.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(aMimeType)) {
      return true;
    }

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

      default:
        return false;
    }
  },
};

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