Fix bug 498690 - support for webdav-sync spec. r=philipp
authorSimon Vaillancourt <simon.at.orcl@gmail.com>
Fri, 19 Feb 2010 19:04:33 +0100
changeset 4966 adc24a08b1b4453a05dea61c82bae54d3103ef1e
parent 4965 910c696262b37f541bdd8b7feecf546b6386f214
child 4967 c63d9bfe9adfb08630e951328fe3187fd68087a2
push idunknown
push userunknown
push dateunknown
reviewersphilipp
bugs498690
Fix bug 498690 - support for webdav-sync spec. r=philipp
calendar/providers/caldav/calDavCalendar.js
calendar/providers/caldav/calDavRequestHandlers.js
--- a/calendar/providers/caldav/calDavCalendar.js
+++ b/calendar/providers/caldav/calDavCalendar.js
@@ -23,16 +23,18 @@
  *   Dan Mosedale <dan.mosedale@oracle.com>
  *   Mike Shaver <mike.x.shaver@oracle.com>
  *   Gary van der Merwe <garyvdm@gmail.com>
  *   Bruno Browning <browning@uwalumni.com>
  *   Matthew Willis <lilmatt@mozilla.com>
  *   Daniel Boelzle <daniel.boelzle@sun.com>
  *   Philipp Kewisch <mozilla@kewis.ch>
  *   Wolfgang Sourdeau <wsourdeau@inverse.ca>
