Test Failed
Push — main ( c8394f...8477f1 )
by Rafael
66:21
created

PDO::calendarQuery()   F

Complexity

Conditions 32
Paths 2177

Size

Total Lines 77
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 32
eloc 42
nc 2177
nop 2
dl 0
loc 77
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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