Completed
Push — develop ( 879d74...52dce9 )
by Seth
04:03
created

import.php ➔ getCanvasContext()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 6
nop 1
dl 0
loc 37
rs 6.7272
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 9 and the first side effect is on line 5.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
if (php_sapi_name() == 'cli') {
4
    // TODO there is a more streamlined way of doing this that escapes me just this second
5
    $_REQUEST['cal'] = $argv[1];
6
    $_REQUEST['canvas_url'] = $argv[2];
7
    $_REQUEST['schedule'] = $argv[3];
8
9
    define('IGNORE_LTI', true);
10
}
11
12
require_once 'common.inc.php';
13
14
use smtech\CanvasPest\CanvasPest;
15
use smtech\ReflexiveCanvasLTI\LTI\ToolProvider;
16
use Battis\Educoder\Pest_Unauthorized;
17
use Battis\Educoder\Pest_ClientError;
18
use Battis\BootstrapSmarty\NotificationMessage;
19
20
/*
21
 * TODO: it would be nice to be able to cleanly remove a synched calendar
22
 */
23
/*
24
 * TODO: it would be nice to be able unschedule a scheduled sync without
25
 * removing the calendar
26
 */
27
/*
28
 * TODO: how about something to extirpate non-synced data (could be done right
29
 * now by brute force -- once overwrite is implemented -- by marking all of the
30
 * cached events as invalid and then importing the calendar and overwriting,
31
 * but that's a little icky)
32
 */
33
/*
34
 * TODO: right now, if a user changes a synced event in Canvas, it will never
35
 * get "corrected" back to the ICS feed... we could cache the Canvas events as
36
 * well as the ICS feed and do a periodic (much less frequent, given the speed
37
 * of looking everything up in the API) check and re-sync modified events too
38
 */
