Fix bug 853236 - Provide access to Google CalDAV v2 API. r=mmecca
authorPhilipp Kewisch <mozilla@kewis.ch>
Sun, 16 Jun 2013 23:28:56 +0200
changeset 12580 723ed450d2e70ea97f8e6572af75650cab7581c4
parent 12579 a17d8582694a9c674bd0f6b3c1b4294f757075a4
child 12581 4590ee122e963168ecf22e9192faa5505a321747
push id9253
push usermozilla@kewis.ch
push dateSun, 16 Jun 2013 21:30:33 +0000
treeherdercomm-central@723ed450d2e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmmecca
bugs853236
Fix bug 853236 - Provide access to Google CalDAV v2 API. r=mmecca
calendar/base/modules/Makefile.in
calendar/base/modules/OAuth2.jsm
calendar/providers/caldav/calDavCalendar.js
calendar/providers/caldav/calDavRequestHandlers.js
--- a/calendar/base/modules/Makefile.in
+++ b/calendar/base/modules/Makefile.in
@@ -16,12 +16,13 @@ EXTRA_JS_MODULES = \
     calItemUtils.jsm \
     calIteratorUtils.jsm \
     calItipUtils.jsm \
     calPrintUtils.jsm \
     calProviderUtils.jsm \
     calRecurrenceUtils.jsm \
     calUtils.jsm \
     calXMLUtils.jsm \
+    OAuth2.jsm \
     ical.js \
     $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/calendar/base/modules/OAuth2.jsm
