// 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:
//   * intelligent, incremental syncing down
//   * handle failures intelligently

// CONSTANTS {{{
const flockIFavoritesService = Components.interfaces.flockIFavoritesService;
const flockIFavoritesWebService = Components.interfaces.flockIFavoritesWebService;
const flockIFavoritesChange = Components.interfaces.flockIFavoritesChange;
const flockIFavoritesChangeFavorite = Components.interfaces.flockIFavoritesChangeFavorite;
const flockIFavoritesChangeCollection = Components.interfaces.flockIFavoritesChangeCollection;
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 nsIPromptService = Components.interfaces.nsIPromptService;
const nsIRDFResource = Components.interfaces.nsIRDFResource;
const nsIRDFService = Components.interfaces.nsIRDFService;
const nsIScriptableUnicodeConverter = Components.interfaces.nsIScriptableUnicodeConverter;

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 RDFSERVICE_CONTRACTID  = '@mozilla.org/rdf/rdf-service;1';
const FAVORITES_CONTRACTID = '@mozilla.org/rdf/datasource;1?name=flock-favorites';
const UNICODECONVERTER_CONTRACTID = '@mozilla.org/intl/scriptableunicodeconverter';


const PREFS = Components.classes[PREFERENCES_CONTRACTID].getService (nsIPrefBranch);
const RDFS = Components.classes[RDFSERVICE_CONTRACTID].getService (nsIRDFService);


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 SHADOWS_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 

/// }}}

/* this library is useful for debugging, but not actually used in the 
   functionality of this component.
function loadSubScript(spec)
{
    var loader = Components.classes['@mozilla.org/moz/jssubscript-loader;1']
        .getService(Components.interfaces.mozIJSSubScriptLoader);
    var context = {};
    loader.loadSubScript(spec, context);
    return context;
}
var flockFavoritesUtils = new(loadSubScript('chrome://browser/content/flock/favorites/flockFavoritesUtils.js').flockFavoritesUtils)();

*/

debug ('loading shadowsWebService\n');
// 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 (), 2) + '-' +
           zeropad (d.getUTCDate (), 2) + 'T' +
           zeropad (d.getUTCHours (), 2) + ':' +
           zeropad (d.getUTCMinutes (), 2) + ':' +
           zeropad (d.getUTCSeconds (), 2) + 'Z'
} // }}}

