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