+ *   Simon Vaillancourt <simon.at.orcl@gmail.com>
+ *
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -196,16 +198,18 @@ calDavCalendar.prototype = {
         this.mTargetCalendar.getAllMetaData({}, cacheIds, cacheValues);
         cacheIds = cacheIds.value;
         cacheValues = cacheValues.value;
         for (var count = 0; count < cacheIds.length; count++) {
             var itemId = cacheIds[count];
             var itemData = cacheValues[count];
             if (itemId == "ctag") {
                 this.mCtag = itemData;
+            } else if (itemId == "sync-token") {
+                this.mWebdavSyncToken = itemData;
             } else {
                 var itemDataArray = itemData.split("\u001A");
                 var etag = itemDataArray[0];
                 var resourcePath = itemDataArray[1];
                 var isInboxItem = itemDataArray[2];
                 if (itemDataArray.length == 3) {
                     this.mHrefIndex[resourcePath] = itemId;
                     var locationPath = decodeURIComponent(resourcePath)
@@ -323,16 +327,28 @@ calDavCalendar.prototype = {
     mFirstRefreshDone: false,
 
     mQueuedQueries: null,
 
     mCtag: null,
 
     mTargetCalendar: null,
 
+    // Contains the last valid synctoken returned
+    // from the server with Webdav Sync enabled servers
+    mWebdavSyncToken: null,
+    // Indicates that the server supports Webdav Sync
+    // see: http://tools.ietf.org/html/draft-daboo-webdav-sync
+    mHasWebdavSyncSupport: false,
+    // By default, assume that the server can return the calendar-data
+    // property on a sync request, if not supported (ie: Apple Server),
+    // a subsequent multiget needs to be sent to the server to retrieve
+    // the calendar-data property.
+    mHasWebdavSyncCalendarDataSupport: true,
+
     get authRealm caldav_get_authRealm() {
         return this.mAuthRealm;
     },
 
     makeUri: function caldav_makeUri(aInsertString, aBaseUri) {
         let baseUri = aBaseUri || this.calendarUri;
         let spec = baseUri.spec + (aInsertString || "");
         if (this.mUriParams) {
@@ -754,16 +770,210 @@ calDavCalendar.prototype = {
                                          false);
         }
         httpchannel.requestMethod = "DELETE";
 
         cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, delListener);
     },
 
     /**
+     * Add an item to the target calendar
+     *
+     * @param href      Item href
+     * @param calData   iCalendar string representation of the item
+     * @param aUri      Base URI of the request
+     * @param aListener Listener
+     */
+    addTargetCalendarItem : function caldav_addTargetCalendarItem(href,calData,aUri, etag, aListener) {
+        let parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
+                               .createInstance(Components.interfaces.calIIcsParser);
+        let uriPathComponentLength = aUri.path.split("/").length;
+        let resourcePath = this.ensurePath(href);
+        try {
+            parser.parseString(calData);
+        } catch (e) {
+            // Warn and continue.
+            // TODO As soon as we have activity manager integration,
+            // this should be replace with logic to notify that a
+            // certain event failed.
+            cal.WARN("Failed to parse item: " + response.toXMLString());
+            return;
+        }
+        // with CalDAV there really should only be one item here
+        let items = parser.getItems({});
+        let propertiesList = parser.getProperties({});
+        let method;
+        for each (var prop in propertiesList) {
+            if (prop.propertyName == "METHOD") {
+                method = prop.value;
+                break;
+            }
+        }
+        let isReply = (method == "REPLY");
+        let item = items[0];
+        if (!item) {
+            cal.WARN("Failed to parse item: " + calData);
+            return;
+        }
+
+        item.calendar = this.superCalendar;
+        if (isReply && this.isInbox(aUri.spec)) {
+            if (this.hasScheduling) {
+                this.processItipReply(item, resourcePath);
+            }
+            cal.WARN("REPLY method but calendar does not support scheduling");
+            return;
+        }
+
+        // Strip of the same number of components as the request
+        // uri's path has. This way we make sure to handle servers
+        // that pass hrefs like /dav/user/Calendar while
+        // the request uri is like /dav/user@example.org/Calendar.
+        let resPathComponents = resourcePath.split("/");
+        resPathComponents.splice(0, uriPathComponentLength - 1);
+        let locationPath = decodeURIComponent(resPathComponents.join("/"));
+        let isInboxItem = this.isInbox(aUri.spec);
+
+        let hrefPath = this.ensurePath(href);
+        if (this.mHrefIndex[hrefPath] &&
+            !this.mItemInfoCache[item.id]) {
+            // If we get here it means a meeting has kept the same filename
+            // but changed its uid, which can happen server side.
+            // Delete the meeting before re-adding it
+            this.deleteTargetCalendarItem(hrefPath);
+        }
+
+        if (this.mItemInfoCache[item.id]) {
+            this.mItemInfoCache[item.id].isNew = false;
+        } else {
+            this.mItemInfoCache[item.id] = { isNew: true };
+        }
+        this.mItemInfoCache[item.id].locationPath = locationPath;
+        this.mItemInfoCache[item.id].isInboxItem = isInboxItem;
+
+        this.mHrefIndex[hrefPath] = item.id;
+        this.mItemInfoCache[item.id].etag = etag;
+        if (this.mItemInfoCache[item.id].isNew) {
+            this.mTargetCalendar.adoptItem(item, aListener);
+        } else {
+            this.mTargetCalendar.modifyItem(item, null, aListener);
+        }
+
+        if (this.isCached) {
+            this.setMetaData(item.id, resourcePath, etag, isInboxItem);
+        }
+    },
+
+    /**
+     * Deletes an item from the target calendar
+     *
+     * @param path Path of the item to delete
+     */
+    deleteTargetCalendarItem: function caldav_deleteTargetCalendarItem(path) {
+        let foundItem;
+        let isDeleted = false;
+        let getItemListener = {
+            onGetResult: function deleteLocalItem_getItem_onResult(aCalendar,
+                                                     aStatus,
+                                                     aItemType,
+                                                     aDetail,
+                                                     aCount,
+                                                     aItems) {
+
+                foundItem = aItems[0];
+            },
+            onOperationComplete: function deleteLocalItem_getItem_onOperationComplete() {}
+        };
+
+        this.mTargetCalendar.getItem(this.mHrefIndex[path],
+                                     getItemListener);
+        // Since the target calendar's operations are synchronous, we can
+        // safely set variables from this function.
+        if (foundItem) {
+            let wasInboxItem = this.mItemInfoCache[foundItem.id].isInboxItem;
+            if ((wasInboxItem && this.isInbox(path)) ||
+                (wasInboxItem === false && !this.isInbox(path))) {
+
+                cal.LOG("CalDAV: deleting item: " + path + ", uid: " + foundItem.id);
+                delete this.mHrefIndex[path];
+                delete this.mItemInfoCache[foundItem.id];
+                this.mTargetCalendar.deleteItem(foundItem,
+                                                getItemListener);
+                isDeleted = true;
+            }
+        }
+        return isDeleted;
+    },
+
+    /**
+     * Perform tasks required after updating items in the calendar such as
+     * notifying the observers and listeners
+     *
+     * @param aChangeLogListener    Change log listener
+     * @param calendarURI           URI of the calendar whose items just got
+     *                              changed
+     */
+    finalizeUpdatedItems: function calDav_finalizeUpdatedItems(aChangeLogListener, calendarURI) {
+        if (this.isCached) {
+            if (aChangeLogListener)
+                aChangeLogListener.onResult({ status: Components.results.NS_OK },
+                                            Components.results.NS_OK);
+        } else {
+            this.mObservers.notify("onLoad", [this]);
+        }
+
+        this.mFirstRefreshDone = true;
+        while (this.mQueuedQueries.length) {
+            let query = this.mQueuedQueries.pop();
+            this.mTargetCalendar.getItems
+                .apply(this.mTargetCalendar, query);
+        }
+        if (this.hasScheduling &&
+            !this.isInBox(calendarURI.spec)) {
+            this.pollInBox();
+        }
+    },
+
+    /**
+     * Notifies the caller that a get request has failed.
+     *
+     * @param errorMsg           Error message
+     * @param aListener          (optional) Listener of the request
+     * @param aChangeLogListener (optional)Listener for cached calendars
+     */
+    notifyGetFailed: function notifyGetFailed(errorMsg, aListener, aChangeLogListener) {
+         cal.WARN(errorMsg);
+         if (this.isCached && aChangeLogListener) {
+             aChangeLogListener.onResult({ status: Components.results.NS_ERROR_FAILURE },
+                                         Components.results.NS_ERROR_FAILURE);
+         }
+
+         // Notify operation listener
+         this.notifyOperationComplete(aListener,
+                                      Components.results.NS_ERROR_FAILURE,
+                                      Components.interfaces.calIOperationListener.GET,
+                                      null,
+                                      errorMsg);
+         // If an error occurrs here, we also need to unqueue the
+         // requests previously queued.
+         while (this.mQueuedQueries.length) {
+             let [,,,,listener] = this.mQueuedQueries.pop();
+             try {
+                 listener.onOperationComplete(this.superCalendar,
+                                              Components.results.NS_ERROR_FAILURE,
+                                              Components.interfaces.calIOperationListener.GET,
+                                              null,
+                                              errorMsg);
+             } catch (e) {
+                 cal.ERROR(e);
+             }
+         }
+     },
+
+    /**
      * Retrieves a specific item from the CalDAV store.
      * Use when an outdated copy of the item is in hand.
      *
      * @param aItem       item to fetch
      * @param aListener   listener for method completion
      */
     getUpdatedItem: function caldav_getUpdatedItem(aItem, aListener, aChangeLogListener) {
 
@@ -771,40 +981,26 @@ calDavCalendar.prototype = {
             this.notifyOperationComplete(aListener,
                                          Components.results.NS_ERROR_FAILURE,
                                          Components.interfaces.calIOperationListener.GET,
                                          null,
                                          "passed in null item");
             return;
         }
 
-        var locationPath = this.getItemLocationPath(aItem);
-        var itemUri = this.makeUri(locationPath);
-
-        var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
-        var D = new Namespace("D", "DAV:");
+        let locationPath = this.getItemLocationPath(aItem);
+        let itemUri = this.makeUri(locationPath);
 
-        var multigetQueryXml =
-          <calendar-multiget xmlns:D={D} xmlns={C}>
-            <D:prop>
-              <D:getetag/>
-              <calendar-data/>
-            </D:prop>
-            <D:href>{itemUri.path}</D:href>
-          </calendar-multiget>;
-
-        if (this.verboseLogging()) {
-            cal.LOG("CalDAV: send(" + this.makeUri().spec + ": " +
-                    multigetQueryXml.toXMLString());
-        }
-
-        this.getCalendarData(this.calendarUri,
-                             xmlHeader + multigetQueryXml.toXMLString(),
-                             aListener,
-                             aChangeLogListener);
+        let multiget = new multigetSyncHandler([itemUri.path],
+                                               this,
+                                               this.makeUri(),
+                                               null,
+                                               aListener,
+                                               aChangeLogListener)
+        multiget.doMultiGet();
     },
 
     // void getItem( in string id, in calIOperationListener aListener );
     getItem: function caldav_getItem(aId, aListener) {
         this.mTargetCalendar.getItem(aId, aListener);
     },
 
     // void getItems( in unsigned long aItemFilter, in unsigned long aCount,
@@ -839,17 +1035,22 @@ calDavCalendar.prototype = {
             // we can't risk several calendars doing this simultaneously so
             // we'll force the renegotiation in a sync query, using HEAD to keep
             // it quick
             let headchannel = cal.prepHttpChannel(this.makeUri(), null, null, this);
             headchannel.requestMethod = "HEAD";
             headchannel.open();
         }
 
-        if (!this.mCtag || !this.mFirstRefreshDone) {
+        // Call getUpdatedItems right away if its the first refresh
+        // *OR* if webdav Sync is enabled (It is redundant to send a request
+        // to get the collection tag (getctag) on a calendar if it supports
+        // webdav sync, the sync request will only return data if something
+        // changed).
+        if (!this.mCtag || !this.mFirstRefreshDone || this.mHasWebdavSyncSupport ) {
             this.getUpdatedItems(this.calendarUri, aChangeLogListener);
             return;
         }
         var thisCalendar = this;
 
         var D = new Namespace("D", "DAV:");
         var CS = new Namespace("CS", "http://calendarserver.org/ns/");
         var queryXml = <D:propfind xmlns:D={D} xmlns:CS={CS}>
@@ -996,16 +1197,22 @@ calDavCalendar.prototype = {
      */
     getUpdatedItems: function caldav_getUpdatedItems(aUri, aChangeLogListener) {
         if (this.mDisabled) {
             // check if maybe our calendar has become available
             this.checkDavResourceType(aChangeLogListener);
             return;
         }
 
+        if (this.mHasWebdavSyncSupport) {
+            webDavSync = new webDavSyncHandler(this,aUri,aChangeLogListener);
+            webDavSync.doWebDAVSync();
+            return;
+        }
+
         let C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
         let D = new Namespace("D", "DAV:");
         default xml namespace = C;
 
         let queryXml = <D:propfind xmlns:D="DAV:">
                         <D:prop>
                             <D:getcontenttype/>
                             <D:resourcetype/>
@@ -1027,242 +1234,16 @@ calDavCalendar.prototype = {
         httpchannel.setRequestHeader("Depth", "1", false);
 
         // Submit the request
         let streamListener = new etagsHandler(this, aUri, aChangeLogListener);
         httpchannel.asyncOpen(streamListener, httpchannel);
     },
 
     /**
-     * Get calendar data
-     *
-     * @param aUri                  The uri to request the items from.
-     *                                NOTE: This must be the uri without any uri
-     *                                     params. They will be appended in this
-     *                                     function.
-     * @param aQuery                The query data, i.e the xml body.
-     * @param aListener             The listener to notify when the operation
-     *                                succeeded.
-     * @param aChangeLogListener    (optional) The listener to notify for cached
-     *                                         calendars.
-     */
-    getCalendarData: function caldav_getCalendarData(aUri, aQuery, aListener, aChangeLogListener) {
-        this.ensureTargetCalendar();
-
-        var thisCalendar = this;
-        var caldataListener = {};
-        var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
-        var D = new Namespace("D", "DAV:");
-
-        caldataListener.onStreamComplete =
-            function getCalendarData_gCD_onStreamComplete(aLoader, aContext, aStatus,
-                                                          aResultLength, aResult) {
-            function notifyFailed(errorMsg) {
-                if (thisCalendar.isCached && aChangeLogListener) {
-                    aChangeLogListener.onResult({ status: Components.results.NS_ERROR_FAILURE },
-                                                Components.results.NS_ERROR_FAILURE);
-                }
-
-                // Notify operation listener
-                thisCalendar.notifyOperationComplete(aListener,
-                                                     Components.results.NS_ERROR_FAILURE,
-                                                     Components.interfaces.calIOperationListener.GET,
-                                                     null,
-                                                     errorMsg);
-                // If an error occurrs here, we also need to unqueue the
-                // requests previously queued.
-                while (thisCalendar.mQueuedQueries.length) {
-                    let [,,,,listener] = thisCalendar.mQueuedQueries.pop();
-                    try {
-                        listener.onOperationComplete(thisCalendar.superCalendar,
-                                                     Components.results.NS_ERROR_FAILURE,
-                                                     Components.interfaces.calIOperationListener.GET,
-                                                     null,
-                                                     errorMsg);
-                    } catch(e) {
-                        cal.ERROR(e);
-                    }
-                }
-            }
-
-
-            let request = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
-            let responseStatus;
-            try {
-                cal.LOG("CalDAV: Status " + request.responseStatus +
-                        " fetching calendar-data for calendar " + thisCalendar.name);
-                responseStatus = request.responseStatus;
-            } catch (ex) {
-                cal.LOG("CalDAV: Error without status fetching calendar-data for calendar " +
-                        thisCalendar.name);
-                responseStatus = "none";
-            }
-            if (responseStatus != 207) {
-                let errorMsg = "CalDAV: Error: got status " + responseStatus +
-                               " fetching calendar data for " + thisCalendar.name + aListener;
-                cal.LOG(errorMsg);
-                notifyFailed(errorMsg);
-                return;
-            }
-            let str = cal.convertByteArray(aResult, aResultLength);
-            if (!str) {
-                let errorMsg = "CalDAV: Failed to parse getCalendarData REPORT for" +
-                               " calendar " + thisCalendar.name;
-                cal.LOG(errorMsg);
-                notifyFailed(errorMsg);
-                return;
-            } else if (thisCalendar.verboseLogging()) {
-                cal.LOG("CalDAV: recv: " + str);
-            }
-
-            // We need this later on, do so once to save some cycles.
-            let uriPathComponentLength = aUri.path.split("/").length;
-
-            if (thisCalendar.isCached) {
-                thisCalendar.superCalendar.startBatch();
-            }
-            try {
-                let multistatus = cal.safeNewXML(str);
-                for each (let response in multistatus.*::response) {
-
-                    var hasNon200 = false;
-                    var non200Statuses = [];
-                    for each (let itemStatus in response..D::["status"]) {
-                      var status = itemStatus.toString().split(" ")[1];
-                      if (status != 200) {
-                          hasNon200 = true;
-                          if (non200Statuses.indexOf(status) < 0) {
-                              non200Statuses.push(status);
-                          }
-                      }
-                    }
-
-                    if (hasNon200) {
-                        cal.LOG("CalDAV: got element status " + non200Statuses.join(", ") +
-                                " while fetching calendar data for " + thisCalendar.name);
-                        continue;
-                    }
-
-                    var etag = response..D::["getetag"].toString();
-                    var href = response..D::["href"].toString();
-                    var resourcePath = thisCalendar.ensurePath(href);
-                    var calData = response..C::["calendar-data"];
-
-                    var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
-                                           .createInstance(Components.interfaces.calIIcsParser);
-                    try {
-                        parser.parseString(calData);
-                    } catch (e) {
-                        // Warn and continue.
-                        // TODO As soon as we have activity manager integration,
-                        // this should be replace with logic to notify that a
-                        // certain event failed.
-                        cal.WARN("Failed to parse item: " + response.toXMLString());
-                        continue;
-                    }
-                    // with CalDAV there really should only be one item here
-                    var items = parser.getItems({});
-                    var propertiesList = parser.getProperties({});
-                    var method;
-                    for each (var prop in propertiesList) {
-                        if (prop.propertyName == "METHOD") {
-                            method = prop.value;
-                            break;
-                        }
-                    }
-                    var isReply = (method == "REPLY");
-                    var item = items[0];
-                    if (!item) {
-                        cal.WARN("Failed to parse item: " + response.toXMLString());
-                        continue;
-                    }
-
-                    item.calendar = thisCalendar.superCalendar;
-                    if (isReply && thisCalendar.isInbox(aUri.spec)) {
-                        if (thisCalendar.hasScheduling) {
-                            thisCalendar.processItipReply(item, resourcePath);
-                        }
-                        continue;
-                    }
-
-                    // Strip of the same number of components as the request
-                    // uri's path has. This way we make sure to handle servers
-                    // that pass hrefs like /dav/user/Calendar while
-                    // the request uri is like /dav/user@example.org/Calendar.
-                    let resPathComponents = resourcePath.split("/");
-                    resPathComponents.splice(0, uriPathComponentLength - 1);
-                    let locationPath = decodeURIComponent(resPathComponents.join("/"));
-                    let isInboxItem = thisCalendar.isInbox(aUri.spec);
-
-                    if (thisCalendar.mItemInfoCache[item.id]) {
-                        thisCalendar.mItemInfoCache[item.id].isNew = false;
-                    } else {
-                        thisCalendar.mItemInfoCache[item.id] = { isNew: true };
-                    }
-                    thisCalendar.mItemInfoCache[item.id].locationPath = locationPath;
-                    thisCalendar.mItemInfoCache[item.id].isInboxItem = isInboxItem;
-
-                    var hrefPath = thisCalendar.ensurePath(href);
-                    thisCalendar.mHrefIndex[hrefPath] = item.id;
-                    thisCalendar.mItemInfoCache[item.id].etag = etag;
-
-                    if (thisCalendar.mItemInfoCache[item.id].isNew) {
-                        thisCalendar.mTargetCalendar.adoptItem(item, aListener);
-                    } else {
-                        thisCalendar.mTargetCalendar.modifyItem(item, null, aListener);
-                    }
-
-                    if (thisCalendar.isCached) {
-                        thisCalendar.setMetaData(item.id, resourcePath, etag, isInboxItem);
-                    }
-
-                    cal.processPendingEvent();
-                }
-                cal.LOG("CalDAV: refresh completed with status " + responseStatus + " at " +
-                        requestUri.spec);
-            } finally {
-                if (thisCalendar.isCached) {
-                    thisCalendar.superCalendar.endBatch();
-                }
-            }
-
-            if (thisCalendar.isCached) {
-                if (aChangeLogListener)
-                    aChangeLogListener.onResult({ status: Components.results.NS_OK },
-                                                Components.results.NS_OK);
-            } else {
-                thisCalendar.mObservers.notify("onLoad", [thisCalendar]);
-            }
-            thisCalendar.mFirstRefreshDone = true;
-            while (thisCalendar.mQueuedQueries.length) {
-                let query = thisCalendar.mQueuedQueries.pop();
-                thisCalendar.mTargetCalendar.getItems
-                            .apply(thisCalendar.mTargetCalendar, query);
-            }
-            if (!thisCalendar.isInbox(aUri.spec)) {
-                thisCalendar.pollInbox();
-            }
-        };
-
-        let requestUri = this.makeUri(null, aUri);
-
-        if (this.verboseLogging()) {
-            cal.LOG("CalDAV: send(" + requestUri.spec + "): " + aQuery);
-        }
-
-        let httpchannel = cal.prepHttpChannel(requestUri,
-                                              aQuery,
-                                              "text/xml; charset=utf-8",
-                                              this);
-        httpchannel.requestMethod = "REPORT";
-        httpchannel.setRequestHeader("Depth", "1", false);
-        cal.sendHttpRequest(cal.createStreamLoader(), httpchannel, caldataListener);
-    },
-
-    /**
      * @see nsIInterfaceRequestor
      * @see calProviderUtils.jsm
      */
     getInterface: cal.InterfaceRequestor_getInterface,
 
     //
     // Helper functions
     //
@@ -1288,16 +1269,17 @@ calDavCalendar.prototype = {
 
         var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
         var D = new Namespace("D", "DAV:");
         var CS = new Namespace("CS", "http://calendarserver.org/ns/");
         var queryXml = <D:propfind xmlns:D="DAV:" xmlns:CS={CS} xmlns:C={C}>
                         <D:prop>
                             <D:resourcetype/>
                             <D:owner/>
+                            <D:supported-report-set/>
                             <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(),
@@ -1359,45 +1341,48 @@ calDavCalendar.prototype = {
             try {
                 var multistatus = cal.safeNewXML(str);
             } catch (ex) {
                 thisCalendar.completeCheckServerInfo(aChangeLogListener,
                                                      Components.interfaces.calIErrors.DAV_NOT_DAV);
                 return;
             }
 
-            // check for server-side ctag support
+            // check for webdav-sync capability
+            // http://tools.ietf.org/html/draft-daboo-webdav-sync
+            if (multistatus..D::["supported-report-set"]..D::["sync-collection"].length() > 0) {
+                LOG("CalDAV: Collection has webdav sync support");
+                thisCalendar.mHasWebdavSyncSupport = true;
+            }
+
+            // check for server-side ctag support only if webdav sync is not available
             var ctag = multistatus..CS::["getctag"].toString();
-            if (ctag.length) {
+            if (!thisCalendar.mHasWebdavSyncSupport && ctag.length) {
                 // We compare the stored ctag with the one we just got, if
                 // they don't match, we update the items in safeRefresh.
                 if (ctag == thisCalendar.mCtag) {
                     thisCalendar.mFirstRefreshDone = true;
                 }
 
                 thisCalendar.mCtag = ctag;
                 thisCalendar.mTargetCalendar.setMetaData("ctag", ctag);
                 if (thisCalendar.verboseLogging()) {
                     cal.LOG("CalDAV: initial ctag " + ctag + " for calendar " +
                             thisCalendar.name);
                 }
             }
 
+            supportedComponentsXml = multistatus..C::["supported-calendar-component-set"];
             // use supported-calendar-component-set if the server supports it; some do not
-            let haveSCCSReport = true;
-            try {
-                supportedComponentsXml = multistatus..C::["supported-calendar-component-set"];
-            } catch (ex) {
-                haveSCCSReport = false;
-            }
-            if (haveSCCSReport && supportedComponentsXml.hasChildren) {
+            if (supportedComponentsXml.C::comp.length() > 0) {
                 thisCalendar.mSupportedItemTypes.length = 0;
-                for each (let sc in supportedComponentsXml.*) {
+                for each (let sc in supportedComponentsXml.C::comp) {
                     let comp = sc.@name.toString();
                     if (thisCalendar.mGenerallySupportedItemTypes.indexOf(comp) >= 0) {
+                        cal.LOG("Adding supported item: " + comp + " for calendar: " + thisCalendar.name)
                         thisCalendar.mSupportedItemTypes.push(comp);
                     }
                 }
             }
 
             // check if owner is specified; might save some work
             thisCalendar.mPrincipalUrl = multistatus..D::["owner"]..D::href.toString() || null;
 
--- a/calendar/providers/caldav/calDavRequestHandlers.js
+++ b/calendar/providers/caldav/calDavRequestHandlers.js
@@ -14,16 +14,18 @@
  * The Original Code is Sun Microsystems code.
  *
  * The Initial Developer of the Original Code is
  *   Philipp Kewisch <mozilla@kewis.ch>
  * Portions created by the Initial Developer are Copyright (C) 2009
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
+ *   Wolfgang Sourdeau <wsourdeau@inverse.ca>
+ *   Simon Vaillancourt <simon.at.orcl@gmail.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -145,28 +147,28 @@ etagsHandler.prototype = {
                 let foundItem;
                 let getItemListener = {
                     onGetResult: function etags_getItem_onResult(aCalendar,
                                                                  aStatus,
                                                                  aItemType,
                                                                  aDetail,
                                                                  aCount,
                                                                  aItems) {
-                    
                         foundItem = aItems[0];
                     },
                     onOperationComplete: function etags_getItem_onOperationComplete() {}
                 };
 
                 this.calendar.mTargetCalendar.getItem(this.calendar.mHrefIndex[path],
                                                       getItemListener);
                 if (foundItem) {
                     let wasInboxItem = this.calendar.mItemInfoCache[foundItem.id].isInboxItem;
                     if ((wasInboxItem && this.calendar.isInbox(this.baseUri.spec)) ||
                         (wasInboxItem === false && !this.calendar.isInbox(this.baseUri.spec))) {
+                        cal.LOG("Deleting local href: " + path)
                         delete this.calendar.mHrefIndex[path];
                         this.calendar.mTargetCalendar.deleteItem(foundItem, null);
                         needsRefresh = true;
                     }
                 }
             }
         } finally {
             if (this.calendar.isCached) {
@@ -187,52 +189,39 @@ etagsHandler.prototype = {
             }
 
             // but do poll the inbox
             if (this.calendar.mShouldPollInbox &&
                 !this.calendar.isInbox(this.baseUri.spec)) {
                 this.calendar.pollInbox();
             }
         } else {
-            let C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
-            let D = new Namespace("D", "DAV:");
-            let multigetQueryXml =
-              <calendar-multiget xmlns:D={D} xmlns={C}>
-                <D:prop>
-                  <D:getetag/>
-                  <calendar-data/>
-                </D:prop>
-              </calendar-multiget>;
-
-            while (this.itemsNeedFetching.length) {
-                let locpath = this.itemsNeedFetching.pop();
-                multigetQueryXml.D::prop += <D:href xmlns:D={D}>{locpath}</D:href>;
-            }
-
-            let multigetQueryString = xmlHeader + multigetQueryXml.toXMLString();
-            this.calendar.getCalendarData(this.baseUri,
-                                          multigetQueryString,
-                                          null,
-                                          this.changelogListener);
+            let multiget = new multigetSyncHandler(this.itemsNeedFetching,
+                                       this.calendar,
+                                       this.baseUri,
+                                       null,
+                                       null,
+                                       this.changeLogListener)
+            multiget.doMultiGet();
         }
     },
 
     onDataAvailable: function eSL_onDataAvailable(request, context, inputStream, offset, count) {
         if (this._reader) {
             // No reader means request error
             this._reader.onDataAvailable(request, context, inputStream, offset, count);
         }
     },
 
 
     /**
      * @see nsISAXErrorHandler
      */
     fatalError: function eH_fatalError() {
-        LOG("CalDAV: Fatal Error parsing etags for " + this.calendar.name);
+        cal.WARN("CalDAV: Fatal Error parsing etags for " + this.calendar.name);
     },
 
 
     /**
      * @see nsISAXContentHandler
      */
     characters: function eH_characters(aValue) {
         if (this.calendar.verboseLogging()) {
@@ -296,17 +285,17 @@ etagsHandler.prototype = {
                         // Only handle calendar items
 
                         if (this.skipIndex < 0) {
                             href = this.calendar.ensurePath(r.href);
                             this.skipIndex = r.href.indexOf(href);
                         } else {
                             href = r.href.substr(this.skipIndex);
                         }
-                        
+                        href = decodeURIComponent(href);
                         if (href && href.length) {
                             this.itemsReported[href] = r.getetag;
 
                             let itemUid = this.calendar.mHrefIndex[href];
                             if (!itemUid ||
                                 r.getetag != this.calendar.mItemInfoCache[itemUid].etag) {
                                 this.itemsNeedFetching.push(href);
                             }
@@ -320,14 +309,640 @@ etagsHandler.prototype = {
             case "getcontenttype":
                 this.tag = null;
                 break;
         }
         if (this.calendar.verboseLogging()) {
             this.logXML += "</" + aQName + ">";
         }
     },
-    
+
     startPrefixMapping: function eH_startPrefixMapping(aPrefix, aUri) { },
     endPrefixMapping: function eH_endPrefixMapping(aPrefix) { },
     ignorableWhitespace: function eH_ignorableWhitespace(aWhiteSpace) { },
     processingInstruction: function eH_processingInstruction(aTarget, aData) { }
 };
+
+/**
+ * This is a handler for the webdav sync request in calDavCalendar.js' getUpdatedItem.
+ * It uses the SAX parser to incrementally parse the items and compose the
+ * resulting multiget.
+ *
+ * @param aCalendar             The (unwrapped) calendar this request belongs to
+ * @param aBaseUri              The URI requested (i.e inbox or collection)
+ * @param aChangeLogListener    (optional) for cached calendars, the listener to
+ *                                notify.
+ */
+function webDavSyncHandler(aCalendar, aBaseUri, aChangeLogListener) {
+    this.calendar = aCalendar;
+    this.baseUri = aBaseUri;
+    this.changelogListener = aChangeLogListener;
+    this._reader = Components.classes["@mozilla.org/saxparser/xmlreader;1"]
+                             .createInstance(Components.interfaces.nsISAXXMLReader);
+    this._reader.contentHandler = this;
+    this._reader.errorHandler = this;
+    this._reader.parseAsync(null);
+
+    this.itemsReported = {};
+    this.itemsNeedFetching = [];
+}
+
+webDavSyncHandler.prototype = {
+    currentResponse: null,
+    tag: null,
+    calendar: null,
+    baseUri: null,
+    newSyncToken: null,
+    changelogListener: null,
+    logXML: "",
+    isInPropStat : false,
+    changeCount : 0,
+    unhandledErrors : 0,
+    itemsReported: null,
+    itemsNeedFetching: null,
+
+    QueryInterface: function QueryInterface(aIID) {
+        return doQueryInterface(this,
+                                webDavSyncHandler.prototype,
+                                aIID,
+                                [Components.interfaces.nsISAXContentHandler,
+                                 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.aChangeLogListener);
+            return;
+        }
+
+        let C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
+        let D = new Namespace("D", "DAV:");
+        default xml namespace = D;
+        let queryXml;
+        if (this.calendar.mHasWebdavSyncCalendarDataSupport) {
+          queryXml = <sync-collection xmlns:C={C}>
+            <sync-token/>
+            <prop>
+              <getcontenttype/>
+              <getetag/>
+              <C:calendar-data/>
+            </prop>
+          </sync-collection>;
+        } else {
+          queryXml = <sync-collection>
+            <sync-token/>
+            <prop>
+              <getcontenttype/>
+              <getetag/>
+            </prop>
+          </sync-collection>;
+        }
+
+        if (this.calendar.mWebdavSyncToken && this.calendar.mWebdavSyncToken.length > 0) {
+            queryXml.D::["sync-token"] = this.calendar.mWebdavSyncToken;
+        }
+
+        let queryString = xmlHeader + queryXml.toXMLString();
+        let requestUri = this.calendar.makeUri(null, this.baseUri);
+
+        if (this.calendar.verboseLogging()) {
+            cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryString);
+        }
+        cal.LOG("CalDAV: webdav-sync Token: " + this.calendar.mWebdavSyncToken);
+        let httpchannel = cal.prepHttpChannel(requestUri,
+                                              queryString,
+                                              "text/xml; charset=utf-8",
+                                              this.calendar);
+        httpchannel.setRequestHeader("Depth", "1", false);
+        httpchannel.requestMethod = "REPORT";
+        // Submit the request
+        httpchannel.asyncOpen(this, httpchannel);
+    },
+
+    /**
+     * @see nsIStreamListener
+     */
+    onStartRequest: function wSL_onStartRequest(request, context) {
+        let httpchannel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
+
+        let responseStatus;
+        try {
+            responseStatus = httpchannel.responseStatus
+        } catch (ex) {
+            cal.WARN("CalDAV: No response status doing webdav sync for calendar " + this.calendar.name);
+        }
+
+        if (responseStatus == 207) {
+            // We only need to parse 207's, anything else is probably a
+            // server error (i.e 50x).
+            httpchannel.contentType = "application/xml";
+            this._reader.onStartRequest(request, context);
+        }
+        // Invalidate sync token with 4xx errors that could indicate the
+        // sync token has become invalid and do a refresh
+        else if (this.calendar.mWebdavSyncToken != null &&
+                 responseStatus >= 400 &&
+                 responseStatus <= 499) {
+            cal.LOG("CalDAV: Reseting sync token because server returned status code: " + responseStatus);
+            this._reader = null;
+            this.calendar.mWebdavSyncToken=null;
+            this.calendar.mTargetCalendar.deleteMetaData("sync-token");
+            this.calendar.safeRefresh(this.aChangeLogListener);
+        } else {
+            cal.WARN("CalDAV: Error doing webdav sync: " + responseStatus);
+            this.calendar.reportDavError(Components.interfaces.calIErrors.DAV_REPORT_ERROR);
+            if (this.calendar.isCached && this.changelogListener) {
+                this.changelogListener.onResult({ status: Components.results.NS_ERROR_FAILURE },
+                                                Components.results.NS_ERROR_FAILURE);
+            }
+            this._reader = null;
+        }
+    },
+
+    onStopRequest: function wSL_onStopRequest(request, context, statusCode) {
+        if (this.calendar.verboseLogging()) {
+            cal.LOG("CalDAV: recv: " + this.logXML);
+        }
+        if (!this._reader) {
+            // No reader means there was a request error
+            cal.LOG("CalDAV: onStopRequest: no reader");
+            return;
+        }
+        try {
+            this._reader.onStopRequest(request, context, statusCode);
+        } finally {
+            this._reader = null;
+        }
+    },
+
+    onDataAvailable: function wSL_onDataAvailable(request, context, inputStream, offset, count) {
+        if (this._reader) {
+            // No reader means request error
+            this._reader.onDataAvailable(request, context, inputStream, offset, count);
+        }
+    },
+
+    /**
+     * @see nsISAXErrorHandler
+     */
+    fatalError: function wH_fatalError() {
+        cal.WARN("CalDAV: Fatal Error doing webdav sync for " + this.calendar.name);
+    },
+
+    /**
+     * @see nsISAXContentHandler
+     */
+    characters: function wH_characters(aValue) {
+        if (this.calendar.verboseLogging()) {
+            this.logXML += aValue;
+        }
+        this.currentResponse[this.tag] += aValue;
+    },
+
+    startDocument: function wH_startDocument() {
+        this.hrefMap = {};
+        this.currentResponse = {};
+        this.tag = null
+        if (this.calendar.isCached) {
+            this.calendar.superCalendar.startBatch();
+        }
+    },
+
+    endDocument: function wH_endDocument() {
+        if (this.unhandledErrors) {
+            this.calendar.superCalendar.endBatch();
+            this.calendar.reportDavError(Components.interfaces.calIErrors.DAV_REPORT_ERROR);
+            if (this.calendar.isCached && this.changelogListener) {
+                this.changelogListener.onResult({ status: Components.results.NS_ERROR_FAILURE },
+                                                Components.results.NS_ERROR_FAILURE);
+            }
+            return;
+        }
+
+        if (this.calendar.mWebdavSyncToken == null) {
+            // null token means reset or first refresh indicating we did
+            // a full sync; remove local items that were not returned in this full
+            // sync
+            for (let path in this.calendar.mHrefIndex) {
+                if (!this.itemsReported[path] &&
+                    path.substr(0, this.baseUri.path.length) == this.baseUri.path) {
+                    this.calendar.deleteTargetCalendarItem(path);
+                }
+            }
+        }
+        if (this.calendar.isCached) {
+            this.calendar.superCalendar.endBatch();
+        }
+
+        if (!this.itemsNeedFetching.length) {
+            if (this.newSyncToken) {
+                this.calendar.mWebdavSyncToken = this.newSyncToken;
+                this.calendar.mTargetCalendar.setMetaData("sync-token",
+                                                          this.newSyncToken);
+                cal.LOG("CalDAV: New webdav-sync Token: " + this.calendar.mWebdavSyncToken);
+            }
+            this.calendar.finalizeUpdatedItems(this.changelogListener,
+                                               this.baseUri);
+        } else {
+            let multiget = new multigetSyncHandler(this.itemsNeedFetching,
+                                                   this.calendar,
+                                                   this.baseUri,
+                                                   this.newSyncToken,
+                                                   null,
+                                                   this.changeLogListener)
+            multiget.doMultiGet();
+        }
+
+    },
+
+    startElement: function wH_startElement(aUri, aLocalName, aQName, aAttributes) {
+        switch (aLocalName) {
+            case "response": // WebDAV Sync draft 3
+                this.currentResponse = {};
+                this.tag = null
+                this.isInPropStat=false;
+                break;
+            case "propstat":
+                this.isInPropStat=true;
+                break;
+            case "status":
+                if (this.isInPropStat) {
+                    this.tag = "propstat_" + aLocalName;
+                }
+                else {
+                    this.tag = aLocalName;
+                }
+                this.currentResponse[this.tag] = "";
+                break;
+            case "calendar-data":
+            case "href":
+            case "getetag":
+            case "getcontenttype":
+            case "sync-token":
+                this.tag = aLocalName.replace(/-/g,'');
+                this.currentResponse[this.tag ] = "";
+                break;
+        }
+        if (this.calendar.verboseLogging()) {
+            this.logXML += "<" + aQName + ">";
+        }
+
+    },
+
+    endElement: function wH_endElement(aUri, aLocalName, aQName) {
+        switch (aLocalName) {
+            case "response": // WebDAV Sync draft 3
+            case "sync-response": // WebDAV Sync draft 0,1,2
+                cal.processPendingEvent();
+                let r = this.currentResponse;
+                if (r.href &&
+                    r.href.length) {
+                    r.href = decodeURIComponent(r.href);
+                }
+                // Deleted item
+                if (r.href && r.href.length &&
+                    r.status &&
+                    r.status.length &&
+                    r.status.indexOf(" 404") > 0) {
+                    if (this.calendar.mHrefIndex[r.href]) {
+                        this.changeCount++;
+                        this.calendar.deleteTargetCalendarItem(r.href);
+                    }
+                    else {
+                        cal.LOG("CalDAV: skipping unfound deleted item : " + r.href);
+                    }
+                // Only handle Created or Updated calendar items
+                } else if (r.getcontenttype &&
+                           r.getcontenttype.substr(0,13) == "text/calendar" &&
+                           r.getetag && r.getetag.length &&
+                           r.href && r.href.length &&
+                           (!r.status ||                 // Draft 3 does not require
+                            r.status.length == 0 ||      // a status for created or updated items but
+                            r.status.indexOf(" 204") ||  // draft 0, 1 and 2 needed it so treat no status
+                            r.status.indexOf(" 201"))) { // and status 201 and 204 the same
+                    this.itemsReported[r.href] = r.getetag;
+                    let itemId = this.calendar.mHrefIndex[r.href];
+                    let oldEtag = (itemId && this.calendar.mItemInfoCache[itemId].etag);
+
+                    if (!oldEtag || oldEtag != r.getetag) {
+                        // Etag mismatch, getting new/updated item.
+                        if (r.calendardata && r.calendardata.length) {
+                            this.changeCount++;
+                            this.calendar.addTargetCalendarItem(r.href,
+                                                                r.calendardata,
+                                                                this.baseUri,
+                                                                r.getetag,
+                                                                null);
+                        } else {
+                            if (this.calendar.mHasWebdavSyncCalendarDataSupport) {
+                                this.calendar.mHasWebdavSyncCalendarDataSupport = false;
+                            }
+                            this.itemsNeedFetching.push(r.href);
+                        }
+                    }
+                // If the response element is still not handled, log an error
+                // only if the content-type is text/calendar or the
+                // response status is different than 404 not found.
+                // We don't care about response elements
+                // on non-calendar resources or whose status is not indicating
+                // a deleted resource.
+                } else if ((r.getcontenttype &&
+                            r.getcontenttype.substr(0,13) == "text/calendar") ||
+                           (r.status &&
+                            r.status.indexOf(" 404") == -1)) {
+                    cal.WARN("CalDAV: Unexpected response, status: " + r.status + ", href: " + r.href);
+                    this.unhandledErrors++;
+                } else {
+                    cal.LOG("CalDAV: Unhandled response element, status: " + r.status + ", href: " + r.href + " contenttype:" + r.getcontenttype);
+                }
+                break;
+            case "sync-token":
+                this.newSyncToken = this.currentResponse[this.tag];
+                break;
+            case "propstat":
+                this.isInPropStat=false;
+                break;
+        }
+        this.tag = null;
+        if (this.calendar.verboseLogging()) {
+            this.logXML += "</" + aQName + ">";
+        }
+    },
+
+    startPrefixMapping: function wH_startPrefixMapping(aPrefix, aUri) { },
+    endPrefixMapping: function wH_endPrefixMapping(aPrefix) { },
+    ignorableWhitespace: function wH_ignorableWhitespace(aWhiteSpace) { },
+    processingInstruction: function wH_processingInstruction(aTarget, aData) { }
+};
+
+/**
+ * This is a handler for the multiget request.
+ * It uses the SAX parser to incrementally parse the items and compose the
+ * resulting multiget.
+ *
+ * @param aItemsNeedFetching    The array of items to fetch
+ * @param aCalendar             The (unwrapped) calendar this request belongs to
+ * @param aBaseUri              The URI requested (i.e inbox or collection)
+ * @param aNewSyncToken         (optional) new Sync token to set if operation successful
+ * @param aListener             (optional) The listener to notify
+ * @param aChangeLogListener    (optional) for cached calendars, the listener to
+ *                                notify.
+ */
+function multigetSyncHandler(aItemsNeedFetching, aCalendar, aBaseUri, aNewSyncToken, aListener, aChangeLogListener) {
+    this.calendar = aCalendar;
+    this.baseUri = aBaseUri;
+    this.listener = aListener;
+    this.newSyncToken = aNewSyncToken;
+    this.changelogListener = aChangeLogListener;
+    this._reader = Components.classes["@mozilla.org/saxparser/xmlreader;1"]
+                             .createInstance(Components.interfaces.nsISAXXMLReader);
+    this._reader.contentHandler = this;
+    this._reader.errorHandler = this;
+    this._reader.parseAsync(null);
+    this.itemsNeedFetching = aItemsNeedFetching;
+}
+multigetSyncHandler.prototype = {
+    currentResponse: null,
+    tag: null,
+    calendar: null,
+    baseUri: null,
+    newSyncToken: null,
+    listener: null,
+    changelogListener: null,
+    logXML: "",
+    unhandledErrors : 0,
+    itemsNeedFetching: null,
+
+    QueryInterface: function QueryInterface(aIID) {
+        return doQueryInterface(this,
+                                multigetSyncHandler.prototype,
+                                aIID,
+                                [Components.interfaces.nsISAXContentHandler,
+                                 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.aChangeLogListener);
+            return;
+        }
+        let C = new Namespace("C", "urn:ietf:params:xml:ns:caldav");
+        let D = new Namespace("D", "DAV:");
+        let queryXml =
+          <calendar-multiget xmlns:D={D} xmlns={C}>
+            <D:prop>
+              <D:getetag/>
+              <calendar-data/>
+            </D:prop>
+          </calendar-multiget>;
+
+        while (this.itemsNeedFetching.length) {
+            let locpath = this.itemsNeedFetching.pop();
+            queryXml.D::prop += <D:href xmlns:D={D}>{locpath}</D:href>;
+        }
+
+        let multigetQueryString = xmlHeader + queryXml.toXMLString();
+
+        let requestUri = this.calendar.makeUri(null, this.baseUri);
+        if (this.calendar.verboseLogging()) {
+            cal.LOG("CalDAV: send(" + requestUri.spec + "): " + queryXml);
+        }
+        let httpchannel = cal.prepHttpChannel(requestUri,
+                                              multigetQueryString,
+                                              "text/xml; charset=utf-8",
+                                              this.calendar);
+        httpchannel.setRequestHeader("Depth", "1", false);
+        httpchannel.requestMethod = "REPORT";
+        // Submit the request
+        httpchannel.asyncOpen(this, httpchannel);
+    },
+
+    /**
+     * @see nsIStreamListener
+     */
+    onStartRequest: function mg_onStartRequest(request, context) {
+        let httpchannel = request.QueryInterface(Components.interfaces.nsIHttpChannel);
+
+        let responseStatus;
+        try {
+            responseStatus = httpchannel.responseStatus
+        } catch (ex) {
+            cal.WARN("CalDAV: No response status doing multiget for calendar " + this.calendar.name);
+        }
+
+        if (responseStatus == 207) {
+            // We only need to parse 207's, anything else is probably a
+            // server error (i.e 50x).
+            httpchannel.contentType = "application/xml";
+            this._reader.onStartRequest(request, context);
+        } else {
+            let errorMsg = "CalDAV: Error: got status " + responseStatus +
+                               " fetching calendar data for " + thisCalendar.name + ", " + aListener;
+            this.calendar.notifyGetFailed(errorMsg,listener,changelogListener);
+            this._reader = null;
+        }
+    },
+
+    onStopRequest: function mg_onStopRequest(request, context, statusCode) {
+        if (this.calendar.verboseLogging()) {
+            cal.LOG("CalDAV: recv: " + this.logXML);
+        }
+        if (!this._reader) {
+            // No reader means there was a request error
+            cal.LOG("CalDAV: onStopRequest: no reader");
+            return;
+        }
+        try {
+            this._reader.onStopRequest(request, context, statusCode);
+        } finally {
+            this._reader = null;
+        }
+    },
+
+    onDataAvailable: function mg_onDataAvailable(request, context, inputStream, offset, count) {
+        if (this._reader) {
+            // No reader means request error
+            this._reader.onDataAvailable(request, context, inputStream, offset, count);
+        }
+    },
+
+    /**
+     * @see nsISAXErrorHandler
+     */
+    fatalError: function mg_fatalError() {
+        cal.WARN("CalDAV: Fatal Error doing multiget for " + this.calendar.name);
+    },
+
+    /**
+     * @see nsISAXContentHandler
+     */
+    characters: function mg_characters(aValue) {
+        if (this.calendar.verboseLogging()) {
+            this.logXML += aValue;
+        }
+        this.currentResponse[this.tag] += aValue;
+    },
+
+    startDocument: function mg_startDocument() {
+        this.hrefMap = {};
+        this.currentResponse = {};
+        this.tag = null
+        if (this.calendar.isCached) {
+            this.calendar.superCalendar.startBatch();
+        }
+    },
+
+    endDocument: function mg_endDocument() {
+        if (this.unhandledErrors) {
+            this.calendar.superCalendar.endBatch();
+            this.calendar.notifyGetFailed("multiget error", listener, changelogListener);
+            return;
+        }
+        if (this.newSyncToken) {
+            this.calendar.mWebdavSyncToken = this.newSyncToken;
+            this.calendar.mTargetCalendar.setMetaData("sync-token", this.newSyncToken);
+          cal.LOG("CalDAV: New webdav-sync Token: " + this.calendar.mWebdavSyncToken);
+        }
+
+        if (this.calendar.isCached) {
+            this.calendar.superCalendar.endBatch();
+        }
+        this.calendar.finalizeUpdatedItems(this.changelogListener,
+                                           this.baseUri);
+    },
+
+    startElement: function mg_startElement(aUri, aLocalName, aQName, aAttributes) {
+        switch (aLocalName) {
+            case "response":
+                this.currentResponse = {};
+                this.tag = null
+                this.isInPropStat=false;
+                break;
+            case "propstat":
+                this.isInPropStat=true;
+                break;
+            case "status":
+                if (this.isInPropStat) {
+                    this.tag = "propstat_" + aLocalName;
+                }
+                else {
+                    this.tag = aLocalName;
+                }
+                this.currentResponse[this.tag] = "";
+                break;
+            case "calendar-data":
+            case "href":
+            case "getetag":
+                this.tag = aLocalName.replace(/-/g,'');
+                this.currentResponse[this.tag ] = "";
+                break;
+        }
+        if (this.calendar.verboseLogging()) {
+            this.logXML += "<" + aQName + ">";
+        }
+
+    },
+
+    endElement: function mg_endElement(aUri, aLocalName, aQName) {
+        switch (aLocalName) {
+            case "response":
+                cal.processPendingEvent();
+                let r = this.currentResponse;
+                if (r.href &&
+                    r.href.length) {
+                    r.href = decodeURIComponent(r.href);
+                }
+                if (r.href && r.href.length &&
+                    r.status &&
+                    r.status.length &&
+                    r.status.indexOf(" 404") > 0) {
+                    if (this.calendar.mHrefIndex[r.href]) {
+                        this.changeCount++;
+                        this.calendar.deleteTargetCalendarItem(r.href);
+                    } else {
+                        cal.LOG("CalDAV: skipping unfound deleted item : " + r.href);
+                    }
+                // Created or Updated item
+                } else if (r.getetag && r.getetag.length &&
+                           r.href && r.href.length &&
+                           r.calendardata && r.calendardata.length) {
+                    let oldEtag;
+                    let itemId = this.calendar.mHrefIndex[r.href];
+                    if (itemId) {
+                        oldEtag = this.calendar.mItemInfoCache[itemId].etag;
+                    } else {
+                        oldEtag = null;
+                    }
+                    if (!oldEtag || oldEtag != r.getetag) {
+                        this.changeCount++;
+                        this.calendar.addTargetCalendarItem(r.href,
+                                                            r.calendardata,
+                                                            this.baseUri,
+                                                            r.getetag,
+                                                            null);
+                    }
+                } else {
+                    cal.WARN("CalDAV: Unexpected response, status: " +
+                             r.status + ", href: " + r.href + " calendar-data:");
+                    this.unhandledErrors++;
+                }
+                break;
+            case "propstat":
+                this.isInPropStat=false;
+                break;
+        }
+        this.tag = null;
+        if (this.calendar.verboseLogging()) {
+            this.logXML += "</" + aQName + ">";
+        }
+    },
+
+    startPrefixMapping: function mg_startPrefixMapping(aPrefix, aUri) { },
+    endPrefixMapping: function mg_endPrefixMapping(aPrefix) { },
+    ignorableWhitespace: function mg_ignorableWhitespace(aWhiteSpace) { },
+    processingInstruction: function mg_processingInstruction(aTarget, aData) { }
+};