/* 
 Copyright 2007-2010 Xmarks Inc.
 
 foxmarks-network.js: implements network interface to Syncd2.
 
 */

const NS_ERROR_REDIRECT_LOOP = 2152398879;
const NS_ERROR_CWD_ERROR = 0x804b0016;

/*

 The Request class handles communication with a server. It is essentially
 an HTTP Javascript remote object broker, delivering objects from the client
 to the server and retrieving Javascript objects from the server in return.
 Request has built-in support for the Foxmarks authentication protocol,
 initiated by a 302 Redirect response.

 Arguments:
     method: a string indicating which HTTP method to use.
     url: either a string or an object containing any of
        protocol, host, and path. If the caller provides a string, that
        literal string is used as the target url. If an object is
        supplied, Request uses whatever components are provided to construct
        a final url, employing defaults for missing components.
     obj: the object to be transmitted to the server.
    opt: an object that contains optional information as described below

 Return value:
    Three different types of errors can occur in processing a request.
    1) Network error: DNS failure, connection reset, etc.
    2) Transport failure: HTTP 404, etc.
    3) App failure: syncd rejects a request because of revision mismatch.

 If an error occurs, we return an integer status code in response.status.


 NEW opt parameter is a object/dict with optional values
    isAuthRequest - whether this is an authenticate request
    headers - any http headers that should be passed in
    ignoreBody - don't do any conversion of the body
    ignoreAuthTokens - don't update or check for authtokens
    noAuthDialog - authenticate but only if it can be done w/o UI
    noJSON - the data being returned isn't json so don't parse it
 */


EXPORTED_SYMBOLS = ["Request"]

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;

var Xmarks;
if(Xmarks === undefined){
    Xmarks = {};
    Cu.import('resource://xmarks/settings.jsm', Xmarks);
    Cu.import('resource://Xmarks/service.jsm', Xmarks);
}

Cu.import("resource://xmarks/base64.jsm");

// Make sure that we've got JSON additions to fundamental objects
var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
    getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://foxmarks/content/foxmarks-json.js", null);
loader.loadSubScript("chrome://foxmarks/content/foxmarks-utils.js", null);
loader.loadSubScript("chrome://foxmarks/content/foxmarks-uitools.js", null);

function Request(method, url, bodyobj, opt) {
    opt = opt || {};
    this._channel = null;
    this._streamLoader = null;
    this._callback = null;
    this._triedAuth = false;
    this._headers = opt.headers;
    this._ignoreBody = opt.ignoreBody;
    this._ignoreAuthTokens = opt.ignoreAuthTokens;
    this._noJSON = opt.noJSON;
    this._noAuthDialog = opt.noAuthDialog;
    try {
        this.JSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
    } catch(e){
        this.JSON = null;
    }
    
    // Argument url is either a string or an object
    // consisting of protocol, host, and path.
    // If it's a string, just take the string that's been
    // given. If it's an object, assemble the components
    // (applying defaults as appropriate) into a url.

    if (typeof(url) == "object") {

        // Set up defaults.
        if (!url.protocol) {
            if (Xmarks.gSettings.securityLevel == 1 ||
                (Xmarks.gSettings.securityLevel == 0 && opt.isAuthRequest)) {
                    url.protocol = "https";
            } else {
                url.protocol = "http";
            }
        }
        if (!url.host) url.host = Xmarks.gSettings.host;
        if (!url.path) url.path = "/";
        if (url.path[0] != "/") url.path = "/" + url.path;
        this._url = url.protocol + "://" + url.host + url.path;
    } else {
        this._url = url;
    }
    this._bodyobj = bodyobj;
    this._method = method;
    this._isAuthRequest = opt.isAuthRequest;
}

