PageRenderTime 53ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Sabre/CalDAV/Backend/PDO.php

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