39
40
/* do we have the vital information (an ICS feed and a URL to a canvas object)? */
41
if (empty($_REQUEST['canvas_url'])) {
42
    $_REQUEST['canvas_url'] =
43
        $toolbox->config('TOOL_CANVAS_API')['url'] .
44
        '/courses/' . $_SESSION[ToolProvider::class]['canvas']['course_id'];
45
}
46
if (isset($_REQUEST['cal']) && isset($_REQUEST['canvas_url'])) {
47
    if ($canvasContext = $toolbox->getCanvasContext($_REQUEST['canvas_url'])) {
48
        /* check ICS feed to be sure it exists */
49
        if ($toolbox->urlExists($_REQUEST['cal'])) {
50
            /* look up the canvas object -- mostly to make sure that it exists! */
51
            $canvasObject = false;
52
            try {
53
                $canvasObject = $toolbox->api_get($canvasContext['verification_url']);
54
            } catch (Exception $e) {
55
                postMessage(
56
                    "Error accessing Canvas object",
57
                    $canvasContext['verification_url'],
58
                    NotificationMessage::DANGER
59
                );
60
            }
61
            if ($canvasObject) {
62
                /* calculate the unique pairing ID of this ICS feed and canvas object */
63
                $pairingHash = getPairingHash($_REQUEST['cal'], $canvasContext['canonical_url']);
64
                $log = Log::singleton('file', __DIR__ . "/logs/$pairingHash.log");
65
                postMessage('Sync started', getSyncTimestamp(), NotificationMessage::INFO);
66
67
                /* tell users that it's started and to cool their jets */
68
                if (php_sapi_name() != 'cli') {
69
                    $toolbox->smarty_assign(
70
                        'calendarPreviewUrl',
71
                        $toolbox->config('TOOL_CANVAS_API')['url'] .
72
                        "/calendar?include_contexts={$canvasContext['context']}_{$canvasObject['id']}"
73
                    );
74
                    $toolbox->smarty_display('import-started.tpl');
75
                }
76
77
                /* parse the ICS feed */
78
                $ics = new vcalendar(
79
                    array(
80
                        'unique_id' => $metadata['APP_ID'],
81
                        'url' => $_REQUEST['cal']
82
                    )
83
                );
84
                $ics->parse();
85
86
                /* log this pairing in the database cache, if it doesn't already exist */
87
                $calendarCacheResponse = $toolbox->mysql_query("
88
                    SELECT *
89
                        FROM `calendars`
90
                        WHERE
91
                            `id` = '$pairingHash'
92
                ");
93
                $calendarCache = $calendarCacheResponse->fetch_assoc();
94
95
                /* if the calendar is already cached, just update the sync timestamp */
96
                if ($calendarCache) {
97
                    $toolbox->mysql_query("
98
                        UPDATE `calendars`
99
                            SET
100
                                `synced` = '" . getSyncTimestamp() . "'
101
                            WHERE
102
                                `id` = '$pairingHash'
103
                    ");
104
                } else {
105
                    $toolbox->mysql_query("
106
                        INSERT INTO `calendars`
107
                            (
108
                                `id`,
109
                                `name`,
110
                                `ics_url`,
111
                                `canvas_url`,
112
                                `synced`,
113
                                `enable_regexp_filter`,
114
                                `include_regexp`,
115
                                `exclude_regexp`
116
                            )
117
                            VALUES (
118
                                '$pairingHash',
119
                                '" . $toolbox->getMySQL()->escape_string($ics->getProperty('X-WR-CALNAME')[1]) . "',
120
                                '{$_REQUEST['cal']}',
121
                                '{$canvasContext['canonical_url']}',
122
                                '" . getSyncTimestamp() . "',
123
                                '" . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER) . "',
124
                                " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ?
125
                                    "'" . $toolbox->getMySQL()->escape_string($_REQUEST['include_regexp']) . "'" :
126
                                    'NULL'
127
                                ) . ",
128
                                " . ($_REQUEST['enable_regexp_filter'] == VALUE_ENABLE_REGEXP_FILTER ?
129
                                    "'" . $toolbox->getMySQL()->escape_string($_REQUEST['exclude_regexp']) . "'" :
130
                                    'NULL'
131
                                ) . "
132
                            )
133
                    ");
134
                }
135
136
                /* refresh calendar information from cache database */
137
                $calendarCacheResponse = $toolbox->mysql_query("
138
                    SELECT *
139
                        FROM `calendars`
140
                        WHERE
141
                            `id` = '$pairingHash'
142
                ");
143
                $calendarCache = $calendarCacheResponse->fetch_assoc();
144
145
                /*
146
                 * walk through $master_array and update the Canvas calendar to
147
                 * match the ICS feed, caching changes in the database */
148
                /*
149
                 * TODO: would it be worth the performance improvement to just
150
                 * process things from today's date forward? (i.e. ignore old
151
                 * items, even if they've changed...)
152
                 */
153
                /*
154
                 * TODO: the best window for syncing would be the term of the
155
                 * course in question, right?
156
                 */
157
                /*
158
                 * TODO: Arbitrarily selecting events in for a year on either
159
                 * side of today's date, probably a better system?
160
                 */
161
                foreach ($ics->selectComponents(
0 ignored issues
show
Bug introduced by
The expression $ics->selectComponents(d...nt', false, true, true) of type false|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
162
                    date('Y')-1, // startYear
163
                    date('m'), // startMonth
164
                    date('d'), // startDay
165
                    date('Y')+1, // endYEar
166
                    date('m'), // endMonth
167
                    date('d'), // endDay
168
                    'vevent', // cType
169
                    false, // flat
170
                    true, // any
171
                    true // split
172
                ) as $year) {
173
                    foreach ($year as $month => $days) {
174
                        foreach ($days as $day => $events) {
175
                            foreach ($events as $i => $event) {
176
                                /* does this event already exist in Canvas? */
177
                                $eventHash = getEventHash($event);
178
179
                                /* if the event should be included... */
180
                                if ($toolbox->filterEvent($event, $calendarCache)) {
181
                                    /* have we cached this event already? */
182
                                    $eventCacheResponse = $toolbox->mysql_query("
183
                                        SELECT *
184
                                            FROM `events`
185
                                            WHERE
186
                                                `calendar` = '{$calendarCache['id']}' AND
187
                                                `event_hash` = '$eventHash'
188
                                    ");
189
190
191
                                    /* if we already have the event cached in its current form, just update
192
                                       the timestamp */
193
                                    $eventCache = $eventCacheResponse->fetch_assoc();
194
                                    if ($eventCache) {
195
                                        $toolbox->mysql_query("
196
                                            UPDATE `events`
197
                                                SET
198
                                                    `synced` = '" . getSyncTimestamp() . "'
199
                                                WHERE
200
                                                    `id` = '{$eventCache['id']}'
201
                                        ");
202
203
                                    /* otherwise, add this new event and cache it */
204
                                    } else {
205
                                        /* multi-day event instance start times need to be changed to _this_ date */
206
                                        $start = new DateTime(
207
                                            iCalUtilityFunctions::_date2strdate($event->getProperty('DTSTART'))
208
                                        );
209
                                        $end = new DateTime(
210
                                            iCalUtilityFunctions::_date2strdate($event->getProperty('DTEND'))
211
                                        );
212
                                        if ($event->getProperty('X-RECURRENCE')) {
213
                                            $start = new DateTime($event->getProperty('X-CURRENT-DTSTART')[1]);
214
                                            $end = new DateTime($event->getProperty('X-CURRENT-DTEND')[1]);
215
                                        }
216
                                        $start->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE));
217
                                        $end->setTimeZone(new DateTimeZone(LOCAL_TIMEZONE));
218
219
                                        try {
220
                                            $calendarEvent = $toolbox->api_post(
221
                                                "/calendar_events",
222
                                                array(
223
                                                    'calendar_event[context_code]' => "{$canvasContext['context']}_{$canvasObject['id']}",
224
                                                    'calendar_event[title]' => preg_replace('%^([^\]]+)(\s*\[[^\]]+\]\s*)+$%', '\\1', strip_tags($event->getProperty('SUMMARY'))),
225
                                                    'calendar_event[description]' => \Michelf\Markdown::defaultTransform(str_replace('\n', "\n\n", $event->getProperty('DESCRIPTION', 1))),
226
                                                    'calendar_event[start_at]' => $start->format(CANVAS_TIMESTAMP_FORMAT),
227
                                                    'calendar_event[end_at]' => $end->format(CANVAS_TIMESTAMP_FORMAT),
228
                                                    'calendar_event[location_name]' => $event->getProperty('LOCATION')
229
                                                )
230
                                            );
231
                                            $toolbox->mysql_query("
232
                                                INSERT INTO `events`
233
                                                    (
234
                                                        `calendar`,
235
                                                        `calendar_event[id]`,
236
                                                        `event_hash`,
237
                                                        `synced`
238
                                                    )
239
                                                    VALUES (
240
                                                        '{$calendarCache['id']}',
241
                                                        '{$calendarEvent['id']}',
242
                                                        '$eventHash',
243
                                                        '" . getSyncTimestamp() . "'
244
                                                    )
245
                                            ");
246
                                        } catch (Exception $e) {
247
                                            postMessage(
248
                                                'Error creating calendar event',
249
                                                $eventHash,
250
                                                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...
251
                                            );
252
                                        }
253
                                    }
254
                                }
255
                            }
256
                        }
257
                    }
258
                }
259
260
                /* clean out previously synced events that are no longer correct */
261
                $deletedEventsResponse = $toolbox->mysql_query("
262
                    SELECT * FROM `events`
263
                        WHERE
264
                            `calendar` = '{$calendarCache['id']}' AND
265
                            `synced` != '" . getSyncTimestamp() . "'
266
                ");
267
                while ($deletedEventCache = $deletedEventsResponse->fetch_assoc()) {
268
                    try {
269
                        $deletedEvent = $toolbox->api_delete(
270
                            "calendar_events/{$deletedEventCache['calendar_event[id]']}",
271
                            array(
272
                                'cancel_reason' => getSyncTimestamp(),
273
                                'as_user_id' => ($canvasContext['context'] == 'user' ? $canvasObject['id'] : '') // TODO: this feels skeevy -- like the empty string will break
274
                            )
275
                        );
276
                    } catch (Pest_Unauthorized $e) {
277
                        /* if the event has been deleted in Canvas, we'll get an error when
278
                           we try to delete it a second time. We still need to delete it from
279
                           our cache database, however */
280
                        postMessage(
281
                            'Cache out-of-sync',
282
                            "calendar_event[{$deletedEventCache['calendar_event[id]']}] no longer exists and will be purged from cache.",
283
                            NotificationMessage::INFO
284
                        );
285
                    } catch (Pest_ClientError $e) {
286
                        postMessage(
287
                            'API Client Error',
288
                            '<pre>' . print_r(array(
289
                                'Status' => $PEST->lastStatus(),
290
                                'Error' => $PEST->lastBody(),
291
                                'Verb' => $verb,
292
                                'URL' => $url,
293
                                'Data' => $data
294
                            ), false) . '</pre>',
295
                            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...
296
                        );
297
                        if (php_sapi_name() != 'cli') {
298
                            $toolbox->smarty_display('page.tpl');
299
                        }
300
                        exit;
301
                    }
302
                    $toolbox->mysql_query("
303
                        DELETE FROM `events`
304
                            WHERE
305
                                `id` = '{$deletedEventCache['id']}'
306
                    ");
307
                }
308
309
                /* if this was a scheduled import (i.e. a sync), update that schedule */
310
                if (isset($_REQUEST['schedule'])) {
311
                    $toolbox->mysql_query("
312
                        UPDATE `schedules`
313
                            SET
314
                                `synced` = '" . getSyncTimestamp() . "'
315
                            WHERE
316
                                `id` = '{$_REQUEST['schedule']}'
317
                    ");
318
                }
319
                /* are we setting up a regular synchronization? */
320
                if (isset($_REQUEST['sync']) && $_REQUEST['sync'] != SCHEDULE_ONCE) {
321
                    // FIXME CRON SYNC SETUP GOES HERE
322
323
                    /* add to the cache database schedule, replacing any schedules for this
324
                       calendar that are already there */
325
                    $schedulesResponse = $toolbox->mysql_query("
326
                        SELECT *
327
                            FROM `schedules`
328
                            WHERE
329
                                `calendar` = '{$calendarCache['id']}'
330
                    ");
331
332
                    if ($schedule = $schedulesResponse->fetch_assoc()) {
333
                        /* only need to worry if the cached schedule is different from the
334
                           new one we just set */
335
                        if ($shellArguments[INDEX_SCHEDULE] != $schedule['schedule']) {
336
                            /* was this the last schedule to require this trigger? */
337
                            $schedulesResponse = $toolbox->mysql_query("
338
                                SELECT *
339
                                    FROM `schedules`
340
                                    WHERE
341
                                        `calendar` != '{$calendarCache['id']}' AND
342
                                        `schedule` == '{$schedule['schedule']}'
343
                            ");
344
                            /* we're the last one, delete it from crontab */
345
                            if ($schedulesResponse->num_rows == 0) {
346
                                $crontabs = preg_replace("%^.*{$schedule['schedule']}.*" . PHP_EOL . '%', '', shell_exec('crontab -l'));
347
                                $filename = md5(getSyncTimestamp()) . '.txt';
348
                                file_put_contents("/tmp/$filename", $crontabs);
349
                                shell_exec("crontab /tmp/$filename");
350
                                postMessage('Unused schedule', "removed schedule '{$schedule['schedule']}' from crontab", NotificationMessage::INFO);
351
                            }
352
353
                            $toolbox->mysql_query("
354
                                UPDATE `schedules`
355
                                    SET
356
                                        `schedule` = '" . $shellArguments[INDEX_SCHEDULE] . "',
357
                                        `synced` = '" . getSyncTimestamp() . "'
358
                                    WHERE
359
                                        `calendar` = '{$calendarCache['id']}'
360
                            ");
361
                        }
362
                    } else {
363
                        $toolbox->mysql_query("
364
                            INSERT INTO `schedules`
365
                                (
366
                                    `calendar`,
367
                                    `schedule`,
368
                                    `synced`
369
                                )
370
                                VALUES (
371
                                    '{$calendarCache['id']}',
372
                                    '" . $_REQUEST['sync'] . "',
373
                                    '" . getSyncTimestamp() . "'
374
                                )
375
                        ");
376
                    }
377
                }
378
379
                /* if we're ovewriting data (for example, if this is a recurring sync, we
380
                   need to remove the events that were _not_ synced this in this round */
381
                if (isset($_REQUEST['overwrite']) && $_REQUEST['overwrite'] == VALUE_OVERWRITE_CANVAS_CALENDAR) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
382
                    // TODO: actually deal with this
383
                }
384
385
                // TODO: deal with messaging based on context
386
387
                postMessage('Finished sync', getSyncTimestamp(), NotificationMessage::INFO);
388
                exit;
389
            } else {
390
                postMessage(
391
                    'Canvas Object  Not Found',
392
                    'The object whose URL you submitted could not be found.<pre>' . print_r(array(
393
                        'Canvas URL' => $_REQUEST['canvas_url'],
394
                        'Canvas Context' => $canvasContext,
395
                        'Canvas Object' => $canvasObject
396
                    ), false) . '</pre>',
397
                    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...
398
                );
399
            }
400
        } else {
401
            postMessage(
402
                'ICS feed  Not Found',
403
                'The calendar whose URL you submitted could not be found.<pre>' . $_REQUEST['cal'] . '</pre>',
404
                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...
405
            );
406
        }
407
    } else {
408
        postMessage(
409
            'Invalid Canvas URL',
410
            'The Canvas URL you submitted could not be parsed.<pre>' . $_REQUEST['canvas_url'] . '</pre>',
411
            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...
412
        );
413
        if (php_sapi_name() != 'cli') {
414
            $toolbox->smarty_display('page.tpl');
415
        }
416
        exit;
417
    }
418
} else {
419
    if (php_sapi_name() != 'cli') {
420
        $toolbox->smarty_display('import.tpl');
421
    }
422
}
423