@@ -0,0 +1,196 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Provides OAuth 2.0 authentication
+ */
+var EXPORTED_SYMBOLS = ["OAuth2"];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource:///modules/http.jsm");
+Cu.import("resource:///modules/Services.jsm");
+Cu.import("resource:///modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/gloda/log4moz.js");
+
+function parseURLData(aData) {
+  let result = {};
+  aData.split("?", 2)[1].split("&").forEach(function (aParam) {
+    let [key, value] = aParam.split("=");
+    result[key] = value;
+  });
+  return result;
+}
+
+function OAuth2(aBaseURI, aScope, aAppKey, aAppSecret) {
+    this.baseURI = aBaseURI;
+    this.authURI = aBaseURI + "oauth2/auth";
+    this.tokenURI = aBaseURI + "oauth2/token";
+    this.consumerKey = aAppKey;
+    this.consumerSecret = aAppSecret;
+    this.scope = aScope;
+
+    this.log = Log4Moz.getConfiguredLogger("TBOAuth");
+}
+
+OAuth2.CODE_AUTHORIZATION = "authorization_code";
+OAuth2.CODE_REFRESH = "refresh_token";
+
+OAuth2.prototype = {
+
+    responseType: "code",
+    consumerKey: null,
+    consumerSecret: null,
+    completionURI: "http://localhost",
+    scope: null,
+
+    accessToken: null,
+    refreshToken: null,
+    tokenExpires: 0,
+    connecting: false,
+
+    connect: function connect(aSuccess, aFailure, aWithUI, aRefresh) {
+        if (this.connecting) {
+            return;
+        }
+
+        this.connectSuccessCallback = aSuccess;
+        this.connectFailureCallback = aFailure;
+
+        if (!aRefresh && this.accessToken) {
+            aSuccess();
+        } else if (this.refreshToken) {
+            this.connecting = true;
+            this.requestAccessToken(this.refreshToken, OAuth2.CODE_REFRESH);
+        } else {
+            if (!aWithUI) {
+                aFailure();
+                return;
+            }
+            this.connecting = true;
+            this.requestAuthorization();
+        }
+    },
+
+    requestAuthorization: function requestAuthorization() {
+        let params = [
+            ["response_type", this.responseType],
+            ["client_id", this.consumerKey],
+            ["redirect_uri", this.completionURI],
+            ["scope", this.scope]
+        ].map(function(p) p[0] + "=" + encodeURIComponent(p[1])).join("&");
+
+        this._browserRequest = {
+            promptText: "auth prompt",
+            account: this,
+            url: this.authURI + "?" + params,
+            _active: true,
+            iconURI: "",
+            cancelled: function() {
+                if (!this._active)
+                    return;
+            },
+
+            loaded: function (aWindow, aWebProgress) {
+                if (!this._active) {
+                    return;
+                }
+
+                this._listener = {
+                    window: aWindow,
+                    webProgress: aWebProgress,
+                    _parent: this.account,
+
+                    QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                                           Ci.nsISupportsWeakReference]),
+
+                    _cleanUp: function() {
+                      this.webProgress.removeProgressListener(this);
+                      this.window.close();
+                      delete this.window;
+                    },
+
+                    _checkForRedirect: function(aURL) {
+                      if (aURL.indexOf(this._parent.completionURI) != 0)
+                        return;
+
+                      this._parent.finishAuthorizationRequest();
+                      this._parent.onAuthorizationReceived(aURL);
+                    },
+
+                    onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+                      const wpl = Ci.nsIWebProgressListener;
+                      if (aStateFlags & (wpl.STATE_START | wpl.STATE_IS_NETWORK))
+                        this._checkForRedirect(aRequest.name);
+                    },
+                    onLocationChange: function(aWebProgress, aRequest, aLocation) {
+                      this._checkForRedirect(aLocation.spec);
+                    },
+                    onProgressChange: function() {},
+                    onStatusChange: function() {},
+                    onSecurityChange: function() {},
+                };
+                aWebProgress.addProgressListener(this._listener,
+                                                 Ci.nsIWebProgress.NOTIFY_ALL);
+            }
+        };
+
+        this.wrappedJSObject = this._browserRequest;
+        Services.ww.openWindow(null,
+                               "chrome://messenger/content/browserRequest.xul",
+                                null, "chrome,centerscreen,width=980px,height=600px", this);
+    },
+    finishAuthorizationRequest: function() {
+      if (!("_browserRequest" in this))
+        return;
+
+      this._browserRequest._active = false;
+      if ("_listener" in this._browserRequest)
+        this._browserRequest._listener._cleanUp();
+      delete this._browserRequest;
+    },
+    onAuthorizationReceived: function(aData) {
+      this.log.info("authorization received" + aData);
+      let results = parseURLData(aData);
+      this.requestAccessToken(results.code, OAuth2.CODE_AUTHORIZATION);
+    },
+
+    requestAccessToken: function requestAccessToken(aCode, aType) {
+
+        let params = [
+            ["client_id", this.consumerKey],
+            ["client_secret", this.consumerSecret],
+            ["grant_type", aType],
+        ];
+
+        if (aType == OAuth2.CODE_AUTHORIZATION) {
+            params.push(["code", aCode]);
+            params.push(["redirect_uri", this.completionURI]);
+        } else if (aType == OAuth2.CODE_REFRESH) {
+            params.push(["refresh_token", aCode]);
+        }
+
+        doXHRequest(this.tokenURI, null, params, this.onAccessTokenReceived, this.onAccessTokenFailed, this);
+    },
+
+    onAccessTokenFailed: function onAccessTokenFailed(aData) {
+        this.refreshToken = null;
+        this.connecting = false;
+        this.connectFailureCallback();
+    },
+
+    onAccessTokenReceived: function onRequestTokenReceived(aData) {
+        let result = JSON.parse(aData);
+
+        this.accessToken = result.access_token;
+        if ("refresh_token" in result) {
+            this.refreshToken = result.refresh_token;
+        }
+        this.tokenExpires = (new Date()).getTime() + (result.expires_in * 1000);
+        this.tokenType = result.token_type;
+
+        this.connecting = false;
+        this.connectSuccessCallback();
+    }
+};
--- a/calendar/providers/caldav/calDavCalendar.js
+++ b/calendar/providers/caldav/calDavCalendar.js
@@ -5,16 +5,17 @@
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 Components.utils.import("resource://calendar/modules/calUtils.jsm");
 Components.utils.import("resource://calendar/modules/calXMLUtils.jsm");
 Components.utils.import("resource://calendar/modules/calIteratorUtils.jsm");
 Components.utils.import("resource://calendar/modules/calProviderUtils.jsm");
 Components.utils.import("resource://calendar/modules/calAuthUtils.jsm");
+Components.utils.import("resource://calendar/modules/OAuth2.jsm");
 
 //
 // calDavCalendar.js
 //
 
 const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>\n';
 
 const davNS = "DAV:"
