/*
 Copyright 2007-2008 Foxmarks Inc.

 foxmarks-password.js: component that implements the details of 
 password sync.

 */


function NiceProcess(iterator, funcDo, funcDone){
    var callback = {
        notify: function(timer){
            try {
                var s = Date.now();
                while (!iterator.done() && Date.now() - s < 100) {
                    funcDo(iterator);
                }
                if(iterator.done()){
                    // dump("ms: " + (Date.now() - s) + "\n");
                        funcDone(iterator);
                } else {
                    // dump("ms: " + (Date.now() - s) + "\n");
                        timer.initWithCallback(callback, 10,
                            Ci.nsITimer.TYPE_ONE_SHOT);
                }
            }catch(e){
                funcDone(iterator, e);
            }
        }
    };
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    timer.initWithCallback(callback, 10,
        Ci.nsITimer.TYPE_ONE_SHOT);
}

function PasswordDatasource(){
    this.Cc = Components.classes;
    this.Ci = Components.interfaces;

    this.JSON = this.Cc["@mozilla.org/dom/json;1"].createInstance(this.Ci.nsIJSON);

    this.encryptor = CreateAESManager();
}

PasswordDatasource.prototype = {
    DiscontinuityPrompt: function() {
        var sb = Xmarks.Bundle().GetStringFromName;

        // get a reference to the prompt service component.
        var ps = this.Cc["@mozilla.org/embedcomp/prompt-service;1"].
          getService(this.Ci.nsIPromptService);

        var flags = ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0 +
              ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_1 +
              ps.BUTTON_TITLE_CANCEL    * ps.BUTTON_POS_2;

        rv = ps.confirmEx(null, sb("passworddisc.title"), sb("passworddisc.body"), flags,
            sb("disc.merge"), sb("disc.download"), null, null, {});

        return rv;
    },
    // 2 - server, 1 -local, other-cancel
    _conflictDialog: function(dest, src){
        var retval = { button: 0 };
        var data = {
            local: { 
                site: src.hostname,
                username: src.username,
                password: src.password
            },
            server: {
                site: dest.hostname,
                username: dest.username,
                password: dest.password
            }
        };
        var topwin = Cc['@mozilla.org/appshell/window-mediator;1'].
            getService(Ci.nsIWindowMediator).
            getMostRecentWindow(null);
        
        if (!topwin) {
            Xmarks.LogWrite("conflictDialog: Couldn't find a topwin!");
            throw 4;
        }
        var win = topwin.openDialog(
            "chrome://foxmarks/content/foxmarks-passwordconflict.xul", "_blank",
            "chrome,dialog,modal,centerscreen", data, retval);
        if(!retval.button) {
            throw 2;
        }
       
       return retval.button;
    },
    ClobberDialog: function(len){
        
        if(len > 1)
            return true;
        var sb = Xmarks.Bundle().GetStringFromName;

        // get a reference to the prompt service component.
        var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"].
          getService(Ci.nsIPromptService);

        var flags = ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_0 +
              ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_1 +
              ps.BUTTON_TITLE_IS_STRING    * ps.BUTTON_POS_2;

        rv = ps.confirmEx(null, sb("pwclobber.title"), sb("pwclobber.body"), flags,
            null, sb("pwclobber.disable"), sb("pwclobber.server"),  null, {});

        if(rv == 1){
            Xmarks.gSettings.setSyncEnabled("passwords", false);
        }
        return rv == 2;
    },
    syncType: "passwords",
    getBaselineName: function(){
        return "xmarks-password-baseline-" + Xmarks.gSettings.hash + "json";
    },
    BaselineLoaded: function(baseline, callback) {
        var self = this;
        baseline.OnTree(
            function(nid){
                self.decode(baseline.Node(nid, true, false));
            },
            function(){
                callback(0);
            }
        );
    },
    encrypt: function(str){
        var result = this.encryptor.encrypt(Xmarks.gSettings.pin, str);
        if(Xmarks.gSettings.forceGC) {
            Components.utils.forceGC();
        }
        return result;
    },
    decrypt: function(str){
        var result =  this.encryptor.decrypt(Xmarks.gSettings.pin, str);
        var goodPIN = result.indexOf('password') >=  0;

        // If we're doing a merge from a reset pin, there will be
        // two pins floating around
        if(!goodPIN && this._oldpin !== undefined){
            result =  this.encryptor.decrypt(this._oldpin, str);
            goodPIN = result.indexOf('password') >=  0;
        }

        while(!goodPIN){
            var newpin = this.getValidPin();
            Xmarks.gSettings.pin = newpin;
            result =  this.encryptor.decrypt(Xmarks.gSettings.pin, str);
            goodPIN = result.indexOf('password') >=  0;
            if(!goodPIN){
                Xmarks.Alert(Xmarks.Bundle().GetStringFromName("error.pinInvalid"));
            }
        }
        return result;
    },
    getValidPin: function(){
        var retval = { okay: false, pin: "" };
        var topwin = Cc['@mozilla.org/appshell/window-mediator;1'].
            getService(Ci.nsIWindowMediator).
            getMostRecentWindow(null);
        
        if (!topwin) {
            Xmarks.LogWrite("getValidPin: Couldn't find a topwin!");
            throw 4;
        }
        var win = topwin.openDialog(
            "chrome://foxmarks/content/foxmarks-invalidpin.xul", "_blank",
            "chrome,dialog,modal,centerscreen", retval);
        if(!retval.okay) {
            Xmarks.gSettings.setSyncEnabled("passwords", false);
            throw 2;
        }

        // WILL- This looks like a mozilla bug, it deallocs result.pin
        //  apparently so you need to assign a new string to it.
        var s = "" + retval.pin;
        return s;
    },
    generateNid: function(login){
        return Xmarks.hex_md5("password|".concat(
                    login.hostname, "|",
                    login.formSubmitURL, "|",
                    login.httpRealm, "|",
                    login.usernameField, "|",
                    login.passwordField, "|",
                    login.username));
    },
    decode: function(node){
        if(!node.private){
            node.private = this.JSON.decode(this.decrypt(node.data));
            if(Xmarks.gSettings.forceGC) {
                Components.utils.forceGC();
            }
        }
        return node.private;
    },
    handleNidConflict: function(lnode, snode){
        var litem = this.decode(lnode);
        var sitem = this.decode(snode);
        if(litem.password == sitem.password)
            return "same";
        else {
            var result = this._conflictDialog(sitem, litem); 
            switch(result){
                case 2:
                    return "server";
                case 1:
                    return "local";
                default:
                    return "cancel";
            }
        }
        return "cancel";;
    },
    _buildList: function(doEncrypt, includeCt, funcDone){
        var mgr = this.Cc["@mozilla.org/login-manager;1"]
            .getService(this.Ci.nsILoginManager);

        var a = mgr.getAllLogins({});
        var ct = a.length;
        var self = this;
        var size = 0;
        //dump("_buildList\n");
        NiceProcess(
            {  ctr: 0,
               a: a,
               len: a.length,
               size: 0,
               result: {},
               done: function(){ return this.ctr >= this.len;}
            },
            function(iter){
                var login = iter.a[iter.ctr];
                if(login.httpRealm != Xmarks.SYNC_REALM &&
                        login.httpRealm != Xmarks.SYNC_REALM_PIN &&
                        login.httpRealm != Xmarks.FOXMARKS_SYNC_REALM &&
                        login.httpRealm != Xmarks.FOXMARKS_SYNC_REALM_PIN){
                    var nid = self.generateNid(login);
                    iter.result[nid] =  {
                        ntype: "password",
                        hostname: login.hostname,
                        formSubmitURL: login.formSubmitURL,
                        httpRealm: login.httpRealm,
                        username: login.username,
                        usernameField: login.usernameField,
                        password: login.password,
                        passwordField: login.passwordField,
                        pnid: NODE_ROOT
                    };
                    if(doEncrypt){
                        iter.result[nid].blob = self.encrypt(
                            iter.result[nid].toJSONString()
                        );
                    }
                    iter.size++;
                }
                iter.ctr++;
            },
            function(iter, e){
                if(includeCt)
                    iter.result.totalLogins = iter.size;
                funcDone(iter.result, e);
            }
        );
    },

    ProvideNodes: function(Caller, AddNode, Complete) {
        var self = this;
        this._buildList(true, false, function(a, e){
            // if buildlist threw an error, just drop out now
            if(e) {
                Complete.apply(Caller, [e]);
                return;
            }
            try {
                var now = new Date();
                var random_stuff = {
                    password: Xmarks.hex_md5(now.getTime().toString())
                };
                var root_node = new Node(
                    NODE_ROOT, 
                    {
                        ntype: "folder", 
                        children: [],
                        private: random_stuff,
                        data: self.encrypt(random_stuff.toJSONString())
                    }
                );
                var nid;
                var obj = {};
                for(nid in a){
                    if(obj[nid] === undefined){ 
                        var item = a[nid];
                        node = new Node(nid, {
                            ntype: "password",
                            data:  item.blob,
                            private: item,
                            pnid: NODE_ROOT
                        });
                        root_node.children.push(nid);
                        AddNode.apply(Caller, [node]);
                    }
                }
                AddNode.apply(Caller, [root_node]);

                Complete.apply(Caller, [0]);
           } catch(e) {
                if(typeof e != "number"){
                    Components.utils.reportError(e);
                    Complete.apply(Caller, [4]);
                } else {
                    Complete.apply(Caller, [e]);
                }
           }
        });
    },
    AcceptNodes: function(ns, callback) {
        var self = this;
        this._buildList(false, false, function(list, e){
            // if buildlist ran into an error, drop out now
            if(e) {
                callback(e);
                return;
            }
            var nid, node;
            var mgr = self.Cc["@mozilla.org/login-manager;1"]
                .getService(self.Ci.nsILoginManager);

            var removeList = [];
            // Remove any nids that aren't in ns
            for(nid in list){
                if(list.hasOwnProperty(nid)){
                    node = ns.Node(nid, false, true);
                    if(node === null){
                        removeList.push(nid);
                    }
                }
            }

            //dump("AcceptNodes\n");
            NiceProcess(
                {
                    a: removeList,
                    len: removeList.length,
                    ctr: 0,
                    done: function() { return this.ctr >= this.len; }
                },
                function(iter){
                    var nid = iter.a[iter.ctr];
                    var item = list[nid];
                    var logins = mgr.findLogins({},
                        item.hostname, 
                        item.formSubmitURL, 
                        item.httpRealm);
                        
                    for(var i = 0; i < logins.length; i++) {
                        if(logins[i].username == item.username) {
                            mgr.removeLogin(logins[i]);
                            break;
                        }
                    }
                    
                    iter.ctr++;
                },
                function(oldIter, e){
                    // If we had an error, just drop out now
                    if(e) {
                        callback(e);
                        return;
                    }
                    var node = ns.Node(NODE_ROOT, false, true);

                    //dump("AcceptNodes 2\n");
                    NiceProcess(
                        {
                            ctr: 0,
                            children: (node.children || []),
                            done: function() {
                                return this.ctr >= this.children.length;
                            }
                        },
                        function(iter){
                            var nid = iter.children[iter.ctr];

                            // Add it
                            node = ns.Node(nid); 
                            var  item = list[nid];
                            var  loginInfo;

                            if(!item){
                                try {
                                    var nitem = self.decode(node);
                                    loginInfo = self.Cc[
                                        "@mozilla.org/login-manager/loginInfo;1"].
                                        createInstance(self.Ci.nsILoginInfo);
                                    loginInfo.init(
                                        nitem.hostname, nitem.formSubmitURL, 
                                        nitem.httpRealm, 
                                        nitem.username, 
                                        nitem.password,
                                        nitem.usernameField, 
                                        nitem.passwordField);
                                    mgr.addLogin(loginInfo);
                                }
                                catch(e){
                                    // Pass cancels up the chain
                                    if(typeof e == "number"){ 
                                        throw e;
                                    } else {
                                        Components.utils.reportError(e);
                                    }
                                }
                            }
                            // Check for changes
                            else if(node.password != item.password){
                                var logins = mgr.findLogins({},
                                        item.hostname, 
                                        item.formSubmitURL, 
                                        item.httpRealm);
                                        
                                for(var i = 0; i < logins.length; i++) {
                                    if(logins[i].username == item.username) {
                                        var nitem = self.decode(node);
                                        loginInfo = self.Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(self.Ci.nsILoginInfo);
                                        loginInfo.init(
                                            nitem.hostname,
                                            nitem.formSubmitURL, 
                                            nitem.httpRealm, 
                                            nitem.username, 
                                            nitem.password,
                                            nitem.usernameField, 
                                            nitem.passwordField
                                        );
                                        mgr.modifyLogin(logins[i], loginInfo);
                                        break;
                                    }
                                }
                            }
                            iter.ctr++;

                        },
                        function(iter, e){
                            e = e || 0;
                            callback(e);
                        }
                    );
                }
             );
         });
    },

    merge: function(dest, source) {
        var snode =source.Node(NODE_ROOT, false, true); 
        this._oldpin = Xmarks.gSettings.pin;
        if(snode == null)
            return;
        var dnode = dest.Node(NODE_ROOT, false, true);
        if(dnode == null){
            var now = new Date();
            var random_stuff = {
                    password: Xmarks.hex_md5(now.getTime().toString())
            };
            dest.Do_insert(NODE_ROOT, {ntype: "folder", children: [],
                data: this.encrypt(random_stuff.toJSONString())});
            dnode = dest.Node(NODE_ROOT, false, true);
        }
        if(!dnode.data){
            var now = new Date();
            var random_stuff = {
                    password: Xmarks.hex_md5(now.getTime().toString())
            };
            ditem = dest.Node(NODE_ROOT,true);
            delete ditem.private;
            ditem.data = this.encrypt(random_stuff.toJSONString());
        }
        var dlist = dnode.children || [];
        var slist = snode.children || [];

        var ctr = slist.length;

        while(ctr--){
            var nid = slist[ctr];
            var sitem = source.Node(nid);
            var ditem = dest.Node(nid, false,true);
            if(!ditem){
                dest.Do_insert(nid, sitem.GetSafeAttrs());
            }
            else {
                var sdata = this.decode(sitem);
                var ddata = this.decode(ditem);
                if(sdata.password != ddata.password){
                    var cResult = this._conflictDialog(ddata, sdata);
                    if(cResult  == 1){
                        ditem = dest.Node(nid,true);
                        delete ditem.private;
                        ditem.data = sitem.data;
                    }
                }
            }
        }
        delete this._oldpin;
    },
    orderIsImportant: false,
    compareNodes: function(snode, onode, attrs){
        if(!snode.data && onode.data){
            attrs['data'] = onode.data;
            return true;
        }
        else if(!onode.data && snode.data){
            attrs['data'] = snode.data;
            return true;
        }
        else if(snode.data === undefined || onode.data === undefined){
            return false;
        }
        var sdata = this.decode(snode);
        var ddata = this.decode(onode);
        attrs.data = onode.data;
        return sdata.password != ddata.password && snode.nid != NODE_ROOT;
    },
    verifyPin: function(pin, node){
        Xmarks.LogWrite("Verifying PIN (Testing Node)");
        Xmarks.LogWrite("Node Stats: " + (node.data ? node.data.length : "null"));
        var result =  this.encryptor.decrypt(pin, node.data);
        Xmarks.LogWrite("Node Verification: " + (result.indexOf('password') >= 0 ?
            "true" : "false"));
        return result.indexOf('password') >= 0;
    },
    WatchForChanges: function(server) {
        return new PasswordSyncWatcher(server);
    }
};
function PasswordSyncWatcher(server){
    this.server = server;
    this.mgr = new PasswordDatasource();
    this.start();
}

