diff --git a/calendarBackendDokuwiki.php b/calendarBackendDokuwiki.php --- a/calendarBackendDokuwiki.php +++ b/calendarBackendDokuwiki.php @@ -1,1226 +1,708 @@ 'displayname', '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', - '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', - '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', - ]; - - /** - * List of subscription properties, and how they map to database fieldnames. - * - * @var array - */ - public $subscriptionPropertyMap = [ - '{DAV:}displayname' => 'displayname', - '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', - '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', - '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', - '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', - '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', - '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', - ]; + //'{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + //'{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ); /** * Creates the backend * * @param \PDO $pdo */ - function __construct(\PDO $pdo) { + function __construct(&$hlp) + { - $this->pdo = $pdo; - $this->hlp =& plugin_load('helper', 'davcal'); + $this->hlp = $hlp; } /** * Returns a list of calendars for a principal. * * Every project is an array with the following keys: * * id, a unique id that will be used by other functions to modify the * calendar. This can be the same as the uri or a database key. * * uri. This is just the 'base uri' or 'filename' of the calendar. * * principaluri. The owner of the calendar. Almost always the same as * principalUri passed to this method. * * Furthermore it can contain webdav properties in clark notation. A very * common one is '{DAV:}displayname'. * * Many clients also require: * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set * For this property, you can just return an instance of * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. * * If you return {http://sabredav.org/ns}read-only and set the value to 1, * ACL will automatically be put in read-only mode. * * @param string $principalUri * @return array */ - function getCalendarsForUser($principalUri) { + function getCalendarsForUser($principalUri) + { $fields = array_values($this->propertyMap); $fields[] = 'id'; $fields[] = 'uri'; $fields[] = 'synctoken'; $fields[] = 'components'; $fields[] = 'principaluri'; $fields[] = 'transparent'; $idInfo = $this->hlp->getCalendarIdsForUser($principalUri); - $idFilter = array_keys($idInfo); - // Making fields a comma-delimited list - $fields = implode(', ', $fields); - $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarTableName . " ORDER BY calendarorder ASC"); - $stmt->execute(); - - $calendars = []; - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - // Filter Calendars by the array returned by the DokuWiki Auth system - if(array_search($row['id'], $idFilter) === false) - continue; - $components = []; - if ($row['components']) { + $calendars = array(); + foreach($idInfo as $id => $data) + { + $row = $this->hlp->getCalendarSettings($id); + $components = array(); + if ($row['components']) + { $components = explode(',', $row['components']); } - $calendar = [ + $calendar = array( 'id' => $row['id'], 'uri' => $row['uri'], - 'principaluri' => $principalUri,//$row['principaluri'], // Overwrite principaluri from database, we actually don't need it. + 'principaluri' => $principalUri,//Overwrite principaluri from database, we actually don't need it. '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ? $row['synctoken'] : '0'), '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components), - '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'), - ]; + //'{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'), + ); if($idInfo[$row['id']]['readonly'] === true) $calendar['{http://sabredav.org/ns}read-only'] = '1'; - foreach ($this->propertyMap as $xmlName => $dbName) { + foreach ($this->propertyMap as $xmlName => $dbName) + { $calendar[$xmlName] = $row[$dbName]; } $calendars[] = $calendar; } return $calendars; } /** * Creates a new calendar for a principal. * * If the creation was a success, an id must be returned that can be used * to reference this calendar in other methods, such as updateCalendar. * * @param string $principalUri * @param string $calendarUri * @param array $properties * @return string */ - function createCalendar($principalUri, $calendarUri, array $properties) { + function createCalendar($principalUri, $calendarUri, array $properties) + { return false; - /* - $fieldNames = [ - 'principaluri', - 'uri', - 'synctoken', - 'transparent', - ]; - $values = [ - ':principaluri' => $principalUri, - ':uri' => $calendarUri, - ':synctoken' => 1, - ':transparent' => 0, - ]; - - // Default value - $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; - $fieldNames[] = 'components'; - if (!isset($properties[$sccs])) { - $values[':components'] = 'VEVENT,VTODO'; - } else { - if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) { - throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet'); - } - $values[':components'] = implode(',', $properties[$sccs]->getValue()); - } - $transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'; - if (isset($properties[$transp])) { - $values[':transparent'] = $properties[$transp]->getValue() === 'transparent'; - } - - foreach ($this->propertyMap as $xmlName => $dbName) { - if (isset($properties[$xmlName])) { - - $values[':' . $dbName] = $properties[$xmlName]; - $fieldNames[] = $dbName; - } - } - - $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")"); - $stmt->execute($values); - - return $this->pdo->lastInsertId(); - */ } /** * Updates properties for a calendar. * * The list of mutations is stored in a Sabre\DAV\PropPatch object. * To do the actual updates, you must tell this object which properties * you're going to process with the handle() method. * * Calling the handle method is like telling the PropPatch object "I * promise I can handle updating this property". * * Read the PropPatch documenation for more info and examples. * * @param string $calendarId * @param \Sabre\DAV\PropPatch $propPatch * @return void */ - function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { + function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) + { $supportedProperties = array_keys($this->propertyMap); - $supportedProperties[] = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'; + + $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) + { + foreach ($mutations as $propertyName => $propertyValue) + { - $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) { - $newValues = []; - foreach ($mutations as $propertyName => $propertyValue) { - - switch ($propertyName) { - case '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' : - $fieldName = 'transparent'; - $newValues[$fieldName] = $propertyValue->getValue() === 'transparent'; + switch ($propertyName) + { + case '{DAV:}displayname' : + $this->hlp->updateCalendarName($calendarId, $propertyValue); + break; + case '{urn:ietf:params:xml:ns:caldav}calendar-description': + $this->hlp->updateCalendarDescription($calendarId, $propertyValue); + break; + case '{urn:ietf:params:xml:ns:caldav}calendar-timezone': + $this->hlp->updateCalendarTimezone($calendarId, $propertyValue); break; default : - $fieldName = $this->propertyMap[$propertyName]; - $newValues[$fieldName] = $propertyValue; break; } } - $valuesSql = []; - foreach ($newValues as $fieldName => $value) { - $valuesSql[] = $fieldName . ' = ?'; - } - - $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ', $valuesSql) . " WHERE id = ?"); - $newValues['id'] = $calendarId; - $stmt->execute(array_values($newValues)); - - $this->addChange($calendarId, "", 2); - return true; }); } /** * Delete a calendar and all it's objects * * @param string $calendarId * @return void */ - function deleteCalendar($calendarId) { - - /* - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?'); - $stmt->execute([$calendarId]); - - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarTableName . ' WHERE id = ?'); - $stmt->execute([$calendarId]); - - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarChangesTableName . ' WHERE calendarid = ?'); - $stmt->execute([$calendarId]); - */ - + function deleteCalendar($calendarId) + { + return; } /** * Returns all calendar objects within a calendar. * * Every item contains an array with the following keys: * * calendardata - The iCalendar-compatible calendar data * * uri - a unique key which will be used to construct the uri. This can * be any arbitrary string, but making sure it ends with '.ics' is a * good idea. This is only the basename, or filename, not the full * path. * * lastmodified - a timestamp of the last modification time * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: * ' "abcdef"') * * size - The size of the calendar objects, in bytes. * * component - optional, a string containing the type of object, such * as 'vevent' or 'vtodo'. If specified, this will be used to populate * the Content-Type header. * * Note that the etag is optional, but it's highly encouraged to return for * speed reasons. * * The calendardata is also optional. If it's not returned * 'getCalendarObject' will be called later, which *is* expected to return * calendardata. * * If neither etag or size are specified, the calendardata will be * used/fetched to determine these numbers. If both are specified the * amount of times this is needed is reduced by a great degree. * * @param string $calendarId * @return array */ - function getCalendarObjects($calendarId) { + function getCalendarObjects($calendarId) + { - $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?'); - $stmt->execute([$calendarId]); - - $result = []; - foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { - $result[] = [ + $arr = $this->hlp->getCalendarObjects($calendarId); + $result = array(); + foreach ($arr as $row) + { + $result[] = array( 'id' => $row['id'], 'uri' => $row['uri'], 'lastmodified' => $row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', 'calendarid' => $row['calendarid'], 'size' => (int)$row['size'], 'component' => strtolower($row['componenttype']), - ]; + ); } return $result; } /** * Returns information from a single calendar object, based on it's object * uri. * * The object uri is only the basename, or filename and not a full path. * * The returned array must have the same keys as getCalendarObjects. The * 'calendardata' object is required here though, while it's not required * for getCalendarObjects. * * This method must return null if the object did not exist. * * @param string $calendarId * @param string $objectUri * @return array|null */ - function getCalendarObject($calendarId, $objectUri) { + function getCalendarObject($calendarId, $objectUri) + { + + $row = $this->hlp->getCalendarObjectByUri($calendarId, $objectUri); - $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?'); - $stmt->execute([$calendarId, $objectUri]); - $row = $stmt->fetch(\PDO::FETCH_ASSOC); + if (!$row) + return null; - if (!$row) return null; - - return [ + return array( 'id' => $row['id'], 'uri' => $row['uri'], 'lastmodified' => $row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', 'calendarid' => $row['calendarid'], 'size' => (int)$row['size'], 'calendardata' => $row['calendardata'], 'component' => strtolower($row['componenttype']), - ]; + ); } /** * Returns a list of calendar objects. * * This method should work identical to getCalendarObject, but instead * return all the calendar objects in the list as an array. * * If the backend supports this, it may allow for some speed-ups. * * @param mixed $calendarId * @param array $uris * @return array */ - function getMultipleCalendarObjects($calendarId, array $uris) { + function getMultipleCalendarObjects($calendarId, array $uris) + { - $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri IN ('; - // Inserting a whole bunch of question marks - $query .= implode(',', array_fill(0, count($uris), '?')); - $query .= ')'; + $arr = $this->hlp->getMultipleCalendarObjectsByUri($calendarId, $uris); - $stmt = $this->pdo->prepare($query); - $stmt->execute(array_merge([$calendarId], $uris)); + $result = array(); + foreach($arr as $row) + { - $result = []; - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $result[] = [ + $result[] = array( 'id' => $row['id'], 'uri' => $row['uri'], 'lastmodified' => $row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', 'calendarid' => $row['calendarid'], 'size' => (int)$row['size'], 'calendardata' => $row['calendardata'], 'component' => strtolower($row['componenttype']), - ]; + ); } return $result; } /** * Creates a new calendar object. * * The object uri is only the basename, or filename and not a full path. * * It is possible return an etag from this function, which will be used in * the response to this PUT request. Note that the ETag must be surrounded * by double-quotes. * * However, you should only really return this ETag if you don't mangle the * calendar-data. If the result of a subsequent GET to this object is not * the exact same as this request body, you should omit the ETag. * * @param mixed $calendarId * @param string $objectUri * @param string $calendarData * @return string|null */ - function createCalendarObject($calendarId, $objectUri, $calendarData) { - - $extraData = $this->getDenormalizedData($calendarData); + function createCalendarObject($calendarId, $objectUri, $calendarData) + { - $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarObjectTableName . ' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)'); - $stmt->execute([ - $calendarId, - $objectUri, - $calendarData, - time(), - $extraData['etag'], - $extraData['size'], - $extraData['componentType'], - $extraData['firstOccurence'], - $extraData['lastOccurence'], - $extraData['uid'], - ]); - $this->addChange($calendarId, $objectUri, 1); - - return '"' . $extraData['etag'] . '"'; - + $etag = $this->hlp->addCalendarEntryToCalendarByICS($calendarId, $objectUri, $calendarData); + + return '"' . $etag . '"'; } /** * Updates an existing calendarobject, based on it's uri. * * The object uri is only the basename, or filename and not a full path. * * It is possible return an etag from this function, which will be used in * the response to this PUT request. Note that the ETag must be surrounded * by double-quotes. * * However, you should only really return this ETag if you don't mangle the * calendar-data. If the result of a subsequent GET to this object is not * the exact same as this request body, you should omit the ETag. * * @param mixed $calendarId * @param string $objectUri * @param string $calendarData * @return string|null */ - function updateCalendarObject($calendarId, $objectUri, $calendarData) { - - $extraData = $this->getDenormalizedData($calendarData); + function updateCalendarObject($calendarId, $objectUri, $calendarData) + { - $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarObjectTableName . ' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?'); - $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]); - - $this->addChange($calendarId, $objectUri, 2); - - return '"' . $extraData['etag'] . '"'; + $etag = $this->hlp->editCalendarEntryToCalendarByICS($calendarId, $objectUri, $calendarData); + return '"' . $etag. '"'; } - /** - * Parses some information from calendar objects, used for optimized - * calendar-queries. - * - * Returns an array with the following keys: - * * etag - An md5 checksum of the object without the quotes. - * * size - Size of the object in bytes - * * componentType - VEVENT, VTODO or VJOURNAL - * * firstOccurence - * * lastOccurence - * * uid - value of the UID property - * - * @param string $calendarData - * @return array - */ - protected function getDenormalizedData($calendarData) { - $vObject = VObject\Reader::read($calendarData); - $componentType = null; - $component = null; - $firstOccurence = null; - $lastOccurence = null; - $uid = null; - foreach ($vObject->getComponents() as $component) { - if ($component->name !== 'VTIMEZONE') { - $componentType = $component->name; - $uid = (string)$component->UID; - break; - } - } - if (!$componentType) { - throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); - } - if ($componentType === 'VEVENT') { - $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); - // Finding the last occurence is a bit harder - if (!isset($component->RRULE)) { - if (isset($component->DTEND)) { - $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); - } elseif (isset($component->DURATION)) { - $endDate = clone $component->DTSTART->getDateTime(); - $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue())); - $lastOccurence = $endDate->getTimeStamp(); - } elseif (!$component->DTSTART->hasTime()) { - $endDate = clone $component->DTSTART->getDateTime(); - $endDate->modify('+1 day'); - $lastOccurence = $endDate->getTimeStamp(); - } else { - $lastOccurence = $firstOccurence; - } - } else { - $it = new VObject\Recur\EventIterator($vObject, (string)$component->UID); - $maxDate = new \DateTime(self::MAX_DATE); - if ($it->isInfinite()) { - $lastOccurence = $maxDate->getTimeStamp(); - } else { - $end = $it->getDtEnd(); - while ($it->valid() && $end < $maxDate) { - $end = $it->getDtEnd(); - $it->next(); - - } - $lastOccurence = $end->getTimeStamp(); - } - - } - } - - return [ - 'etag' => md5($calendarData), - 'size' => strlen($calendarData), - 'componentType' => $componentType, - 'firstOccurence' => $firstOccurence, - 'lastOccurence' => $lastOccurence, - 'uid' => $uid, - ]; - - } /** * Deletes an existing calendar object. * * The object uri is only the basename, or filename and not a full path. * * @param string $calendarId * @param string $objectUri * @return void */ - function deleteCalendarObject($calendarId, $objectUri) { - - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?'); - $stmt->execute([$calendarId, $objectUri]); - - $this->addChange($calendarId, $objectUri, 3); + function deleteCalendarObject($calendarId, $objectUri) + { + $this->hlp->deleteCalendarEntryForCalendarByUri($calendarId, $objectUri); } /** * Performs a calendar-query on the contents of this calendar. * * The calendar-query is defined in RFC4791 : CalDAV. Using the * calendar-query it is possible for a client to request a specific set of * object, based on contents of iCalendar properties, date-ranges and * iCalendar component types (VTODO, VEVENT). * * This method should just return a list of (relative) urls that match this * query. * * The list of filters are specified as an array. The exact array is * documented by \Sabre\CalDAV\CalendarQueryParser. * * Note that it is extremely likely that getCalendarObject for every path * returned from this method will be called almost immediately after. You * may want to anticipate this to speed up these requests. * * This method provides a default implementation, which parses *all* the * iCalendar objects in the specified calendar. * * This default may well be good enough for personal use, and calendars * that aren't very large. But if you anticipate high usage, big calendars * or high loads, you are strongly adviced to optimize certain paths. * * The best way to do so is override this method and to optimize * specifically for 'common filters'. * * Requests that are extremely common are: * * requests for just VEVENTS * * requests for just VTODO * * requests with a time-range-filter on a VEVENT. * * ..and combinations of these requests. It may not be worth it to try to * handle every possible situation and just rely on the (relatively * easy to use) CalendarQueryValidator to handle the rest. * * Note that especially time-range-filters may be difficult to parse. A * time-range filter specified on a VEVENT must for instance also handle * recurrence rules correctly. * A good example of how to interprete all these filters can also simply * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct * as possible, so it gives you a good idea on what type of stuff you need * to think of. * * This specific implementation (for the PDO) backend optimizes filters on * specific components, and VEVENT time-ranges. * * @param string $calendarId * @param array $filters * @return array */ - function calendarQuery($calendarId, array $filters) { - - $componentType = null; - $requirePostFilter = true; - $timeRange = null; - - // if no filters were specified, we don't need to filter after a query - if (!$filters['prop-filters'] && !$filters['comp-filters']) { - $requirePostFilter = false; - } - - // Figuring out if there's a component filter - if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { - $componentType = $filters['comp-filters'][0]['name']; - - // Checking if we need post-filters - if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) { - $requirePostFilter = false; - } - // There was a time-range filter - if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) { - $timeRange = $filters['comp-filters'][0]['time-range']; - - // If start time OR the end time is not specified, we can do a - // 100% accurate mysql query. - if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) { - $requirePostFilter = false; - } - } - - } - - if ($requirePostFilter) { - $query = "SELECT uri, calendardata FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid"; - } else { - $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid"; - } - - $values = [ - 'calendarid' => $calendarId, - ]; - - if ($componentType) { - $query .= " AND componenttype = :componenttype"; - $values['componenttype'] = $componentType; - } - - if ($timeRange && $timeRange['start']) { - $query .= " AND lastoccurence > :startdate"; - $values['startdate'] = $timeRange['start']->getTimeStamp(); - } - if ($timeRange && $timeRange['end']) { - $query .= " AND firstoccurence < :enddate"; - $values['enddate'] = $timeRange['end']->getTimeStamp(); - } - - $stmt = $this->pdo->prepare($query); - $stmt->execute($values); - - $result = []; - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - if ($requirePostFilter) { - if (!$this->validateFilterForObject($row, $filters)) { - continue; - } - } - $result[] = $row['uri']; - - } - + function calendarQuery($calendarId, array $filters) + { + $result = $this->hlp->calendarQuery($calendarId, $filters); return $result; - } /** * Searches through all of a users calendars and calendar objects to find * an object with a specific UID. * * This method should return the path to this object, relative to the * calendar home, so this path usually only contains two parts: * * calendarpath/objectpath.ics * * If the uid is not found, return null. * * This method should only consider * objects that the principal owns, so * any calendars owned by other principals that also appear in this * collection should be ignored. * * @param string $principalUri * @param string $uid * @return string|null */ - function getCalendarObjectByUID($principalUri, $uid) { - - $query = <<calendarObjectTableName AS calendarobjects -LEFT JOIN - $this->calendarTableName AS calendars - ON calendarobjects.calendarid = calendars.id -WHERE - calendars.principaluri = ? - AND - calendarobjects.uid = ? -SQL; - - $stmt = $this->pdo->prepare($query); - $stmt->execute([$principalUri, $uid]); - - if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - return $row['calendaruri'] . '/' . $row['objecturi']; + function getCalendarObjectByUID($principalUri, $uid) + { + $calids = array_keys($this->hlp->getCalendarIsForUser($principalUri)); + $event = $this->hlp->getEventWithUid($uid); + + if(in_array($event['calendarid'], $calids)) + { + $settings = $this->hlp->getCalendarSettings($event['calendarid']); + return $settings['uri'] . '/' . $event['uri']; } - + return null; } /** * The getChanges method returns all the changes that have happened, since * the specified syncToken in the specified calendar. * * This function should return an array, such as the following: * * [ * 'syncToken' => 'The current synctoken', * 'added' => [ * 'new.txt', * ], * 'modified' => [ * 'modified.txt', * ], * 'deleted' => [ * 'foo.php.bak', * 'old.txt' * ] * ]; * * The returned syncToken property should reflect the *current* syncToken * of the calendar, as reported in the {http://sabredav.org/ns}sync-token * property this is needed here too, to ensure the operation is atomic. * * If the $syncToken argument is specified as null, this is an initial * sync, and all members should be reported. * * The modified property is an array of nodenames that have changed since * the last token. * * The deleted property is an array with nodenames, that have been deleted * from collection. * * The $syncLevel argument is basically the 'depth' of the report. If it's * 1, you only have to report changes that happened only directly in * immediate descendants. If it's 2, it should also include changes from * the nodes below the child collections. (grandchildren) * * The $limit argument allows a client to specify how many results should * be returned at most. If the limit is not specified, it should be treated * as infinite. * * If the limit (infinite or not) is higher than you're willing to return, * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. * * If the syncToken is expired (due to data cleanup) or unknown, you must * return null. * * The limit is 'suggestive'. You are free to ignore it. * * @param string $calendarId * @param string $syncToken * @param int $syncLevel * @param int $limit * @return array */ - function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { - - // Current synctoken - $stmt = $this->pdo->prepare('SELECT synctoken FROM ' . $this->calendarTableName . ' WHERE id = ?'); - $stmt->execute([ $calendarId ]); - $currentToken = $stmt->fetchColumn(0); - - if (is_null($currentToken)) return null; - - $result = [ - 'syncToken' => $currentToken, - 'added' => [], - 'modified' => [], - 'deleted' => [], - ]; - - if ($syncToken) { - - $query = "SELECT uri, operation FROM " . $this->calendarChangesTableName . " WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; - if ($limit > 0) $query .= " LIMIT " . (int)$limit; - - // Fetching all changes - $stmt = $this->pdo->prepare($query); - $stmt->execute([$syncToken, $currentToken, $calendarId]); - - $changes = []; - - // This loop ensures that any duplicates are overwritten, only the - // last change on a node is relevant. - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $changes[$row['uri']] = $row['operation']; - - } - - foreach ($changes as $uri => $operation) { - - switch ($operation) { - case 1 : - $result['added'][] = $uri; - break; - case 2 : - $result['modified'][] = $uri; - break; - case 3 : - $result['deleted'][] = $uri; - break; - } - - } - } else { - // No synctoken supplied, this is the initial sync. - $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = ?"; - $stmt = $this->pdo->prepare($query); - $stmt->execute([$calendarId]); - - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); - } + function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) + { + $result = $this->hlp->getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit); return $result; - - } - - /** - * Adds a change record to the calendarchanges table. - * - * @param mixed $calendarId - * @param string $objectUri - * @param int $operation 1 = add, 2 = modify, 3 = delete. - * @return void - */ - protected function addChange($calendarId, $objectUri, $operation) { - - $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarChangesTableName . ' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM ' . $this->calendarTableName . ' WHERE id = ?'); - $stmt->execute([ - $objectUri, - $calendarId, - $operation, - $calendarId - ]); - $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarTableName . ' SET synctoken = synctoken + 1 WHERE id = ?'); - $stmt->execute([ - $calendarId - ]); - } /** * Returns a list of subscriptions for a principal. * * Every subscription is an array with the following keys: * * id, a unique id that will be used by other functions to modify the * subscription. This can be the same as the uri or a database key. * * uri. This is just the 'base uri' or 'filename' of the subscription. * * principaluri. The owner of the subscription. Almost always the same as * principalUri passed to this method. * * source. Url to the actual feed * * Furthermore, all the subscription info must be returned too: * * 1. {DAV:}displayname * 2. {http://apple.com/ns/ical/}refreshrate * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos * should not be stripped). * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms * should not be stripped). * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if * attachments should not be stripped). * 7. {http://apple.com/ns/ical/}calendar-color * 8. {http://apple.com/ns/ical/}calendar-order * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set * (should just be an instance of * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of * default components). * * @param string $principalUri * @return array */ - function getSubscriptionsForUser($principalUri) { - - $fields = array_values($this->subscriptionPropertyMap); - $fields[] = 'id'; - $fields[] = 'uri'; - $fields[] = 'source'; - $fields[] = 'principaluri'; - $fields[] = 'lastmodified'; - - // Making fields a comma-delimited list - $fields = implode(', ', $fields); - $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarSubscriptionsTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC"); - $stmt->execute([$principalUri]); - - $subscriptions = []; - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - - $subscription = [ - 'id' => $row['id'], - 'uri' => $row['uri'], - 'principaluri' => $row['principaluri'], - 'source' => $row['source'], - 'lastmodified' => $row['lastmodified'], - - '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']), - ]; - - foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { - if (!is_null($row[$dbName])) { - $subscription[$xmlName] = $row[$dbName]; - } - } - - $subscriptions[] = $subscription; - - } - - return $subscriptions; + function getSubscriptionsForUser($principalUri) + { + return array(); } /** * Creates a new subscription for a principal. * * If the creation was a success, an id must be returned that can be used to reference * this subscription in other methods, such as updateSubscription. * * @param string $principalUri * @param string $uri * @param array $properties * @return mixed */ - function createSubscription($principalUri, $uri, array $properties) { - - $fieldNames = [ - 'principaluri', - 'uri', - 'source', - 'lastmodified', - ]; - - if (!isset($properties['{http://calendarserver.org/ns/}source'])) { - throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); - } - - $values = [ - ':principaluri' => $principalUri, - ':uri' => $uri, - ':source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), - ':lastmodified' => time(), - ]; - - foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { - if (isset($properties[$xmlName])) { - - $values[':' . $dbName] = $properties[$xmlName]; - $fieldNames[] = $dbName; - } - } - - $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarSubscriptionsTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")"); - $stmt->execute($values); - - return $this->pdo->lastInsertId(); + function createSubscription($principalUri, $uri, array $properties) + { + return null; } /** * Updates a subscription * * The list of mutations is stored in a Sabre\DAV\PropPatch object. * To do the actual updates, you must tell this object which properties * you're going to process with the handle() method. * * Calling the handle method is like telling the PropPatch object "I * promise I can handle updating this property". * * Read the PropPatch documenation for more info and examples. * * @param mixed $subscriptionId * @param \Sabre\DAV\PropPatch $propPatch * @return void */ - function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) { - - $supportedProperties = array_keys($this->subscriptionPropertyMap); - $supportedProperties[] = '{http://calendarserver.org/ns/}source'; - - $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) { - - $newValues = []; - - foreach ($mutations as $propertyName => $propertyValue) { - - if ($propertyName === '{http://calendarserver.org/ns/}source') { - $newValues['source'] = $propertyValue->getHref(); - } else { - $fieldName = $this->subscriptionPropertyMap[$propertyName]; - $newValues[$fieldName] = $propertyValue; - } - - } - - // Now we're generating the sql query. - $valuesSql = []; - foreach ($newValues as $fieldName => $value) { - $valuesSql[] = $fieldName . ' = ?'; - } - - $stmt = $this->pdo->prepare("UPDATE " . $this->calendarSubscriptionsTableName . " SET " . implode(', ', $valuesSql) . ", lastmodified = ? WHERE id = ?"); - $newValues['lastmodified'] = time(); - $newValues['id'] = $subscriptionId; - $stmt->execute(array_values($newValues)); - - return true; - - }); - + function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) + { + return; } /** * Deletes a subscription * * @param mixed $subscriptionId * @return void */ - function deleteSubscription($subscriptionId) { + function deleteSubscription($subscriptionId) + { - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarSubscriptionsTableName . ' WHERE id = ?'); - $stmt->execute([$subscriptionId]); + return; } /** * Returns a single scheduling object. * * The returned array should contain the following elements: * * uri - A unique basename for the object. This will be used to * construct a full uri. * * calendardata - The iCalendar object * * lastmodified - The last modification date. Can be an int for a unix * timestamp, or a PHP DateTime object. * * etag - A unique token that must change if the object changed. * * size - The size of the object, in bytes. * * @param string $principalUri * @param string $objectUri * @return array */ - function getSchedulingObject($principalUri, $objectUri) { - - $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?'); - $stmt->execute([$principalUri, $objectUri]); - $row = $stmt->fetch(\PDO::FETCH_ASSOC); - - if (!$row) return null; + function getSchedulingObject($principalUri, $objectUri) + { - return [ - 'uri' => $row['uri'], - 'calendardata' => $row['calendardata'], - 'lastmodified' => $row['lastmodified'], - 'etag' => '"' . $row['etag'] . '"', - 'size' => (int)$row['size'], - ]; + return null; } /** * Returns all scheduling objects for the inbox collection. * * These objects should be returned as an array. Every item in the array * should follow the same structure as returned from getSchedulingObject. * * The main difference is that 'calendardata' is optional. * * @param string $principalUri * @return array */ - function getSchedulingObjects($principalUri) { - - $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ?'); - $stmt->execute([$principalUri]); - - $result = []; - foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { - $result[] = [ - 'calendardata' => $row['calendardata'], - 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], - 'etag' => '"' . $row['etag'] . '"', - 'size' => (int)$row['size'], - ]; - } - - return $result; + function getSchedulingObjects($principalUri) + { + return null; } /** * Deletes a scheduling object * * @param string $principalUri * @param string $objectUri * @return void */ - function deleteSchedulingObject($principalUri, $objectUri) { + function deleteSchedulingObject($principalUri, $objectUri) + { - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?'); - $stmt->execute([$principalUri, $objectUri]); - + return; } /** * Creates a new scheduling object. This should land in a users' inbox. * * @param string $principalUri * @param string $objectUri * @param string $objectData * @return void */ - function createSchedulingObject($principalUri, $objectUri, $objectData) { + function createSchedulingObject($principalUri, $objectUri, $objectData) + { - $stmt = $this->pdo->prepare('INSERT INTO ' . $this->schedulingObjectTableName . ' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)'); - $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData) ]); + return; } } diff --git a/calendarserver.php b/calendarserver.php --- a/calendarserver.php +++ b/calendarserver.php @@ -1,112 +1,100 @@ getConfig('disable_sync') === 1) { if($conf['allowdebug']) dbglog('Synchronisation is disabled'); die('Synchronisation is disabled'); } -/* Database */ -$pdo = new PDO('sqlite:'.$sqlFile); -$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - //Mapping PHP errors to exceptions function exception_error_handler($errno, $errstr, $errfile, $errline) { if($conf['allowdebug']) dbglog('Exception occured: '.$errstr); throw new ErrorException($errstr, 0, $errno, $errfile, $errline); } //set_error_handler("exception_error_handler"); // Files we need require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); require_once(DOKU_PLUGIN.'davcal/authBackendDokuwiki.php'); require_once(DOKU_PLUGIN.'davcal/principalBackendDokuwiki.php'); require_once(DOKU_PLUGIN.'davcal/calendarBackendDokuwiki.php'); // Backends - our DokuWiki backends $authBackend = new DokuWikiSabreAuthBackend(); -$calendarBackend = new DokuWikiSabreCalendarBackend($pdo); +$calendarBackend = new DokuWikiSabreCalendarBackend($hlp); $principalBackend = new DokuWikiSabrePrincipalBackend(); // Directory structure -$tree = [ +$tree = array( new Sabre\CalDAV\Principal\Collection($principalBackend), new Sabre\CalDAV\CalendarRoot($principalBackend, $calendarBackend), -]; +); $server = new Sabre\DAV\Server($tree); if (isset($baseUri)) $server->setBaseUri($baseUri); /* Server Plugins */ $authPlugin = new Sabre\DAV\Auth\Plugin($authBackend); $server->addPlugin($authPlugin); $aclPlugin = new Sabre\DAVACL\Plugin(); $server->addPlugin($aclPlugin); /* CalDAV support */ $caldavPlugin = new Sabre\CalDAV\Plugin(); $server->addPlugin($caldavPlugin); /* Calendar subscription support */ //$server->addPlugin( // new Sabre\CalDAV\Subscriptions\Plugin() //); /* Calendar scheduling support */ //$server->addPlugin( // new Sabre\CalDAV\Schedule\Plugin() //); /* WebDAV-Sync plugin */ $server->addPlugin(new Sabre\DAV\Sync\Plugin()); // Support for html frontend $browser = new Sabre\DAV\Browser\Plugin(); $server->addPlugin($browser); if($conf['allowdebug']) dbglog('$server->exec()'); // And off we go! $server->exec(); diff --git a/helper.php b/helper.php --- a/helper.php +++ b/helper.php @@ -1,1156 +1,1650 @@ sqlite =& plugin_load('helper', 'sqlite'); global $conf; if($conf['allowdebug']) dbglog('---- DAVCAL helper.php init'); if(!$this->sqlite) { if($conf['allowdebug']) dbglog('This plugin requires the sqlite plugin. Please install it.'); msg('This plugin requires the sqlite plugin. Please install it.'); return; } if(!$this->sqlite->init('davcal', DOKU_PLUGIN.'davcal/db/')) { if($conf['allowdebug']) dbglog('Error initialising the SQLite DB for DAVCal'); return; } } /** * Retrieve meta data for a given page * * @param string $id optional The page ID * @return array The metadata */ private function getMeta($id = null) { global $ID; global $INFO; if ($id === null) $id = $ID; if($ID === $id && $INFO['meta']) { $meta = $INFO['meta']; } else { $meta = p_get_metadata($id); } return $meta; } /** * Retrieve the meta data for a given page * * @param string $id optional The page ID * @return array with meta data */ public function getCalendarMetaForPage($id = null) { if(is_null($id)) { global $ID; $id = $ID; } $meta = $this->getMeta($id); if(isset($meta['plugin_davcal'])) return $meta['plugin_davcal']; else return array(); } /** * Filter calendar pages and return only those where the current * user has at least read permission. * * @param array $calendarPages Array with calendar pages to check * @return array with filtered calendar pages */ public function filterCalendarPagesByUserPermission($calendarPages) { $retList = array(); foreach($calendarPages as $page => $data) { if(auth_quickaclcheck($page) >= AUTH_READ) { $retList[$page] = $data; } } return $retList; } /** * Get all calendar pages used by a given page * based on the stored metadata * * @param string $id optional The page id * @return mixed The pages as array or false */ public function getCalendarPagesByMeta($id = null) { if(is_null($id)) { global $ID; $id = $ID; } $meta = $this->getCalendarMetaForPage($id); if(isset($meta['id'])) { // Filter the list of pages by permission $pages = $this->filterCalendarPagesByUserPermission($meta['id']); if(empty($pages)) return false; return $pages; } return false; } /** * Get a list of calendar names/pages/ids/colors * for an array of page ids * * @param array $calendarPages The calendar pages to retrieve * @return array The list */ public function getCalendarMapForIDs($calendarPages) { $data = array(); foreach($calendarPages as $page => $color) { $calid = $this->getCalendarIdForPage($page); if($calid !== false) { $settings = $this->getCalendarSettings($calid); $name = $settings['displayname']; $write = (auth_quickaclcheck($page) > AUTH_READ); $data[] = array('name' => $name, 'page' => $page, 'calid' => $calid, 'color' => $color, 'write' => $write); } } return $data; } /** * Get the saved calendar color for a given page. * * @param string $id optional The page ID * @return mixed The color on success, otherwise false */ public function getCalendarColorForPage($id = null) { if(is_null($id)) { global $ID; $id = $ID; } $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; return $this->getCalendarColorForCalendar($calid); } /** * Get the saved calendar color for a given calendar ID. * * @param string $id optional The calendar ID * @return mixed The color on success, otherwise false */ public function getCalendarColorForCalendar($calid) { if(isset($this->cachedValues['calendarcolor'][$calid])) return $this->cachedValues['calendarcolor'][$calid]; $row = $this->getCalendarSettings($calid); if(!isset($row['calendarcolor'])) return false; $color = $row['calendarcolor']; $this->cachedValues['calendarcolor'][$calid] = $color; return $color; } /** * Get the user's principal URL for iOS sync * @param string $user the user name * @return the URL to the principal sync */ public function getPrincipalUrlForUser($user) { if(is_null($user)) return false; $url = DOKU_URL.'lib/plugins/davcal/calendarserver.php/principals/'.$user; return $url; } /** * Set the calendar color for a given page. * * @param string $color The color definition * @param string $id optional The page ID * @return boolean True on success, otherwise false */ public function setCalendarColorForPage($color, $id = null) { if(is_null($id)) { global $ID; $id = $ID; } $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; $query = "UPDATE calendars SET calendarcolor = ? ". " WHERE id = ?"; $res = $this->sqlite->query($query, $color, $calid); if($res !== false) { $this->cachedValues['calendarcolor'][$calid] = $color; return true; } return false; } /** * Set the calendar name and description for a given page with a given * page id. * If the calendar doesn't exist, the calendar is created! * * @param string $name The name of the new calendar * @param string $description The description of the new calendar * @param string $id (optional) The ID of the page * @param string $userid The userid of the creating user * * @return boolean True on success, otherwise false. */ public function setCalendarNameForPage($name, $description, $id = null, $userid = null) { if(is_null($id)) { global $ID; $id = $ID; } if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { $userid = uniqid('davcal-'); } } $calid = $this->getCalendarIdForPage($id); if($calid === false) return $this->createCalendarForPage($name, $description, $id, $userid); $query = "UPDATE calendars SET displayname = ?, description = ? WHERE id = ?"; $res = $this->sqlite->query($query, $name, $description, $calid); if($res !== false) return true; return false; } /** + * Update a calendar's displayname + * + * @param int $calid The calendar's ID + * @param string $name The new calendar name + * + * @return boolean True on success, otherwise false + */ + public function updateCalendarName($calid, $name) + { + $query = "UPDATE calendars SET displayname = ? WHERE id = ?"; + $res = $this->sqlite->query($query, $calid, $name); + if($res !== false) + { + $this->updateSyncTokenLog($calid, '', 'modified'); + return true; + } + return false; + } + + /** + * Update the calendar description + * + * @param int $calid The calendar's ID + * @param string $description The new calendar's description + * + * @return boolean True on success, otherwise false + */ + public function updateCalendarDescription($calid, $description) + { + $query = "UPDATE calendars SET description = ? WHERE id = ?"; + $res = $this->sqlite->query($query, $calid, $description); + if($res !== false) + { + $this->updateSyncTokenLog($calid, '', 'modified'); + return true; + } + return false; + } + + /** + * Update a calendar's timezone information + * + * @param int $calid The calendar's ID + * @param string $timezone The new timezone to set + * + * @return boolean True on success, otherwise false + */ + public function updateCalendarTimezone($calid, $timezone) + { + $query = "UPDATE calendars SET timezone = ? WHERE id = ?"; + $res = $this->sqlite->query($query, $calid, $timezone); + if($res !== false) + { + $this->updateSyncTokenLog($calid, '', 'modified'); + return true; + } + return false; + } + + /** * Save the personal settings to the SQLite database 'calendarsettings'. * * @param array $settings The settings array to store * @param string $userid (optional) The userid to store * * @param boolean True on success, otherwise false */ public function savePersonalSettings($settings, $userid = null) { if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { return false; } } $this->sqlite->query("BEGIN TRANSACTION"); $query = "DELETE FROM calendarsettings WHERE userid = ?"; $this->sqlite->query($query, $userid); foreach($settings as $key => $value) { $query = "INSERT INTO calendarsettings (userid, key, value) VALUES (?, ?, ?)"; $res = $this->sqlite->query($query, $userid, $key, $value); if($res === false) return false; } $this->sqlite->query("COMMIT TRANSACTION"); $this->cachedValues['settings'][$userid] = $settings; return true; } /** * Retrieve the settings array for a given user id. * Some sane defaults are returned, currently: * * timezone => local * weeknumbers => 0 * workweek => 0 * * @param string $userid (optional) The user id to retrieve * * @return array The settings array */ public function getPersonalSettings($userid = null) { // Some sane default settings $settings = array( 'timezone' => $this->getConf('timezone'), 'weeknumbers' => $this->getConf('weeknumbers'), 'workweek' => $this->getConf('workweek'), 'monday' => $this->getConf('monday'), 'timeformat' => $this->getConf('timeformat') ); if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { return $settings; } } if(isset($this->cachedValues['settings'][$userid])) return $this->cachedValues['settings'][$userid]; $query = "SELECT key, value FROM calendarsettings WHERE userid = ?"; $res = $this->sqlite->query($query, $userid); $arr = $this->sqlite->res2arr($res); foreach($arr as $row) { $settings[$row['key']] = $row['value']; } $this->cachedValues['settings'][$userid] = $settings; return $settings; } /** * Retrieve the calendar ID based on a page ID from the SQLite table * 'pagetocalendarmapping'. * * @param string $id (optional) The page ID to retrieve the corresponding calendar * * @return mixed the ID on success, otherwise false */ public function getCalendarIdForPage($id = null) { if(is_null($id)) { global $ID; $id = $ID; } if(isset($this->cachedValues['calid'][$id])) return $this->cachedValues['calid'][$id]; $query = "SELECT calid FROM pagetocalendarmapping WHERE page = ?"; $res = $this->sqlite->query($query, $id); $row = $this->sqlite->res2row($res); if(isset($row['calid'])) { $calid = $row['calid']; $this->cachedValues['calid'] = $calid; return $calid; } return false; } /** * Retrieve the complete calendar id to page mapping. * This is necessary to be able to retrieve a list of * calendars for a given user and check the access rights. * * @return array The mapping array */ public function getCalendarIdToPageMapping() { $query = "SELECT calid, page FROM pagetocalendarmapping"; $res = $this->sqlite->query($query); $arr = $this->sqlite->res2arr($res); return $arr; } /** * Retrieve all calendar IDs a given user has access to. * The user is specified by the principalUri, so the * user name is actually split from the URI component. * * Access rights are checked against DokuWiki's ACL * and applied accordingly. * * @param string $principalUri The principal URI to work on * * @return array An associative array of calendar IDs */ public function getCalendarIdsForUser($principalUri) { global $auth; $user = explode('/', $principalUri); $user = end($user); $mapping = $this->getCalendarIdToPageMapping(); $calids = array(); $ud = $auth->getUserData($user); $groups = $ud['grps']; foreach($mapping as $row) { $id = $row['calid']; $page = $row['page']; $acl = auth_aclcheck($page, $user, $groups); if($acl >= AUTH_READ) { $write = $acl > AUTH_READ; $calids[$id] = array('readonly' => !$write); } } return $calids; } /** * Create a new calendar for a given page ID and set name and description * accordingly. Also update the pagetocalendarmapping table on success. * * @param string $name The calendar's name * @param string $description The calendar's description * @param string $id (optional) The page ID to work on * @param string $userid (optional) The user ID that created the calendar * * @return boolean True on success, otherwise false */ public function createCalendarForPage($name, $description, $id = null, $userid = null) { if(is_null($id)) { global $ID; $id = $ID; } if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { $userid = uniqid('davcal-'); } } $values = array('principals/'.$userid, $name, str_replace(array('/', ' ', ':'), '_', $id), $description, 'VEVENT,VTODO', 0, 1); $query = "INSERT INTO calendars (principaluri, displayname, uri, description, components, transparent, synctoken) ". "VALUES (?, ?, ?, ?, ?, ?, ?)"; $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6]); if($res === false) return false; // Get the new calendar ID $query = "SELECT id FROM calendars WHERE principaluri = ? AND displayname = ? AND ". "uri = ? AND description = ?"; $res = $this->sqlite->query($query, $values[0], $values[1], $values[2], $values[3]); $row = $this->sqlite->res2row($res); // Update the pagetocalendarmapping table with the new calendar ID if(isset($row['id'])) { $query = "INSERT INTO pagetocalendarmapping (page, calid) VALUES (?, ?)"; $res = $this->sqlite->query($query, $id, $row['id']); return ($res !== false); } return false; } /** + * Add a new calendar entry to the given calendar. Calendar data is + * specified as ICS file, thus it needs to be parsed first. + * + * This is mainly needed for the sync support. + * + * @param int $calid The calendar's ID + * @param string $uri The new object URI + * @param string $ics The ICS file + * + * @return mixed The etag. + */ + public function addCalendarEntryToCalendarByICS($calid, $uri, $ics) + { + $extraData = $this->getDenormalizedData($ics); + + $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)"; + $res = $this->sqlite->query($query, + $calid, + $uri, + $ics, + time(), + $extraData['etag'], + $extraData['size'], + $extraData['componentType'], + $extraData['firstOccurence'], + $extraData['lastOccurence'], + $extraData['uid']); + // If successfully, update the sync token database + if($res !== false) + { + $this->updateSyncTokenLog($calid, $uri, 'added'); + } + return $extraData['etag']; + } + + /** + * Edit a calendar entry by providing a new ICS file. This is mainly + * needed for the sync support. + * + * @param int $calid The calendar's IS + * @param string $uri The object's URI to modify + * @param string $ics The new object's ICS file + */ + public function editCalendarEntryToCalendarByICS($calid, $uri, $ics) + { + $extraData = $this->getDenormalizedData($ics); + + $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?"; + $res = $this->sqlite->query($query, + $ics, + time(), + $extraData['etag'], + $extraData['size'], + $extraData['componentType'], + $extraData['firstOccurence'], + $extraData['lastOccurence'], + $extraData['uid'], + $calid, + $uri + ); + if($res !== false) + { + $this->updateSyncTokenLog($calid, $uri, 'modified'); + } + return $extraData['etag']; + } + + /** * Add a new iCal entry for a given page, i.e. a given calendar. * * The parameter array needs to contain * detectedtz => The timezone as detected by the browser * currenttz => The timezone in use by the calendar * eventfrom => The event's start date * eventfromtime => The event's start time * eventto => The event's end date * eventtotime => The event's end time * eventname => The event's name * eventdescription => The event's description * * @param string $id The page ID to work on * @param string $user The user who created the calendar * @param string $params A parameter array with values to create * * @return boolean True on success, otherwise false */ public function addCalendarEntryToCalendarForPage($id, $user, $params) { if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') $timezone = new \DateTimeZone($params['currenttz']); elseif($params['currenttz'] === 'local') $timezone = new \DateTimeZone($params['detectedtz']); else $timezone = new \DateTimeZone('UTC'); // Retrieve dates from settings $startDate = explode('-', $params['eventfrom']); $startTime = explode(':', $params['eventfromtime']); $endDate = explode('-', $params['eventto']); $endTime = explode(':', $params['eventtotime']); // Load SabreDAV require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); $vcalendar = new \Sabre\VObject\Component\VCalendar(); // Add VCalendar, UID and Event Name $event = $vcalendar->add('VEVENT'); $uuid = \Sabre\VObject\UUIDUtil::getUUID(); $event->add('UID', $uuid); $event->summary = $params['eventname']; // Add a description if requested $description = $params['eventdescription']; if($description !== '') $event->add('DESCRIPTION', $description); // Add attachments $attachments = $params['attachments']; if(!is_null($attachments)) foreach($attachments as $attachment) $event->add('ATTACH', $attachment); // Create a timestamp for last modified, created and dtstamp values in UTC $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); $event->add('DTSTAMP', $dtStamp); $event->add('CREATED', $dtStamp); $event->add('LAST-MODIFIED', $dtStamp); // Adjust the start date, based on the given timezone information $dtStart = new \DateTime(); $dtStart->setTimezone($timezone); $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); // Only add the time values if it's not an allday event if($params['allday'] != '1') $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); // Adjust the end date, based on the given timezone information $dtEnd = new \DateTime(); $dtEnd->setTimezone($timezone); $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); // Only add the time values if it's not an allday event if($params['allday'] != '1') $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); // According to the VCal spec, we need to add a whole day here if($params['allday'] == '1') $dtEnd->add(new \DateInterval('P1D')); // Really add Start and End events $dtStartEv = $event->add('DTSTART', $dtStart); $dtEndEv = $event->add('DTEND', $dtEnd); // Adjust the DATE format for allday events if($params['allday'] == '1') { $dtStartEv['VALUE'] = 'DATE'; $dtEndEv['VALUE'] = 'DATE'; } // Actually add the values to the database $calid = $this->getCalendarIdForPage($id); $uri = uniqid('dokuwiki-').'.ics'; $now = new DateTime(); $eventStr = $vcalendar->serialize(); $query = "INSERT INTO calendarobjects (calendarid, uri, calendardata, lastmodified, componenttype, firstoccurence, lastoccurence, size, etag, uid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; $res = $this->sqlite->query($query, $calid, $uri, $eventStr, $now->getTimestamp(), 'VEVENT', $event->DTSTART->getDateTime()->getTimeStamp(), $event->DTEND->getDateTime()->getTimeStamp(), strlen($eventStr), md5($eventStr), $uuid); // If successfully, update the sync token database if($res !== false) { $this->updateSyncTokenLog($calid, $uri, 'added'); return true; } return false; } /** * Retrieve the calendar settings of a given calendar id * * @param string $calid The calendar ID * * @return array The calendar settings array */ public function getCalendarSettings($calid) { - $query = "SELECT principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id= ? "; + $query = "SELECT id, principaluri, calendarcolor, displayname, uri, description, components, transparent, synctoken FROM calendars WHERE id= ? "; $res = $this->sqlite->query($query, $calid); $row = $this->sqlite->res2row($res); return $row; } /** * Retrieve all events that are within a given date range, * based on the timezone setting. * * There is also support for retrieving recurring events, * using Sabre's VObject Iterator. Recurring events are represented * as individual calendar entries with the same UID. * * @param string $id The page ID to work with * @param string $user The user ID to work with * @param string $startDate The start date as a string * @param string $endDate The end date as a string * @param string $color (optional) The calendar's color * * @return array An array containing the calendar entries. */ public function getEventsWithinDateRange($id, $user, $startDate, $endDate, $timezone, $color = null) { if($timezone !== '' && $timezone !== 'local') $timezone = new \DateTimeZone($timezone); else $timezone = new \DateTimeZone('UTC'); $data = array(); // Load SabreDAV require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); $calid = $this->getCalendarIdForPage($id); if(is_null($color)) $color = $this->getCalendarColorForCalendar($calid); $query = "SELECT calendardata, componenttype, uid FROM calendarobjects WHERE calendarid = ?"; $startTs = null; $endTs = null; if($startDate !== null) { $startTs = new \DateTime($startDate); $query .= " AND lastoccurence > ".$this->sqlite->quote_string($startTs->getTimestamp()); } if($endDate !== null) { $endTs = new \DateTime($endDate); $query .= " AND firstoccurence < ".$this->sqlite->quote_string($endTs->getTimestamp()); } // Retrieve matching calendar objects $res = $this->sqlite->query($query, $calid); $arr = $this->sqlite->res2arr($res); // Parse individual calendar entries foreach($arr as $row) { if(isset($row['calendardata'])) { $entry = array(); $vcal = \Sabre\VObject\Reader::read($row['calendardata']); $recurrence = $vcal->VEVENT->RRULE; // If it is a recurring event, pass it through Sabre's EventIterator if($recurrence != null) { $rEvents = new \Sabre\VObject\Recur\EventIterator(array($vcal->VEVENT)); $rEvents->rewind(); while($rEvents->valid()) { $event = $rEvents->getEventObject(); // If we are after the given time range, exit if(($endTs !== null) && ($rEvents->getDtStart()->getTimestamp() > $endTs->getTimestamp())) break; // If we are before the given time range, continue if(($startTs != null) && ($rEvents->getDtEnd()->getTimestamp() < $startTs->getTimestamp())) { $rEvents->next(); continue; } // If we are within the given time range, parse the event $data[] = $this->convertIcalDataToEntry($event, $id, $timezone, $row['uid'], $color, true); $rEvents->next(); } } else $data[] = $this->convertIcalDataToEntry($vcal->VEVENT, $id, $timezone, $row['uid'], $color); } } return $data; } /** * Helper function that parses the iCal data of a VEVENT to a calendar entry. * * @param \Sabre\VObject\VEvent $event The event to parse * @param \DateTimeZone $timezone The timezone object * @param string $uid The entry's UID * @param boolean $recurring (optional) Set to true to define a recurring event * * @return array The parse calendar entry */ private function convertIcalDataToEntry($event, $page, $timezone, $uid, $color, $recurring = false) { $entry = array(); $start = $event->DTSTART; // Parse only if the start date/time is present if($start !== null) { $dtStart = $start->getDateTime(); $dtStart->setTimezone($timezone); // moment.js doesn't like times be given even if // allDay is set to true // This should fix T23 if($start['VALUE'] == 'DATE') { $entry['allDay'] = true; $entry['start'] = $dtStart->format("Y-m-d"); } else { $entry['allDay'] = false; $entry['start'] = $dtStart->format(\DateTime::ATOM); } } $end = $event->DTEND; // Parse only if the end date/time is present if($end !== null) { $dtEnd = $end->getDateTime(); $dtEnd->setTimezone($timezone); if($end['VALUE'] == 'DATE') $entry['end'] = $dtEnd->format("Y-m-d"); else $entry['end'] = $dtEnd->format(\DateTime::ATOM); } $description = $event->DESCRIPTION; if($description !== null) $entry['description'] = (string)$description; else $entry['description'] = ''; $attachments = $event->ATTACH; if($attachments !== null) { $entry['attachments'] = array(); foreach($attachments as $attachment) $entry['attachments'][] = (string)$attachment; } $entry['title'] = (string)$event->summary; $entry['id'] = $uid; $entry['page'] = $page; $entry['color'] = $color; $entry['recurring'] = $recurring; return $entry; } /** * Retrieve an event by its UID * * @param string $uid The event's UID * * @return mixed The table row with the given event */ public function getEventWithUid($uid) { $query = "SELECT calendardata, calendarid, componenttype, uri FROM calendarobjects WHERE uid = ?"; $res = $this->sqlite->query($query, $uid); $row = $this->sqlite->res2row($res); return $row; } /** + * Retrieve information of a calendar's object, not including the actual + * calendar data! This is mainly neede for the sync support. + * + * @param int $calid The calendar ID + * + * @return mixed The result + */ + public function getCalendarObjects($calid) + { + $query = "SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM calendarobjects WHERE calendarid = ?"; + $res = $this->sqlite->query($query, $calid); + $arr = $this->sqlite->res2arr($res); + return $arr; + } + + /** + * Retrieve a single calendar object by calendar ID and URI + * + * @param int $calid The calendar's ID + * @param string $uri The object's URI + * + * @return mixed The result + */ + public function getCalendarObjectByUri($calid, $uri) + { + $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri = ?"; + $res = $this->sqlite->query($query, $calid, $uri); + $row = $this->sqlite->res2row($res); + return $row; + } + + /** + * Retrieve several calendar objects by specifying an array of URIs. + * This is mainly neede for sync. + * + * @param int $calid The calendar's ID + * @param array $uris An array of URIs + * + * @return mixed The result + */ + public function getMultipleCalendarObjectsByUri($calid, $uris) + { + $query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM calendarobjects WHERE calendarid = ? AND uri IN ("; + // Inserting a whole bunch of question marks + $query .= implode(',', array_fill(0, count($uris), '?')); + $query .= ')'; + $vals = array_merge(array($calid), $uris); + + $res = $this->sqlite->query($query, $vals); + $arr = $this->sqlite->res2arr($res); + return $arr; + } + + /** * Retrieve all calendar events for a given calendar ID * * @param string $calid The calendar's ID * * @return array An array containing all calendar data */ public function getAllCalendarEvents($calid) { $query = "SELECT calendardata, uid, componenttype, uri FROM calendarobjects WHERE calendarid = ?"; $res = $this->sqlite->query($query, $calid); $arr = $this->sqlite->res2arr($res); return $arr; } /** * Edit a calendar entry for a page, given by its parameters. * The params array has the same format as @see addCalendarEntryForPage * * @param string $id The page's ID to work on * @param string $user The user's ID to work on * @param array $params The parameter array for the edited calendar event * * @return boolean True on success, otherwise false */ public function editCalendarEntryForPage($id, $user, $params) { if($params['currenttz'] !== '' && $params['currenttz'] !== 'local') $timezone = new \DateTimeZone($params['currenttz']); elseif($params['currenttz'] === 'local') $timezone = new \DateTimeZone($params['detectedtz']); else $timezone = new \DateTimeZone('UTC'); // Parse dates $startDate = explode('-', $params['eventfrom']); $startTime = explode(':', $params['eventfromtime']); $endDate = explode('-', $params['eventto']); $endTime = explode(':', $params['eventtotime']); // Retrieve the existing event based on the UID $uid = $params['uid']; $event = $this->getEventWithUid($uid); // Load SabreDAV require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); if(!isset($event['calendardata'])) return false; $uri = $event['uri']; $calid = $event['calendarid']; // Parse the existing event $vcal = \Sabre\VObject\Reader::read($event['calendardata']); $vevent = $vcal->VEVENT; // Set the new event values $vevent->summary = $params['eventname']; $dtStamp = new \DateTime(null, new \DateTimeZone('UTC')); $description = $params['eventdescription']; // Remove existing timestamps to overwrite them $vevent->remove('DESCRIPTION'); $vevent->remove('DTSTAMP'); $vevent->remove('LAST-MODIFIED'); $vevent->remove('ATTACH'); // Add new time stamps and description $vevent->add('DTSTAMP', $dtStamp); $vevent->add('LAST-MODIFIED', $dtStamp); if($description !== '') $vevent->add('DESCRIPTION', $description); // Add attachments $attachments = $params['attachments']; if(!is_null($attachments)) foreach($attachments as $attachment) $vevent->add('ATTACH', $attachment); // Setup DTSTART $dtStart = new \DateTime(); $dtStart->setTimezone($timezone); $dtStart->setDate(intval($startDate[0]), intval($startDate[1]), intval($startDate[2])); if($params['allday'] != '1') $dtStart->setTime(intval($startTime[0]), intval($startTime[1]), 0); // Setup DTEND $dtEnd = new \DateTime(); $dtEnd->setTimezone($timezone); $dtEnd->setDate(intval($endDate[0]), intval($endDate[1]), intval($endDate[2])); if($params['allday'] != '1') $dtEnd->setTime(intval($endTime[0]), intval($endTime[1]), 0); // According to the VCal spec, we need to add a whole day here if($params['allday'] == '1') $dtEnd->add(new \DateInterval('P1D')); $vevent->remove('DTSTART'); $vevent->remove('DTEND'); $dtStartEv = $vevent->add('DTSTART', $dtStart); $dtEndEv = $vevent->add('DTEND', $dtEnd); // Remove the time for allday events if($params['allday'] == '1') { $dtStartEv['VALUE'] = 'DATE'; $dtEndEv['VALUE'] = 'DATE'; } $now = new DateTime(); $eventStr = $vcal->serialize(); // Actually write to the database $query = "UPDATE calendarobjects SET calendardata = ?, lastmodified = ?, ". "firstoccurence = ?, lastoccurence = ?, size = ?, etag = ? WHERE uid = ?"; $res = $this->sqlite->query($query, $eventStr, $now->getTimestamp(), $dtStart->getTimestamp(), $dtEnd->getTimestamp(), strlen($eventStr), md5($eventStr), $uid); if($res !== false) { $this->updateSyncTokenLog($calid, $uri, 'modified'); return true; } return false; } /** + * Delete an event from a calendar by calendar ID and URI + * + * @param int $calid The calendar's ID + * @param string $uri The object's URI + * + * @return true + */ + public function deleteCalendarEntryForCalendarByUri($calid, $uri) + { + $query = "DELETE FROM calendarobjects WHERE calendarid = ? AND uri = ?"; + $res = $this->sqlite->query($query, $calid, $uri); + if($res !== false) + { + $this->updateSyncTokenLog($calid, $uri, 'deleted'); + } + return true; + } + + /** * Delete a calendar entry for a given page. Actually, the event is removed * based on the entry's UID, so that page ID is no used. * * @param string $id The page's ID (unused) * @param array $params The parameter array to work with * * @return boolean True */ public function deleteCalendarEntryForPage($id, $params) { $uid = $params['uid']; $event = $this->getEventWithUid($uid); $calid = $event['calendarid']; $uri = $event['uri']; $query = "DELETE FROM calendarobjects WHERE uid = ?"; $res = $this->sqlite->query($query, $uid); if($res !== false) { $this->updateSyncTokenLog($calid, $uri, 'deleted'); } return true; } /** * Retrieve the current sync token for a calendar * * @param string $calid The calendar id * * @return mixed The synctoken or false */ public function getSyncTokenForCalendar($calid) { $row = $this->getCalendarSettings($calid); if(isset($row['synctoken'])) return $row['synctoken']; return false; } /** * Helper function to convert the operation name to * an operation code as stored in the database * * @param string $operationName The operation name * * @return mixed The operation code or false */ public function operationNameToOperation($operationName) { switch($operationName) { case 'added': return 1; break; case 'modified': return 2; break; case 'deleted': return 3; break; } return false; } /** * Update the sync token log based on the calendar id and the * operation that was performed. * * @param string $calid The calendar ID that was modified * @param string $uri The calendar URI that was modified * @param string $operation The operation that was performed * * @return boolean True on success, otherwise false */ private function updateSyncTokenLog($calid, $uri, $operation) { $currentToken = $this->getSyncTokenForCalendar($calid); $operationCode = $this->operationNameToOperation($operation); if(($operationCode === false) || ($currentToken === false)) return false; $values = array($uri, $currentToken, $calid, $operationCode ); $query = "INSERT INTO calendarchanges (uri, synctoken, calendarid, operation) VALUES(?, ?, ?, ?)"; $res = $this->sqlite->query($query, $uri, $currentToken, $calid, $operationCode); if($res === false) return false; $currentToken++; $query = "UPDATE calendars SET synctoken = ? WHERE id = ?"; $res = $this->sqlite->query($query, $currentToken, $calid); return ($res !== false); } /** * Return the sync URL for a given Page, i.e. a calendar * * @param string $id The page's ID * @param string $user (optional) The user's ID * * @return mixed The sync url or false */ public function getSyncUrlForPage($id, $user = null) { if(is_null($userid)) { if(isset($_SERVER['REMOTE_USER']) && !is_null($_SERVER['REMOTE_USER'])) { $userid = $_SERVER['REMOTE_USER']; } else { return false; } } $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; $calsettings = $this->getCalendarSettings($calid); if(!isset($calsettings['uri'])) return false; $syncurl = DOKU_URL.'lib/plugins/davcal/calendarserver.php/calendars/'.$user.'/'.$calsettings['uri']; return $syncurl; } /** * Return the private calendar's URL for a given page * * @param string $id the page ID * * @return mixed The private URL or false */ public function getPrivateURLForPage($id) { $calid = $this->getCalendarIdForPage($id); if($calid === false) return false; return $this->getPrivateURLForCalendar($calid); } /** * Return the private calendar's URL for a given calendar ID * * @param string $calid The calendar's ID * * @return mixed The private URL or false */ public function getPrivateURLForCalendar($calid) { if(isset($this->cachedValues['privateurl'][$calid])) return $this->cachedValues['privateurl'][$calid]; $query = "SELECT url FROM calendartoprivateurlmapping WHERE calid = ?"; $res = $this->sqlite->query($query, $calid); $row = $this->sqlite->res2row($res); if(!isset($row['url'])) { $url = uniqid("dokuwiki-").".ics"; $query = "INSERT INTO calendartoprivateurlmapping (url, calid) VALUES(?, ?)"; $res = $this->sqlite->query($query, $url, $calid); if($res === false) return false; } else { $url = $row['url']; } $url = DOKU_URL.'lib/plugins/davcal/ics.php/'.$url; $this->cachedValues['privateurl'][$calid] = $url; return $url; } /** * Retrieve the calendar ID for a given private calendar URL * * @param string $url The private URL * * @return mixed The calendar ID or false */ public function getCalendarForPrivateURL($url) { $query = "SELECT calid FROM calendartoprivateurlmapping WHERE url = ?"; $res = $this->sqlite->query($query, $url); $row = $this->sqlite->res2row($res); if(!isset($row['calid'])) return false; return $row['calid']; } /** * Return a given calendar as ICS feed, i.e. all events in one ICS file. * * @param string $calid The calendar ID to retrieve * * @return mixed The calendar events as string or false */ public function getCalendarAsICSFeed($calid) { $calSettings = $this->getCalendarSettings($calid); if($calSettings === false) return false; $events = $this->getAllCalendarEvents($calid); if($events === false) return false; // Load SabreDAV require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); $out = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//DAVCal//DAVCal for DokuWiki//EN\r\nCALSCALE:GREGORIAN\r\nX-WR-CALNAME:"; $out .= $calSettings['displayname']."\r\n"; foreach($events as $event) { $vcal = \Sabre\VObject\Reader::read($event['calendardata']); $evt = $vcal->VEVENT; $out .= $evt->serialize(); } $out .= "END:VCALENDAR\r\n"; return $out; } /** * Retrieve a configuration option for the plugin * * @param string $key The key to query * @return mixed The option set, null if not found */ public function getConfig($key) { return $this->getConf($key); } + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. Taken nearly unmodified from Sabre's PDO backend + * + * Returns an array with the following keys: + * * etag - An md5 checksum of the object without the quotes. + * * size - Size of the object in bytes + * * componentType - VEVENT, VTODO or VJOURNAL + * * firstOccurence + * * lastOccurence + * * uid - value of the UID property + * + * @param string $calendarData + * @return array + */ + protected function getDenormalizedData($calendarData) + { + require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); + + $vObject = \Sabre\VObject\Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + $uid = null; + foreach ($vObject->getComponents() as $component) + { + if ($component->name !== 'VTIMEZONE') + { + $componentType = $component->name; + $uid = (string)$component->UID; + break; + } + } + if (!$componentType) + { + return false; + } + if ($componentType === 'VEVENT') + { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurence is a bit harder + if (!isset($component->RRULE)) + { + if (isset($component->DTEND)) + { + $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); + } + elseif (isset($component->DURATION)) + { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue())); + $lastOccurence = $endDate->getTimeStamp(); + } + elseif (!$component->DTSTART->hasTime()) + { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate->modify('+1 day'); + $lastOccurence = $endDate->getTimeStamp(); + } + else + { + $lastOccurence = $firstOccurence; + } + } + else + { + $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID); + $maxDate = new \DateTime('2038-01-01'); + if ($it->isInfinite()) + { + $lastOccurence = $maxDate->getTimeStamp(); + } + else + { + $end = $it->getDtEnd(); + while ($it->valid() && $end < $maxDate) + { + $end = $it->getDtEnd(); + $it->next(); + } + $lastOccurence = $end->getTimeStamp(); + } + } + } + + return array( + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => $firstOccurence, + 'lastOccurence' => $lastOccurence, + 'uid' => $uid, + ); + + } + + /** + * Query a calendar by ID and taking several filters into account. + * This is heavily based on Sabre's PDO backend. + * + * @param int $calendarId The calendar's ID + * @param array $filters The filter array to apply + * + * @return mixed The result + */ + public function calendarQuery($calendarId, $filters) + { + $componentType = null; + $requirePostFilter = true; + $timeRange = null; + + // if no filters were specified, we don't need to filter after a query + if (!$filters['prop-filters'] && !$filters['comp-filters']) + { + $requirePostFilter = false; + } + + // Figuring out if there's a component filter + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) + { + $componentType = $filters['comp-filters'][0]['name']; + + // Checking if we need post-filters + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) + { + $requirePostFilter = false; + } + // There was a time-range filter + if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) + { + $timeRange = $filters['comp-filters'][0]['time-range']; + + // If start time OR the end time is not specified, we can do a + // 100% accurate mysql query. + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) + { + $requirePostFilter = false; + } + } + + } + + if ($requirePostFilter) + { + $query = "SELECT uri, calendardata FROM calendarobjects WHERE calendarid = ?"; + } + else + { + $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; + } + + $values = array( + $calendarId + ); + + if ($componentType) + { + $query .= " AND componenttype = ?"; + $values[] = $componentType; + } + + if ($timeRange && $timeRange['start']) + { + $query .= " AND lastoccurence > ?"; + $values[] = $timeRange['start']->getTimeStamp(); + } + if ($timeRange && $timeRange['end']) + { + $query .= " AND firstoccurence < ?"; + $values[] = $timeRange['end']->getTimeStamp(); + } + + $res = $this->sqlite->query($query, $values); + $arr = $this->sqlite->res2arr($res); + + $result = array(); + foreach($arr as $row) + { + if ($requirePostFilter) + { + if (!$this->validateFilterForObject($row, $filters)) + { + continue; + } + } + $result[] = $row['uri']; + + } + + return $result; + } + + /** + * This method validates if a filter (as passed to calendarQuery) matches + * the given object. Taken from Sabre's PDO backend + * + * @param array $object + * @param array $filters + * @return bool + */ + protected function validateFilterForObject($object, $filters) + { + require_once(DOKU_PLUGIN.'davcal/vendor/autoload.php'); + // Unfortunately, setting the 'calendardata' here is optional. If + // it was excluded, we actually need another call to get this as + // well. + if (!isset($object['calendardata'])) + { + $object = $this->getCalendarObjectByUri($object['calendarid'], $object['uri']); + } + + $vObject = \Sabre\VObject\Reader::read($object['calendardata']); + $validator = new \Sabre\CalDAV\CalendarQueryValidator(); + + return $validator->validate($vObject, $filters); + + } + + /** + * Retrieve changes for a given calendar based on the given syncToken. + * + * @param int $calid The calendar's ID + * @param int $syncToken The supplied sync token + * @param int $syncLevel The sync level + * @param int $limit The limit of changes + * + * @return array The result + */ + public function getChangesForCalendar($calid, $syncToken, $syncLevel, $limit = null) + { + // Current synctoken + $currentToken = $this->getSyncTokenForCalendar($calid); + + if ($currentToken === false) return null; + + $result = array( + 'syncToken' => $currentToken, + 'added' => array(), + 'modified' => array(), + 'deleted' => array(), + ); + + if ($syncToken) + { + + $query = "SELECT uri, operation FROM calendarchanges WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; + if ($limit > 0) $query .= " LIMIT " . (int)$limit; + + // Fetching all changes + $res = $this->sqlite->query($query, $syncToken, $currentToken, $calid); + if($res === false) + return null; + + $arr = $this->sqlite->res2arr($res); + $changes = array(); + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + foreach($arr as $row) + { + $changes[$row['uri']] = $row['operation']; + } + + foreach ($changes as $uri => $operation) + { + switch ($operation) + { + case 1 : + $result['added'][] = $uri; + break; + case 2 : + $result['modified'][] = $uri; + break; + case 3 : + $result['deleted'][] = $uri; + break; + } + + } + } + else + { + // No synctoken supplied, this is the initial sync. + $query = "SELECT uri FROM calendarobjects WHERE calendarid = ?"; + $res = $this->sqlite->query($query); + $arr = $this->sqlite->res2arr($res); + + $result['added'] = $arr; + } + return $result; + } + } diff --git a/plugin.info.txt b/plugin.info.txt --- a/plugin.info.txt +++ b/plugin.info.txt @@ -1,7 +1,7 @@ base davcal author Andreas Boehler email dev@aboehler.at -date 2016-05-06 +date 2016-05-11 name Calendar PlugIn with CalDAV sharing support desc Create one calendar per page and share/subscribe via CalDAV url http://www.dokuwiki.org/plugin:davcal