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