Signed-off-by: Georg Ehrke <developer@georgehrke.com>tags/v12.0.0beta1
@@ -670,6 +670,78 @@ CREATE TABLE calendarobjects ( | |||
</declaration> | |||
</table> | |||
<table> | |||
<name>*dbprefix*calendarobjects_properties</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/> |
@@ -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); |
@@ -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,102 @@ | |||
<?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\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; | |||
} | |||
} |
@@ -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 | |||
*/ | |||
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')); | |||
} | |||
} |
@@ -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,163 @@ | |||
<?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\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; | |||
} | |||
} |
@@ -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, |