// vim: ts=4 sw=4 expandtab cindent foldmethod=marker
//
// BEGIN FLOCK GPL
// 
// Copyright Flock Inc. 2005-2006
// http://flock.com
// 
// This file may be used under the terms of of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
// 
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
// 
// END FLOCK GPL
//

// FIXME:
//   * moving XML parsing to E4X
//   * intelligent, incremental syncing down
//   * handle delicious failures intelligently

function DEBUG (x) { }
//function DEBUG (x) { debug('DELICIOUS: '+x+'\n'); }

// CONSTANTS {{{
const flockIFavoritesService = Components.interfaces.flockIFavoritesService;
const flockIFavoritesWebService = Components.interfaces.flockIFavoritesWebService;
const flockIFavoritesChange = Components.interfaces.flockIFavoritesChange;
const flockIFavoritesChangeFavorite = Components.interfaces.flockIFavoritesChangeFavorite;
const flockIFavoritesQueue = Components.interfaces.flockIFavoritesQueue;
const flockIFavoriteProperties = Components.interfaces.flockIFavoriteProperties;
const nsISupports = Components.interfaces.nsISupports;
const nsITimer = Components.interfaces.nsITimer;
const nsITimerCallback = Components.interfaces.nsITimerCallback;
const nsIXMLHttpRequest = Components.interfaces.nsIXMLHttpRequest;
const nsIPasswordManager = Components.interfaces.nsIPasswordManager;
const nsIPassword = Components.interfaces.nsIPassword;
const nsIDOMSerializer = Components.interfaces.nsIDOMSerializer;
const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
const nsIAuthPrompt = Components.interfaces.nsIAuthPrompt;
const nsIInterfaceRequestor = Components.interfaces.nsIInterfaceRequestor;
const nsIHttpAuthManager = Components.interfaces.nsIHttpAuthManager;

const TIMER_CONTRACTID = '@mozilla.org/timer;1';
const XMLHTTPREQUEST_CONTRACTID = '@mozilla.org/xmlextras/xmlhttprequest;1';
const PASSWORDMANAGER_CONTRACTID = '@mozilla.org/passwordmanager;1';
const XMLSERIALIZER_CONTRACTID = '@mozilla.org/xmlextras/xmlserializer;1';
const FFU_CONTRACTID = '@flock.com/favorites/utils;1';
const PREFERENCES_CONTRACTID = '@mozilla.org/preferences-service;1';
const PROMPT_CONTRACTID = '@mozilla.org/embedcomp/prompt-service;1';
const HTTPAUTH_CONTRACTID = '@mozilla.org/network/http-auth-manager;1';

const DIRECTION_TO_WEB_SERVICE = flockIFavoritesQueue.DIRECTION_TO_WEB_SERVICE;
const DIRECTION_FROM_WEB_SERVICE = flockIFavoritesQueue.DIRECTION_FROM_WEB_SERVICE;

const FIELD_WSID                 = flockIFavoriteProperties.FIELD_WSID;
const FIELD_TAGS                 = flockIFavoriteProperties.FIELD_TAGS;
const FIELD_PERMISSIONS          = flockIFavoriteProperties.FIELD_PERMISSIONS;
const FIELD_NAME                 = flockIFavoriteProperties.FIELD_NAME;
const FIELD_SHORTCUTURL          = flockIFavoriteProperties.FIELD_SHORTCUTURL;
const FIELD_DESCRIPTION          = flockIFavoriteProperties.FIELD_DESCRIPTION;
const FIELD_NSYNC                = flockIFavoriteProperties.FIELD_NSYNC;
const FIELD_LASTVISITDATE        = flockIFavoriteProperties.FIELD_LASTVISITDATE;
const FIELD_LASTMODIFIEDDATE     = flockIFavoriteProperties.FIELD_LASTMODIFIEDDATE;
const FIELD_ADDEDDATE            = flockIFavoriteProperties.FIELD_ADDEDDATE;
const FIELD_PRIVATE              = flockIFavoriteProperties.FIELD_PRIVATE;

const DELICIOUS_MIN_WAIT = 1; // wait a minimum of two seconds between calls
const TIMEOUT = 10; // cancel requests that take more than 10 seconds 

