diff --git a/import.php b/import.php index 19ca218..1ba579f 100644 --- a/import.php +++ b/import.php @@ -35,6 +35,14 @@ $_REQUEST['schedule'] = urldecode($argv[3]); } +/** + * Check to see if a URL exists + **/ +function urlExists($url) { + $handle = fopen($url, 'r'); + return $handle !== false; +} + /** * compute the calendar context for the canvas object based on its URL **/ @@ -124,351 +132,361 @@ function filterEvent($event, $calendarCache) { // TODO: need to do OAuth here, so that users are forced to authenticate to verify that they have permission to update these calendars! if ($canvasContext = getCanvasContext($_REQUEST['canvas_url'])) { - /* look up the canvas object -- mostly to make sure that it exists! */ - if ($canvasObject = callCanvasApi(CANVAS_API_GET, $canvasContext['verification_url'])) { - - /* calculate the unique pairing ID of this ICS feed and canvas object */ - $pairingHash = getPairingHash($_REQUEST['cal'], $canvasContext['canonical_url']); - - debugFlag('START', getSyncTimestamp()); - - /* tell users that it's started and to cool their jets */ - displayPage(' -
The calendar import that you requested has begun. You may leave this page at anytime. You can see the progress of the import by visiting this calendar in Canvas.
" - ); + /* check ICS feed to be sure it exists */ + if(urlExists($_REQUEST['cal'])) { + /* look up the canvas object -- mostly to make sure that it exists! */ + if ($canvasObject = callCanvasApi(CANVAS_API_GET, $canvasContext['verification_url'])) { - /* use phpicalendar to parse the ICS feed into $master_array */ - define('BASE', './phpicalendar/'); - require_once(BASE . 'functions/date_functions.php'); - require_once(BASE . 'functions/init.inc.php'); - require_once(BASE . 'functions/ical_parser.php'); - displayError( - $master_array, - false, null, null, - DEBUGGING_GENERAL - ); - - /* log this pairing in the database cache, if it doesn't already exist */ - $calendarCacheResponse = mysqlQuery(" - SELECT * - FROM `calendars` - WHERE - `id` = '$pairingHash' - "); - $calendarCache = $calendarCacheResponse->fetch_assoc(); + /* calculate the unique pairing ID of this ICS feed and canvas object */ + $pairingHash = getPairingHash($_REQUEST['cal'], $canvasContext['canonical_url']); + + debugFlag('START', getSyncTimestamp()); - /* if the calendar is already cached, just update the sync timestamp */ - if ($calendarCache) { - mysqlQuery(" - UPDATE `calendars` - SET - `synced` = '" . getSyncTimestamp() . "' + /* tell users that it's started and to cool their jets */ + displayPage(' +The calendar import that you requested has begun. You may leave this page at anytime. You can see the progress of the import by visiting this calendar in Canvas.
" + ); + + /* use phpicalendar to parse the ICS feed into $master_array */ + define('BASE', './phpicalendar/'); + require_once(BASE . 'functions/date_functions.php'); + require_once(BASE . 'functions/init.inc.php'); + require_once(BASE . 'functions/ical_parser.php'); + displayError( + $master_array, + false, null, null, + DEBUGGING_GENERAL + ); + + /* log this pairing in the database cache, if it doesn't already exist */ + $calendarCacheResponse = mysqlQuery(" + SELECT * + FROM `calendars` WHERE `id` = '$pairingHash' "); - } else { - mysqlQuery(" - INSERT INTO `calendars` - ( - `id`, - `name`, - `ics_url`, - `canvas_url`, - `synced`, - `enable_regexp_filter`, - `include_regexp`, - `exclude_regexp` - ) - VALUES ( - '$pairingHash', - '" . addslashes($master_array['calendar_name']) . "', - '{$_REQUEST['cal']}', - '{$canvasContext['canonical_url']}', - '" . getSyncTimestamp() . "', - '" . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER) . "', - " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ? "'" . addslashes($_REQUEST['include_regexp']) . "'" : 'NULL') . ", - " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ? "'" . addslashes($_REQUEST['exclude_regexp']) . "'" : 'NULL') . " - ) + $calendarCache = $calendarCacheResponse->fetch_assoc(); + + /* if the calendar is already cached, just update the sync timestamp */ + if ($calendarCache) { + mysqlQuery(" + UPDATE `calendars` + SET + `synced` = '" . getSyncTimestamp() . "' + WHERE + `id` = '$pairingHash' + "); + } else { + mysqlQuery(" + INSERT INTO `calendars` + ( + `id`, + `name`, + `ics_url`, + `canvas_url`, + `synced`, + `enable_regexp_filter`, + `include_regexp`, + `exclude_regexp` + ) + VALUES ( + '$pairingHash', + '" . addslashes($master_array['calendar_name']) . "', + '{$_REQUEST['cal']}', + '{$canvasContext['canonical_url']}', + '" . getSyncTimestamp() . "', + '" . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER) . "', + " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ? "'" . addslashes($_REQUEST['include_regexp']) . "'" : 'NULL') . ", + " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ? "'" . addslashes($_REQUEST['exclude_regexp']) . "'" : 'NULL') . " + ) + "); + } + + /* refresh calendar information from cache database */ + $calendarCacheResponse = mysqlQuery(" + SELECT * + FROM `calendars` + WHERE + `id` = '$pairingHash' "); - } - - /* refresh calendar information from cache database */ - $calendarCacheResponse = mysqlQuery(" - SELECT * - FROM `calendars` - WHERE - `id` = '$pairingHash' - "); - $calendarCache = $calendarCacheResponse->fetch_assoc(); - - /* walk through $master_array and update the Canvas calendar to match the - ICS feed, caching changes in the database */ - // TODO: would it be worth the performance improvement to just process things from today's date forward? (i.e. ignore old items, even if they've changed...) - foreach($master_array as $date => $times) { - if (date_create_from_format('Ymd', $date)) { - foreach($times as $time => $uids) { - foreach($uids as $uid => $event) { - /* urldecode all of the fields of the event, for easier processing! */ - foreach ($event as $key => $value) { - $event[$key] = urldecode($value); - } - - /* does this event already exist in Canvas? */ - $eventHash = getEventHash($date, $time, $uid, $event); - - /* filter event -- clean up formatting and check for regexp filtering */ - $event = filterEvent($event, $calendarCache); - - /* if the event should be included... */ - if ($event) { - - /* have we cached this event already? */ - $eventCacheResponse = mysqlQuery(" - SELECT * - FROM `events` - WHERE - `calendar` = '{$calendarCache['id']}' AND - `event_hash` = '$eventHash' - "); - + $calendarCache = $calendarCacheResponse->fetch_assoc(); - /* if we already have the event cached in its current form, just update - the timestamp */ - $eventCache = $eventCacheResponse->fetch_assoc(); - if (DEBUGGING & DEBUGGING_MYSQL) displayError($eventCache); - if ($eventCache) { - mysqlQuery(" - UPDATE `events` - SET - `synced` = '" . getSyncTimestamp() . "' + /* walk through $master_array and update the Canvas calendar to match the + ICS feed, caching changes in the database */ + // TODO: would it be worth the performance improvement to just process things from today's date forward? (i.e. ignore old items, even if they've changed...) + foreach($master_array as $date => $times) { + if (date_create_from_format('Ymd', $date)) { + foreach($times as $time => $uids) { + foreach($uids as $uid => $event) { + /* urldecode all of the fields of the event, for easier processing! */ + foreach ($event as $key => $value) { + $event[$key] = urldecode($value); + } + + /* does this event already exist in Canvas? */ + $eventHash = getEventHash($date, $time, $uid, $event); + + /* filter event -- clean up formatting and check for regexp filtering */ + $event = filterEvent($event, $calendarCache); + + /* if the event should be included... */ + if ($event) { + + /* have we cached this event already? */ + $eventCacheResponse = mysqlQuery(" + SELECT * + FROM `events` WHERE - `id` = '{$eventCache['id']}' + `calendar` = '{$calendarCache['id']}' AND + `event_hash` = '$eventHash' "); - - /* otherwise, add this new event and cache it */ - } else { - /* multi-day event instance start times need to be changed to _this_ date */ - $start = new DateTime("@{$event['start_unixtime']}"); - $start->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE)); - $start->setDate(substr($date, 0, 4), substr($date, 4, 2), substr($date, 6, 2)); - $end = new DateTime("@{$event['end_unixtime']}"); - $end->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE)); - $end->setDate(substr($date, 0, 4), substr($date, 4, 2), substr($date, 6, 2)); - - $calendarEvent = callCanvasApi( - CANVAS_API_POST, - "/calendar_events", - array( - 'calendar_event[context_code]' => "{$canvasContext['context']}_{$canvasObject['id']}", - 'calendar_event[title]' => $event['event_text'], - 'calendar_event[description]' => $event['description'], - 'calendar_event[start_at]' => $start->format(CANVAS_TIMESTAMP_FORMAT), - 'calendar_event[end_at]' => $end->format(CANVAS_TIMESTAMP_FORMAT), - 'calendar_event[location_name]' => $event['location'], - 'as_user_id' => ($canvasContext['context'] == 'user' ? $canvasObject['id'] : '') // TODO: this feels skeevy -- like the empty string will break - ) - ); - $icalEventJson = json_encode($event); - $calendarEventJson = json_encode($calendarEvent); - mysqlQuery(" - INSERT INTO `events` - ( - `calendar`, - `calendar_event[id]`, - `event_hash`, - `synced` - ) - VALUES ( - '{$calendarCache['id']}', - '{$calendarEvent['id']}', - '$eventHash', - '" . getSyncTimestamp() . "' + + /* if we already have the event cached in its current form, just update + the timestamp */ + $eventCache = $eventCacheResponse->fetch_assoc(); + if (DEBUGGING & DEBUGGING_MYSQL) displayError($eventCache); + if ($eventCache) { + mysqlQuery(" + UPDATE `events` + SET + `synced` = '" . getSyncTimestamp() . "' + WHERE + `id` = '{$eventCache['id']}' + "); + + /* otherwise, add this new event and cache it */ + } else { + /* multi-day event instance start times need to be changed to _this_ date */ + $start = new DateTime("@{$event['start_unixtime']}"); + $start->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE)); + $start->setDate(substr($date, 0, 4), substr($date, 4, 2), substr($date, 6, 2)); + + $end = new DateTime("@{$event['end_unixtime']}"); + $end->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE)); + $end->setDate(substr($date, 0, 4), substr($date, 4, 2), substr($date, 6, 2)); + + $calendarEvent = callCanvasApi( + CANVAS_API_POST, + "/calendar_events", + array( + 'calendar_event[context_code]' => "{$canvasContext['context']}_{$canvasObject['id']}", + 'calendar_event[title]' => $event['event_text'], + 'calendar_event[description]' => $event['description'], + 'calendar_event[start_at]' => $start->format(CANVAS_TIMESTAMP_FORMAT), + 'calendar_event[end_at]' => $end->format(CANVAS_TIMESTAMP_FORMAT), + 'calendar_event[location_name]' => $event['location']/*, + 'as_user_id' => ($canvasContext['context'] == 'user' ? $canvasObject['id'] : '') // TODO: this feels skeevy -- like the empty string will break */ ) - "); + ); + $icalEventJson = json_encode($event); + $calendarEventJson = json_encode($calendarEvent); + mysqlQuery(" + INSERT INTO `events` + ( + `calendar`, + `calendar_event[id]`, + `event_hash`, + `synced` + ) + VALUES ( + '{$calendarCache['id']}', + '{$calendarEvent['id']}', + '$eventHash', + '" . getSyncTimestamp() . "' + ) + "); + } } } } } } - } - - /* clean out previously synced events that are no longer correct */ - $deletedEventsResponse = mysqlQuery(" - SELECT * FROM `events` - WHERE - `calendar` = '{$calendarCache['id']}' AND - `synced` != '" . getSyncTimestamp() . "' - "); - while ($deletedEventCache = $deletedEventsResponse->fetch_assoc()) { - try { - $deletedEvent = callCanvasApi( - CANVAS_API_DELETE, - "/calendar_events/{$deletedEventCache['calendar_event[id]']}", - array( - 'cancel_reason' => getSyncTimestamp(), - 'as_user_id' => ($canvasContext['context'] == 'user' ? $canvasObject['id'] : '') // TODO: this feels skeevy -- like the empty string will break - ), - CANVAS_API_EXCEPTION_CLIENT - ); - } catch (Pest_Unauthorized $e) { - /* if the event has been deleted in Canvas, we'll get an error when - we try to delete it a second time. We still need to delete it from - our cache database, however */ - debugFlag("Cache out-of-sync: calendar_event[{$deletedEventCache['calendar_event[id]']}] no longer exists and will be purged from cache."); - } catch (Pest_ClientError $e) { - displayError( - array( - 'Status' => $PEST->lastStatus(), - 'Error' => $PEST->lastBody(), - 'Verb' => $verb, - 'URL' => $url, - 'Data' => $data - ), - true, - 'API Client Error' - ); - exit; - } - mysqlQuery(" - DELETE FROM `events` - WHERE - `id` = '{$deletedEventCache['id']}' - "); - } - /* if this was a scheduled import (i.e. a sync), update that schedule */ - if (isset($_REQUEST['schedule'])) { - mysqlQuery(" - UPDATE `schedules` - SET - `synced` = '" . getSyncTimestamp() . "' + /* clean out previously synced events that are no longer correct */ + $deletedEventsResponse = mysqlQuery(" + SELECT * FROM `events` WHERE - `id` = '{$_REQUEST['schedule']}' + `calendar` = '{$calendarCache['id']}' AND + `synced` != '" . getSyncTimestamp() . "' "); - } - - /* are we setting up a regular synchronization? */ - if (isset($_REQUEST['sync']) && $_REQUEST['sync'] != SCHEDULE_ONCE) { - $shellArguments[INDEX_COMMAND] = dirname(__FILE__) . '/sync.sh'; - $shellArguments[INDEX_SCHEDULE] = $_REQUEST['sync']; - $shellArguments[INDEX_WEB_PATH] = 'http://localhost' . dirname($_SERVER['PHP_SELF']); - $crontab = null; - switch ($_REQUEST['sync']) { - case SCHEDULE_WEEKLY: { - $crontab = '0 0 * * 0'; - break; - } - case SCHEDULE_DAILY: { - $crontab = '0 0 * * *'; - break; - } - case SCHEDULE_HOURLY: { - $crontab = '0 * * * *'; - break; - } - case SCHEDULE_CUSTOM: { - $shellArguments[INDEX_SCHEDULE] = md5($_REQUEST['crontab'] . getSyncTimestamp()); - $crontab = trim($_REQUEST['crontab']); + while ($deletedEventCache = $deletedEventsResponse->fetch_assoc()) { + try { + $deletedEvent = callCanvasApi( + CANVAS_API_DELETE, + "/calendar_events/{$deletedEventCache['calendar_event[id]']}", + array( + 'cancel_reason' => getSyncTimestamp(), + 'as_user_id' => ($canvasContext['context'] == 'user' ? $canvasObject['id'] : '') // TODO: this feels skeevy -- like the empty string will break + ), + CANVAS_API_EXCEPTION_CLIENT + ); + } catch (Pest_Unauthorized $e) { + /* if the event has been deleted in Canvas, we'll get an error when + we try to delete it a second time. We still need to delete it from + our cache database, however */ + debugFlag("Cache out-of-sync: calendar_event[{$deletedEventCache['calendar_event[id]']}] no longer exists and will be purged from cache."); + } catch (Pest_ClientError $e) { + displayError( + array( + 'Status' => $PEST->lastStatus(), + 'Error' => $PEST->lastBody(), + 'Verb' => $verb, + 'URL' => $url, + 'Data' => $data + ), + true, + 'API Client Error' + ); + exit; } + mysqlQuery(" + DELETE FROM `events` + WHERE + `id` = '{$deletedEventCache['id']}' + "); } - /* schedule crontab trigger, if it doesn't already exist */ - $crontab .= ' ' . implode(' ', $shellArguments); - - /* thank you http://stackoverflow.com/a/4421284 ! */ - $crontabs = shell_exec('crontab -l'); - /* check to see if this sync is already scheduled */ - if (strpos($crontabs, $crontab) === false) { - $filename = md5(getSyncTimestamp()) . '.txt'; - file_put_contents("/tmp/$filename", $crontabs . $crontab . PHP_EOL); - shell_exec("crontab /tmp/$filename"); - debugFlag("added new schedule '" . $shellArguments[INDEX_SCHEDULE] . "' to crontab"); + /* if this was a scheduled import (i.e. a sync), update that schedule */ + if (isset($_REQUEST['schedule'])) { + mysqlQuery(" + UPDATE `schedules` + SET + `synced` = '" . getSyncTimestamp() . "' + WHERE + `id` = '{$_REQUEST['schedule']}' + "); } - /* try to make sure that we have execute access to sync.sh */ - chmod('sync.sh', 0775); - - /* add to the cache database schedule, replacing any schedules for this - calendar that are already there */ - $schedulesResponse = mysqlQuery(" - SELECT * - FROM `schedules` - WHERE - `calendar` = '{$calendarCache['id']}' - "); - - if ($schedule = $schedulesResponse->fetch_assoc()) { - - /* only need to worry if the cached schedule is different from the - new one we just set */ - if ($shellArguments[INDEX_SCHEDULE] != $schedule['schedule']) { - /* was this the last schedule to require this trigger? */ - $schedulesResponse = mysqlQuery(" - SELECT * - FROM `schedules` - WHERE - `calendar` != '{$calendarCache['id']}' AND - `schedule` == '{$schedule['schedule']}' - "); - /* we're the last one, delete it from crontab */ - if ($schedulesResponse->num_rows == 0) { - $crontabs = preg_replace("%^.*{$schedule['schedule']}.*" . PHP_EOL . '%', '', shell_exec('crontab -l')); - $filename = md5(getSyncTimestamp()) . '.txt'; - file_put_contents("/tmp/$filename", $crontabs); - shell_exec("crontab /tmp/$filename"); - debugFlag("removed unused schedule '{$schedule['schedule']}' from crontab"); + /* are we setting up a regular synchronization? */ + if (isset($_REQUEST['sync']) && $_REQUEST['sync'] != SCHEDULE_ONCE) { + $shellArguments[INDEX_COMMAND] = dirname(__FILE__) . '/sync.sh'; + $shellArguments[INDEX_SCHEDULE] = $_REQUEST['sync']; + $shellArguments[INDEX_WEB_PATH] = 'http://localhost' . dirname($_SERVER['PHP_SELF']); + $crontab = null; + switch ($_REQUEST['sync']) { + case SCHEDULE_WEEKLY: { + $crontab = '0 0 * * 0'; + break; + } + case SCHEDULE_DAILY: { + $crontab = '0 0 * * *'; + break; + } + case SCHEDULE_HOURLY: { + $crontab = '0 * * * *'; + break; + } + case SCHEDULE_CUSTOM: { + $shellArguments[INDEX_SCHEDULE] = md5($_REQUEST['crontab'] . getSyncTimestamp()); + $crontab = trim($_REQUEST['crontab']); } + } + + /* schedule crontab trigger, if it doesn't already exist */ + $crontab .= ' ' . implode(' ', $shellArguments); + + /* thank you http://stackoverflow.com/a/4421284 ! */ + $crontabs = shell_exec('crontab -l'); + /* check to see if this sync is already scheduled */ + if (strpos($crontabs, $crontab) === false) { + $filename = md5(getSyncTimestamp()) . '.txt'; + file_put_contents("/tmp/$filename", $crontabs . $crontab . PHP_EOL); + shell_exec("crontab /tmp/$filename"); + debugFlag("added new schedule '" . $shellArguments[INDEX_SCHEDULE] . "' to crontab"); + } + + /* try to make sure that we have execute access to sync.sh */ + chmod('sync.sh', 0775); + + /* add to the cache database schedule, replacing any schedules for this + calendar that are already there */ + $schedulesResponse = mysqlQuery(" + SELECT * + FROM `schedules` + WHERE + `calendar` = '{$calendarCache['id']}' + "); + if ($schedule = $schedulesResponse->fetch_assoc()) { + + /* only need to worry if the cached schedule is different from the + new one we just set */ + if ($shellArguments[INDEX_SCHEDULE] != $schedule['schedule']) { + /* was this the last schedule to require this trigger? */ + $schedulesResponse = mysqlQuery(" + SELECT * + FROM `schedules` + WHERE + `calendar` != '{$calendarCache['id']}' AND + `schedule` == '{$schedule['schedule']}' + "); + /* we're the last one, delete it from crontab */ + if ($schedulesResponse->num_rows == 0) { + $crontabs = preg_replace("%^.*{$schedule['schedule']}.*" . PHP_EOL . '%', '', shell_exec('crontab -l')); + $filename = md5(getSyncTimestamp()) . '.txt'; + file_put_contents("/tmp/$filename", $crontabs); + shell_exec("crontab /tmp/$filename"); + debugFlag("removed unused schedule '{$schedule['schedule']}' from crontab"); + } + + mysqlQuery(" + UPDATE `schedules` + SET + `schedule` = '" . $shellArguments[INDEX_SCHEDULE] . "', + `synced` = '" . getSyncTimestamp() . "' + WHERE + `calendar` = '{$calendarCache['id']}' + "); + } + } else { mysqlQuery(" - UPDATE `schedules` - SET - `schedule` = '" . $shellArguments[INDEX_SCHEDULE] . "', - `synced` = '" . getSyncTimestamp() . "' - WHERE - `calendar` = '{$calendarCache['id']}' + INSERT INTO `schedules` + ( + `calendar`, + `schedule`, + `synced` + ) + VALUES ( + '{$calendarCache['id']}', + '" . $shellArguments[INDEX_SCHEDULE] . "', + '" . getSyncTimestamp() . "' + ) "); } - } else { - mysqlQuery(" - INSERT INTO `schedules` - ( - `calendar`, - `schedule`, - `synced` - ) - VALUES ( - '{$calendarCache['id']}', - '" . $shellArguments[INDEX_SCHEDULE] . "', - '" . getSyncTimestamp() . "' - ) - "); } - } + + /* if we're ovewriting data (for example, if this is a recurring sync, we + need to remove the events that were _not_ synced this in this round */ + if ($_REQUEST['overwrite'] == VALUE_OVERWRITE_CANVAS_CALENDAR) { + // TODO: actually deal with this + } + + // TODO: deal with messaging based on context - /* if we're ovewriting data (for example, if this is a recurring sync, we - need to remove the events that were _not_ synced this in this round */ - if ($_REQUEST['overwrite'] == VALUE_OVERWRITE_CANVAS_CALENDAR) { - // TODO: actually deal with this + debugFlag('FINISH'); + exit; + } else { + displayError( + array( + 'Canvas URL' => $_REQUEST['canvas_url'], + 'Canvas Context' => $canvasContext, + 'Canvas Object' => $canvasObject + ), + true, + 'Canvas Object Not Found', + 'The object whose URL you submitted could not be found.' + ); } - - // TODO: deal with messaging based on context - - debugFlag('FINISH'); - exit; } else { displayError( - array( - 'Canvas URL' => $_REQUEST['canvas_url'], - 'Canvas Context' => $canvasContext, - 'Canvas Object' => $canvasObject - ), - true, - 'Canvas Object Not Found', - 'The object whose URL you submitted could not be found.' + $_REQUEST['cal'], + false, + 'ICS feed Not Found', + 'The calendar whose URL you submitted could not be found.' ); - } + } } else { displayError( $_REQUEST['canvas_url'],