Skip to content

Commit

Permalink
[buteo-sync-plugin-carddav] Improve addressbook-set parsing. Contribu…
Browse files Browse the repository at this point in the history
…tes to MER#1304

This commit adds support for parsing addressbook-set responses which
include multiple propstat elements.  Previously, this adapter assumed
that only one propstat per response would be returned.

Some services (e.g. Cozy) return multiple propstat elements, some with
404 NOT FOUND status values, in order to communicate the lack of some
non-essential properties (e.g., displayname).

This commit also allows syncing of addressbook collections for which
no sync-token or c-tag are provided by the remote server.  In that
case, we do manual delta detection.

Contributes to MER#1304
  • Loading branch information
Chris Adams committed Oct 14, 2015
1 parent 9549a35 commit 1cacb80
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 24 deletions.
8 changes: 7 additions & 1 deletion src/carddav.cpp
Expand Up @@ -541,7 +541,13 @@ void CardDav::downsyncAddressbookContent(const QList<ReplyParser::AddressBookInf
q->m_defaultAddressbook = infos[i].url;
}

if (infos[i].syncToken.isEmpty()) {
if (infos[i].syncToken.isEmpty() && infos[i].ctag.isEmpty()) {
// we cannot use either sync-token or ctag for this addressbook.
// we need to manually calculate the complete delta.
LOG_DEBUG("No sync-token or ctag given for addressbook:" << infos[i].url << ", manual delta detection required");
q->m_addressbookCtags[infos[i].url] = infos[i].ctag; // ctag is empty :. we will use manual detection.
fetchContactMetadata(infos[i].url);
} else if (infos[i].syncToken.isEmpty()) {
// we cannot use sync-token for this addressbook, but instead ctag.
const QString &existingCtag(q->m_addressbookCtags[infos[i].url]); // from OOB
if (existingCtag.isEmpty()) {
Expand Down
129 changes: 106 additions & 23 deletions src/replyparser.cpp
Expand Up @@ -255,38 +255,121 @@ QList<ReplyParser::AddressBookInformation> ReplyParser::parseAddressbookInformat
responses << response;
}

// parse the information about each addressbook (response element)
Q_FOREACH (const QVariant &rv, responses) {
QVariantMap rmap = rv.toMap();
ReplyParser::AddressBookInformation currInfo;
currInfo.url = QUrl::fromPercentEncoding(rmap.value("href").toMap().value("@text").toString().toUtf8());
currInfo.ctag = rmap.value("propstat").toMap().value("prop").toMap().value("getctag").toMap().value("@text").toString();
currInfo.syncToken = rmap.value("propstat").toMap().value("prop").toMap().value("sync-token").toMap().value("@text").toString();
currInfo.displayName = rmap.value("propstat").toMap().value("prop").toMap().value("displayname").toMap().value("@text").toString();
QStringList resourceTypeKeys = rmap.value("propstat").toMap().value("prop").toMap().value("resourcetype").toMap().keys();
if (resourceTypeKeys.isEmpty()
|| (resourceTypeKeys.size() == 1 && resourceTypeKeys.contains(QStringLiteral("collection"), Qt::CaseInsensitive))
|| (resourceTypeKeys.contains(QStringLiteral("addressbook"), Qt::CaseInsensitive))) {
// This is probably a carddav addressbook collection.
// Despite section 5.2 of RFC6352 stating that a CardDAV
// server MUST return the 'addressbook' value in the resource types
// property, some CardDAV implementations (eg, Memotoo) do not.
LOG_DEBUG(Q_FUNC_INFO << "parsing information for addressbook:" << currInfo.url);

// some services (e.g. Cozy) return multiple propstat elements in each response
QVariantList propstats;
if (rmap.value("propstat").type() == QVariant::List) {
propstats = rmap.value("propstat").toList();
} else {
// the resource is explicitly described as non-addressbook resource.
LOG_DEBUG(Q_FUNC_INFO << "ignoring non-addressbook response");
continue;
QVariantMap propstat = rmap.value("propstat").toMap();
propstats << propstat;
}
QString status = rmap.value("propstat").toMap().value("status").toMap().value("@text").toString();
if (status.contains(QRegularExpression("2[0-9][0-9]"))) { // any HTTP 2xx response
if (currInfo.ctag.isEmpty() && currInfo.syncToken.isEmpty()) {
LOG_DEBUG(Q_FUNC_INFO << "ignoring addressbook:" << currInfo.url << "due to lack of ctag");
} else {
LOG_DEBUG(Q_FUNC_INFO << "found valid addressbook:" << currInfo.url);
infos.append(currInfo);

// examine the propstat elements to find the features we're interested in
enum ResourceStatus { StatusUnknown = 0,
StatusExplicitly2xxOk = 1,
StatusExplicitlyTrue = 1,
StatusExplicitlyNotOk = 2,
StatusExplicitlyFalse = 2 };
ResourceStatus addressbookResourceSpecified = StatusUnknown; // valid values are Unknown/True/False
ResourceStatus resourcetypeStatus = StatusUnknown; // valid values are Unknown/2xxOk/NotOk
ResourceStatus otherPropertyStatus = StatusUnknown; // valid values are Unknown/2xxOk/NotOk
Q_FOREACH (const QVariant &vpropstat, propstats) {
QVariantMap propstat = vpropstat.toMap();
const QVariantMap &prop(propstat.value("prop").toMap());
if (prop.contains("getctag")) {
currInfo.ctag = prop.value("getctag").toMap().value("@text").toString();
}
if (prop.contains("sync-token")) {
currInfo.syncToken = prop.value("sync-token").toMap().value("@text").toString();
}
if (prop.contains("displayname")) {
currInfo.displayName = prop.value("displayname").toMap().value("@text").toString();
}
bool thisPropstatIsForResourceType = false;
if (prop.contains("resourcetype")) {
thisPropstatIsForResourceType = true;
QStringList resourceTypeKeys = prop.value("resourcetype").toMap().keys();
if ((resourceTypeKeys.size() == 1 && resourceTypeKeys.contains(QStringLiteral("collection"), Qt::CaseInsensitive))
|| (resourceTypeKeys.contains(QStringLiteral("addressbook"), Qt::CaseInsensitive))) {
// This is probably a carddav addressbook collection.
// Despite section 5.2 of RFC6352 stating that a CardDAV
// server MUST return the 'addressbook' value in the resource types
// property, some CardDAV implementations (eg, Memotoo) do not.
addressbookResourceSpecified = StatusExplicitlyTrue;
LOG_DEBUG(Q_FUNC_INFO << "have addressbook resource:" << currInfo.url);
} else {
// the resource is explicitly described as non-addressbook resource.
addressbookResourceSpecified = StatusExplicitlyFalse;
LOG_DEBUG(Q_FUNC_INFO << "have non-addressbook resource:" << currInfo.url);
}
}
// Some services (e.g. Cozy) return multiple propstats
// where only one will refer to the resourcetype property itself;
// others will refer to incidental properties like displayname etc.
// Each propstat will (should) contain a status code, which applies
// only to the properties referred to within the propstat.
// Thus, a 404 code may only apply to a displayname, etc.
if (propstat.contains("status")) {
static const QRegularExpression Http2xxOk("2[0-9][0-9]");
QString status = propstat.value("status").toMap().value("@text").toString();
bool statusOk = status.contains(Http2xxOk); // any HTTP 2xx OK response
if (thisPropstatIsForResourceType) {
// This status applies to the resourcetype property.
if (statusOk) {
resourcetypeStatus = StatusExplicitly2xxOk; // explicitly ok
} else {
resourcetypeStatus = StatusExplicitlyNotOk; // explicitly not ok
LOG_DEBUG(Q_FUNC_INFO << "response has non-OK status:" << status
<< "for properties:" << prop.keys()
<< "for url:" << currInfo.url);
}
} else {
// This status applies to some other property.
// In some cases (e.g. Memotoo) we may need
// to infer that this status refers to the
// entire response.
if (statusOk) {
otherPropertyStatus = StatusExplicitly2xxOk; // explicitly ok
} else {
otherPropertyStatus = StatusExplicitlyNotOk; // explicitly not ok
LOG_DEBUG(Q_FUNC_INFO << "response has non-OK status:" << status
<< "for non-resourcetype properties:" << prop.keys()
<< "for url:" << currInfo.url);
}
}
}
}

// now check to see if we have all of the required information
if (resourcetypeStatus == StatusExplicitly2xxOk) {
// we definitely had a well-specified resourcetype response, with 200 OK status.
LOG_DEBUG(Q_FUNC_INFO << "have addressbook resource with status OK:" << currInfo.url);
} else if (propstats.count() == 1 // only one response element
&& addressbookResourceSpecified == StatusUnknown // resource type unknown
&& otherPropertyStatus == StatusExplicitly2xxOk) { // status was explicitly ok
// we assume that this was an implicit Addressbook Collection resourcetype response.
LOG_DEBUG(Q_FUNC_INFO << "have probable addressbook resource with status OK:" << currInfo.url);
} else {
// we either cannot infer that this was an Addressbook Collection
// or we were told explicitly that the collection status was NOT OK.
LOG_DEBUG(Q_FUNC_INFO << "ignoring resource:" << currInfo.url << "due to type or status:"
<< addressbookResourceSpecified << resourcetypeStatus << otherPropertyStatus);
continue;
}

// add the addressbook to our return list. If we have no sync-token or c-tag, we do manual delta detection.
if (currInfo.ctag.isEmpty() && currInfo.syncToken.isEmpty()) {
LOG_DEBUG(Q_FUNC_INFO << "addressbook:" << currInfo.url << "has no sync-token or c-tag");
} else {
LOG_DEBUG(Q_FUNC_INFO << "ignoring addressbook:" << currInfo.url << "due to invalid status:" << status);
LOG_DEBUG(Q_FUNC_INFO << "found valid addressbook:" << currInfo.url << "with sync-token or c-tag");
}
infos.append(currInfo);
}

return infos;
Expand Down

0 comments on commit 1cacb80

Please sign in to comment.