Documentation

This commit is contained in:
Robin Appelman 2017-02-01 14:56:25 +01:00
commit 3f23e989f0
8 changed files with 152 additions and 21 deletions

58
README.md Normal file
View file

@ -0,0 +1,58 @@
# SearchDAV
A sabre/dav plugin to implement rfc5323 SEARCH
## Usage
The plugin implements the DAV specific parts of the rfc but leaves the actual search
implementation to the user of the plugin.
This is done by implementing the `\SearchDAV\Backend\ISearchBackend` interface and passing
it to the plugin during construction.
### Basic usage
```php
$server = new \Sabre\DAV\Server();
$server->addPlugin(new \SearchDAV\DAV\SearchPlugin(new MySearchBackend()));
$server->exec();
```
### Terms
The rfc uses the following terms to describe various part of search handling
- Search scope: the DAV resource that is being queries.
- Search arbiter: the end point to which the SEARCH request can be made.
Note that a single search arbiter can support searching in multiple scopes
- Search grammar: The type of search query that is supported by a scope
rfc5323 requires any implementation to at least implement "basicsearch" which is
also currently the only supported grammar in this plugin
- Search schema: Details on how to use a search grammar,
such as the supported properties that can be searched for
### ISearchBackend
The `ISearchBackend` defines the arbiter end point, which scopes are valid to query,
the search schema that is supported and implements the actual search.
For a full list of methods required and their description see [`ISearchBackend.php`](src/Backend/ISearchBackend.php)
### BasicSearch
The `BasicSearch` class defines the query that was made by the client and consists of four parts:
- select: the properties are requested.
- from: the scope(s) in which the search should be made.
- where: the filter parameters for the search.
- orderBy: how the search results should be ordered.
For further information about these elements see
[`BasicSearch.php`](src/XML/BasicSearch.php), [`Scope.php`](src/XML/Scope.php),
[`Operator.php`](src/XML/Operator.php) and [`Order.php`](src/XML/Order.php)

View file

@ -23,6 +23,7 @@ namespace SearchDAV\Backend;
use Sabre\DAV\INode; use Sabre\DAV\INode;
use SearchDAV\XML\BasicSearch; use SearchDAV\XML\BasicSearch;
use SearchDAV\XML\Scope;
interface ISearchBackend { interface ISearchBackend {
/** /**
@ -31,6 +32,11 @@ interface ISearchBackend {
* The search arbiter is the URI that the client will send it's SEARCH requests to * The search arbiter is the URI that the client will send it's SEARCH requests to
* Note that this is not required to be the same as the search scopes which determine what to search in * Note that this is not required to be the same as the search scopes which determine what to search in
* *
* The returned value should be a path relative the root of the dav server.
*
* For example, if you want to support SEARCH requests on `https://example.com/dav.php/search`
* with the sabre/dav server listening on `/dav.php` you should return `search` as arbiter path.
*
* @return string * @return string
*/ */
public function getArbiterPath(); public function getArbiterPath();
@ -38,23 +44,43 @@ interface ISearchBackend {
/** /**
* Whether or not the search backend supports search requests on this scope * Whether or not the search backend supports search requests on this scope
* *
* @param string $href * The scope defines the resource that it being searched, such as a folder or address book.
* @param string|integer $depth 0, 1 or 'inifinite' *
* Note that a search arbiter has no inherit limitations on which scopes it can support and scopes
* that reside on a different dav server entirely might be considered valid by an implementation.
*
* One example use case for this would be a service that provides additional indexing on a 3rd party service.
*
* @param string $href an absolute uri of the search scope
* @param string|integer $depth 0, 1 or 'infinite'
* @param string|null $path the path of the search scope relative to the dav server, or null if the scope is outside the dav server
* @return bool * @return bool
*/ */
public function isValidScope($href, $depth); public function isValidScope($href, $depth, $path);
/** /**
* List the available properties that can be used in search * List the available properties that can be used in search
* *
* This is used to tell the search client what properties can be queried, used to filter and used to sort.
*
* Since sabre's PropFind handling mechanism is used to return the properties to the client, it's required that all
* properties which are listed as selectable have a PropFind handler set.
*
* @param string $href an absolute uri of the search scope
* @param string|null $path the path of the search scope relative to the dav server, or null if the scope is outside the dav server
* @return SearchPropertyDefinition[] * @return SearchPropertyDefinition[]
*/ */
public function getPropertyDefinitionsForScope($href); public function getPropertyDefinitionsForScope($href, $path);
/** /**
* @param INode $searchNode the DAV node that the search request was made to * Preform the search request
*
* The search results consist of the uri for the found resource and an INode describing the resource
* To return the properties requested by the query sabre's existing PropFind method is used, thus the search implementation
* is not required to collect these properties and is free to ignore the `select` part of the query
*
* @param BasicSearch $query * @param BasicSearch $query
* @return SearchResult[] * @return SearchResult[]
*/ */
public function search(INode $searchNode, BasicSearch $query); public function search(BasicSearch $query);
} }

View file

@ -54,9 +54,10 @@ class QueryParser extends Service {
}, },
'{DAV:}scope' => Scope::class, '{DAV:}scope' => Scope::class,
'{DAV:}where' => function (Reader $reader) { '{DAV:}where' => function (Reader $reader) {
return array_map(function ($element) { $operators = array_map(function ($element) {
return $element['value']; return $element['value'];
}, $reader->parseGetElements()); }, $reader->parseGetElements());
return (isset($operators[0])) ? $operators[0] : null;
}, },
'{DAV:}prop' => Element\Elements::class, '{DAV:}prop' => Element\Elements::class,
'{DAV:}order' => Order::class, '{DAV:}order' => Order::class,

