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 SearchDAV\XML\BasicSearch;
use SearchDAV\XML\Scope;
interface ISearchBackend {
/**
@ -31,6 +32,11 @@ interface ISearchBackend {
* 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
*
* 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
*/
public function getArbiterPath();
@ -38,23 +44,43 @@ interface ISearchBackend {
/**
* Whether or not the search backend supports search requests on this scope
*
* @param string $href
* @param string|integer $depth 0, 1 or 'inifinite'
* The scope defines the resource that it being searched, such as a folder or address book.
*
* 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
*/
public function isValidScope($href, $depth);
public function isValidScope($href, $depth, $path);
/**
* 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[]
*/
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
* @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:}where' => function (Reader $reader) {
return array_map(function ($element) {
$operators = array_map(function ($element) {
return $element['value'];
}, $reader->parseGetElements());
return (isset($operators[0])) ? $operators[0] : null;
},
'{DAV:}prop' => Element\Elements::class,
'{DAV:}order' => Order::class,

View file

@ -22,6 +22,7 @@
namespace SearchDAV\DAV;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\INode;
use Sabre\DAV\Node;
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
*
@ -77,7 +86,7 @@ class SearchPlugin extends ServerPlugin {
* @return array
*/
public function getHTTPMethods($uri) {
$path = ($uri === '' && $this->server->getBaseUri() === '/') ? '' : $this->server->calculateUri($uri);
$path = $this->getPathFromUri($uri);
if ($this->searchBackend->getArbiterPath() === $path) {
return ['SEARCH'];
} else {
@ -112,10 +121,9 @@ class SearchPlugin extends ServerPlugin {
}
/** @var BasicSearch $query */
$query = $xml['{DAV:}basicsearch'];
$node = $this->server->tree->getNodeForPath($request->getPath());
$response->setStatus(207);
$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);
$response->setBody($data);
return false;
@ -127,8 +135,9 @@ class SearchPlugin extends ServerPlugin {
$query = $xml['{DAV:}basicsearch'];
$scopes = $query->from;
$results = array_map(function (Scope $scope) {
if ($this->searchBackend->isValidScope($scope->href, $scope->depth)) {
$searchProperties = $this->searchBackend->getPropertyDefinitionsForScope($scope->href);
$scopePath = $this->getPathFromUri($scope->href);
if ($this->searchBackend->isValidScope($scope->href, $scope->depth, $scopePath)) {
$searchProperties = $this->searchBackend->getPropertyDefinitionsForScope($scope->href, $scopePath);
$searchSchema = $this->getBasicSearchForProperties($searchProperties);
return new QueryDiscoverResponse($scope->href, $searchSchema, 200);
} else {

View file

@ -24,11 +24,14 @@ namespace SearchDAV\XML;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* The object representation of a search query made by the client
*/
class BasicSearch implements XmlDeserializable {
/**
* @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;
/**
@ -38,11 +41,20 @@ class BasicSearch implements XmlDeserializable {
*/
public $from;
/**
* @var Operator[]
* @var Operator
*
* The search operator, either a comparison ('gt', 'eq', ...) or a boolean operator ('and', 'or', 'not')
*/
public $where;
/**
* @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;

View file

@ -53,6 +53,8 @@ class Operator implements XmlDeserializable {
* - string: property name for comparison
* - Literal: literal value for comparison
* - 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;

View file

@ -29,7 +29,17 @@ class Order implements XmlDeserializable {
const ASC = 'ascending';
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;
static function xmlDeserialize(Reader $reader) {
@ -38,7 +48,7 @@ class Order implements XmlDeserializable {
$childs = \Sabre\Xml\Deserializer\keyValue($reader);
$order->order = isset($childs['{DAV:}descending']) ? self::DESC : self::ASC;
$order->properties = $childs['{DAV:}prop'];
$order->property = $childs['{DAV:}prop'][0];
return $order;
}

View file

@ -26,7 +26,20 @@ use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class Scope implements XmlDeserializable {
/**
* @var string
*
* The absolute url of the search scope
*/
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;
static function xmlDeserialize(Reader $reader) {