Request.prototype = {

    /*

    Process a request. Given the protocol, host, and path, construct a
    url and execute the specified method against that url, inserting
    the given message body.

    When request completes, it calls the provided callback function,
    passing a single object (the "response object"). 

    */

    Start: function(callback) {
        Xmarks.LogWrite(">>> " + this._method + " " + 
            this._StripPassword(this._url));
        if (this._bodyobj && !this._isAuthRequest) {
            Xmarks.LogWrite(">>> Body is: " + 
                (!Xmarks.gSettings.getDebugOption("no-verbose") ?
                this._bodyobj.toJSONString() : "(disabled)"));
        }
        this._callback = callback;

        // Create a channel.
        var ios = Cc["@mozilla.org/network/io-service;1"].
            getService(Ci.nsIIOService);
        try {
            this._channel = ios.newChannelFromURI(ios.newURI(this._url, 
                    "UTF-8", null));
        } catch (e) {
            Xmarks.LogWrite("Couldn't create channel from " + 
                this._url + ", error:" + e.toSource());
            callback(1008);
            return;
        }

        this._channel.QueryInterface(Ci.nsIUploadChannel);
        this._channel.loadFlags |= 
            (Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHE);

        // If we have a body to transmit, set it up as an upload stream.
        if (this._bodyobj) {
            var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                createInstance(Ci.nsIScriptableUnicodeConverter);
            converter.charset = "UTF-8";
            var stream = converter.convertToInputStream(
                this._bodyobj.toJSONString());
            this._channel.setUploadStream(stream, "application/json", -1);
        }

        // Special setup only for HTTP channels.
        if (this._channel instanceof Ci.nsIHttpChannel) {

            // Set the channel's method if necessary.
            if (this._method) {
                this._channel.requestMethod = this._method;
            }

            // Disable redirection.
            this._channel.redirectionLimit = 0;
            this._channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;

            // Set the user agent.
            this._channel.setRequestHeader("User-Agent", 
                this._channel.getRequestHeader("User-Agent") + 
                    " Xmarks-Fx/" + Xmarks.FoxmarksVersion(), false);

            if(!this._ignoreAuthTokens){
                var auth = Xmarks.gSettings.auth || "";
                if(auth.length > 0){
                    this._channel.setRequestHeader("Authorization", 
                        "XMAuth " + auth, false);
                    this._channel.setRequestHeader("X-Xmarks-Auth",
                        auth, false);
                }
            }

            // Set other headers if provided.
            if (this._headers) {
                var self = this;
                forEach(this._headers, function(v, k) {
                    self._channel.setRequestHeader(k, v, false);
                } );
            }
        }

        // Create a stream loader for retrieving the response.
        this._streamLoader = Cc["@mozilla.org/network/stream-loader;1"]
            .createInstance(Ci.nsIStreamLoader);
 
        // Fire it up. Note that this results in the channel's AsyncOpen
        // being called; if we have set an upload stream above, it will be
        // transmitted as part of the request. This is a bit strange, but
        // appears to be correct.
        try {
            // Before Firefox 3...
            this._streamLoader.init(this._channel, this, null);
        } catch(e) {
            // Firefox 3 style...
            this._streamLoader.init(this);
            this._channel.asyncOpen(this._streamLoader, null);
        }
        return;
    },

    Cancel: function() {
        if (this._channel && this._channel.isPending()){
            Xmarks.LogWrite("Cancelling Network Request");
            this._channel.cancel(0x804b0002);
        }
    },

    onStreamComplete: function(loader, ctxt, status, resultLength, result) {
        var response = {};
        if (Components.isSuccessCode(status)) {
            try {
                status = this._channel.responseStatus || 200;
            } catch (e) {
                this._callback( { status: 1005, errormsg: 
                        "Disable automatic proxy settings detection."} );
                return;
            }

            var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                createInstance(Ci.nsIScriptableUnicodeConverter);
            converter.charset = "utf-8";
            var msg = "";
            if (status == 200 || status == 201 || status == 204) {
                if(!this._ignoreBody){
                    msg = converter.convertFromByteArray(result, resultLength);
                }
                if (msg && msg.length && !this._ignoreBody) {
                    if(this._noJSON){
                        response = {
                            status: 0,
                            txt: msg
                        };
                    } else {
                        try {
                            response = this.JSON ? this.JSON.decode(msg) :
                                eval("(" + msg + ")");
                            if(response.auth && response.auth.length > 0){
                                Xmarks.gSettings.auth = response.auth;
                            }

                            if(response.username && 
                              response.username.toLowerCase() != 
                                Xmarks.gSettings.username.toLowerCase() && 
                              Xmarks.gSettings.username.indexOf('@') < 0){
                                response = { status: 1010, 
                                    errormsg: "Invalid server response" }; 
                                Xmarks.LogWrite("CurrUser: " + 
                                    Xmarks.gSettings.username);
                                Xmarks.LogWrite("ServerUser: " + 
                                    response.username);
                                Xmarks.LogWrite("JSON Response: " + msg);
                            }
                        } catch(e) {
                            Xmarks.LogWrite("Failed to parse message: " + msg);
                            response = { status: 1010, 
                                errormsg: "Invalid server response" }; 
                        }
                    }
                }
                if (response.status == null) {
                    response.status = 0;
                }

                // New Auth stuff
                if(response.status == 302){
                    Xmarks.LogWrite(">>> Authenticating...");

                    if (this._triedAuth) {
                        Xmarks.LogWrite("In authentication loop.");
                        Xmarks.LogWrite("Possible proxy server caching issue.");
                        this._callback( { status: 1012 } );
                        return;
                    }

                    if (this._noAuthDialog) {
                        Xmarks.LogWrite("Client skips authentication");
                        this._callback( { status: 403 } );
                        return;
                    }

                    Cu.import("resource://xmarks/service.jsm", Xmarks);
                    var s = Xmarks.DoModalAuth(true, true);
                    if (!s) {
                        // ... and try again.
                        this._triedAuth = true;
                        this.Start(this._callback);
                    } else {
                        this._callback( { status: s } );
                    }
                    return;
                }
                
            } else {
                response = { status: status, "errormsg" : msg };
            }
            try {   // Pass back the etag if there is one.
                response.etag = this._channel.getResponseHeader("Etag");
            } catch (e) {}

            if(Xmarks.gSettings.getDebugOption("no-verbose")){
                Xmarks.LogWrite(">>> Callback (disabled)");
            } else {
                var oldauth = response.auth;
                delete response.auth;
                if(this._noJSON){
                    Xmarks.LogWrite(">>> Callback (not json, status: " + 
                        response.status + ")");
                } else {
                    Xmarks.LogWrite(">>> Callback " + response.toSource());
                }
                response.auth = oldauth;
            }
            this._callback(response);
            return;
        } else {
            if (status == NS_ERROR_REDIRECT_LOOP && !this._isAuthRequest) {
                this._callback( { status: status });
                return;
            } else if (status == NS_ERROR_CWD_ERROR) {
                Xmarks.LogWrite("CWD error (mapping to 404)");
                this._callback( { status: 404 } );
            } else {
                Xmarks.LogWrite("network request failed; status is " +
                        status.toString(16));
                this._callback( { status: status });
            }
        }
    },

    _StripPassword: function(url) {
        var exp = /^(.*):(.*)@(.*)$/
        var match = url.match(exp);
        return match ? match[1] + "@" + match[3] : url;
    }
}

