diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index 0efd2a46d6..de87f02e8f 100644 --- a/apps/dav/appinfo/database.xml +++ b/apps/dav/appinfo/database.xml @@ -670,6 +670,78 @@ CREATE TABLE calendarobjects ( + + *dbprefix*calendarobjects_properties + + + id + integer + 0 + true + 1 + true + 11 + + + calendarid + integer + + true + 11 + + + objectid + integer + + true + true + 11 + + + name + text + + false + 64 + + + parameter + text + + false + 64 + + + value + text + + false + 255 + + + calendarobject_index + + objectid + ascending + + + + calendarobject_name_index + + name + ascending + + + + calendarobject_value_index + + value + ascending + + + +
+ *dbprefix*dav_shares diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 0902c48724..684feb89c2 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ WebDAV endpoint AGPL owncloud.org - 1.2.0 + 1.3.0 diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index a4fed4f198..c59a2dcd14 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -44,6 +44,7 @@ use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropPatch; use Sabre\HTTP\URLUtil; +use Sabre\VObject\Component\VCalendar; use Sabre\VObject\DateTimeParser; use Sabre\VObject\Reader; use Sabre\VObject\Recur\EventIterator; @@ -108,6 +109,17 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', ]; + /** @var array properties to index */ + public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION', + 'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT', + 'ORGANIZER']; + + /** @var array parameters to index */ + public static $indexParameters = [ + 'ATTENDEE' => ['CN'], + 'ORGANIZER' => ['CN'], + ]; + /** * @var string[] Map of uid => display name */ @@ -134,6 +146,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** @var bool */ private $legacyEndpoint; + /** @var string */ + private $dbObjectPropertiesTable = 'calendarobjects_properties'; + /** * CalDavBackend constructor. * @@ -746,6 +761,11 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $stmt->execute([$calendarId]); $this->sharingBackend->deleteAllShares($calendarId); + + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbObjectPropertiesTable) + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->execute(); } /** @@ -940,6 +960,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ]) ->execute(); + $this->updateProperties($calendarId, $objectUri, $calendarData); + $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent( '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', [ @@ -990,6 +1012,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) ->execute(); + $this->updateProperties($calendarId, $objectUri, $calendarData); + $data = $this->getCalendarObject($calendarId, $objectUri); if (is_array($data)) { $this->dispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent( @@ -1050,6 +1074,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?'); $stmt->execute([$calendarId, $objectUri]); + $this->purgeProperties($calendarId, $data['id']); + $this->addChange($calendarId, $objectUri, 3); } @@ -1167,6 +1193,139 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $result; } + /** + * custom Nextcloud search extension for CalDAV + * + * @param string $principalUri + * @param array $filters + * @param integer|null $limit + * @param integer|null $offset + * @return array + */ + public function calendarSearch($principalUri, array $filters, $limit=null, $offset=null) { + $calendars = $this->getCalendarsForUser($principalUri); + $ownCalendars = []; + $sharedCalendars = []; + + $uriMapper = []; + + foreach($calendars as $calendar) { + if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) { + $ownCalendars[] = $calendar['id']; + } else { + $sharedCalendars[] = $calendar['id']; + } + $uriMapper[$calendar['id']] = $calendar['uri']; + } + if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) { + return []; + } + + $query = $this->db->getQueryBuilder(); + // Calendar id expressions + $calendarExpressions = []; + foreach($ownCalendars as $id) { + $calendarExpressions[] = $query->expr() + ->eq('c.calendarid', $query->createNamedParameter($id)); + } + foreach($sharedCalendars as $id) { + $calendarExpressions[] = $query->expr()->andX( + $query->expr()->eq('c.calendarid', + $query->createNamedParameter($id)), + $query->expr()->eq('c.classification', + $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)) + ); + } + + if (count($calendarExpressions) === 1) { + $calExpr = $calendarExpressions[0]; + } else { + $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions); + } + + // Component expressions + $compExpressions = []; + foreach($filters['comps'] as $comp) { + $compExpressions[] = $query->expr() + ->eq('c.componenttype', $query->createNamedParameter($comp)); + } + + if (count($compExpressions) === 1) { + $compExpr = $compExpressions[0]; + } else { + $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions); + } + + $propExpressions = []; + foreach($filters['props'] as $prop) { + $propExpressions[] = $query->expr()->andX( + $query->expr()->eq('i.name', $query->createNamedParameter($prop)), + $query->expr()->isNull('i.parameter') + ); + } + + if (count($propExpressions) === 1) { + $propExpr = $propExpressions[0]; + } else { + $propExpr = call_user_func_array([$query->expr(), 'orX'], $propExpressions); + } + + $paramExpressions = []; + foreach($filters['params'] as $param) { + $paramExpressions[] = $query->expr()->andX( + $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])), + $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter'])) + ); + } + + if (count($paramExpressions) === 1) { + $paramExpr = $paramExpressions[0]; + } else { + $paramExpr = call_user_func_array([$query->expr(), 'orX'], $paramExpressions); + } + + $offset = 0; + + $query->select(['c.calendarid', 'c.uri']) + ->from('calendarobjects_properties', 'i') + ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id')) + ->where($calExpr) + ->andWhere($compExpr) + ->andWhere($query->expr()->orX($propExpr, $paramExpr)) + ->andWhere($query->expr()->like('i.value', $query->createNamedParameter($filters['search-term']))) + ->setFirstResult($offset) + ->setMaxResults($limit); + + $stmt = $query->execute(); + + $result = []; + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $uriMapper[$row['calendarid']] . '/' . $row['uri']; + } + + return $result; + } + + /** + * This method validates if a filter (as passed to calendarSearch) matches + * the given object. + * + * @param array $object + * @param array $filters + * @return bool + */ + protected function validateFilterForCalendarSearch(array $object, array $filters) { + $vObject = Reader::read($object['calendardata']); + + $validator = new Search\CalendarSearchValidator(); + $result = $validator->validate($vObject, $filters); + + // Destroy circular references so PHP will GC the object. + $vObject->destroy(); + + return $result; + } + /** * Searches through all of a users calendars and calendar objects to find * an object with a specific UID. @@ -1820,6 +1979,125 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $this->sharingBackend->applyShareAcl($resourceId, $acl); } + + + /** + * update properties table + * + * @param int $calendarId + * @param string $objectUri + * @param string $calendarData + */ + protected function updateProperties($calendarId, $objectUri, $calendarData) { + $objectId = $this->getCalendarObjectId($calendarId, $objectUri); + $vCalendar = $this->readCalendarData($calendarData); + + $this->purgeProperties($calendarId, $objectId); + + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbObjectPropertiesTable) + ->values( + [ + 'calendarid' => $query->createNamedParameter($calendarId), + 'objectid' => $query->createNamedParameter($objectId), + 'name' => $query->createParameter('name'), + 'parameter' => $query->createParameter('parameter'), + 'value' => $query->createParameter('value'), + ] + ); + + $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO']; + foreach ($vCalendar->getComponents() as $component) { + if (!in_array($component->name, $indexComponents)) { + continue; + } + + foreach ($component->children() as $property) { + if (in_array($property->name, self::$indexProperties)) { + $value = $property->getValue(); + // is this a shitty db? + if ($this->db->supports4ByteText()) { + $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); + } + $value = substr($value, 0, 254); + + $query->setParameter('name', $property->name); + $query->setParameter('parameter', null); + $query->setParameter('value', $value); + $query->execute(); + } + + if (in_array($property->name, array_keys(self::$indexParameters))) { + $parameters = $property->parameters(); + $indexedParametersForProperty = self::$indexParameters[$property->name]; + + foreach ($parameters as $key => $value) { + if (in_array($key, $indexedParametersForProperty)) { + // is this a shitty db? + if ($this->db->supports4ByteText()) { + $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); + } + $value = substr($value, 0, 254); + + $query->setParameter('name', $property->name); + $query->setParameter('parameter', substr($key, 0, 254)); + $query->setParameter('value', substr($value, 0, 254)); + $query->execute(); + } + } + } + } + } + } + + /** + * read VCalendar data into a VCalendar object + * + * @param string $objectData + * @return VCalendar + */ + protected function readCalendarData($objectData) { + return Reader::read($objectData); + } + + /** + * delete all properties from a given calendar object + * + * @param int $calendarId + * @param int $objectId + */ + protected function purgeProperties($calendarId, $objectId) { + $query = $this->db->getQueryBuilder(); + $query->delete($this->dbObjectPropertiesTable) + ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId))) + ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); + $query->execute(); + } + + /** + * get ID from a given calendar object + * + * @param int $calendarId + * @param string $uri + * @return int + */ + protected function getCalendarObjectId($calendarId, $uri) { + $query = $this->db->getQueryBuilder(); + $query->select('id')->from('calendarobjects') + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) + ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); + + $result = $query->execute(); + $objectIds = $result->fetch(); + $result->closeCursor(); + + if (!isset($objectIds['id'])) { + throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri); + } + + return (int)$objectIds['id']; + } + private function convertPrincipal($principalUri, $toV2) { if ($this->principalBackend->getPrincipalPrefix() === 'principals') { list(, $name) = URLUtil::splitPath($principalUri); diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php index 7320754e6d..2aa2c9caa3 100644 --- a/apps/dav/lib/CalDAV/CalendarHome.php +++ b/apps/dav/lib/CalDAV/CalendarHome.php @@ -111,4 +111,14 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome { throw new NotFound('Node with name \'' . $name . '\' could not be found'); } + + /** + * @param array $filters + * @param integer|null $limit + * @param integer|null $offset + */ + function calendarSearch(array $filters, $limit=null, $offset=null) { + $principalUri = $this->principalInfo['uri']; + return $this->caldavBackend->calendarSearch($principalUri, $filters, $limit, $offset); + } } diff --git a/apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php b/apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php new file mode 100644 index 0000000000..7e52e84ebd --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php @@ -0,0 +1,102 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search; + +use Sabre\VObject; + +class CalendarSearchValidator { + + /** + * Verify if a list of filters applies to the calendar data object + * + * The list of filters must be formatted as parsed by Xml\Request\CalendarSearchReport + * + * @param VObject\Component\VCalendar $vObject + * @param array $filters + * @return bool + */ + function validate(VObject\Component\VCalendar $vObject, array $filters) { + $comps = $vObject->getComponents(); + $filters['comps'][] = 'VTIMEZONE'; + + $matches = false; + foreach($comps as $comp) { + if ($comp->name === 'VTIMEZONE') { + continue; + } + if ($matches) { + break; + } + + // check comps + if (!in_array($comp->name, $filters['comps'])) { + return false; + } + + $children = $comp->children(); + foreach($children as $child) { + if (!($child instanceof VObject\Property)) { + continue; + } + if ($matches) { + break; + } + + foreach($filters['props'] as $prop) { + if ($child->name !== $prop) { + continue; + } + + $value = $child->getValue(); + if (substr_count($value, $filters['search-term'])) { + $matches = true; + break; + } + } + + foreach($filters['params'] as $param) { + $propName = $param['property']; + $paramName = $param['parameter']; + + if ($child->name !== $propName) { + continue; + } + if ($matches) { + break; + } + + $parameters = $child->parameters(); + foreach ($parameters as $key => $value) { + if ($paramName !== $key) { + continue; + } + if (substr_count($value, $filters['search-term'])) { + $matches = true; + break; + } + } + } + } + } + + return $matches; + } +} diff --git a/apps/dav/lib/CalDAV/Search/SearchPlugin.php b/apps/dav/lib/CalDAV/Search/SearchPlugin.php new file mode 100644 index 0000000000..ad36f39e23 --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/SearchPlugin.php @@ -0,0 +1,159 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search; + +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use OCA\DAV\CalDAV\CalendarHome; + +class SearchPlugin extends ServerPlugin { + const NS_Nextcloud = 'http://nextcloud.com/ns'; + + /** + * Reference to SabreDAV server object. + * + * @var \Sabre\DAV\Server + */ + protected $server; + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return string[] + */ + public function getFeatures() { + // May have to be changed to be detected + return ['nc-calendar-search']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() { + return 'nc-calendar-search'; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + */ + public function initialize(Server $server) { + $this->server = $server; + + $server->on('report', [$this, 'report']); + + $server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search'] = + 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport'; + } + + /** + * This functions handles REPORT requests specific to CalDAV + * + * @param string $reportName + * @param mixed $report + * @param mixed $path + * @return bool + */ + function report($reportName, $report, $path) { + switch ($reportName) { + case '{' . self::NS_Nextcloud . '}calendar-search' : + $this->server->transactionType = 'report-nc-calendar-search'; + $this->calendarSearch($report); + return false; + } + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * @return array + */ + public function getSupportedReportSet($uri) { + $node = $this->server->tree->getNodeForPath($uri); + + $reports = []; + if ($node instanceof CalendarHome) { + $reports[] = '{' . self::NS_Nextcloud . '}calendar-search'; + } + + return $reports; + } + + /** + * This function handles the calendar-query REPORT + * + * This report is used by clients to request calendar objects based on + * complex conditions. + * + * @param Xml\Request\CalendarSearchReport $report + * @return void + */ + private function calendarSearch($report) { + $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); + $depth = $this->server->getHTTPDepth(0); + + // The default result is an empty array + $result = []; + + // If we're dealing with the calendar home, the calendar home itself is + // responsible for the calendar-query + if ($node instanceof CalendarHome && $depth == 2) { + + $nodePaths = $node->calendarSearch($report->filters, $report->limit, $report->offset); + + foreach ($nodePaths as $path) { + list($properties) = $this->server->getPropertiesForPath( + $this->server->getRequestUri() . '/' . $path, + $report->properties); + $result[] = $properties; + } + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', + 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody( + $this->server->generateMultiStatus($result, + $prefer['return'] === 'minimal')); + } +} \ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php new file mode 100644 index 0000000000..a2a3c910f0 --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php @@ -0,0 +1,47 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search\Xml\Filter; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use OCA\DAV\CalDAV\Search\SearchPlugin; + +class CompFilter implements XmlDeserializable { + + /** + * @param Reader $reader + * @throws BadRequest + * @return string + */ + static function xmlDeserialize(Reader $reader) { + $att = $reader->parseAttributes(); + $componentName = $att['name']; + + $reader->parseInnerTree(); + + if (!is_string($componentName)) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}comp-filter requires a valid name attribute'); + } + + return $componentName; + } +} diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php new file mode 100644 index 0000000000..f5de78b539 --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php @@ -0,0 +1,43 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search\Xml\Filter; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use OCA\DAV\CalDAV\Search\SearchPlugin; + +class LimitFilter implements XmlDeserializable { + + /** + * @param Reader $reader + * @throws BadRequest + * @return int + */ + static function xmlDeserialize(Reader $reader) { + $value = $reader->parseInnerTree(); + if (!is_int($value) && !is_string($value)) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}limit has illegal value'); + } + + return intval($value); + } +} \ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php new file mode 100644 index 0000000000..7257e1e72f --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php @@ -0,0 +1,43 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search\Xml\Filter; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use OCA\DAV\CalDAV\Search\SearchPlugin; + +class OffsetFilter implements XmlDeserializable { + + /** + * @param Reader $reader + * @throws BadRequest + * @return int + */ + static function xmlDeserialize(Reader $reader) { + $value = $reader->parseInnerTree(); + if (!is_int($value) && !is_string($value)) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}offset has illegal value'); + } + + return intval($value); + } +} \ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php new file mode 100644 index 0000000000..1c443763da --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php @@ -0,0 +1,55 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search\Xml\Filter; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use OCA\DAV\CalDAV\Search\SearchPlugin; + +class ParamFilter implements XmlDeserializable { + + /** + * @param Reader $reader + * @throws BadRequest + * @return string + */ + static function xmlDeserialize(Reader $reader) { + $att = $reader->parseAttributes(); + $property = $att['property']; + $parameter = $att['name']; + + $reader->parseInnerTree(); + + if (!is_string($property)) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}param-filter requires a valid property attribute'); + + } + if (!is_string($parameter)) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}param-filter requires a valid parameter attribute'); + } + + return [ + 'property' => $property, + 'parameter' => $parameter, + ]; + } +} diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php new file mode 100644 index 0000000000..06b41d91c6 --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php @@ -0,0 +1,47 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search\Xml\Filter; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use OCA\DAV\CalDAV\Search\SearchPlugin; + +class PropFilter implements XmlDeserializable { + + /** + * @param Reader $reader + * @throws BadRequest + * @return string + */ + static function xmlDeserialize(Reader $reader) { + $att = $reader->parseAttributes(); + $componentName = $att['name']; + + $reader->parseInnerTree(); + + if (!is_string($componentName)) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}prop-filter requires a valid name attribute'); + } + + return $componentName; + } +} diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php new file mode 100644 index 0000000000..8779e2b394 --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php @@ -0,0 +1,43 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search\Xml\Filter; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use OCA\DAV\CalDAV\Search\SearchPlugin; + +class SearchTermFilter implements XmlDeserializable { + + /** + * @param Reader $reader + * @throws BadRequest + * @return string + */ + static function xmlDeserialize(Reader $reader) { + $value = $reader->parseInnerTree(); + if (!is_string($value)) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}search-term has illegal value'); + } + + return $value; + } +} \ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php new file mode 100644 index 0000000000..cda5d6dcc2 --- /dev/null +++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php @@ -0,0 +1,163 @@ + + * + * @copyright Copyright (c) 2017 Georg Ehrke + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\DAV\CalDAV\Search\Xml\Request; + +use Sabre\CalDAV\Plugin; +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use OCA\DAV\CalDAV\Search\SearchPlugin; + +/** + * CalendarSearchReport request parser. + * + * This class parses the {urn:ietf:params:xml:ns:caldav}calendar-query + * REPORT, as defined in: + * + * https:// link to standard + */ +class CalendarSearchReport implements XmlDeserializable { + + /** + * An array with requested properties. + * + * @var array + */ + public $properties; + + /** + * List of property/component filters. + * + * @var array + */ + public $filters; + + /** + * @var int + */ + public $limit; + + /** + * @var int + */ + public $offset; + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + $elems = $reader->parseInnerTree([ + '{http://nextcloud.com/ns}comp-filter' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter', + '{http://nextcloud.com/ns}prop-filter' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter', + '{http://nextcloud.com/ns}param-filter' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\ParamFilter', + '{http://nextcloud.com/ns}search-term' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter', + '{http://nextcloud.com/ns}limit' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\LimitFilter', + '{http://nextcloud.com/ns}offset' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\OffsetFilter', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'filters' => [], + 'properties' => [], + 'limit' => null, + 'offset' => null + ]; + + if (!is_array($elems)) { + $elems = []; + } + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{' . Plugin::NS_CALDAV . '}calendar-data'])) { + $newProps += $elem['value']['{' . Plugin::NS_CALDAV . '}calendar-data']; + } + break; + case '{' . SearchPlugin::NS_Nextcloud . '}filter': + foreach ($elem['value'] as $subElem) { + if ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}comp-filter') { + if (!is_array($newProps['filters']['comps'])) { + $newProps['filters']['comps'] = []; + } + $newProps['filters']['comps'][] = $subElem['value']; + } elseif ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}prop-filter') { + if (!is_array($newProps['filters']['props'])) { + $newProps['filters']['props'] = []; + } + $newProps['filters']['props'][] = $subElem['value']; + } elseif ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}param-filter') { + if (!is_array($newProps['filters']['params'])) { + $newProps['filters']['params'] = []; + } + $newProps['filters']['params'][] = $subElem['value']; + } elseif ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}search-term') { + $newProps['filters']['search-term'] = $subElem['value']; + } + } + break; + case '{' . SearchPlugin::NS_Nextcloud . '}limit': + $newProps['limit'] = $elem['value']; + break; + case '{' . SearchPlugin::NS_Nextcloud . '}offset': + $newProps['offset'] = $elem['value']; + break; + + } + } + + if (empty($newProps['filters'])) { + throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}filter element is required for this request'); + } + + $propsOrParamsDefined = (!empty($newProps['filters']['props']) || !empty($newProps['filters'])); + $noCompsDefined = empty($newProps['filters']['comps']); + if ($propsOrParamsDefined && $noCompsDefined) { + throw new BadRequest('{' . SearchPlugin::NS_Nextcloud . '}prop-filter or {' . SearchPlugin::NS_Nextcloud . '}param-filter given without any {' . SearchPlugin::NS_Nextcloud . '}comp-filter'); + } + + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + return $obj; + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index e051747748..5b0715b0da 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -216,6 +216,7 @@ class Server { \OC::$server->getCommentsManager(), $userSession )); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Search\SearchPlugin()); if ($view !== null) { $this->server->addPlugin(new FilesReportPlugin( $this->server->tree,