const SEND_WAIT = 1; // wait one second between sending updates
const RECV_WAIT = 60*5; // wait 

const DELICIOUS_PROT = 'https'
const DELICIOUS_HOST = 'api.del.icio.us';
const DELICIOUS_PORT = 443;
const DELICIOUS_PATH = '/v1/';

const PREF_USERNAME = 'flock.favorites.delicious.username';
const PREF_PASSWORD = 'flock.favorites.delicious.password';
const PWMGR_HOST = ':favorites:webservice:delicious:';

const PREFS = Components.classes[PREFERENCES_CONTRACTID].getService (nsIPrefBranch);
const AUTHMGR = Components.classes[HTTPAUTH_CONTRACTID].getService (nsIHttpAuthManager);
var PWMGR = null;
/// }}}

// Password Manager utility functions {{{
function ensurePasswordManager () {
    if (!PWMGR) {
        PWMGR = Components.classes[PASSWORDMANAGER_CONTRACTID]
            .getService (nsIPasswordManager);
    }
}
function getAccountDetails () {
    ensurePasswordManager();
    var e = PWMGR.enumerator;
    while (e.hasMoreElements ()) {
        var pw = e.getNext ().QueryInterface (nsIPassword);
        if (pw.host == PWMGR_HOST) {
            return pw;
        }
    }
    return null;
}
function getUsername () {
    var pw = getAccountDetails ();
    if (pw) {
        return pw.user;
    } else {
        return null;
    }
}
function getPassword () {
    var pw = getAccountDetails ();
    if (pw) {
        return pw.password;
    } else {
        return null;
    }
}
function setAccountDetails (aUsername, aPassword) {
    ensurePasswordManager();
    var un = getUsername ();
    if (un) {
        PWMGR.removeUser (PWMGR_HOST, un);
    }
    PWMGR.addUser (PWMGR_HOST, aUsername, aPassword);
}
/// }}}

// Date utility functions {{{

// seconds since epoch
function secondsSinceEpoch () { return (new Date()).getTime()/1000; }

const re_iso8601 = new RegExp(
    "^(\\d{4})(-(\\d{2})(-(\\d{2})" +
    "(T(\\d{2}):(\\d{2})(:(\\d{2})(\\.(\\d+))?)?" +
    "(Z|(([-+])(\\d{2}):(\\d{2})))?)?)?)?$");
function parseDate(datestr) // {{{
{
    var date = null;

    datestr = datestr.replace(/^\s*|\s*$/g, '');

    var dateval = Date.parse(datestr);

    if (dateval) {
        date = new Date(dateval);
    } else {
        /* this is a parser for the W3C subset of ISO 8601 dates
         * http://www.w3.org/TR/NOTE-datetime
         */
        var m = datestr.match(re_iso8601);

        if (m) {
            date = new Date(Date.UTC(m[1]))

            m[3]  && date.setUTCMonth   (m[3] - 1);
            m[5]  && date.setUTCDate    (m[5]);
            m[7]  && date.setUTCHours   (m[7]);
            m[8]  && date.setUTCMinutes (m[8]);
            m[10] && date.setUTCSeconds (m[10]);

            m[12] && date.setUTCMilliseconds(('0.' + m[12]) * 1000);

            if (m[14]) {
                var tz_off = m[16] * 60 + Number(m[17]);
                tz_off *= m[15] == '-' ? -1 : 1;
                tz_off *= 60 * 1000;

                date.setTime(date.getTime() - tz_off);
            }

        }
    }

    return date;
} // }}}

function isoDate (d) { // {{{
    function zeropad (s, l) {
        s = s.toString(); // force it to a string
        while (s.length < l) {
            s = '0' + s;
        }
        return s;
    }

    // if d is a number, turn it into a date
    if (typeof d == 'number') {
        var dt = new Date()
        dt.setTime (d);
        d = dt;
    }

    // YYYY-MM-DDThh:mm:ssZ
    return zeropad (d.getUTCFullYear (), 4) + '-' +
           zeropad (d.getUTCMonth () + 1, 2) + '-' +
           zeropad (d.getUTCDate (), 2) + 'T' +
           zeropad (d.getUTCHours (), 2) + ':' +
           zeropad (d.getUTCMinutes (), 2) + ':' +
           zeropad (d.getUTCSeconds (), 2) + 'Z'
} // }}}

