add Nextcloud Search extension to CalDAVtags/v12.0.0beta1
@@ -670,6 +670,78 @@ CREATE TABLE calendarobjects ( | |||
</declaration> | |||
</table> | |||
<table> | |||
<name>*dbprefix*calendarobjects_props</name> | |||
<declaration> | |||
<field> | |||
<name>id</name> | |||
<type>integer</type> | |||
<default>0</default> | |||
<notnull>true</notnull> | |||
<autoincrement>1</autoincrement> | |||
<unsigned>true</unsigned> | |||
<length>11</length> | |||
</field> | |||
<field> | |||
<name>calendarid</name> | |||
<type>integer</type> | |||
<default></default> | |||
<notnull>true</notnull> | |||
<length>11</length> | |||
</field> | |||
<field> | |||
<name>objectid</name> | |||
<type>integer</type> | |||
<default></default> | |||
<notnull>true</notnull> | |||
<unsigned>true</unsigned> | |||
<length>11</length> | |||
</field> | |||
<field> | |||
<name>name</name> | |||
<type>text</type> | |||
<default></default> | |||
<notnull>false</notnull> | |||
<length>64</length> | |||
</field> | |||
<field> | |||
<name>parameter</name> | |||
<type>text</type> | |||
<default></default> | |||
<notnull>false</notnull> | |||
<length>64</length> | |||
</field> | |||
<field> | |||
<name>value</name> | |||
<type>text</type> | |||
<default></default> | |||
<notnull>false</notnull> | |||
<length>255</length> | |||
</field> | |||
<index> | |||
<name>calendarobject_index</name> | |||
<field> | |||
<name>objectid</name> | |||
<sorting>ascending</sorting> | |||
</field> | |||
</index> | |||
<index> | |||
<name>calendarobject_name_index</name> | |||
<field> | |||
<name>name</name> | |||
<sorting>ascending</sorting> | |||
</field> | |||
</index> | |||
<index> | |||
<name>calendarobject_value_index</name> | |||
<field> | |||
<name>value</name> | |||
<sorting>ascending</sorting> | |||
</field> | |||
</index> | |||
</declaration> | |||
</table> | |||
<table> | |||
<name>*dbprefix*dav_shares</name> | |||
<declaration> |
@@ -5,7 +5,7 @@ | |||
<description>WebDAV endpoint</description> | |||
<licence>AGPL</licence> | |||
<author>owncloud.org</author> | |||
<version>1.2.0</version> | |||
<version>1.3.0</version> | |||
<default_enable/> | |||
<types> | |||
<filesystem/> | |||
@@ -24,6 +24,7 @@ | |||
<post-migration> | |||
<step>OCA\DAV\Migration\FixBirthdayCalendarComponent</step> | |||
<step>OCA\DAV\Migration\CalDAVRemoveEmptyValue</step> | |||
<step>OCA\DAV\Migration\BuildCalendarSearchIndex</step> | |||
</post-migration> | |||
</repair-steps> | |||
<commands> |
@@ -1,11 +1,13 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2016, ownCloud, Inc. | |||
* @copyright Copyright (c) 2017 Georg Ehrke | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* @author Stefan Weil <sw@weilnetz.de> | |||
* @author Thomas Citharel <tcit@tcit.fr> | |||
* @author Thomas Müller <thomas.mueller@tmit.eu> | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @license AGPL-3.0 | |||
* | |||
@@ -44,6 +46,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 +111,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 +148,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription | |||
/** @var bool */ | |||
private $legacyEndpoint; | |||
/** @var string */ | |||
private $dbObjectPropertiesTable = 'calendarobjects_props'; | |||
/** | |||
* CalDavBackend constructor. | |||
* | |||
@@ -746,6 +763,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 +962,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 +1014,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 +1076,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 +1195,125 @@ 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); | |||
} | |||
if (!isset($filters['props'])) { | |||
$filters['props'] = []; | |||
} | |||
if (!isset($filters['params'])) { | |||
$filters['params'] = []; | |||
} | |||
$propParamExpressions = []; | |||
foreach($filters['props'] as $prop) { | |||
$propParamExpressions[] = $query->expr()->andX( | |||
$query->expr()->eq('i.name', $query->createNamedParameter($prop)), | |||
$query->expr()->isNull('i.parameter') | |||
); | |||
} | |||
foreach($filters['params'] as $param) { | |||
$propParamExpressions[] = $query->expr()->andX( | |||
$query->expr()->eq('i.name', $query->createNamedParameter($param['property'])), | |||
$query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter'])) | |||
); | |||
} | |||
if (count($propParamExpressions) === 1) { | |||
$propParamExpr = $propParamExpressions[0]; | |||
} else { | |||
$propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions); | |||
} | |||
$query->select(['c.calendarid', 'c.uri']) | |||
->from($this->dbObjectPropertiesTable, 'i') | |||
->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id')) | |||
->where($calExpr) | |||
->andWhere($compExpr) | |||
->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)) { | |||
$path = $uriMapper[$row['calendarid']] . '/' . $row['uri']; | |||
if (!in_array($path, $result)) { | |||
$result[] = $path; | |||
} | |||
} | |||
return $result; | |||
} | |||
/** | |||
* Searches through all of a users calendars and calendar objects to find | |||
* an object with a specific UID. | |||
@@ -1820,6 +1967,130 @@ 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 | |||
*/ | |||
public function updateProperties($calendarId, $objectUri, $calendarData) { | |||
$objectId = $this->getCalendarObjectId($calendarId, $objectUri); | |||
try { | |||
$vCalendar = $this->readCalendarData($calendarData); | |||
} catch (\Exception $ex) { | |||
return; | |||
} | |||
$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); |
@@ -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); | |||
} | |||
} |
@@ -0,0 +1,159 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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 | |||
*/ | |||
public 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(2); | |||
// 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')); | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |
@@ -0,0 +1,55 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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, | |||
]; | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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; | |||
} | |||
} |
@@ -0,0 +1,167 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
namespace OCA\DAV\CalDAV\Search\Xml\Request; | |||
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']); | |||
break; | |||
case '{' . SearchPlugin::NS_Nextcloud . '}filter': | |||
foreach ($elem['value'] as $subElem) { | |||
if ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}comp-filter') { | |||
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 (!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 (!isset($newProps['filters']['params']) || !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']['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'); | |||
} | |||
if (!isset($newProps['filters']['search-term'])) { | |||
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) { | |||
$obj->$key = $value; | |||
} | |||
return $obj; | |||
} | |||
} |
@@ -0,0 +1,86 @@ | |||
<?php | |||
/** | |||
* @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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'); | |||
} | |||
} |
@@ -0,0 +1,120 @@ | |||
<?php | |||
/** | |||
* @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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', 'uri', '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; | |||
} | |||
} |
@@ -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, |
@@ -1,10 +1,12 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2016, ownCloud, Inc. | |||
* @copyright Copyright (c) 2017 Georg Ehrke | |||
* | |||
* @author Joas Schilling <coding@schilljs.com> | |||
* @author Thomas Citharel <tcit@tcit.fr> | |||
* @author Thomas Müller <thomas.mueller@tmit.eu> | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @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 = <<<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:Test Event | |||
DTSTART;VALUE=DATE-TIME:20130912T130000Z | |||
DTEND;VALUE=DATE-TIME:20130912T140000Z | |||
CLASS:PUBLIC | |||
END:VEVENT | |||
END:VCALENDAR | |||
EOD; | |||
$this->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); | |||
} | |||
} |
@@ -0,0 +1,339 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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 = <<<XML | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
<nc:filter> | |||
<nc:comp-filter name="VEVENT" /> | |||
<nc:comp-filter name="VTODO" /> | |||
<nc:prop-filter name="SUMMARY" /> | |||
<nc:prop-filter name="LOCATION" /> | |||
<nc:prop-filter name="ATTENDEE" /> | |||
<nc:param-filter property="ATTENDEE" name="CN" /> | |||
<nc:search-term>foo</nc:search-term> | |||
</nc:filter> | |||
<nc:limit>10</nc:limit> | |||
<nc:offset>5</nc:offset> | |||
</nc:calendar-search> | |||
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 = <<<XML | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
<nc:filter> | |||
<nc:comp-filter name="VEVENT" /> | |||
<nc:prop-filter name="SUMMARY" /> | |||
<nc:search-term>foo</nc:search-term> | |||
</nc:filter> | |||
</nc:calendar-search> | |||
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 = <<<XML | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
<nc:filter> | |||
<nc:prop-filter name="SUMMARY" /> | |||
<nc:prop-filter name="LOCATION" /> | |||
<nc:prop-filter name="ATTENDEE" /> | |||
<nc:param-filter property="ATTENDEE" name="CN" /> | |||
<nc:search-term>foo</nc:search-term> | |||
</nc:filter> | |||
<nc:limit>10</nc:limit> | |||
<nc:offset>5</nc:offset> | |||
</nc:calendar-search> | |||
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 | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
</nc:calendar-search> | |||
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 = <<<XML | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
<nc:filter> | |||
<nc:comp-filter name="VEVENT" /> | |||
<nc:comp-filter name="VTODO" /> | |||
<nc:prop-filter name="SUMMARY" /> | |||
<nc:prop-filter name="LOCATION" /> | |||
<nc:prop-filter name="ATTENDEE" /> | |||
<nc:param-filter property="ATTENDEE" name="CN" /> | |||
</nc:filter> | |||
<nc:limit>10</nc:limit> | |||
<nc:offset>5</nc:offset> | |||
</nc:calendar-search> | |||
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 = <<<XML | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
<nc:filter> | |||
<nc:comp-filter name="VEVENT" /> | |||
<nc:comp-filter name="VTODO" /> | |||
<nc:search-term>foo</nc:search-term> | |||
</nc:filter> | |||
</nc:calendar-search> | |||
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 = <<<XML | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
<nc:filter> | |||
<nc:comp-filter name="VEVENT" /> | |||
<nc:prop-filter name="SUMMARY" /> | |||
<nc:search-term>foo</nc:search-term> | |||
</nc:filter> | |||
</nc:calendar-search> | |||
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 = <<<XML | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> | |||
<d:prop> | |||
<d:getetag /> | |||
<c:calendar-data /> | |||
</d:prop> | |||
<nc:filter> | |||
<nc:comp-filter name="VEVENT" /> | |||
<nc:param-filter property="ATTENDEE" name="CN" /> | |||
<nc:search-term>foo</nc:search-term> | |||
</nc:filter> | |||
</nc:calendar-search> | |||
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(); | |||
} | |||
} |
@@ -0,0 +1,124 @@ | |||
<?php | |||
/** | |||
* @author Georg Ehrke <oc.list@georgehrke.com> | |||
* | |||
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com> | |||
* @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 <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
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); | |||
} | |||
} |