]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/CalDAV/Backend/PDO.php
removed community home addon
[friendica-addons.git] / dav / SabreDAV / lib / Sabre / CalDAV / Backend / PDO.php
1 <?php
2
3 use Sabre\VObject;
4
5 /**
6  * PDO CalDAV backend
7  *
8  * This backend is used to store calendar-data in a PDO database, such as
9  * sqlite or MySQL
10  *
11  * @package Sabre
12  * @subpackage CalDAV
13  * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
14  * @author Evert Pot (http://www.rooftopsolutions.nl/)
15  * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
16  */
17 class Sabre_CalDAV_Backend_PDO extends Sabre_CalDAV_Backend_Abstract {
18
19     /**
20      * We need to specify a max date, because we need to stop *somewhere*
21      *
22      * On 32 bit system the maximum for a signed integer is 2147483647, so
23      * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
24      * in 2038-01-19 to avoid problems when the date is converted
25      * to a unix timestamp.
26      */
27     const MAX_DATE = '2038-01-01';
28
29     /**
30      * pdo
31      *
32      * @var PDO
33      */
34     protected $pdo;
35
36     /**
37      * The table name that will be used for calendars
38      *
39      * @var string
40      */
41     protected $calendarTableName;
42
43     /**
44      * The table name that will be used for calendar objects
45      *
46      * @var string
47      */
48     protected $calendarObjectTableName;
49
50     /**
51      * List of CalDAV properties, and how they map to database fieldnames
52      *
53      * Add your own properties by simply adding on to this array
54      *
55      * @var array
56      */
57     public $propertyMap = array(
58         '{DAV:}displayname'                          => 'displayname',
59         '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
60         '{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
61         '{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
62         '{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
63     );
64
65     /**
66      * Creates the backend
67      *
68      * @param PDO $pdo
69      * @param string $calendarTableName
70      * @param string $calendarObjectTableName
71      */
72     public function __construct(PDO $pdo, $calendarTableName = 'calendars', $calendarObjectTableName = 'calendarobjects') {
73
74         $this->pdo = $pdo;
75         $this->calendarTableName = $calendarTableName;
76         $this->calendarObjectTableName = $calendarObjectTableName;
77
78     }
79
80     /**
81      * Returns a list of calendars for a principal.
82      *
83      * Every project is an array with the following keys:
84      *  * id, a unique id that will be used by other functions to modify the
85      *    calendar. This can be the same as the uri or a database key.
86      *  * uri, which the basename of the uri with which the calendar is
87      *    accessed.
88      *  * principaluri. The owner of the calendar. Almost always the same as
89      *    principalUri passed to this method.
90      *
91      * Furthermore it can contain webdav properties in clark notation. A very
92      * common one is '{DAV:}displayname'.
93      *
94      * @param string $principalUri
95      * @return array
96      */
97     public function getCalendarsForUser($principalUri) {
98
99         $fields = array_values($this->propertyMap);
100         $fields[] = 'id';
101         $fields[] = 'uri';
102         $fields[] = 'ctag';
103         $fields[] = 'components';
104         $fields[] = 'principaluri';
105
106         // Making fields a comma-delimited list
107         $fields = implode(', ', $fields);
108         $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM ".$this->calendarTableName." WHERE principaluri = ? ORDER BY calendarorder ASC");
109         $stmt->execute(array($principalUri));
110
111         $calendars = array();
112         while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
113
114             $components = array();
115             if ($row['components']) {
116                 $components = explode(',',$row['components']);
117             }
118
119             $calendar = array(
120                 'id' => $row['id'],
121                 'uri' => $row['uri'],
122                 'principaluri' => $row['principaluri'],
123                 '{' . Sabre_CalDAV_Plugin::NS_CALENDARSERVER . '}getctag' => $row['ctag']?$row['ctag']:'0',
124                 '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre_CalDAV_Property_SupportedCalendarComponentSet($components),
125             );
126
127
128             foreach($this->propertyMap as $xmlName=>$dbName) {
129                 $calendar[$xmlName] = $row[$dbName];
130             }
131
132             $calendars[] = $calendar;
133
134         }
135
136         return $calendars;
137
138     }
139
140     /**
141      * Creates a new calendar for a principal.
142      *
143      * If the creation was a success, an id must be returned that can be used to reference
144      * this calendar in other methods, such as updateCalendar
145      *
146      * @param string $principalUri
147      * @param string $calendarUri
148      * @param array $properties
149      * @return string
150      */
151     public function createCalendar($principalUri, $calendarUri, array $properties) {
152
153         $fieldNames = array(
154             'principaluri',
155             'uri',
156             'ctag',
157         );
158         $values = array(
159             ':principaluri' => $principalUri,
160             ':uri'          => $calendarUri,
161             ':ctag'         => 1,
162         );
163
164         // Default value
165         $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
166         $fieldNames[] = 'components';
167         if (!isset($properties[$sccs])) {
168             $values[':components'] = 'VEVENT,VTODO';
169         } else {
170             if (!($properties[$sccs] instanceof Sabre_CalDAV_Property_SupportedCalendarComponentSet)) {
171                 throw new Sabre_DAV_Exception('The ' . $sccs . ' property must be of type: Sabre_CalDAV_Property_SupportedCalendarComponentSet');
172             }
173             $values[':components'] = implode(',',$properties[$sccs]->getValue());
174         }
175
176         foreach($this->propertyMap as $xmlName=>$dbName) {
177             if (isset($properties[$xmlName])) {
178
179                 $values[':' . $dbName] = $properties[$xmlName];
180                 $fieldNames[] = $dbName;
181             }
182         }
183
184         $stmt = $this->pdo->prepare("INSERT INTO ".$this->calendarTableName." (".implode(', ', $fieldNames).") VALUES (".implode(', ',array_keys($values)).")");
185         $stmt->execute($values);
186
187         return $this->pdo->lastInsertId();
188
189     }
190
191     /**
192      * Updates properties for a calendar.
193      *
194      * The mutations array uses the propertyName in clark-notation as key,
195      * and the array value for the property value. In the case a property
196      * should be deleted, the property value will be null.
197      *
198      * This method must be atomic. If one property cannot be changed, the
199      * entire operation must fail.
200      *
201      * If the operation was successful, true can be returned.
202      * If the operation failed, false can be returned.
203      *
204      * Deletion of a non-existent property is always successful.
205      *
206      * Lastly, it is optional to return detailed information about any
207      * failures. In this case an array should be returned with the following
208      * structure:
209      *
210      * array(
211      *   403 => array(
212      *      '{DAV:}displayname' => null,
213      *   ),
214      *   424 => array(
215      *      '{DAV:}owner' => null,
216      *   )
217      * )
218      *
219      * In this example it was forbidden to update {DAV:}displayname.
220      * (403 Forbidden), which in turn also caused {DAV:}owner to fail
221      * (424 Failed Dependency) because the request needs to be atomic.
222      *
223      * @param string $calendarId
224      * @param array $mutations
225      * @return bool|array
226      */
227     public function updateCalendar($calendarId, array $mutations) {
228
229         $newValues = array();
230         $result = array(
231             200 => array(), // Ok
232             403 => array(), // Forbidden
233             424 => array(), // Failed Dependency
234         );
235
236         $hasError = false;
237
238         foreach($mutations as $propertyName=>$propertyValue) {
239
240             // We don't know about this property.
241             if (!isset($this->propertyMap[$propertyName])) {
242                 $hasError = true;
243                 $result[403][$propertyName] = null;
244                 unset($mutations[$propertyName]);
245                 continue;
246             }
247
248             $fieldName = $this->propertyMap[$propertyName];
249             $newValues[$fieldName] = $propertyValue;
250
251         }
252
253         // If there were any errors we need to fail the request
254         if ($hasError) {
255             // Properties has the remaining properties
256             foreach($mutations as $propertyName=>$propertyValue) {
257                 $result[424][$propertyName] = null;
258             }
259
260             // Removing unused statuscodes for cleanliness
261             foreach($result as $status=>$properties) {
262                 if (is_array($properties) && count($properties)===0) unset($result[$status]);
263             }
264
265             return $result;
266
267         }
268
269         // Success
270
271         // Now we're generating the sql query.
272         $valuesSql = array();
273         foreach($newValues as $fieldName=>$value) {
274             $valuesSql[] = $fieldName . ' = ?';
275         }
276         $valuesSql[] = 'ctag = ctag + 1';
277
278         $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ',$valuesSql) . " WHERE id = ?");
279         $newValues['id'] = $calendarId;
280         $stmt->execute(array_values($newValues));
281
282         return true;
283
284     }
285
286     /**
287      * Delete a calendar and all it's objects
288      *
289      * @param string $calendarId
290      * @return void
291      */
292     public function deleteCalendar($calendarId) {
293
294         $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
295         $stmt->execute(array($calendarId));
296
297         $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?');
298         $stmt->execute(array($calendarId));
299
300     }
301
302     /**
303      * Returns all calendar objects within a calendar.
304      *
305      * Every item contains an array with the following keys:
306      *   * id - unique identifier which will be used for subsequent updates
307      *   * calendardata - The iCalendar-compatible calendar data
308      *   * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
309      *   * lastmodified - a timestamp of the last modification time
310      *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
311      *   '  "abcdef"')
312      *   * calendarid - The calendarid as it was passed to this function.
313      *   * size - The size of the calendar objects, in bytes.
314      *
315      * Note that the etag is optional, but it's highly encouraged to return for
316      * speed reasons.
317      *
318      * The calendardata is also optional. If it's not returned
319      * 'getCalendarObject' will be called later, which *is* expected to return
320      * calendardata.
321      *
322      * If neither etag or size are specified, the calendardata will be
323      * used/fetched to determine these numbers. If both are specified the
324      * amount of times this is needed is reduced by a great degree.
325      *
326      * @param string $calendarId
327      * @return array
328      */
329     public function getCalendarObjects($calendarId) {
330
331         $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
332         $stmt->execute(array($calendarId));
333
334         $result = array();
335         foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
336             $result[] = array(
337                 'id'           => $row['id'],
338                 'uri'          => $row['uri'],
339                 'lastmodified' => $row['lastmodified'],
340                 'etag'         => '"' . $row['etag'] . '"',
341                 'calendarid'   => $row['calendarid'],
342                 'size'         => (int)$row['size'],
343             );
344         }
345
346         return $result;
347
348     }
349
350     /**
351      * Returns information from a single calendar object, based on it's object
352      * uri.
353      *
354      * The returned array must have the same keys as getCalendarObjects. The
355      * 'calendardata' object is required here though, while it's not required
356      * for getCalendarObjects.
357      *
358      * @param string $calendarId
359      * @param string $objectUri
360      * @return array
361      */
362     public function getCalendarObject($calendarId,$objectUri) {
363
364         $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
365         $stmt->execute(array($calendarId, $objectUri));
366         $row = $stmt->fetch(\PDO::FETCH_ASSOC);
367
368         if(!$row) return null;
369
370         return array(
371             'id'           => $row['id'],
372             'uri'          => $row['uri'],
373             'lastmodified' => $row['lastmodified'],
374             'etag'         => '"' . $row['etag'] . '"',
375             'calendarid'   => $row['calendarid'],
376             'size'         => (int)$row['size'],
377             'calendardata' => $row['calendardata'],
378          );
379
380     }
381
382
383     /**
384      * Creates a new calendar object.
385      *
386      * It is possible return an etag from this function, which will be used in
387      * the response to this PUT request. Note that the ETag must be surrounded
388      * by double-quotes.
389      *
390      * However, you should only really return this ETag if you don't mangle the
391      * calendar-data. If the result of a subsequent GET to this object is not
392      * the exact same as this request body, you should omit the ETag.
393      *
394      * @param mixed $calendarId
395      * @param string $objectUri
396      * @param string $calendarData
397      * @return string|null
398      */
399     public function createCalendarObject($calendarId,$objectUri,$calendarData) {
400
401         $extraData = $this->getDenormalizedData($calendarData);
402
403         $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence) VALUES (?,?,?,?,?,?,?,?,?)');
404         $stmt->execute(array(
405             $calendarId,
406             $objectUri,
407             $calendarData,
408             time(),
409             $extraData['etag'],
410             $extraData['size'],
411             $extraData['componentType'],
412             $extraData['firstOccurence'],
413             $extraData['lastOccurence'],
414         ));
415         $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?');
416         $stmt->execute(array($calendarId));
417
418         return '"' . $extraData['etag'] . '"';
419
420     }
421
422     /**
423      * Updates an existing calendarobject, based on it's uri.
424      *
425      * It is possible return an etag from this function, which will be used in
426      * the response to this PUT request. Note that the ETag must be surrounded
427      * by double-quotes.
428      *
429      * However, you should only really return this ETag if you don't mangle the
430      * calendar-data. If the result of a subsequent GET to this object is not
431      * the exact same as this request body, you should omit the ETag.
432      *
433      * @param mixed $calendarId
434      * @param string $objectUri
435      * @param string $calendarData
436      * @return string|null
437      */
438     public function updateCalendarObject($calendarId,$objectUri,$calendarData) {
439
440         $extraData = $this->getDenormalizedData($calendarData);
441
442         $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ? WHERE calendarid = ? AND uri = ?');
443         $stmt->execute(array($calendarData,time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'] ,$calendarId,$objectUri));
444         $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?');
445         $stmt->execute(array($calendarId));
446
447         return '"' . $extraData['etag'] . '"';
448
449     }
450
451     /**
452      * Parses some information from calendar objects, used for optimized
453      * calendar-queries.
454      *
455      * Returns an array with the following keys:
456      *   * etag
457      *   * size
458      *   * componentType
459      *   * firstOccurence
460      *   * lastOccurence
461      *
462      * @param string $calendarData
463      * @return array
464      */
465     protected function getDenormalizedData($calendarData) {
466
467         $vObject = VObject\Reader::read($calendarData);
468         $componentType = null;
469         $component = null;
470         $firstOccurence = null;
471         $lastOccurence = null;
472         foreach($vObject->getComponents() as $component) {
473             if ($component->name!=='VTIMEZONE') {
474                 $componentType = $component->name;
475                 break;
476             }
477         }
478         if (!$componentType) {
479             throw new Sabre_DAV_Exception_BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
480         }
481         if ($componentType === 'VEVENT') {
482             $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
483             // Finding the last occurence is a bit harder
484             if (!isset($component->RRULE)) {
485                 if (isset($component->DTEND)) {
486                     $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
487                 } elseif (isset($component->DURATION)) {
488                     $endDate = clone $component->DTSTART->getDateTime();
489                     $endDate->add(VObject\DateTimeParser::parse($component->DURATION->value));
490                     $lastOccurence = $endDate->getTimeStamp();
491                 } elseif ($component->DTSTART->getDateType()===VObject\Property\DateTime::DATE) {
492                     $endDate = clone $component->DTSTART->getDateTime();
493                     $endDate->modify('+1 day');
494                     $lastOccurence = $endDate->getTimeStamp();
495                 } else {
496                     $lastOccurence = $firstOccurence;
497                 }
498             } else {
499                 $it = new VObject\RecurrenceIterator($vObject, (string)$component->UID);
500                 $maxDate = new DateTime(self::MAX_DATE);
501                 if ($it->isInfinite()) {
502                     $lastOccurence = $maxDate->getTimeStamp();
503                 } else {
504                     $end = $it->getDtEnd();
505                     while($it->valid() && $end < $maxDate) {
506                         $end = $it->getDtEnd();
507                         $it->next();
508
509                     }
510                     $lastOccurence = $end->getTimeStamp();
511                 }
512
513             }
514         }
515
516         return array(
517             'etag' => md5($calendarData),
518             'size' => strlen($calendarData),
519             'componentType' => $componentType,
520             'firstOccurence' => $firstOccurence,
521             'lastOccurence'  => $lastOccurence,
522         );
523
524     }
525
526     /**
527      * Deletes an existing calendar object.
528      *
529      * @param string $calendarId
530      * @param string $objectUri
531      * @return void
532      */
533     public function deleteCalendarObject($calendarId,$objectUri) {
534
535         $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
536         $stmt->execute(array($calendarId,$objectUri));
537         $stmt = $this->pdo->prepare('UPDATE '. $this->calendarTableName .' SET ctag = ctag + 1 WHERE id = ?');
538         $stmt->execute(array($calendarId));
539
540     }
541
542     /**
543      * Performs a calendar-query on the contents of this calendar.
544      *
545      * The calendar-query is defined in RFC4791 : CalDAV. Using the
546      * calendar-query it is possible for a client to request a specific set of
547      * object, based on contents of iCalendar properties, date-ranges and
548      * iCalendar component types (VTODO, VEVENT).
549      *
550      * This method should just return a list of (relative) urls that match this
551      * query.
552      *
553      * The list of filters are specified as an array. The exact array is
554      * documented by Sabre_CalDAV_CalendarQueryParser.
555      *
556      * Note that it is extremely likely that getCalendarObject for every path
557      * returned from this method will be called almost immediately after. You
558      * may want to anticipate this to speed up these requests.
559      *
560      * This method provides a default implementation, which parses *all* the
561      * iCalendar objects in the specified calendar.
562      *
563      * This default may well be good enough for personal use, and calendars
564      * that aren't very large. But if you anticipate high usage, big calendars
565      * or high loads, you are strongly adviced to optimize certain paths.
566      *
567      * The best way to do so is override this method and to optimize
568      * specifically for 'common filters'.
569      *
570      * Requests that are extremely common are:
571      *   * requests for just VEVENTS
572      *   * requests for just VTODO
573      *   * requests with a time-range-filter on a VEVENT.
574      *
575      * ..and combinations of these requests. It may not be worth it to try to
576      * handle every possible situation and just rely on the (relatively
577      * easy to use) CalendarQueryValidator to handle the rest.
578      *
579      * Note that especially time-range-filters may be difficult to parse. A
580      * time-range filter specified on a VEVENT must for instance also handle
581      * recurrence rules correctly.
582      * A good example of how to interprete all these filters can also simply
583      * be found in Sabre_CalDAV_CalendarQueryFilter. This class is as correct
584      * as possible, so it gives you a good idea on what type of stuff you need
585      * to think of.
586      *
587      * This specific implementation (for the PDO) backend optimizes filters on
588      * specific components, and VEVENT time-ranges.
589      *
590      * @param string $calendarId
591      * @param array $filters
592      * @return array
593      */
594     public function calendarQuery($calendarId, array $filters) {
595
596         $result = array();
597         $validator = new Sabre_CalDAV_CalendarQueryValidator();
598
599         $componentType = null;
600         $requirePostFilter = true;
601         $timeRange = null;
602
603         // if no filters were specified, we don't need to filter after a query
604         if (!$filters['prop-filters'] && !$filters['comp-filters']) {
605             $requirePostFilter = false;
606         }
607
608         // Figuring out if there's a component filter
609         if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
610             $componentType = $filters['comp-filters'][0]['name'];
611
612             // Checking if we need post-filters
613             if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
614                 $requirePostFilter = false;
615             }
616             // There was a time-range filter
617             if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
618                 $timeRange = $filters['comp-filters'][0]['time-range'];
619             }
620
621         }
622
623         if ($requirePostFilter) {
624             $query = "SELECT uri, calendardata FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid";
625         } else {
626             $query = "SELECT uri FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid";
627         }
628
629         $values = array(
630             'calendarid' => $calendarId,
631         );
632
633         if ($componentType) {
634             $query.=" AND componenttype = :componenttype";
635             $values['componenttype'] = $componentType;
636         }
637
638         if ($timeRange && $timeRange['start']) {
639             $query.=" AND lastoccurence > :startdate";
640             $values['startdate'] = $timeRange['start']->getTimeStamp();
641         }
642         if ($timeRange && $timeRange['end']) {
643             $query.=" AND firstoccurence < :enddate";
644             $values['enddate'] = $timeRange['end']->getTimeStamp();
645         }
646
647         $stmt = $this->pdo->prepare($query);
648         $stmt->execute($values);
649
650         $result = array();
651         while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
652             if ($requirePostFilter) {
653                 if (!$this->validateFilterForObject($row, $filters)) {
654                     continue;
655                 }
656             }
657             $result[] = $row['uri'];
658
659         }
660
661         return $result;
662
663     }
664 }