1<?php
2
3namespace Sabre\CalDAV\Backend;
4
5use Sabre\VObject;
6use Sabre\CalDAV;
7use Sabre\DAV;
8use Sabre\DAV\Exception\Forbidden;
9
10/**
11 * PDO CalDAV backend
12 *
13 * This backend is used to store calendar-data in a PDO database, such as
14 * sqlite or MySQL
15 *
16 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
17 * @author Evert Pot (http://evertpot.com/)
18 * @license http://sabre.io/license/ Modified BSD License
19 */
20class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
21
22    /**
23     * We need to specify a max date, because we need to stop *somewhere*
24     *
25     * On 32 bit system the maximum for a signed integer is 2147483647, so
26     * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
27     * in 2038-01-19 to avoid problems when the date is converted
28     * to a unix timestamp.
29     */
30    const MAX_DATE = '2038-01-01';
31
32    /**
33     * pdo
34     *
35     * @var \PDO
36     */
37    protected $pdo;
38
39    /**
40     * The table name that will be used for calendars
41     *
42     * @var string
43     */
44    public $calendarTableName = 'calendars';
45
46    /**
47     * The table name that will be used for calendar objects
48     *
49     * @var string
50     */
51    public $calendarObjectTableName = 'calendarobjects';
52
53    /**
54     * The table name that will be used for tracking changes in calendars.
55     *
56     * @var string
57     */
58    public $calendarChangesTableName = 'calendarchanges';
59
60    /**
61     * The table name that will be used inbox items.
62     *
63     * @var string
64     */
65    public $schedulingObjectTableName = 'schedulingobjects';
66
67    /**
68     * The table name that will be used for calendar subscriptions.
69     *
70     * @var string
71     */
72    public $calendarSubscriptionsTableName = 'calendarsubscriptions';
73
74    /**
75     * List of CalDAV properties, and how they map to database fieldnames
76     * Add your own properties by simply adding on to this array.
77     *
78     * Note that only string-based properties are supported here.
79     *
80     * @var array
81     */
82    public $propertyMap = [
83        '{DAV:}displayname'                                   => 'displayname',
84        '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
85        '{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
86        '{http://apple.com/ns/ical/}calendar-order'           => 'calendarorder',
87        '{http://apple.com/ns/ical/}calendar-color'           => 'calendarcolor',
88    ];
89
90    /**
91     * List of subscription properties, and how they map to database fieldnames.
92     *
93     * @var array
94     */
95    public $subscriptionPropertyMap = [
96        '{DAV:}displayname'                                           => 'displayname',
97        '{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
98        '{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
99        '{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
100        '{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
101        '{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
102        '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
103    ];
104
105    /**
106     * Creates the backend
107     *
108     * @param \PDO $pdo
109     */
110    function __construct(\PDO $pdo) {
111
112        $this->pdo = $pdo;
113
114    }
115
116    /**
117     * Returns a list of calendars for a principal.
118     *
119     * Every project is an array with the following keys:
120     *  * id, a unique id that will be used by other functions to modify the
121     *    calendar. This can be the same as the uri or a database key.
122     *  * uri. This is just the 'base uri' or 'filename' of the calendar.
123     *  * principaluri. The owner of the calendar. Almost always the same as
124     *    principalUri passed to this method.
125     *
126     * Furthermore it can contain webdav properties in clark notation. A very
127     * common one is '{DAV:}displayname'.
128     *
129     * Many clients also require:
130     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
131     * For this property, you can just return an instance of
132     * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet.
133     *
134     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
135     * ACL will automatically be put in read-only mode.
136     *
137     * @param string $principalUri
138     * @return array
139     */
140    function getCalendarsForUser($principalUri) {
141
142        $fields = array_values($this->propertyMap);
143        $fields[] = 'id';
144        $fields[] = 'uri';
145        $fields[] = 'synctoken';
146        $fields[] = 'components';
147        $fields[] = 'principaluri';
148        $fields[] = 'transparent';
149
150        // Making fields a comma-delimited list
151        $fields = implode(', ', $fields);
152        $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC");
153        $stmt->execute([$principalUri]);
154
155        $calendars = [];
156        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
157
158            $components = [];
159            if ($row['components']) {
160                $components = explode(',', $row['components']);
161            }
162
163            $calendar = [
164                'id'                                                                 => $row['id'],
165                'uri'                                                                => $row['uri'],
166                'principaluri'                                                       => $row['principaluri'],
167                '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag'                  => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ? $row['synctoken'] : '0'),
168                '{http://sabredav.org/ns}sync-token'                                 => $row['synctoken'] ? $row['synctoken'] : '0',
169                '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components),
170                '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'         => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
171            ];
172
173
174            foreach ($this->propertyMap as $xmlName => $dbName) {
175                $calendar[$xmlName] = $row[$dbName];
176            }
177
178            $calendars[] = $calendar;
179
180        }
181
182        return $calendars;
183
184    }
185
186    /**
187     * Creates a new calendar for a principal.
188     *
189     * If the creation was a success, an id must be returned that can be used
190     * to reference this calendar in other methods, such as updateCalendar.
191     *
192     * @param string $principalUri
193     * @param string $calendarUri
194     * @param array $properties
195     * @return string
196     */
197    function createCalendar($principalUri, $calendarUri, array $properties) {
198
199        $fieldNames = [
200            'principaluri',
201            'uri',
202            'synctoken',
203            'transparent',
204        ];
205        $values = [
206            ':principaluri' => $principalUri,
207            ':uri'          => $calendarUri,
208            ':synctoken'    => 1,
209            ':transparent'  => 0,
210        ];
211
212        // Default value
213        $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
214        $fieldNames[] = 'components';
215        if (!isset($properties[$sccs])) {
216            $values[':components'] = 'VEVENT,VTODO';
217        } else {
218            if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) {
219                throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet');
220            }
221            $values[':components'] = implode(',', $properties[$sccs]->getValue());
222        }
223        $transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
224        if (isset($properties[$transp])) {
225            $values[':transparent'] = $properties[$transp]->getValue() === 'transparent';
226        }
227
228        foreach ($this->propertyMap as $xmlName => $dbName) {
229            if (isset($properties[$xmlName])) {
230
231                $values[':' . $dbName] = $properties[$xmlName];
232                $fieldNames[] = $dbName;
233            }
234        }
235
236        $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")");
237        $stmt->execute($values);
238
239        return $this->pdo->lastInsertId();
240
241    }
242
243    /**
244     * Updates properties for a calendar.
245     *
246     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
247     * To do the actual updates, you must tell this object which properties
248     * you're going to process with the handle() method.
249     *
250     * Calling the handle method is like telling the PropPatch object "I
251     * promise I can handle updating this property".
252     *
253     * Read the PropPatch documenation for more info and examples.
254     *
255     * @param string $calendarId
256     * @param \Sabre\DAV\PropPatch $propPatch
257     * @return void
258     */
259    function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {
260
261        $supportedProperties = array_keys($this->propertyMap);
262        $supportedProperties[] = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
263
264        $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
265            $newValues = [];
266            foreach ($mutations as $propertyName => $propertyValue) {
267
268                switch ($propertyName) {
269                    case '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' :
270                        $fieldName = 'transparent';
271                        $newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
272                        break;
273                    default :
274                        $fieldName = $this->propertyMap[$propertyName];
275                        $newValues[$fieldName] = $propertyValue;
276                        break;
277                }
278
279            }
280            $valuesSql = [];
281            foreach ($newValues as $fieldName => $value) {
282                $valuesSql[] = $fieldName . ' = ?';
283            }
284
285            $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ', $valuesSql) . " WHERE id = ?");
286            $newValues['id'] = $calendarId;
287            $stmt->execute(array_values($newValues));
288
289            $this->addChange($calendarId, "", 2);
290
291            return true;
292
293        });
294
295    }
296
297    /**
298     * Delete a calendar and all it's objects
299     *
300     * @param string $calendarId
301     * @return void
302     */
303    function deleteCalendar($calendarId) {
304
305        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?');
306        $stmt->execute([$calendarId]);
307
308        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarTableName . ' WHERE id = ?');
309        $stmt->execute([$calendarId]);
310
311        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarChangesTableName . ' WHERE calendarid = ?');
312        $stmt->execute([$calendarId]);
313
314    }
315
316    /**
317     * Returns all calendar objects within a calendar.
318     *
319     * Every item contains an array with the following keys:
320     *   * calendardata - The iCalendar-compatible calendar data
321     *   * uri - a unique key which will be used to construct the uri. This can
322     *     be any arbitrary string, but making sure it ends with '.ics' is a
323     *     good idea. This is only the basename, or filename, not the full
324     *     path.
325     *   * lastmodified - a timestamp of the last modification time
326     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
327     *   '  "abcdef"')
328     *   * size - The size of the calendar objects, in bytes.
329     *   * component - optional, a string containing the type of object, such
330     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
331     *     the Content-Type header.
332     *
333     * Note that the etag is optional, but it's highly encouraged to return for
334     * speed reasons.
335     *
336     * The calendardata is also optional. If it's not returned
337     * 'getCalendarObject' will be called later, which *is* expected to return
338     * calendardata.
339     *
340     * If neither etag or size are specified, the calendardata will be
341     * used/fetched to determine these numbers. If both are specified the
342     * amount of times this is needed is reduced by a great degree.
343     *
344     * @param string $calendarId
345     * @return array
346     */
347    function getCalendarObjects($calendarId) {
348
349        $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?');
350        $stmt->execute([$calendarId]);
351
352        $result = [];
353        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
354            $result[] = [
355                'id'           => $row['id'],
356                'uri'          => $row['uri'],
357                'lastmodified' => $row['lastmodified'],
358                'etag'         => '"' . $row['etag'] . '"',
359                'calendarid'   => $row['calendarid'],
360                'size'         => (int)$row['size'],
361                'component'    => strtolower($row['componenttype']),
362            ];
363        }
364
365        return $result;
366
367    }
368
369    /**
370     * Returns information from a single calendar object, based on it's object
371     * uri.
372     *
373     * The object uri is only the basename, or filename and not a full path.
374     *
375     * The returned array must have the same keys as getCalendarObjects. The
376     * 'calendardata' object is required here though, while it's not required
377     * for getCalendarObjects.
378     *
379     * This method must return null if the object did not exist.
380     *
381     * @param string $calendarId
382     * @param string $objectUri
383     * @return array|null
384     */
385    function getCalendarObject($calendarId, $objectUri) {
386
387        $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?');
388        $stmt->execute([$calendarId, $objectUri]);
389        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
390
391        if (!$row) return null;
392
393        return [
394            'id'            => $row['id'],
395            'uri'           => $row['uri'],
396            'lastmodified'  => $row['lastmodified'],
397            'etag'          => '"' . $row['etag'] . '"',
398            'calendarid'    => $row['calendarid'],
399            'size'          => (int)$row['size'],
400            'calendardata'  => $row['calendardata'],
401            'component'     => strtolower($row['componenttype']),
402         ];
403
404    }
405
406    /**
407     * Returns a list of calendar objects.
408     *
409     * This method should work identical to getCalendarObject, but instead
410     * return all the calendar objects in the list as an array.
411     *
412     * If the backend supports this, it may allow for some speed-ups.
413     *
414     * @param mixed $calendarId
415     * @param array $uris
416     * @return array
417     */
418    function getMultipleCalendarObjects($calendarId, array $uris) {
419
420        $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri IN (';
421        // Inserting a whole bunch of question marks
422        $query .= implode(',', array_fill(0, count($uris), '?'));
423        $query .= ')';
424
425        $stmt = $this->pdo->prepare($query);
426        $stmt->execute(array_merge([$calendarId], $uris));
427
428        $result = [];
429        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
430
431            $result[] = [
432                'id'           => $row['id'],
433                'uri'          => $row['uri'],
434                'lastmodified' => $row['lastmodified'],
435                'etag'         => '"' . $row['etag'] . '"',
436                'calendarid'   => $row['calendarid'],
437                'size'         => (int)$row['size'],
438                'calendardata' => $row['calendardata'],
439                'component'    => strtolower($row['componenttype']),
440            ];
441
442        }
443        return $result;
444
445    }
446
447
448    /**
449     * Creates a new calendar object.
450     *
451     * The object uri is only the basename, or filename and not a full path.
452     *
453     * It is possible return an etag from this function, which will be used in
454     * the response to this PUT request. Note that the ETag must be surrounded
455     * by double-quotes.
456     *
457     * However, you should only really return this ETag if you don't mangle the
458     * calendar-data. If the result of a subsequent GET to this object is not
459     * the exact same as this request body, you should omit the ETag.
460     *
461     * @param mixed $calendarId
462     * @param string $objectUri
463     * @param string $calendarData
464     * @return string|null
465     */
466    function createCalendarObject($calendarId, $objectUri, $calendarData) {
467
468        $extraData = $this->getDenormalizedData($calendarData);
469
470        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarObjectTableName . ' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)');
471        $stmt->execute([
472            $calendarId,
473            $objectUri,
474            $calendarData,
475            time(),
476            $extraData['etag'],
477            $extraData['size'],
478            $extraData['componentType'],
479            $extraData['firstOccurence'],
480            $extraData['lastOccurence'],
481            $extraData['uid'],
482        ]);
483        $this->addChange($calendarId, $objectUri, 1);
484
485        return '"' . $extraData['etag'] . '"';
486
487    }
488
489    /**
490     * Updates an existing calendarobject, based on it's uri.
491     *
492     * The object uri is only the basename, or filename and not a full path.
493     *
494     * It is possible return an etag from this function, which will be used in
495     * the response to this PUT request. Note that the ETag must be surrounded
496     * by double-quotes.
497     *
498     * However, you should only really return this ETag if you don't mangle the
499     * calendar-data. If the result of a subsequent GET to this object is not
500     * the exact same as this request body, you should omit the ETag.
501     *
502     * @param mixed $calendarId
503     * @param string $objectUri
504     * @param string $calendarData
505     * @return string|null
506     */
507    function updateCalendarObject($calendarId, $objectUri, $calendarData) {
508
509        $extraData = $this->getDenormalizedData($calendarData);
510
511        $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarObjectTableName . ' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?');
512        $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]);
513
514        $this->addChange($calendarId, $objectUri, 2);
515
516        return '"' . $extraData['etag'] . '"';
517
518    }
519
520    /**
521     * Parses some information from calendar objects, used for optimized
522     * calendar-queries.
523     *
524     * Returns an array with the following keys:
525     *   * etag - An md5 checksum of the object without the quotes.
526     *   * size - Size of the object in bytes
527     *   * componentType - VEVENT, VTODO or VJOURNAL
528     *   * firstOccurence
529     *   * lastOccurence
530     *   * uid - value of the UID property
531     *
532     * @param string $calendarData
533     * @return array
534     */
535    protected function getDenormalizedData($calendarData) {
536
537        $vObject = VObject\Reader::read($calendarData);
538        $componentType = null;
539        $component = null;
540        $firstOccurence = null;
541        $lastOccurence = null;
542        $uid = null;
543        foreach ($vObject->getComponents() as $component) {
544            if ($component->name !== 'VTIMEZONE') {
545                $componentType = $component->name;
546                $uid = (string)$component->UID;
547                break;
548            }
549        }
550        if (!$componentType) {
551            throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
552        }
553        if ($componentType === 'VEVENT') {
554            $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
555            // Finding the last occurence is a bit harder
556            if (!isset($component->RRULE)) {
557                if (isset($component->DTEND)) {
558                    $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
559                } elseif (isset($component->DURATION)) {
560                    $endDate = clone $component->DTSTART->getDateTime();
561                    $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
562                    $lastOccurence = $endDate->getTimeStamp();
563                } elseif (!$component->DTSTART->hasTime()) {
564                    $endDate = clone $component->DTSTART->getDateTime();
565                    $endDate->modify('+1 day');
566                    $lastOccurence = $endDate->getTimeStamp();
567                } else {
568                    $lastOccurence = $firstOccurence;
569                }
570            } else {
571                $it = new VObject\Recur\EventIterator($vObject, (string)$component->UID);
572                $maxDate = new \DateTime(self::MAX_DATE);
573                if ($it->isInfinite()) {
574                    $lastOccurence = $maxDate->getTimeStamp();
575                } else {
576                    $end = $it->getDtEnd();
577                    while ($it->valid() && $end < $maxDate) {
578                        $end = $it->getDtEnd();
579                        $it->next();
580
581                    }
582                    $lastOccurence = $end->getTimeStamp();
583                }
584
585            }
586        }
587
588        return [
589            'etag'           => md5($calendarData),
590            'size'           => strlen($calendarData),
591            'componentType'  => $componentType,
592            'firstOccurence' => $firstOccurence,
593            'lastOccurence'  => $lastOccurence,
594            'uid'            => $uid,
595        ];
596
597    }
598
599    /**
600     * Deletes an existing calendar object.
601     *
602     * The object uri is only the basename, or filename and not a full path.
603     *
604     * @param string $calendarId
605     * @param string $objectUri
606     * @return void
607     */
608    function deleteCalendarObject($calendarId, $objectUri) {
609
610        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?');
611        $stmt->execute([$calendarId, $objectUri]);
612
613        $this->addChange($calendarId, $objectUri, 3);
614
615    }
616
617    /**
618     * Performs a calendar-query on the contents of this calendar.
619     *
620     * The calendar-query is defined in RFC4791 : CalDAV. Using the
621     * calendar-query it is possible for a client to request a specific set of
622     * object, based on contents of iCalendar properties, date-ranges and
623     * iCalendar component types (VTODO, VEVENT).
624     *
625     * This method should just return a list of (relative) urls that match this
626     * query.
627     *
628     * The list of filters are specified as an array. The exact array is
629     * documented by \Sabre\CalDAV\CalendarQueryParser.
630     *
631     * Note that it is extremely likely that getCalendarObject for every path
632     * returned from this method will be called almost immediately after. You
633     * may want to anticipate this to speed up these requests.
634     *
635     * This method provides a default implementation, which parses *all* the
636     * iCalendar objects in the specified calendar.
637     *
638     * This default may well be good enough for personal use, and calendars
639     * that aren't very large. But if you anticipate high usage, big calendars
640     * or high loads, you are strongly adviced to optimize certain paths.
641     *
642     * The best way to do so is override this method and to optimize
643     * specifically for 'common filters'.
644     *
645     * Requests that are extremely common are:
646     *   * requests for just VEVENTS
647     *   * requests for just VTODO
648     *   * requests with a time-range-filter on a VEVENT.
649     *
650     * ..and combinations of these requests. It may not be worth it to try to
651     * handle every possible situation and just rely on the (relatively
652     * easy to use) CalendarQueryValidator to handle the rest.
653     *
654     * Note that especially time-range-filters may be difficult to parse. A
655     * time-range filter specified on a VEVENT must for instance also handle
656     * recurrence rules correctly.
657     * A good example of how to interprete all these filters can also simply
658     * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
659     * as possible, so it gives you a good idea on what type of stuff you need
660     * to think of.
661     *
662     * This specific implementation (for the PDO) backend optimizes filters on
663     * specific components, and VEVENT time-ranges.
664     *
665     * @param string $calendarId
666     * @param array $filters
667     * @return array
668     */
669    function calendarQuery($calendarId, array $filters) {
670
671        $componentType = null;
672        $requirePostFilter = true;
673        $timeRange = null;
674
675        // if no filters were specified, we don't need to filter after a query
676        if (!$filters['prop-filters'] && !$filters['comp-filters']) {
677            $requirePostFilter = false;
678        }
679
680        // Figuring out if there's a component filter
681        if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
682            $componentType = $filters['comp-filters'][0]['name'];
683
684            // Checking if we need post-filters
685            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
686                $requirePostFilter = false;
687            }
688            // There was a time-range filter
689            if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
690                $timeRange = $filters['comp-filters'][0]['time-range'];
691
692                // If start time OR the end time is not specified, we can do a
693                // 100% accurate mysql query.
694                if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
695                    $requirePostFilter = false;
696                }
697            }
698
699        }
700
701        if ($requirePostFilter) {
702            $query = "SELECT uri, calendardata FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid";
703        } else {
704            $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid";
705        }
706
707        $values = [
708            'calendarid' => $calendarId,
709        ];
710
711        if ($componentType) {
712            $query .= " AND componenttype = :componenttype";
713            $values['componenttype'] = $componentType;
714        }
715
716        if ($timeRange && $timeRange['start']) {
717            $query .= " AND lastoccurence > :startdate";
718            $values['startdate'] = $timeRange['start']->getTimeStamp();
719        }
720        if ($timeRange && $timeRange['end']) {
721            $query .= " AND firstoccurence < :enddate";
722            $values['enddate'] = $timeRange['end']->getTimeStamp();
723        }
724
725        $stmt = $this->pdo->prepare($query);
726        $stmt->execute($values);
727
728        $result = [];
729        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
730            if ($requirePostFilter) {
731                if (!$this->validateFilterForObject($row, $filters)) {
732                    continue;
733                }
734            }
735            $result[] = $row['uri'];
736
737        }
738
739        return $result;
740
741    }
742
743    /**
744     * Searches through all of a users calendars and calendar objects to find
745     * an object with a specific UID.
746     *
747     * This method should return the path to this object, relative to the
748     * calendar home, so this path usually only contains two parts:
749     *
750     * calendarpath/objectpath.ics
751     *
752     * If the uid is not found, return null.
753     *
754     * This method should only consider * objects that the principal owns, so
755     * any calendars owned by other principals that also appear in this
756     * collection should be ignored.
757     *
758     * @param string $principalUri
759     * @param string $uid
760     * @return string|null
761     */
762    function getCalendarObjectByUID($principalUri, $uid) {
763
764        $query = <<<SQL
765SELECT
766    calendars.uri AS calendaruri, calendarobjects.uri as objecturi
767FROM
768    $this->calendarObjectTableName AS calendarobjects
769LEFT JOIN
770    $this->calendarTableName AS calendars
771    ON calendarobjects.calendarid = calendars.id
772WHERE
773    calendars.principaluri = ?
774    AND
775    calendarobjects.uid = ?
776SQL;
777
778        $stmt = $this->pdo->prepare($query);
779        $stmt->execute([$principalUri, $uid]);
780
781        if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
782            return $row['calendaruri'] . '/' . $row['objecturi'];
783        }
784
785    }
786
787    /**
788     * The getChanges method returns all the changes that have happened, since
789     * the specified syncToken in the specified calendar.
790     *
791     * This function should return an array, such as the following:
792     *
793     * [
794     *   'syncToken' => 'The current synctoken',
795     *   'added'   => [
796     *      'new.txt',
797     *   ],
798     *   'modified'   => [
799     *      'modified.txt',
800     *   ],
801     *   'deleted' => [
802     *      'foo.php.bak',
803     *      'old.txt'
804     *   ]
805     * ];
806     *
807     * The returned syncToken property should reflect the *current* syncToken
808     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
809     * property this is needed here too, to ensure the operation is atomic.
810     *
811     * If the $syncToken argument is specified as null, this is an initial
812     * sync, and all members should be reported.
813     *
814     * The modified property is an array of nodenames that have changed since
815     * the last token.
816     *
817     * The deleted property is an array with nodenames, that have been deleted
818     * from collection.
819     *
820     * The $syncLevel argument is basically the 'depth' of the report. If it's
821     * 1, you only have to report changes that happened only directly in
822     * immediate descendants. If it's 2, it should also include changes from
823     * the nodes below the child collections. (grandchildren)
824     *
825     * The $limit argument allows a client to specify how many results should
826     * be returned at most. If the limit is not specified, it should be treated
827     * as infinite.
828     *
829     * If the limit (infinite or not) is higher than you're willing to return,
830     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
831     *
832     * If the syncToken is expired (due to data cleanup) or unknown, you must
833     * return null.
834     *
835     * The limit is 'suggestive'. You are free to ignore it.
836     *
837     * @param string $calendarId
838     * @param string $syncToken
839     * @param int $syncLevel
840     * @param int $limit
841     * @return array
842     */
843    function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
844
845        // Current synctoken
846        $stmt = $this->pdo->prepare('SELECT synctoken FROM ' . $this->calendarTableName . ' WHERE id = ?');
847        $stmt->execute([ $calendarId ]);
848        $currentToken = $stmt->fetchColumn(0);
849
850        if (is_null($currentToken)) return null;
851
852        $result = [
853            'syncToken' => $currentToken,
854            'added'     => [],
855            'modified'  => [],
856            'deleted'   => [],
857        ];
858
859        if ($syncToken) {
860
861            $query = "SELECT uri, operation FROM " . $this->calendarChangesTableName . " WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken";
862            if ($limit > 0) $query .= " LIMIT " . (int)$limit;
863
864            // Fetching all changes
865            $stmt = $this->pdo->prepare($query);
866            $stmt->execute([$syncToken, $currentToken, $calendarId]);
867
868            $changes = [];
869
870            // This loop ensures that any duplicates are overwritten, only the
871            // last change on a node is relevant.
872            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
873
874                $changes[$row['uri']] = $row['operation'];
875
876            }
877
878            foreach ($changes as $uri => $operation) {
879
880                switch ($operation) {
881                    case 1 :
882                        $result['added'][] = $uri;
883                        break;
884                    case 2 :
885                        $result['modified'][] = $uri;
886                        break;
887                    case 3 :
888                        $result['deleted'][] = $uri;
889                        break;
890                }
891
892            }
893        } else {
894            // No synctoken supplied, this is the initial sync.
895            $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = ?";
896            $stmt = $this->pdo->prepare($query);
897            $stmt->execute([$calendarId]);
898
899            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
900        }
901        return $result;
902
903    }
904
905    /**
906     * Adds a change record to the calendarchanges table.
907     *
908     * @param mixed $calendarId
909     * @param string $objectUri
910     * @param int $operation 1 = add, 2 = modify, 3 = delete.
911     * @return void
912     */
913    protected function addChange($calendarId, $objectUri, $operation) {
914
915        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarChangesTableName . ' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM ' . $this->calendarTableName . ' WHERE id = ?');
916        $stmt->execute([
917            $objectUri,
918            $calendarId,
919            $operation,
920            $calendarId
921        ]);
922        $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarTableName . ' SET synctoken = synctoken + 1 WHERE id = ?');
923        $stmt->execute([
924            $calendarId
925        ]);
926
927    }
928
929    /**
930     * Returns a list of subscriptions for a principal.
931     *
932     * Every subscription is an array with the following keys:
933     *  * id, a unique id that will be used by other functions to modify the
934     *    subscription. This can be the same as the uri or a database key.
935     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
936     *  * principaluri. The owner of the subscription. Almost always the same as
937     *    principalUri passed to this method.
938     *  * source. Url to the actual feed
939     *
940     * Furthermore, all the subscription info must be returned too:
941     *
942     * 1. {DAV:}displayname
943     * 2. {http://apple.com/ns/ical/}refreshrate
944     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
945     *    should not be stripped).
946     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
947     *    should not be stripped).
948     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
949     *    attachments should not be stripped).
950     * 7. {http://apple.com/ns/ical/}calendar-color
951     * 8. {http://apple.com/ns/ical/}calendar-order
952     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
953     *    (should just be an instance of
954     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
955     *    default components).
956     *
957     * @param string $principalUri
958     * @return array
959     */
960    function getSubscriptionsForUser($principalUri) {
961
962        $fields = array_values($this->subscriptionPropertyMap);
963        $fields[] = 'id';
964        $fields[] = 'uri';
965        $fields[] = 'source';
966        $fields[] = 'principaluri';
967        $fields[] = 'lastmodified';
968
969        // Making fields a comma-delimited list
970        $fields = implode(', ', $fields);
971        $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarSubscriptionsTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC");
972        $stmt->execute([$principalUri]);
973
974        $subscriptions = [];
975        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
976
977            $subscription = [
978                'id'           => $row['id'],
979                'uri'          => $row['uri'],
980                'principaluri' => $row['principaluri'],
981                'source'       => $row['source'],
982                'lastmodified' => $row['lastmodified'],
983
984                '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
985            ];
986
987            foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
988                if (!is_null($row[$dbName])) {
989                    $subscription[$xmlName] = $row[$dbName];
990                }
991            }
992
993            $subscriptions[] = $subscription;
994
995        }
996
997        return $subscriptions;
998
999    }
1000
1001    /**
1002     * Creates a new subscription for a principal.
1003     *
1004     * If the creation was a success, an id must be returned that can be used to reference
1005     * this subscription in other methods, such as updateSubscription.
1006     *
1007     * @param string $principalUri
1008     * @param string $uri
1009     * @param array $properties
1010     * @return mixed
1011     */
1012    function createSubscription($principalUri, $uri, array $properties) {
1013
1014        $fieldNames = [
1015            'principaluri',
1016            'uri',
1017            'source',
1018            'lastmodified',
1019        ];
1020
1021        if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1022            throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1023        }
1024
1025        $values = [
1026            ':principaluri' => $principalUri,
1027            ':uri'          => $uri,
1028            ':source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1029            ':lastmodified' => time(),
1030        ];
1031
1032        foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
1033            if (isset($properties[$xmlName])) {
1034
1035                $values[':' . $dbName] = $properties[$xmlName];
1036                $fieldNames[] = $dbName;
1037            }
1038        }
1039
1040        $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarSubscriptionsTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")");
1041        $stmt->execute($values);
1042
1043        return $this->pdo->lastInsertId();
1044
1045    }
1046
1047    /**
1048     * Updates a subscription
1049     *
1050     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1051     * To do the actual updates, you must tell this object which properties
1052     * you're going to process with the handle() method.
1053     *
1054     * Calling the handle method is like telling the PropPatch object "I
1055     * promise I can handle updating this property".
1056     *
1057     * Read the PropPatch documenation for more info and examples.
1058     *
1059     * @param mixed $subscriptionId
1060     * @param \Sabre\DAV\PropPatch $propPatch
1061     * @return void
1062     */
1063    function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) {
1064
1065        $supportedProperties = array_keys($this->subscriptionPropertyMap);
1066        $supportedProperties[] = '{http://calendarserver.org/ns/}source';
1067
1068        $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1069
1070            $newValues = [];
1071
1072            foreach ($mutations as $propertyName => $propertyValue) {
1073
1074                if ($propertyName === '{http://calendarserver.org/ns/}source') {
1075                    $newValues['source'] = $propertyValue->getHref();
1076                } else {
1077                    $fieldName = $this->subscriptionPropertyMap[$propertyName];
1078                    $newValues[$fieldName] = $propertyValue;
1079                }
1080
1081            }
1082
1083            // Now we're generating the sql query.
1084            $valuesSql = [];
1085            foreach ($newValues as $fieldName => $value) {
1086                $valuesSql[] = $fieldName . ' = ?';
1087            }
1088
1089            $stmt = $this->pdo->prepare("UPDATE " . $this->calendarSubscriptionsTableName . " SET " . implode(', ', $valuesSql) . ", lastmodified = ? WHERE id = ?");
1090            $newValues['lastmodified'] = time();
1091            $newValues['id'] = $subscriptionId;
1092            $stmt->execute(array_values($newValues));
1093
1094            return true;
1095
1096        });
1097
1098    }
1099
1100    /**
1101     * Deletes a subscription
1102     *
1103     * @param mixed $subscriptionId
1104     * @return void
1105     */
1106    function deleteSubscription($subscriptionId) {
1107
1108        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarSubscriptionsTableName . ' WHERE id = ?');
1109        $stmt->execute([$subscriptionId]);
1110
1111    }
1112
1113    /**
1114     * Returns a single scheduling object.
1115     *
1116     * The returned array should contain the following elements:
1117     *   * uri - A unique basename for the object. This will be used to
1118     *           construct a full uri.
1119     *   * calendardata - The iCalendar object
1120     *   * lastmodified - The last modification date. Can be an int for a unix
1121     *                    timestamp, or a PHP DateTime object.
1122     *   * etag - A unique token that must change if the object changed.
1123     *   * size - The size of the object, in bytes.
1124     *
1125     * @param string $principalUri
1126     * @param string $objectUri
1127     * @return array
1128     */
1129    function getSchedulingObject($principalUri, $objectUri) {
1130
1131        $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?');
1132        $stmt->execute([$principalUri, $objectUri]);
1133        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
1134
1135        if (!$row) return null;
1136
1137        return [
1138            'uri'          => $row['uri'],
1139            'calendardata' => $row['calendardata'],
1140            'lastmodified' => $row['lastmodified'],
1141            'etag'         => '"' . $row['etag'] . '"',
1142            'size'         => (int)$row['size'],
1143         ];
1144
1145    }
1146
1147    /**
1148     * Returns all scheduling objects for the inbox collection.
1149     *
1150     * These objects should be returned as an array. Every item in the array
1151     * should follow the same structure as returned from getSchedulingObject.
1152     *
1153     * The main difference is that 'calendardata' is optional.
1154     *
1155     * @param string $principalUri
1156     * @return array
1157     */
1158    function getSchedulingObjects($principalUri) {
1159
1160        $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ?');
1161        $stmt->execute([$principalUri]);
1162
1163        $result = [];
1164        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1165            $result[] = [
1166                'calendardata' => $row['calendardata'],
1167                'uri'          => $row['uri'],
1168                'lastmodified' => $row['lastmodified'],
1169                'etag'         => '"' . $row['etag'] . '"',
1170                'size'         => (int)$row['size'],
1171            ];
1172        }
1173
1174        return $result;
1175
1176    }
1177
1178    /**
1179     * Deletes a scheduling object
1180     *
1181     * @param string $principalUri
1182     * @param string $objectUri
1183     * @return void
1184     */
1185    function deleteSchedulingObject($principalUri, $objectUri) {
1186
1187        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?');
1188        $stmt->execute([$principalUri, $objectUri]);
1189
1190    }
1191
1192    /**
1193     * Creates a new scheduling object. This should land in a users' inbox.
1194     *
1195     * @param string $principalUri
1196     * @param string $objectUri
1197     * @param string $objectData
1198     * @return void
1199     */
1200    function createSchedulingObject($principalUri, $objectUri, $objectData) {
1201
1202        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->schedulingObjectTableName . ' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)');
1203        $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData) ]);
1204
1205    }
1206
1207}
1208