Completed
Push — develop ( 0629c3...ee4e21 )
by Seth
02:32
created

import.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
if (php_sapi_name() == 'cli') {
4
    $pairingHash = $argv[1];
5
    define('IGNORE_LTI', true);
6
}
7
8
require_once 'common.inc.php';
9
10
use smtech\ReflexiveCanvasLTI\LTI\ToolProvider;
11
use Battis\Educoder\Pest_Unauthorized;
12
use Battis\Educoder\Pest_ClientError;
13
use Battis\BootstrapSmarty\NotificationMessage;
14
15
/*
16
 * TODO: it would be nice to be able to cleanly remove a synched calendar
17
 */
18
/*
19
 * TODO: it would be nice to be able unschedule a scheduled sync without
20
 * removing the calendar
21
 */
22
/*
23
 * TODO: how about something to extirpate non-synced data (could be done right
24
 * now by brute force -- once overwrite is implemented -- by marking all of the
25
 * cached events as invalid and then importing the calendar and overwriting,
26
 * but that's a little icky)
27
 */
28
/*
29
 * TODO: right now, if a user changes a synced event in Canvas, it will never
30
 * get "corrected" back to the ICS feed... we could cache the Canvas events as
31
 * well as the ICS feed and do a periodic (much less frequent, given the speed
32
 * of looking everything up in the API) check and re-sync modified events too
33
 */
34
35
/*
36
 * pretend we have the interactive parameters if we were passed a pairing hash
37
 * by sync.php
38
 */
