diff --git a/inc/caldav-client-v2.php b/inc/caldav-client-v2.php
index c6e2bdc7..c6e0e48c 100644
--- a/inc/caldav-client-v2.php
+++ b/inc/caldav-client-v2.php
@@ -120,7 +120,7 @@ class CalDAVClient {
* @param string $etag The etag to match / not match against.
*/
function SetMatch( $match, $etag = '*' ) {
- $this->headers[] = sprintf( "%s-Match: %s", ($match ? "If" : "If-None"), $etag);
+ $this->headers['match'] = sprintf( "%s-Match: %s", ($match ? "If" : "If-None"), $etag);
}
/**
@@ -129,7 +129,7 @@ class CalDAVClient {
* @param int $depth The depth, default to infinity
*/
function SetDepth( $depth = '0' ) {
- $this->headers[] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
+ $this->headers['depth'] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
}
/**
@@ -145,10 +145,10 @@ class CalDAVClient {
/**
* Add a Content-type: header.
*
- * @param int $type The content type
+ * @param string $type The content type
*/
function SetContentType( $type ) {
- $this->headers[] = "Content-type: $type";
+ $this->headers['content-type'] = "Content-type: $type";
}
/**
@@ -173,20 +173,19 @@ class CalDAVClient {
else {
$this->httpResponse = trim(substr($response, 0, $pos));
$this->xmlResponse = trim(substr($response, $pos));
+ $this->xmlResponse = preg_replace('{>[^>]*$}s', '>',$this->xmlResponse );
$parser = xml_parser_create_ns('UTF-8');
xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 );
- if ( 0 == xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) ) {
- printf( "XML parsing error: %s - %s\n", xml_get_error_code($this->parser), xml_error_string(xml_get_error_code($this->parser)) );
+ if ( xml_parse_into_struct( $parser, $this->xmlResponse, $this->xmlnodes, $this->xmltags ) === 0 ) {
+ printf( "XML parsing error: %s - %s\n", xml_get_error_code($parser), xml_error_string(xml_get_error_code($parser)) );
+// debug_print_backtrace();
+// echo "\nNodes array............................................................\n"; print_r( $this->xmlnodes );
+// echo "\nTags array............................................................\n"; print_r( $this->xmltags );
+ printf( "\nXML Reponse:\n%s\n", $this->xmlResponse );
}
-/* else {
- echo "\nNodes array............................................................\n";
- print_r( $this->xmlnodes );
- echo "\nTags array............................................................\n";
- print_r( $this->xmltags );
- }
-*/
+
xml_parser_free($parser);
}
}
@@ -204,8 +203,16 @@ class CalDAVClient {
*
* @return HTTP headers
*/
- function GetHttpResponse() {
- return $this->httpResponse;
+ function GetResponseHeaders() {
+ return $this->httpResponseHeaders;
+ }
+ /**
+ * Output http response body
+ *
+ * @return HTTP body
+ */
+ function GetResponseBody() {
+ return $this->httpResponseBody;
}
/**
* Output xml request
@@ -237,10 +244,16 @@ class CalDAVClient {
if ( !isset($url) ) $url = $this->base_url;
$this->request_url = $url;
- $headers[] = $this->requestMethod." ". $this->request_url . " HTTP/1.1";
+ $url = preg_replace('{^https?://[^/]+}', '', $url);
+ // URLencode if it isn't already
+/* if ( !preg_match( '{(%\x\x)}', $url) && preg_match( '{[^.-_/a-z0-9]}', $url ) ) {
+ $url = str_replace(rawurlencode('/'),'/',rawurlencode($url));
+ }*/
+ $headers[] = $this->requestMethod." ". $url . " HTTP/1.1";
$headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
$headers[] = "Host: ".$this->server .":".$this->port;
+ if ( !isset($this->headers['content-type']) ) $this->headers['content-type'] = "Content-type: text/plain";
foreach( $this->headers as $ii => $head ) {
$headers[] = $head;
}
@@ -250,16 +263,29 @@ class CalDAVClient {
$this->httpRequest = join("\r\n",$headers);
$this->xmlRequest = $this->body;
+ $this->httpResponse = '';
+ $this->xmlResponse = '';
+
$fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
if ( !(get_resource_type($fip) == 'stream') ) return false;
if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; }
- $rsp = "";
- while( !feof($fip) ) { $rsp .= fgets($fip,8192); }
+ $response = "";
+ while( !feof($fip) ) { $response .= fgets($fip,8192); }
fclose($fip);
+ $pos = strpos($response, "\n\n");
+ if ( $pos === false ) {
+ $this->httpResponseHeaders = $response;
+ $this->httpResponseBody = '';
+ }
+ else {
+ $this->httpResponseHeaders = substr($response,0,pos+1);
+ $this->httpResponseBody = substr($response, $pos + 2);
+ }
+
$this->headers = array(); // reset the headers array for our next request
- $this->ParseResponse($rsp);
- return $rsp;
+ $this->ParseResponse($response);
+ return $response;
}
@@ -311,6 +337,18 @@ class CalDAVClient {
}
+ /**
+ * Get the HEAD of a single item from the server.
+ *
+ * @param string $url The URL to HEAD
+ */
+ function DoHEADRequest( $url ) {
+ $this->body = "";
+ $this->requestMethod = "HEAD";
+ return $this->DoRequest( $url );
+ }
+
+
/**
* PUT a text/icalendar resource, returning the etag
*
@@ -327,14 +365,19 @@ class CalDAVClient {
if ( $etag != null ) {
$this->SetMatch( ($etag != '*'), $etag );
}
- $this->SetContentType("text/icalendar");
- $headers = $this->DoRequest($url);
+ $this->SetContentType('text/icalendar; encoding="utf-8"');
+ $this->DoRequest($url);
- /**
- * RSCDS will always return the real etag on PUT. Other CalDAV servers may need
- * more work, but we are assuming we are running against RSCDS in this case.
- */
- $etag = preg_replace( '/^.*Etag: "?([^"\r\n]+)"?\r?\n.*/is', '$1', $headers );
+ $etag = null;
+ if ( preg_match( '{^ETag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
+ if ( !isset($etag) || $etag == '' ) {
+ printf( "No etag in:\n%s\n", $this->httpResponseHeaders );
+ $this->DoHEADRequest( $url );
+ if ( preg_match( '{^Etag:\s+"([^"]*)"\s*$}im', $this->httpResponseHeaders, $matches ) ) $etag = $matches[1];
+ if ( !isset($etag) || $etag == '' ) {
+ printf( "Still No etag in:\n%s\n", $this->httpResponseHeaders );
+ }
+ }
return $etag;
}
@@ -431,7 +474,7 @@ class CalDAVClient {
foreach( $this->xmltags[$tagname] AS $k => $v ) {
$j = $v + 1;
if ( $this->xmlnodes[$j]['tag'] == 'DAV::href' ) {
- return $this->xmlnodes[$j]['value'];
+ return rawurldecode($this->xmlnodes[$j]['value']);
}
}
return null;
@@ -439,7 +482,7 @@ class CalDAVClient {
/**
- * Return the href containing this property
+ * Return the href containing this property. Except only if it's inside a status != 200
*
* @param string $tagname The tag name of the property to find the href for
* @param integer $which Which instance of the tag should we use
@@ -447,13 +490,18 @@ class CalDAVClient {
function HrefForProp( $tagname, $i = 0 ) {
if ( isset($this->xmltags[$tagname]) && isset($this->xmltags[$tagname][$i]) ) {
$j = $this->xmltags[$tagname][$i];
- while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::prop' );
- if ( $j > 0 ) {
- while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' );
- if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
- return $this->xmlnodes[$j]['value'];
- }
+ while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' ) {
+// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
+ if ( $this->xmlnodes[$j]['tag'] == 'DAV::status' && $this->xmlnodes[$j]['value'] != 'HTTP/1.1 200 OK' ) return null;
}
+// printf( "Node[$j]: %s\n", $this->xmlnodes[$j]['tag']);
+ if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
+// printf( "Value[$j]: %s\n", $this->xmlnodes[$j]['value']);
+ return rawurldecode($this->xmlnodes[$j]['value']);
+ }
+ }
+ else {
+ printf( "xmltags[$tagname] or xmltags[$tagname][$i] is not set\n");
}
return null;
}
@@ -472,7 +520,7 @@ class CalDAVClient {
if ( $j > 0 ) {
while( $j-- > 0 && $this->xmlnodes[$j]['tag'] != 'DAV::href' );
if ( $j > 0 && isset($this->xmlnodes[$j]['value']) ) {
- return $this->xmlnodes[$j]['value'];
+ return rawurldecode($this->xmlnodes[$j]['value']);
}
}
}
@@ -552,7 +600,7 @@ class CalDAVClient {
while( $this->xmlnodes[++$v]['type'] != 'close' && $this->xmlnodes[$v]['tag'] != 'urn:ietf:params:xml:ns:caldav:calendar-home-set' ) {
// printf( "Tag: '%s' = '%s'\n", $this->xmlnodes[$v]['tag'], $this->xmlnodes[$v]['value']);
if ( $this->xmlnodes[$v]['tag'] == 'DAV::href' && isset($this->xmlnodes[$v]['value']) )
- $calendar_home[] = $this->xmlnodes[$v]['value'];
+ $calendar_home[] = rawurldecode($this->xmlnodes[$v]['value']);
}
}
@@ -581,7 +629,7 @@ class CalDAVClient {
}
foreach( $this->xmltags['DAV::href'] AS $i => $hnode ) {
- $href = $this->xmlnodes[$hnode]['value'];
+ $href = rawurldecode($this->xmlnodes[$hnode]['value']);
if ( !isset($calendar_urls[$href]) ) continue;
@@ -608,18 +656,47 @@ class CalDAVClient {
}
+ /**
+ * Find the calendars, from the calendar_home_set
+ */
+ function GetCalendarDetails( $url = null ) {
+ if ( isset($url) ) $this->SetCalendar($url);
+
+ $calendar_properties = array( 'resourcetype', 'displayname', 'http://calendarserver.org/ns/:getctag', 'urn:ietf:params:xml:ns:caldav:calendar-timezone', 'supported-report-set' );
+ $this->DoPROPFINDRequest( $this->calendar_url, $calendar_properties, 0);
+
+ $hnode = $this->xmltags['DAV::href'][0];
+ $href = rawurldecode($this->xmlnodes[$hnode]['value']);
+
+ $calendar = new CalendarInfo($href);
+ $ok_props = $this->GetOKProps($hnode);
+ foreach( $ok_props AS $k => $v ) {
+ $name = preg_replace( '{^.*:}', '', $v['tag'] );
+ if ( isset($v['value'] ) ) {
+ $calendar->{$name} = $v['value'];
+ }
+/* else {
+ printf( "Calendar property '%s' has no text content\n", $v['tag'] );
+ }*/
+ }
+
+ return $calendar;
+ }
+
+
/**
* Get all etags for a calendar
*/
function GetCollectionETags( $url = null ) {
if ( isset($url) ) $this->SetCalendar($url);
- $this->DoPROPFINDRequest( $this->calendar_url, array('getetag'), 1);
+ $this->DoPROPFINDRequest( $this->calendar_url, array('getetag','supported-report-set'), 1);
$etags = array();
if ( isset($this->xmltags['DAV::getetag']) ) {
foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) {
- $events[$this->HrefForProp('DAV::getetag', $k)] = $v['value'];
+ $href = $this->HrefForProp('DAV::getetag', $k);
+ if ( isset($href) ) $etags[$href] = $this->xmlnodes[$v]['value'];
}
}
@@ -634,30 +711,33 @@ class CalDAVClient {
if ( isset($url) ) $this->SetCalendar($url);
- $hrefs = array();
+ $hrefs = '';
foreach( $event_hrefs AS $k => $href ) {
- $hrefs .= ''.rawurlencode($url).'';
+ $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
+ $hrefs .= ''.$href.'';
}
$this->body = <<
$hrefs
-
+
EOXML;
$this->requestMethod = "REPORT";
$this->SetContentType("text/xml");
$this->DoRequest( $this->calendar_url );
- $etags = array();
- if ( isset($this->xmltags['DAV::getetag']) ) {
- foreach( $this->xmltags['DAV::getetag'] AS $k => $v ) {
- $events[$this->HrefForProp('DAV::getetag', $k)] = $v['value'];
+ $events = array();
+ if ( isset($this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data']) ) {
+ foreach( $this->xmltags['urn:ietf:params:xml:ns:caldav:calendar-data'] AS $k => $v ) {
+ $href = $this->HrefForProp('urn:ietf:params:xml:ns:caldav:calendar-data', $k);
+// echo "Calendar-data:\n"; print_r($this->xmlnodes[$v]);
+ $events[$href] = $this->xmlnodes[$v]['value'];
}
}
- return $etags;
+ return $events;
}
@@ -704,7 +784,7 @@ EOXML;
}
break;
case 'DAV::href':
- $response['href'] = basename( $v['value'] );
+ $response['href'] = basename( rawurldecode($v['value']) );
break;
case 'DAV::getetag':
$response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
@@ -830,12 +910,12 @@ EOFILTER;
* Get the calendar entry by HREF
*
* @param string $href The href from a call to GetEvents or GetTodos etc.
- * @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
*
* @return string The iCalendar of the calendar entry
*/
- function GetEntryByHref( $href, $relative_url = '' ) {
- return $this->DoGETRequest( $relative_url . $href );
+ function GetEntryByHref( $href ) {
+ $href = str_replace( rawurlencode('/'),'/',rawurlencode($href));
+ return $this->DoGETRequest( $href );
}
}
diff --git a/scripts/sync-pull.php b/scripts/sync-pull.php
deleted file mode 100755
index c44dba4f..00000000
--- a/scripts/sync-pull.php
+++ /dev/null
@@ -1,174 +0,0 @@
-#!/usr/bin/php
-sync_all = false;
-
-function parse_arguments() {
- global $args;
-
- $opts = getopt( 'u:U:p:a' );
- foreach( $opts AS $k => $v ) {
- switch( $k ) {
- case 'u': $args->url = $v; break;
- case 'U': $args->user = $v; break;
- case 'p': $args->pass = $v; break;
- case 'a': $args->sync_all = 1; break;
- case 'c': $args->local_collection_path = $v; break;
- default: $args->{$k} = $v;
- }
- }
-}
-
-parse_arguments();
-
-// E.g.
-// sync-pull.php -U andrew@example.net -p 53cret -u https://www.google.com/calendar/dav/andrew@example.net/events -c /andrew/gsync/
-//
-
-if ( !preg_match( '{/$}', $args->url ) ) $args->url .= '/';
-
-$caldav = new CalDAVClient( $args->url, $args->user, $args->pass );
-
-// This will find the 'Principal URL' which we can query for user-related
-// properties.
-$principal_url = $caldav->FindPrincipal($args->url);
-
-// This will find the 'Calendar Home URL' which will be the folder(s) which
-// contain all of the user's calendars
-$calendar_home_set = $caldav->FindCalendarHome();
-
-$calendar = null;
-
-// This will go through the calendar_home_set and find all of the users
-// calendars on the remote server.
-$calendars = $caldav->FindCalendars();
-if ( count($calendars) < 1 ) {
- printf( "No calendars found based on '%s'\n", $args->url );
-}
-
-// Now we have all of the remote calendars, we will look for the URL that
-// matches what we were originally supplied. While this seems laborious
-// because we already have it, it means we could provide a match in some
-// other way (e.g. on displayname) and we could also present a list to
-// the user which is built from following the above process.
-foreach( $calendars AS $k => $a_calendar ) {
- if ( rawurldecode($a_calendar->url) == rawurldecode($args->url) ) $calendar = $a_calendar;
-}
-if ( !isset($calendar) ) $calendar = $calendars[0];
-
-// In reality we could have omitted all of the above parts, If we really do
-// know the correct URL at the start.
-printf( "Calendar '%s' is at %s\n", $calendar->displayname, $calendar->url );
-
-// Generate a consistent filename for our synchronisation cache
-$sync_cache_filename = md5($args->url . $args->user . $calendar->url);
-
-// Do we just need to sync everything across and overwrite all the local stuff?
-$sync_all = ( !file_exists($sync_cache_filename) || $args->sync_all);
-
-if ( ! $sync_all ) {
- /**
- * Read a structure out of the cache file containing:
- * server_getctag - A collection tag (string)
- * server_etags - An array of event tags (strings) keyed on filename, from the server
- * local_etags - An array of event tags (strings) keyed on filename, from local DAViCal
- */
- $cache = unserialize( file_get_contents($sync_cache_filename) );
-
- // First compare the ctag for the calendar
- if ( isset($cache) && isset($cache->server_ctag) && isset($calendar->getctag) && $calendar->getctag == $cache->server_ctag ) {
- printf( 'No changes to calendar "%s"'."\n", $args->url );
- exit(0);
- }
-}
-if ( !isset($cache) || !isset($cache->server_ctag) ) $sync_all = true;
-
-// Everything now will be at our calendar URL
-$caldav->SetCalendar($calendar->url);
-
-// So it seems we do need to sync. We now need to check each individual event
-// which might have changed, so we pull a list of event etags from the server.
-$server_etags = $caldav->GetCollectionETags($calendar->url);
-
-$newcache = (object) array( 'server_ctag' => $calendar->getctag, 'server_etags' => array(), 'local_etags' => array() );
-
-if ( $sync_all ) {
- // The easy case. Sync them all, delete nothing
- $insert_urls = array_flip($server_etags);
- $update_urls = array();
- $delete_urls = array();
- foreach( $server_etags AS $href => $etag ) {
- $fname = preg_replace('{^.*/}', '', $href);
- $newcache->server_etags[$fname] = $etag;
- }
-}
-else {
- // Only sync the ones where the etag has changed. Delete any that are no
- // longer present at the remote end.
- $insert_urls = array();
- $update_urls = array();
- foreach( $server_etags AS $href => $etag ) {
- $fname = preg_replace('{^.*/}', '', $href);
- $newcache->server_etags[$fname] = $etag;
- if ( isset($cache->server_etags[$fname]) ) {
- $cache_etag = $cache->server_etags[$fname];
- unset($cache->server_etags[$fname]);
- if ( $cache_etag == $etag ) continue;
- $update_urls[] = $href;
- }
- else {
- $insert_urls[] = $href;
- }
- }
- $delete_urls = array_flip($cache->server_etags);
-}
-
-
-// Fetch the calendar data
-$events = $caldav->CalendarMultiget( array_merge( $insert_urls, $update_urls) );
-
-/**
-* @TODO: We should really check for collisions locally in case the local ETag is
-* also different to the one we saved earlier.
-*/
-// Update the local system with these events
-foreach( $events AS $href => $event ) {
- // Do what we need to write $v into the local calendar we are syncing to
- // at the
- $fname = preg_replace('{^.*/}', '', $href);
- $local_fname = $args->local_collection_path . $fname;
- simple_write_resource( $local_fname, $event, (isset($insert_urls[$href]) ? 'INSERT' : 'UPDATE') );
-}
-
-/**
-* @TODO: We should not delete locally in the case that the local ETag is different
-* to the one we saved earlier.
-*/
-// Delete any events which were present in our cache, but are not on the master server
-foreach( $delete_urls AS $k => $v ) {
- $fname = preg_replace('{^.*/}', '', $href);
- $local_fname = $args->local_collection_path . $fname;
- $qry = new AwlQuery('DELETE FROM caldav_data WHERE dav_name = :dav_name', array( ':dav_name' => $local_fname ) );
- $qry->Exec('sync_pull',__LINE__,__FILE__);
-}
-
-// Now (re)write the cache file reflecting the current state.
-$cache_file = fopen($sync_cache_filename, 'w');
-fwrite( $cache_file, serialize($newcache) );
-fclose($cache_file);
diff --git a/scripts/sync-remote-caldav.php b/scripts/sync-remote-caldav.php
new file mode 100755
index 00000000..2bf1bbd9
--- /dev/null
+++ b/scripts/sync-remote-caldav.php
@@ -0,0 +1,346 @@
+#!/usr/bin/php
+sync_all = false; // Back to basics and sync everything into one mess
+$args->local_changes_win = true; // If true, and something has changed at both places, our local update will overwrite the remote
+
+$args->sync_in = false; // If true, remote changes will be applied locally
+$args->sync_out = false; // If true, local changes will be applied remotely
+
+$args->cache_directory = '.sync-cache';
+
+function parse_arguments() {
+ global $args;
+
+ $opts = getopt( 'u:U:p:c:w:ioa' );
+ foreach( $opts AS $k => $v ) {
+ switch( $k ) {
+ case 'u': $args->url = $v; break;
+ case 'U': $args->user = $v; break;
+ case 'p': $args->pass = $v; break;
+ case 'a': $args->sync_all = true; break;
+ case 'c': $args->local_collection_path = $v; break;
+ case 'w': $args->local_changes_win = (strtolower($v) != 'remote' ); break;
+ case 'i': $args->sync_in = true; break;
+ case 'o': $args->sync_out = true; break;
+ default: $args->{$k} = $v;
+ }
+ }
+}
+
+parse_arguments();
+
+
+
+if ( !preg_match('{/$}', $args->local_collection_path) ) $args->local_collection_path .= '/';
+if ( !preg_match('{^/[^/]+/[^/]+/$}', $args->local_collection_path) ) {
+ printf( "The local URL of '%s' looks wrong. It should be formed as '/username/collection/'\n", $args->local_collection_path );
+}
+
+if ( !preg_match('{/$}', $args->url) ) $args->url .= '/';
+
+$caldav = new CalDAVClient( $args->url, $args->user, $args->pass );
+
+// // This will find the 'Principal URL' which we can query for user-related
+// // properties.
+// $principal_url = $caldav->FindPrincipal($args->url);
+//
+// // This will find the 'Calendar Home URL' which will be the folder(s) which
+// // contain all of the user's calendars
+// $calendar_home_set = $caldav->FindCalendarHome();
+//
+// $calendar = null;
+//
+// // This will go through the calendar_home_set and find all of the users
+// // calendars on the remote server.
+// $calendars = $caldav->FindCalendars();
+// if ( count($calendars) < 1 ) {
+// printf( "No calendars found based on '%s'\n", $args->url );
+// }
+//
+// // Now we have all of the remote calendars, we will look for the URL that
+// // matches what we were originally supplied. While this seems laborious
+// // because we already have it, it means we could provide a match in some
+// // other way (e.g. on displayname) and we could also present a list to
+// // the user which is built from following the above process.
+// foreach( $calendars AS $k => $a_calendar ) {
+// if ( $a_calendar->url == $args->url ) $calendar = $a_calendar;
+// }
+// if ( !isset($calendar) ) $calendar = $calendars[0];
+
+// In reality we could have omitted all of the above parts, If we really do
+// know the correct URL at the start.
+
+// Everything now will be at our calendar URL
+$caldav->SetCalendar($args->url);
+
+$calendar = $caldav->GetCalendarDetails();
+
+printf( "Remote calendar '%s' is at %s\n", $calendar->displayname, $calendar->url );
+
+// Generate a consistent filename for our synchronisation cache
+if ( ! file_exists($args->cache_directory) && ! is_dir($args->cache_directory) ) {
+ mkdir($args->cache_directory, 0750 ); // Not incredibly sensitive file contents - URLs and ETags
+}
+$sync_cache_filename = $args->cache_directory .'/'. md5($args->user . $calendar->url);
+
+// Do we just need to sync everything across and overwrite all the local stuff?
+$sync_all = ( !file_exists($sync_cache_filename) || $args->sync_all);
+$sync_in = false;
+$sync_out = false;
+if ( $args->sync_in || !$args->sync_out ) $sync_in = true;
+if ( $args->sync_out || !$args->sync_in ) $sync_out = true;
+
+
+if ( ! $sync_all ) {
+ /**
+ * Read a structure out of the cache file containing:
+ * server_getctag - A collection tag (string) from the remote server
+ * local_getctag - A collection tag (string) from the local DB
+ * server_etags - An array of event tags (strings) keyed on filename, from the server
+ * local_etags - An array of event tags (strings) keyed on filename, from local DAViCal
+ */
+ $cache = unserialize( file_get_contents($sync_cache_filename) );
+
+ // First compare the ctag for the calendar
+ if ( isset($cache) && isset($cache->server_ctag) && isset($calendar->getctag) && $calendar->getctag == $cache->server_ctag ) {
+ printf( 'No changes to remote calendar "%s" at "%s"'."\n", $calendar->displayname, $calendar->url );
+ $sync_in = false;
+ }
+
+ $qry = new AwlQuery('SELECT collection_id, dav_displayname AS displayname, dav_etag AS getctag FROM collection WHERE dav_name = :collection_dav_name', array(':collection_dav_name' => $args->local_collection_path) );
+ if ( $qry->Exec('sync-pull',__LINE__,__FILE__) && $qry->rows() > 0 ) {
+ $local_calendar = $qry->Fetch();
+
+ // First compare the ctag for the calendar
+ if ( isset($cache) && isset($cache->local_ctag) && isset($local_calendar->getctag) && $local_calendar->getctag == $cache->local_ctag ) {
+ printf( 'No changes to local calendar "%s" at "%s"'."\n", $local_calendar->displayname, $args->local_collection_path );
+ $sync_out = false;
+ }
+ }
+}
+if ( !isset($cache) || !isset($cache->server_ctag) ) $sync_all = true;
+
+$remote_event_prefix = preg_replace('{^https?://[^/]+/}', '/', $calendar->url);
+$insert_urls = array();
+$update_urls = array();
+$local_delete_urls = array();
+$server_delete_urls = array();
+$push_urls = array();
+$push_events = array();
+
+$newcache = (object) array( 'server_ctag' => $calendar->getctag,
+ 'local_ctag' => (isset($local_calendar->getctag) ? $local_calendar->getctag : null),
+ 'server_etags' => array(), 'local_etags' => array() );
+if ( isset($cache) ) {
+ if ( !$sync_in && isset($cache->server_etags) ) $newcache->server_etags = $cache->server_etags;
+ if ( !$sync_out && isset($cache->local_etags) ) $newcache->local_etags = $cache->local_etags;
+}
+
+if ( $sync_in ) {
+ // So it seems we do need to sync. We now need to check each individual event
+ // which might have changed, so we pull a list of event etags from the server.
+ $server_etags = $caldav->GetCollectionETags();
+ // printf( "\nGetCollectionEtags Response:\n%s\n", $caldav->GetXmlResponse() );
+ // print_r( $server_etags );
+
+
+
+ if ( $sync_all ) {
+ // The easy case. Sync them all, delete nothing
+ $insert_urls = $server_etags;
+ foreach( $server_etags AS $href => $etag ) {
+ $fname = preg_replace('{^.*/}', '', $href);
+ $newcache->server_etags[$fname] = $etag;
+ printf( 'Need to pull "%s"'."\n", $href );
+ }
+ }
+ else {
+ // Only sync the ones where the etag has changed. Delete any that are no
+ // longer present at the remote end.
+ foreach( $server_etags AS $href => $etag ) {
+ $fname = preg_replace('{^.*/}', '', $href);
+ $newcache->server_etags[$fname] = $etag;
+ if ( isset($cache->server_etags[$fname]) ) {
+ $cache_etag = $cache->server_etags[$fname];
+ unset($cache->server_etags[$fname]);
+ if ( $cache_etag == $etag ) continue;
+ $update_urls[$href] = 1;
+ printf( 'Need to pull to update "%s"'."\n", $href );
+ }
+ else {
+ $insert_urls[$href] = 1;
+ printf( 'Need to pull to insert "%s"'."\n", $href );
+ }
+ }
+ $local_delete_urls = $cache->server_etags;
+ }
+
+
+ // Fetch the calendar data
+ $events = $caldav->CalendarMultiget( array_merge( array_keys($insert_urls), array_keys($update_urls)) );
+ // printf( "\nCalendarMultiget Request:\n%s\n Response:\n%s\n", $caldav->GetXmlRequest(), $caldav->GetXmlResponse() );
+ // print_r($events);
+
+ if ( !preg_match( '{/$}', $remote_event_prefix) ) $remote_event_prefix .= '/';
+}
+
+if ( $sync_out ) {
+ /**
+ * This is a fairly tricky bit. We find local changes and check to see if they
+ * are collisions. We actually have to check the data for a collision, since the
+ * real data may in fact be identical, e.g. because of the -a option or something.
+ *
+ * Once we have verified that the target objects actually *are* different, then:
+ * Change vs No change => The change is propagated to the other server
+ * DELETE vs UPDATE/INSERT => DELETE always loses
+ * UPDATE vs UPDATE => pick the winner according to arbitrary setting (see top of file)
+ * INSERT vs INSERT => pick the winner according to arbitrary setting (see top of file) v. unlikely
+ */
+ // Read the local ETag from DAViCal.
+ $qry = new AwlQuery( 'SELECT dav_name, dav_etag, caldav_data FROM caldav_data WHERE collection_id = (SELECT collection_id FROM collection WHERE dav_name = :collection_dav_name)',
+ array(':collection_dav_name' => $args->local_collection_path) );
+ if ( $qry->Exec('sync-pull',__LINE__,__FILE__) && $qry->rows() > 0 ) {
+ $local_etags = array();
+ while( $local = $qry->Fetch() ) {
+ $fname = preg_replace('{^.*/}', '', $local->dav_name);
+ $newcache->local_etags[$fname] = $local->dav_etag;
+ if ( !$sync_all && isset($cache->local_etags[$fname]) ) {
+ $cache_etag = $cache->local_etags[$fname];
+ unset($cache->local_etags[$fname]);
+ if ( $cache_etag == $local->dav_etag ) continue;
+ }
+ if ( isset($insert_urls[$remote_event_prefix.$fname]) ) {
+ if ( $local->caldav_data == $events[$remote_event_prefix.$fname] ) {
+ // Not actually changed. Ignore it at *both* ends!
+ unset($insert_urls[$remote_event_prefix.$fname]);
+ continue;
+ }
+ if ( $args->local_changes_win )
+ unset($insert_urls[$remote_event_prefix.$fname]);
+ else
+ continue;
+ }
+ if ( isset($update_urls[$remote_event_prefix.$fname]) ) {
+ if ( $local->caldav_data == $events[$remote_event_prefix.$fname] ) {
+ // Not actually changed. Ignore it at *both* ends!
+ unset($update_urls[$remote_event_prefix.$fname]);
+ continue;
+ }
+ if ( $args->local_changes_win )
+ unset($update_urls[$remote_event_prefix.$fname]);
+ else
+ continue;
+ }
+ $push_urls[$fname] = (isset($cache->server_etags[$remote_event_prefix.$fname]) ? $cache->server_etags[$remote_event_prefix.$fname] : '*');
+ $push_events[$fname] = $local->caldav_data;
+ printf( 'Need to push "%s"'."\n", $local->dav_name );
+ }
+
+ if ( !$sync_all ) {
+ foreach( $cache->local_etags AS $href => $etag ) {
+ $fname = preg_replace('{^.*/}', '', $local->dav_name);
+ if ( !isset($insert_urls[$remote_event_prefix.$fname])
+ && !isset($update_urls[$remote_event_prefix.$fname])
+ && isset($cache->server_etags[$remote_event_prefix.$fname]) ) {
+ $server_delete_urls[$fname] = $cache->server_etags[$remote_event_prefix.$fname];
+ }
+ }
+ }
+ }
+}
+
+
+if ( $sync_in ) {
+ // Delete any local events which have been removed from the remote server
+ foreach( $local_delete_urls AS $k => $v ) {
+ $fname = preg_replace('{^.*/}', '', $href);
+ $local_fname = $args->local_collection_path . $fname;
+ $qry = new AwlQuery('DELETE FROM caldav_data WHERE dav_name = :dav_name', array( ':dav_name' => $local_fname ) );
+ $qry->Exec('sync_pull',__LINE__,__FILE__);
+ unset($newcache->local_etags[$fname]);
+ }
+
+
+ unset($c->dbg['querystring']);
+ // Update the local system with events that are new or updated on the remote server
+ foreach( $events AS $href => $event ) {
+ // Do what we need to write $v into the local calendar we are syncing to
+ // at the
+ $fname = preg_replace('{^.*/}', '', $href);
+ $local_fname = $args->local_collection_path . $fname;
+ simple_write_resource( $local_fname, $event, (isset($insert_urls[$href]) ? 'INSERT' : 'UPDATE') );
+ $newcache->local_etags[$fname] = md5($event);
+ }
+
+ $qry = new AwlQuery('SELECT collection_id, dav_displayname AS displayname, dav_etag AS getctag FROM collection WHERE dav_name = :collection_dav_name', array(':collection_dav_name' => $args->local_collection_path) );
+ if ( $qry->Exec('sync-pull',__LINE__,__FILE__) && $qry->rows() > 0 ) {
+ $local_calendar = $qry->Fetch();
+ if ( isset($local_calendar->getctag) ) $newcache->local_ctag = $local_calendar->getctag;
+ }
+}
+
+if ( $sync_out ) {
+ // Delete any remote events which have been removed from the local server
+ foreach( $server_delete_urls AS $href => $etag ) {
+ $caldav->DoDELETERequest( $args->url . $href, $etag );
+ printf( "\nDELETE Response:\n%s\n", $caldav->GetResponseHeaders() );
+ unset($newcache->server_etags[$fname]);
+ }
+
+ // Push locally updated events to the remote server
+ foreach( $push_urls AS $href => $etag ) {
+ $new_etag = $caldav->DoPUTRequest( $args->url . $href, $push_events[$href], $etag );
+ printf( "\nPUT:\n%s\nResponse:\n%s\n", $caldav->GetHttpRequest(), $caldav->GetResponseHeaders() );
+ $newcache->server_etags[$fname] = $new_etag;
+ }
+
+ $calendar = $caldav->GetCalendarDetails();
+ if ( isset($calendar->getctag) ) $newcache->server_ctag = $calendar->getctag;
+}
+
+// Now (re)write the cache file reflecting the current state.
+$cache_file = fopen($sync_cache_filename, 'w');
+fwrite( $cache_file, serialize($newcache) );
+fclose($cache_file);
+
+print_r($newcache);
\ No newline at end of file