/* 
 Copyright 2007-2009 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
 */


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 (url instanceof 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 |= 
            Components.interfaces.nsIRequest.LOAD_BYPASS_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;
            }

            //try {
            //    dump("cookie: " + this._channel.getResponseHeader("Set-Cookie") + "\n");
           // }catch(e){}
            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){
                    function RestartAfterAuth(response) {
                        if (response.status == 0){
                            Xmarks.gSettings.auth = response.auth;
                            // We're authenticated. Retry original request.
                            self.Start(self._callback);
                            return;
                        } else {
                            // Auth failed. Return failure code.
                            if (response.message == "Wrong username or password.") {
                                response.status = 401;
                            }
                            self._callback(response);
                            return;
                        }
                    }

                    Xmarks.LogWrite(">>> Authenticating...");

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

                    if (this._noAuthDialog) {
                        if ((Xmarks.gSettings.rememberPassword && 
                                Xmarks.gSettings.masterPasswordSet) ||
                            (!Xmarks.gSettings.rememberPassword && 
                                !Xmarks.gSettings.sessionPassword)) {
                            Xmarks.LogWrite("Client skips authentication");
                            this._callback( { status: 403 } );
                            return;
                        }
                    }

                    var location = response.authtoken_location;
                    var self = this;    // keep a handle on the original request
                    // parse out the protocol, host, and path
                    var colonslashslash = location.indexOf("://");
                    var nextslash = location.indexOf("/", colonslashslash + 3);
                    if (colonslashslash < 0 || nextslash < 0) {
                        throw Error("Couldn't parse location string " + location);
                    }
                    var url = {}
                    url.host = location.substr(colonslashslash + 3, 
                        nextslash - (colonslashslash + 3)); 
                    url.host = Xmarks.gSettings.getCharPref('host-login', url.host);

                    url.path = location.substr(nextslash);
                    Xmarks.LogWrite("Requesting password.");
                    try {
                        var pw = Xmarks.gSettings.password;
                    } catch (e) {
                        Xmarks.LogWrite("User canceled password request.");
                        this._callback(2);
                        return;
                    }
                    var authReq = new Request(
                        "POST", 
                        url,
                        { 
                            username: Xmarks.gSettings.username, 
                            password: Base64.encode(pw)
                        },
                        { 
                            isAuthRequest: true
                        }
                    );
                    this._triedAuth = true;
                    authReq.Start(RestartAfterAuth);
                    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;
    }
}