// }}}
// The shadowsWebService flockIFavoritesWebService implementation {{{
function shadowsWebService () {/*{{{*/
    debug ('constructed shadowsWebService\n');
    this.mQueue = null;

    this.mSendTimer = Components.classes[TIMER_CONTRACTID]
        .createInstance(nsITimer);
    this.mSendTimer.initWithCallback (this, SEND_WAIT*1000, 
            nsITimer.TYPE_REPEATING_SLACK);
    debug ('set shadowsWebService timer\n');
    // set attributes
    this.name = 'Shadows';
    this.description = 'A free service that lets you keep public and private favorites online.';
    this.signupPage = 'http://www.shadows.com/signup/createaccount.aspx';
    this.id = 'shadows';
    this.supportedFields = FIELD_NAME|FIELD_DESCRIPTION|FIELD_TAGS|FIELD_PRIVATE|FIELD_ADDEDDATE|FIELD_LASTMODIFIEDDATE;
    this.supportsCollections = true;
    this.supportsExplicitDeletes = true;
    this.version = 1;
}/*}}}*/
shadowsWebService.prototype.promptForLogin = function () { // {{{
    var svc = Components.classes[PROMPT_CONTRACTID]
        .getService (nsIPromptService);
    var un = '';
    var pw = '';
    try {
        un = shadows.PrefsManager.getUsername('');
    } catch (e) { }
    try {
        pw = shadows.PrefsManager.getPassword('');
    } catch (e) { }
    var u = { value: un }
    var p = { value: pw }
    var ok = svc.promptUsernameAndPassword (null, 'Shadows.com Login Details', 
            'Enter your Shadows.com user name and password', u, p, null, {value:0});
    if (!ok) {
        u.value = '';
        p.value = '';
    }
    shadows.PrefsManager.setUsername(u.value);
    shadows.PrefsManager.setPassword(p.value);
} // }}}    
shadowsWebService.prototype.username = function () { // {{{
    var un = null;
    while (un == null) {
        // try to get the preference
        try {
            un = shadows.PrefsManager.getUsername('');
        } catch (e) { }

        if (un == null) {
            this.promptForLogin ();
        }
    }
    return un;
} // }}}
shadowsWebService.prototype.password = function () { // {{{
    var pw = null;
    while (pw == null) {
        // try to get the preference
        try {
            pw = shadows.PrefsManager.getPassword('');
        } catch (e) { }

        if (pw == null) {
            this.promptForLogin ();
        }
    }
    return pw;
} // }}}
shadowsWebService.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
shadowsWebService.prototype.notify = function (aTimer) {//{{{
    if (aTimer == this.mSendTimer) {
        if (!this.mQueue) {
            return;
        }
        
        //debug ('shadowsWebService.notify queue exists\n');
        var change = this.mQueue.nextChange (DIRECTION_TO_WEB_SERVICE);
        if (change == null) {
            // no change pending
            return;
        }
        var fchange = null;
        try {
            fchange = change.QueryInterface (flockIFavoritesChangeFavorite);
        } catch (e) { }
        if (fchange != null) {
            this._changeFavorite (fchange);
            return;
        }

        var cchange = null;
        try {
            cchange = change.QueryInterface (flockIFavoritesChangeCollection);
        } catch (e) { }
        if (cchange != null) {
            this._changeCollection (cchange);
            return;
        }

    } else {
        debug ('callback for unknown timer: '+aTimer+'\n');
    }
} // }}}
shadowsWebService.prototype._changeFavorite = function (aChange) {//{{{
    if (aChange.isDelete) {
        shadows.RemoveBookmarks (this.username(), this.password(), [aChange.url]);
    } else {
        var p = aChange.properties;

        // translate flock tags into shadows tags.
        var tags = '';
        if (p.fields & FIELD_TAGS) {
            // ugly, but better than nothing
            tags = p.tags.split(',').join(' ');
        }

        //Use version=0 now - the client always wins
        shadows.setBookmarks (this.username(), this.password(),
            [{URL: aChange.url, Title: p.fields&FIELD_NAME?p.name:'', 
                Description: p.fields&FIELD_DESCRIPTION?p.description:'', 
                Tags: tags, Rating: 0, PrivacyFlag: p.private?1:0, 
                Version: 0, PermaLink: ""}]);
    }
}//}}}
shadowsWebService.prototype._changeCollection = function (aChange) {//{{{
    debug ('shadowsWebService._changeCollection ('+aChange+')\n');
    if (aChange.isDelete) {
        shadows.RemoveCollections (this.username(), this.password(), [aChange.name]);
    } else {
        var OrderedUrls=new Array();
        while (aChange.contents.hasMoreElements ()) {
            var r = aChange.contents.getNext ();
            var url = r.QueryInterface (nsIRDFResource).ValueUTF8;
            OrderedUrls.push (r.QueryInterface (nsIRDFResource).ValueUTF8);
        }
        //Use version=0 now - the client always wins
        shadows.setCollections (this.username(), this.password(),[{CollectionName: aChange.name, Version: 0, PrivacyFlag: 1, OrderedUrls: OrderedUrls}]);
    }
}//}}}

