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