From 11fac523f330d1be6fcd820056c45ef8c13b0715 Mon Sep 17 00:00:00 2001 From: "daniel.boelzle%sun.com" Date: Wed, 14 Nov 2007 09:43:47 +0000 Subject: [PATCH] Bug 373370 Send If-Match / If-None-Match headers on PUT & DELETE; patch=bbrowning, r=dbo git-svn-id: svn://10.0.0.236/trunk@239378 18797224-902f-48f8-a5cc-f745e15eee43 --- .../providers/caldav/calDavCalendar.js | 407 ++++++++++-------- 1 file changed, 217 insertions(+), 190 deletions(-) diff --git a/mozilla/calendar/providers/caldav/calDavCalendar.js b/mozilla/calendar/providers/caldav/calDavCalendar.js index 3cdba8acefc..8f422855b6e 100644 --- a/mozilla/calendar/providers/caldav/calDavCalendar.js +++ b/mozilla/calendar/providers/caldav/calDavCalendar.js @@ -48,8 +48,6 @@ // XXXdmose deal with generation goop -// XXXdmose need to re-query for add & modify to get up-to-date items - // XXXdmose deal with locking // XXXdmose need to make and use better error reporting interface for webdav @@ -277,100 +275,6 @@ calDavCalendar.prototype = { this.mObservers.remove(aObserver); }, - /** - * Fetches etag from server and compares with local cached version - * before we add/adopt/modify/delete item. - * - * @param aMethod requested method (adopt/modify/delete) - * @param aItem item to check - * @param aListener listener from original request - * @param aOldItem aOldItem argument in modifyItem requests - */ - fetchEtag: function caldavFE(aMethod, aItem, aListener, aOldItem) { - if (this.readOnly) { - throw calIErrors.CAL_IS_READONLY; - } - - var serverEtag = null; - var listener = new WebDavListener(); - - var thisCalendar = this; - - var itemUri = this.mCalendarUri.clone(); - - try { - itemUri.spec = this.makeUri(aItem.getProperty("X-MOZ-LOCATIONPATH")); - LOG("using X-MOZ-LOCATIONPATH: " + itemUri.spec); - } catch (ex) { - // XXX how are we REALLY supposed to figure this out? - itemUri.spec = this.makeUri(aItem.id + ".ics"); - } - - var itemResource = new WebDavResource(itemUri); - - listener.onOperationComplete = function OOC(aStatusCode, - aResource, - aOperation, - aClosure) - { - var mismatch = (serverEtag != thisCalendar.mEtagCache[aItem.id]); - - switch (aMethod) { - case CALDAV_ADOPT_ITEM: - if (serverEtag != null) { - // The server thinks it already has an item we want to - // create as new. This either a server error or we're - // trying to copy an item onto itself; either way the - // safe thing to do is abort the operation. - LOG("CalDAV: non-null etag in adoptItem"); - thisCalendar.readOnly = true; - } else { - thisCalendar.performAdoptItem(aItem, aListener); - } - break; - - case CALDAV_MODIFY_ITEM: - if (mismatch) { - LOG("CalDAV: etag mismatch in modifyItem"); - thisCalendar.promptOverwrite(aMethod, aItem, - aListener, aOldItem); - } else { - thisCalendar.performModifyItem(aItem, aOldItem, aListener); - } - break; - - case CALDAV_DELETE_ITEM: - if (mismatch) { - LOG("CalDAV: etag mismatch in deleteItem"); - thisCalendar.promptOverwrite(aMethod, aItem, - aListener, null); - } else { - thisCalendar.performDeleteItem(aItem, aListener); - } - break; - - default: - thisCalendar.mEtagCache[aItem.id] = serverEtag; - break; - } - } - - listener.onOperationDetail = function OOD(aStatusCode, aResource, - aOperation, aDetail, - aClosure) { - LOG("fetchEtag: onOperationDetail aStatusCode=" + aStatusCode); - - var props = aDetail.QueryInterface(Components.interfaces.nsIProperties); - serverEtag = props.get("DAV: getetag", - Components.interfaces.nsISupportsString).toString(); - } - - var webdavSvc = Components.classes['@mozilla.org/webdav/service;1']. - getService(Components.interfaces.nsIWebDAVService); - webdavSvc.getResourceProperties(itemResource, 1, ["DAV: getetag"], - false, listener, this, null); - }, - promptOverwrite: function caldavPO(aMethod, aItem, aListener, aOldItem) { var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. getService(Components.interfaces.nsIPromptService); @@ -400,9 +304,9 @@ calDavCalendar.prototype = { if (choice == 0) { if (aMethod == CALDAV_MODIFY_ITEM) { - this.performModifyItem(aItem, aOldItem, aListener); + this.doModifyItem(aItem, aOldItem, aListener, true); } else { - this.performDeleteItem(aItem, aListener); + this.doDeleteItem(aItem, aListener, true); } } else { this.getUpdatedItem(aItem, aListener); @@ -413,36 +317,81 @@ calDavCalendar.prototype = { mEtagCache: null, /** - * addItem() is required by the IDL, but simply calls adoptItem(). - * Actually adding the item to the CalDAV store takes place in - * performAdoptItem(). + * prepare channel with standard request headers * - * @param aItem item to check - * @param aListener listener for method completion + * @param arUri channel Uri + * @param aUpload prep channel for upload */ - addItem: function caldavAI(aItem, aListener) { - var newItem = aItem.clone(); - return this.adoptItem(newItem, aListener); - }, - - /** - * Sends data regarding the requested new item off for etag checking. - * - * @param aItem item to check - * @param aListener listener for method completion - */ - adoptItem: function caldavAI2(aItem, aListener) { - this.fetchEtag(CALDAV_ADOPT_ITEM, aItem, aListener, null); + + prepChannel: function caldavPC(aUri, aUpload) { + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var channel = ioService.newChannelFromURI(aUri); + + var httpchannel = channel.QueryInterface(Components.interfaces + .nsIHttpChannel); + if (aUpload) { + httpchannel = httpchannel.QueryInterface(Components.interfaces. + nsIUploadChannel); + } + + httpchannel.setRequestHeader("Accept", "text/xml", false); + httpchannel.setRequestHeader("Accept-Charset", "utf-8,*;q=0.1", false); + + return httpchannel; }, /** - * Performs the actual addition of the item to CalDAV store, after etag - * checking. + * prepare upload stream + * + * @param aString data to upload + */ + + prepUploadStream: function caldavPUS(aString) { + var converter = Components.classes + ["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Components.interfaces. + nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + + var stream = converter.convertToInputStream(aString); + + return stream; + }, + + /** + * addItem(); required by calICalendar.idl + * we actually use doAdoptItem() + * + * @param aItem item to add + * @param aListener listener for method completion + */ + + addItem: function caldavAI(aItem, aListener) { + var newItem = aItem.clone(); + return this.doAdoptItem(newItem, aListener, false); + }, + + /** + * adooptItem(); required by calICalendar.idl + * we actually use doAdoptItem() * * @param aItem item to check * @param aListener listener for method completion */ - performAdoptItem: function caldavPAI(aItem, aListener) { + adoptItem: function caldavAtI(aItem, aListener) { + var newItem = aItem.clone(); + return this.doAdoptItem(newItem, aListener, false); + }, + + /** + * Performs the actual addition of the item to CalDAV store + * + * @param aItem item to add + * @param aListener listener for method completion + * @param aIgnoreEtag ignore item etag + */ + doAdoptItem: function caldavaDAI(aItem, aListener, aIgnoreEtag) { if (aItem.id == null && aItem.isMutable) { aItem.id = getUUID(); } @@ -457,39 +406,42 @@ calDavCalendar.prototype = { return; } - // XXX how are we REALLY supposed to figure this out? var locationPath = aItem.id + ".ics"; var itemUri = this.mCalendarUri.clone(); itemUri.spec = this.makeUri(locationPath); LOG("itemUri.spec = " + itemUri.spec); - var eventResource = new WebDavResource(itemUri); - var listener = new WebDavListener(); + var addListener = {}; var thisCalendar = this; - listener.onOperationComplete = - function onPutComplete(aStatusCode, aResource, aOperation, aClosure) { - + addListener.onStreamComplete = + function onPutComplete(aLoader, aContext, aStatus, aResultLength, + aResult) { + var status = aContext.responseStatus; // 201 = HTTP "Created" // - if (aStatusCode == 201) { + if (status == 201) { LOG("Item added successfully"); var retVal = Components.results.NS_OK; - // CalDAV does not require that the etag returned on PUT will - // be identical to the etag returned on later operations - // (CalDAV 5.3.4), so we best re-query. - thisCalendar.fetchEtag(CALDAV_CACHE_ETAG, aItem, null, null); + // Some CalDAV servers will modify items on PUT (add X-props, + // change location, etc) so we'd best re-fetch in order to know + // the current state of the item + // Observers will be notified in getUpdatedItem() + thisCalendar.getUpdatedItem(aItem, aListener); - } else if (aStatusCode == 200) { + } else if (status == 200) { LOG("CalDAV: 200 received from server: server malfunction"); retVal = Components.results.NS_ERROR_FAILURE; + } else if (status == 412) { + LOG("CalDAV: etag exists on adopt item: server malfunction"); + retVal = Components.results.NS_ERROR_FAILURE; } else { - if (aStatusCode > 999) { - aStatusCode = "0x" + aStatusCode.toString(16); + if (status > 999) { + status = "0x" + aStatusCode.toString(16); } // XXX real error handling - LOG("Error adding item: " + aStatusCode); + LOG("Error adding item: " + status); retVal = Components.results.NS_ERROR_FAILURE; } @@ -506,39 +458,50 @@ calDavCalendar.prototype = { + ex + "; ignoring"); } } - // notify observers if (Components.isSuccessCode(retVal)) { thisCalendar.mObservers.notify("onAddItem", [aItem]); } } - + aItem.calendar = this; aItem.generation = 1; aItem.setProperty("X-MOZ-LOCATIONPATH", locationPath); aItem.makeImmutable(); - LOG("icalString = " + aItem.icalString); + // LOG("icalString = " + aItem.icalString); - // XXX use if not exists - // do WebDAV put - var webSvc = Components.classes['@mozilla.org/webdav/service;1'] - .getService(Components.interfaces.nsIWebDAVService); - webSvc.putFromString(eventResource, "text/calendar; charset=utf-8", - aItem.icalString, listener, this, null); + var httpchannel = this.prepChannel(itemUri, true); + var uploadStream = this.prepUploadStream(aItem.icalString); + httpchannel.setUploadStream(uploadStream, "text/calendar; charset=utf-8", + -1); + if (!aIgnoreEtag) { + httpchannel.setRequestHeader("If-None-Match", "*", false); + } + + var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"] + .createInstance(Components.interfaces + .nsIStreamLoader); + + if (isOnBranch) { + streamLoader.init(httpchannel, addListener, httpchannel); + } else { + streamLoader.init(addListener); + channel.asyncOpen(streamLoader, httpchannel); + } return; }, /** - * Sends info about the request to modify an item off for etag checking. + * modifyItem(); required by calICalendar.idl + * we actually use doModifyItem() * * @param aItem item to check - * @param aOldItem aOldItem argument in modifyItem requests - * @param aListener listener from original request + * @param aListener listener for method completion */ - modifyItem: function caldavMI(aItem, aOldItem, aListener) { - this.fetchEtag(CALDAV_MODIFY_ITEM, aItem, aListener, aOldItem); + modifyItem: function caldavMI(aNewItem, aOldItem, aListener) { + return this.doModifyItem(aNewItem, aOldItem, aListener, false); }, /** @@ -547,8 +510,9 @@ calDavCalendar.prototype = { * @param aItem item to check * @param aOldItem previous version of item to be modified * @param aListener listener from original request + * @param aIgnoreEtag ignore item etag */ - performModifyItem: function caldavPMI(aNewItem, aOldItem, aListener) { + doModifyItem: function caldavMI(aNewItem, aOldItem, aListener, aIgnoreEtag) { if (aNewItem.id == null) { @@ -580,7 +544,6 @@ calDavCalendar.prototype = { eventUri.spec = this.makeUri(aNewItem.getProperty("X-MOZ-LOCATIONPATH")); LOG("using X-MOZ-LOCATIONPATH: " + eventUri.spec); } catch (ex) { - // XXX how are we REALLY supposed to figure this out? eventUri.spec = this.makeUri(aNewItem.id + ".ics"); } @@ -599,9 +562,7 @@ calDavCalendar.prototype = { aNewItem.generation += 1; - var eventResource = new WebDavResource(eventUri); - - var listener = new WebDavListener(); + var modListener = {}; var thisCalendar = this; const icssvc = Components.classes["@mozilla.org/calendar/ics-service;1"]. @@ -617,24 +578,28 @@ calDavCalendar.prototype = { } var modifiedItemICS = modifiedItem.serializeToICS(); - listener.onOperationComplete = function(aStatusCode, aResource, - aOperation, aClosure) { + modListener.onStreamComplete = function(aLoader, aContext, aStatus, + aResultLength, aResult) { // 201 = HTTP "Created" // 204 = HTTP "No Content" // - if (aStatusCode == 204 || aStatusCode == 201) { + var status = aContext.responseStatus; + if (status == 204 || status == 201) { LOG("Item modified successfully."); var retVal = Components.results.NS_OK; - // CalDAV does not require that the etag returned on PUT will - // be identical to the etag returned on later operations - // (CalDAV 5.3.4), so we best re-query. - thisCalendar.fetchEtag(CALDAV_CACHE_ETAG, aNewItem, null, null); - + // Some CalDAV servers will modify items on PUT (add X-props, + // change location, etc) so we'd best re-fetch in order to know + // the current state of the item + // Observers will be notified in getUpdatedItem() + thisCalendar.getUpdatedItem(aNewItem, aListener); + } else if (status == 412) { + thisCalendar.promptOverwrite(CALDAV_MODIFY_ITEM, aNewItem, + aListener, aOldItem); } else { - if (aStatusCode > 999) { - aStatusCode = "0x " + aStatusCode.toString(16); + if (status > 999) { + status = "0x " + status.toString(16); } - LOG("Error modifying item: " + aStatusCode); + LOG("Error modifying item: " + status); // XXX deal with non-existent item here, other // real error handling @@ -667,26 +632,41 @@ calDavCalendar.prototype = { return; } - // XXX use if-exists stuff here // XXX use etag as generation - // do WebDAV put + + var httpchannel = this.prepChannel(eventUri, true); + var uploadStream = this.prepUploadStream(modifiedItemICS); + httpchannel.setUploadStream(uploadStream, "text/calendar; charset=utf-8", + -1); + if (!aIgnoreEtag) { + httpchannel.setRequestHeader("If-Match", + this.mEtagCache[aNewItem.id], false); + } + LOG("modifyItem: PUTting = " + modifiedItemICS); - var webSvc = Components.classes['@mozilla.org/webdav/service;1'] - .getService(Components.interfaces.nsIWebDAVService); - webSvc.putFromString(eventResource, "text/calendar; charset=utf-8", - modifiedItemICS, listener, this, null); + var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"] + .createInstance(Components.interfaces + .nsIStreamLoader); + + if (isOnBranch) { + streamLoader.init(httpchannel, modListener, httpchannel); + } else { + streamLoader.init(modListener); + channel.asyncOpen(streamLoader, httpchannel); + } return; }, /** - * Sends data regarding requested deletion off for etag checking. - * - * @param aItem item to check + * deleteItem(); required by calICalendar.idl + * the actual deletion is done in doDeleteItem() + * + * @param aItem item to delete * @param aListener listener for method completion */ deleteItem: function caldavDI(aItem, aListener) { - this.fetchEtag(CALDAV_DELETE_ITEM, aItem, aListener, null); + return this.doDeleteItem(aItem, aListener, false); }, /** @@ -694,8 +674,9 @@ calDavCalendar.prototype = { * * @param aItem item to delete * @param aListener listener for method completion + * @param aIgnoreEtag ignore item etag */ - performDeleteItem: function caldavPDI(aItem, aListener) { + doDeleteItem: function caldavDDI(aItem, aListener, aIgnoreEtag) { if (aItem.id == null) { if (aListener) @@ -716,23 +697,40 @@ calDavCalendar.prototype = { eventUri.spec = this.makeUri(aItem.id + ".ics"); } - var eventResource = new WebDavResource(eventUri); - - var listener = new WebDavListener(); + var delListener = {}; var thisCalendar = this; - - listener.onOperationComplete = - function onOperationComplete(aStatusCode, aResource, aOperation, - aClosure) { - + var realListener = aListener; // need to access from callback + + delListener.onStreamComplete = + function caldavDLoSC(aLoader, aContext, aStatus, aResultLength, aResult) { + + var status = aContext.responseStatus; // 204 = HTTP "No content" // - if (aStatusCode == 204) { + if (status == 204) { delete thisCalendar.mEtagCache[aItem.id]; LOG("Item deleted successfully."); var retVal = Components.results.NS_OK; + } + else if (status == 412) { + // item has either been modified or deleted by someone else + // check to see which + + var httpchannel2 = thisCalendar.prepChannel(eventUri, false); + httpchannel2.requestMethod = "HEAD"; + var streamLoader2 = Components.classes + ["@mozilla.org/network/stream-loader;1"] + .createInstance(Components.interfaces + .nsIStreamLoader); + if (isOnBranch) { + streamLoader2.init(httpchannel2, delListener2, httpchannel2); + } else { + streamLoader2.init(streamListener2); + channel.asyncOpen(streamLoader2, httpchannel2); + } + } else { - LOG("Error deleting item: " + aStatusCode); + LOG("Error deleting item: " + status); // XXX real error handling here retVal = Components.results.NS_ERROR_FAILURE; } @@ -756,12 +754,38 @@ calDavCalendar.prototype = { thisCalendar.mObservers.notify("onDeleteItem", [aItem]); } } + var delListener2 = {}; + delListener2.onStreamComplete = + function caldavDL2oSC(aLoader, aContext, aStatus, aResultLength, aResult) { + var status2 = aContext.responseStatus; + if (status2 == 404) { + // someone else already deleted it + thisCalendar.mObservers.notify("onDeleteItem", [aItem]); + return; + } else { + thisCalendar.promptOverwrite(CALDAV_DELETE_ITEM, aItem, + realListener, null); + } + } // XXX check generation - // do WebDAV remove - var webSvc = Components.classes['@mozilla.org/webdav/service;1'] - .getService(Components.interfaces.nsIWebDAVService); - webSvc.remove(eventResource, listener, this, null); + var httpchannel = this.prepChannel(eventUri, false); + if (!aIgnoreEtag) { + httpchannel.setRequestHeader("If-Match", this.mEtagCache[aItem.id], + false); + } + httpchannel.requestMethod = "DELETE"; + + var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"] + .createInstance(Components.interfaces + .nsIStreamLoader); + + if (isOnBranch) { + streamLoader.init(httpchannel, delListener, httpchannel); + } else { + streamLoader.init(delListener); + channel.asyncOpen(streamLoader, httpchannel); + } return; }, @@ -791,6 +815,8 @@ calDavCalendar.prototype = { if (aItem instanceof Components.interfaces.calITodo) { itemType = "VTODO"; } + + var queryStatuses = new Array(); var C = new Namespace("C", "urn:ietf:params:xml:ns:caldav"); var D = new Namespace("D", "DAV:"); @@ -816,7 +842,8 @@ calDavCalendar.prototype = { ; this.reportInternal(xmlHeader + queryXml.toXMLString(), - false, null, null, 1, aListener, aItem); + false, null, null, 1, aListener, aItem, 1, + queryStatuses); return; },