// }}}
// Delicious response parsing {{{
function parseResult (xml) { //{{{
    var results = xml.getElementsByTagName ('result');
    if (results.length != 1) {
        return null;
    }
    var code = results.item(0).attributes.getNamedItem ('code');
    return code.nodeValue;
} //}}}
function parsePosts (xml) { // {{{
    // so we want to parse into an array of flockIFavoritesChange

    var changeqi = function (aIID) {
        if (aIID.equals(flockIFavoritesChange) || 
                aIID.equals(flockIFavoritesChangeFavorite) || 
                aIID.equals(nsISupports)) {
            return this;
        }
        throw Components.results.NS_ERROR_NO_INTERFACE;
    }

    var changes = new Array ();

    var posts = new XML (Components.classes[XMLSERIALIZER_CONTRACTID]
            .createInstance (nsIDOMSerializer)
            .serializeToString(xml.documentElement));
    if (posts.name () != 'posts') {
        // the root element must be <posts>
        return null;
    }

    var ps = posts.post; // the list of <post> children
    for (var i in ps) {
        var post = ps[i];
        var properties = {
            fields: FIELD_NAME|FIELD_DESCRIPTION|FIELD_TAGS|FIELD_ADDEDDATE|FIELD_PRIVATE,
            name: post.@description.toString (),
            description: post.@extended.toString (),
            addedDate: parseDate(post.@time.toString ()).getTime(), 
            private: post.@shared == 'no',
        }
        var tags = post.@tag.toString ();
        if (tags == 'system:unfiled') {
            // this is a bogus tag we need to ignore
            tags = '';
        }
        /* translating from delicious back to flock tags:
            - replace spaces with commas
            - replace underscores with spaces
        */
        tags = tags.replace(/ /g, ',').replace(/_/g, ' ');
        // lets remove collection: tags for the time being
        tags = tags.split(/,/).filter (
                function (x) { return !x.match (/^collection:/); }).join (',');

        properties.tags = tags;
        changes.push ({
            url: post.@href.toString(),
            isDelete: false,
            properties: properties,
            QueryInterface: changeqi,
        });
    }

    return changes;
} //}}}
// }}}



// the DeliciousCall object wraps up everything involved in a call /*{{{*/
function DeliciousCall (noun, verb, args, ignoreAuthFailure) {/*{{{*/
    DEBUG ('DelicousCall ('+noun+','+verb+','+args+')');
    var url = noun+'/'+verb;
    var argsarray = new Array ();
    if (!args) {
        args = new Array ();
    }
    for (key in args) {
        DEBUG (' '+key+'->'+args[key]);
        argsarray.push (encodeURIComponent(key) + '=' + 
                encodeURIComponent(args[key]));
    }
    if (argsarray.length) {
        url = url + '?' + argsarray.join('&');
    }
    DEBUG ('url: '+url);
    this.url = url;
    this.request = null; // will be set to the XMLHttpRequest when its sent
    this.tries = 3;
    this.ignoreAuthFailure = ignoreAuthFailure;
}/*}}}*/
DeliciousCall.prototype.onError = function (message) {/*{{{*/
    DEBUG ('DeliciousCall.onError ('+message+')');
}/*}}}*/
DeliciousCall.prototype.onResult = function (message, xml) {/*{{{*/
    DEBUG ('DeliciousCall.onResult ('+message+','+xml+')');
}/*}}}*/
DeliciousCall.prototype.onTimeout = function () {/*{{{*/
    DEBUG ('DeliciousCall.onTimeout ()');
}/*}}}*/
DeliciousCall.prototype.requestComplete = function () {/*{{{*/
    DEBUG ('DeliciousCall.requestComplete');
    try {
        DEBUG ('text='+this.request.responseText+'');
        var text = this.request.responseText;
        // strip invalid XML control characters from del.icio.us response
        // the only allowed control chars are \x09 \x0A \x0D
        text = text.replace (/[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F]/g, '');
        var xml = Components.classes['@mozilla.org/xmlextras/domparser;1']
                .createInstance(Components.interfaces.nsIDOMParser)
                .parseFromString (text, 'text/xml');
        DEBUG (' xml='+xml);
        DEBUG (' this.request.responseText='+text);
        if (this.request.status == 200) {
            this.onResult (this.request.responseText, xml);
        } else {
            this.onError (this.request.statusMessage);
        }
    } catch (e) {
        this.onError ('exception '+e);
    }
}/*}}}*/
/*}}}*/

