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