PasswordSyncWatcher.prototype = {
    start: function(){
        var os = Cc["@mozilla.org/observer-service;1"].
            getService(Ci.nsIObserverService);
        os.addObserver(this, "foxmarks-checkforpasswordchange", false);
        os.addObserver(this, "foxmarks-watchersuspend", false);
    },
    _suspended: false,

    /*
        I took this code from nsLoginManagerPrompter.js
        to make sure we watch the same notify box that the
        loginmanager does.
    */
   _getNotifyBox : function (win) {
       try {
           // Get topmost window, in case we're in a frame.
           var notifyWindow = win.top;

           // Some sites pop up a temporary login window, when disappears
           // upon submission of credentials. We want to put the notification
           // bar in the opener window if this seems to be happening.
           if (notifyWindow.opener) {
               var webnav = notifyWindow
                                   .QueryInterface(Ci.nsIInterfaceRequestor)
                                   .getInterface(Ci.nsIWebNavigation);
               var chromeWin = webnav
                                   .QueryInterface(Ci.nsIDocShellTreeItem)
                                   .rootTreeItem
                                   .QueryInterface(Ci.nsIInterfaceRequestor)
                                   .getInterface(Ci.nsIDOMWindow);
               var chromeDoc = chromeWin.document.documentElement;

               // Check to see if the current window was opened with chrome
               // disabled, and if so use the opener window. But if the window
               // has been used to visit other pages (ie, has a history),
               // assume it'll stick around and *don't* use the opener.
               if (chromeDoc.getAttribute("chromehidden") &&
                   webnav.sessionHistory.count == 1) {
                   notifyWindow = notifyWindow.opener;
               }
           }


           // Find the <browser> which contains notifyWindow, by looking
           // through all the open windows and all the <browsers> in each.
           var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
                    getService(Ci.nsIWindowMediator);
           var enumerator = wm.getEnumerator("navigator:browser");
           var tabbrowser = null;
           var foundBrowser = null;

           while (!foundBrowser && enumerator.hasMoreElements()) {
               var win = enumerator.getNext();
               tabbrowser = win.getBrowser(); 
               foundBrowser = tabbrowser.getBrowserForDocument(
                                                 notifyWindow.document);
           }

           // Return the notificationBox associated with the browser.
           // Apparently sometimes the notify box doesn't have this
           // function so we should test for it. Bug #4122
           if (foundBrowser){
               var box = tabbrowser.getNotificationBox(foundBrowser);
               return (box && box.getNotificationWithValue) ? box : null;
           }

       } catch (e) {
           // If any errors happen, just assume no notification box.
       }

       return null;
   },  
    formsubmit : function (form) {
        var doc = form.ownerDocument;
        var win = doc.defaultView;
        var self = this;

        // check if there is a password field
        var ct = form.elements.length;
        var found = false;
        while(ct--){
            if(form.elements[ct].type == "password"){
                found = true;
                break;
            }
        }
        if(found){
            this.monitorForChange(win);
        }
        return true;
    },

    scheduleChangeTest: function(wait){
        var self = this;
        var callbackSingle = {
            notify: function(){
                try {
                    self.checkForChanges();
                } catch(e){
                    Xmarks.LogWrite("PwdM Error: " + e.message + "(" +
                            e.fileName + ": " + e.lineNumber + ")");
                }
            }
        };
        var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
        timer.initWithCallback(callbackSingle, wait,
            Ci.nsITimer.TYPE_ONE_SHOT);
    },
    monitorForClose: function(box,type){
        var self = this;
        var timerRepeating;
        var maxCtr = 20;

        var callbackWait = {
            notify: function(){
                if(!box || (box && !box.getNotificationWithValue)){
                    timerRepeating.cancel();
                } else if(box.getNotificationWithValue(type) == null){
                    try {
                        self.checkForChanges();
                    } catch(e){
                        Xmarks.LogWrite("PwdM Error: " + e.message + "(" +
                                e.fileName + ": " + e.lineNumber + ")");
                    }
                    timerRepeating.cancel();
                    }
                else if(maxCtr-- == 0)
                    timerRepeating.cancel();
            }
        };
        timerRepeating = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
        timerRepeating .initWithCallback(callbackWait, 1000,
            Ci.nsITimer.TYPE_REPEATING_SLACK);
    },
    checkNotifyBox: function(){
        var notifyType = "";
        var self = this;

        var box = self._getNotifyBox(win);
        if(box && box.getNotificationWithValue){
            notifyType = "password-save";
            if(box.getNotificationWithValue(notifyType) == null){
                notifyType = "password-change";
                if(box.getNotificationWithValue(notifyType) == null){
                    notifyType = "";
                }
            }
        }

        return notifyType;
    },
    monitorForChange: function(win){
        var self = this;
        var numTimes = 0;
        var callbackFirst = {
            notify: function(){
                /* Check if we have a notification.
                    if so, poll it until it closes
                */
                numTimes++;
                var box = self._getNotifyBox(win);
                var secondTry = false;
                if(box && box.getNotificationWithValue){
                    var notifyType = "password-save";
                    if(box.getNotificationWithValue(notifyType) == null){
                        notifyType = "password-change";
                        if(box.getNotificationWithValue(notifyType) == null){
                            notifyType = "";
                        }
                    }
                    if(notifyType != ""){
                        self.monitorForClose(box, notifyType);
                    }
                    else
                        self.scheduleChangeTest(8000);
                }
                // there are times that the loginManager
                //  will put up a dialog instead of a notification
                //  box.  In this case, and when they change a password,
                //  we'll wait 10 seconds more and then check for changes
                else {
                    if(numTimes > 4){
                        self.scheduleChangeTest(8000);
                    }
                    else {
                        timer.initWithCallback(callbackFirst, 500,
                            Ci.nsITimer.TYPE_ONE_SHOT);
                    }
                }
            }
        };

        // let things settle for a second and see what happens
        var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
        timer.initWithCallback(callbackFirst, 500,
            Ci.nsITimer.TYPE_ONE_SHOT);
    },
    observe: function(subject, topic, data)  { 
        var self = this;

        switch(topic){
            case "foxmarks-checkforpasswordchange":
                Xmarks.LogWrite("Checking for password changes.");
                this.checkForChanges();
                return;
            case "foxmarks-watchersuspend":
                this._suspended = data == "1";
                return;
        }
    },
    checkForChanges: function(){
        var self = this;
        if(!Xmarks.gSettings.isSyncEnabled("passwords") || !Xmarks.gSettings.haveSynced || self._suspended)
            return;

        var funcNotifyChange = function(data){
            var lm = Date.now();
            if (!self.lastModified || lm > self.lastModified && !self._suspended) {
                self.lastModified = lm;
                var os = Cc["@mozilla.org/observer-service;1"]
                    .getService(Ci.nsIObserverService);
                os.notifyObservers(null, "foxmarks-datasourcechanged", 
                    lm + ";passwords");
            }
        };
        this.server.getBaseline("passwords", function(status, ns){
            if(status)
                return;
            
            var mydata = {
                totalLogins: 0
            };
            var funcCompareList = function(data, e){
                // if buildlist returned an error, just drop out silently and hope for the best
                if(e){
                    return;
                }

                if(mydata.totalLogins != data.totalLogins){
                    funcNotifyChange(data);
                    return;
                }
                for(var field in data){
                    if(data.hasOwnProperty(field)){
                        if(!mydata.hasOwnProperty(field) ||
                            mydata[field].password != data[field].password){
                            funcNotifyChange(data);
                            return;
                        }
                    }
                }
            };
            ns.OnTree(
                function(nid){
                    if(nid != NODE_ROOT){
                        mydata.totalLogins++;
                        mydata[nid] = ns.Node(nid).private;
                    }
                },
                function(){
                    self.mgr._buildList(false, true, funcCompareList);
                }
            );

        });

    }
};