// the Delicious object manages a "connection" to a DelicousAPI server. {{{
// it handles queuing and throttling of requests and authentication
function Delicious (svc, url) {/*{{{*/
    if (url) {
        this.url = url;
    } else {
        this.url = DELICIOUS_PROT+'://'+DELICIOUS_HOST+':'+DELICIOUS_PORT+
            DELICIOUS_PATH;
    }

    // okay, lets migrate these settings from prefs to the password manager
    try {
        var un = PREFS.getCharPref (PREF_USERNAME);
        var pw = PREFS.getCharPref (PREF_PASSWORD);
        if (un) {
            setAccountDetails (un, pw);
        }
        PREFS.clearUserPref (PREF_USERNAME);
        PREFS.clearUserPref (PREF_PASSWORD);
    } catch (e) { }

    this.svc = svc;

    this.queue = new Array ();
    this.inProgress = false;
    this.inHTTP = false;
    this.lastCall = null;
    this.lastCallStarted = 0;
    this.lastCallEnded = 0;
    this.enabled = true;

    // set up a 1 second repeating timer
    this.timer = Components.classes[TIMER_CONTRACTID].createInstance(nsITimer);
    this.timer.initWithCallback (this, DELICIOUS_MIN_WAIT*1000, nsITimer.TYPE_REPEATING_SLACK);
}/*}}}*/
Delicious.prototype.setEnabled = function (aEnabled) {/*{{{*/
    this.enabled = aEnabled;
    if(!aEnabled) {
        this.queue = new Array();
    }
}/*}}}*/
Delicious.prototype.notify = function (timer) {/*{{{*/
    // nsITimer callback

    // DEBUG ('timer callback');
    // DEBUG (' inProgress = '+this.inProgress);
    // DEBUG (' queue.length = '+this.queue.length);
    // DEBUG (' lastCallEnded = '+this.lastCallEnded);
    // if we can send another request lets do it
    if (this.canSendRequest ()) {
        DEBUG ('sending next request');
        this.sendNextRequest ();
        return;
    }

/* FIXME: timeout support in br0ken
    // if the current request has timed out 
    if (this.inHTTP && this.lastCallStarted 
            && (this.lastCallStarted+TIMEOUT > secondsSinceEpoch())) {
        // lets cancel it
        DEBUG ('timed out after '+(secondsSinceEpoch()-this.lastCallStarted)+' seconds (timeout: '+TIMEOUT+'s)');
        DEBUG (' '+(this.lastCallStarted+TIMEOUT)+' > '+secondsSinceEpoch());

        this.lastCall.request.abort ();

        DEBUG ('calling onTimeout()');
        // pass an error back
        if (this.lastCall) {
            this.lastCall.onTimeout ()
        }

        // and get ready for the next call
        this.lastCall = null;
        this.lastCallEnded = secondsSinceEpoch ();
        this.inProgress = false;
        return;
    }
*/
}/*}}}*/
Delicious.prototype.submit = function (call) {/*{{{*/
    DEBUG ('Delicious.submit ('+call+')');
    call.tries--;
    if (call.tries < 0) {
        // tried too many times
        DEBUG ('Delicious.submit - giving up');
        return;
    }
    // add this call to the queue
    this.queue.push (call);
}/*}}}*/
Delicious.prototype.canSendRequest = function () {/*{{{*/
    return !this.inProgress 
        && (this.queue.length > 0)
        && this.enabled
        && (this.lastCallEnded+DELICIOUS_MIN_WAIT<=secondsSinceEpoch());
}/*}}}*/
Delicious.prototype.sendNextRequest = function () {/*{{{*/
    DEBUG ('Delicious.sendNextRequest');
    // now we have a call in progress
    this.inProgress = true;
    this.lastCallStarted = secondsSinceEpoch ();

    // get the call
    this.lastCall = this.queue.shift ();

    // build the request
    this.inHTTP = true;
    this.lastCall.request = Components.classes[XMLHTTPREQUEST_CONTRACTID]
        .createInstance (nsIXMLHttpRequest);
    DEBUG ('request url: '+(this.url+this.lastCall.url)+'\n');
    var account = getAccountDetails ();
    if (account) {
        this.lastCall.request.open ('GET', this.url+this.lastCall.url, true, 
                account.user, account.password);
    } else {
        this.lastCall.request.open ('GET', this.url+this.lastCall.url, true);
    }
    this.lastCall.request.channel.notificationCallbacks = {
        // nsISupports
        QueryInterface: function (iid) {
            if (iid.equals (nsISupports) ||
                iid.equals (nsIAuthPrompt) ||
                iid.equals (nsIInterfaceRequestor)) {
                    return this;
            }
            throw Components.results.NS_ERROR_NO_INTERFACE;
        },
        // nsIInterfaceRequestor
        getInterface: function (iid, result) {
            if (iid.equals (nsIAuthPrompt)) {
                return this;
            }
            Components.returnCode = Components.results.NS_ERROR_NO_INTERFACE;
            return null;
        },
        // nsIAuthPromptInterface
        prompt: function (dialogTitle, text, passwordRealm, savePAssword, 
                defaultText, result) {
            return false;
        },
        promptUsernameAndPassword: function (dialogTitle, text, 
                passwordRealm, savePassword, user, pwd) {
            return false;
        },
        promptPassword: function (dialogTitle, text, passwordRealm, savePassword, user) {
            return false;
        },
    }

    var delicious = this;
    this.lastCall.request.onreadystatechange = function (aEvt) {
        DEBUG ('onReadyStateChange: event='+aEvt+' readyState='+delicious.lastCall.request.readyState+'\n');
        switch (delicious.lastCall.request.readyState) {
            case 4:
                DEBUG (' complete!');
                delicious.inHTTP = false;
                // FIXME: handle 503
                // call the callbacks

                // clear the http auth cache for delicious
                AUTHMGR.setAuthIdentity (DELICIOUS_PROT, DELICIOUS_HOST, 
                        DELICIOUS_PORT, null, 'del.icio.us API', 
                        DELICIOUS_PATH + delicious.lastCall.url, '', '', '');

                delicious.lastCall.requestComplete ();
                var ignoreAuthFailure = delicious.lastCall.ignoreAuthFailure
                // and clean up
                var status = delicious.lastCall.request.status;
                delicious.lastCall = null;
                delicious.lastCallEnded = secondsSinceEpoch ();
                delicious.inProgress = false;

                if (status == 401 && !ignoreAuthFailure) {
                    Components.classes['@mozilla.org/rdf/datasource;1?name=flock-favorites']
                            .getService (flockIFavoritesService).authenticationFailed ();
                }
                break;
        }
    }
         
    // FIXME: change user agent?
    if (this.lastCall.listener) this.lastCall.listener.onStart();
    this.lastCall.request.send (null);
}/*}}}*/
// }}}

