#!/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);