From 701e96e1b21c3b23d638be8da3e0824784ff6e11 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Thu, 11 Mar 2010 23:40:47 +1300 Subject: [PATCH] Some updates to the caldav client library and an example script. --- inc/caldav-client-v2.php | 184 +++++++++++++----- scripts/sync-pull.php | 174 ----------------- scripts/sync-remote-caldav.php | 346 +++++++++++++++++++++++++++++++++ 3 files changed, 478 insertions(+), 226 deletions(-) delete mode 100755 scripts/sync-pull.php create mode 100755 scripts/sync-remote-caldav.php 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