2010-10-04 15:06:18 +02:00
/*
* Copyright ( C ) 2010 Intel Corporation
*/
# include "WebDAVSource.h"
2010-10-05 13:17:06 +02:00
# include <boost/bind.hpp>
# include <boost/algorithm/string/replace.hpp>
2010-10-05 14:48:59 +02:00
# include <boost/algorithm/string/predicate.hpp>
2010-11-09 14:16:02 +01:00
# include <boost/algorithm/string/classification.hpp>
2011-05-23 11:04:51 +02:00
# include <boost/algorithm/string/find.hpp>
2011-04-14 10:10:17 +02:00
# include <boost/scoped_ptr.hpp>
2010-10-05 13:17:06 +02:00
2011-01-27 12:32:37 +01:00
# include <syncevo/LogRedirect.h>
2011-01-25 17:07:07 +01:00
# include <stdio.h>
# include <errno.h>
2010-10-04 15:06:18 +02:00
SE_BEGIN_CXX
2011-08-12 21:03:34 +02:00
BoolConfigProperty & WebDAVCredentialsOkay ( )
{
static BoolConfigProperty okay ( " webDAVCredentialsOkay " , " credentials were accepted before " ) ;
return okay ;
}
2011-04-15 13:30:51 +02:00
2011-04-20 18:13:16 +02:00
# ifdef ENABLE_DAV
2010-10-08 15:10:40 +02:00
/**
* Retrieve settings from SyncConfig .
* NULL pointer for config is allowed .
*/
class ContextSettings : public Neon : : Settings {
2011-04-15 13:30:51 +02:00
boost : : shared_ptr < SyncConfig > m_context ;
2010-11-10 10:06:07 +01:00
std : : string m_url ;
bool m_googleUpdateHack ;
bool m_googleChildHack ;
2010-11-30 17:45:40 +01:00
bool m_googleAlarmHack ;
2011-04-15 13:30:51 +02:00
// credentials were valid in the past: stored persistently in tracking node
bool m_credentialsOkay ;
2010-11-10 10:06:07 +01:00
2010-10-08 15:10:40 +02:00
public :
2011-04-15 13:30:51 +02:00
ContextSettings ( const boost : : shared_ptr < SyncConfig > & context ) :
2010-11-30 17:45:40 +01:00
m_context ( context ) ,
m_googleUpdateHack ( false ) ,
m_googleChildHack ( false ) ,
2011-04-15 13:30:51 +02:00
m_googleAlarmHack ( false ) ,
m_credentialsOkay ( false )
2010-10-08 15:10:40 +02:00
{
if ( m_context ) {
vector < string > urls = m_context - > getSyncURL ( ) ;
2011-05-23 11:04:51 +02:00
string urlWithUsername ;
2010-10-29 17:25:20 +02:00
if ( ! urls . empty ( ) ) {
2011-05-23 11:04:51 +02:00
m_url = urlWithUsername = urls . front ( ) ;
2011-04-11 12:55:13 +02:00
std : : string username = m_context - > getSyncUsername ( ) ;
2011-05-23 11:04:51 +02:00
boost : : replace_all ( urlWithUsername , " %u " , Neon : : URI : : escape ( username ) ) ;
2010-11-10 10:06:07 +01:00
}
2011-05-23 11:04:51 +02:00
Neon : : URI uri = Neon : : URI : : parse ( urlWithUsername ) ;
2010-11-10 10:06:07 +01:00
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 " ) ) {
m_googleUpdateHack = true ;
} else if ( boost : : iequals ( * flag , " ChildHack " ) ) {
m_googleChildHack = true ;
2010-11-30 17:45:40 +01:00
} else if ( boost : : iequals ( * flag , " AlarmHack " ) ) {
m_googleAlarmHack = true ;
} else if ( boost : : iequals ( * flag , " Google " ) ) {
m_googleUpdateHack =
m_googleChildHack =
m_googleAlarmHack = true ;
2010-11-10 10:06:07 +01:00
} else {
SE_THROW ( StringPrintf ( " unknown SyncEvolution flag %s in URL %s " ,
std : : string ( flag - > begin ( ) , flag - > end ( ) ) . c_str ( ) ,
2011-05-23 11:04:51 +02:00
urlWithUsername . c_str ( ) ) ) ;
2010-11-10 10:06:07 +01:00
}
}
2011-05-05 13:47:20 +02:00
} else if ( arg - > end ( ) ! = arg - > begin ( ) ) {
2010-11-10 10:06:07 +01:00
SE_THROW ( StringPrintf ( " unknown parameter %s in URL %s " ,
std : : string ( arg - > begin ( ) , arg - > end ( ) ) . c_str ( ) ,
2011-05-23 11:04:51 +02:00
urlWithUsername . c_str ( ) ) ) ;
2010-11-10 10:06:07 +01:00
}
2010-10-29 17:25:20 +02:00
}
2011-08-12 21:03:34 +02:00
boost : : shared_ptr < FilterConfigNode > node = m_context - > getNode ( WebDAVCredentialsOkay ( ) ) ;
m_credentialsOkay = WebDAVCredentialsOkay ( ) . getPropertyValue ( * node ) ;
2010-10-08 15:10:40 +02:00
}
}
2011-01-25 17:07:07 +01:00
void setURL ( const std : : string url ) { m_url = url ; }
2010-11-10 10:06:07 +01:00
virtual std : : string getURL ( ) { return m_url ; }
2010-10-08 15:10:40 +02:00
virtual bool verifySSLHost ( )
{
return ! m_context | | m_context - > getSSLVerifyHost ( ) ;
}
virtual bool verifySSLCertificate ( )
{
return ! m_context | | m_context - > getSSLVerifyServer ( ) ;
}
2010-11-22 14:52:34 +01:00
virtual std : : string proxy ( )
{
if ( ! m_context | |
! m_context - > getUseProxy ( ) ) {
return " " ;
} else {
return m_context - > getProxyHost ( ) ;
}
}
2010-11-30 17:45:40 +01:00
virtual bool googleUpdateHack ( ) const { return m_googleUpdateHack ; }
virtual bool googleChildHack ( ) const { return m_googleChildHack ; }
virtual bool googleAlarmHack ( ) const { return m_googleChildHack ; }
2010-11-10 10:06:07 +01:00
2011-02-09 15:47:41 +01:00
virtual int timeoutSeconds ( ) const { return m_context - > getRetryDuration ( ) ; }
2011-04-15 13:30:51 +02:00
virtual int retrySeconds ( ) const {
int seconds = m_context - > getRetryInterval ( ) ;
if ( seconds > = 0 ) {
seconds / = ( 120 / 5 ) ; // default: 2min => 5s
}
return seconds ;
}
2011-02-09 15:47:41 +01:00
2010-10-08 15:10:40 +02:00
virtual void getCredentials ( const std : : string & realm ,
std : : string & username ,
std : : string & password )
{
if ( m_context ) {
2011-04-11 12:55:13 +02:00
username = m_context - > getSyncUsername ( ) ;
password = m_context - > getSyncPassword ( ) ;
2010-10-08 15:10:40 +02:00
}
}
2011-04-15 13:30:51 +02:00
virtual bool getCredentialsOkay ( ) { return m_credentialsOkay ; }
virtual void setCredentialsOkay ( bool okay ) {
2011-04-27 16:48:49 +02:00
if ( m_credentialsOkay ! = okay & & m_context ) {
2011-08-12 21:03:34 +02:00
boost : : shared_ptr < FilterConfigNode > node = m_context - > getNode ( WebDAVCredentialsOkay ( ) ) ;
2011-08-01 13:05:04 +02:00
if ( ! node - > isReadOnly ( ) ) {
2011-08-12 21:03:34 +02:00
WebDAVCredentialsOkay ( ) . setProperty ( * node , okay ) ;
2011-08-01 13:05:04 +02:00
node - > flush ( ) ;
}
2011-06-29 16:12:47 +02:00
m_credentialsOkay = okay ;
2011-04-15 13:30:51 +02:00
}
}
2010-10-08 15:10:40 +02:00
virtual int logLevel ( )
{
return m_context ?
m_context - > getLogLevel ( ) :
0 ;
}
} ;
2010-10-05 11:02:33 +02:00
WebDAVSource : : WebDAVSource ( const SyncSourceParams & params ,
const boost : : shared_ptr < Neon : : Settings > & settings ) :
TrackingSyncSource ( params ) ,
m_settings ( settings )
2010-10-04 15:06:18 +02:00
{
2010-10-08 15:10:40 +02:00
if ( ! m_settings ) {
2011-01-25 17:07:07 +01:00
m_contextSettings . reset ( new ContextSettings ( params . m_context ) ) ;
m_settings = m_contextSettings ;
2010-10-08 15:10:40 +02:00
}
2011-06-20 10:48:00 +02:00
/* 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 ) ;
2011-10-10 13:36:04 +02:00
// 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 ( " ) ;
2010-10-04 15:06:18 +02:00
}
2011-01-26 14:40:12 +01:00
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 ;
}
}
}
2010-10-04 15:06:18 +02:00
void WebDAVSource : : open ( )
2011-05-13 11:51:53 +02:00
{
// Nothing to do here, expensive initialization is in contactServer().
}
2011-10-10 13:36:04 +02:00
static bool setFirstURL ( Neon : : URI & result ,
const std : : string & name ,
const Neon : : URI & uri )
{
result = uri ;
// stop
return false ;
}
2011-05-13 11:51:53 +02:00
void WebDAVSource : : contactServer ( )
2010-10-04 15:06:18 +02:00
{
2011-05-18 10:01:36 +02:00
if ( ! m_calendar . empty ( ) & &
m_session ) {
// we have done this work before, no need to repeat it
}
2011-10-10 13:36:04 +02:00
SE_LOG_DEBUG ( NULL , NULL , " using libneon %s with %s " ,
ne_version_string ( ) , Neon : : features ( ) . c_str ( ) ) ;
2011-06-17 14:48:43 +02:00
// 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 ) ;
// m_contextSettings = m_settings, so this sets m_settings->getURL()
m_contextSettings - > setURL ( database ) ;
// start talking to host defined by m_settings->getURL()
m_session = Neon : : Session : : create ( m_settings ) ;
// force authentication
std : : string user , pw ;
m_settings - > getCredentials ( " " , user , pw ) ;
m_session - > forceAuthorization ( user , pw ) ;
return ;
}
2011-10-10 13:36:04 +02:00
// Create session and find first collection (the default).
m_calendar = Neon : : URI ( ) ;
findCollections ( boost : : bind ( setFirstURL ,
boost : : ref ( m_calendar ) ,
_1 , _2 ) ) ;
if ( m_calendar . empty ( ) ) {
throwError ( " no database found " ) ;
}
SE_LOG_DEBUG ( NULL , NULL , " picked final 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 ( LoggerBase : : instance ( ) . getLevel ( ) > = Logger : : DEV ) {
try {
SE_LOG_DEBUG ( NULL , 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 , NULL , " %s WebDAV capabilities: %s " ,
m_session - > getURL ( ) . c_str ( ) ,
Flags2String ( caps , descr ) . c_str ( ) ) ;
} catch ( . . . ) {
Exception : : handle ( ) ;
}
}
# endif // HAVE_LIBNEON_OPTIONS
}
bool WebDAVSource : : findCollections ( const boost : : function < bool ( const std : : string & ,
const Neon : : URI & ) > & storeResult )
{
bool res = true ; // completed
bool found = false ;
2011-04-14 10:10:17 +02:00
int timeoutSeconds = m_settings - > timeoutSeconds ( ) ;
int retrySeconds = m_settings - > retrySeconds ( ) ;
SE_LOG_DEBUG ( this , NULL , " timout %ds, retry %ds => %s " ,
timeoutSeconds , retrySeconds ,
( timeoutSeconds < = 0 | |
retrySeconds < = 0 ) ? " resending disabled " : " resending allowed " ) ;
2011-01-25 17:07:07 +01:00
std : : string username , password ;
m_contextSettings - > getCredentials ( " " , username , password ) ;
// 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.
std : : string url = m_settings - > getURL ( ) ;
if ( url . empty ( ) & & m_contextSettings ) {
size_t pos = username . find ( ' @ ' ) ;
if ( pos = = username . npos ) {
2011-02-10 10:06:47 +01:00
// throw authentication error to indicate that the credentials are wrong
throwError ( STATUS_UNAUTHORIZED , StringPrintf ( " syncURL not configured and username %s does not contain a domain " , username . c_str ( ) ) ) ;
2011-01-25 17:07:07 +01:00
}
std : : string domain = username . substr ( pos + 1 ) ;
FILE * in = NULL ;
try {
2011-04-14 10:10:17 +02:00
Timespec startTime = Timespec : : monotonic ( ) ;
retry :
2011-01-25 17:07:07 +01:00
in = popen ( StringPrintf ( " syncevo-webdav-lookup '%s' '%s' " ,
serviceType ( ) . c_str ( ) ,
domain . c_str ( ) ) . c_str ( ) ,
" r " ) ;
if ( ! in ) {
2011-02-10 10:06:47 +01:00
throwError ( " syncURL not configured and starting syncevo-webdav-lookup for DNS SRV lookup failed " , errno ) ;
2011-01-25 17:07:07 +01:00
}
// 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 ;
m_contextSettings - > setURL ( buffer ) ;
SE_LOG_DEBUG ( this , NULL , " found syncURL '%s' via DNS SRV " , buffer ) ;
int res = pclose ( in ) ;
in = NULL ;
switch ( res ) {
case 0 :
break ;
case 2 :
throwError ( StringPrintf ( " syncURL not configured and 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 ( StringPrintf ( " syncURL not configured and DNS SRV search for %s in %s did not find the service " , serviceType ( ) . c_str ( ) , domain . c_str ( ) ) ) ;
break ;
2011-04-14 10:10:17 +02:00
default : {
Timespec now = Timespec : : monotonic ( ) ;
if ( retrySeconds > 0 & &
timeoutSeconds > 0 ) {
if ( now < startTime + timeoutSeconds ) {
SE_LOG_DEBUG ( this , NULL , " DNS SRV search failed due to network issues, retry in %d seconds " ,
retrySeconds ) ;
Sleep ( retrySeconds ) ;
goto retry ;
} else {
SE_LOG_INFO ( this , NULL , " DNS SRV search timed out after %d seconds " , timeoutSeconds ) ;
}
}
2011-02-10 10:06:47 +01:00
// probably network problem
throwError ( STATUS_TRANSPORT_FAILURE , StringPrintf ( " syncURL not configured and DNS SRV search for %s in %s failed " , serviceType ( ) . c_str ( ) , domain . c_str ( ) ) ) ;
2011-01-25 17:07:07 +01:00
break ;
}
2011-04-14 10:10:17 +02:00
}
2011-01-25 17:07:07 +01:00
} catch ( . . . ) {
if ( in ) {
pclose ( in ) ;
}
throw ;
}
}
// start talking to host defined by m_settings->getURL()
m_session = Neon : : Session : : create ( m_settings ) ;
2010-10-05 11:02:33 +02:00
2010-11-25 10:08:07 +01:00
// Find default calendar. Same for address book, with slightly
// different parameters.
2010-10-05 13:17:06 +02:00
//
2010-11-09 14:16:02 +01:00
// Stops when:
// - current path is calendar collection (= contains VEVENTs)
// Gives up:
// - when running in circles
// - nothing else to try out
// - tried 10 times
// Follows:
2011-03-01 15:21:53 +01:00
// - current-user-principal
// - CalDAV calendar-home-set
2010-11-09 14:16:02 +01:00
// - collections
//
2011-03-01 15:21:53 +01:00
// TODO: hrefs and redirects are assumed to be on the same host - support switching host
2010-11-09 14:16:02 +01:00
// 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 = 10 ;
std : : set < std : : string > tried ;
2011-01-25 17:07:07 +01:00
std : : list < std : : string > candidates ;
2010-11-09 14:16:02 +01:00
std : : string path = m_session - > getURI ( ) . m_path ;
2010-10-05 14:24:16 +02:00
Neon : : Session : : PropfindPropCallback_t callback =
boost : : bind ( & WebDAVSource : : openPropCallback ,
this , _1 , _2 , _3 , _4 ) ;
2010-10-05 13:17:06 +02:00
2011-04-14 10:10:17 +02:00
// 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 m_davProps.
// Therefore resending is okay.
Timespec finalDeadline = createDeadline ( ) ; // no resending if left empty
2011-10-04 15:59:41 +02:00
// 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 ( path . empty ( ) | | path = = " / " ) {
std : : string wellknown = wellKnownURL ( ) ;
if ( ! wellknown . empty ( ) ) {
candidates . push_back ( wellknown ) ;
}
}
2010-11-09 14:16:02 +01:00
while ( true ) {
2011-05-23 11:04:51 +02:00
bool usernameInserted = false ;
2011-01-25 17:07:07 +01:00
std : : string next ;
2011-05-23 11:04:51 +02:00
// 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 ( path , " %u " ) ) {
boost : : replace_all ( path , " %u " , Neon : : URI : : escape ( username ) ) ;
usernameInserted = true ;
}
2010-11-25 10:08:07 +01:00
// must normalize so that we can compare against results from server
path = Neon : : URI : : normalizePath ( path , true ) ;
2010-11-09 14:16:02 +01:00
SE_LOG_DEBUG ( NULL , NULL , " testing %s " , path . c_str ( ) ) ;
tried . insert ( path ) ;
2011-01-25 17:07:07 +01:00
// 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
2011-04-14 10:10:17 +02:00
// the root path as fallback.
2011-03-01 17:53:15 +01:00
if ( path = = " /.well-known/caldav/ " | |
path = = " /.well-known/carddav/ " ) {
2011-01-25 17:07:07 +01:00
// remove trailing slash added by normalization, to be aligned with draft-daboo-srv-caldav-10
path . resize ( path . size ( ) - 1 ) ;
2011-03-01 17:53:15 +01:00
// 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.
candidates . push_back ( " / " ) ;
2011-01-25 17:07:07 +01:00
// TODO: Google Calendar, with workarounds
// candidates.push_back(StringPrintf("/calendar/dav/%s/user/", Neon::URI::escape(username).c_str()));
}
bool success = false ;
try {
2011-04-14 10:10:17 +02:00
// disable resending for some known cases where it never succeeds
Timespec deadline = finalDeadline ;
if ( boost : : starts_with ( path , " /.well-known " ) & &
m_settings - > getURL ( ) . find ( " yahoo.com " ) ! = string : : npos ) {
deadline = Timespec ( ) ;
}
2011-01-25 17:07:07 +01:00
if ( LoggerBase : : instance ( ) . getLevel ( ) > = Logger : : DEV ) {
2011-03-01 15:21:53 +01:00
// First dump WebDAV "allprops" properties (does not contain
// properties which must be asked for explicitly!). Only
// relevant for debugging.
SE_LOG_DEBUG ( NULL , NULL , " debugging: read all WebDAV properties of %s " , path . c_str ( ) ) ;
2011-01-25 17:07:07 +01:00
Neon : : Session : : PropfindPropCallback_t callback =
boost : : bind ( & WebDAVSource : : openPropCallback ,
this , _1 , _2 , _3 , _4 ) ;
2011-04-14 10:10:17 +02:00
m_session - > propfindProp ( path , 0 , NULL , callback , deadline ) ;
2011-01-25 17:07:07 +01:00
}
2010-11-09 14:16:02 +01:00
2011-01-25 17:07:07 +01:00
// 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.
2011-03-01 15:21:53 +01:00
//
// 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.
// The rationale is that this cuts down on the number of
// requests for https while still being secure. For
// http the setup already is insecure if the transport
// isn't trusted (sends PIM data as plain text).
//
// 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
std : : string user , pw ;
m_settings - > getCredentials ( " " , user , pw ) ;
m_session - > forceAuthorization ( user , pw ) ;
2011-01-25 17:07:07 +01:00
m_davProps . clear ( ) ;
static const ne_propname caldav [ ] = {
// WebDAV ACL
{ " DAV: " , " alternate-URI-set " } ,
{ " DAV: " , " principal-URL " } ,
2011-03-01 15:21:53 +01:00
{ " DAV: " , " current-user-principal " } ,
2011-01-25 17:07:07 +01:00
{ " 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 " } ,
// 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 " } ,
{ NULL , NULL }
} ;
2011-03-01 15:21:53 +01:00
SE_LOG_DEBUG ( NULL , NULL , " read relevant properties of %s " , path . c_str ( ) ) ;
2011-04-14 10:10:17 +02:00
m_session - > propfindProp ( path , 0 , caldav , callback , deadline ) ;
2011-01-25 17:07:07 +01:00
success = true ;
2011-03-01 15:21:53 +01:00
} catch ( const Neon : : RedirectException & ex ) {
// follow to new location
Neon : : URI next = Neon : : URI : : parse ( ex . getLocation ( ) ) ;
Neon : : URI old = m_session - > getURI ( ) ;
if ( next . m_scheme ! = old . m_scheme | |
next . m_host ! = old . m_host | |
next . m_port ! = old . m_port ) {
SE_LOG_DEBUG ( NULL , NULL , " ignore redirection to different server (not implemented): %s " ,
ex . getLocation ( ) . c_str ( ) ) ;
if ( candidates . empty ( ) ) {
// nothing left to try, bail out with this error
2011-10-10 13:36:04 +02:00
// unless we found something else already
if ( found ) {
break ;
} else {
throw ;
}
2011-03-01 15:21:53 +01:00
}
} else {
candidates . push_front ( next . m_path ) ;
}
2011-05-23 11:04:51 +02:00
} catch ( const TransportStatusException & ex ) {
SE_LOG_DEBUG ( NULL , NULL , " TransportStatusException: %s " , ex . what ( ) ) ;
if ( ex . syncMLStatus ( ) = = 404 & & boost : : find_first ( 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? " ,
path . c_str ( ) , username . c_str ( ) ) ;
int code = 401 ;
SE_THROW_EXCEPTION_STATUS ( TransportStatusException , descr , SyncMLStatus ( code ) ) ;
2011-06-29 02:57:38 +02:00
} else {
if ( candidates . empty ( ) ) {
// nothing left to try, bail out with this error
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 , NULL , " ignore error for URI candidate: %s " , ex . what ( ) ) ;
}
2011-05-23 11:04:51 +02:00
}
2011-01-25 17:07:07 +01:00
} catch ( const Exception & ex ) {
if ( candidates . empty ( ) ) {
// nothing left to try, bail out with this error
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 , NULL , " ignore error for URI candidate: %s " , ex . what ( ) ) ;
}
2010-11-09 14:16:02 +01:00
}
2011-01-25 17:07:07 +01:00
if ( success ) {
2011-10-10 13:36:04 +02:00
Props_t : : iterator pathProps = m_davProps . find ( path ) ;
if ( pathProps = = m_davProps . end ( ) ) {
2011-03-01 17:53:15 +01:00
// No reply for requested path? Happens with Yahoo Calendar server,
// which returns information about "/dav" when asked about "/".
// Move to that path.
if ( ! m_davProps . empty ( ) ) {
string newpath = m_davProps . begin ( ) - > first ;
SE_LOG_DEBUG ( NULL , NULL , " use properties for '%s' instead of '%s' " ,
newpath . c_str ( ) , path . c_str ( ) ) ;
path = newpath ;
}
}
2011-10-10 13:36:04 +02:00
StringMap & props = pathProps - > second ;
2011-01-25 17:07:07 +01:00
if ( typeMatches ( props ) ) {
2011-10-10 13:36:04 +02:00
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
found = true ;
it = props . find ( " DAV::displayname " ) ;
Neon : : URI uri = m_session - > getURI ( ) ;
uri . m_path = path ;
res = storeResult ( it = = props . end ( ) ?
std : : string ( " <no name> " ) :
it - > second ,
uri ) ;
if ( ! res ) {
// done
break ;
}
2011-01-25 17:07:07 +01:00
}
2011-03-01 15:21:53 +01:00
// find next path:
// prefer CardDAV:calendar-home-set or CalDAV:addressbook-home-set
std : : string home = extractHREF ( props [ homeSetProp ( ) ] ) ;
if ( ! home . empty ( ) & &
tried . find ( home ) = = tried . end ( ) ) {
next = home ;
SE_LOG_DEBUG ( NULL , NULL , " follow home-set property to %s " , next . c_str ( ) ) ;
}
// alternatively, follow principal URL
if ( next . empty ( ) ) {
std : : string principal = extractHREF ( props [ " DAV::current-user-principal " ] ) ;
if ( ! principal . empty ( ) & &
tried . find ( principal ) = = tried . end ( ) ) {
next = principal ;
SE_LOG_DEBUG ( NULL , NULL , " follow current-user-prinicipal to %s " , next . c_str ( ) ) ;
2010-11-09 14:16:02 +01:00
}
}
2011-03-01 15:21:53 +01:00
// finally, recursively descend into collections
2011-01-25 17:07:07 +01:00
if ( next . empty ( ) ) {
const std : : string & type = props [ " DAV::resourcetype " ] ;
if ( type . find ( " <DAV:collection></DAV:collection> " ) ! = type . npos ) {
// 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 , NULL , " list items in %s " , path . c_str ( ) ) ;
static const ne_propname props [ ] = {
{ " 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 " } ,
{ " 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 }
} ;
m_davProps . clear ( ) ;
2011-04-14 10:10:17 +02:00
m_session - > propfindProp ( path , 1 , props , callback , finalDeadline ) ;
2011-10-04 15:52:16 +02:00
std : : set < std : : string > subs ;
2011-01-25 17:07:07 +01:00
BOOST_FOREACH ( Props_t : : value_type & entry , m_davProps ) {
const std : : string & sub = entry . first ;
const std : : string & subType = entry . second [ " DAV::resourcetype " ] ;
// new candidates are:
// - untested
// - not already a candidate
2011-10-10 13:36:04 +02:00
// - a resource, but not the CalDAV schedule-inbox/outbox
2011-03-01 15:21:53 +01:00
// - 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 (?).
2011-01-25 17:07:07 +01:00
if ( tried . find ( sub ) = = tried . end ( ) & &
std : : find ( candidates . begin ( ) , candidates . end ( ) , sub ) = = candidates . end ( ) & &
2011-03-01 15:21:53 +01:00
subType . find ( " <DAV:collection></DAV:collection> " ) ! = subType . npos & &
2011-10-10 13:36:04 +02:00
subType . find ( " <urn:ietf:params:xml:ns:caldavschedule- " ) = = subType . npos & &
2011-10-04 15:52:16 +02:00
subType . find ( " <http://calendarserver.org/ns/shared " ) = = subType . npos & &
( typeMatches ( entry . second ) | | ! ignoreCollection ( entry . second ) ) ) {
subs . insert ( sub ) ;
2011-01-25 17:07:07 +01:00
SE_LOG_DEBUG ( NULL , NULL , " new candidate: %s " , sub . c_str ( ) ) ;
2011-10-04 15:52:16 +02:00
} else {
SE_LOG_DEBUG ( NULL , NULL , " skipping: %s " , sub . c_str ( ) ) ;
2010-11-25 10:08:07 +01:00
}
2010-11-09 14:16:02 +01:00
}
2011-10-04 15:52:16 +02:00
// insert before other candidates, sorted
// alphabetically
candidates . insert ( candidates . begin ( ) ,
subs . begin ( ) ,
subs . end ( ) ) ;
2010-11-09 14:16:02 +01:00
}
}
}
2011-01-25 17:07:07 +01:00
2010-11-09 14:16:02 +01:00
if ( next . empty ( ) ) {
2011-10-04 16:03:07 +02:00
// use next untried candidate
next = " " ;
while ( ! candidates . empty ( ) ) {
std : : string candidate = candidates . front ( ) ;
candidates . pop_front ( ) ;
if ( tried . find ( candidate ) = = tried . end ( ) ) {
next = candidate ;
break ;
}
}
if ( next . empty ( ) ) {
// done searching
2011-10-10 13:36:04 +02:00
break ;
2010-11-09 14:16:02 +01:00
}
SE_LOG_DEBUG ( NULL , NULL , " follow candidate %s " , next . c_str ( ) ) ;
}
counter + + ;
if ( counter > limit ) {
2010-11-25 10:08:07 +01:00
throwError ( StringPrintf ( " giving up search for collection after %d attempts " , limit ) ) ;
2010-11-09 14:16:02 +01:00
}
path = next ;
2010-10-05 14:48:59 +02:00
}
2010-11-09 14:16:02 +01:00
2011-10-10 13:36:04 +02:00
return res ;
2010-10-05 13:17:06 +02:00
}
2011-03-01 15:21:53 +01:00
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 " " ;
}
2010-10-05 13:17:06 +02:00
void WebDAVSource : : openPropCallback ( 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
2010-11-09 14:16:02 +01:00
std : : string name ;
if ( prop - > nspace ) {
name = prop - > nspace ;
}
name + = " : " ;
name + = prop - > name ;
if ( value ) {
m_davProps [ uri . m_path ] [ name ] = value ;
boost : : trim_if ( m_davProps [ uri . m_path ] [ name ] ,
boost : : is_space ( ) ) ;
}
2010-10-04 15:06:18 +02:00
}
bool WebDAVSource : : isEmpty ( )
{
2011-05-18 10:01:36 +02:00
contactServer ( ) ;
2010-10-05 16:02:58 +02:00
// listing all items is relatively efficient, let's use that
2010-11-25 10:08:07 +01:00
// TODO: use truncated result search
2010-10-05 16:02:58 +02:00
RevisionMap_t revisions ;
listAllItems ( revisions ) ;
return revisions . empty ( ) ;
2010-10-04 15:06:18 +02:00
}
void WebDAVSource : : close ( )
{
2010-10-05 11:02:33 +02:00
m_session . reset ( ) ;
2010-10-04 15:06:18 +02:00
}
2011-10-10 13:36:04 +02:00
static bool storeCollection ( SyncSource : : Databases & result ,
const std : : string & name ,
const Neon : : URI & uri )
{
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 ) ) ;
return true ;
}
2010-10-04 15:06:18 +02:00
WebDAVSource : : Databases WebDAVSource : : getDatabases ( )
{
Databases result ;
2011-10-10 13:36:04 +02:00
// do a scan if at least username is set
std : : string username , password ;
m_contextSettings - > getCredentials ( " " , username , password ) ;
if ( ! username . empty ( ) ) {
findCollections ( boost : : bind ( storeCollection ,
boost : : ref ( result ) ,
_1 , _2 ) ) ;
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> " ) ) ;
}
2010-10-04 15:06:18 +02:00
return result ;
}
2011-01-25 19:29:33 +01:00
void WebDAVSource : : getSynthesisInfo ( SynthesisInfo & info ,
XMLConfigFragments & fragments )
{
TrackingSyncSource : : getSynthesisInfo ( info , fragments ) ;
// TODO: instead of identifying the peer based on the
// session URI, use some information gathered about
// it during open()
if ( m_session ) {
string host = m_session - > getURI ( ) . m_host ;
if ( host . find ( " google " ) ! = host . npos ) {
info . m_backendRule = " GOOGLE " ;
fragments . m_remoterules [ " GOOGLE " ] =
" <remoterule name='GOOGLE'> \n "
" <deviceid>none</deviceid> \n "
2011-04-20 12:50:02 +02:00
// enable extensions, just in case (not relevant yet for calendar)
" <include rule= \" ALL \" /> \n "
2011-01-25 19:29:33 +01:00
" </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 "
2011-01-27 14:02:10 +01:00
// BDAY is ignored if it has the compact 19991231 instead of
// 1999-12-31, although both are valid.
" <include rule='EXTENDED-DATE-FORMAT'/> \n "
2011-04-20 12:50:02 +02:00
// Yahoo accepts extensions, so send them. However, it
// doesn't seem to store the X-EVOLUTION-UI-SLOT parameter
// extensions.
" <include rule= \" ALL \" /> \n "
" </remoterule> " ;
} else {
// fallback: generic CalDAV/CardDAV, with all properties
// enabled (for example, X-EVOLUTION-UI-SLOT)
info . m_backendRule = " WEBDAV " ;
fragments . m_remoterules [ " WEBDAV " ] =
" <remoterule name='WEBDAV'> \n "
" <deviceid>none</deviceid> \n "
" <include rule= \" ALL \" /> \n "
2011-01-25 19:29:33 +01:00
" </remoterule> " ;
}
SE_LOG_DEBUG ( this , NULL , " using data conversion rules for '%s' " , info . m_backendRule . c_str ( ) ) ;
}
}
2011-06-17 14:48:43 +02:00
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 ( ) ;
}
}
2011-06-22 17:38:32 +02:00
/**
* 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 ( )
{
Timespec deadline = createDeadline ( ) ;
Neon : : Session : : PropfindPropCallback_t callback =
boost : : bind ( & WebDAVSource : : openPropCallback ,
this , _1 , _2 , _3 , _4 ) ;
m_davProps [ m_calendar . m_path ] [ " http://calendarserver.org/ns/:getctag " ] = " " ;
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 = m_davProps [ m_calendar . m_path ] [ " http://calendarserver.org/ns/:getctag " ] ;
return ctag ;
}
2011-06-17 14:48:43 +02:00
2010-10-06 11:21:11 +02:00
static const ne_propname getetag [ ] = {
{ " DAV: " , " getetag " } ,
2010-11-25 10:08:07 +01:00
{ " DAV: " , " resourcetype " } ,
2010-10-06 11:21:11 +02:00
{ NULL , NULL }
} ;
2010-10-04 15:06:18 +02:00
void WebDAVSource : : listAllItems ( RevisionMap_t & revisions )
{
2010-10-05 16:02:58 +02:00
bool failed = false ;
2011-04-14 10:10:17 +02:00
Timespec deadline = createDeadline ( ) ;
2010-10-06 11:21:11 +02:00
m_session - > propfindURI ( m_calendar . m_path , 1 , getetag ,
2010-10-05 16:02:58 +02:00
boost : : bind ( & WebDAVSource : : listAllItemsCallback ,
this , _1 , _2 , boost : : ref ( revisions ) ,
2011-04-14 10:10:17 +02:00
boost : : ref ( failed ) ) ,
deadline ) ;
2010-10-05 16:02:58 +02:00
if ( failed ) {
SE_THROW ( " incomplete listing of all items " ) ;
}
}
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 "
} ;
2010-11-25 11:28:56 +01:00
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 ;
}
2010-11-09 17:16:43 +01:00
std : : string uid = path2luid ( uri . m_path ) ;
2010-10-05 16:02:58 +02:00
if ( uid . empty ( ) ) {
2010-11-25 11:28:56 +01:00
// skip collection itself (should have been detected as collection already)
2010-10-05 16:02:58 +02:00
return ;
}
2010-11-25 11:28:56 +01:00
const char * etag = ne_propset_value ( results , & prop ) ;
2010-10-05 16:02:58 +02:00
if ( etag ) {
2010-10-06 11:21:11 +02:00
std : : string rev = ETag2Rev ( etag ) ;
SE_LOG_DEBUG ( NULL , NULL , " item %s = rev %s " ,
uid . c_str ( ) , rev . c_str ( ) ) ;
revisions [ uid ] = rev ;
2010-10-05 16:02:58 +02:00
} else {
failed = true ;
SE_LOG_ERROR ( NULL , NULL ,
" %s: %s " ,
uri . toURL ( ) . c_str ( ) ,
Neon : : Status2String ( ne_propset_status ( results , & prop ) ) . c_str ( ) ) ;
}
2010-10-04 15:06:18 +02:00
}
2010-10-06 11:21:11 +02:00
std : : string WebDAVSource : : path2luid ( const std : : string & path )
{
if ( boost : : starts_with ( path , m_calendar . m_path ) ) {
2010-11-09 17:16:43 +01:00
return Neon : : URI : : unescape ( path . substr ( m_calendar . m_path . size ( ) ) ) ;
2010-10-06 11:21:11 +02:00
} else {
return path ;
}
}
std : : string WebDAVSource : : luid2path ( const std : : string & luid )
{
2010-11-09 17:16:43 +01:00
if ( boost : : starts_with ( luid , " / " ) ) {
return luid ;
} else {
return m_calendar . resolve ( Neon : : URI : : escape ( luid ) ) . m_path ;
}
2010-10-06 11:21:11 +02:00
}
2010-10-04 15:06:18 +02:00
void WebDAVSource : : readItem ( const string & uid , std : : string & item , bool raw )
{
2011-04-14 10:10:17 +02:00
Timespec deadline = createDeadline ( ) ;
2011-04-15 13:30:51 +02:00
m_session - > startOperation ( " GET " , deadline ) ;
2011-04-14 10:10:17 +02:00
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 ( ) ) ;
2011-04-15 13:30:51 +02:00
if ( req . run ( ) ) {
2011-04-14 10:10:17 +02:00
break ;
}
}
2010-10-04 15:06:18 +02:00
}
TrackingSyncSource : : InsertItemResult WebDAVSource : : insertItem ( const string & uid , const std : : string & item , bool raw )
{
2010-10-06 11:21:11 +02:00
std : : string new_uid ;
std : : string rev ;
2011-09-13 10:58:49 +02:00
InsertItemResultState state = ITEM_OKAY ;
2010-10-06 11:21:11 +02:00
2011-04-14 10:10:17 +02:00
Timespec deadline = createDeadline ( ) ; // no resending if left empty
2011-04-15 13:30:51 +02:00
m_session - > startOperation ( " PUT " , deadline ) ;
2010-10-06 11:21:11 +02:00
std : : string result ;
2011-04-14 10:10:17 +02:00
int counter = 0 ;
retry :
counter + + ;
result = " " ;
2010-10-06 11:21:11 +02:00
if ( uid . empty ( ) ) {
2010-11-25 10:08:07 +01:00
// Pick a resource name (done by derived classes, by default random),
2010-10-06 11:21:11 +02:00
// catch unexpected conflicts via If-None-Match: *.
2010-11-25 10:08:07 +01:00
std : : string buffer ;
const std : : string * data = createResourceName ( item , buffer , new_uid ) ;
2010-10-06 11:21:11 +02:00
Neon : : Request req ( * m_session , " PUT " , luid2path ( new_uid ) ,
2010-11-25 10:08:07 +01:00
* data , result ) ;
2011-04-14 10:10:17 +02:00
// 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.
if ( counter = = 1 ) {
req . addHeader ( " If-None-Match " , " * " ) ;
}
2010-11-25 10:08:07 +01:00
req . addHeader ( " Content-Type " , contentType ( ) . c_str ( ) ) ;
2011-04-15 13:30:51 +02:00
if ( ! req . run ( ) ) {
2011-04-14 10:10:17 +02:00
goto retry ;
}
2010-10-06 11:21:11 +02:00
SE_LOG_DEBUG ( NULL , 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 ;
default :
2010-10-29 17:25:20 +02:00
SE_THROW_EXCEPTION_STATUS ( TransportStatusException ,
std : : string ( " unexpected status for insert: " ) +
Neon : : Status2String ( req . getStatus ( ) ) ,
SyncMLStatus ( req . getStatus ( ) - > code ) ) ;
2010-10-06 11:21:11 +02:00
break ;
}
rev = getETag ( req ) ;
std : : string real_luid = getLUID ( req ) ;
if ( ! real_luid . empty ( ) ) {
2010-10-08 15:13:58 +02:00
// 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 , NULL , " new item mapped to %s " , real_luid . c_str ( ) ) ;
2010-10-06 11:21:11 +02:00
new_uid = real_luid ;
2010-10-08 15:13:58 +02:00
// TODO: find a better way of detecting unexpected updates.
2011-09-13 10:58:49 +02:00
// state = ...
2010-11-25 10:08:07 +01:00
} 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.
RevisionMap_t revisions ;
bool failed = false ;
m_session - > propfindURI ( luid2path ( new_uid ) , 0 , getetag ,
boost : : bind ( & WebDAVSource : : listAllItemsCallback ,
this , _1 , _2 , boost : : ref ( revisions ) ,
2011-04-14 10:10:17 +02:00
boost : : ref ( failed ) ) ,
deadline ) ;
2010-11-25 10:08:07 +01:00
// 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 , NULL , " %s mapped to %s by peer " ,
new_uid . c_str ( ) ,
revisions . begin ( ) - > first . c_str ( ) ) ;
new_uid = revisions . begin ( ) - > first ;
2011-09-13 10:58:49 +02:00
state = ITEM_REPLACED ;
2010-11-25 10:08:07 +01:00
}
2010-10-06 11:21:11 +02:00
}
} else {
new_uid = uid ;
2010-11-25 10:08:07 +01:00
std : : string buffer ;
const std : : string * data = setResourceName ( item , buffer , new_uid ) ;
2010-10-06 11:21:11 +02:00
Neon : : Request req ( * m_session , " PUT " , luid2path ( new_uid ) ,
2010-11-25 10:08:07 +01:00
* data , result ) ;
2011-04-14 10:10:17 +02:00
// See above for discussion of idempotent and PUT.
// req.setFlag(NE_REQFLAG_IDEMPOTENT, 0);
2010-11-25 10:08:07 +01:00
req . addHeader ( " Content-Type " , contentType ( ) ) ;
2010-10-06 11:21:11 +02:00
// 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);
2011-04-15 13:30:51 +02:00
if ( ! req . run ( ) ) {
2011-04-14 10:10:17 +02:00
goto retry ;
}
2010-10-06 11:21:11 +02:00
SE_LOG_DEBUG ( NULL , 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 :
2010-11-04 15:59:52 +01:00
// Huh? Shouldn't happen, but Google sometimes reports it
// even when updating an item. Accept it.
// SE_THROW("unexpected creation instead of update");
2010-10-06 11:21:11 +02:00
break ;
default :
2010-10-29 17:25:20 +02:00
SE_THROW_EXCEPTION_STATUS ( TransportStatusException ,
std : : string ( " unexpected status for update: " ) +
Neon : : Status2String ( req . getStatus ( ) ) ,
SyncMLStatus ( req . getStatus ( ) - > code ) ) ;
2010-10-06 11:21:11 +02:00
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 ) ,
2011-04-14 10:10:17 +02:00
boost : : ref ( failed ) ) ,
deadline ) ;
2010-10-06 11:21:11 +02:00
rev = revisions [ new_uid ] ;
if ( failed | | rev . empty ( ) ) {
SE_THROW ( " could not retrieve ETag " ) ;
}
}
2011-09-13 10:58:49 +02:00
return InsertItemResult ( new_uid , rev , state ) ;
2010-10-04 15:06:18 +02:00
}
2010-10-06 11:21:11 +02:00
std : : string WebDAVSource : : ETag2Rev ( const std : : string & etag )
{
std : : string res = etag ;
if ( boost : : starts_with ( res , " W/ " ) ) {
res . erase ( 0 , 2 ) ;
}
2011-10-04 16:05:59 +02:00
if ( res . size ( ) > = 2 & &
res [ 0 ] = = ' " ' & &
res [ res . size ( ) - 1 ] = = ' " ' ) {
2010-10-06 11:21:11 +02:00
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 ) ;
}
}
2010-10-04 15:06:18 +02:00
2011-10-04 15:52:16 +02:00
bool WebDAVSource : : ignoreCollection ( 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 ;
}
2011-04-14 10:10:17 +02:00
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 ( ) ;
}
}
2010-10-04 15:06:18 +02:00
void WebDAVSource : : removeItem ( const string & uid )
{
2011-04-14 10:10:17 +02:00
Timespec deadline = createDeadline ( ) ;
2011-04-15 13:30:51 +02:00
m_session - > startOperation ( " DELETE " , deadline ) ;
2010-10-06 11:43:39 +02:00
std : : string item , result ;
2011-04-14 10:10:17 +02:00
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);
2011-04-15 13:30:51 +02:00
if ( req - > run ( ) ) {
2011-04-14 10:10:17 +02:00
break ;
}
}
2010-10-06 11:43:39 +02:00
SE_LOG_DEBUG ( NULL , NULL , " remove item status: %s " ,
2011-04-14 10:10:17 +02:00
Neon : : Status2String ( req - > getStatus ( ) ) . c_str ( ) ) ;
switch ( req - > getStatusCode ( ) ) {
2010-10-06 11:43:39 +02:00
case 204 :
// the expected outcome
break ;
2011-05-10 09:28:30 +02:00
case 404 :
// possibly already removed, ignore
SE_LOG_DEBUG ( this , NULL , " 404 - already removed? " ) ;
break ;
2010-10-06 11:43:39 +02:00
default :
2010-10-29 17:25:20 +02:00
SE_THROW_EXCEPTION_STATUS ( TransportStatusException ,
std : : string ( " unexpected status for removal: " ) +
2011-04-14 10:10:17 +02:00
Neon : : Status2String ( req - > getStatus ( ) ) ,
SyncMLStatus ( req - > getStatus ( ) - > code ) ) ;
2010-10-06 11:43:39 +02:00
break ;
}
2010-10-04 15:06:18 +02:00
}
2011-04-20 18:13:16 +02:00
# endif /* ENABLE_DAV */
2010-10-04 15:06:18 +02:00
SE_END_CXX
# ifdef ENABLE_MODULES
# include "WebDAVSourceRegister.cpp"
# endif