// The deliciousWebService flockIFavoritesWebService implementation {{{
function deliciousWebService () {/*{{{*/
    DEBUG ('constructed deliciousWebService');
    this.mQueue = null;

    this.mDelicious = new Delicious (this);

    this.mLastUpdate = 0;

    this.mSendTimer = Components.classes[TIMER_CONTRACTID]
        .createInstance(nsITimer);
    this.mSendTimer.initWithCallback (this, SEND_WAIT*1000, 
            nsITimer.TYPE_REPEATING_SLACK);

    // set attributes
    this.name = 'del.icio.us';
    this.description = 'Yahoo\'s free service where lots of people share their favorites online and publicly.';
    this.signupPage = 'http://del.icio.us/register?partner=flock';
    this.id = 'delicious';
    this.supportedFields = FIELD_NAME|FIELD_DESCRIPTION|FIELD_TAGS|FIELD_ADDEDDATE|FIELD_PRIVATE;
    this.supportsCollections = false;
    this.supportsExplicitDeletes = false;
    this.version = 1;
}/*}}}*/
deliciousWebService.prototype.QueryInterface = function (iid) {/*{{{*/
    if (iid.equals(flockIFavoritesWebService) || 
            iid.equals(nsITimerCallback) ||
            iid.equals(nsISupports)) {
        return this;
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
}/*}}}*/
// nsITimerCallback implementation
deliciousWebService.prototype.notify = function (aTimer) {//{{{
    var queue = this.mQueue;
    if (aTimer == this.mSendTimer) {
        if (this.mDelicious.inCall || this.mDelicious.queue.length) {
            return;
        }
        if (!this.mQueue) {
            return;
        }


        var change = this.mQueue.nextChange (DIRECTION_TO_WEB_SERVICE);
        if (change == null) {
            // no change pending
            return;
        }

        try {
            change = change.QueryInterface (flockIFavoritesChangeFavorite);
            if (change == null) {
                // not the right kind of change
                return;
            }
        } catch (e) {
            // not the right kind of change
            return;
        }

        if (change.isDelete) {
            var call = new DeliciousCall ('posts', 'delete', {url: change.url}, false);
            call.onError = function (message) { 
                // re-queue failed request
                queue.deleteFavorite (change.url, change.properties, 
                        DIRECTION_TO_WEB_SERVICE);
            }
            call.onResult = function (message, xml) { }
            call.onTimeout = function () { 
                // re-queue failed request
                queue.deleteFavorite (change.url, change.properties, 
                        DIRECTION_TO_WEB_SERVICE);
            }
            this.mDelicious.submit (call);
        } else {
            /*
            var ffu = Components.classes[FFU_CONTRACTID]
                .getService (flockIFavoritesUtils);

            DEBUG ('adding: '+ffu.propertiesToString (change.properties));
            */
            var args = { replace: 'yes', url: change.url, tags: '' };
            if (change.properties.fields & FIELD_NAME) {
                args.description = change.properties.name;
            }
            if (change.properties.fields & FIELD_DESCRIPTION) {
                args.extended = change.properties.description;
            }
            if (change.properties.fields & FIELD_TAGS) {
                /* the translation is something like this: 
                     - remove spaces surrounding commas
                     - turn spaces into pluses
                     - turn commas into spaces
                 */
                args.tags = change.properties.tags.
                    // remove extra whitespace
                    replace(/^ */,'').replace(/ *$/,'').replace(/ *, */g,',').
                    // replace spaces with _ and commas with spaces
                    replace(/ /g,'_').replace(/,/g, ' ');
            }
            if (change.properties.fields & FIELD_ADDEDDATE) {
                args.dt = isoDate (change.properties.addedDate);
            }
            if (change.properties.fields & FIELD_PRIVATE && change.properties.private) {
                args.shared = 'no';
            }
            var call = new DeliciousCall ('posts', 'add', args, false);
            call.onError = function (message) { 
                // re-queue failed request
                queue.changeFavorite (change.url, change.properties, 
                        DIRECTION_TO_WEB_SERVICE);
            }
            call.onResult = function (message, xml) { 
                //result = parseDeliciousResponse (xml);
            }
            call.onTimeout = function () { 
                // re-queue failed request
                queue.changeFavorite (change.url, change.properties, 
                        DIRECTION_TO_WEB_SERVICE);
            }

            this.mDelicious.submit (call);
        }

    } else {
        DEBUG ('callback for unknown timer: '+aTimer);
    }
}//}}}


// flockIFavoritesWebService implementation
deliciousWebService.prototype.setFavoritesQueue = function (aQueue) {/*{{{*/
    DEBUG ('deliciousWebService.setFavoritesQueue('+aQueue+')');
    this.mQueue = aQueue;
}/*}}}*/
deliciousWebService.prototype.getAllFavorites = function (aListener) {/*{{{*/
    DEBUG ('deliciousWebService.getAllFavorites('+aListener+')');
    var service = this;
    var call = new DeliciousCall ('posts', 'all', {}, false);
    call.onError = function (message) {
        aListener.onFailed (message);
    }
    call.onResult = function (message, xml) { 
        var changes = parsePosts (xml);
        if (changes == null) {
            aListener.onFailed ('failed to parse posts/all response');
            return;
        }
        // turn the changes array into an nsISimpleEnumerator
        changes.hasMoreElements = function () { return this.length != 0; }
        changes.getNext = function () { return this.shift (); }
        aListener.onSucceeded (changes);
    }
    call.onTimeout = function () {
        aListener.onFailed ('Timeout');
    }
    this.mDelicious.submit (call);
}/*}}}*/
deliciousWebService.prototype.getSyncRequired = function (aListener) {/*{{{*/
    DEBUG ('deliciousWebService.getSyncRequired()');
    var service = this;
    var call = new DeliciousCall ('posts', 'update', {}, false);
    call.onError = function (message) {
        aListener.onFailed (message);
    }
    call.onResult = function (message, xml) { 
        var updates_xml = xml.getElementsByTagName ('update');
        if (updates_xml == null || updates_xml.length !=1) {
            aListener.onFailed ('failed to parse update response');
            return;
        }
        var update_xml = updates_xml.item (0);
        var attrs = update_xml.attributes;
        for (var i=0; i<attrs.length; i++) {
            if (attrs[i].nodeName == 'time') {
                var update = parseDate (attrs[i].nodeValue);
                if (update == null) {
                    aListener.onFailed ('failed to parse update date');
                    return;
                }
                if (update > service.mLastUpdate) {
                    // there's been a change
                    service.mLastUpdate = update;
                    aListener.onSucceeded (true);
                    return
                }
                // no change
                aListener.onSucceeded (false);
                return
            }
        }
        aListener.onFailed ('failed to parse update response');
    }
    call.onTimeout = function () {
        aListener.onFailed ('Timeout');
    }
    this.mDelicious.submit (call);
}/*}}}*/
deliciousWebService.prototype.__defineGetter__('webView', function() { // {{{
    DEBUG ('deliciousWebService.webView getter');
    var un = getUsername ();
    if (un) {
        return 'http://del.icio.us/'+un+'/';
    } else {
        return null;
    }

});/*}}}*/
deliciousWebService.prototype.login = function (aUserName, aPassword, aListener) {/*{{{*/
    DEBUG ('deliciousWebService.login("'+aUserName+'", "'+aPassword+'", '+aListener+')');

    setAccountDetails (aUserName, aPassword);

    var service = this;
    var call = new DeliciousCall ('posts', 'update', {}, true);
    call.onError = function (message) {
        aListener.onFailed (message);
    }
    call.onResult = function (message, xml) { 
        aListener.onSucceeded ();
    }
    call.onTimeout = function () {
        aListener.onFailed ('Timeout');
    }
    this.mDelicious.submit (call);
}/*}}}*/
deliciousWebService.prototype.getUsername = function () {/*{{{*/
    DEBUG ('deliciousWebService.getUsername()');
    return getUsername ();
}/*}}}*/
deliciousWebService.prototype.getPassword = function () {/*{{{*/
    DEBUG ('deliciousWebService.getPassword()');
    return getPassword ();
}/*}}}*/
deliciousWebService.prototype.serviceDisabled = function () {/*{{{*/
    DEBUG ('deliciousWebService.serviceDisabled()');
    // we need to clear some state
    this.mLastUpdate = 0;
}/*}}}*/
// }}}

// JavaScript XPCOM Boilerplate {{{

// XPCOM Boilerplate {{{
function loadLibrary(aFilename) {
    var file = Components.classes["@mozilla.org/file/directory_service;1"]
        .getService(Components.interfaces.nsIProperties)
        .get("ComsD", Components.interfaces.nsIFile);
    file.append(aFilename);

    var ios = Components.classes["@mozilla.org/network/io-service;1"]
        .getService(Components.interfaces.nsIIOService);
    var fileHandler = ios.getProtocolHandler("file")
        .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    var spec = fileHandler.getURLSpecFromFile(file);

    var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
        .getService(Components.interfaces.mozIJSSubScriptLoader);

    scriptLoader.loadSubScript(spec);
}
loadLibrary("flockXPCOMTemplate.js.lib");
// }}}

var module = new xpcomModule ('{26fec785-bb4d-4695-bd8e-6f27f6dc2d49}',
        '@flock.com/favorites/service/delicious;1', 
        'Delicious Favorites Web Service', 
        deliciousWebService);
module.setFavoritesWebService ('delicious-favorites-service');
function NSGetModule(aCompMgr, aFileSpec) {
    return module;
};
// }}}