// flockIFavoritesWebService implementation
shadowsWebService.prototype.setFavoritesQueue = function (aQueue) {/*{{{*/
    debug ('shadowsWebService.setFavoritesQueue('+aQueue+')\n');
    this.mQueue = aQueue;
}/*}}}*/
shadowsWebService.prototype.getAllFavorites = function (aListener) {/*{{{*/
    debug ('shadowsWebService.getAllFavorites('+aListener+')\n');
    var callback = new Object();
    callback.onResult = function(result) {
        if (result['Response']['OverallStatus']['Entry']['Status']==401) {
            //Invalid login - prompt user again next pass
            /* shadows.PrefsManager.resetUsernamePassword(); */
            aListener.onFault(401, "Invalid Login or Password");
            Components.classes[FAVORITES_CONTRACTID]
                .getService (flockIFavoritesService).authenticationFailed ();
        } else {
            var changeqi = function (aIID) {
                if (aIID.equals(flockIFavoritesChange) || 
                        aIID.equals(flockIFavoritesChangeFavorite) || 
                        aIID.equals(nsISupports)) {
                    return this;
                }
                throw Components.results.NS_ERROR_NO_INTERFACE;
            }
            changes = new Array();
            for (var ps in result['CollectionList']) {
                post=result['CollectionList'][ps];

                var properties = {
                    fields: FIELD_NAME|FIELD_DESCRIPTION|FIELD_TAGS|FIELD_PRIVATE|FIELD_ADDEDDATE|FIELD_LASTMODIFIEDDATE,
                    name: post['Title'],
                    description: post['Description'],
                    private: (post['PrivacyFlag'])==1,
                    tags: post['Tags'].split(' ').join(','),
                    addedDate: parseDate (post['Info']['CreatedTime']).getTime(),
                    lastModifiedDate: parseDate (post['Info']['LastUpdatedTime']).getTime(),
                }
                debug('new change to queue, url:"'+post['URL']+'"\n');
                var converter = Components.classes[UNICODECONVERTER_CONTRACTID]
                    .createInstance(nsIScriptableUnicodeConverter);
                converter.charset = 'UTF-8';
                post['URL'] = converter.ConvertFromUnicode(post['URL']) + converter.Finish();
                var deleteMe=false;
                if (post['PrivacyFlag'] == -1)  {
                    deleteMe=true;
                }
                changes.push ({
                    url: post['URL'],
                    isDelete: deleteMe,
                    properties: properties,
                    QueryInterface: changeqi
                });
            }
            changes.hasMoreElements = function () { return this.length != 0; }
            changes.getNext = function () { return this.shift (); }
            //Set the high water mark
            aListener.onSucceeded (changes);
            //TODO: FIXME: We shouldn't really set this until we know that all the changes we just queued made it in
            shadows.PrefsManager.setBookmarkVersion(result['Response']['OverallStatus']['Version']);
        }
    }
    callback.onFault = function (aFaultCode, aFaultString) {
        debug('callback.onFault aFaultCode "'+aFaultCode+'" aFaultString "'+aFaultString+'"\n');
        aListener.onFailed(aFaultString);
    }
    callback.onError = function (aMessage) {
        debug('callback.onError aMessage "'+aMessage+'"\n');
        aListener.onFailed(aMessage);
    }
    shadows.getAllBookmarks (this.username(), this.password(), shadows.PrefsManager.getBookmarkVersion(0), callback);

}/*}}}*/
shadowsWebService.prototype.getSyncRequired = function (aListener) {/*{{{*/

    debug ('shadowsWebService.getSyncRequired()\n');
    var callbackBookmarkVersion = new Object();
    callbackBookmarkVersion.onResult = function(result) {
        debug ('shadowsWebService.getSyncRequired() - checking max bookmark version. Site: "' + result['OverallStatus']['Version'] + '" Local: "' + shadows.PrefsManager.getBookmarkVersion(0) + '"\n');
        if (result['OverallStatus']['Version'] > shadows.PrefsManager.getBookmarkVersion(0)) {
            debug ('shadowsWebService.getSyncRequired() - bookmarks need sync\n');
            aListener.onSucceeded (true);
        } else {
            debug ('shadowsWebService.getSyncRequired() - retrieving max collection version\n');
                var callbackCollectionVersion = new Object();
                callbackCollectionVersion.onResult = function(result) {
                    debug ('shadowsWebService.getSyncRequired() - checking max collection version. Site: "' + result['OverallStatus']['Version'] + '" Local: "' + shadows.PrefsManager.getCollectionVersion(0) + '"\n');
                    if (result['OverallStatus']['Version'] > shadows.PrefsManager.getCollectionVersion(0)) {
                        debug ('shadowsWebService.getSyncRequired() - collections need sync\n');
                        aListener.onSucceeded (true);
                    } else {
                        debug ('shadowsWebService.getSyncRequired() - no sync needed\n');
                        aListener.onSucceeded (false);
                    }
                }
                callbackCollectionVersion.onFault = function (aFaultCode, aFaultString) {
                    debug('callbackCollectionVersion.onFault aFaultCode "'+aFaultCode+'" aFaultString "'+aFaultString+'"\n');
                    aListener.onFailed(aFaultString);
                }
                callbackCollectionVersion.onError = function (aMessage) {
                    debug('callbackCollectionVersion.onError aMessage "'+aMessage+'"\n');
                    aListener.onFailed(aMessage);
                }
            shadows.GetCollectionVersion (shadows.PrefsManager.getUsername(''), shadows.PrefsManager.getPassword(''), callbackCollectionVersion);
        }
    }
    callbackBookmarkVersion.onFault = function (aFaultCode, aFaultString) {
        debug('callbackBookmarkVersion.onFault aFaultCode "'+aFaultCode+'" aFaultString "'+aFaultString+'"\n');
        aListener.onFailed(aFaultString);
    }
    callbackBookmarkVersion.onError = function (aMessage) {
        debug('callbackBookmarkVersion.onError aMessage "'+aMessage+'"\n');
        aListener.onFailed(aMessage);
    }
    debug ('shadowsWebService.getSyncRequired() - retrieving max bookmark version\n');
    shadows.GetBookmarkVersion (this.username(), this.password(), callbackBookmarkVersion);
}/*}}}*/
shadowsWebService.prototype.__defineGetter__('webView', function() { // {{{
    debug ('shadowsWebService.webView getter\n');
    var url = 'http://www.shadows.com/';

    var login = this.username();

    if (login) {
        return url+'users/' + login;
    } else {
        return url;
    }
}); // }}}
shadowsWebService.prototype.getAllCollections = function (aListener) {/*{{{*/
    debug ('shadowsWebService.getAllCollections('+aListener+')\n');
    var callback = new Object ();
    callback.onResult = function (result) {
        if (result['Response']['OverallStatus']['Entry']['Status']==401) {
            //Invalid login - prompt user again next pass
            /* shadows.PrefsManager.resetUsernamePassword(); */
            aListener.onFault(401, "Invalid Login or Password");
            Components.classes[FAVORITES_CONTRACTID]
                .getService (flockIFavoritesService).authenticationFailed ();

        } else {
            var changeqi = function (aIID) {
                if (aIID.equals(flockIFavoritesChange) || 
                        aIID.equals(flockIFavoritesChangeCollection) || 
                        aIID.equals(nsISupports)) {
                    return this;
                }
                throw Components.results.NS_ERROR_NO_INTERFACE;
            }
            changes = new Array();
            for (var ps in result['CollectionList']) {
                var post=result['CollectionList'][ps];
                debug('new change to queue, name:"'+post['CollectionName']+'"\n');

                var contents = post['OrderedUrls'];
        	    contents.hasMoreElements = function () { return this.length != 0; }
        	    contents.getNext = function () { return RDFS.GetResource (this.shift ()); }
                var deleteMe=false;
                //FIXME: Why doesn't this work? It works for favorites...
                if (post['PrivacyFlag'] == -1)  {
                    deleteMe=true;
                }
                var change = {
                    name: post['CollectionName'],
                    isDelete: deleteMe,
                    QueryInterface: changeqi,
                    contents: post['OrderedUrls'],
                };

                changes.push (change);
            }
            changes.hasMoreElements = function () { return this.length != 0; }
            changes.getNext = function () { return this.shift (); }
            //Set the high water mark
            aListener.onSucceeded (changes);
            //TODO: FIXME: We shouldn't really set this until we know that all the changes we just queued made it in
            shadows.PrefsManager.setCollectionVersion(result['Response']['OverallStatus']['Version']);
        }
    }
    callback.onFault = function (aFaultCode, aFaultString) {
        debug('callback.onFault aFaultCode "'+aFaultCode+'" aFaultString "'+aFaultString+'"\n');
        aListener.onFailed(aFaultString);
    }
    callback.onError = function (aMessage) {
        debug('callback.onError aMessage "'+aMessage+'"\n');
        aListener.onFailed(aMessage);
    }
    shadows.getAllCollections (this.username(), this.password(), shadows.PrefsManager.getCollectionVersion(0), callback);
    
}/*}}}*/
shadowsWebService.prototype.login = function (aUserName, aPassword, aListener) {/*{{{*/
    debug ('shadowsWebService.login("'+aUserName+'", "'+aPassword+'", '+aListener+')\n');

    shadows.PrefsManager.setUsername (aUserName);
    shadows.PrefsManager.setPassword (aPassword);

    var callback = new Object ();
    callback.onResult = function (result) {
        debug ('shadowsWebService.login.callback('+result+')\n');
        debug ('result.OverallStatus.Entry.Status: '+result.OverallStatus.Entry.Status+'\n');
        if (result.OverallStatus.Entry.Status == 0) {
            aListener.onSucceeded ();
        } else {
            aListener.onFailed (result.OverallStatus.Entry.Message);
        }
    }
    callback.onFault = function (aFaultCode, aFaultString) {
        debug('callback.onFault aFaultCode "'+aFaultCode+'" aFaultString "'+aFaultString+'"\n');
        aListener.onFailed(aFaultString);
    }
    callback.onError = function (aMessage) {
        debug('callback.onError aMessage "'+aMessage+'"\n');
        aListener.onFailed(aMessage);
    }

    shadows.GetBookmarkVersion (aUserName, aPassword, callback);
}/*}}}*/
shadowsWebService.prototype.getUsername = function () {/*{{{*/
    return shadows.PrefsManager.getUsername('');
}/*}}}*/
shadowsWebService.prototype.getPassword = function () {/*{{{*/
    return shadows.PrefsManager.getPassword('');
}/*}}}*/
shadowsWebService.prototype.serviceDisabled = function () {/*{{{*/
    shadows.PrefsManager.resetBookmarkVersion ();
    shadows.PrefsManager.resetCollectionVersion ();
}/*}}}*/

/*}}}*/
debug ('loading shadowsWebService boilerplate\n');
// 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 ('{4eac53f6-82b1-4051-8fca-fe064d127b83}',
        '@flock.com/favorites/service/shadows;1', 
        'Shadows Favorites Web Service', 
        shadowsWebService);
module.setFavoritesWebService ('shadows-favorites-service');
function NSGetModule(aCompMgr, aFileSpec) {
    return module;
};
// }}}

loadLibrary ('flockXmlRpcHelper.js.lib');

function loadLibraryFromSpec(spec)
{
    var loader = Components.classes['@mozilla.org/moz/jssubscript-loader;1']
        .getService(Components.interfaces.mozIJSSubScriptLoader);
    loader.loadSubScript(spec);
}
debug ('loading shadowsWebService contrib/shadows\n');
loadLibraryFromSpec('chrome://browser/content/flock/contrib/shadows.js');
debug ('loaded shadowsWebService contrib/shadows\n');
