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
This commit is contained in:
daniel.boelzle%sun.com
2007-11-14 09:43:47 +00:00
parent 30297b41b1
commit 11fac523f3

View File

@@ -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 = {
</calendar-query>;
this.reportInternal(xmlHeader + queryXml.toXMLString(),
false, null, null, 1, aListener, aItem);
false, null, null, 1, aListener, aItem, 1,
queryStatuses);
return;
},