From 57b543a9188f84031d81713117f3df515ab13e84 Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Sat, 25 Mar 2017 11:56:40 +0100 Subject: [PATCH 1/8] add Nextcloud Search extension to CalDAV Signed-off-by: Georg Ehrke --- apps/dav/appinfo/database.xml | 72 +++++ apps/dav/appinfo/info.xml | 2 +- apps/dav/lib/CalDAV/CalDavBackend.php | 278 ++++++++++++++++++ apps/dav/lib/CalDAV/CalendarHome.php | 10 + .../CalDAV/Search/CalendarSearchValidator.php | 102 +++++++ apps/dav/lib/CalDAV/Search/SearchPlugin.php | 159 ++++++++++ .../CalDAV/Search/Xml/Filter/CompFilter.php | 47 +++ .../CalDAV/Search/Xml/Filter/LimitFilter.php | 43 +++ .../CalDAV/Search/Xml/Filter/OffsetFilter.php | 43 +++ .../CalDAV/Search/Xml/Filter/ParamFilter.php | 55 ++++ .../CalDAV/Search/Xml/Filter/PropFilter.php | 47 +++ .../Search/Xml/Filter/SearchTermFilter.php | 43 +++ .../Xml/Request/CalendarSearchReport.php | 163 ++++++++++ apps/dav/lib/Server.php | 1 + 14 files changed, 1064 insertions(+), 1 deletion(-) create mode 100644 apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php create mode 100644 apps/dav/lib/CalDAV/Search/SearchPlugin.php create mode 100644 apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php create mode 100644 apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php create mode 100644 apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php create mode 100644 apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php create mode 100644 apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php create mode 100644 apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php create mode 100644 apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index 0efd2a46d61..de87f02e8f8 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 0902c487247..684feb89c2d 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 a4fed4f1982..c59a2dcd147 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 7320754e6df..2aa2c9caa36 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 00000000000..7e52e84ebdd --- /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 00000000000..ad36f39e234 --- /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 00000000000..a2a3c910f05 --- /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 00000000000..f5de78b539c --- /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 00000000000..7257e1e72f9 --- /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 00000000000..1c443763daf --- /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 00000000000..06b41d91c6e --- /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 00000000000..8779e2b3940 --- /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 00000000000..cda5d6dcc25 --- /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 e0517477488..5b0715b0dad 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, From e760cda96f2f13babaafe7d2eaf3c1cc8554f38c Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Mon, 24 Apr 2017 01:19:38 +0200 Subject: [PATCH 2/8] remove unused CalendarSearchValidator Signed-off-by: Georg Ehrke --- apps/dav/lib/CalDAV/CalDavBackend.php | 20 ---- .../CalDAV/Search/CalendarSearchValidator.php | 102 ------------------ 2 files changed, 122 deletions(-) delete mode 100644 apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index c59a2dcd147..4a652b16054 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1306,26 +1306,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 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. diff --git a/apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php b/apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php deleted file mode 100644 index 7e52e84ebdd..00000000000 --- a/apps/dav/lib/CalDAV/Search/CalendarSearchValidator.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * @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; - } -} From 40eec1e63c65c3a506ff88bdaf4cf611776ee034 Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Tue, 25 Apr 2017 11:55:31 +0200 Subject: [PATCH 3/8] add repairstep with backgroundjob to index calendar data Signed-off-by: Georg Ehrke --- apps/dav/appinfo/info.xml | 1 + apps/dav/lib/CalDAV/CalDavBackend.php | 2 +- .../Migration/BuildCalendarSearchIndex.php | 86 +++++++++++++ .../BuildCalendarSearchIndexBackgroundJob.php | 120 ++++++++++++++++++ 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 apps/dav/lib/Migration/BuildCalendarSearchIndex.php create mode 100644 apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 684feb89c2d..2d9f73b3f43 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -24,6 +24,7 @@ OCA\DAV\Migration\FixBirthdayCalendarComponent OCA\DAV\Migration\CalDAVRemoveEmptyValue + OCA\DAV\Migration\BuildCalendarSearchIndex diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 4a652b16054..40155341e51 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1968,7 +1968,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param string $objectUri * @param string $calendarData */ - protected function updateProperties($calendarId, $objectUri, $calendarData) { + public function updateProperties($calendarId, $objectUri, $calendarData) { $objectId = $this->getCalendarObjectId($calendarId, $objectUri); $vCalendar = $this->readCalendarData($calendarData); diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndex.php b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php new file mode 100644 index 00000000000..da4b4f4fe84 --- /dev/null +++ b/apps/dav/lib/Migration/BuildCalendarSearchIndex.php @@ -0,0 +1,86 @@ + + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ +namespace OCA\DAV\Migration; + +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class BuildCalendarSearchIndex implements IRepairStep { + + /** @var IDBConnection */ + private $db; + + /** @var IJobList */ + private $jobList; + + /** @var IConfig */ + private $config; + + /** + * @param IDBConnection $db + * @param IJobList $jobList + * @param IConfig $config + */ + public function __construct(IDBConnection $db, + IJobList $jobList, + IConfig $config) { + $this->db = $db; + $this->jobList = $jobList; + $this->config = $config; + } + + /** + * @return string + */ + public function getName() { + return 'Registering building of calendar search index as background job'; + } + + /** + * @param IOutput $output + */ + public function run(IOutput $output) { + // only run once + if ($this->config->getAppValue('dav', 'buildCalendarSearchIndex') === 'yes') { + $output->info('Repair step already executed'); + return; + } + + $query = $this->db->getQueryBuilder(); + $query->select($query->createFunction('MAX(id)')) + ->from('calendarobjects'); + $maxId = (int)$query->execute()->fetchColumn(); + + $output->info('Add background job'); + $this->jobList->add(BuildCalendarSearchIndexBackgroundJob::class, [ + 'offset' => 0, + 'stopAt' => $maxId + ]); + + // if all were done, no need to redo the repair during next upgrade + $this->config->setAppValue('dav', 'buildCalendarSearchIndex', 'yes'); + } +} \ No newline at end of file diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php new file mode 100644 index 00000000000..c80289c1b30 --- /dev/null +++ b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php @@ -0,0 +1,120 @@ + + * + * @author Georg Ehrke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ +namespace OCA\DAV\Migration; + +use OC\BackgroundJob\QueuedJob; +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IDBConnection; +use OCP\ILogger; + +class BuildCalendarSearchIndexBackgroundJob extends QueuedJob { + + /** @var IDBConnection */ + private $db; + + /** @var CalDavBackend */ + private $calDavBackend; + + /** @var ILogger */ + private $logger; + + /** @var IJobList */ + private $jobList; + + /** @var ITimeFactory */ + private $timeFactory; + + /** + * @param IDBConnection $db + * @param CalDavBackend $calDavBackend + * @param ILogger $logger + * @param IJobList $jobList + * @param ITimeFactory $timeFactory + */ + public function __construct(IDBConnection $db, + CalDavBackend $calDavBackend, + ILogger $logger, + IJobList $jobList, + ITimeFactory $timeFactory) { + $this->db = $db; + $this->calDavBackend = $calDavBackend; + $this->logger = $logger; + $this->jobList = $jobList; + $this->timeFactory = $timeFactory; + } + + public function run($arguments) { + $offset = $arguments['offset']; + $stopAt = $arguments['stopAt']; + + $this->logger->info('Building calendar index (' . $offset .'/' . $stopAt . ')'); + + $offset = $this->buildIndex($offset, $stopAt); + + if ($offset >= $stopAt) { + $this->logger->info('Building calendar index done'); + } else { + $this->jobList->add(self::class, [ + 'offset' => $offset, + 'stopAt' => $stopAt + ]); + $this->logger->info('New building calendar index job scheduled with offset ' . $offset); + } + } + + /** + * @param int $offset + * @param int $stopAt + * @return int + */ + private function buildIndex($offset, $stopAt) { + $startTime = $this->timeFactory->getTime(); + + $query = $this->db->getQueryBuilder(); + $query->select(['id', 'calendarid', 'objecturi', 'calendardata']) + ->from('calendarobjects') + ->where($query->expr()->lte('id', $query->createNamedParameter($stopAt))) + ->andWhere($query->expr()->gt('id', $query->createNamedParameter($offset))) + ->orderBy('id', 'ASC'); + + $stmt = $query->execute(); + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $offset = $row['id']; + + $calendarData = $row['calendardata']; + if (is_resource($calendarData)) { + $calendarData = stream_get_contents($calendarData); + } + + $this->calDavBackend->updateProperties($row['calendarid'], $row['uri'], $calendarData); + + if (($this->timeFactory->getTime() - $startTime) > 15) { + return $offset; + } + } + + return $stopAt; + } +} From dd424fcb7b26745c6bebc323df282509c28bd563 Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Tue, 25 Apr 2017 16:37:13 +0200 Subject: [PATCH 4/8] unit test CalDAV Search Plugin Signed-off-by: Georg Ehrke --- apps/dav/lib/CalDAV/Search/SearchPlugin.php | 6 +- .../Xml/Request/CalendarSearchReport.php | 14 +- .../Request/CalendarSearchReportTest.php | 335 ++++++++++++++++++ .../unit/CalDAV/Search/SearchPluginTest.php | 124 +++++++ 4 files changed, 469 insertions(+), 10 deletions(-) create mode 100644 apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php create mode 100644 apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php diff --git a/apps/dav/lib/CalDAV/Search/SearchPlugin.php b/apps/dav/lib/CalDAV/Search/SearchPlugin.php index ad36f39e234..d658a50437d 100644 --- a/apps/dav/lib/CalDAV/Search/SearchPlugin.php +++ b/apps/dav/lib/CalDAV/Search/SearchPlugin.php @@ -86,9 +86,9 @@ class SearchPlugin extends ServerPlugin { * @param mixed $path * @return bool */ - function report($reportName, $report, $path) { + public function report($reportName, $report, $path) { switch ($reportName) { - case '{' . self::NS_Nextcloud . '}calendar-search' : + case '{' . self::NS_Nextcloud . '}calendar-search': $this->server->transactionType = 'report-nc-calendar-search'; $this->calendarSearch($report); return false; @@ -127,7 +127,7 @@ class SearchPlugin extends ServerPlugin { */ private function calendarSearch($report) { $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); - $depth = $this->server->getHTTPDepth(0); + $depth = $this->server->getHTTPDepth(2); // The default result is an empty array $result = []; diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php index cda5d6dcc25..2c92a41e16b 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php @@ -20,7 +20,6 @@ */ namespace OCA\DAV\CalDAV\Search\Xml\Request; -use Sabre\CalDAV\Plugin; use Sabre\DAV\Exception\BadRequest; use Sabre\Xml\Reader; use Sabre\Xml\XmlDeserializable; @@ -107,24 +106,21 @@ class CalendarSearchReport implements XmlDeserializable { 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'])) { + if (!isset($newProps['filters']['comps']) || !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'])) { + if (!isset($newProps['filters']['props']) || !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'])) { + if (!isset($newProps['filters']['params']) || !is_array($newProps['filters']['params'])) { $newProps['filters']['params'] = []; } $newProps['filters']['params'][] = $subElem['value']; @@ -153,6 +149,10 @@ class CalendarSearchReport implements XmlDeserializable { throw new BadRequest('{' . SearchPlugin::NS_Nextcloud . '}prop-filter or {' . SearchPlugin::NS_Nextcloud . '}param-filter given without any {' . SearchPlugin::NS_Nextcloud . '}comp-filter'); } + if (!isset($newProps['filters']['search-term'])) { + throw new BadRequest('{' . SearchPlugin::NS_Nextcloud . '}search-term is required for this request'); + } + $obj = new self(); foreach ($newProps as $key => $value) { diff --git a/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php new file mode 100644 index 00000000000..15c776db07f --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php @@ -0,0 +1,335 @@ + + * + * @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\Tests\unit\CalDAV\Search\Xml\Request; + +use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport; +use Sabre\Xml\Reader; +use Test\TestCase; + +class CalendarSearchReportTest extends TestCase { + + private $elementMap = [ + '{http://nextcloud.com/ns}calendar-search' => + 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport', + ]; + + public function testFoo() { + $xml = << + + + + + + + + + + + + + foo + + 10 + 5 + +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION', + 'ATTENDEE' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = 10; + $calendarSearchReport->offset = 5; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + public function testNoLimitOffset() { + $xml = << + + + + + + + + + foo + + +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + ], + 'props' => [ + 'SUMMARY', + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + * @expectedExceptionMessage {http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter given without any {http://nextcloud.com/ns}comp-filter + */ + public function testRequiresCompFilter() { + $xml = << + + + + + + + + + + + foo + + 10 + 5 + +XML; + + $this->parse($xml); + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + * @expectedExceptionMessage The {http://nextcloud.com/ns}filter element is required for this request + */ + public function testRequiresFilter() { + $xml = << + + + + + + +XML; + + $this->parse($xml); + } + + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + * @expectedExceptionMessage {http://nextcloud.com/ns}search-term is required for this request + */ + public function testNoSearchTerm() { + $xml = << + + + + + + + + + + + + + + 10 + 5 + +XML; + + $result = $this->parse($xml); + } + + public function testCompOnly() { + $xml = << + + + + + + + + + foo + + +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + public function testPropOnly() { + $xml = << + + + + + + + + + foo + + +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + ], + 'props' => [ + 'SUMMARY', + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + public function testParamOnly() { + $xml = << + + + + + + + + + foo + + +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + private function parse($xml, array $elementMap = []) { + $reader = new Reader(); + $reader->elementMap = array_merge($this->elementMap, $elementMap); + $reader->xml($xml); + return $reader->parse(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php new file mode 100644 index 00000000000..fc647bb5daf --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php @@ -0,0 +1,124 @@ + + * + * @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\Tests\unit\CalDAV\Search; + +use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\Search\SearchPlugin; +use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport; +use Test\TestCase; + +class SearchPluginTest extends TestCase { + + protected $server; + + /** @var \OCA\DAV\CalDAV\Search\SearchPlugin $plugin */ + protected $plugin; + + public function setUp() { + parent::setUp(); + + $this->server = $this->createMock(\Sabre\DAV\Server::class); + $this->server->tree = $this->createMock(\Sabre\DAV\Tree::class); + $this->server->httpResponse = $this->createMock(\Sabre\HTTP\Response::class); + + $this->plugin = new SearchPlugin(); + $this->plugin->initialize($this->server); + } + + public function testGetFeatures() { + $this->assertEquals(['nc-calendar-search'], $this->plugin->getFeatures()); + } + + public function testGetName() { + $this->assertEquals('nc-calendar-search', $this->plugin->getPluginName()); + } + + public function testInitialize() { + $server = $this->createMock(\Sabre\DAV\Server::class); + + $plugin = new SearchPlugin(); + + $server->expects($this->at(0)) + ->method('on') + ->with('report', [$plugin, 'report']); + + $plugin->initialize($server); + + $this->assertEquals( + $server->xml->elementMap['{http://nextcloud.com/ns}calendar-search'], + 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' + ); + } + + public function testReportUnknown() { + $result = $this->plugin->report('{urn:ietf:params:xml:ns:caldav}calendar-query', 'REPORT', null); + $this->assertEquals($result, null); + $this->assertNotEquals($this->server->transactionType, 'report-nc-calendar-search'); + } + + public function testReport() { + $report = $this->createMock(CalendarSearchReport::class); + $report->filters = []; + $calendarHome = $this->createMock(CalendarHome::class); + $this->server->expects($this->at(0)) + ->method('getRequestUri') + ->with() + ->will($this->returnValue('/re/quest/u/r/i')); + $this->server->tree->expects($this->at(0)) + ->method('getNodeForPath') + ->with('/re/quest/u/r/i') + ->will($this->returnValue($calendarHome)); + $this->server->expects($this->at(1)) + ->method('getHTTPDepth') + ->with(2) + ->will($this->returnValue(2)); + $calendarHome->expects($this->at(0)) + ->method('calendarSearch') + ->will($this->returnValue([])); + + $this->plugin->report('{http://nextcloud.com/ns}calendar-search', $report, ''); + } + + public function testSupportedReportSetNoCalendarHome() { + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/foo/bar') + ->will($this->returnValue(null)); + + $reports = $this->plugin->getSupportedReportSet('/foo/bar'); + $this->assertEquals([], $reports); + } + + public function testSupportedReportSet() { + $calendarHome = $this->createMock(CalendarHome::class); + + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/bar/foo') + ->will($this->returnValue($calendarHome)); + + $reports = $this->plugin->getSupportedReportSet('/bar/foo'); + $this->assertEquals([ + '{http://nextcloud.com/ns}calendar-search' + ], $reports); + } +} From ac3cc5211b4b3b5671afa2feefab15303270e25f Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Tue, 25 Apr 2017 16:42:41 +0200 Subject: [PATCH 5/8] updateProperties: catch exception when reading calendar data Signed-off-by: Georg Ehrke --- apps/dav/lib/CalDAV/CalDavBackend.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 40155341e51..80dc27c20d0 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1970,7 +1970,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ public function updateProperties($calendarId, $objectUri, $calendarData) { $objectId = $this->getCalendarObjectId($calendarId, $objectUri); - $vCalendar = $this->readCalendarData($calendarData); + + try { + $vCalendar = $this->readCalendarData($calendarData); + } catch (\Exception $ex) { + return; + } $this->purgeProperties($calendarId, $objectId); From c76633bb8aabff7f160d1b5cd0b7f2a259d009a7 Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Tue, 25 Apr 2017 18:20:32 +0200 Subject: [PATCH 6/8] require at least one param or prop filter element Signed-off-by: Georg Ehrke --- .../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php | 6 +++++- .../unit/CalDAV/Search/Request/CalendarSearchReportTest.php | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php index 2c92a41e16b..4d22f310c24 100644 --- a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php +++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php @@ -143,7 +143,7 @@ class CalendarSearchReport implements XmlDeserializable { throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}filter element is required for this request'); } - $propsOrParamsDefined = (!empty($newProps['filters']['props']) || !empty($newProps['filters'])); + $propsOrParamsDefined = (!empty($newProps['filters']['props']) || !empty($newProps['filters']['params'])); $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'); @@ -153,6 +153,10 @@ class CalendarSearchReport implements XmlDeserializable { throw new BadRequest('{' . SearchPlugin::NS_Nextcloud . '}search-term is required for this request'); } + if (empty($newProps['filters']['props']) && empty($newProps['filters']['params'])) { + throw new BadRequest('At least one{' . SearchPlugin::NS_Nextcloud . '}prop-filter or {' . SearchPlugin::NS_Nextcloud . '}param-filter is required for this request'); + } + $obj = new self(); foreach ($newProps as $key => $value) { diff --git a/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php index 15c776db07f..20bac8aa9f5 100644 --- a/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php +++ b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php @@ -202,6 +202,10 @@ XML; $result = $this->parse($xml); } + /** + * @expectedException \Sabre\DAV\Exception\BadRequest + * @expectedExceptionMessage At least one{http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter is required for this request + */ public function testCompOnly() { $xml = << From 8d00458b568185dfb405c79f38930bc4a64855ff Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Tue, 25 Apr 2017 19:26:47 +0200 Subject: [PATCH 7/8] unit test custom calendar search Signed-off-by: Georg Ehrke --- apps/dav/lib/CalDAV/CalDavBackend.php | 50 ++++--- .../BuildCalendarSearchIndexBackgroundJob.php | 2 +- .../tests/unit/CalDAV/CalDavBackendTest.php | 132 ++++++++++++++++++ 3 files changed, 162 insertions(+), 22 deletions(-) diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 80dc27c20d0..a2b7a2e83b2 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1,11 +1,13 @@ * @author Stefan Weil * @author Thomas Citharel * @author Thomas Müller + * @author Georg Ehrke * * @license AGPL-3.0 * @@ -1256,51 +1258,57 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions); } - $propExpressions = []; + if (!isset($filters['props'])) { + $filters['props'] = []; + } + if (!isset($filters['params'])) { + $filters['params'] = []; + } + + $propParamExpressions = []; foreach($filters['props'] as $prop) { - $propExpressions[] = $query->expr()->andX( + $propParamExpressions[] = $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( + $propParamExpressions[] = $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]; + if (count($propParamExpressions) === 1) { + $propParamExpr = $propParamExpressions[0]; } else { - $paramExpr = call_user_func_array([$query->expr(), 'orX'], $paramExpressions); + $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions); } - $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); + ->andWhere($propParamExpr) + ->andWhere($query->expr()->iLike('i.value', + $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))); + + if ($offset) { + $query->setFirstResult($offset); + } + if ($limit) { + $query->setMaxResults($limit); + } $stmt = $query->execute(); $result = []; while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - $result[] = $uriMapper[$row['calendarid']] . '/' . $row['uri']; + $path = $uriMapper[$row['calendarid']] . '/' . $row['uri']; + if (!in_array($path, $result)) { + $result[] = $path; + } } return $result; diff --git a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php index c80289c1b30..a4fa2c63e02 100644 --- a/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php +++ b/apps/dav/lib/Migration/BuildCalendarSearchIndexBackgroundJob.php @@ -93,7 +93,7 @@ class BuildCalendarSearchIndexBackgroundJob extends QueuedJob { $startTime = $this->timeFactory->getTime(); $query = $this->db->getQueryBuilder(); - $query->select(['id', 'calendarid', 'objecturi', 'calendardata']) + $query->select(['id', 'calendarid', 'uri', 'calendardata']) ->from('calendarobjects') ->where($query->expr()->lte('id', $query->createNamedParameter($stopAt))) ->andWhere($query->expr()->gt('id', $query->createNamedParameter($offset))) diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php index 5adda30c19d..dc531b5a64a 100644 --- a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php +++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php @@ -1,10 +1,12 @@ * @author Thomas Citharel * @author Thomas Müller + * @author Georg Ehrke * * @license AGPL-3.0 * @@ -489,4 +491,134 @@ EOD; 'unknown class -> private' => [CalDavBackend::CLASSIFICATION_PRIVATE, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:VERTRAULICH\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"], ]; } + + public function testCalendarSearch() { + $calendarId = $this->createTestCalendar(); + + $uri = static::getUniqueID('calobj'); + $calData = <<backend->createCalendarObject($calendarId, $uri, $calData); + + $search1 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'search-term' => 'Test', + ]); + $this->assertEquals(count($search1), 1); + + + // update the card + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:123 Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +ATTENDEE;CN=test:mailto:foo@bar.com +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->updateCalendarObject($calendarId, $uri, $calData); + + $search2 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'search-term' => 'Test', + ]); + $this->assertEquals(count($search2), 0); + + $search3 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 'Test', + ]); + $this->assertEquals(count($search3), 1); + + // t matches both summary and attendee's CN, but we want unique results + $search4 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 't', + ]); + $this->assertEquals(count($search4), 1); + + $this->backend->deleteCalendarObject($calendarId, $uri); + + $search5 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 't', + ]); + $this->assertEquals(count($search5), 0); + } } From 0f8a9514de445c47a79d18a3ca075295398b4c60 Mon Sep 17 00:00:00 2001 From: Georg Ehrke Date: Wed, 26 Apr 2017 10:06:10 +0200 Subject: [PATCH 8/8] rename calendarobjects_properties -> calendarobjects_props Signed-off-by: Georg Ehrke --- apps/dav/appinfo/database.xml | 2 +- apps/dav/lib/CalDAV/CalDavBackend.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index de87f02e8f8..b3a69de070c 100644 --- a/apps/dav/appinfo/database.xml +++ b/apps/dav/appinfo/database.xml @@ -671,7 +671,7 @@ CREATE TABLE calendarobjects (
- *dbprefix*calendarobjects_properties + *dbprefix*calendarobjects_props id diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index a2b7a2e83b2..fcf73cb0f31 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -149,7 +149,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription private $legacyEndpoint; /** @var string */ - private $dbObjectPropertiesTable = 'calendarobjects_properties'; + private $dbObjectPropertiesTable = 'calendarobjects_props'; /** * CalDavBackend constructor. @@ -1286,7 +1286,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } $query->select(['c.calendarid', 'c.uri']) - ->from('calendarobjects_properties', 'i') + ->from($this->dbObjectPropertiesTable, 'i') ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id')) ->where($calExpr) ->andWhere($compExpr)