@@ -218,17 +219,17 @@ calDavCalendar.prototype = {
         }
     },
 
     // in calIGenericOperationListener aListener
     replayChangesOn: function caldav_replayChangesOn(aChangeLogListener) {
         if (!this.checkedServerInfo) {
             // If we haven't refreshed yet, then we should check the resource
             // type first. This will call refresh() again afterwards.
-            this.checkDavResourceType(aChangeLogListener);
+            this.setupAuthentication(aChangeLogListener);
         } else {
             this.safeRefresh(aChangeLogListener);
         }
     },
     setMetaData: function caldav_setMetaData(id, path, etag, isInboxItem) {
         if (this.mOfflineStorage.setMetaData) {
             if (id) {
                 var dataString = [etag,path,(isInboxItem ? "true" : "false")].join("\u001A");
@@ -314,16 +315,30 @@ calDavCalendar.prototype = {
                     this.mItemInfoCache[itemId] = item;
                 }
             }
         }
 
         this.ensureMetaData();
     },
 
+    prepHttpChannel: function(aUri, aUploadData, aContentType, aNotificationCallbacks, aExisting) {
+        let channel = cal.prepHttpChannel.apply(cal, arguments);
+
+        // Google does its CalDAV v2 authentication via OAuth. Since there is
+        // no builtin OAuth support, we have to inject the tokens here.
+        if (aUri.host == "apidata.googleusercontent.com" &&
+            this.oauth && this.oauth.accessToken) {
+            let hdr = "Bearer " + this.oauth.accessToken;
+            channel.setRequestHeader("Authorization", hdr, false);
+        }
+
+        return channel;
+    },
+
     //
     // calICalendar interface
     //
 
     // readonly attribute AUTF8String type;
     get type() { return "caldav"; },
 
     mDisabled: true,
@@ -633,20 +648,20 @@ calDavCalendar.prototype = {
                 thisCalendar.notifyPureOperationComplete(aListener,
                                                          listenerStatus,
                                                          cIOL.ADD,
                                                          parentItem.id,
                                                          listenerDetail);
             }
         };
 
-        let httpchannel = cal.prepHttpChannel(itemUri,
-                                              serializedItem,
-                                              "text/calendar; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(itemUri,
+                                               serializedItem,
+                                               "text/calendar; charset=utf-8",
+                                               this);
 
         if (!aIgnoreEtag) {
             httpchannel.setRequestHeader("If-None-Match", "*", false);
         }
 
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, addListener);
     },
 
@@ -759,20 +774,20 @@ calDavCalendar.prototype = {
                 thisCalendar.notifyPureOperationComplete(aListener,
                                                          listenerStatus,
                                                          cIOL.MODIFY,
                                                          aNewItem.id,
                                                          listenerDetail);
             }
         };
 
-        let httpchannel = cal.prepHttpChannel(eventUri,
-                                              modifiedItemICS,
-                                              "text/calendar; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(eventUri,
+                                               modifiedItemICS,
+                                               "text/calendar; charset=utf-8",
+                                               this);
 
         if (!aIgnoreEtag) {
             httpchannel.setRequestHeader("If-Match",
                                          this.mItemInfoCache[aNewItem.id].etag,
                                          false);
         }
 
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, modListener);
@@ -863,20 +878,20 @@ calDavCalendar.prototype = {
                             // listeners will be notified there.
                             thisCalendar.mOfflineStorage.deleteItem(aItem, aListener);
                             return;
                         }
                     }
                 } else if (responseStatus == 412 || responseStatus == 409) {
                     // item has either been modified or deleted by someone else check to see which
                     cal.LOG("CalDAV: Item has been modified on server, checking if it has been deleted");
-                    let httpchannel2 = cal.prepHttpChannel(eventUri,
-                                                           null,
-                                                           null,
-                                                           thisCalendar);
+                    let httpchannel2 = thisCalendar.prepHttpChannel(eventUri,
+                                                                    null,
+                                                                    null,
+                                                                    thisCalendar);
                     httpchannel2.requestMethod = "HEAD";
                     cal.sendHttpRequest(cal.createStreamLoader(), httpchannel2, delListener2);
                     return;
                 } else if (responseStatus >= 500 && responseStatus <= 510) {
                     listenerStatus = Components.results.NS_ERROR_NOT_AVAILABLE;
                     listenerDetail = "Server Replied with " + responseStatus;
                 } else if (responseStatus) {
                     cal.ERROR("CalDAV: Unexpected status deleting item from " +
@@ -941,17 +956,17 @@ calDavCalendar.prototype = {
                                                          listenerDetail);
             }
         };
 
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: Deleting " + eventUri.spec);
         }
 
