1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace smtech\CanvasICSSync\SyncIntoCanvas; |
4
|
|
|
|
5
|
|
|
use DateTime; |
6
|
|
|
use Exception; |
7
|
|
|
use vcalendar; |
8
|
|
|
use Battis\DataUtilities; |
9
|
|
|
use smtech\CanvasICSSync\SyncIntoCanvas\Transform\Transform; |
10
|
|
|
use smtech\CanvasICSSync\SyncIntoCanvas\Filter\Filter; |
11
|
|
|
|
12
|
|
|
class Calendar |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* Canvas calendar context |
16
|
|
|
* @var CalendarContext |
17
|
|
|
*/ |
18
|
|
|
protected $context; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* ICS or webcal feed URL |
22
|
|
|
* @var string |
23
|
|
|
*/ |
24
|
|
|
protected $feedUrl; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Name of this calendar (extracted from feed) |
28
|
|
|
* @var string |
29
|
|
|
*/ |
30
|
|
|
protected $name; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Transformations for events in this calendar_event |
34
|
|
|
* @var Transform[] |
35
|
|
|
*/ |
36
|
|
|
protected $transforms; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Filters for events in this calendar |
40
|
|
|
* @var Filter[] |
41
|
|
|
*/ |
42
|
|
|
protected $filters; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Construct a Calendar object |
46
|
|
|
* |
47
|
|
|
* @param string $canvasUrl URL of a Canvas calendar context |
48
|
|
|
* @param string $feedUrl URL of a webcal or ICS calendar feed |
49
|
|
|
* @param boolean $enableFilter (Optional, default `false`) |
50
|
|
|
* @param string $include (Optional) Regular expression to select events |
51
|
|
|
* for inclusion in the calendar sync |
52
|
|
|
* @param string $exclude (Optional) Regular expression to select events |
53
|
|
|
* for exclusion from the calendar sync |
54
|
|
|
*/ |
55
|
|
|
public function __construct($canvasUrl, $feedUrl, $enableFilter = false, $include = null, $exclude = null) |
56
|
|
|
{ |
57
|
|
|
$this->setContext(new CalendarContext($canvasUrl)); |
58
|
|
|
$this->setFeedUrl($feedUrl); |
59
|
|
|
$this->setFilter(new Filter( |
60
|
|
|
$enableFilter, |
|
|
|
|
61
|
|
|
$include, |
62
|
|
|
$exclude |
63
|
|
|
)); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Set the Canvas calendar context |
68
|
|
|
* |
69
|
|
|
* @param CalendarContext $context |
70
|
|
|
* @throws Exception If `$context` is null |
71
|
|
|
*/ |
72
|
|
|
public function setContext(CalendarContext $context) |
73
|
|
|
{ |
74
|
|
|
if (!empty($context)) { |
75
|
|
|
$this->context = $context; |
76
|
|
|
} else { |
77
|
|
|
throw new Exception( |
78
|
|
|
'Context cannot be null' |
79
|
|
|
); |
80
|
|
|
} |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Get the Canvas calendar context |
85
|
|
|
* |
86
|
|
|
* @return CalendarContext |
87
|
|
|
*/ |
88
|
|
|
public function getContext() |
89
|
|
|
{ |
90
|
|
|
return $this->context; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Set the webcal or ICS feed URl for this calendar |
95
|
|
|
* |
96
|
|
|
* @param string $feedUrl |
97
|
|
|
* @throws Exception If `$feedUrl` is not a valid URL |
98
|
|
|
*/ |
99
|
|
|
public function setFeedUrl($feedUrl) |
100
|
|
|
{ |
101
|
|
|
if (!empty($feedUrl)) { |
102
|
|
|
/* crude test to see if the feed is a valid URL */ |
103
|
|
|
$handle = fopen($feedUrl, 'r'); |
104
|
|
|
if ($handle !== false) { |
105
|
|
|
$this->feedUrl = $feedUrl; |
106
|
|
|
} |
107
|
|
|
} |
108
|
|
|
throw new Exception( |
109
|
|
|
'Feed must be a valid URL' |
110
|
|
|
); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Get the feed URL for this calendar |
115
|
|
|
* |
116
|
|
|
* @return string |
117
|
|
|
*/ |
118
|
|
|
public function getFeedUrl() |
119
|
|
|
{ |
120
|
|
|
return $this->feedUrl; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Set the name of the calendar |
125
|
|
|
* |
126
|
|
|
* @param string $name |
127
|
|
|
*/ |
128
|
|
|
public function setName($name) |
129
|
|
|
{ |
130
|
|
|
$this->name = (string) $name; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Get the name of the calendar |
135
|
|
|
* |
136
|
|
|
* @return string |
137
|
|
|
*/ |
138
|
|
|
public function getName() |
139
|
|
|
{ |
140
|
|
|
return $this->name; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Set the regular expression filter for this calendar |
145
|
|
|
* |
146
|
|
|
* @param Filter $filter |
147
|
|
|
*/ |
148
|
|
|
public function setFilter(Filter $filter) |
149
|
|
|
{ |
150
|
|
|
$this->filter = $filter; |
|
|
|
|
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Get the regular expression filter for this calendar |
155
|
|
|
* |
156
|
|
|
* @return Filter |
157
|
|
|
*/ |
158
|
|
|
public function getFilter() |
159
|
|
|
{ |
160
|
|
|
return $this->filter; |
|
|
|
|
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Generate a unique ID to identify this particular pairing of ICS feed and |
165
|
|
|
* Canvas calendar |
166
|
|
|
**/ |
167
|
|
|
public function getId($algorithm = 'md5') |
168
|
|
|
{ |
169
|
|
|
return hash($algorithm, $this->getContext()->getCanonicalUrl() . $this->getFeedUrl()); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Get the Canvas context code for this calendar |
174
|
|
|
* |
175
|
|
|
* @return string |
176
|
|
|
*/ |
177
|
|
|
public function getContextCode() |
178
|
|
|
{ |
179
|
|
|
return $this->getContext()->getContext() . '_' . $this->getContext()->getId(); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Save this calendar to the database |
184
|
|
|
* |
185
|
|
|
* @return void |
186
|
|
|
*/ |
187
|
|
|
public function save() |
188
|
|
|
{ |
189
|
|
|
$db = Syncable::getDatabase(); |
190
|
|
|
$find = $db->prepare( |
191
|
|
|
"SELECT * FROM `calendars` WHERE `id` = :id" |
192
|
|
|
); |
193
|
|
|
$update = $db->prepare( |
194
|
|
|
"UPDATE `calendars` |
195
|
|
|
SET |
196
|
|
|
`name` = :name, |
197
|
|
|
`canvas_url` = :canvas_url, |
198
|
|
|
`ics_url` = :ics_url, |
199
|
|
|
`synced` = :synced, |
200
|
|
|
`enable_regex_filter` = :enable_regex_filter, |
201
|
|
|
`include_regexp` = :include_regexp, |
202
|
|
|
`exclude_regexp` = :exclude_regexp |
203
|
|
|
WHERE |
204
|
|
|
`id` = :id" |
205
|
|
|
); |
206
|
|
|
$insert = $db->prepare( |
207
|
|
|
"INSERT INTO `calendars` |
208
|
|
|
( |
209
|
|
|
`id`, |
210
|
|
|
`name`, |
211
|
|
|
`canvas_url`, |
212
|
|
|
`ics_url`, |
213
|
|
|
`synced`, |
214
|
|
|
`enable_regex_filter`, |
215
|
|
|
`include_regexp`, |
216
|
|
|
`exclude_regexp` |
217
|
|
|
) VALUES ( |
218
|
|
|
:id, |
219
|
|
|
:name, |
220
|
|
|
:canvas_url, |
221
|
|
|
:ics_url, |
222
|
|
|
:synced |
223
|
|
|
:enable_regex_filter, |
224
|
|
|
:include_regexp, |
225
|
|
|
:exclude_regexp |
226
|
|
|
)" |
227
|
|
|
); |
228
|
|
|
$params = [ |
229
|
|
|
'id' => $this->getId(), |
230
|
|
|
'name' => $this->getName(), |
231
|
|
|
'canvas_url' => $this->getContext()->getCanonicalUrl(), |
232
|
|
|
'ics_url' => $this->getFeedUrl(), |
233
|
|
|
'synced' => Syncable::getTimestamp(), |
234
|
|
|
'enable_regex_filter' => $this->getFilter()->isEnabled(), |
235
|
|
|
'include_regexp' => $this->getFilter()->getIncludeExpression(), |
|
|
|
|
236
|
|
|
'exclude_regexp' => $this->getFilter()->getExcludeExpression() |
|
|
|
|
237
|
|
|
]; |
238
|
|
|
|
239
|
|
|
$find->execute($params); |
240
|
|
|
if ($find->fetch() !== false) { |
241
|
|
|
$update->execute($params); |
242
|
|
|
} else { |
243
|
|
|
$insert->execute($params); |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Load a Calendar from the database |
249
|
|
|
* |
250
|
|
|
* @param string|int $id |
251
|
|
|
* @return Calendar |
252
|
|
|
*/ |
253
|
|
|
public static function load($id) |
254
|
|
|
{ |
255
|
|
|
$find = Syncable::getDatabase()->prepare( |
256
|
|
|
"SELECT * FROM `calendars` WHERE `id` = :id" |
257
|
|
|
); |
258
|
|
|
$find->execute($id); |
259
|
|
|
if (($calendar = $find->fetch()) !== false) { |
260
|
|
|
return new Calendar( |
261
|
|
|
$calendar['canvas_url'], |
262
|
|
|
$calendar['ics_url'], |
263
|
|
|
$calendar['enable_regex_filter'], |
264
|
|
|
$calendar['include_regexp'], |
265
|
|
|
$calendar['exclude_regexp'] |
266
|
|
|
); |
267
|
|
|
} |
268
|
|
|
return null; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Delete this calendar from the database |
273
|
|
|
* |
274
|
|
|
* @return void |
275
|
|
|
*/ |
276
|
|
|
public function delete() |
277
|
|
|
{ |
278
|
|
|
$delete = Syncable::getDatabase()->prepare( |
279
|
|
|
"DELETE FROM `calendars` WHERE `id` = :id" |
280
|
|
|
); |
281
|
|
|
$delete->execute($this->getId()); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Sync the calendar feed into Canvas |
286
|
|
|
* |
287
|
|
|
* @param Log $log |
288
|
|
|
* @return void |
289
|
|
|
*/ |
290
|
|
|
public function sync(Log $log) |
291
|
|
|
{ |
292
|
|
|
try { |
293
|
|
|
Syncable::getApi()->get($this->getContext()->getVerificationUrl()); |
294
|
|
|
} catch (Exception $e) { |
295
|
|
|
$this->logThrow(new Exception("Cannot sync calendars without a valid Canvas context"), $log); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
if (!DataUtilities::URLexists($this->getFeedUrl())) { |
299
|
|
|
$this->logThrow(new Exception("Cannot sync calendars with a valid calendar feed"), $log); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
$this->log(Syncable::getTimestamp() . ' sync started', $log); |
303
|
|
|
|
304
|
|
|
$this->save(); |
305
|
|
|
|
306
|
|
|
$ics = new vcalendar([ |
307
|
|
|
'unique_id' => __FILE__, |
308
|
|
|
'url' => $this->getFeedUrl() |
309
|
|
|
]); |
310
|
|
|
$ics->parse(); |
311
|
|
|
|
312
|
|
|
/* |
313
|
|
|
* TODO: would it be worth the performance improvement to just process |
314
|
|
|
* things from today's date forward? (i.e. ignore old items, even |
315
|
|
|
* if they've changed...) |
316
|
|
|
*/ |
317
|
|
|
/* |
318
|
|
|
* TODO:0 the best window for syncing would be the term of the course |
319
|
|
|
* in question, right? issue:12 |
320
|
|
|
*/ |
321
|
|
|
/* |
322
|
|
|
* TODO:0 Arbitrarily selecting events in for a year on either side of |
323
|
|
|
* today's date, probably a better system? issue:12 |
324
|
|
|
*/ |
325
|
|
|
if (($components = $ics->selectComponents( |
326
|
|
|
date('Y') - 1, // startYear |
327
|
|
|
date('m'), // startMonth |
328
|
|
|
date('d'), // startDay |
329
|
|
|
date('Y') + 1, // endYEar |
330
|
|
|
date('m'), // endMonth |
331
|
|
|
date('d'), // endDay |
332
|
|
|
'vevent', // cType |
333
|
|
|
false, // flat |
334
|
|
|
true, // any |
335
|
|
|
true // split |
336
|
|
|
)) !== false) { |
337
|
|
|
foreach ($components as $year) { |
338
|
|
|
foreach ($year as $month => $days) { |
339
|
|
|
foreach ($days as $day => $events) { |
340
|
|
|
foreach ($events as $i => $_event) { |
341
|
|
|
try { |
342
|
|
|
$event = new Event($_event, $this); |
343
|
|
|
$include = true; |
344
|
|
|
foreach ($this->filters as $filter) { |
345
|
|
|
$include &= $filter->filter($event); |
346
|
|
|
} |
347
|
|
|
if ($include) { |
348
|
|
|
foreach ($this->transforms as $transform) { |
349
|
|
|
$transform->transform($event); |
350
|
|
|
} |
351
|
|
|
$event->save(); |
352
|
|
|
} else { |
353
|
|
|
$event->delete(); |
354
|
|
|
} |
355
|
|
|
} catch (Exception $e) { |
356
|
|
|
$this->logThrow($e, $log); |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
try { |
365
|
|
|
Event::purgeUnmatched(Syncable::getTimestamp(), $this); |
366
|
|
|
} catch (Exception $e) { |
367
|
|
|
$this->logThrow($e, $log); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
$this->log(Syncable::getTimestamp() . ' sync finished', $log); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Write to the log, if we have one |
375
|
|
|
* |
376
|
|
|
* @param string $message |
377
|
|
|
* @param Log $log |
378
|
|
|
* @param mixed $priority |
379
|
|
|
* @return void |
380
|
|
|
*/ |
381
|
|
|
private function log($message, Log $log, $priority = PEAR_LOG_INFO) |
382
|
|
|
{ |
383
|
|
|
if ($log) { |
384
|
|
|
$log->log($message, $priority); |
385
|
|
|
} |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* Write exception to the log, if we have one, or re-throw the exception |
390
|
|
|
* |
391
|
|
|
* @param Exception $exception |
392
|
|
|
* @param Log $log |
393
|
|
|
* @param string $priority |
394
|
|
|
* @return void |
395
|
|
|
* @throws Exception If no log is available |
396
|
|
|
*/ |
397
|
|
|
private function logThrow( |
398
|
|
|
Exception $exception, |
399
|
|
|
Log $log, |
400
|
|
|
$priority = PEAR_LOG_ERR |
401
|
|
|
) { |
402
|
|
|
if ($log) { |
403
|
|
|
$log->log($exception->getMessage() . ': ' . $exception->getTraceAsString(), $priority); |
404
|
|
|
} else { |
405
|
|
|
throw $exception; |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.