View file

@ -22,6 +22,7 @@
namespace SearchDAV\DAV; namespace SearchDAV\DAV;
use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\INode; use Sabre\DAV\INode;
use Sabre\DAV\Node; use Sabre\DAV\Node;
use Sabre\DAV\PropFind; use Sabre\DAV\PropFind;
@ -70,6 +71,14 @@ class SearchPlugin extends ServerPlugin {
} }
} }
private function getPathFromUri($uri) {
try {
return ($uri === '' && $this->server->getBaseUri() === '/') ? '' : $this->server->calculateUri($uri);
} catch (Forbidden $e) {
return null;
}
}
/** /**
* SEARCH is allowed for users files * SEARCH is allowed for users files
* *
@ -77,7 +86,7 @@ class SearchPlugin extends ServerPlugin {
* @return array * @return array
*/ */
public function getHTTPMethods($uri) { public function getHTTPMethods($uri) {
$path = ($uri === '' && $this->server->getBaseUri() === '/') ? '' : $this->server->calculateUri($uri); $path = $this->getPathFromUri($uri);
if ($this->searchBackend->getArbiterPath() === $path) { if ($this->searchBackend->getArbiterPath() === $path) {
return ['SEARCH']; return ['SEARCH'];
} else { } else {
@ -112,10 +121,9 @@ class SearchPlugin extends ServerPlugin {
} }
/** @var BasicSearch $query */ /** @var BasicSearch $query */
$query = $xml['{DAV:}basicsearch']; $query = $xml['{DAV:}basicsearch'];
$node = $this->server->tree->getNodeForPath($request->getPath());
$response->setStatus(207); $response->setStatus(207);
$response->setHeader('Content-Type', 'application/xml; charset="utf-8"'); $response->setHeader('Content-Type', 'application/xml; charset="utf-8"');
$results = $this->searchBackend->search($node, $query); $results = $this->searchBackend->search($query);
$data = $this->server->generateMultiStatus($this->getPropertiesIteratorResults($results, $query->select), false); $data = $this->server->generateMultiStatus($this->getPropertiesIteratorResults($results, $query->select), false);
$response->setBody($data); $response->setBody($data);
return false; return false;
@ -127,8 +135,9 @@ class SearchPlugin extends ServerPlugin {
$query = $xml['{DAV:}basicsearch']; $query = $xml['{DAV:}basicsearch'];
$scopes = $query->from; $scopes = $query->from;
$results = array_map(function (Scope $scope) { $results = array_map(function (Scope $scope) {
if ($this->searchBackend->isValidScope($scope->href, $scope->depth)) { $scopePath = $this->getPathFromUri($scope->href);
$searchProperties = $this->searchBackend->getPropertyDefinitionsForScope($scope->href); if ($this->searchBackend->isValidScope($scope->href, $scope->depth, $scopePath)) {
$searchProperties = $this->searchBackend->getPropertyDefinitionsForScope($scope->href, $scopePath);
$searchSchema = $this->getBasicSearchForProperties($searchProperties); $searchSchema = $this->getBasicSearchForProperties($searchProperties);
return new QueryDiscoverResponse($scope->href, $searchSchema, 200); return new QueryDiscoverResponse($scope->href, $searchSchema, 200);
} else { } else {

View file

@ -24,11 +24,14 @@ namespace SearchDAV\XML;
use Sabre\Xml\Reader; use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
/**
* The object representation of a search query made by the client
*/
class BasicSearch implements XmlDeserializable { class BasicSearch implements XmlDeserializable {
/** /**
* @var string[] * @var string[]
* *
* The list of properties to be selected in clark notation * The list of properties to be selected, specified in clark notation
*/ */
public $select; public $select;
/** /**
@ -38,11 +41,20 @@ class BasicSearch implements XmlDeserializable {
*/ */
public $from; public $from;
/** /**
* @var Operator[] * @var Operator
*
* The search operator, either a comparison ('gt', 'eq', ...) or a boolean operator ('and', 'or', 'not')
*/ */
public $where; public $where;
/** /**
* @var Order[] * @var Order[]
*
* The list of order operations that should be used to order the results.
*
* Each order operations consists of a property to sort on and a sort direction.
* If more then one order operations are specified, the comparisons for ordering should
* be applied in the order that the order operations are defined in with the earlier comparisons being
* more significant.
*/ */
public $orderBy; public $orderBy;

View file

@ -53,6 +53,8 @@ class Operator implements XmlDeserializable {
* - string: property name for comparison * - string: property name for comparison
* - Literal: literal value for comparison * - Literal: literal value for comparison
* - Operation: nested operation for and/or/not operations * - Operation: nested operation for and/or/not operations
*
* Which type and what number of argument an Operator takes depends on the operator type.
*/ */
public $arguments; public $arguments;

View file

@ -29,7 +29,17 @@ class Order implements XmlDeserializable {
const ASC = 'ascending'; const ASC = 'ascending';
const DESC = 'descending'; const DESC = 'descending';
public $properties; /**
* @var string
*
* The property that should be sorted on.
*/
public $property;
/**
* @var string 'ascending' or 'descending'
*
* The sort direction
*/
public $order; public $order;
static function xmlDeserialize(Reader $reader) { static function xmlDeserialize(Reader $reader) {
@ -38,7 +48,7 @@ class Order implements XmlDeserializable {
$childs = \Sabre\Xml\Deserializer\keyValue($reader); $childs = \Sabre\Xml\Deserializer\keyValue($reader);
$order->order = isset($childs['{DAV:}descending']) ? self::DESC : self::ASC; $order->order = isset($childs['{DAV:}descending']) ? self::DESC : self::ASC;
$order->properties = $childs['{DAV:}prop']; $order->property = $childs['{DAV:}prop'][0];
return $order; return $order;
} }

View file

@ -26,7 +26,20 @@ use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
class Scope implements XmlDeserializable { class Scope implements XmlDeserializable {
/**
* @var string
*
* The absolute url of the search scope
*/
public $href; public $href;
/**
* @var string|int 0, 1 or 'infinite'
*
* How deep the search query should be with 0 being only the scope itself,
* 1 being all direct child entries of the scope and infinite being all entries
* in the scope collection at any depth.
*/
public $depth; public $depth;
static function xmlDeserialize(Reader $reader) { static function xmlDeserialize(Reader $reader) {