WebDAV: handle UID conflicts

When asked to insert a VJOURNAL which already existed (= same UID),
CalDAV servers respond with a 412 "Precondition failed" error. This
needs to be detected and translated into an "item needs to be merged"
result so that the engine can load the existing item, merge the data,
and then write back.

A test for this, testInsertTwice, will be committed separately.  The
code was written so that it handles the same error when using CardDAV.
However, this was not tested because CardDAV test data does not have a
UID (wouldn't trigger the problem) and Radicale did not report 412 when
adding the UID.
This commit is contained in:
Patrick Ohly 2012-07-02 14:50:57 +02:00
parent b0173cbff4
commit 2add74a5ea
3 changed files with 107 additions and 7 deletions

View file

@ -1332,7 +1332,7 @@ void WebDAVSource::listAllItems(RevisionMap_t &revisions)
Neon::XMLParser parser;
parser.initReportParser(boost::bind(&WebDAVSource::checkItem, this,
boost::ref(revisions),
_1, _2, boost::ref(data)));
_1, _2, &data));
parser.pushHandler(boost::bind(Neon::XMLParser::accept, "urn:ietf:params:xml:ns:caldav", "calendar-data", _2, _3),
boost::bind(Neon::XMLParser::append, boost::ref(data), _2, _3));
Neon::Request report(*getSession(), "REPORT", getCalendar().m_path, query, parser);
@ -1345,6 +1345,83 @@ void WebDAVSource::listAllItems(RevisionMap_t &revisions)
}
}
std::string WebDAVSource::findByUID(const std::string &uid,
const Timespec &deadline)
{
RevisionMap_t revisions;
std::string query;
if (getContent() == "VCARD") {
// use CardDAV
query =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
"<C:addressbook-query xmlns:D=\"DAV:\"\n"
"xmlns:C=\"urn:ietf:params:xml:ns:carddav:addressbook\">\n"
"<D:prop>\n"
"<D:getetag/>\n"
"</D:prop>\n"
"<C:filter>\n"
"<C:comp-filter name=\"" + getContent() + "\">\n"
"<C:prop-filter name=\"UID\">\n"
"<C:text-match>" + uid + "</C:text-match>\n"
"</C:prop-filter>\n"
"</C:comp-filter>\n"
"</C:filter>\n"
"</C:addressbook-query>\n";
} else {
// use CalDAV
query =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
"<C:calendar-query xmlns:D=\"DAV:\"\n"
"xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n"
"<D:prop>\n"
"<D:getetag/>\n"
"</D:prop>\n"
"<C:filter>\n"
"<C:comp-filter name=\"VCALENDAR\">\n"
"<C:comp-filter name=\"" + getContent() + "\">\n"
"<C:prop-filter name=\"UID\">\n"
// Collation from RFC 4791, not supported yet by all servers.
// Apple Calendar Server did not like CDATA.
// TODO: escape special characters.
"<C:text-match" /* collation=\"i;octet\" */ ">" /* <[CDATA[ */ + uid + /* ]]> */ "</C:text-match>\n"
"</C:prop-filter>\n"
"</C:comp-filter>\n"
"</C:comp-filter>\n"
"</C:filter>\n"
"</C:calendar-query>\n";
}
getSession()->startOperation("REPORT 'UID lookup'", deadline);
while (true) {
Neon::XMLParser parser;
parser.initReportParser(boost::bind(&WebDAVSource::checkItem, this,
boost::ref(revisions),
_1, _2, (std::string *)0));
Neon::Request report(*getSession(), "REPORT", getCalendar().m_path, query, parser);
report.addHeader("Depth", "1");
report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
if (report.run()) {
break;
}
}
switch (revisions.size()) {
case 0:
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
"object not found",
SyncMLStatus(404));
break;
case 1:
return revisions.begin()->first;
break;
default:
// should not happen
SE_THROW(StringPrintf("UID %s not unique?!", uid.c_str()));
}
// not reached
return "";
}
void WebDAVSource::listAllItemsCallback(const Neon::URI &uri,
const ne_prop_result_set *results,
RevisionMap_t &revisions,
@ -1387,7 +1464,7 @@ void WebDAVSource::listAllItemsCallback(const Neon::URI &uri,
int WebDAVSource::checkItem(RevisionMap_t &revisions,
const std::string &href,
const std::string &etag,
std::string &data)
std::string *data)
{
// Ignore responses with no data: this is not perfect (should better
// try to figure out why there is no data), but better than
@ -1395,20 +1472,23 @@ int WebDAVSource::checkItem(RevisionMap_t &revisions,
//
// One situation is the response for the collection itself,
// which comes with a 404 status and no data with Google Calendar.
if (data.empty()) {
if (data && data->empty()) {
return 0;
}
// No need to parse, user content cannot start at start of line in
// iCalendar 2.0.
if (data.find("\nBEGIN:" + getContent()) != data.npos) {
if (!data ||
data->find("\nBEGIN:" + getContent()) != data->npos) {
std::string davLUID = path2luid(Neon::URI::parse(href).m_path);
std::string rev = ETag2Rev(etag);
revisions[davLUID] = rev;
}
// reset data for next item
data.clear();
if (data) {
data->clear();
}
return 0;
}
@ -1503,7 +1583,8 @@ TrackingSyncSource::InsertItemResult WebDAVSource::insertItem(const string &uid,
req.addHeader("If-None-Match", "*");
}
req.addHeader("Content-Type", contentType().c_str());
if (!req.run()) {
static const std::set<int> expected = boost::assign::list_of(412);
if (!req.run(&expected)) {
goto retry;
}
SE_LOG_DEBUG(NULL, NULL, "add item status: %s",
@ -1516,6 +1597,16 @@ TrackingSyncSource::InsertItemResult WebDAVSource::insertItem(const string &uid,
case 201:
// created
break;
case 412: {
// "Precondition Failed": our only precondition is the one about
// If-None-Match, which means that there must be an existing item
// with the same UID. Go find it, so that we can report back the
// right luid.
std::string uid = extractUID(item);
std::string luid = findByUID(uid, deadline);
return InsertItemResult(luid, "", ITEM_NEEDS_MERGE);
break;
}
default:
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
std::string("unexpected status for insert: ") +

View file

@ -192,6 +192,12 @@ class WebDAVSource : public TrackingSyncSource, private boost::noncopyable
*/
virtual const std::string *setResourceName(const std::string &item, std::string &buffer, const std::string &luid);
/**
* Find one item by its UID property value and return the corresponding
* resource name relative to the current collection (aka luid).
*/
std::string findByUID(const std::string &uid, const Timespec &deadline);
/**
* Get UID property value from vCard 3.0 or iCalendar 2.0 text
* items.
@ -242,7 +248,7 @@ class WebDAVSource : public TrackingSyncSource, private boost::noncopyable
int checkItem(RevisionMap_t &revisions,
const std::string &href,
const std::string &etag,
std::string &data);
std::string *data);
void backupData(const boost::function<Operations::BackupData_t> &op,
const Operations::ConstBackupInfo &oldBackup,

View file

@ -440,6 +440,9 @@ void LocalTests::addTests() {
ADD_TEST(LocalTests, testSimpleInsert);
ADD_TEST(LocalTests, testLocalDeleteAll);
ADD_TEST(LocalTests, testComplexInsert);
if (config.m_insertItem.find("\nUID:") != std::string::npos) {
ADD_TEST(LocalTests, testInsertTwice);
}
if (!config.m_updateItem.empty()) {
ADD_TEST(LocalTests, testLocalUpdate);