-        let httpchannel = cal.prepHttpChannel(eventUri, null, null, this);
+        let httpchannel = this.prepHttpChannel(eventUri, null, null, this);
         if (!aIgnoreEtag) {
             let etag = this.mItemInfoCache[aItem.id].etag;
             cal.LOG("CalDAV: Will only delete if matches etag " + etag);
             httpchannel.setRequestHeader("If-Match", etag, false);
         }
         httpchannel.requestMethod = "DELETE";
 
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, delListener);
@@ -1290,17 +1305,17 @@ calDavCalendar.prototype = {
 
         this.ensureTargetCalendar();
 
         if (this.mAuthScheme == "Digest") {
             // the auth could have timed out and be in need of renegotiation
             // we can't risk several calendars doing this simultaneously so
             // we'll force the renegotiation in a sync query, using OPTIONS to keep
             // it quick
-            let headchannel = cal.prepHttpChannel(this.makeUri(), null, null, this);
+            let headchannel = this.prepHttpChannel(this.makeUri(), null, null, this);
             headchannel.requestMethod = "OPTIONS";
             headchannel.open();
             headchannel.QueryInterface(Components.interfaces.nsIHttpChannel);
             try {
               if (headchannel.responseStatus != 200) {
                 throw "OPTIONS returned unexpected status code: " + headchannel.responseStatus;
               }
             }
@@ -1329,20 +1344,20 @@ calDavCalendar.prototype = {
               '<D:prop>' +
                 '<CS:getctag/>' +
               '</D:prop>' +
             '</D:propfind>';
 
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: send(" + this.makeUri().spec + "): " + queryXml);
         }
-        let httpchannel = cal.prepHttpChannel(this.makeUri(),
-                                              queryXml,
-                                              "text/xml; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(this.makeUri(),
+                                               queryXml,
+                                               "text/xml; charset=utf-8",
+                                               this);
         httpchannel.setRequestHeader("Depth", "0", false);
         httpchannel.requestMethod = "PROPFIND";
 
         var streamListener = {};
         streamListener.onStreamComplete =
             function safeRefresh_safeRefresh_onStreamComplete(aLoader, aContext, aStatus, aResultLength, aResult) {
             let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
             try {
@@ -1364,17 +1379,17 @@ calDavCalendar.prototype = {
                 if (thisCalendar.isCached && aChangeLogListener) {
                     aChangeLogListener.onResult({ status: Components.results.NS_ERROR_FAILURE },
                                                 Components.results.NS_ERROR_FAILURE);
                 }
                 return;
             } else if (request.responseStatus == 207 && thisCalendar.mDisabled) {
                 // Looks like the calendar is there again, check its resource
                 // type first.
-                thisCalendar.checkDavResourceType(aChangeLogListener);
+                thisCalendar.setupAuthentication(aChangeLogListener);
                 return;
              }
 
             let str = cal.convertByteArray(aResult, aResultLength);
             if (!str) {
                 cal.LOG("CalDAV: Failed to get ctag from server for calendar " +
                         thisCalendar.name);
             } else if (thisCalendar.verboseLogging()) {
@@ -1460,17 +1475,17 @@ calDavCalendar.prototype = {
      *                                     params. They will be appended in this
      *                                     function.
      * @param aChangeLogListener    (optional) The listener to notify for cached
      *                                         calendars.
      */
     getUpdatedItems: function caldav_getUpdatedItems(aUri, aChangeLogListener) {
         if (this.mDisabled) {
             // check if maybe our calendar has become available
-            this.checkDavResourceType(aChangeLogListener);
+            this.setupAuthentication(aChangeLogListener);
             return;
         }
 
         if (this.mHasWebdavSyncSupport) {
             webDavSync = new webDavSyncHandler(this,aUri,aChangeLogListener);
             webDavSync.doWebDAVSync();
             return;
         }
@@ -1485,20 +1500,20 @@ calDavCalendar.prototype = {
               '</D:prop>' +
             '</D:propfind>';
 
         let requestUri = this.makeUri(null, aUri);
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
         }
 
-        let httpchannel = cal.prepHttpChannel(requestUri,
-                                              queryXml,
-                                              "text/xml; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(requestUri,
+                                               queryXml,
+                                               "text/xml; charset=utf-8",
+                                               this);
         httpchannel.requestMethod = "PROPFIND";
         httpchannel.setRequestHeader("Depth", "1", false);
 
         // Submit the request
         let streamListener = new etagsHandler(this, aUri, aChangeLogListener);
         httpchannel.asyncOpen(streamListener, httpchannel);
     },
 
@@ -1508,21 +1523,80 @@ calDavCalendar.prototype = {
      */
     getInterface: cal.InterfaceRequestor_getInterface,
 
     //
     // Helper functions
     //
 
     /**
-     * Checks that the calendar URI exists and is a CalDAV calendar. This is the
+     * Sets up any needed prerequisites regarding authentication. This is the
      * beginning of a chain of asynchronous calls. This function will, when
      * done, call the next function related to checking resource type, server
      * capabilties, etc.
      *
+     * setupAuthentication                         * You are here
+     * checkDavResourceType
+     * checkServerCaps
+     * findPrincipalNS
+     * checkPrincipalsNameSpace
+     * completeCheckServerInfo
+     */
+    setupAuthentication: function(aChangeLogListener) {
+        let self = this;
+        function authSuccess() {
+            self.checkDavResourceType(aChangeLogListener);
+        }
+        function authFailed() {
+            self.setProperty("disabled", "true");
+            self.setProperty("auto-enabled", "true");
+            self.completeCheckServerInfo(aChangeLogListener, Components.results.NS_ERROR_FAILURE);
+        }
+        if (this.mUri.host == "apidata.googleusercontent.com") {
+            if (!this.oauth) {
+                this.oauth = new OAuth2(OAUTH_BASE_URI, OAUTH_SCOPE,
+                                        OAUTH_CLIENT_ID, OAUTH_HASH);
+                let sessionId = this.id;
+                let pwMgrId = "Google CalDAV v2";
+
+                Object.defineProperty(this.oauth, "refreshToken", {
+                    get: function getRefreshToken() {
+                        if (!this.mRefreshToken) {
+                            var pass = { value: null };
+                            cal.auth.passwordManagerGet(sessionId, pass, sessionId, pwMgrId);
+                            this.mRefreshToken = pass.value;
+                        }
+                        return this.mRefreshToken;
+                    },
+                    set: function setRefreshToken(val) {
+                        if (!val) {
+                            cal.auth.passwordManagerRemove(sessionId, sessionId, pwMgrId);
+                        } else {
+                            cal.auth.passwordManagerSave(sessionId, val, sessionId, pwMgrId);
+                        }
+                        return (this.mRefreshToken = val);
+                    },
+                    enumerable: true
+                });
+            }
+
+            if (this.oauth.accessToken) {
+                authSuccess();
+            } else {
+                this.oauth.connect(authSuccess, authFailed, true);
+            }
+        } else {
+            authSuccess();
+        }
+    },
+
+    /**
+     * Checks that the calendar URI exists and is a CalDAV calendar.
+     *
+     * setupAuthentication
      * checkDavResourceType                        * You are here
      * checkServerCaps
      * findPrincipalNS
      * checkPrincipalsNameSpace
      * completeCheckServerInfo
      */
     checkDavResourceType: function caldav_checkDavResourceType(aChangeLogListener) {
         this.ensureTargetCalendar();
@@ -1542,20 +1616,20 @@ calDavCalendar.prototype = {
                 '<C:supported-calendar-component-set/>' +
                 '<CS:getctag/>' +
               '</D:prop>' +
             '</D:propfind>';
 
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: send: " + queryXml);
         }
-        let httpchannel = cal.prepHttpChannel(this.makeUri(),
-                                              queryXml,
-                                              "text/xml; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(this.makeUri(),
+                                               queryXml,
+                                               "text/xml; charset=utf-8",
+                                               this);
         httpchannel.setRequestHeader("Depth", "0", false);
         httpchannel.requestMethod = "PROPFIND";
 
         var streamListener = {};
 
         streamListener.onStreamComplete =
             function checkDavResourceType_oSC(aLoader, aContext, aStatus, aResultLength, aResult) {
             let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
@@ -1707,27 +1781,28 @@ calDavCalendar.prototype = {
             thisCalendar.checkServerCaps(aChangeLogListener);
         };
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, streamListener);
     },
 
     /**
      * Checks server capabilities.
      *
+     * setupAuthentication
      * checkDavResourceType
      * checkServerCaps                              * You are here
      * findPrincipalNS
      * checkPrincipalsNameSpace
      * completeCheckServerInfo
      */
     checkServerCaps: function caldav_checkServerCaps(aChangeLogListener, calHomeSetUrlRetry) {
         let homeSet = this.makeUri(null, this.mCalHomeSet);
         var thisCalendar = this;
 
-        let httpchannel = cal.prepHttpChannel(homeSet, null, null, this);
+        let httpchannel = this.prepHttpChannel(homeSet, null, null, this);
 
         httpchannel.requestMethod = "OPTIONS";
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: send: OPTIONS " + homeSet.spec);
         }
 
         var streamListener = {};
         streamListener.onStreamComplete =
@@ -1806,16 +1881,17 @@ calDavCalendar.prototype = {
 
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, streamListener);
     },
 
     /**
      * Locates the principal namespace. This function should soely be called
      * from checkServerCaps to find the principal namespace.
      *
+     * setupAuthentication
      * checkDavResourceType
      * checkServerCaps
      * findPrincipalNS                              * You are here
      * checkPrincipalsNameSpace
      * completeCheckServerInfo
      */
     findPrincipalNS: function caldav_findPrincipalNS(aChangeLogListener) {
         if (this.principalUrl) {
@@ -1834,20 +1910,20 @@ calDavCalendar.prototype = {
               '<D:prop>' +
                 '<D:principal-collection-set/>' +
               '</D:prop>' +
             '</D:propfind>';
 
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: send: " + homeSet.spec + "\n"  + queryXml);
         }
-        let httpchannel = cal.prepHttpChannel(homeSet,
-                                              queryXml,
-                                              "text/xml; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(homeSet,
+                                               queryXml,
+                                               "text/xml; charset=utf-8",
+                                               this);
 
         httpchannel.setRequestHeader("Depth", "0", false);
         httpchannel.requestMethod = "PROPFIND";
 
         var streamListener = {};
         streamListener.onStreamComplete =
             function findInOutboxes_oSC(aLoader, aContext, aStatus,
                                          aResultLength, aResult) {
@@ -1890,16 +1966,17 @@ calDavCalendar.prototype = {
 
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, streamListener);
     },
 
     /**
      * Checks the principals namespace for scheduling info. This function should
      * soely be called from findPrincipalNS
      *
+     * setupAuthentication
      * checkDavResourceType
      * checkServerCaps
      * findPrincipalNS
      * checkPrincipalsNameSpace                     * You are here
      * completeCheckServerInfo
      *
      * @param aNameSpaceList    List of available namespaces
      */
@@ -1961,20 +2038,20 @@ calDavCalendar.prototype = {
         // We want a trailing slash, ensure it.
         let nextNS = aNameSpaceList.pop().replace(/([^\/])$/, "$1/");
         let requestUri = makeURL(this.calendarUri.prePath + this.ensureEncodedPath(nextNS));
 
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: send: " + queryMethod + " " + requestUri.spec + "\n" + queryXml);
         }
 
-        let httpchannel = cal.prepHttpChannel(requestUri,
-                                              queryXml,
-                                              "text/xml; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(requestUri,
+                                               queryXml,
+                                               "text/xml; charset=utf-8",
+                                               this);
 
         httpchannel.requestMethod = queryMethod;
         if (queryDepth == 0) {
             // Set header, doing this for Depth: 1 is not needed since thats the
             // default.
             httpchannel.setRequestHeader("Depth", "0", false);
         }
 
@@ -2078,16 +2155,17 @@ calDavCalendar.prototype = {
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, streamListener);
     },
 
     /**
      * This is called to complete checking the server info. It should be the
      * final call when checking server options. This will either report the
      * error or if it is a success then refresh the calendar.
      *
+     * setupAuthentication
      * checkDavResourceType
      * checkServerCaps
      * findPrincipalNS
      * checkPrincipalsNameSpace
      * completeCheckServerInfo                      * You are here
      */
     completeCheckServerInfo: function caldav_completeCheckServerInfo(aChangeLogListener, aError) {
         if (Components.isSuccessCode(aError)) {
@@ -2234,20 +2312,20 @@ calDavCalendar.prototype = {
         fbComp.addProperty(prop);
         fbQuery.addSubcomponent(fbComp);
         fbQuery = fbQuery.serializeToICS();
         if (this.verboseLogging()) {
             cal.LOG("CalDAV: send (Originator=" + organizer +
                     ",Recipient=" + mailto_aCalId + "): " + fbQuery);
         }
 
-        let httpchannel = cal.prepHttpChannel(this.makeUri(null, this.outboxUrl),
-                                              fbQuery,
-                                              "text/calendar; charset=utf-8",
-                                              this);
+        let httpchannel = this.prepHttpChannel(this.makeUri(null, this.outboxUrl),
+                                               fbQuery,
+                                               "text/calendar; charset=utf-8",
+                                               this);
         httpchannel.requestMethod = "POST";
         httpchannel.setRequestHeader("Originator", organizer, false);
         httpchannel.setRequestHeader("Recipient", mailto_aCalId, false);
 
         var streamListener = {};
 
         streamListener.onStreamComplete =
             function caldav_GFBI_oSC(aLoader, aContext, aStatus,
@@ -2555,20 +2633,20 @@ calDavCalendar.prototype = {
                                        .createInstance(Components.interfaces.calIIcsSerializer);
             serializer.addItems([item], 1);
             var methodProp = getIcsService().createIcalProperty("METHOD");
             methodProp.value = aItipItem.responseMethod;
             serializer.addProperty(methodProp);
             var uploadData = serializer.serializeToString();
             let requestUri = this.makeUri(null, this.outboxUrl);
 
-            let httpchannel = cal.prepHttpChannel(requestUri,
-                                                  uploadData,
-                                                  "text/calendar; charset=utf-8",
-                                                  this);
+            let httpchannel = this.prepHttpChannel(requestUri,
+                                                   uploadData,
+                                                   "text/calendar; charset=utf-8",
+                                                   this);
             httpchannel.requestMethod = "POST";
             httpchannel.setRequestHeader("Originator", this.calendarUserAddress, false);
             for each (var recipient in aRecipients) {
                 httpchannel.setRequestHeader("Recipient", recipient.id, true);
             }
 
             var thisCalendar = this;
             var streamListener = {
@@ -2677,21 +2755,21 @@ calDavCalendar.prototype = {
         let uploadContent;
         if (aOldChannel instanceof Components.interfaces.nsIUploadChannel &&
             aOldChannel instanceof Components.interfaces.nsIHttpChannel &&
             aOldChannel.uploadStream) {
             uploadData = aOldChannel.uploadStream;
             uploadContent = aOldChannel.getRequestHeader("Content-Type");
         }
 
-        cal.prepHttpChannel(null,
-                            uploadData,
-                            uploadContent,
-                            this,
-                            aNewChannel);
+        this.prepHttpChannel(null,
+                             uploadData,
+                             uploadContent,
+                             this,
+                             aNewChannel);
 
         // Make sure we can get/set headers on both channels.
         aNewChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
         aOldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
 
         function copyHeader(aHdr) {
             try {
                 let hdrValue = aOldChannel.getRequestHeader(aHdr);
@@ -2720,16 +2798,33 @@ calDavCalendar.prototype = {
         aCallback.onRedirectVerifyCallback(Components.results.NS_OK);
     }
 };
 
 function calDavObserver(aCalendar) {
     this.mCalendar = aCalendar;
 }
 
+// Before you spend time trying to find out what this means, please note that
+// doing so and using the information WILL cause Google to revoke Lightning's
+// privileges,  which means not one Lightning user will be able to connect to
+// Google Calendar via CalDAV. This will cause unhappy users all around which
+// means that the Lightning developers will have to spend more time with user
+// support, which means less time for features, releases and bugfixes.  For a
+// paid developer this would actually mean financial harm.
+//
+// Do you really want all of this to be your fault? Instead of using the
+// information contained here please get your own copy, its really easy.
+this["\x65\x76\x61\x6C"]([String["\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F"+
+"\x64\x65"](("dpotu!PBVUI`CBTF`VSJ!>!#iuuqt;00bddpvout/hpphmf/dpn0p0#<dpot"+
+"u!PBVUI`TDPQF!>!#iuuqt;00xxx/hpphmfbqjt/dpn0bvui0dbmfoebs#<dpotu!PBVUI`DM"+
+"JFOU`JE!>!#831674:95649/bqqt/hpphmfvtfsdpoufou/dpn#<dpotu!PBVUI`IBTI!>!#z"+
+"Vs7YVgyvsbguj7s8{1TTfJR#<")["\x63\x68\x61\x72\x43\x6F\x64\x65\x41\x74"](i)-
+1)for(i in (function(){let x=231;while(x--)yield x})())].reverse().join(""))
+
 calDavObserver.prototype = {
     mCalendar: null,
     mInBatch: false,
 
     // calIObserver:
     onStartBatch: function() {
         this.mCalendar.observers.notify("onStartBatch");
         this.mInBatch = true;
--- a/calendar/providers/caldav/calDavRequestHandlers.js
+++ b/calendar/providers/caldav/calDavRequestHandlers.js
@@ -319,17 +319,17 @@ webDavSyncHandler.prototype = {
         Components.interfaces.nsISAXErrorHandler,
         Components.interfaces.nsIRequestObserver,
         Components.interfaces.nsIStreamListener
     ]),
 
     doWebDAVSync: function doWebDAVSync() {
         if (this.calendar.mDisabled) {
             // check if maybe our calendar has become available
-            this.calendar.checkDavResourceType(this.changeLogListener);
+            this.calendar.setupAuthentication(this.changeLogListener);
             return;
         }
 
 
         let syncTokenString = "<sync-token/>";
         if (this.calendar.mWebdavSyncToken && this.calendar.mWebdavSyncToken.length > 0) {
             let syncToken = cal.xml.escapeString(this.calendar.mWebdavSyncToken);
             syncTokenString = "<sync-token>" + syncToken + "</sync-token>";
@@ -346,20 +346,20 @@ webDavSyncHandler.prototype = {
           '</sync-collection>';
 
         let requestUri = this.calendar.makeUri(null, this.baseUri);
 
         if (this.calendar.verboseLogging()) {
             cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
         }
         cal.LOG("CalDAV: webdav-sync Token: " + this.calendar.mWebdavSyncToken);
-        let httpchannel = cal.prepHttpChannel(requestUri,
-                                              queryXml,
-                                              "text/xml; charset=utf-8",
-                                              this.calendar);
+        let httpchannel = this.calendar.prepHttpChannel(requestUri,
+                                                        queryXml,
+                                                        "text/xml; charset=utf-8",
+                                                        this.calendar);
         httpchannel.setRequestHeader("Depth", "1", false);
         httpchannel.requestMethod = "REPORT";
         // Submit the request
         httpchannel.asyncOpen(this, httpchannel);
     },
 
     /**
      * @see nsIStreamListener
@@ -657,17 +657,17 @@ multigetSyncHandler.prototype = {
         Components.interfaces.nsISAXErrorHandler,
         Components.interfaces.nsIRequestObserver,
         Components.interfaces.nsIStreamListener
     ]),
 
     doMultiGet: function doMultiGet() {
         if (this.calendar.mDisabled) {
             // check if maybe our calendar has become available
-            this.calendar.checkDavResourceType(this.changeLogListener);
+            this.calendar.setupAuthentication(this.changeLogListener);
             return;
         }
 
         let batchSize = cal.getPrefSafe("calendar.caldav.multigetBatchSize", 100);
         let hrefString = "";
         while (this.itemsNeedFetching.length && batchSize > 0) {
             batchSize--;
             // ensureEncodedPath extracts only the path component of the item and
@@ -685,20 +685,20 @@ multigetSyncHandler.prototype = {
             '</D:prop>' +
             hrefString +
           '</C:calendar-multiget>';
 
         let requestUri = this.calendar.makeUri(null, this.baseUri);
         if (this.calendar.verboseLogging()) {
             cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
         }
-        let httpchannel = cal.prepHttpChannel(requestUri,
-                                              queryXml,
-                                              "text/xml; charset=utf-8",
-                                              this.calendar);
+        let httpchannel = this.calendar.prepHttpChannel(requestUri,
+                                                        queryXml,
+                                                        "text/xml; charset=utf-8",
+                                                        this.calendar);
         httpchannel.setRequestHeader("Depth", "1", false);
         httpchannel.requestMethod = "REPORT";
         // Submit the request
         httpchannel.asyncOpen(this, httpchannel);
     },
 
     /**
      * @see nsIStreamListener