39
if (!empty($pairingHash)) {
40
    $calendars = $toolbox->mysql_query("SELECT * FROM `calendars` WHERE `id` = '$pairingHash'");
41
    $calendar = $calendars->fetch_assoc();
42
    $_REQUEST['cal'] = $calendar['ics_url'];
43
    $_REQUEST['canvas_url'] = $calendar['canvas_url'];
44
}
45
46
/* do we have the vital information (an ICS feed and a URL to a canvas object)? */
47
if (empty($_REQUEST['canvas_url'])) {
48
    $_REQUEST['canvas_url'] =
49
        $toolbox->config('TOOL_CANVAS_API')['url'] .
50
        '/courses/' . $_SESSION[ToolProvider::class]['canvas']['course_id'];
51
}
52
if (isset($_REQUEST['cal']) && isset($_REQUEST['canvas_url'])) {
53
    if ($canvasContext = $toolbox->getCanvasContext($_REQUEST['canvas_url'])) {
54
        /* check ICS feed to be sure it exists */
55
        if ($toolbox->urlExists($_REQUEST['cal'])) {
56
            /* look up the canvas object -- mostly to make sure that it exists! */
57
            $canvasObject = false;
58
            try {
59
                $canvasObject = $toolbox->api_get($canvasContext['verification_url']);
60
            } catch (Exception $e) {
61
                $toolbox->postMessage(
62
                    "Error accessing Canvas object",
63
                    $canvasContext['verification_url'],
64
                    NotificationMessage::DANGER
65
                );
66
            }
67
            if ($canvasObject) {
68
                /*
69
                 * calculate the unique pairing ID of this ICS feed and canvas
70
                 * object, if we don't already have one
71
                 */
72
                if (empty($pairingHash)) {
73
                    $pairingHash = $toolbox->getPairingHash($_REQUEST['cal'], $canvasContext['canonical_url']);
74
                }
75
                $log = Log::singleton('file', __DIR__ . "/logs/$pairingHash.log");
76
                $toolbox->postMessage('Sync started', $toolbox->getSyncTimestamp(), NotificationMessage::INFO);
77
78
                /* tell users that it's started and to cool their jets */
79
                if (php_sapi_name() != 'cli') {
80
                    $toolbox->smarty_assign(
81
                        'calendarPreviewUrl',
82
                        $toolbox->config('TOOL_CANVAS_API')['url'] .
83
                        "/calendar?include_contexts={$canvasContext['context']}_{$canvasObject['id']}"
84
                    );
85
                    $toolbox->smarty_display('import-started.tpl');
86
                }
87
88
                /* parse the ICS feed */
89
                $ics = new vcalendar(
90
                    array(
91
                        'unique_id' => $toolbox->config('TOOL_ID'),
92
                        'url' => $_REQUEST['cal']
93
                    )
94
                );
95
                $ics->parse();
96
97
                /* log this pairing in the database cache, if it doesn't already exist */
98
                $calendarCacheResponse = $toolbox->mysql_query("
99
                    SELECT *
100
                        FROM `calendars`
101
                        WHERE
102
                            `id` = '$pairingHash'
103
                ");
104
                $calendarCache = $calendarCacheResponse->fetch_assoc();
105
106
                /* if the calendar is already cached, just update the sync timestamp */
107
                if ($calendarCache) {
108
                    $toolbox->mysql_query("
109
                        UPDATE `calendars`
110
                            SET
111
                                `synced` = '" . $toolbox->getSyncTimestamp() . "'
112
                            WHERE
113
                                `id` = '$pairingHash'
114
                    ");
115
                } else {
116
                    $toolbox->mysql_query("
117
                        INSERT INTO `calendars`
118
                            (
119
                                `id`,
120
                                `name`,
121
                                `ics_url`,
122
                                `canvas_url`,
123
                                `synced`,
124
                                `enable_regexp_filter`,
125
                                `include_regexp`,
126
                                `exclude_regexp`
127
                            )
128
                            VALUES (
129
                                '$pairingHash',
130
                                '" . $toolbox->getMySQL()->escape_string($ics->getProperty('X-WR-CALNAME')[1]) . "',
131
                                '{$_REQUEST['cal']}',
132
                                '{$canvasContext['canonical_url']}',
133
                                '" . $toolbox->getSyncTimestamp() . "',
134
                                '" . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER) . "',
135
                                " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ?
136
                                    "'" . $toolbox->getMySQL()->escape_string($_REQUEST['include_regexp']) . "'" : 'NULL'
137
                                ) . ",
138
                                " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ?
139
                                    "'" . $toolbox->getMySQL()->escape_string($_REQUEST['exclude_regexp']) . "'" : 'NULL'
140
                                ) . "
141
                            )
142
                    ");
143
                }
144
145
                /* refresh calendar information from cache database */
146
                $calendarCacheResponse = $toolbox->mysql_query("
147
                    SELECT *
148
                        FROM `calendars`
149
                        WHERE
150
                            `id` = '$pairingHash'
151
                ");
152
                $calendarCache = $calendarCacheResponse->fetch_assoc();
153
154
                /*
155
                 * walk through $master_array and update the Canvas calendar to
156
                 * match the ICS feed, caching changes in the database */
157
                /*
158
                 * TODO: would it be worth the performance improvement to just
159
                 * process things from today's date forward? (i.e. ignore old
160
                 * items, even if they've changed...)
161
                 */
162
                /*
163
                 * TODO: the best window for syncing would be the term of the
164
                 * course in question, right?
165
                 */
166
                /*
167
                 * TODO: Arbitrarily selecting events in for a year on either
168
                 * side of today's date, probably a better system?
169
                 */
170
                foreach ($ics->selectComponents(
171
                    date('Y') - 1, // startYear
172
                    date('m'), // startMonth
173
                    date('d'), // startDay
174
                    date('Y') + 1, // endYEar
175
                    date('m'), // endMonth
176
                    date('d'), // endDay
177
                    'vevent', // cType
178
                    false, // flat
179
                    true, // any
180
                    true // split
181
                ) as $year) {
182
                    foreach ($year as $month => $days) {
183
                        foreach ($days as $day => $events) {
184
                            foreach ($events as $i => $event) {
185
                                /* does this event already exist in Canvas? */
186
                                $eventHash = $toolbox->getEventHash($event);
187
188
                                /* if the event should be included... */
189
                                if ($toolbox->filterEvent($event, $calendarCache)) {
190
                                    /* have we cached this event already? */
191
                                    $eventCacheResponse = $toolbox->mysql_query("
192
                                        SELECT *
193
                                            FROM `events`
194
                                            WHERE
195
                                                `calendar` = '{$calendarCache['id']}' AND
196
                                                `event_hash` = '$eventHash'
197
                                    ");
198
199
200
                                    /* if we already have the event cached in its current form, just update
201
                                       the timestamp */
202
                                    $eventCache = $eventCacheResponse->fetch_assoc();
203
                                    if ($eventCache) {
204
                                        $toolbox->mysql_query("
205
                                            UPDATE `events`
206
                                                SET
207
                                                    `synced` = '" . $toolbox->getSyncTimestamp() . "'
208
                                                WHERE
209
                                                    `id` = '{$eventCache['id']}'
210
                                        ");
211
212
                                    /* otherwise, add this new event and cache it */
213
                                    } else {
214
                                        /* multi-day event instance start times need to be changed to _this_ date */
215
                                        $start = new DateTime(
216
                                            iCalUtilityFunctions::_date2strdate($event->getProperty('DTSTART'))
217
                                        );
218
                                        $end = new DateTime(
219
                                            iCalUtilityFunctions::_date2strdate($event->getProperty('DTEND'))
220
                                        );
221
                                        if ($event->getProperty('X-RECURRENCE')) {
222
                                            $start = new DateTime($event->getProperty('X-CURRENT-DTSTART')[1]);
223
                                            $end = new DateTime($event->getProperty('X-CURRENT-DTEND')[1]);
224
                                        }
225
                                        $start->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE));
226
                                        $end->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE));
227
228
                                        try {
229
                                            $calendarEvent = $toolbox->api_post(
230
                                                "/calendar_events",
231
                                                array(
232
                                                    'calendar_event[context_code]' => "{$canvasContext['context']}_{$canvasObject['id']}",
233
                                                    'calendar_event[title]' => preg_replace('%^([^\]]+)(\s*\[[^\]]+\]\s*)+$%', '\\1', strip_tags($event->getProperty('SUMMARY'))),
234
                                                    'calendar_event[description]' => \Michelf\Markdown::defaultTransform(str_replace('\n', "\n\n", $event->getProperty('DESCRIPTION', 1))),
235
                                                    'calendar_event[start_at]' => $start->format(CANVAS_TIMESTAMP_FORMAT),
236
                                                    'calendar_event[end_at]' => $end->format(CANVAS_TIMESTAMP_FORMAT),
237
                                                    'calendar_event[location_name]' => $event->getProperty('LOCATION')
238
                                                )
239
                                            );
240
                                            $toolbox->mysql_query("
241
                                                INSERT INTO `events`
242
                                                    (
243
                                                        `calendar`,
244
                                                        `calendar_event[id]`,
245
                                                        `event_hash`,
246
                                                        `synced`
247
                                                    )
248
                                                    VALUES (
249
                                                        '{$calendarCache['id']}',
250
                                                        '{$calendarEvent['id']}',
251
                                                        '$eventHash',
252
                                                        '" . $toolbox->getSyncTimestamp() . "'
253
                                                    )
254
                                            ");
255
                                        } catch (Exception $e) {
256
                                            $toolbox->postMessage(
257
                                                'Error creating calendar event',
258
                                                $eventHash,
259
                                                NotificationMessage::ERROR
0 ignored issues
show
Deprecated Code introduced by
The constant Battis\BootstrapSmarty\NotificationMessage::ERROR has been deprecated with message: Use `DANGER` instead for consistency with Bootstrap

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
260
                                            );
261
                                        }
262
                                    }
263
                                }
264
                            }
265
                        }
266
                    }
267
                }
268
269
                /* clean out previously synced events that are no longer correct */
270
                $deletedEventsResponse = $toolbox->mysql_query("
271
                    SELECT * FROM `events`
272
                        WHERE
273
                            `calendar` = '{$calendarCache['id']}' AND
274
                            `synced` != '" . $toolbox->getSyncTimestamp() . "'
275
                ");
276
                while ($deletedEventCache = $deletedEventsResponse->fetch_assoc()) {
277
                    try {
278
                        $deletedEvent = $toolbox->api_delete(
279
                            "calendar_events/{$deletedEventCache['calendar_event[id]']}",
280
                            array(
281
                                'cancel_reason' => $toolbox->getSyncTimestamp(),
282
                                'as_user_id' => ($canvasContext['context'] == 'user' ? $canvasObject['id'] : '') // TODO: this feels skeevy -- like the empty string will break
283
                            )
284
                        );
285
                    } catch (Pest_Unauthorized $e) {
286
                        /* if the event has been deleted in Canvas, we'll get an error when
287
                           we try to delete it a second time. We still need to delete it from
288
                           our cache database, however */
289
                        $toolbox->postMessage(
290
                            'Cache out-of-sync',
291
                            "calendar_event[{$deletedEventCache['calendar_event[id]']}] no longer exists and will be purged from cache.",
292
                            NotificationMessage::INFO
293
                        );
294
                    } catch (Pest_ClientError $e) {
295
                        $toolbox->postMessage(
296
                            'API Client Error',
297
                            '<pre>' . print_r(array(
298
                                'Status' => $PEST->lastStatus(),
299
                                'Error' => $PEST->lastBody(),
300
                                'Verb' => $verb,
301
                                'URL' => $url,
302
                                'Data' => $data
303
                            ), false) . '</pre>',
304
                            NotificationMessage::ERROR
0 ignored issues
show
Deprecated Code introduced by
The constant Battis\BootstrapSmarty\NotificationMessage::ERROR has been deprecated with message: Use `DANGER` instead for consistency with Bootstrap

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
305
                        );
306
                        if (php_sapi_name() != 'cli') {
307
                            $toolbox->smarty_display('page.tpl');
308
                        }
309
                        exit;
310
                    }
311
                    $toolbox->mysql_query("
312
                        DELETE FROM `events`
313
                            WHERE
314
                                `id` = '{$deletedEventCache['id']}'
315
                    ");
316
                }
317
318
                /* if this was a scheduled import (i.e. a sync), update that schedule */
319
                if (isset($_REQUEST['schedule'])) {
320
                    $toolbox->mysql_query("
321
                        UPDATE `schedules`
322
                            SET
323
                                `synced` = '" . $toolbox->getSyncTimestamp() . "'
324
                            WHERE
325
                                `id` = '{$_REQUEST['schedule']}'
326
                    ");
327
                }
328
                /* are we setting up a regular synchronization? */
329
                if (isset($_REQUEST['sync']) && $_REQUEST['sync'] != SCHEDULE_ONCE) {
330
                    // FIXME CRON SYNC SETUP GOES HERE
331
332
                    /* add to the cache database schedule, replacing any schedules for this
333
                       calendar that are already there */
334
                    $schedulesResponse = $toolbox->mysql_query("
335
                        SELECT *
336
                            FROM `schedules`
337
                            WHERE
338
                                `calendar` = '{$calendarCache['id']}'
339
                    ");
340
341
                    if ($schedule = $schedulesResponse->fetch_assoc()) {
342
                        /* only need to worry if the cached schedule is different from the
343
                           new one we just set */
344
                        if ($shellArguments[INDEX_SCHEDULE] != $schedule['schedule']) {
345
                            /* was this the last schedule to require this trigger? */
346
                            $schedulesResponse = $toolbox->mysql_query("
347
                                SELECT *
348
                                    FROM `schedules`
349
                                    WHERE
350
                                        `calendar` != '{$calendarCache['id']}' AND
351
                                        `schedule` == '{$schedule['schedule']}'
352
                            ");
353
                            /* we're the last one, delete it from crontab */
354
                            if ($schedulesResponse->num_rows == 0) {
355
                                $crontabs = preg_replace("%^.*{$schedule['schedule']}.*" . PHP_EOL . '%', '', shell_exec('crontab -l'));
356
                                $filename = md5($toolbox->getSyncTimestamp()) . '.txt';
357
                                file_put_contents("/tmp/$filename", $crontabs);
358
                                shell_exec("crontab /tmp/$filename");
359
                                $toolbox->postMessage('Unused schedule', "removed schedule '{$schedule['schedule']}' from crontab", NotificationMessage::INFO);
360
                            }
361
362
                            $toolbox->mysql_query("
363
                                UPDATE `schedules`
364
                                    SET
365
                                        `schedule` = '" . $shellArguments[INDEX_SCHEDULE] . "',
366
                                        `synced` = '" . $toolbox->getSyncTimestamp() . "'
367
                                    WHERE
368
                                        `calendar` = '{$calendarCache['id']}'
369
                            ");
370
                        }
371
                    } else {
372
                        $toolbox->mysql_query("
373
                            INSERT INTO `schedules`
374
                                (
375
                                    `calendar`,
376
                                    `schedule`,
377
                                    `synced`
378
                                )
379
                                VALUES (
380
                                    '{$calendarCache['id']}',
381
                                    '" . $_REQUEST['sync'] . "',
382
                                    '" . $toolbox->getSyncTimestamp() . "'
383
                                )
384
                        ");
385
                    }
386
                }
387
388
                /* if we're ovewriting data (for example, if this is a recurring sync, we
389
                   need to remove the events that were _not_ synced this in this round */
390
                if (isset($_REQUEST['overwrite']) && $_REQUEST['overwrite'] == VALUE_OVERWRITE_CANVAS_CALENDAR) {
391
                    // TODO: actually deal with this
392
                }
393
394
                // TODO: deal with messaging based on context
395
396
                $toolbox->postMessage('Finished sync', $toolbox->getSyncTimestamp(), NotificationMessage::INFO);
397
                exit;
398
            } else {
399
                $toolbox->postMessage(
400
                    'Canvas Object  Not Found',
401
                    'The object whose URL you submitted could not be found.<pre>' . print_r(array(
402
                        'Canvas URL' => $_REQUEST['canvas_url'],
403
                        'Canvas Context' => $canvasContext,
404
                        'Canvas Object' => $canvasObject
405
                    ), false) . '</pre>',
406
                    NotificationMessage::ERROR
0 ignored issues
show
Deprecated Code introduced by
The constant Battis\BootstrapSmarty\NotificationMessage::ERROR has been deprecated with message: Use `DANGER` instead for consistency with Bootstrap

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
407
                );
408
            }
409
        } else {
410
            $toolbox->postMessage(
411
                'ICS feed  Not Found',
412
                'The calendar whose URL you submitted could not be found.<pre>' . $_REQUEST['cal'] . '</pre>',
413
                NotificationMessage::ERROR
0 ignored issues
show
Deprecated Code introduced by
The constant Battis\BootstrapSmarty\NotificationMessage::ERROR has been deprecated with message: Use `DANGER` instead for consistency with Bootstrap

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
414
            );
415
        }
416
    } else {
417
        $toolbox->postMessage(
418
            'Invalid Canvas URL',
419
            'The Canvas URL you submitted could not be parsed.<pre>' . $_REQUEST['canvas_url'] . '</pre>',
420
            NotificationMessage::ERROR
0 ignored issues
show
Deprecated Code introduced by
The constant Battis\BootstrapSmarty\NotificationMessage::ERROR has been deprecated with message: Use `DANGER` instead for consistency with Bootstrap

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
421
        );
422
        if (php_sapi_name() != 'cli') {
423
            $toolbox->smarty_display('page.tpl');
424
        }
425
        exit;
426
    }
427
} else {
428
    if (php_sapi_name() != 'cli') {
429
        $toolbox->smarty_display('import.tpl');
430
    }
431
}
432