Completed
Push — develop ( 7b6f26...f78c00 )
by Seth
02:21
created

Calendar::getContext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace smtech\CanvasICSSync\SyncIntoCanvas;
4
5
use DateTime;
6
use Exception;
7
use vcalendar;
8
use Battis\DataUtilities;
9
10
class Calendar
11
{
12
    /**
13
     * Canvas calendar context
14
     * @var CalendarContext
15
     */
16
    protected $context;
17
18
    /**
19
     * ICS or webcal feed URL
20
     * @var string
21
     */
22
    protected $feedUrl;
23
24
    /**
25
     * Name of this calendar (extracted from feed)
26
     * @var string
27
     */
28
    protected $name;
29
30
    /**
31
     * Filter for events in this calendar
32
     * @var Filter
33
     */
34
    protected $filter;
35
36
    /**
37
     * Construct a Calendar object
38
     *
39
     * @param string $canvasUrl URL of a Canvas calendar context
40
     * @param string $feedUrl URL of a webcal or ICS calendar feed
41
     * @param boolean $enableFilter (Optional, default `false`)
42
     * @param string $include (Optional) Regular expression to select events
43
     *     for inclusion in the calendar sync
44
     * @param string $exclude (Optional) Regular expression to select events
45
     *     for exclusion from the calendar sync
46
     */
47
    public function __construct($canvasUrl, $feedUrl, $enableFilter = false, $include = null, $exclude = null)
48
    {
49
        $this->setContext(new CalendarContext($canvasUrl));
50
        $this->setFeedUrl($feedUrl);
51
        $this->setFilter(new Filter(
52
            $enableFilter,
53
            $include,
54
            $exclude
55
        ));
56
    }
57
58
    /**
59
     * Set the Canvas calendar context
60
     *
61
     * @param CalendarContext $context
62
     * @throws Exception If `$context` is null
63
     */
64
    public function setContext(CalendarContext $context)
65
    {
66
        if (!empty($context)) {
67
            $this->context = $context;
68
        } else {
69
            throw new Exception(
70
                'Context cannot be null'
71
            );
72
        }
73
    }
74
75
    /**
76
     * Get the Canvas calendar context
77
     *
78
     * @return CalendarContext
79
     */
80
    public function getContext()
81
    {
82
        return $this->context;
83
    }
84
85
    /**
86
     * Set the webcal or ICS feed URl for this calendar
87
     *
88
     * @param string $feedUrl
89
     * @throws Exception If `$feedUrl` is not a valid URL
90
     */
91
    public function setFeedUrl($feedUrl)
92
    {
93
        if (!empty($feedUrl)) {
94
            /* crude test to see if the feed is a valid URL */
95
            $handle = fopen($feedUrl, 'r');
96
            if ($handle !== false) {
97
                $this->feedUrl = $feedUrl;
98
            }
99
        }
100
        throw new Exception(
101
            'Feed must be a valid URL'
102
        );
103
    }
104
105
    /**
106
     * Get the feed URL for this calendar
107
     *
108
     * @return string
109
     */
110
    public function getFeedUrl()
111
    {
112
        return $this->feedUrl;
113
    }
114
115
    /**
116
     * Set the name of the calendar
117
     *
118
     * @param string $name
119
     */
120
    public function setName($name)
121
    {
122
        $this->name = (string) $name;
123
    }
124
125
    /**
126
     * Get the name of the calendar
127
     *
128
     * @return string
129
     */
130
    public function getName()
131
    {
132
        return $this->name;
133
    }
134
135
    /**
136
     * Set the regular expression filter for this calendar
137
     *
138
     * @param Filter $filter
139
     */
140
    public function setFilter(Filter $filter)
141
    {
142
        $this->filter = $filter;
143
    }
144
145
    /**
146
     * Get the regular expression filter for this calendar
147
     *
148
     * @return Filter
149
     */
150
    public function getFilter()
151
    {
152
        return $this->filter;
153
    }
154
155
    /**
156
     * Generate a unique ID to identify this particular pairing of ICS feed and
157
     * Canvas calendar
158
     **/
159
    public function getId($algorithm = 'md5')
160
    {
161
        return hash($algorithm, $this->getContext()->getCanonicalUrl() . $this->getFeedUrl());
162
    }
163
164
    /**
165
     * Get the Canvas context code for this calendar
166
     *
167
     * @return string
168
     */
169
    public function getContextCode()
170
    {
171
        return $this->getContext()->getContext() . '_' . $this->getContext()->getId();
172
    }
173
174
    /**
175
     * Save this calendar to the database
176
     *
177
     * @return void
178
     */
179
    public function save()
180
    {
181
        $db = Syncable::getDatabase();
182
        $find = $db->prepare(
183
            "SELECT * FROM `calendars` WHERE `id` = :id"
184
        );
185
        $update = $db->prepare(
186
            "UPDATE `calendars`
187
                SET
188
                    `name` = :name,
189
                    `canvas_url` = :canvas_url,
190
                    `ics_url` = :ics_url,
191
                    `synced` = :synced,
192
                    `enable_regex_filter` = :enable_regex_filter,
193
                    `include_regexp` = :include_regexp,
194
                    `exclude_regexp` = :exclude_regexp
195
                WHERE
196
                `id` = :id"
197
        );
198
        $insert = $db->prepare(
199
            "INSERT INTO `calendars`
200
                (
201
                    `id`,
202
                    `name`,
203
                    `canvas_url`,
204
                    `ics_url`,
205
                    `synced`,
206
                    `enable_regex_filter`,
207
                    `include_regexp`,
208
                    `exclude_regexp`
209
                ) VALUES (
210
                    :id,
211
                    :name,
212
                    :canvas_url,
213
                    :ics_url,
214
                    :synced
215
                    :enable_regex_filter,
216
                    :include_regexp,
217
                    :exclude_regexp
218
                )"
219
        );
220
        $params = [
221
            'id' => $this->getId(),
222
            'name' => $this->getName(),
223
            'canvas_url' => $this->getContext()->getCanonicalUrl(),
224
            'ics_url' => $this->getFeedUrl(),
225
            'synced' => Syncable::getSyncTimestamp(),
0 ignored issues
show
Bug introduced by
The method getSyncTimestamp() does not seem to exist on object<smtech\CanvasICSS...yncIntoCanvas\Syncable>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
226
            'enable_regex_filter' => $this->getFilter()->isEnabled(),
227
            'include_regexp' => $this->getFilter()->getIncludeExpression(),
228
            'exclude_regexp' => $this->getFilter()->getExcludeExpression()
229
        ];
230
231
        $find->execute($params);
232
        if ($find->fetch() !== false) {
233
            $update->execute($params);
234
        } else {
235
            $insert->execute($params);
236
        }
237
    }
238
239
    /**
240
     * Load a Calendar from the database
241
     *
242
     * @param string|int $id
243
     * @return Calendar
244
     */
245
    public static function load($id)
246
    {
247
        $find = Syncable::getDatabase()->prepare(
248
            "SELECT * FROM `calendars` WHERE `id` = :id"
249
        );
250
        $find->execute($id);
251
        if (($calendar = $find->fetch()) !== false) {
252
            return new Calendar(
253
                $calendar['canvas_url'],
254
                $calendar['ics_url'],
255
                $calendar['enable_regex_filter'],
256
                $calendar['include_regexp'],
257
                $calendar['exclude_regexp']
258
            );
259
        }
260
        return null;
261
    }
262
263
    /**
264
     * Delete this calendar from the database
265
     *
266
     * @return void
267
     */
268
    public function delete()
269
    {
270
        $delete = Syncable::getDatabase()->prepare(
271
            "DELETE FROM `calendars` WHERE `id` = :id"
272
        );
273
        $delete->execute($this->getId());
274
    }
275
276
    /**
277
     * Sync the calendar feed into Canvas
278
     *
279
     * @param Log $log
280
     * @return void
281
     */
282
    public function sync(Log $log)
283
    {
284
        try {
285
            Syncable::getApi()->get($this->getContext()->getVerificationUrl());
286
        } catch (Exception $e) {
287
            $this->logThrow(new Exception("Cannot sync calendars without a valid Canvas context"), $log);
288
        }
289
290
        if (!DataUtilities::URLexists($this->getFeedUrl())) {
291
            $this->logThrow(new Exception("Cannot sync calendars with a valid calendar feed"), $log);
292
        }
293
294
        $this->log(Syncable::getTimestamp() . ' sync started', $log);
295
296
        $this->save();
297
298
        $ics = new vcalendar([
299
            'unique_id' => __FILE__,
300
            'url' => $this->getFeedUrl()
301
        ]);
302
        $ics->parse();
303
304
        /*
305
         * TODO: would it be worth the performance improvement to just process
306
         *     things from today's date forward? (i.e. ignore old items, even
307
         *     if they've changed...)
308
         */
309
        /*
310
         * TODO:0 the best window for syncing would be the term of the course
311
         *     in question, right? issue:12
312
         */
313
        /*
314
         * TODO:0 Arbitrarily selecting events in for a year on either side of
315
         *     today's date, probably a better system? issue:12
316
         */
317
        if (($components = $ics->selectComponents(
318
            date('Y') - 1, // startYear
319
            date('m'), // startMonth
320
            date('d'), // startDay
321
            date('Y') + 1, // endYEar
322
            date('m'), // endMonth
323
            date('d'), // endDay
324
            'vevent', // cType
325
            false, // flat
326
            true, // any
327
            true // split
328
        )) !== false) {
329
            foreach ($components as $year) {
330
                foreach ($year as $month => $days) {
331
                    foreach ($days as $day => $events) {
332
                        foreach ($events as $i => $_event) {
333
                            try {
334
                                $event = new Event($_event, $this);
335
                                if ($this->getFilter()->filter($event)) {
336
                                    $event->save();
337
                                } else {
338
                                    $event->delete();
339
                                }
340
                            } catch (Exception $e) {
341
                                $this->logThrow($e, $log);
342
                            }
343
                        }
344
                    }
345
                }
346
            }
347
        }
348
349
        try {
350
            Event::purgeUnmatched(Syncable::getTimestamp(), $this);
351
        } catch (Exception $e) {
352
            $this->logThrow($e, $log);
353
        }
354
355
        $this->log(Syncable::getTimestamp() . ' sync finished', $log);
356
    }
357
358
    /**
359
     * Write to the log, if we have one
360
     *
361
     * @param string $message
362
     * @param Log $log
363
     * @param mixed $priority
364
     * @return void
365
     */
366
    private function log($message, Log $log, $priority = PEAR_LOG_INFO)
367
    {
368
        if ($log) {
369
            $log->log($message, $priority);
370
        }
371
    }
372
373
    /**
374
     * Write exception to the log, if we have one, or re-throw the exception
375
     *
376
     * @param Exception $exception
377
     * @param Log $log
378
     * @param string $priority
379
     * @return void
380
     * @throws Exception If no log is available
381
     */
382
    private function logThrow(
383
        Exception $exception,
384
        Log $log,
385
        $priority = PEAR_LOG_ERR
386
    ) {
387
        if ($log) {
388
            $log->log($exception->getMessage() . ': ' . $exception->getTraceAsString(), $priority);
389
        } else {
390
            throw $exception;
391
        }
392
    }
393
}
394