2385 lines
96 KiB
C++
2385 lines
96 KiB
C++
/*
|
|
* Copyright (C) 2010 Intel Corporation
|
|
*/
|
|
|
|
#include "WebDAVSource.h"
|
|
|
|
#include <boost/bind.hpp>
|
|
#include <boost/algorithm/string/replace.hpp>
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <boost/algorithm/string/classification.hpp>
|
|
#include <boost/algorithm/string/find.hpp>
|
|
#include <boost/scoped_ptr.hpp>
|
|
|
|
#include <syncevo/LogRedirect.h>
|
|
#include <syncevo/IdentityProvider.h>
|
|
|
|
#include <boost/assign.hpp>
|
|
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
|
|
SE_BEGIN_CXX
|
|
|
|
BoolConfigProperty &WebDAVCredentialsOkay()
|
|
{
|
|
static BoolConfigProperty okay("webDAVCredentialsOkay", "credentials were accepted before");
|
|
return okay;
|
|
}
|
|
|
|
#ifdef ENABLE_DAV
|
|
|
|
/**
|
|
* Retrieve settings from SyncConfig.
|
|
* NULL pointer for config is allowed.
|
|
*/
|
|
class ContextSettings : public Neon::Settings {
|
|
public:
|
|
/**
|
|
* Base URL(s) for WebDAV service.
|
|
* More than one can be give as candidate for scanning.
|
|
*/
|
|
typedef std::vector<std::string> URLs;
|
|
|
|
private:
|
|
boost::shared_ptr<SyncConfig> m_context;
|
|
SyncSourceConfig *m_sourceConfig;
|
|
URLs m_urls;
|
|
std::string m_urlsDescription;
|
|
std::string m_url;
|
|
std::string m_urlDescription;
|
|
/** do change tracking without relying on CTag */
|
|
bool m_noCTag;
|
|
bool m_googleUpdateHack;
|
|
bool m_googleAlarmHack;
|
|
// credentials were valid in the past: stored persistently in tracking node
|
|
bool m_credentialsOkay;
|
|
|
|
public:
|
|
ContextSettings(const boost::shared_ptr<SyncConfig> &context,
|
|
SyncSourceConfig *sourceConfig) :
|
|
m_context(context),
|
|
m_sourceConfig(sourceConfig),
|
|
m_noCTag(false),
|
|
m_googleUpdateHack(false),
|
|
m_googleAlarmHack(false),
|
|
m_credentialsOkay(false)
|
|
{
|
|
URLs urls;
|
|
std::string description = "<unset>";
|
|
|
|
std::string syncName = m_context->getConfigName();
|
|
if (syncName.empty()) {
|
|
syncName = "<none>";
|
|
}
|
|
|
|
// check source config first
|
|
if (m_sourceConfig) {
|
|
urls.push_back(m_sourceConfig->getDatabaseID());
|
|
std::string &url = urls.front();
|
|
std::string sourceName = m_sourceConfig->getName();
|
|
if (sourceName.empty()) {
|
|
sourceName = "<none>";
|
|
}
|
|
description = StringPrintf("sync config '%s', datastore config '%s', database='%s'",
|
|
syncName.c_str(),
|
|
sourceName.c_str(),
|
|
url.c_str());
|
|
}
|
|
|
|
// fall back to sync context
|
|
if ((urls.empty() || (urls.size() == 1 && urls.front().empty())) && m_context) {
|
|
urls = m_context->getSyncURL();
|
|
description = StringPrintf("sync config '%s', syncURL='%s'",
|
|
syncName.c_str(),
|
|
boost::join(urls, " ").c_str());
|
|
}
|
|
|
|
// remember result and set flags
|
|
setURLs(urls, description);
|
|
if (!urls.empty()) {
|
|
setURL(urls.front(), description);
|
|
}
|
|
|
|
// m_credentialsOkay: no corresponding setting when using
|
|
// credentials + URL from source config, in which case we
|
|
// never know that credentials should work (bad for Google,
|
|
// with its temporary authentication errors)
|
|
if (m_context) {
|
|
boost::shared_ptr<FilterConfigNode> node = m_context->getNode(WebDAVCredentialsOkay());
|
|
m_credentialsOkay = WebDAVCredentialsOkay().getPropertyValue(*node);
|
|
}
|
|
}
|
|
|
|
void setURLs(const URLs &urls, const std::string &description) { m_urls = urls; m_urlsDescription = description; }
|
|
URLs getURLs() { return m_urls; }
|
|
std::string getURLsDescription() { return m_urlsDescription; }
|
|
void setURL(const std::string &url, const std::string &description) { initializeFlags(url); m_url = url; m_urlDescription = description; }
|
|
std::string getURL() { return m_url; }
|
|
std::string getURLDescription() { return m_urlDescription; }
|
|
|
|
virtual bool verifySSLHost()
|
|
{
|
|
return !m_context || m_context->getSSLVerifyHost();
|
|
}
|
|
|
|
virtual bool verifySSLCertificate()
|
|
{
|
|
return !m_context || m_context->getSSLVerifyServer();
|
|
}
|
|
|
|
virtual std::string proxy()
|
|
{
|
|
if (!m_context ||
|
|
!m_context->getUseProxy()) {
|
|
return "";
|
|
} else {
|
|
return m_context->getProxyHost();
|
|
}
|
|
}
|
|
|
|
bool noCTag() const { return m_noCTag; }
|
|
virtual bool googleUpdateHack() const { return m_googleUpdateHack; }
|
|
virtual bool googleAlarmHack() const { return m_googleAlarmHack; }
|
|
|
|
virtual int timeoutSeconds() const { return m_context->getRetryDuration(); }
|
|
virtual int retrySeconds() const {
|
|
int seconds = m_context->getRetryInterval();
|
|
if (seconds >= 0) {
|
|
seconds /= (120 / 5); // default: 2min => 5s
|
|
}
|
|
return seconds;
|
|
}
|
|
|
|
virtual void getCredentials(const std::string &realm,
|
|
std::string &username,
|
|
std::string &password);
|
|
|
|
virtual boost::shared_ptr<AuthProvider> getAuthProvider();
|
|
|
|
std::string getUsername()
|
|
{
|
|
lookupAuthProvider();
|
|
return m_authProvider->getUsername();
|
|
}
|
|
|
|
virtual bool getCredentialsOkay() { return m_credentialsOkay; }
|
|
virtual void setCredentialsOkay(bool okay) {
|
|
if (m_credentialsOkay != okay && m_context) {
|
|
boost::shared_ptr<FilterConfigNode> node = m_context->getNode(WebDAVCredentialsOkay());
|
|
if (!node->isReadOnly()) {
|
|
WebDAVCredentialsOkay().setProperty(*node, okay);
|
|
node->flush();
|
|
}
|
|
m_credentialsOkay = okay;
|
|
}
|
|
}
|
|
|
|
virtual int logLevel()
|
|
{
|
|
return m_context ?
|
|
m_context->getLogLevel().get() :
|
|
Logger::instance().getLevel();
|
|
}
|
|
|
|
private:
|
|
void initializeFlags(const std::string &url);
|
|
boost::shared_ptr<AuthProvider> m_authProvider;
|
|
void lookupAuthProvider();
|
|
};
|
|
|
|
|
|
void ContextSettings::getCredentials(const std::string &realm,
|
|
std::string &username,
|
|
std::string &password)
|
|
{
|
|
lookupAuthProvider();
|
|
Credentials creds = m_authProvider->getCredentials();
|
|
username = creds.m_username;
|
|
password = creds.m_password;
|
|
}
|
|
|
|
boost::shared_ptr<AuthProvider> ContextSettings::getAuthProvider()
|
|
{
|
|
lookupAuthProvider();
|
|
return m_authProvider;
|
|
}
|
|
|
|
void ContextSettings::lookupAuthProvider()
|
|
{
|
|
if (m_authProvider) {
|
|
return;
|
|
}
|
|
|
|
UserIdentity identity;
|
|
InitStateString password;
|
|
|
|
// prefer source config if anything is set there
|
|
const char *credentialsFrom = "undefined";
|
|
if (m_sourceConfig) {
|
|
identity = m_sourceConfig->getUser();
|
|
password = m_sourceConfig->getPassword();
|
|
credentialsFrom = "datastore config";
|
|
}
|
|
|
|
// fall back to context
|
|
if (m_context && !identity.wasSet() && !password.wasSet()) {
|
|
identity = m_context->getSyncUser();
|
|
password = m_context->getSyncPassword();
|
|
credentialsFrom = "context";
|
|
}
|
|
SE_LOG_DEBUG(NULL, "using username '%s' from %s for WebDAV, password %s",
|
|
identity.toString().c_str(),
|
|
credentialsFrom,
|
|
password.wasSet() ? "was set" : "not set");
|
|
|
|
// lookup actual authentication method instead of assuming username/password
|
|
m_authProvider = AuthProvider::create(identity, password);
|
|
}
|
|
|
|
void ContextSettings::initializeFlags(const std::string &url)
|
|
{
|
|
bool googleUpdate = false,
|
|
googleAlarm = false,
|
|
noCTag = false;
|
|
|
|
Neon::URI uri = Neon::URI::parse(url);
|
|
typedef boost::split_iterator<string::iterator> string_split_iterator;
|
|
for (string_split_iterator arg =
|
|
boost::make_split_iterator(uri.m_query, boost::first_finder("&", boost::is_iequal()));
|
|
arg != string_split_iterator();
|
|
++arg) {
|
|
static const std::string keyword = "SyncEvolution=";
|
|
if (boost::istarts_with(*arg, keyword)) {
|
|
std::string params(arg->begin() + keyword.size(), arg->end());
|
|
for (string_split_iterator flag =
|
|
boost::make_split_iterator(params,
|
|
boost::first_finder(",", boost::is_iequal()));
|
|
flag != string_split_iterator();
|
|
++flag) {
|
|
if (boost::iequals(*flag, "UpdateHack")) {
|
|
googleUpdate = true;
|
|
} else if (boost::iequals(*flag, "ChildHack")) {
|
|
// Not used anymore, flag ignored.
|
|
} else if (boost::iequals(*flag, "AlarmHack")) {
|
|
googleAlarm = true;
|
|
} else if (boost::iequals(*flag, "Google")) {
|
|
googleUpdate =
|
|
googleAlarm = true;
|
|
} else if (boost::iequals(*flag, "NoCTag")) {
|
|
noCTag = true;
|
|
} else {
|
|
SE_THROW(StringPrintf("unknown SyncEvolution flag %s in URL %s",
|
|
std::string(flag->begin(), flag->end()).c_str(),
|
|
url.c_str()));
|
|
}
|
|
}
|
|
} else if (arg->end() != arg->begin()) {
|
|
SE_THROW(StringPrintf("unknown parameter %s in URL %s",
|
|
std::string(arg->begin(), arg->end()).c_str(),
|
|
url.c_str()));
|
|
}
|
|
}
|
|
|
|
// store final result
|
|
m_googleUpdateHack = googleUpdate;
|
|
m_googleAlarmHack = googleAlarm;
|
|
m_noCTag = noCTag;
|
|
}
|
|
|
|
WebDAVSource::Props_t::mapped_type & WebDAVSource::Props_t::operator [] (const WebDAVSource::Props_t::key_type &key)
|
|
{
|
|
iterator it = find(key);
|
|
if (it != end()) {
|
|
return it->second;
|
|
} else {
|
|
push_back(value_type(key, mapped_type()));
|
|
return back().second;
|
|
}
|
|
}
|
|
|
|
WebDAVSource::Props_t::iterator WebDAVSource::Props_t::find(const WebDAVSource::Props_t::key_type &key) {
|
|
for (iterator it = begin();
|
|
it != end();
|
|
++it) {
|
|
if (it->first == key) {
|
|
return it;
|
|
}
|
|
}
|
|
return end();
|
|
}
|
|
|
|
WebDAVSource::WebDAVSource(const SyncSourceParams ¶ms,
|
|
const boost::shared_ptr<Neon::Settings> &settings) :
|
|
TrackingSyncSource(params),
|
|
m_settings(settings)
|
|
{
|
|
if (!m_settings) {
|
|
m_contextSettings.reset(new ContextSettings(params.m_context, this));
|
|
m_settings = m_contextSettings;
|
|
}
|
|
|
|
/* insert contactServer() into BackupData_t and RestoreData_t (implemented by SyncSourceRevisions) */
|
|
m_operations.m_backupData = boost::bind(&WebDAVSource::backupData,
|
|
this, m_operations.m_backupData, _1, _2, _3);
|
|
m_operations.m_restoreData = boost::bind(&WebDAVSource::restoreData,
|
|
this, m_operations.m_restoreData, _1, _2, _3);
|
|
|
|
// ignore the "Request ends, status 207 class 2xx, error line:" printed by neon
|
|
LogRedirect::addIgnoreError(", error line:");
|
|
// ignore error messages in returned data
|
|
LogRedirect::addIgnoreError("Read block (");
|
|
}
|
|
|
|
static const std::string UID("\nUID:");
|
|
|
|
const std::string *WebDAVSource::createResourceName(const std::string &item, std::string &buffer, std::string &luid)
|
|
{
|
|
luid = extractUID(item);
|
|
std::string suffix = getSuffix();
|
|
if (luid.empty()) {
|
|
// must modify item
|
|
luid = UUID();
|
|
buffer = item;
|
|
size_t start = buffer.find("\nEND:" + getContent());
|
|
if (start != buffer.npos) {
|
|
start++;
|
|
buffer.insert(start, StringPrintf("UID:%s\r\n", luid.c_str()));
|
|
}
|
|
luid += suffix;
|
|
return &buffer;
|
|
} else {
|
|
luid += suffix;
|
|
return &item;
|
|
}
|
|
}
|
|
|
|
const std::string *WebDAVSource::setResourceName(const std::string &item, std::string &buffer, const std::string &luid)
|
|
{
|
|
std::string olduid = luid;
|
|
std::string suffix = getSuffix();
|
|
if (boost::ends_with(olduid, suffix)) {
|
|
olduid.resize(olduid.size() - suffix.size());
|
|
}
|
|
|
|
// First check if the item already contains the right UID
|
|
// or at least some UID. If there is a UID, we trust it to be correct,
|
|
// because our guess here (resource name == UID) can be wrong, for
|
|
// example for items created by other clients or by us when using
|
|
// POST and letting the server choose the resource name.
|
|
//
|
|
// This relies on our peer doing the right thing.
|
|
size_t start, end;
|
|
std::string uid = extractUID(item, &start, &end);
|
|
if (uid == olduid || !uid.empty()) {
|
|
return &item;
|
|
}
|
|
|
|
// insert or overwrite
|
|
buffer = item;
|
|
if (start != std::string::npos) {
|
|
// overwrite
|
|
buffer.replace(start, end - start, olduid);
|
|
} else {
|
|
// insert
|
|
start = buffer.find("\nEND:" + getContent());
|
|
if (start != buffer.npos) {
|
|
start++;
|
|
buffer.insert(start, StringPrintf("UID:%s\n", olduid.c_str()));
|
|
}
|
|
}
|
|
return &buffer;
|
|
}
|
|
|
|
|
|
|
|
std::string WebDAVSource::extractUID(const std::string &item, size_t *startp, size_t *endp)
|
|
{
|
|
std::string luid;
|
|
if (startp) {
|
|
*startp = std::string::npos;
|
|
}
|
|
if (endp) {
|
|
*endp = std::string::npos;
|
|
}
|
|
// find UID, use that plus ".vcf" as resource name (expected by Yahoo Contacts)
|
|
size_t start = item.find(UID);
|
|
if (start != item.npos) {
|
|
start += UID.size();
|
|
size_t end = item.find("\n", start);
|
|
if (end != item.npos) {
|
|
if (startp) {
|
|
*startp = start;
|
|
}
|
|
luid = item.substr(start, end - start);
|
|
if (boost::ends_with(luid, "\r")) {
|
|
luid.resize(luid.size() - 1);
|
|
}
|
|
// keep checking for more lines because of folding
|
|
while (end + 1 < item.size() &&
|
|
item[end + 1] == ' ') {
|
|
start = end + 1;
|
|
end = item.find("\n", start);
|
|
if (end == item.npos) {
|
|
// incomplete, abort
|
|
luid = "";
|
|
if (startp) {
|
|
*startp = std::string::npos;
|
|
}
|
|
break;
|
|
}
|
|
luid += item.substr(start, end - start);
|
|
if (boost::ends_with(luid, "\r")) {
|
|
luid.resize(luid.size() - 1);
|
|
}
|
|
}
|
|
// success, return all information
|
|
if (endp) {
|
|
// don't include \r or \n
|
|
*endp = item[end - 1] == '\r' ?
|
|
end - 1 :
|
|
end;
|
|
}
|
|
}
|
|
}
|
|
return luid;
|
|
}
|
|
|
|
std::string WebDAVSource::getSuffix() const
|
|
{
|
|
return getContent() == "VCARD" ?
|
|
".vcf" :
|
|
".ics";
|
|
}
|
|
|
|
void WebDAVSource::replaceHTMLEntities(std::string &item)
|
|
{
|
|
while (true) {
|
|
bool found = false;
|
|
|
|
std::string decoded;
|
|
size_t last = 0; // last character copied
|
|
size_t next = 0; // next character to be looked at
|
|
while (true) {
|
|
next = item.find('&', next);
|
|
size_t start = next;
|
|
if (next == item.npos) {
|
|
// finish decoding
|
|
if (found) {
|
|
decoded.append(item, last, item.size() - last);
|
|
}
|
|
break;
|
|
}
|
|
next++;
|
|
size_t end = next;
|
|
while (end != item.size()) {
|
|
char c = item[end];
|
|
if ((c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
(c == '#')) {
|
|
end++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (end == item.size() || item[end] != ';') {
|
|
// Invalid character between & and ; or no
|
|
// proper termination? No entity, continue
|
|
// decoding in next loop iteration.
|
|
next = end;
|
|
continue;
|
|
}
|
|
unsigned char c = 0;
|
|
if (next < end) {
|
|
if (item[next] == '#') {
|
|
// decimal or hexadecimal number
|
|
next++;
|
|
if (next < end) {
|
|
int base;
|
|
if (item[next] == 'x') {
|
|
// hex
|
|
base = 16;
|
|
next++;
|
|
} else {
|
|
base = 10;
|
|
}
|
|
while (next < end) {
|
|
unsigned char v = tolower(item[next]);
|
|
if (v >= '0' && v <= '9') {
|
|
next++;
|
|
c = c * base + (v - '0');
|
|
} else if (base == 16 && v >= 'a' && v <= 'f') {
|
|
next++;
|
|
c = c * base + (v - 'a') + 10;
|
|
} else {
|
|
// invalid character, abort scanning of this entity
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// check for entities
|
|
struct {
|
|
const char *m_name;
|
|
unsigned char m_character;
|
|
} entities[] = {
|
|
// core entries, extend as needed...
|
|
{ "quot", '"' },
|
|
{ "amp", '&' },
|
|
{ "apos", '\'' },
|
|
{ "lt", '<' },
|
|
{ "gt", '>' },
|
|
{ NULL, 0 }
|
|
};
|
|
int i = 0;
|
|
while (true) {
|
|
const char *name = entities[i].m_name;
|
|
if (!name) {
|
|
break;
|
|
}
|
|
if (!item.compare(next, end - next, name)) {
|
|
c = entities[i].m_character;
|
|
next += strlen(name);
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
if (next == end) {
|
|
// swallowed all characters in entity, must be valid:
|
|
// copy all uncopied characters plus the new one
|
|
found = true;
|
|
decoded.reserve(item.size());
|
|
decoded.append(item, last, start - last);
|
|
decoded.append(1, c);
|
|
last = end + 1;
|
|
}
|
|
}
|
|
next = end + 1;
|
|
}
|
|
if (found) {
|
|
item = decoded;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WebDAVSource::open()
|
|
{
|
|
// Nothing to do here, expensive initialization is in contactServer().
|
|
}
|
|
|
|
static bool setFirstURL(Neon::URI &result,
|
|
bool &resultIsReadOnly,
|
|
const std::string &name,
|
|
const Neon::URI &uri,
|
|
bool isReadOnly)
|
|
{
|
|
if (result.empty() ||
|
|
// Overwrite read-only with read/write collection.
|
|
(resultIsReadOnly && !isReadOnly)) {
|
|
result = uri;
|
|
resultIsReadOnly = isReadOnly;
|
|
}
|
|
// Stop if read/write found.
|
|
return resultIsReadOnly;
|
|
}
|
|
|
|
void WebDAVSource::contactServer()
|
|
{
|
|
if (!m_calendar.empty() &&
|
|
m_session) {
|
|
// we have done this work before, no need to repeat it
|
|
return;
|
|
}
|
|
|
|
SE_LOG_DEBUG(NULL, "using libneon %s with %s",
|
|
ne_version_string(), Neon::features().c_str());
|
|
|
|
// Can we skip auto-detection because a full resource URL is set?
|
|
std::string database = getDatabaseID();
|
|
if (!database.empty() &&
|
|
m_contextSettings) {
|
|
m_calendar = Neon::URI::parse(database, true);
|
|
// m_contextSettings = m_settings, so this sets m_settings->getURL()
|
|
m_contextSettings->setURL(database,
|
|
StringPrintf("%s database=%s",
|
|
getDisplayName().c_str(),
|
|
database.c_str()));
|
|
// start talking to host defined by m_settings->getURL()
|
|
m_session = Neon::Session::create(m_settings);
|
|
SE_LOG_INFO(getDisplayName(), "using configured database=%s", database.c_str());
|
|
// force authentication via username/password or OAuth2
|
|
m_session->forceAuthorization(m_settings->getAuthProvider());
|
|
return;
|
|
}
|
|
|
|
// Create session and find first collection (the default). Prefer
|
|
// read/write collections over read-only, just like getDatabases()
|
|
// does.
|
|
bool isReadOnly;
|
|
m_calendar = Neon::URI();
|
|
SE_LOG_INFO(getDisplayName(), "determine final URL based on %s",
|
|
m_contextSettings ? m_contextSettings->getURLDescription().c_str() : "");
|
|
findCollections(boost::bind(setFirstURL,
|
|
boost::ref(m_calendar),
|
|
boost::ref(isReadOnly),
|
|
_1, _2, _3));
|
|
if (m_calendar.empty()) {
|
|
throwError(SE_HERE, "no database found");
|
|
}
|
|
SE_LOG_INFO(getDisplayName(), "final URL path %s", m_calendar.m_path.c_str());
|
|
|
|
// Check some server capabilities. Purely informational at this
|
|
// point, doesn't have to succeed either (Google 401 throttling
|
|
// workaround not active here, so it may really fail!).
|
|
#ifdef HAVE_LIBNEON_OPTIONS
|
|
if (Logger::instance().getLevel() >= Logger::DEV) {
|
|
try {
|
|
SE_LOG_DEBUG(NULL, "read capabilities of %s", m_calendar.toURL().c_str());
|
|
m_session->startOperation("OPTIONS", Timespec());
|
|
int caps = m_session->options(m_calendar.m_path);
|
|
static const Flag descr[] = {
|
|
{ NE_CAP_DAV_CLASS1, "Class 1 WebDAV (RFC 2518)" },
|
|
{ NE_CAP_DAV_CLASS2, "Class 2 WebDAV (RFC 2518)" },
|
|
{ NE_CAP_DAV_CLASS3, "Class 3 WebDAV (RFC 4918)" },
|
|
{ NE_CAP_MODDAV_EXEC, "mod_dav 'executable' property" },
|
|
{ NE_CAP_DAV_ACL, "WebDAV ACL (RFC 3744)" },
|
|
{ NE_CAP_VER_CONTROL, "DeltaV version-control" },
|
|
{ NE_CAP_CO_IN_PLACE, "DeltaV checkout-in-place" },
|
|
{ NE_CAP_VER_HISTORY, "DeltaV version-history" },
|
|
{ NE_CAP_WORKSPACE, "DeltaV workspace" },
|
|
{ NE_CAP_UPDATE, "DeltaV update" },
|
|
{ NE_CAP_LABEL, "DeltaV label" },
|
|
{ NE_CAP_WORK_RESOURCE, "DeltaV working-resouce" },
|
|
{ NE_CAP_MERGE, "DeltaV merge" },
|
|
{ NE_CAP_BASELINE, "DeltaV baseline" },
|
|
{ NE_CAP_ACTIVITY, "DeltaV activity" },
|
|
{ NE_CAP_VC_COLLECTION, "DeltaV version-controlled-collection" },
|
|
{ 0, NULL }
|
|
};
|
|
SE_LOG_DEBUG(NULL, "%s WebDAV capabilities: %s",
|
|
m_session->getURL().c_str(),
|
|
Flags2String(caps, descr).c_str());
|
|
} catch (const Neon::FatalException &ex) {
|
|
throw;
|
|
} catch (...) {
|
|
Exception::handle();
|
|
}
|
|
}
|
|
#endif // HAVE_LIBNEON_OPTIONS
|
|
}
|
|
|
|
class Candidate {
|
|
public:
|
|
enum Flags {
|
|
LIST = (1u << 0), // Also list all members to find more candidates.
|
|
NONE = 0
|
|
};
|
|
|
|
/** Normalizes url if non-empty, interprets it relative to uri. */
|
|
Candidate(const Neon::URI &uri, const std::string &url, int flags) :
|
|
m_uri(uri),
|
|
m_flags(flags)
|
|
{
|
|
if (url.empty()) {
|
|
m_uri.m_path = "";
|
|
} else {
|
|
// Use normalize path with current host, unless the url contained
|
|
// its own host and protocol.
|
|
Neon::URI other = Neon::URI::parse(url);
|
|
if (other.m_scheme.empty()) {
|
|
other.m_scheme = uri.m_scheme;
|
|
}
|
|
if (!other.m_port) {
|
|
other.m_port = uri.m_port;
|
|
}
|
|
if (other.m_host.empty()) {
|
|
other.m_host = uri.m_host;
|
|
}
|
|
m_uri = other;
|
|
}
|
|
}
|
|
Candidate(const Neon::URI &uri, int flags) :
|
|
m_uri(uri),
|
|
m_flags(flags)
|
|
{}
|
|
Candidate() :
|
|
m_flags(NONE)
|
|
{}
|
|
Neon::URI m_uri;
|
|
int m_flags;
|
|
|
|
// operator bool () const { return !m_path.empty(); }
|
|
bool empty() const { return m_uri.empty(); }
|
|
|
|
bool operator < (const Candidate &other) const {
|
|
int compare = m_uri.compare(other.m_uri);
|
|
return compare < 0 || (compare == 0 && m_flags < other.m_flags);
|
|
}
|
|
|
|
bool operator == (const Candidate &other) const {
|
|
return m_uri == other.m_uri && m_flags == other.m_flags;
|
|
}
|
|
};
|
|
|
|
std::string WebDAVSource::lookupDNSSRV(const std::string &domain)
|
|
{
|
|
std::string url;
|
|
int timeoutSeconds = m_settings->timeoutSeconds();
|
|
int retrySeconds = m_settings->retrySeconds();
|
|
|
|
FILE *in = NULL;
|
|
try {
|
|
Timespec startTime = Timespec::monotonic();
|
|
|
|
retry:
|
|
in = popen(StringPrintf("syncevo-webdav-lookup '%s' '%s'",
|
|
serviceType().c_str(),
|
|
domain.c_str()).c_str(),
|
|
"r");
|
|
if (!in) {
|
|
throwError(SE_HERE, "starting syncevo-webdav-lookup for DNS SRV lookup failed", errno);
|
|
}
|
|
// ridicuously long URLs are truncated...
|
|
char buffer[1024];
|
|
size_t read = fread(buffer, 1, sizeof(buffer) - 1, in);
|
|
buffer[read] = 0;
|
|
if (read > 0 && buffer[read - 1] == '\n') {
|
|
read--;
|
|
}
|
|
buffer[read] = 0;
|
|
url = buffer;
|
|
int res = pclose(in);
|
|
in = NULL;
|
|
if (res != -1 && WIFEXITED(res)) {
|
|
res = WEXITSTATUS(res);
|
|
} else {
|
|
res = -1;
|
|
}
|
|
switch (res) {
|
|
case 0:
|
|
SE_LOG_DEBUG(getDisplayName(), "found syncURL '%s' via DNS SRV", buffer);
|
|
break;
|
|
case 2:
|
|
throwError(SE_HERE, StringPrintf("syncevo-webdav-lookup did not find a DNS utility to search for %s in %s", serviceType().c_str(), domain.c_str()));
|
|
break;
|
|
case 3:
|
|
throwError(SE_HERE, StringPrintf("DNS SRV search for %s in %s did not find the service", serviceType().c_str(), domain.c_str()));
|
|
break;
|
|
case -1:
|
|
throwError(SE_HERE, StringPrintf("DNS SRV search for %s in %s failed", serviceType().c_str(), domain.c_str()));
|
|
break;
|
|
default: {
|
|
Timespec now = Timespec::monotonic();
|
|
if (retrySeconds > 0 &&
|
|
timeoutSeconds > 0) {
|
|
if (now < startTime + timeoutSeconds) {
|
|
SE_LOG_DEBUG(getDisplayName(), "DNS SRV search failed due to network issues, retry in %d seconds",
|
|
retrySeconds);
|
|
Sleep(retrySeconds);
|
|
goto retry;
|
|
} else {
|
|
SE_LOG_INFO(getDisplayName(), "DNS SRV search timed out after %d seconds", timeoutSeconds);
|
|
}
|
|
}
|
|
|
|
// probably network problem
|
|
throwError(SE_HERE, STATUS_TRANSPORT_FAILURE, StringPrintf("DNS SRV search for %s in %s failed", serviceType().c_str(), domain.c_str()));
|
|
break;
|
|
}
|
|
}
|
|
} catch (...) {
|
|
if (in) {
|
|
pclose(in);
|
|
}
|
|
throw;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
bool WebDAVSource::findCollections(const boost::function<bool (const std::string &,
|
|
const Neon::URI &,
|
|
bool isReadOnly)> &storeResult)
|
|
{
|
|
bool res = true; // completed
|
|
int timeoutSeconds = m_settings->timeoutSeconds();
|
|
int retrySeconds = m_settings->retrySeconds();
|
|
SE_LOG_DEBUG(getDisplayName(), "timout %ds, retry %ds => %s",
|
|
timeoutSeconds, retrySeconds,
|
|
(timeoutSeconds <= 0 ||
|
|
retrySeconds <= 0) ? "resending disabled" : "resending allowed");
|
|
|
|
boost::shared_ptr<AuthProvider> authProvider = m_contextSettings->getAuthProvider();
|
|
std::string username = authProvider->getUsername();
|
|
|
|
// If no URL was configured, then try DNS SRV lookup.
|
|
// syncevo-webdav-lookup and at least one of the tools
|
|
// it depends on (host, nslookup, adnshost, ...) must
|
|
// be in the shell search path.
|
|
//
|
|
// Only our own m_contextSettings allows overriding the
|
|
// URL. Not an issue, in practice it is always used.
|
|
bool didDNS = false;
|
|
ContextSettings::URLs urls;
|
|
if (m_contextSettings) {
|
|
urls = m_contextSettings->getURLs();
|
|
} else {
|
|
urls.push_back(m_settings->getURL());
|
|
}
|
|
if ((urls.empty() || (urls.size() == 1 && urls.front().empty())) && m_contextSettings) {
|
|
didDNS = true;
|
|
size_t pos = username.find('@');
|
|
if (pos == username.npos) {
|
|
// throw authentication error to indicate that the credentials are wrong
|
|
throwError(SE_HERE, STATUS_UNAUTHORIZED, StringPrintf("syncURL not configured and username %s does not contain a domain", username.c_str()));
|
|
}
|
|
std::string domain = username.substr(pos + 1);
|
|
std::string url = lookupDNSSRV(domain);
|
|
urls.clear();
|
|
urls.push_back(url);
|
|
m_contextSettings->setURLs(urls,
|
|
StringPrintf("DNS SRV URL for domain %s and service %s",
|
|
domain.c_str(),
|
|
serviceType().c_str()));
|
|
}
|
|
|
|
// start talking to host defined by m_settings->getURL()
|
|
m_session = Neon::Session::create(m_settings);
|
|
SE_LOG_INFO(getDisplayName(), "start database search at %s%s%s",
|
|
m_settings->getURL().c_str(),
|
|
m_contextSettings ? ", from " : "",
|
|
m_contextSettings ? m_contextSettings->getURLDescription().c_str() : "");
|
|
|
|
// Find default calendar. Same for address book, with slightly
|
|
// different parameters.
|
|
//
|
|
// Stops when:
|
|
// - current path is calendar collection (= contains VEVENTs)
|
|
// Gives up:
|
|
// - when running in circles
|
|
// - nothing else to try out
|
|
// - tried 10 times
|
|
// Follows:
|
|
// - current-user-principal
|
|
// - CalDAV calendar-home-set
|
|
// - collections
|
|
//
|
|
// TODO: support more than one calendar. Instead of stopping at the first one,
|
|
// scan more throroughly, then decide deterministically.
|
|
int counter = 0;
|
|
const int limit = 1000;
|
|
// Keeps track of paths to look at and those which were already
|
|
// tested. What is done for each candidate varies.
|
|
class Tried : public std::set<Candidate> {
|
|
std::list<Candidate> m_candidates;
|
|
bool m_found;
|
|
public:
|
|
Tried() : m_found(false) {}
|
|
|
|
/** Was path not tested yet and is not already a candidate? */
|
|
bool isNew(const Candidate &candidate) {
|
|
return !candidate.empty() && find(candidate) == end() &&
|
|
std::find(m_candidates.begin(), m_candidates.end(), candidate) == m_candidates.end();
|
|
}
|
|
|
|
/** Hand over next candidate to caller, empty if none available. */
|
|
Candidate getNextCandidate() {
|
|
if (!m_candidates.empty() ) {
|
|
Candidate candidate = m_candidates.front();
|
|
m_candidates.pop_front();
|
|
return candidate;
|
|
} else {
|
|
return Candidate();
|
|
}
|
|
}
|
|
|
|
/** remember that path was tested */
|
|
void insert(const Candidate &candidate) {
|
|
std::set<Candidate>::insert(candidate);
|
|
m_candidates.remove(candidate);
|
|
}
|
|
enum Position {
|
|
FRONT,
|
|
BACK
|
|
};
|
|
void addCandidate(const Candidate &candidate, Position position) {
|
|
if (isNew(candidate)) {
|
|
if (position == FRONT) {
|
|
m_candidates.push_front(candidate);
|
|
} else {
|
|
m_candidates.push_back(candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
void foundResult() { m_found = true; }
|
|
|
|
/** Nothing left to try and nothing found => bail out with error for last candidate. */
|
|
bool errorIsFatal() { return m_candidates.empty() && !m_found; }
|
|
} tried;
|
|
|
|
// Populate URLs to be scanned with configured URLs.
|
|
BOOST_FOREACH(const std::string &url, urls) {
|
|
Neon::URI uri = Neon::URI::parse(url);
|
|
// Avoid listing members for the initial URLs. If the user gave us
|
|
// the root of a generic WebDAV server, a recursive listing of
|
|
// all resource collections on it will take too long. We only
|
|
// list the home sets.
|
|
Candidate candidate(Neon::URI::parse(url), Candidate::NONE);
|
|
tried.addCandidate(candidate, Tried::BACK);
|
|
|
|
// Add well-known URL as fallback to be tried if configured
|
|
// path was empty. eGroupware also replies with a redirect for the
|
|
// empty path, but relying on that alone is risky because it isn't
|
|
// specified.
|
|
if (candidate.m_uri.m_path.empty() || candidate.m_uri.m_path == "/") {
|
|
std::string wellknown = wellKnownURL();
|
|
if (!wellknown.empty()) {
|
|
tried.addCandidate(Candidate(candidate.m_uri, wellknown, Candidate::NONE), Tried::BACK);
|
|
}
|
|
}
|
|
}
|
|
|
|
Candidate candidate = tried.getNextCandidate();
|
|
Props_t davProps;
|
|
Neon::Session::PropfindPropCallback_t callback =
|
|
boost::bind(&WebDAVSource::openPropCallback,
|
|
this, boost::ref(davProps), _1, _2, _3, _4);
|
|
|
|
// With Yahoo! the initial connection often failed with 50x
|
|
// errors. Retrying individual requests is error prone because at
|
|
// least one (asking for .well-known/[caldav|carddav]) always
|
|
// results in 502. Let the PROPFIND requests be resent, but in
|
|
// such a way that the overall discovery will never take longer
|
|
// than the total configured timeout period.
|
|
//
|
|
// The PROPFIND with openPropCallback is idempotent, because it
|
|
// will just overwrite previously found information in davProps.
|
|
// Therefore resending is okay.
|
|
Timespec finalDeadline = createDeadline(); // no resending if left empty
|
|
|
|
// Remember whether we have found the home set. If we do not come
|
|
// across it as part of the regular search, then we need to search
|
|
// a bit harder for it.
|
|
bool haveHomeSet = false;
|
|
|
|
// Remember whether we have results for https://apidata.googleusercontent.com:443/caldav/v2.
|
|
bool haveGoogleCalDAV2 = false;
|
|
|
|
while (true) {
|
|
bool usernameInserted = false;
|
|
Candidate next;
|
|
|
|
// Replace %u with the username, if the %u is found. Also, keep track
|
|
// of this event happening, because if we later on get a 404 error,
|
|
// we will convert it to 401 only if the path contains the username
|
|
// and it was indeed us who put the username there (not the server).
|
|
if (boost::find_first(candidate.m_uri.m_path, "%u")) {
|
|
boost::replace_all(candidate.m_uri.m_path, "%u", Neon::URI::escape(username));
|
|
usernameInserted = true;
|
|
}
|
|
|
|
tried.insert(candidate);
|
|
SE_LOG_DEBUG(NULL, "testing %s", candidate.m_uri.toURL().c_str());
|
|
Neon::URI currentURI = m_session->getURI();
|
|
Neon::URI &newURI = candidate.m_uri;
|
|
bool success = false;
|
|
bool isWellKnown = boost::starts_with(candidate.m_uri.m_path, "/.well-known/");
|
|
|
|
// Special hack Google: if we already have results for the current CalDAV
|
|
// endpoint, then don't try the legacy one.
|
|
if (newURI.m_host == "www.google.com" &&
|
|
(boost::starts_with(newURI.m_path, "/calendar/dav/") || newURI.m_path =="/calendar/dav") &&
|
|
haveGoogleCalDAV2) {
|
|
SE_LOG_DEBUG(getDisplayName(), "skipping legacy Google CalDAV");
|
|
goto next;
|
|
}
|
|
|
|
// Accessing the well-known URIs should lead to a redirect, but
|
|
// with Yahoo! Calendar all I got was a 502 "connection refused".
|
|
// Yahoo! Contacts also doesn't redirect. Instead on ends with
|
|
// a Principal resource - perhaps reading that would lead further.
|
|
//
|
|
// So anyway, let's try the well-known URI first, but also add
|
|
// the root path as fallback.
|
|
if (candidate.m_uri.m_path == "/.well-known/caldav/" ||
|
|
candidate.m_uri.m_path == "/.well-known/carddav/") {
|
|
// remove trailing slash added by normalization, to be aligned with draft-daboo-srv-caldav-10
|
|
candidate.m_uri.m_path.resize(candidate.m_uri.m_path.size() - 1);
|
|
|
|
// Yahoo! Calendar returns no redirect. According to rfc4918 appendix-E,
|
|
// a client may simply try the root path in case of such a failure,
|
|
// which happens to work for Yahoo.
|
|
tried.addCandidate(Candidate(currentURI, "/", Candidate::NONE), Tried::BACK);
|
|
// TODO: Google Calendar, with workarounds
|
|
// candidates.push_back(StringPrintf("/calendar/dav/%s/user/", Neon::URI::escape(username).c_str()));
|
|
}
|
|
|
|
try {
|
|
if (newURI.m_scheme != currentURI.m_scheme ||
|
|
newURI.m_host != currentURI.m_host ||
|
|
newURI.getPort() != currentURI.getPort()) {
|
|
// Need to re-initialize the session.
|
|
if (m_contextSettings) {
|
|
SE_LOG_DEBUG(getDisplayName(), "switching HTTP session from %s to %s",
|
|
currentURI.toURL().c_str(),
|
|
newURI.toURL().c_str());
|
|
m_contextSettings->setURL(newURI.toURL(),
|
|
"redirect during database scan");
|
|
} else {
|
|
SE_THROW(StringPrintf("switching HTTP session from %s to %s not possible at the moment",
|
|
currentURI.toURL().c_str(),
|
|
newURI.toURL().c_str()));
|
|
}
|
|
m_session = Neon::Session::create(m_settings);
|
|
}
|
|
currentURI = newURI;
|
|
|
|
// disable resending for some known cases where it never succeeds
|
|
Timespec deadline = finalDeadline;
|
|
if (isWellKnown &&
|
|
m_settings->getURL().find("yahoo.com") != string::npos) {
|
|
deadline = Timespec();
|
|
}
|
|
|
|
if (Logger::instance().getLevel() >= Logger::DEV) {
|
|
// First dump WebDAV "allprops" properties (does not contain
|
|
// properties which must be asked for explicitly!). Only
|
|
// relevant for debugging.
|
|
try {
|
|
SE_LOG_DEBUG(NULL, "debugging: read all WebDAV properties of %s", candidate.m_uri.toURL().c_str());
|
|
// Use OAuth2, if available.
|
|
boost::shared_ptr<AuthProvider> authProvider = m_settings->getAuthProvider();
|
|
if (authProvider->methodIsSupported(AuthProvider::AUTH_METHOD_OAUTH2)) {
|
|
m_session->forceAuthorization(authProvider);
|
|
}
|
|
Neon::Session::PropfindPropCallback_t callback =
|
|
boost::bind(&WebDAVSource::openPropCallback,
|
|
this, boost::ref(davProps), _1, _2, _3, _4);
|
|
m_session->propfindProp(candidate.m_uri.m_path, 0, NULL, callback, Timespec());
|
|
} catch (const Neon::FatalException &ex) {
|
|
throw;
|
|
} catch (...) {
|
|
handleException(HANDLE_EXCEPTION_NO_ERROR);
|
|
}
|
|
}
|
|
|
|
// Now ask for some specific properties of interest for us.
|
|
// Using CALDAV:allprop would be nice, but doesn't seem to
|
|
// be possible with Neon.
|
|
//
|
|
// The "current-user-principal" is particularly relevant,
|
|
// because it leads us from
|
|
// "/.well-known/[carddav/caldav]" (or whatever that
|
|
// redirected to) to the current user and its
|
|
// "[calendar/addressbook]-home-set".
|
|
//
|
|
// Apple Calendar Server only returns that information if
|
|
// we force authorization to be used. Otherwise it returns
|
|
// <current-user-principal>
|
|
// <unauthenticated/>
|
|
// </current-user-principal>
|
|
//
|
|
// We send valid credentials here, using Basic authorization,
|
|
// if configured to use credentials instead of something like OAuth2.
|
|
// The rationale is that this cuts down on the number of
|
|
// requests for https while still being secure. For
|
|
// http, our Neon wrapper is smart enough to ignore our request.
|
|
//
|
|
// See also:
|
|
// http://tools.ietf.org/html/rfc4918#appendix-E
|
|
// http://lists.w3.org/Archives/Public/w3c-dist-auth/2005OctDec/0243.html
|
|
// http://thread.gmane.org/gmane.comp.web.webdav.neon.general/717/focus=719
|
|
m_session->forceAuthorization(m_settings->getAuthProvider());
|
|
davProps.clear();
|
|
// Avoid asking for CardDAV properties when only using CalDAV
|
|
// and vice versa, to avoid breaking both when the server is only
|
|
// broken for one of them (like Google, which (temporarily?) sent
|
|
// invalid CardDAV properties).
|
|
static const ne_propname caldav[] = {
|
|
// WebDAV ACL
|
|
{ "DAV:", "alternate-URI-set" },
|
|
{ "DAV:", "principal-URL" },
|
|
{ "DAV:", "current-user-principal" },
|
|
{ "DAV:", "group-member-set" },
|
|
{ "DAV:", "group-membership" },
|
|
{ "DAV:", "displayname" },
|
|
{ "DAV:", "resourcetype" },
|
|
// CalDAV
|
|
{ "urn:ietf:params:xml:ns:caldav", "calendar-home-set" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "calendar-description" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "calendar-timezone" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "supported-calendar-component-set" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "supported-calendar-data" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "max-resource-size" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "min-date-time" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "max-date-time" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "max-instances" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "max-attendees-per-instance" },
|
|
// ACL, http://www.ietf.org/rfc/rfc3744.txt
|
|
{ "DAV:", "current-user-privilege-set" },
|
|
{ NULL, NULL }
|
|
};
|
|
static const ne_propname carddav[] = {
|
|
// WebDAV ACL
|
|
{ "DAV:", "alternate-URI-set" },
|
|
{ "DAV:", "principal-URL" },
|
|
{ "DAV:", "current-user-principal" },
|
|
{ "DAV:", "group-member-set" },
|
|
{ "DAV:", "group-membership" },
|
|
{ "DAV:", "displayname" },
|
|
{ "DAV:", "resourcetype" },
|
|
// CardDAV
|
|
{ "urn:ietf:params:xml:ns:carddav", "addressbook-home-set" },
|
|
{ "urn:ietf:params:xml:ns:carddav", "principal-address" },
|
|
{ "urn:ietf:params:xml:ns:carddav", "addressbook-description" },
|
|
{ "urn:ietf:params:xml:ns:carddav", "supported-address-data" },
|
|
{ "urn:ietf:params:xml:ns:carddav", "max-resource-size" },
|
|
// ACL, http://www.ietf.org/rfc/rfc3744.txt
|
|
{ "DAV:", "current-user-privilege-set" },
|
|
{ NULL, NULL }
|
|
};
|
|
SE_LOG_DEBUG(NULL, "read relevant properties of %s", candidate.m_uri.toURL().c_str());
|
|
m_session->propfindProp(candidate.m_uri.m_path, 0,
|
|
getContent() == "VCARD" ? carddav : caldav,
|
|
callback, deadline);
|
|
success = true;
|
|
} catch (const Neon::FatalException &ex) {
|
|
throw;
|
|
} catch (const Neon::RedirectException &ex) {
|
|
// follow to new location
|
|
Neon::URI next = Neon::URI::parse(ex.getLocation(), true);
|
|
// keep old host + scheme + port if not set in next location
|
|
if (next.m_scheme.empty()) {
|
|
next.m_scheme = currentURI.m_scheme;
|
|
}
|
|
if (next.m_host.empty()) {
|
|
next.m_host = currentURI.m_host;
|
|
}
|
|
if (!next.m_port) {
|
|
next.m_port = currentURI.m_port;
|
|
}
|
|
Candidate nextCandidate(next, candidate.m_flags);
|
|
if (tried.isNew(nextCandidate)) {
|
|
SE_LOG_DEBUG(NULL, "new candidate from %s -> %s redirect",
|
|
currentURI.toURL().c_str(),
|
|
next.toURL().c_str());
|
|
tried.addCandidate(nextCandidate, Tried::FRONT);
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, "already known candidate from %s -> %s redirect",
|
|
currentURI.toURL().c_str(),
|
|
next.toURL().c_str());
|
|
}
|
|
} catch (const TransportStatusException &ex) {
|
|
SE_LOG_DEBUG(NULL, "TransportStatusException: %s", ex.what());
|
|
if (ex.syncMLStatus() == 404 && boost::find_first(candidate.m_uri.m_path, username) && usernameInserted) {
|
|
// We're actually looking at an authentication error: the path to the calendar has
|
|
// not been found, so the username was wrong. Let's hijack the error message and
|
|
// code of the exception by throwing a new one.
|
|
string descr = StringPrintf("Path not found: %s. Is the username '%s' correct?",
|
|
candidate.m_uri.toURL().c_str(), username.c_str());
|
|
int code = 401;
|
|
SE_THROW_EXCEPTION_STATUS(TransportStatusException, descr, SyncMLStatus(code));
|
|
} else if (isWellKnown && !didDNS) {
|
|
// The server doesn't have the .well-known redirect that we were looking for.
|
|
// We might be able to find the right server via DNS SRV lookup instead.
|
|
// Happens with [www].icloud.com, for which DNS SRV points to
|
|
// https://[caldav|carddav].icloud.com:443/.well-known/[caldav|carddav]
|
|
std::string domain = m_session->getURI().m_host;
|
|
std::string wwwdomain;
|
|
static const char WWW[] = "www.";
|
|
if (boost::starts_with(domain, WWW)) {
|
|
wwwdomain = domain;
|
|
domain.erase(0, sizeof(WWW) - 1);
|
|
}
|
|
std::string url;
|
|
didDNS = true;
|
|
try {
|
|
SE_LOG_DEBUG(getDisplayName(), "try DNS SRV lookup after .well-known failed: %s", domain.c_str());
|
|
url = lookupDNSSRV(domain);
|
|
} catch (const Exception &ex) {
|
|
if (!wwwdomain.empty()) {
|
|
SE_LOG_DEBUG(getDisplayName(), "try DNS SRV lookup with www prefix: %s", wwwdomain.c_str());
|
|
url = lookupDNSSRV(wwwdomain);
|
|
} else if (tried.errorIsFatal()) {
|
|
throw;
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, "ignore error for DNS SRV fallback: %s", ex.what());
|
|
}
|
|
}
|
|
if (!url.empty()) {
|
|
Neon::URI uri = Neon::URI::parse(url);
|
|
Candidate dnsCandidate(uri, Candidate::NONE);
|
|
if (tried.isNew(dnsCandidate)) {
|
|
tried.addCandidate(dnsCandidate, Tried::FRONT);
|
|
SE_LOG_DEBUG(getDisplayName(), "new candidate from DNS SRV lookup: %s", uri.toURL().c_str());
|
|
}
|
|
}
|
|
} else {
|
|
if (tried.errorIsFatal()) {
|
|
throw;
|
|
} else {
|
|
// ignore the error (whatever it was!), try next
|
|
// candidate; needed to handle 502 "Connection
|
|
// refused" for /.well-known/caldav/ from Yahoo!
|
|
// Calendar
|
|
SE_LOG_DEBUG(NULL, "ignore error for URI candidate: %s", ex.what());
|
|
}
|
|
}
|
|
} catch (const Exception &ex) {
|
|
if (tried.errorIsFatal()) {
|
|
throw;
|
|
} else {
|
|
// ignore the error (whatever it was!), try next
|
|
// candidate; needed to handle 502 "Connection
|
|
// refused" for /.well-known/caldav/ from Yahoo!
|
|
// Calendar
|
|
SE_LOG_DEBUG(NULL, "ignore error for URI candidate: %s", ex.what());
|
|
}
|
|
}
|
|
|
|
if (success) {
|
|
Props_t::iterator pathProps = davProps.find(candidate.m_uri.m_path);
|
|
if (pathProps == davProps.end()) {
|
|
// No reply for requested path? Happens with Yahoo Calendar server,
|
|
// which returns information about "/dav" when asked about "/".
|
|
// Move to that path.
|
|
if (!davProps.empty()) {
|
|
pathProps = davProps.begin();
|
|
string newpath = pathProps->first;
|
|
SE_LOG_DEBUG(NULL, "use properties for '%s' instead of '%s'",
|
|
newpath.c_str(), candidate.m_uri.toURL().c_str());
|
|
candidate.m_uri.m_path = newpath;
|
|
}
|
|
}
|
|
StringMap *props = pathProps == davProps.end() ? NULL : &pathProps->second;
|
|
bool isResult = false;
|
|
std::string type;
|
|
if (props) {
|
|
type = (*props)["DAV::resourcetype"];
|
|
}
|
|
bool isCollection = type.find("<DAV:collection></DAV:collection>") != type.npos;
|
|
if (isCollection && props && isLeafCollection(*props) && typeMatches(*props)) {
|
|
isResult = true;
|
|
StringMap::const_iterator it;
|
|
|
|
// TODO: filter out CalDAV collections which do
|
|
// not contain the right components
|
|
// (urn:ietf:params:xml:ns:caldav:supported-calendar-component-set)
|
|
|
|
// found something
|
|
tried.foundResult();
|
|
it = props->find("DAV::displayname");
|
|
Neon::URI uri = m_session->getURI();
|
|
uri.m_path = candidate.m_uri.m_path;
|
|
std::string name;
|
|
if (it != props->end()) {
|
|
name = it->second;
|
|
}
|
|
|
|
// Might be read-only. Assume it is read/write unless we
|
|
// find the opposite.
|
|
bool isReadOnly = false;
|
|
it = props->find("DAV::current-user-privilege-set");
|
|
if (it != props->end()) {
|
|
const std::string &priviliges = it->second;
|
|
SE_LOG_DEBUG(NULL, "current-user-privilege-set: %s", priviliges.c_str());
|
|
// Be careful here: parsing XML with string operations is fragile,
|
|
// so don't go to read-only mode if we don't find DAV::read.
|
|
// Also beware of the double vs. single colon oddity from libneon.
|
|
if ((priviliges.find("DAV::write") == priviliges.npos &&
|
|
priviliges.find("DAV::read") != priviliges.npos) ||
|
|
(priviliges.find("DAV:write") == priviliges.npos &&
|
|
priviliges.find("DAV:read") != priviliges.npos)) {
|
|
isReadOnly = true;
|
|
}
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, "no current-user-privilege-set, assume read/write");
|
|
}
|
|
|
|
SE_LOG_DEBUG(NULL, "found %s = %s",
|
|
name.c_str(),
|
|
uri.toURL().c_str());
|
|
if (uri.m_host == "apidata.googleusercontent.com" &&
|
|
boost::starts_with(uri.m_path, "/caldav/v2/")) {
|
|
haveGoogleCalDAV2 = true;
|
|
}
|
|
res = storeResult(name,
|
|
uri,
|
|
isReadOnly);
|
|
if (!res) {
|
|
// done
|
|
break;
|
|
}
|
|
}
|
|
|
|
// find next path:
|
|
// prefer CardDAV:calendar-home-set or CalDAV:addressbook-home-set
|
|
std::list<std::string> homes;
|
|
if (props) {
|
|
homes = extractHREFs((*props)[homeSetProp()]);
|
|
}
|
|
BOOST_FOREACH(const std::string &home, homes) {
|
|
// The home set is a collection of collections, so it
|
|
// cannot be the collection we look for. But it contains them,
|
|
// so we must list its content.
|
|
Candidate homeCandidate(m_session->getURI(), home, Candidate::LIST);
|
|
if (tried.isNew(homeCandidate)) {
|
|
haveHomeSet = true;
|
|
if (next.empty()) {
|
|
// Follow it directly before any other
|
|
// candidates because the home set is most
|
|
// likely to contain the default collection.
|
|
SE_LOG_DEBUG(NULL, "follow home-set property to %s", homeCandidate.m_uri.toURL().c_str());
|
|
next = homeCandidate;
|
|
} else {
|
|
SE_LOG_DEBUG(NULL, "new candidate from home-set property %s", home.c_str());
|
|
tried.addCandidate(homeCandidate, Tried::FRONT);
|
|
}
|
|
}
|
|
}
|
|
// alternatively, follow principal URL
|
|
if (next.empty()) {
|
|
Candidate principal(m_session->getURI(),
|
|
props ? extractHREF((*props)["DAV::current-user-principal"]) : "",
|
|
Candidate::NONE);
|
|
|
|
// TODO:
|
|
// xmlns:d="DAV:"
|
|
// <d:current-user-principal><d:href>/m8/carddav/principals/__uids__/patrick.ohly@googlemail.com/</d:href></d:current-user-principal>
|
|
if (tried.isNew(principal)) {
|
|
next = principal;
|
|
SE_LOG_DEBUG(NULL, "follow current-user-prinicipal to %s", next.m_uri.toURL().c_str());
|
|
}
|
|
}
|
|
|
|
if (isResult && next.empty() && !haveHomeSet) {
|
|
// We found a valid collection without having seen the
|
|
// home set, and the meta data of the collection does
|
|
// not point us to the principal or the home set.
|
|
//
|
|
// Happens with Google CaldDAV, causing us to not find
|
|
// other calendars if scan started at the default
|
|
// calendar. As a workaround, walk up the uri and check
|
|
// them for meta data.
|
|
std::string path = candidate.m_uri.m_path;
|
|
size_t pos;
|
|
while ((pos = path.rfind('/')) != path.npos) {
|
|
path.resize(pos);
|
|
Candidate parent(m_session->getURI(), path.empty() ? "/" : path, Candidate::NONE);
|
|
if (tried.isNew(parent)) {
|
|
SE_LOG_DEBUG(NULL, "check parent %s", parent.m_uri.toURL().c_str());
|
|
tried.addCandidate(parent, Tried::BACK);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, recursively descend into some collections.
|
|
if (isCollection) {
|
|
if (props && isLeafCollection(*props)) {
|
|
// The goal here was to prevent diving into collections which are
|
|
// known to not contain other relevant collections.
|
|
SE_LOG_DEBUG(NULL, "skipping listing because collection cannot contain other relevant collections: %s", candidate.m_uri.toURL().c_str());
|
|
} else if (!(candidate.m_flags & Candidate::LIST)) {
|
|
SE_LOG_DEBUG(NULL, "skipping listing because we don't know whether collection contains relevant collections: %s", candidate.m_uri.toURL().c_str());
|
|
} else {
|
|
// List members and find new candidates.
|
|
// Yahoo! Calendar does not return resources contained in /dav/<user>/Calendar/
|
|
// if <allprops> is used. Properties must be requested explicitly.
|
|
SE_LOG_DEBUG(NULL, "list items in %s", candidate.m_uri.toURL().c_str());
|
|
// See findCollections() for the reason why we are not mixing CalDAV and CardDAV
|
|
// properties.
|
|
static const ne_propname caldav[] = {
|
|
{ "DAV:", "displayname" },
|
|
{ "DAV:", "resourcetype" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "calendar-home-set" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "calendar-description" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "calendar-timezone" },
|
|
{ "urn:ietf:params:xml:ns:caldav", "supported-calendar-component-set" },
|
|
{ NULL, NULL }
|
|
};
|
|
static const ne_propname carddav[] = {
|
|
{ "DAV:", "displayname" },
|
|
{ "DAV:", "resourcetype" },
|
|
{ "urn:ietf:params:xml:ns:carddav", "addressbook-home-set" },
|
|
{ "urn:ietf:params:xml:ns:carddav", "addressbook-description" },
|
|
{ "urn:ietf:params:xml:ns:carddav", "supported-address-data" },
|
|
{ NULL, NULL }
|
|
};
|
|
davProps.clear();
|
|
m_session->propfindProp(candidate.m_uri.m_path, 1,
|
|
getContent() == "VCARD" ? carddav : caldav,
|
|
callback, finalDeadline);
|
|
|
|
// Also list recursively. The home set may be an
|
|
// "ordinary collection that has child or
|
|
// descendant calendar collections owned by the
|
|
// principal" (RFC 4791).
|
|
int subFlags = Candidate::LIST;
|
|
BOOST_FOREACH(Props_t::value_type &entry, davProps) {
|
|
const std::string &sub = entry.first;
|
|
const std::string &subType = entry.second["DAV::resourcetype"];
|
|
Candidate subCandidate(m_session->getURI(), sub, subFlags);
|
|
// new candidates are:
|
|
// - untested
|
|
// - not already a candidate
|
|
// - a resource, but not the CalDAV schedule-inbox/outbox
|
|
// - not shared ("global-addressbook" in Apple Calendar Server),
|
|
// because these are unlikely to be the right "default" collection
|
|
//
|
|
// Trying to prune away collections here which are not of the
|
|
// right type *and* cannot contain collections of the right
|
|
// type (example: Apple Calendar Server "inbox" under
|
|
// calendar-home-set URL with type "CALDAV:schedule-inbox") requires
|
|
// knowledge not current provided by derived classes. TODO (?).
|
|
if (!tried.isNew(subCandidate)) {
|
|
SE_LOG_DEBUG(NULL, "skipping because already checked: %s", sub.c_str());
|
|
} else if (subType.find("<DAV:collection></DAV:collection>") == subType.npos ||
|
|
subType.find("<urn:ietf:params:xml:ns:caldavschedule-") != subType.npos) {
|
|
SE_LOG_DEBUG(NULL, "skipping because of wrong resourcetype: %s\n%s",
|
|
sub.c_str(),
|
|
subType.c_str());
|
|
#if 0
|
|
// Do not ignore shared collections. We might have read-write
|
|
// access (for example, Google marks additional calendars as
|
|
// 'shared').
|
|
} else if (subType.find("<http://calendarserver.org/ns/shared") != subType.npos) {
|
|
SE_LOG_DEBUG(NULL, "skipping because it is shared: %s", sub.c_str());
|
|
#endif
|
|
} else if (!typeMatches(entry.second)) {
|
|
SE_LOG_DEBUG(NULL, "skipping because of wrong type: %s", sub.c_str());
|
|
} else {
|
|
Candidate subCandidate(m_session->getURI(), sub, subFlags);
|
|
if (tried.isNew(subCandidate)) {
|
|
tried.addCandidate(subCandidate, Tried::BACK);
|
|
SE_LOG_DEBUG(NULL, "new sub candidate: %s", sub.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
next:
|
|
if (next.empty()) {
|
|
// use next untried candidate
|
|
next = tried.getNextCandidate();
|
|
if (next.empty()) {
|
|
// done searching
|
|
break;
|
|
}
|
|
SE_LOG_DEBUG(NULL, "follow candidate %s", next.m_uri.toURL().c_str());
|
|
}
|
|
|
|
counter++;
|
|
if (counter > limit) {
|
|
throwError(SE_HERE, StringPrintf("giving up search for collection after %d attempts", limit));
|
|
}
|
|
candidate = next;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
std::string WebDAVSource::extractHREF(const std::string &propval)
|
|
{
|
|
// all additional parameters after opening resp. closing tag
|
|
static const std::string hrefStart = "<DAV:href";
|
|
static const std::string hrefEnd = "</DAV:href";
|
|
size_t start = propval.find(hrefStart);
|
|
start = propval.find('>', start);
|
|
if (start != propval.npos) {
|
|
start++;
|
|
size_t end = propval.find(hrefEnd, start);
|
|
if (end != propval.npos) {
|
|
return propval.substr(start, end - start);
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::list<std::string> WebDAVSource::extractHREFs(const std::string &propval)
|
|
{
|
|
std::list<std::string> res;
|
|
|
|
// all additional parameters after opening resp. closing tag
|
|
static const std::string hrefStart = "<DAV:href";
|
|
static const std::string hrefEnd = "</DAV:href";
|
|
size_t current = 0;
|
|
while (current < propval.size()) {
|
|
size_t start = propval.find(hrefStart, current);
|
|
start = propval.find('>', start);
|
|
if (start != propval.npos) {
|
|
start++;
|
|
size_t end = propval.find(hrefEnd, start);
|
|
if (end != propval.npos) {
|
|
res.push_back(propval.substr(start, end - start));
|
|
current = end;
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
void WebDAVSource::openPropCallback(Props_t &davProps,
|
|
const Neon::URI &uri,
|
|
const ne_propname *prop,
|
|
const char *value,
|
|
const ne_status *status)
|
|
{
|
|
// TODO: recognize CALDAV:calendar-timezone and use it for local time conversion of events
|
|
std::string name;
|
|
if (prop->nspace) {
|
|
name = prop->nspace;
|
|
}
|
|
name += ":";
|
|
name += prop->name;
|
|
if (value) {
|
|
davProps[uri.m_path][name] = value;
|
|
boost::trim_if(davProps[uri.m_path][name],
|
|
boost::is_space());
|
|
}
|
|
}
|
|
|
|
bool WebDAVSource::isEmpty()
|
|
{
|
|
contactServer();
|
|
|
|
// listing all items is relatively efficient, let's use that
|
|
// TODO: use truncated result search
|
|
RevisionMap_t revisions;
|
|
listAllItems(revisions);
|
|
return revisions.empty();
|
|
}
|
|
|
|
void WebDAVSource::close()
|
|
{
|
|
m_session.reset();
|
|
}
|
|
|
|
static bool storeCollection(SyncSource::Databases &result,
|
|
const std::string &name,
|
|
const Neon::URI &uri,
|
|
bool isReadOnly)
|
|
{
|
|
std::string url = uri.toURL();
|
|
|
|
// avoid duplicates
|
|
BOOST_FOREACH(const SyncSource::Database &entry, result) {
|
|
if (entry.m_uri == url) {
|
|
// already found before
|
|
return true;
|
|
}
|
|
}
|
|
|
|
result.push_back(SyncSource::Database(name, url, false, isReadOnly));
|
|
return true;
|
|
}
|
|
|
|
WebDAVSource::Databases WebDAVSource::getDatabases()
|
|
{
|
|
Databases result;
|
|
|
|
// do a scan if some kind of credentials were set
|
|
if (m_contextSettings->getAuthProvider()->wasConfigured()) {
|
|
findCollections(boost::bind(storeCollection,
|
|
boost::ref(result),
|
|
_1, _2, _3));
|
|
|
|
// Move all read-only collections to the end of the array.
|
|
// They are probably not the default calendar (for example,
|
|
// with ownCloud we find a read-only "Birthday Calendar"
|
|
// before the "Default Calendar").
|
|
//
|
|
// WebDAVSource::contactServer() does the same.
|
|
size_t e = result.size(), i = 0;
|
|
while (i < e) {
|
|
if (result[i].m_isReadOnly) {
|
|
// Move to end.
|
|
result.push_back(result[i]);
|
|
// Remove at current position.
|
|
result.erase(result.begin() + i);
|
|
// Check that position again and ignore the already
|
|
// checked entry at the end.
|
|
e--;
|
|
} else {
|
|
// Next position.
|
|
i++;
|
|
}
|
|
}
|
|
|
|
if (!result.empty()) {
|
|
result.front().m_isDefault = true;
|
|
}
|
|
} else {
|
|
result.push_back(Database("select database via absolute URL, set username/password to scan, set syncURL to base URL if server does not support auto-discovery",
|
|
"<path>"));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void WebDAVSource::getSynthesisInfo(SynthesisInfo &info,
|
|
XMLConfigFragments &fragments)
|
|
{
|
|
contactServer();
|
|
|
|
TrackingSyncSource::getSynthesisInfo(info, fragments);
|
|
|
|
// only CalDAV enforces unique UID
|
|
std::string content = getContent();
|
|
if (content == "VEVENT" || content == "VTODO" || content == "VJOURNAL") {
|
|
info.m_globalIDs = true;
|
|
}
|
|
if (content == "VEVENT") {
|
|
info.m_backendRule = "HAVE-SYNCEVOLUTION-EXDATE-DETACHED";
|
|
} else if (content == "VCARD") {
|
|
// Assume that a CardDAV server has and preserves UID values.
|
|
info.m_backendRule = "CARDDAV";
|
|
fragments.m_remoterules["CARDDAV"] =
|
|
" <remoterule name='CARDDAV'>\n"
|
|
" <deviceid>none</deviceid>\n"
|
|
" <noemptyproperties>yes</noemptyproperties>\n"
|
|
" <include rule='HAVE-EVOLUTION-UI-SLOT'/>\n"
|
|
" <include rule='HAVE-EVOLUTION-UI-SLOT-IN-IMPP'/>\n"
|
|
" <include rule='HAVE-VCARD-UID'/>\n"
|
|
" <include rule='HAVE-ABLABEL-PROPERTY'/>\n"
|
|
" </remoterule>";
|
|
// Assume that a CardDAV server uses IMPP (RFC 4770) and
|
|
// Apple Address book (X-AB) extensions. Convert to the traditional,
|
|
// internal fields (ANNIVERSARY, JABBER, etc.) after reading
|
|
// from a CardDAV server and from the traditional fields
|
|
// before writing.
|
|
info.m_beforeWriteScript = "$VCARD_BEFOREWRITE_SCRIPT_WEBDAV;\n";
|
|
info.m_afterReadScript = "$VCARD_AFTERREAD_SCRIPT_WEBDAV;\n";
|
|
}
|
|
|
|
// TODO: instead of identifying the peer based on the
|
|
// session URI, use some information gathered about
|
|
// it during contactServer()
|
|
if (m_session) {
|
|
string host = m_session->getURI().m_host;
|
|
if (host.find("google") != host.npos) {
|
|
info.m_backendRule = "GOOGLE";
|
|
// Same as CARDDAV above, minus HAVE-EVOLUTION-UI-SLOT-IN-IMPP.
|
|
// Sending IMPP;X-SERVICE-TYPE=..;X-EVOLUTION-UI-SLOT=
|
|
// causes Google to ignore X-SERVICE-TYPE.
|
|
fragments.m_remoterules["GOOGLE"] =
|
|
" <remoterule name='GOOGLE'>\n"
|
|
" <deviceid>none</deviceid>\n"
|
|
" <noemptyproperties>yes</noemptyproperties>\n"
|
|
" <include rule='HAVE-EVOLUTION-UI-SLOT'/>\n"
|
|
// " <include rule='HAVE-EVOLUTION-UI-SLOT-IN-IMPP'/>\n"
|
|
" <include rule='HAVE-VCARD-UID'/>\n"
|
|
" <include rule='HAVE-ABLABEL-PROPERTY'/>\n"
|
|
" </remoterule>";
|
|
} else if (host.find("yahoo") != host.npos) {
|
|
info.m_backendRule = "YAHOO";
|
|
fragments.m_remoterules["YAHOO"] =
|
|
" <remoterule name='YAHOO'>\n"
|
|
" <deviceid>none</deviceid>\n"
|
|
// Yahoo! Contacts reacts with a "500 - internal server error"
|
|
// to an empty X-GENDER property. In general, empty properties
|
|
// should never be necessary in CardDAV and CalDAV, because
|
|
// sent items conceptually replace the one on the server, so
|
|
// disable them all.
|
|
" <noemptyproperties>yes</noemptyproperties>\n"
|
|
// BDAY is ignored if it has the compact 19991231 instead of
|
|
// 1999-12-31, although both are valid.
|
|
" <include rule='EXTENDED-DATE-FORMAT'/>\n"
|
|
// Yahoo accepts extensions, so send them. However, it
|
|
// doesn't seem to store the X-EVOLUTION-UI-SLOT parameter
|
|
// extensions.
|
|
" <include rule=\"ALL\"/>\n"
|
|
" <include rule=\"HAVE-VCARD-UID\"/>\n"
|
|
" <include rule=\"HAVE-ABLABEL-PROPERTY\"/>\n"
|
|
" </remoterule>";
|
|
}
|
|
}
|
|
SE_LOG_DEBUG(getDisplayName(), "using data conversion rules for '%s'", info.m_backendRule.c_str());
|
|
}
|
|
|
|
void WebDAVSource::storeServerInfos()
|
|
{
|
|
if (getDatabaseID().empty()) {
|
|
// user did not select resource, remember the one used for the
|
|
// next sync
|
|
setDatabaseID(m_calendar.toURL());
|
|
getProperties()->flush();
|
|
}
|
|
}
|
|
|
|
void WebDAVSource::checkPostSupport()
|
|
{
|
|
if (m_postPath.wasSet()) {
|
|
return;
|
|
}
|
|
|
|
static const ne_propname getaddmember[] = {
|
|
{ "DAV:", "add-member" },
|
|
{ NULL, NULL }
|
|
};
|
|
Timespec deadline = createDeadline();
|
|
Props_t davProps;
|
|
Neon::Session::PropfindPropCallback_t callback =
|
|
boost::bind(&WebDAVSource::openPropCallback,
|
|
this, boost::ref(davProps), _1, _2, _3, _4);
|
|
SE_LOG_DEBUG(NULL, "check POST support of %s", m_calendar.m_path.c_str());
|
|
m_session->propfindProp(m_calendar.m_path, 0, getaddmember, callback, deadline);
|
|
// Fatal communication problems will be reported via exceptions.
|
|
// Once we get here, invalid or incomplete results can be
|
|
// treated as "don't have revision string".
|
|
m_postPath = extractHREF(davProps[m_calendar.m_path]["DAV::add-member"]);
|
|
SE_LOG_DEBUG(NULL, "%s POST support: %s",
|
|
m_calendar.m_path.c_str(),
|
|
m_postPath.empty() ? "<none>" : m_postPath.get().c_str());
|
|
}
|
|
|
|
/**
|
|
* See https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-ctag.txt
|
|
*/
|
|
static const ne_propname getctag[] = {
|
|
{ "http://calendarserver.org/ns/", "getctag" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
std::string WebDAVSource::databaseRevision()
|
|
{
|
|
if (m_contextSettings && m_contextSettings->noCTag()) {
|
|
// return empty string to disable usage of CTag
|
|
return "";
|
|
}
|
|
|
|
contactServer();
|
|
|
|
Timespec deadline = createDeadline();
|
|
Props_t davProps;
|
|
Neon::Session::PropfindPropCallback_t callback =
|
|
boost::bind(&WebDAVSource::openPropCallback,
|
|
this, boost::ref(davProps), _1, _2, _3, _4);
|
|
SE_LOG_DEBUG(NULL, "read ctag of %s", m_calendar.m_path.c_str());
|
|
m_session->propfindProp(m_calendar.m_path, 0, getctag, callback, deadline);
|
|
// Fatal communication problems will be reported via exceptions.
|
|
// Once we get here, invalid or incomplete results can be
|
|
// treated as "don't have revision string".
|
|
string ctag = davProps[m_calendar.m_path]["http://calendarserver.org/ns/:getctag"];
|
|
return ctag;
|
|
}
|
|
|
|
|
|
static const ne_propname getetag[] = {
|
|
{ "DAV:", "getetag" },
|
|
{ "DAV:", "resourcetype" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
void WebDAVSource::listAllItems(RevisionMap_t &revisions)
|
|
{
|
|
contactServer();
|
|
|
|
if (!getContentMixed()) {
|
|
// Can use simple PROPFIND because we do not have to
|
|
// double-check that each item really contains the right data.
|
|
bool failed = false;
|
|
Timespec deadline = createDeadline();
|
|
m_session->propfindURI(m_calendar.m_path, 1, getetag,
|
|
boost::bind(&WebDAVSource::listAllItemsCallback,
|
|
this, _1, _2, boost::ref(revisions),
|
|
boost::ref(failed)),
|
|
deadline);
|
|
if (failed) {
|
|
SE_THROW("incomplete listing of all items");
|
|
}
|
|
} else {
|
|
// We have to read item data and verify that it really is
|
|
// something we have to (and may) work on. Currently only
|
|
// happens for CalDAV, CardDAV items are uniform. The CalDAV
|
|
// comp-filter alone should the trick, but some servers (for
|
|
// example Radicale 0.7) ignore it and thus we could end up
|
|
// deleting items we were not meant to touch.
|
|
const std::string 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"
|
|
"<C:calendar-data>\n"
|
|
"<C:comp name=\"VCALENDAR\">\n"
|
|
"<C:comp name=\"" + getContent() + "\">\n"
|
|
"<C:prop name=\"UID\"/>\n"
|
|
"</C:comp>\n"
|
|
"</C:comp>\n"
|
|
"</C:calendar-data>\n"
|
|
"</D:prop>\n"
|
|
// filter expected by Yahoo! Calendar
|
|
"<C:filter>\n"
|
|
"<C:comp-filter name=\"VCALENDAR\">\n"
|
|
"<C:comp-filter name=\"" + getContent() + "\">\n"
|
|
"</C:comp-filter>\n"
|
|
"</C:comp-filter>\n"
|
|
"</C:filter>\n"
|
|
"</C:calendar-query>\n";
|
|
Timespec deadline = createDeadline();
|
|
getSession()->startOperation("REPORT 'meta data'", deadline);
|
|
while (true) {
|
|
string data;
|
|
Neon::XMLParser parser;
|
|
parser.initReportParser(boost::bind(&WebDAVSource::checkItem, this,
|
|
boost::ref(revisions),
|
|
_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);
|
|
report.addHeader("Depth", "1");
|
|
report.addHeader("Content-Type", "application/xml; charset=\"utf-8\"");
|
|
if (report.run()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
bool &failed)
|
|
{
|
|
static const ne_propname prop = {
|
|
"DAV:", "getetag"
|
|
};
|
|
static const ne_propname resourcetype = {
|
|
"DAV:", "resourcetype"
|
|
};
|
|
|
|
const char *type = ne_propset_value(results, &resourcetype);
|
|
if (type && strstr(type, "<DAV:collection></DAV:collection>")) {
|
|
// skip collections
|
|
return;
|
|
}
|
|
|
|
std::string uid = path2luid(uri.m_path);
|
|
if (uid.empty()) {
|
|
// skip collection itself (should have been detected as collection already)
|
|
return;
|
|
}
|
|
|
|
const char *etag = ne_propset_value(results, &prop);
|
|
if (etag) {
|
|
std::string rev = ETag2Rev(etag);
|
|
SE_LOG_DEBUG(NULL, "item %s = rev %s",
|
|
uid.c_str(), rev.c_str());
|
|
revisions[uid] = rev;
|
|
} else {
|
|
failed = true;
|
|
SE_LOG_ERROR(NULL,
|
|
"%s: %s",
|
|
uri.toURL().c_str(),
|
|
Neon::Status2String(ne_propset_status(results, &prop)).c_str());
|
|
}
|
|
}
|
|
|
|
int WebDAVSource::checkItem(RevisionMap_t &revisions,
|
|
const std::string &href,
|
|
const std::string &etag,
|
|
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
|
|
// failing.
|
|
//
|
|
// One situation is the response for the collection itself,
|
|
// which comes with a 404 status and no data with Google Calendar.
|
|
if (data && data->empty()) {
|
|
return 0;
|
|
}
|
|
|
|
// No need to parse, user content cannot start at start of line in
|
|
// iCalendar 2.0.
|
|
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
|
|
if (data) {
|
|
data->clear();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
std::string WebDAVSource::path2luid(const std::string &path)
|
|
{
|
|
// m_calendar.m_path is normalized, path is not.
|
|
// Before comparing, normalize it.
|
|
std::string res = Neon::URI::normalizePath(path, false);
|
|
if (boost::starts_with(res, m_calendar.m_path)) {
|
|
res = Neon::URI::unescape(res.substr(m_calendar.m_path.size()));
|
|
} else {
|
|
// keep full, absolute path as LUID
|
|
}
|
|
return res;
|
|
}
|
|
|
|
std::string WebDAVSource::luid2path(const std::string &luid)
|
|
{
|
|
if (boost::starts_with(luid, "/")) {
|
|
return luid;
|
|
} else {
|
|
return m_calendar.resolve(Neon::URI::escape(luid)).m_path;
|
|
}
|
|
}
|
|
|
|
void WebDAVSource::readItem(const string &uid, std::string &item, bool raw)
|
|
{
|
|
Timespec deadline = createDeadline();
|
|
m_session->startOperation("GET", deadline);
|
|
while (true) {
|
|
item.clear();
|
|
Neon::Request req(*m_session, "GET", luid2path(uid),
|
|
"", item);
|
|
// useful with CardDAV: server might support more than vCard 3.0, but we don't
|
|
req.addHeader("Accept", contentType());
|
|
try {
|
|
if (req.run()) {
|
|
break;
|
|
}
|
|
} catch (const TransportStatusException &ex) {
|
|
if (ex.syncMLStatus() == 410) {
|
|
// Radicale reports 410 'Gone'. Hmm, okay.
|
|
// Let's map it to the expected 404.
|
|
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
|
|
"object not found (was 410 'Gone')",
|
|
SyncMLStatus(404));
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
TrackingSyncSource::InsertItemResult WebDAVSource::insertItem(const string &uid, const std::string &item, bool raw)
|
|
{
|
|
std::string new_uid;
|
|
std::string rev;
|
|
InsertItemResultState state = ITEM_OKAY;
|
|
|
|
// By default use PUT. Change that to POST when creating new items
|
|
// and server supports it. That avoids the problem of having to
|
|
// choose a path and figuring out whether the server really used it.
|
|
static const char putOperation[] = "PUT";
|
|
static const char postOperation[] = "POST";
|
|
const char *operation = putOperation;
|
|
if (uid.empty()) {
|
|
checkPostSupport();
|
|
if (!m_postPath.empty()) {
|
|
operation = postOperation;
|
|
}
|
|
}
|
|
Timespec deadline = createDeadline(); // no resending if left empty
|
|
m_session->startOperation(operation, deadline);
|
|
std::string result;
|
|
int counter = 0;
|
|
retry:
|
|
counter++;
|
|
result = "";
|
|
if (uid.empty()) {
|
|
// Pick a resource name (done by derived classes, by default random),
|
|
// catch unexpected conflicts via If-None-Match: *.
|
|
std::string buffer;
|
|
const std::string *data = createResourceName(item, buffer, new_uid);
|
|
Neon::Request req(*m_session, operation,
|
|
operation == postOperation ? m_postPath : luid2path(new_uid),
|
|
*data, result);
|
|
// Clearing the idempotent flag would allow us to clearly
|
|
// distinguish between a connection error (no changes made
|
|
// on server) and a server failure (may or may not have
|
|
// changed something) because it'll close the connection
|
|
// first.
|
|
//
|
|
// But because we are going to try resending
|
|
// the PUT anyway in case of 5xx errors we might as well
|
|
// treat it like an idempotent request (which it is,
|
|
// in a way, because we'll try to get our data onto
|
|
// the server no matter what) and keep reusing an
|
|
// existing connection.
|
|
// req.setFlag(NE_REQFLAG_IDEMPOTENT, 0);
|
|
|
|
// For this to work we must allow the server to overwrite
|
|
// an item that we might have created before. Don't allow
|
|
// that in the first attempt. Only relevant for PUT.
|
|
if (operation != postOperation && counter == 1) {
|
|
req.addHeader("If-None-Match", "*");
|
|
}
|
|
req.addHeader("Content-Type", contentType().c_str());
|
|
static const std::set<int> expected = boost::assign::list_of(412)(403);
|
|
if (!req.run(&expected)) {
|
|
goto retry;
|
|
}
|
|
SE_LOG_DEBUG(NULL, "add item status: %s",
|
|
Neon::Status2String(req.getStatus()).c_str());
|
|
switch (req.getStatusCode()) {
|
|
case 204:
|
|
// stored, potentially in a different resource than requested
|
|
// when the UID was recognized
|
|
break;
|
|
case 201:
|
|
// created
|
|
break;
|
|
case 403:
|
|
// For a POST, this might be a UID conflict that we didn't detect
|
|
// ourselves. Happens for VJOURNAL and the testInsertTwice test
|
|
// when testing with Apple Calendar server. It then returns:
|
|
// Content-Type: text/xml
|
|
// Body:
|
|
// <?xml version='1.0' encoding='UTF-8'?>
|
|
// <error xmlns='DAV:'>
|
|
// <no-uid-conflict xmlns='urn:ietf:params:xml:ns:caldav'>
|
|
// <href xmlns='DAV:'>/calendars/__uids__/user01/tasks/c5490e736b6836c4d353d98bc78b3a3d.ics</href>
|
|
// </no-uid-conflict>
|
|
// <error-description xmlns='http://twistedmatrix.com/xml_namespace/dav/'>UID already exists</error-description>
|
|
// </error>
|
|
//
|
|
// Handling that would be nice (see FDO #77424), but for now we just
|
|
// do the same as for "Precondition Failed" and search for the UID.
|
|
if (operation == postOperation) {
|
|
try {
|
|
std::string uid = extractUID(item);
|
|
if (!uid.empty()) {
|
|
std::string luid = findByUID(uid, deadline);
|
|
return InsertItemResult(luid, "", ITEM_NEEDS_MERGE);
|
|
}
|
|
} catch (...) {
|
|
// Ignore the error and report the original problem below.
|
|
Exception::log();
|
|
}
|
|
}
|
|
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
|
|
std::string("unexpected status for PUT: ") +
|
|
Neon::Status2String(req.getStatus()),
|
|
SyncMLStatus(req.getStatus()->code));
|
|
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: ") +
|
|
Neon::Status2String(req.getStatus()),
|
|
SyncMLStatus(req.getStatus()->code));
|
|
break;
|
|
}
|
|
rev = getETag(req);
|
|
std::string real_luid = getLUID(req);
|
|
if (!real_luid.empty()) {
|
|
// Google renames the resource automatically to something of the form
|
|
// <UID>.ics. Interestingly enough, our 1234567890!@#$%^&*()<>@dummy UID
|
|
// test case leads to a resource path which Google then cannot find
|
|
// via CalDAV. client-test must run with CLIENT_TEST_SIMPLE_UID=1...
|
|
SE_LOG_DEBUG(NULL, "new item mapped to %s", real_luid.c_str());
|
|
new_uid = real_luid;
|
|
// TODO: find a better way of detecting unexpected updates.
|
|
// state = ...
|
|
} else if (!rev.empty()) {
|
|
// Yahoo Contacts returns an etag, but no href. For items
|
|
// that were really created as requested, that's okay. But
|
|
// Yahoo Contacts silently merges the new contact with an
|
|
// existing one, presumably if it is "similar" enough. The
|
|
// web interface allows creating identical contacts
|
|
// multiple times; not so CardDAV. We are not even told
|
|
// the path of that other contact... Detect this by
|
|
// checking whether the item really exists.
|
|
//
|
|
// Google also returns an etag without a href. However,
|
|
// Google really creates a new item. We cannot tell here
|
|
// whether merging took place. As we are supporting Google
|
|
// but not Yahoo at the moment, let's assume that a new item
|
|
// was created.
|
|
RevisionMap_t revisions;
|
|
bool failed = false;
|
|
m_session->propfindURI(luid2path(new_uid), 0, getetag,
|
|
boost::bind(&WebDAVSource::listAllItemsCallback,
|
|
this, _1, _2, boost::ref(revisions),
|
|
boost::ref(failed)),
|
|
deadline);
|
|
// Turns out we get a result for our original path even in
|
|
// the case of a merge, although the original path is not
|
|
// listed when looking at the collection. Let's use that
|
|
// to return the "real" uid to our caller.
|
|
if (revisions.size() == 1 &&
|
|
revisions.begin()->first != new_uid) {
|
|
SE_LOG_DEBUG(NULL, "%s mapped to %s by peer",
|
|
new_uid.c_str(),
|
|
revisions.begin()->first.c_str());
|
|
new_uid = revisions.begin()->first;
|
|
// This would have to be uncommented for Yahoo.
|
|
// state = ITEM_REPLACED;
|
|
}
|
|
}
|
|
} else {
|
|
new_uid = uid;
|
|
std::string buffer;
|
|
const std::string *data = setResourceName(item, buffer, new_uid);
|
|
Neon::Request req(*m_session, "PUT", luid2path(new_uid),
|
|
*data, result);
|
|
// See above for discussion of idempotent and PUT.
|
|
// req.setFlag(NE_REQFLAG_IDEMPOTENT, 0);
|
|
req.addHeader("Content-Type", contentType());
|
|
// TODO: match exactly the expected revision, aka ETag,
|
|
// or implement locking. Note that the ETag might not be
|
|
// known, for example in this case:
|
|
// - PUT succeeds
|
|
// - PROPGET does not
|
|
// - insertItem() fails
|
|
// - Is retried? Might need slow sync in this case!
|
|
//
|
|
// req.addHeader("If-Match", etag);
|
|
if (!req.run()) {
|
|
goto retry;
|
|
}
|
|
SE_LOG_DEBUG(NULL, "update item status: %s",
|
|
Neon::Status2String(req.getStatus()).c_str());
|
|
switch (req.getStatusCode()) {
|
|
case 204:
|
|
// the expected outcome, as we were asking for an overwrite
|
|
break;
|
|
case 201:
|
|
// Huh? Shouldn't happen, but Google sometimes reports it
|
|
// even when updating an item. Accept it.
|
|
// SE_THROW("unexpected creation instead of update");
|
|
break;
|
|
default:
|
|
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
|
|
std::string("unexpected status for update: ") +
|
|
Neon::Status2String(req.getStatus()),
|
|
SyncMLStatus(req.getStatus()->code));
|
|
break;
|
|
}
|
|
rev = getETag(req);
|
|
std::string real_luid = getLUID(req);
|
|
if (!real_luid.empty() && real_luid != new_uid) {
|
|
SE_THROW(StringPrintf("updating item: real luid %s does not match old luid %s",
|
|
real_luid.c_str(), new_uid.c_str()));
|
|
}
|
|
}
|
|
|
|
if (rev.empty()) {
|
|
// Server did not include etag header. Must request it
|
|
// explicitly (leads to race condition!). Google Calendar
|
|
// assigns a new ETag even if the body has not changed,
|
|
// so any kind of caching of ETag would not work either.
|
|
bool failed = false;
|
|
RevisionMap_t revisions;
|
|
m_session->propfindURI(luid2path(new_uid), 0, getetag,
|
|
boost::bind(&WebDAVSource::listAllItemsCallback,
|
|
this, _1, _2, boost::ref(revisions),
|
|
boost::ref(failed)),
|
|
deadline);
|
|
rev = revisions[new_uid];
|
|
if (failed || rev.empty()) {
|
|
SE_THROW("could not retrieve ETag");
|
|
}
|
|
}
|
|
|
|
return InsertItemResult(new_uid, rev, state);
|
|
}
|
|
|
|
std::string WebDAVSource::ETag2Rev(const std::string &etag)
|
|
{
|
|
std::string res = etag;
|
|
if (boost::starts_with(res, "W/")) {
|
|
res.erase(0, 2);
|
|
}
|
|
if (res.size() >= 2 &&
|
|
res[0] == '"' &&
|
|
res[res.size() - 1] == '"') {
|
|
res = res.substr(1, res.size() - 2);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
std::string WebDAVSource::getLUID(Neon::Request &req)
|
|
{
|
|
std::string location = req.getResponseHeader("Location");
|
|
if (location.empty()) {
|
|
return location;
|
|
} else {
|
|
return path2luid(Neon::URI::parse(location).m_path);
|
|
}
|
|
}
|
|
|
|
bool WebDAVSource::isLeafCollection(const StringMap &props) const
|
|
{
|
|
// CardDAV and CalDAV both promise to not contain anything
|
|
// unrelated to them
|
|
StringMap::const_iterator it = props.find("DAV::resourcetype");
|
|
if (it != props.end()) {
|
|
const std::string &type = it->second;
|
|
// allow parameters (no closing bracket)
|
|
// and allow also "carddavaddressbook" (caused by invalid Neon
|
|
// string concatenation?!)
|
|
if (type.find("<urn:ietf:params:xml:ns:caldav:calendar") != type.npos ||
|
|
type.find("<urn:ietf:params:xml:ns:caldavcalendar") != type.npos ||
|
|
type.find("<urn:ietf:params:xml:ns:carddav:addressbook") != type.npos ||
|
|
type.find("<urn:ietf:params:xml:ns:carddavaddressbook") != type.npos) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Timespec WebDAVSource::createDeadline() const
|
|
{
|
|
int timeoutSeconds = m_settings->timeoutSeconds();
|
|
int retrySeconds = m_settings->retrySeconds();
|
|
if (timeoutSeconds > 0 &&
|
|
retrySeconds > 0) {
|
|
return Timespec::monotonic() + timeoutSeconds;
|
|
} else {
|
|
return Timespec();
|
|
}
|
|
}
|
|
|
|
void WebDAVSource::removeItem(const string &uid)
|
|
{
|
|
Timespec deadline = createDeadline();
|
|
m_session->startOperation("DELETE", deadline);
|
|
std::string item, result;
|
|
boost::scoped_ptr<Neon::Request> req;
|
|
while (true) {
|
|
req.reset(new Neon::Request(*m_session, "DELETE", luid2path(uid),
|
|
item, result));
|
|
// TODO: match exactly the expected revision, aka ETag,
|
|
// or implement locking.
|
|
// req.addHeader("If-Match", etag);
|
|
static const std::set<int> expected = boost::assign::list_of(412);
|
|
if (req->run(&expected)) {
|
|
break;
|
|
}
|
|
}
|
|
SE_LOG_DEBUG(NULL, "remove item status: %s",
|
|
Neon::Status2String(req->getStatus()).c_str());
|
|
switch (req->getStatusCode()) {
|
|
case 204:
|
|
// the expected outcome
|
|
break;
|
|
case 200:
|
|
// reported by Radicale, also okay
|
|
break;
|
|
case 412:
|
|
// Radicale reports 412 'Precondition Failed'. Hmm, okay.
|
|
// Let's map it to the expected 404.
|
|
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
|
|
"object not found (was 412 'Precondition Failed')",
|
|
SyncMLStatus(404));
|
|
break;
|
|
default:
|
|
SE_THROW_EXCEPTION_STATUS(TransportStatusException,
|
|
std::string("unexpected status for removal: ") +
|
|
Neon::Status2String(req->getStatus()),
|
|
SyncMLStatus(req->getStatus()->code));
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endif /* ENABLE_DAV */
|
|
|
|
SE_END_CXX
|
|
|
|
|
|
#ifdef ENABLE_MODULES
|
|
# include "WebDAVSourceRegister.cpp"
|
|
#endif
|