Completed
Push — develop ( 61ee3a...f2bd12 )
by Seth
08:02
created

Calendar::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 1
eloc 7
nc 1
nop 5
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,
0 ignored issues
show
Unused Code introduced by
The call to Filter::__construct() has too many arguments starting with $enableFilter.

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.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property filter does not seem to exist. Did you mean filters?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property filter does not seem to exist. Did you mean filters?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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(),
0 ignored issues
show
Bug introduced by
The method getIncludeExpression() does not seem to exist on object<smtech\CanvasICSS...toCanvas\Filter\Filter>.

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...
236
            'exclude_regexp' => $this->getFilter()->getExcludeExpression()
0 ignored issues
show
Bug introduced by
The method getExcludeExpression() does not seem to exist on object<smtech\CanvasICSS...toCanvas\Filter\Filter>.

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...
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