Compare commits

...

36 commits

Author SHA1 Message Date
3865288b69
Merge pull request #14 from nickvergessen/PHP-8.4
feat: Add PHP 8.4 support
2024-11-08 16:54:16 +01:00
Joas Schilling
2b4e9a4811
feat: Add PHP 8.4 support
Signed-off-by: Joas Schilling <coding@schilljs.com>
2024-11-08 12:13:26 +01:00
8339181df3
Merge pull request #13 from artonge/patch-1
Add handling for is-defined operator
2023-11-14 23:32:04 +01:00
Louis
168c5f9cc4
Add handling for is-defined operator 2023-11-14 17:40:44 +01:00
52c8cfc21b add alias for DATATYPE_NONNEGATIVE_INTEGER 2022-04-11 15:02:49 +02:00
acb32ab84d minor fixes 2022-04-11 14:53:38 +02:00
d3eb42cd90 more types 2022-04-08 17:49:15 +02:00
721b8825d8 comment 2022-04-08 17:33:30 +02:00
b47321646b add phpstan in ci 2022-04-08 17:29:52 +02:00
4d51484de3 fmt 2022-04-08 17:28:32 +02:00
3d85afde98 psalm fixes 2022-04-08 17:26:19 +02:00
1290acf675 lint/psalm 2022-04-08 17:17:26 +02:00
c5d79716d0 bumb ci versions 2022-04-08 17:10:58 +02:00
7bf414d47a
Merge pull request #12 from CarlSchwan/feat/allow-preloading
Allow preloading of properties in one go
2022-04-08 15:01:51 +00:00
Carl Schwan
9c1e52272c Allow preloading of properties in one go
Make it possible to not be affected by a N+1 issue when doing a search

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2022-04-08 16:17:25 +02:00
c69806d900 fix scrutinizer type warning 2020-02-06 14:56:26 +01:00
286f1527d4 scrutinizer config 2020-02-06 14:56:15 +01:00
199b246c2a add github actions ci 2020-02-06 14:46:18 +01:00
49c010f4d7 update sabredav and phpunit 2020-02-06 14:33:40 +01:00
946bc1bf69
Merge pull request #11 from kesselb/master
Load tests only on dev
2019-11-09 18:46:57 +00:00
Daniel Kesselberg
68d6c966a4 Load tests only on dev 2019-11-09 10:51:25 +01:00
9b78893f83
Merge pull request #10 from kesselb/master
Add test case for !$reader->read then break
2019-11-06 16:16:40 +00:00
Daniel Kesselberg
1ee911ba45 Add test case for !$reader->read then break 2019-11-06 17:01:22 +01:00
5ba2a3cc31 add some type hinting 2019-11-06 14:33:04 +01:00
76953efe15 update to phpunit 7 2019-11-06 14:28:07 +01:00
7174c02348 fix test 2019-11-06 14:15:26 +01:00
Naofumi
2f87fb84d3 add testSearchQueryInfiniteLoopEmptyLiteral(). 2019-11-06 14:09:12 +01:00
Naofumi
c880b7edcd Don't call readText() in an empty element, otherwise
all the subsequent elements are exhausted.
2019-11-06 14:08:56 +01:00
Naofumi
faf4ef87fd Fix a possible infinite loop. 2019-11-06 14:03:40 +01:00
a469e21c43
Merge pull request #8 from kesselb/fix/undefined-index
Prevent "Undefined index: 0" for where condition
2019-11-04 10:09:57 +00:00
ae858d47f5
Merge pull request #7 from kesselb/enh/contains-node
Fix parsing of {DAV:}contains element
2019-11-04 10:00:28 +00:00
Daniel Kesselberg
f9b08747fc Fix Undefined offset: 0 2019-11-03 15:38:25 +01:00
Daniel Kesselberg
bef5795c98 Fix parsing of {DAV:}contains element 2019-11-03 11:43:54 +01:00
9c24c70774 update php versions for travis 2019-02-20 18:03:32 +01:00
aeb7610589 "where" is optional 2019-02-20 18:03:32 +01:00
e8cf5e7644 strict typing and php 7.1 2019-02-20 18:03:32 +01:00
42 changed files with 716 additions and 232 deletions

21
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: CI
on: [push]
jobs:
build-test:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.4', '8.0', '8.4']
steps:
- uses: actions/checkout@v2
- name: Set up php${{ matrix.php-versons }}
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.php-versions }}
- name: Install dependencies
run: composer i
- name: PHPUnit
run: ./vendor/phpunit/phpunit/phpunit -c tests/phpunit.xml

41
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Php Lint
on: [push, pull_request]
jobs:
php-linters:
runs-on: ubuntu-20.04
strategy:
matrix:
php-versions: ['7.4', '8.0', '8.4']
name: php${{ matrix.php-versions }} lint
steps:
- name: Checkout
uses: actions/checkout@master
- name: Set up php${{ matrix.php-versons }}
uses: shivammathur/setup-php@master
with:
php-version: ${{ matrix.php-versions }}
coverage: none
extensions: inotify
- name: Install dependencies
run: composer i
- name: Lint
run: composer run lint
php-cs-fixer:
name: php-cs check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Set up php${{ matrix.php-versions }}
uses: shivammathur/setup-php@master
with:
php-version: 7.4
tools: composer:v1
coverage: none
extensions: inotify
- name: Install dependencies
run: composer i
- name: Run coding standards check
run: composer run cs:check

36
.github/workflows/static-analysis.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Php Static analysis
on: [push, pull_request]
jobs:
psalm:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@master
- name: Set up php
uses: shivammathur/setup-php@master
with:
php-version: 7.4
tools: composer:v1
coverage: none
extensions: redis
- name: Install dependencies
run: composer i
- name: Run coding standards check
run: composer run psalm
phpstan:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@master
- name: Set up php
uses: shivammathur/setup-php@master
with:
php-version: 7.4
tools: composer:v1
coverage: none
extensions: redis
- name: Install dependencies
run: composer i
- name: Run coding standards check
run: composer run psalm

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
composer.lock composer.lock
vendor vendor
coverage.xml
.phpunit.result.cache
*.cache

15
.php_cs.dist Normal file
View file

@ -0,0 +1,15 @@
<?php
$finder = PhpCsFixer\Finder::create()
->exclude('vendor')
->in(__DIR__)
;
return PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'braces' => ['position_after_functions_and_oop_constructs' => 'same'],
'binary_operator_spaces' => ['align_double_arrow' => true, 'align_equals' => false],
])
->setIndent("\t")
->setFinder($finder)
;

6
.scrutinizer.yml Normal file
View file

@ -0,0 +1,6 @@
build:
nodes:
analysis:
tests:
override:
- php-scrutinizer-run

View file

@ -1,9 +1,8 @@
language: php language: php
php: php:
- '5.6'
- '7.0'
- '7.1'
- '7.2' - '7.2'
- '7.3'
- '7.4'
cache: cache:
directories: directories:

View file

@ -9,16 +9,31 @@
} }
], ],
"require": { "require": {
"php": ">=5.6", "php": ">=7.3 || >=8.0",
"sabre/dav": "^3.2.0" "sabre/dav": "^4.0.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.8" "php-parallel-lint/php-parallel-lint": "^1.0",
"friendsofphp/php-cs-fixer": "^2",
"phpstan/phpstan": "^0.12",
"psalm/phar": "^4.3",
"phpunit/phpunit": "^8"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"SearchDAV\\": "src/", "SearchDAV\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"SearchDAV\\Test\\": "tests/" "SearchDAV\\Test\\": "tests/"
} }
},
"scripts": {
"lint": "parallel-lint --exclude src --exclude vendor --exclude target --exclude build .",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm.phar",
"phpstan": "phpstan analyse --level 5 src"
} }
} }

15
psalm.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<psalm
errorLevel="3"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,13 +21,14 @@
namespace SearchDAV\Backend; namespace SearchDAV\Backend;
use Sabre\DAV\INode;
use SearchDAV\Query\Query; use SearchDAV\Query\Query;
interface ISearchBackend { interface ISearchBackend {
/** /**
* Get the path of the search arbiter of this backend * Get the path of the search arbiter of this backend
* *
* 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 its 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. * The returned value should be a path relative the root of the dav server.
@ -37,10 +38,10 @@ interface ISearchBackend {
* *
* @return string * @return string
*/ */
public function getArbiterPath(); public function getArbiterPath(): string;
/** /**
* Whether or not the search backend supports search requests on this scope * Whether the search backend supports search requests on this scope
* *
* The scope defines the resource that it being searched, such as a folder or address book. * The scope defines the resource that it being searched, such as a folder or address book.
* *
@ -54,7 +55,7 @@ interface ISearchBackend {
* @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 * @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, $path); public function isValidScope(string $href, $depth, ?string $path): bool;
/** /**
* List the available properties that can be used in search * List the available properties that can be used in search
@ -68,7 +69,7 @@ interface ISearchBackend {
* @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 * @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, $path); public function getPropertyDefinitionsForScope(string $href, ?string $path): array;
/** /**
* Preform the search request * Preform the search request
@ -80,5 +81,14 @@ interface ISearchBackend {
* @param Query $query * @param Query $query
* @return SearchResult[] * @return SearchResult[]
*/ */
public function search(Query $query); public function search(Query $query): array;
/**
* Called by the search plugin once the nodes to be returned have been found.
* This can be used to more efficiently load the requested properties for the results.
*
* @param INode[] $nodes
* @param string[] $requestProperties
*/
public function preloadPropertyFor(array $nodes, array $requestProperties): void;
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -26,6 +26,7 @@ class SearchPropertyDefinition {
const DATATYPE_STRING = self::XS . 'string'; const DATATYPE_STRING = self::XS . 'string';
const DATATYPE_INTEGER = self::XS . 'integer'; const DATATYPE_INTEGER = self::XS . 'integer';
const DATATYPE_NONNEGATIVE_INTEGER = self::XS . 'nonNegativeInteger'; const DATATYPE_NONNEGATIVE_INTEGER = self::XS . 'nonNegativeInteger';
const DATATYPE_NON_NEGATIVE_INTEGER = self::XS . 'nonNegativeInteger';
const DATATYPE_DECIMAL = self::XS . 'decimal'; const DATATYPE_DECIMAL = self::XS . 'decimal';
const DATATYPE_DATETIME = self::XS . 'dateTime'; const DATATYPE_DATETIME = self::XS . 'dateTime';
const DATATYPE_BOOLEAN = self::XS . 'boolean'; const DATATYPE_BOOLEAN = self::XS . 'boolean';
@ -48,13 +49,13 @@ class SearchPropertyDefinition {
* SearchProperty constructor. * SearchProperty constructor.
* *
* @param string $name the name and namespace of the property in clark notation * @param string $name the name and namespace of the property in clark notation
* @param bool $searchable whether or not this property can be used as part of a search query * @param bool $searchable whether this property can be used as part of a search query
* @param bool $selectable whether or not this property can be returned as part of a search result * @param bool $selectable whether this property can be returned as part of a search result
* @param bool $sortable whether or not this property can be used to sort the search result * @param bool $sortable whether this property can be used to sort the search result
* @param string $dataType the datatype of the property, one of the SearchProperty::DATATYPE_ constants or any XSD datatype in clark notation * @param string $dataType the datatype of the property, one of the SearchProperty::DATATYPE_ constants or any XSD datatype in clark notation
* @param bool $caseSensitive whether or not comparisons on the property are case sensitive, only applies to string propertries * @param bool $caseSensitive whether comparisons on the property are case-sensitive, only applies to string properties
*/ */
public function __construct($name, $searchable, $selectable, $sortable, $dataType = self::DATATYPE_STRING, $caseSensitive = true) { public function __construct(string $name, bool $selectable, bool $searchable, bool $sortable, string $dataType = self::DATATYPE_STRING, bool $caseSensitive = true) {
$this->searchable = $searchable; $this->searchable = $searchable;
$this->selectable = $selectable; $this->selectable = $selectable;
$this->sortable = $sortable; $this->sortable = $sortable;

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,7 +21,6 @@
namespace SearchDAV\Backend; namespace SearchDAV\Backend;
use Sabre\DAV\INode; use Sabre\DAV\INode;
class SearchResult { class SearchResult {
@ -36,7 +35,7 @@ class SearchResult {
* @param INode $node * @param INode $node
* @param string $href * @param string $href
*/ */
public function __construct(INode $node, $href) { public function __construct(INode $node, string $href) {
$this->node = $node; $this->node = $node;
$this->href = $href; $this->href = $href;
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -54,7 +54,7 @@ class DiscoverHandler {
$this->queryParser = $queryParser; $this->queryParser = $queryParser;
} }
public function handelDiscoverRequest($xml, RequestInterface $request, ResponseInterface $response) { public function handelDiscoverRequest($xml, RequestInterface $request, ResponseInterface $response): bool {
if (!isset($xml['{DAV:}basicsearch'])) { if (!isset($xml['{DAV:}basicsearch'])) {
$response->setStatus(400); $response->setStatus(400);
$response->setBody('Unexpected xml content for query-schema-discovery, expected basicsearch'); $response->setBody('Unexpected xml content for query-schema-discovery, expected basicsearch');
@ -80,7 +80,7 @@ class DiscoverHandler {
return false; return false;
} }
private function hashDefinition(SearchPropertyDefinition $definition) { private function hashDefinition(SearchPropertyDefinition $definition): string {
return $definition->dataType return $definition->dataType
. (($definition->searchable) ? '1' : '0') . (($definition->searchable) ? '1' : '0')
. (($definition->sortable) ? '1' : '0') . (($definition->sortable) ? '1' : '0')
@ -91,18 +91,13 @@ class DiscoverHandler {
* @param SearchPropertyDefinition[] $propertyDefinitions * @param SearchPropertyDefinition[] $propertyDefinitions
* @return BasicSearchSchema * @return BasicSearchSchema
*/ */
private function getBasicSearchForProperties(array $propertyDefinitions) { private function getBasicSearchForProperties(array $propertyDefinitions): BasicSearchSchema {
/** @var PropDesc[] $groups */ /** @var PropDesc[] $groups */
$groups = []; $groups = [];
foreach ($propertyDefinitions as $propertyDefinition) { foreach ($propertyDefinitions as $propertyDefinition) {
$key = $this->hashDefinition($propertyDefinition); $key = $this->hashDefinition($propertyDefinition);
if (!isset($groups[$key])) { if (!isset($groups[$key])) {
$desc = new PropDesc(); $groups[$key] = new PropDesc($propertyDefinition);
$desc->dataType = $propertyDefinition->dataType;
$desc->sortable = $propertyDefinition->sortable;
$desc->selectable = $propertyDefinition->selectable;
$desc->searchable = $propertyDefinition->searchable;
$groups[$key] = $desc;
} }
$groups[$key]->properties[] = $propertyDefinition->name; $groups[$key]->properties[] = $propertyDefinition->name;
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -37,7 +37,7 @@ class PathHelper {
$this->server = $server; $this->server = $server;
} }
public function getPathFromUri($uri) { public function getPathFromUri(string $uri): ?string {
if (strpos($uri, '://') === false) { if (strpos($uri, '://') === false) {
return $uri; return $uri;
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -30,6 +30,8 @@ use SearchDAV\XML\Literal;
use SearchDAV\XML\Operator; use SearchDAV\XML\Operator;
use SearchDAV\XML\Order; use SearchDAV\XML\Order;
use SearchDAV\XML\Scope; use SearchDAV\XML\Scope;
use function Sabre\Xml\Deserializer\keyValue;
use function Sabre\Xml\Deserializer\repeatingElements;
class QueryParser extends Service { class QueryParser extends Service {
public $namespaceMap = [ public $namespaceMap = [
@ -46,13 +48,13 @@ class QueryParser extends Service {
'{DAV:}query-schema-discovery' => Element\KeyValue::class, '{DAV:}query-schema-discovery' => Element\KeyValue::class,
'{DAV:}basicsearch' => BasicSearch::class, '{DAV:}basicsearch' => BasicSearch::class,
'{DAV:}select' => function (Reader $reader) { '{DAV:}select' => function (Reader $reader) {
return \Sabre\Xml\Deserializer\keyValue($reader, '{DAV:}scope')['{DAV:}prop']; return keyValue($reader, '{DAV:}scope')['{DAV:}prop'];
}, },
'{DAV:}from' => function (Reader $reader) { '{DAV:}from' => function (Reader $reader) {
return \Sabre\Xml\Deserializer\repeatingElements($reader, '{DAV:}scope'); return repeatingElements($reader, '{DAV:}scope');
}, },
'{DAV:}orderby' => function (Reader $reader) { '{DAV:}orderby' => function (Reader $reader) {
return \Sabre\Xml\Deserializer\repeatingElements($reader, '{DAV:}order'); return repeatingElements($reader, '{DAV:}order');
}, },
'{DAV:}scope' => Scope::class, '{DAV:}scope' => Scope::class,
'{DAV:}where' => function (Reader $reader) { '{DAV:}where' => function (Reader $reader) {
@ -74,6 +76,7 @@ class QueryParser extends Service {
'{DAV:}contains' => Operator::class, '{DAV:}contains' => Operator::class,
'{DAV:}not' => Operator::class, '{DAV:}not' => Operator::class,
'{DAV:}is-collection' => Operator::class, '{DAV:}is-collection' => Operator::class,
'{DAV:}is-defined' => Operator::class,
'{DAV:}limit' => Limit::class, '{DAV:}limit' => Limit::class,
]; ];
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -22,6 +22,7 @@
namespace SearchDAV\DAV; namespace SearchDAV\DAV;
use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind; use Sabre\DAV\PropFind;
use Sabre\DAV\Server; use Sabre\DAV\Server;
use Sabre\HTTP\ResponseInterface; use Sabre\HTTP\ResponseInterface;
@ -54,7 +55,7 @@ class SearchHandler {
$this->server = $server; $this->server = $server;
} }
public function handleSearchRequest($xml, ResponseInterface $response) { public function handleSearchRequest($xml, ResponseInterface $response): bool {
if (!isset($xml['{DAV:}basicsearch'])) { if (!isset($xml['{DAV:}basicsearch'])) {
$response->setStatus(400); $response->setStatus(400);
$response->setBody('Unexpected xml content for search request, expected basicsearch'); $response->setBody('Unexpected xml content for search request, expected basicsearch');
@ -62,11 +63,6 @@ class SearchHandler {
} }
/** @var BasicSearch $query */ /** @var BasicSearch $query */
$query = $xml['{DAV:}basicsearch']; $query = $xml['{DAV:}basicsearch'];
if (!$query->where) {
$response->setStatus(400);
$response->setBody('Parse error: Missing {DAV:}where from {DAV:}basicsearch');
return false;
}
if (!$query->select) { if (!$query->select) {
$response->setStatus(400); $response->setStatus(400);
$response->setBody('Parse error: Missing {DAV:}select from {DAV:}basicsearch'); $response->setBody('Parse error: Missing {DAV:}select from {DAV:}basicsearch');
@ -89,7 +85,10 @@ class SearchHandler {
$response->setBody($e->getMessage()); $response->setBody($e->getMessage());
return false; return false;
} }
$data = $this->server->generateMultiStatus(iterator_to_array($this->getPropertiesIteratorResults($results, $query->select)), false); $data = $this->server->generateMultiStatus(iterator_to_array($this->getPropertiesIteratorResults(
$results,
$query->select
)), false);
$response->setBody($data); $response->setBody($data);
return false; return false;
} }
@ -98,8 +97,9 @@ class SearchHandler {
* @param BasicSearch $xml * @param BasicSearch $xml
* @param SearchPropertyDefinition[] $allProps * @param SearchPropertyDefinition[] $allProps
* @return Query * @return Query
* @throws BadRequest
*/ */
private function getQueryForXML(BasicSearch $xml, array $allProps) { private function getQueryForXML(BasicSearch $xml, array $allProps): Query {
$orderBy = array_map(function (\SearchDAV\XML\Order $order) use ($allProps) { $orderBy = array_map(function (\SearchDAV\XML\Order $order) use ($allProps) {
if (!isset($allProps[$order->property])) { if (!isset($allProps[$order->property])) {
throw new BadRequest('requested order by property is not a valid property for this scope'); throw new BadRequest('requested order by property is not a valid property for this scope');
@ -112,7 +112,7 @@ class SearchHandler {
}, $xml->orderBy); }, $xml->orderBy);
$select = array_map(function ($propName) use ($allProps) { $select = array_map(function ($propName) use ($allProps) {
if (!isset($allProps[$propName])) { if (!isset($allProps[$propName])) {
return; return null;
} }
$prop = $allProps[$propName]; $prop = $allProps[$propName];
if (!$prop->selectable) { if (!$prop->selectable) {
@ -122,12 +122,18 @@ class SearchHandler {
}, $xml->select); }, $xml->select);
$select = array_filter($select); $select = array_filter($select);
$where = $this->transformOperator($xml->where, $allProps); $where = $xml->where ? $this->transformOperator($xml->where, $allProps) : null;
return new Query($select, $xml->from, $where, $orderBy, $xml->limit); return new Query($select, $xml->from, $where, $orderBy, $xml->limit);
} }
private function transformOperator(\SearchDAV\XML\Operator $operator, array $allProps) { /**
* @param \SearchDAV\XML\Operator $operator
* @param SearchPropertyDefinition[] $allProps
* @return Operator
* @throws BadRequest
*/
private function transformOperator(\SearchDAV\XML\Operator $operator, array $allProps): Operator {
$arguments = array_map(function ($argument) use ($allProps) { $arguments = array_map(function ($argument) use ($allProps) {
if (is_string($argument)) { if (is_string($argument)) {
if (!isset($allProps[$argument])) { if (!isset($allProps[$argument])) {
@ -138,11 +144,13 @@ class SearchHandler {
throw new BadRequest('requested search property is not searchable'); throw new BadRequest('requested search property is not searchable');
} }
return $prop; return $prop;
} else if ($argument instanceof \SearchDAV\XML\Operator) { } else {
if ($argument instanceof \SearchDAV\XML\Operator) {
return $this->transformOperator($argument, $allProps); return $this->transformOperator($argument, $allProps);
} else { } else {
return $argument; return $argument;
} }
}
}, $operator->arguments); }, $operator->arguments);
return new Operator($operator->type, $arguments); return new Operator($operator->type, $arguments);
@ -158,16 +166,20 @@ class SearchHandler {
* If a depth of 1 is requested child elements will also be returned. * If a depth of 1 is requested child elements will also be returned.
* *
* @param SearchResult[] $results * @param SearchResult[] $results
* @param array $propertyNames * @param string[] $propertyNames
* @param int $depth * @param int $depth
* @return \Iterator * @return \Iterator<array>
*/ */
private function getPropertiesIteratorResults($results, $propertyNames = [], $depth = 0) { private function getPropertiesIteratorResults(array $results, array $propertyNames = [], int $depth = 0): \Iterator {
$propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS;
$this->searchBackend->preloadPropertyFor(array_map(function (SearchResult $result): INode {
return $result->node;
}, $results), $propertyNames);
foreach ($results as $result) { foreach ($results as $result) {
$node = $result->node; $node = $result->node;
$propFind = new PropFind($result->href, (array)$propertyNames, $depth, $propFindType); $propFind = new PropFind($result->href, $propertyNames, $depth, $propFindType);
$r = $this->server->getPropertiesByNode($propFind, $node); $r = $this->server->getPropertiesByNode($propFind, $node);
if ($r) { if ($r) {
$result = $propFind->getResultForMultiStatus(); $result = $propFind->getResultForMultiStatus();

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -34,9 +34,6 @@ use SearchDAV\XML\SupportedQueryGrammar;
class SearchPlugin extends ServerPlugin { class SearchPlugin extends ServerPlugin {
const SEARCHDAV_NS = 'https://github.com/icewind1991/SearchDAV/ns'; const SEARCHDAV_NS = 'https://github.com/icewind1991/SearchDAV/ns';
/** @var Server */
private $server;
/** @var ISearchBackend */ /** @var ISearchBackend */
private $searchBackend; private $searchBackend;
@ -57,8 +54,7 @@ class SearchPlugin extends ServerPlugin {
$this->queryParser = new QueryParser(); $this->queryParser = new QueryParser();
} }
public function initialize(Server $server) { public function initialize(Server $server): void {
$this->server = $server;
$this->pathHelper = new PathHelper($server); $this->pathHelper = new PathHelper($server);
$this->search = new SearchHandler($this->searchBackend, $this->pathHelper, $server); $this->search = new SearchHandler($this->searchBackend, $this->pathHelper, $server);
$this->discover = new DiscoverHandler($this->searchBackend, $this->pathHelper, $this->queryParser); $this->discover = new DiscoverHandler($this->searchBackend, $this->pathHelper, $this->queryParser);
@ -67,7 +63,7 @@ class SearchPlugin extends ServerPlugin {
$server->on('propFind', [$this, 'propFindHandler']); $server->on('propFind', [$this, 'propFindHandler']);
} }
public function propFindHandler(PropFind $propFind, INode $node) { public function propFindHandler(PropFind $propFind, INode $node): void {
if ($propFind->getPath() === $this->searchBackend->getArbiterPath()) { if ($propFind->getPath() === $this->searchBackend->getArbiterPath()) {
$propFind->handle('{DAV:}supported-query-grammar-set', new SupportedQueryGrammar()); $propFind->handle('{DAV:}supported-query-grammar-set', new SupportedQueryGrammar());
} }
@ -76,11 +72,11 @@ class SearchPlugin extends ServerPlugin {
/** /**
* SEARCH is allowed for users files * SEARCH is allowed for users files
* *
* @param string $uri * @param string $path
* @return array * @return string[]
*/ */
public function getHTTPMethods($uri) { public function getHTTPMethods($path): array {
$path = $this->pathHelper->getPathFromUri($uri); $path = $this->pathHelper->getPathFromUri($path);
if ($this->searchBackend->getArbiterPath() === $path) { if ($this->searchBackend->getArbiterPath() === $path) {
return ['SEARCH']; return ['SEARCH'];
} else { } else {
@ -88,16 +84,16 @@ class SearchPlugin extends ServerPlugin {
} }
} }
public function optionHandler(RequestInterface $request, ResponseInterface $response) { public function optionHandler(RequestInterface $request, ResponseInterface $response): void {
if ($request->getPath() === $this->searchBackend->getArbiterPath()) { if ($request->getPath() === $this->searchBackend->getArbiterPath()) {
$response->addHeader('DASL', '<DAV:basicsearch>'); $response->addHeader('DASL', '<DAV:basicsearch>');
} }
} }
public function searchHandler(RequestInterface $request, ResponseInterface $response) { public function searchHandler(RequestInterface $request, ResponseInterface $response): bool {
$contentType = $request->getHeader('Content-Type'); $contentType = $request->getHeader('Content-Type') ?? '';
// Currently we only support xml search queries // Currently, we only support xml search queries
if ((strpos($contentType, 'text/xml') === false) && (strpos($contentType, 'application/xml') === false)) { if ((strpos($contentType, 'text/xml') === false) && (strpos($contentType, 'application/xml') === false)) {
return true; return true;
} }
@ -108,7 +104,7 @@ class SearchPlugin extends ServerPlugin {
try { try {
$xml = $this->queryParser->parse( $xml = $this->queryParser->parse(
$request->getBody(), $request->getBodyAsString(),
$request->getUrl(), $request->getUrl(),
$documentType $documentType
); );

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
* *
@ -21,7 +21,6 @@
namespace SearchDAV\Query; namespace SearchDAV\Query;
class Limit { class Limit {
/** /**
* @var integer * @var integer

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
* *
@ -21,7 +21,6 @@
namespace SearchDAV\Query; namespace SearchDAV\Query;
class Literal { class Literal {
/** /**
* @var string|boolean|\DateTime|integer * @var string|boolean|\DateTime|integer

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
* *
@ -41,14 +41,15 @@ class Operator {
* The type of operation, one of the Operator::OPERATION_* constants * The type of operation, one of the Operator::OPERATION_* constants
*/ */
public $type; public $type;
/** /**
* @var (Literal|SearchPropDefinition|Operation)[] * @var (Literal|\SearchDAV\Backend\SearchPropertyDefinition|Operator)[]
* *
* The list of arguments for the operation * The list of arguments for the operation
* *
* - SearchPropDefinition: property for comparison * - SearchPropDefinition: property for comparison
* - Literal: literal value for comparison * - Literal: literal value for comparison
* - Operation: nested operation for and/or/not operations * - Operator: nested operation for and/or/not operations
* *
* Which type and what number of argument an Operator takes depends on the operator type. * Which type and what number of argument an Operator takes depends on the operator type.
*/ */
@ -58,9 +59,9 @@ class Operator {
* Operator constructor. * Operator constructor.
* *
* @param string $type * @param string $type
* @param array $arguments * @param (Literal|\SearchDAV\Backend\SearchPropertyDefinition|Operator)[] $arguments
*/ */
public function __construct($type = '', array $arguments = []) { public function __construct(string $type = '', array $arguments = []) {
$this->type = $type; $this->type = $type;
$this->arguments = $arguments; $this->arguments = $arguments;
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
* *
@ -21,7 +21,6 @@
namespace SearchDAV\Query; namespace SearchDAV\Query;
use SearchDAV\Backend\SearchPropertyDefinition; use SearchDAV\Backend\SearchPropertyDefinition;
class Order { class Order {
@ -46,7 +45,7 @@ class Order {
* @param SearchPropertyDefinition $property * @param SearchPropertyDefinition $property
* @param string $order * @param string $order
*/ */
public function __construct(SearchPropertyDefinition $property, $order) { public function __construct(SearchPropertyDefinition $property, string $order) {
$this->property = $property; $this->property = $property;
$this->order = $order; $this->order = $order;
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
* *
@ -21,7 +21,6 @@
namespace SearchDAV\Query; namespace SearchDAV\Query;
use SearchDAV\Backend\SearchPropertyDefinition; use SearchDAV\Backend\SearchPropertyDefinition;
class Query { class Query {
@ -38,7 +37,7 @@ class Query {
*/ */
public $from; public $from;
/** /**
* @var Operator * @var ?Operator
* *
* The search operator, either a comparison ('gt', 'eq', ...) or a boolean operator ('and', 'or', 'not') * The search operator, either a comparison ('gt', 'eq', ...) or a boolean operator ('and', 'or', 'not')
*/ */
@ -49,7 +48,7 @@ class Query {
* The list of order operations that should be used to order the results. * 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. * 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 * If more than 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 * be applied in the order that the order operations are defined in with the earlier comparisons being
* more significant. * more significant.
*/ */
@ -65,11 +64,11 @@ class Query {
* Query constructor. * Query constructor.
* @param SearchPropertyDefinition[] $select * @param SearchPropertyDefinition[] $select
* @param Scope[] $from * @param Scope[] $from
* @param Operator $where * @param Operator|null $where
* @param Order[] $orderBy * @param Order[] $orderBy
* @param Limit $limit * @param Limit $limit
*/ */
public function __construct(array $select, array $from, Operator $where, array $orderBy, Limit $limit) { public function __construct(array $select, array $from, ?Operator $where, array $orderBy, Limit $limit) {
$this->select = $select; $this->select = $select;
$this->from = $from; $this->from = $from;
$this->where = $where; $this->where = $where;

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl>
* *
@ -21,7 +21,6 @@
namespace SearchDAV\Query; namespace SearchDAV\Query;
class Scope { class Scope {
/** /**
* @var string * @var string
@ -52,7 +51,7 @@ class Scope {
* @param int|string $depth * @param int|string $depth
* @param string|null $path * @param string|null $path
*/ */
public function __construct($href = '', $depth = 1, $path = null) { public function __construct(string $href = '', $depth = 1, ?string $path = null) {
$this->href = $href; $this->href = $href;
$this->depth = $depth; $this->depth = $depth;
$this->path = $path; $this->path = $path;

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -24,6 +24,7 @@ namespace SearchDAV\XML;
use Sabre\Xml\ParseException; use Sabre\Xml\ParseException;
use Sabre\Xml\Reader; use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
use function Sabre\Xml\Deserializer\keyValue;
/** /**
* The object representation of a search query made by the client * The object representation of a search query made by the client
@ -42,7 +43,7 @@ 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') * The search operator, either a comparison ('gt', 'eq', ...) or a boolean operator ('and', 'or', 'not')
*/ */
@ -65,26 +66,33 @@ class BasicSearch implements XmlDeserializable {
*/ */
public $limit; public $limit;
public function __construct(array $select, array $from, ?Operator $where, array $orderBy, Limit $limit) {
$this->select = $select;
$this->from = $from;
$this->where = $where;
$this->orderBy = $orderBy;
$this->limit = $limit;
}
/** /**
* @param Reader $reader * @param Reader $reader
* @return BasicSearch * @return BasicSearch
* @throws ParseException * @throws ParseException
*/ */
static function xmlDeserialize(Reader $reader) { public static function xmlDeserialize(Reader $reader): BasicSearch {
$search = new self(); $elements = keyValue($reader);
$elements = \Sabre\Xml\Deserializer\keyValue($reader);
if (!isset($elements['{DAV:}from'])) { if (!isset($elements['{DAV:}from'])) {
throw new ParseException('Missing {DAV:}from when parsing {DAV:}basicsearch'); throw new ParseException('Missing {DAV:}from when parsing {DAV:}basicsearch');
} }
$search->select = isset($elements['{DAV:}select']) ? $elements['{DAV:}select'] : []; return new BasicSearch(
$search->from = $elements['{DAV:}from']; $elements['{DAV:}select'] ?? [],
$search->where = isset($elements['{DAV:}where']) ? $elements['{DAV:}where'] : null; $elements['{DAV:}from'],
$search->orderBy = isset($elements['{DAV:}orderby']) ? $elements['{DAV:}orderby'] : []; $elements['{DAV:}where'] ?? null,
$search->limit = isset($elements['{DAV:}limit']) ? $elements['{DAV:}limit'] : new Limit(); $elements['{DAV:}orderby'] ?? [],
$elements['{DAV:}limit'] ?? new Limit()
return $search; );
} }
} }

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,7 +21,6 @@
namespace SearchDAV\XML; namespace SearchDAV\XML;
use Sabre\Xml\Writer; use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable; use Sabre\Xml\XmlSerializable;
@ -38,7 +37,7 @@ class BasicSearchSchema implements XmlSerializable {
$this->properties = $properties; $this->properties = $properties;
} }
function xmlSerialize(Writer $writer) { public function xmlSerialize(Writer $writer): void {
$childs = array_map(function (PropDesc $propDesc) { $childs = array_map(function (PropDesc $propDesc) {
return [ return [
'name' => '{DAV:}propdesc', 'name' => '{DAV:}propdesc',

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -24,15 +24,16 @@ namespace SearchDAV\XML;
use Sabre\Xml\Reader; use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
use SearchDAV\DAV\SearchPlugin; use SearchDAV\DAV\SearchPlugin;
use function Sabre\Xml\Deserializer\keyValue;
/** /**
* The limit and offset of a search query * The limit and offset of a search query
*/ */
class Limit extends \SearchDAV\Query\Limit implements XmlDeserializable { class Limit extends \SearchDAV\Query\Limit implements XmlDeserializable {
static function xmlDeserialize(Reader $reader) { public static function xmlDeserialize(Reader $reader): Limit {
$limit = new self(); $limit = new self();
$elements = \Sabre\Xml\Deserializer\keyValue($reader); $elements = keyValue($reader);
$namespace = SearchPlugin::SEARCHDAV_NS; $namespace = SearchPlugin::SEARCHDAV_NS;
$limit->maxResults = isset($elements['{DAV:}nresults']) ? $elements['{DAV:}nresults'] : 0; $limit->maxResults = isset($elements['{DAV:}nresults']) ? $elements['{DAV:}nresults'] : 0;

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,15 +21,19 @@
namespace SearchDAV\XML; namespace SearchDAV\XML;
use Sabre\Xml\Reader; use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
class Literal extends \SearchDAV\Query\Literal implements XmlDeserializable { class Literal extends \SearchDAV\Query\Literal implements XmlDeserializable {
static function xmlDeserialize(Reader $reader) { public static function xmlDeserialize(Reader $reader): Literal {
$literal = new self(); $literal = new self();
if ($reader->isEmptyElement) {
$literal->value = '';
} else {
$literal->value = $reader->readText(); $literal->value = $reader->readText();
}
$reader->read(); $reader->read();
return $literal; return $literal;

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -23,6 +23,7 @@ namespace SearchDAV\XML;
use Sabre\Xml\Reader; use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
use SearchDAV\Query\Operator as QueryOperator;
class Operator implements XmlDeserializable { class Operator implements XmlDeserializable {
/** /**
@ -32,7 +33,7 @@ class Operator implements XmlDeserializable {
*/ */
public $type; public $type;
/** /**
* @var (Literal|string|Operation)[] * @var (Literal|string|Operator)[]
* *
* The list of arguments for the operation * The list of arguments for the operation
* *
@ -48,32 +49,41 @@ class Operator implements XmlDeserializable {
* Operator constructor. * Operator constructor.
* *
* @param string $type * @param string $type
* @param array $arguments * @param (Literal|string|Operator)[] $arguments
*/ */
public function __construct($type = '', array $arguments = []) { public function __construct(string $type = '', array $arguments = []) {
$this->type = $type; $this->type = $type;
$this->arguments = $arguments; $this->arguments = $arguments;
} }
static function xmlDeserialize(Reader $reader) { public static function xmlDeserialize(Reader $reader): Operator {
$operator = new self(); $operator = new self();
$operator->type = $reader->getClark(); $operator->type = $reader->getClark() ?? '';
if ($reader->isEmptyElement) { if ($reader->isEmptyElement) {
$reader->next(); $reader->next();
return $operator; return $operator;
} }
if ($operator->type === QueryOperator::OPERATION_CONTAINS) {
$operator->arguments[] = $reader->readString();
$reader->next();
return $operator;
}
$reader->read(); $reader->read();
do { do {
if ($reader->nodeType === Reader::ELEMENT) { if ($reader->nodeType === Reader::ELEMENT) {
$argument = $reader->parseCurrentElement(); $argument = $reader->parseCurrentElement();
if ($argument['name'] === '{DAV:}prop') { if ($argument['name'] === '{DAV:}prop') {
$operator->arguments[] = $argument['value'][0]; $operator->arguments[] = $argument['value'][0] ?? '';
} else { } else {
$operator->arguments[] = $argument['value']; $operator->arguments[] = $argument['value'];
} }
} else { } else {
$reader->read(); if (!$reader->read()) {
break;
}
} }
} while ($reader->nodeType !== Reader::END_ELEMENT); } while ($reader->nodeType !== Reader::END_ELEMENT);

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,9 +21,9 @@
namespace SearchDAV\XML; namespace SearchDAV\XML;
use Sabre\Xml\Reader; use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
use function Sabre\Xml\Deserializer\keyValue;
class Order implements XmlDeserializable { class Order implements XmlDeserializable {
/** /**
@ -45,15 +45,15 @@ class Order implements XmlDeserializable {
* @param string $property * @param string $property
* @param string $order * @param string $order
*/ */
public function __construct($property = '', $order = \SearchDAV\Query\Order::ASC) { public function __construct(string $property = '', string $order = \SearchDAV\Query\Order::ASC) {
$this->property = $property; $this->property = $property;
$this->order = $order; $this->order = $order;
} }
static function xmlDeserialize(Reader $reader) { public static function xmlDeserialize(Reader $reader): Order {
$order = new self(); $order = new self();
$childs = \Sabre\Xml\Deserializer\keyValue($reader); $childs = keyValue($reader);
$order->order = array_key_exists('{DAV:}descending', $childs) ? \SearchDAV\Query\Order::DESC : \SearchDAV\Query\Order::ASC; $order->order = array_key_exists('{DAV:}descending', $childs) ? \SearchDAV\Query\Order::DESC : \SearchDAV\Query\Order::ASC;
$order->property = $childs['{DAV:}prop'][0]; $order->property = $childs['{DAV:}prop'][0];

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,9 +21,9 @@
namespace SearchDAV\XML; namespace SearchDAV\XML;
use Sabre\Xml\Writer; use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable; use Sabre\Xml\XmlSerializable;
use SearchDAV\Backend\SearchPropertyDefinition;
class PropDesc implements XmlSerializable { class PropDesc implements XmlSerializable {
/** /**
@ -47,7 +47,14 @@ class PropDesc implements XmlSerializable {
*/ */
public $sortable; public $sortable;
function xmlSerialize(Writer $writer) { public function __construct(SearchPropertyDefinition $propertyDefinition) {
$this->dataType = $propertyDefinition->dataType;
$this->sortable = $propertyDefinition->sortable;
$this->selectable = $propertyDefinition->selectable;
$this->searchable = $propertyDefinition->searchable;
}
public function xmlSerialize(Writer $writer): void {
$data = [ $data = [
'{DAV:}dataType' => [$this->dataType => null] '{DAV:}dataType' => [$this->dataType => null]
]; ];

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,9 +21,9 @@
namespace SearchDAV\XML; namespace SearchDAV\XML;
use Sabre\DAV\Xml\Element\Response; use Sabre\DAV\Xml\Element\Response;
use Sabre\Xml\Writer; use Sabre\Xml\Writer;
use function Sabre\HTTP\encodePath;
class QueryDiscoverResponse extends Response { class QueryDiscoverResponse extends Response {
/** /**
@ -38,17 +38,19 @@ class QueryDiscoverResponse extends Response {
* @param BasicSearchSchema|null $schema * @param BasicSearchSchema|null $schema
* @param null|int|string $httpStatus * @param null|int|string $httpStatus
*/ */
function __construct($href, BasicSearchSchema $schema = null, $httpStatus = null) { public function __construct($href, ?BasicSearchSchema $schema = null, $httpStatus = null) {
if ($httpStatus !== null) {
$httpStatus = (string)$httpStatus;
}
parent::__construct($href, [], $httpStatus); parent::__construct($href, [], $httpStatus);
$this->schema = $schema; $this->schema = $schema;
} }
function xmlSerialize(Writer $writer) { public function xmlSerialize(Writer $writer): void {
if ($status = $this->getHTTPStatus()) { if ($status = $this->getHTTPStatus()) {
$writer->writeElement('{DAV:}status', 'HTTP/1.1 ' . $status . ' ' . \Sabre\HTTP\Response::$statusCodes[$status]); $writer->writeElement('{DAV:}status', 'HTTP/1.1 ' . $status . ' ' . \Sabre\HTTP\Response::$statusCodes[$status]);
} }
$writer->writeElement('{DAV:}href', \Sabre\HTTP\encodePath($this->getHref())); $writer->writeElement('{DAV:}href', encodePath($this->getHref()));
if ($this->schema) { if ($this->schema) {
$writer->writeElement('{DAV:}query-schema', [ $writer->writeElement('{DAV:}query-schema', [

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -23,12 +23,13 @@ namespace SearchDAV\XML;
use Sabre\Xml\Reader; use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable; use Sabre\Xml\XmlDeserializable;
use function Sabre\Xml\Deserializer\keyValue;
class Scope extends \SearchDAV\Query\Scope implements XmlDeserializable { class Scope extends \SearchDAV\Query\Scope implements XmlDeserializable {
static function xmlDeserialize(Reader $reader) { public static function xmlDeserialize(Reader $reader): Scope {
$scope = new self(); $scope = new self();
$values = \Sabre\Xml\Deserializer\keyValue($reader); $values = keyValue($reader);
$scope->href = $values['{DAV:}href']; $scope->href = $values['{DAV:}href'];
$scope->depth = $values['{DAV:}depth']; $scope->depth = $values['{DAV:}depth'];

View file

@ -1,4 +1,4 @@
<?php <?php declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
* *
@ -21,19 +21,16 @@
namespace SearchDAV\XML; namespace SearchDAV\XML;
use Sabre\Xml\Writer; use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable; use Sabre\Xml\XmlSerializable;
class SupportedQueryGrammar implements XmlSerializable { class SupportedQueryGrammar implements XmlSerializable {
const GRAMMAR_BASICSEARCH = '{DAV:}basicsearch'; const GRAMMAR_BASIC_SEARCH = '{DAV:}basicsearch';
public $grammar = self::GRAMMAR_BASICSEARCH; public function xmlSerialize(Writer $writer): void {
function xmlSerialize(Writer $writer) {
$writer->startElement('{DAV:}supported-query-grammar'); $writer->startElement('{DAV:}supported-query-grammar');
$writer->startElement('{DAV:}grammar'); $writer->startElement('{DAV:}grammar');
$writer->startElement($this->grammar); $writer->startElement(self::GRAMMAR_BASIC_SEARCH);
$writer->endElement(); $writer->endElement();
$writer->endElement(); $writer->endElement();
$writer->endElement(); $writer->endElement();

View file

@ -21,7 +21,6 @@
namespace SearchDAV\Test; namespace SearchDAV\Test;
use Sabre\DAV\INode; use Sabre\DAV\INode;
use Sabre\DAV\SimpleFile; use Sabre\DAV\SimpleFile;
use SearchDAV\Backend\ISearchBackend; use SearchDAV\Backend\ISearchBackend;
@ -31,26 +30,29 @@ use SearchDAV\XML\BasicSearch;
use SearchDAV\Backend\SearchPropertyDefinition; use SearchDAV\Backend\SearchPropertyDefinition;
class DummyBackend implements ISearchBackend { class DummyBackend implements ISearchBackend {
public function getArbiterPath() { public function getArbiterPath(): string {
return ''; return '';
} }
public function isValidScope($href, $depth, $path) { public function isValidScope(string $href, $depth, ?string $path): bool {
return true; return true;
} }
public function getPropertyDefinitionsForScope($href, $path) { public function getPropertyDefinitionsForScope($href, $path): array {
return [ return [
new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
new SearchPropertyDefinition('{DAV:}displayname', true, true, true), new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{http://ns.nextcloud.com:}fileid', false, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), new SearchPropertyDefinition('{http://ns.nextcloud.com:}fileid', false, true, true, SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER),
]; ];
} }
public function search(Query $query) { public function search(Query $query): array {
return [ return [
new SearchResult(new SimpleFile('foo.txt', 'foobar', 'text/plain'), '/bar/foo.txt') new SearchResult(new SimpleFile('foo.txt', 'foobar', 'text/plain'), '/bar/foo.txt')
]; ];
} }
public function preloadPropertyFor(array $nodes, array $requestProperties): void {
}
} }

View file

@ -21,10 +21,11 @@
namespace SearchDAV\Test; namespace SearchDAV\Test;
use PHPUnit\Framework\TestCase;
use Sabre\DAV\Server; use Sabre\DAV\Server;
use SearchDAV\DAV\PathHelper; use SearchDAV\DAV\PathHelper;
class PathHelperTest extends \PHPUnit_Framework_TestCase { class PathHelperTest extends TestCase {
public function uriProvider() { public function uriProvider() {
return [ return [
['/', '', ''], ['/', '', ''],

View file

@ -21,7 +21,8 @@
namespace SearchDAV\Test; namespace SearchDAV\Test;
use PHPUnit\Framework\TestCase;
use Sabre\Xml\ParseException;
use Sabre\Xml\Service; use Sabre\Xml\Service;
use SearchDAV\DAV\QueryParser; use SearchDAV\DAV\QueryParser;
use SearchDAV\XML\BasicSearch; use SearchDAV\XML\BasicSearch;
@ -32,8 +33,7 @@ use SearchDAV\XML\Order;
use SearchDAV\XML\Scope; use SearchDAV\XML\Scope;
use SearchDAV\XML\SupportedQueryGrammar; use SearchDAV\XML\SupportedQueryGrammar;
class QueryParserTest extends TestCase {
class QueryParserTest extends \PHPUnit_Framework_TestCase {
public function testParseBasicQuery() { public function testParseBasicQuery() {
$query = file_get_contents(__DIR__ . '/basicquery.xml'); $query = file_get_contents(__DIR__ . '/basicquery.xml');
$parser = new QueryParser(); $parser = new QueryParser();
@ -48,14 +48,14 @@ class QueryParserTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals(['{DAV:}getcontentlength'], $search->select); $this->assertEquals(['{DAV:}getcontentlength'], $search->select);
$this->assertEquals([ $this->assertEquals([
new Scope('/container1/', 'infinity') new Scope('/container1/', 'infinity'),
], $search->from); ], $search->from);
$this->assertEquals(new Operator(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, [ $this->assertEquals(new Operator(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, [
'{DAV:}getcontentlength', '{DAV:}getcontentlength',
new Literal(10000) new Literal(10000),
]), $search->where); ]), $search->where);
$this->assertEquals([ $this->assertEquals([
new Order('{DAV:}getcontentlength', \SearchDAV\Query\Order::ASC) new Order('{DAV:}getcontentlength', \SearchDAV\Query\Order::ASC),
], $search->orderBy); ], $search->orderBy);
} }
@ -73,14 +73,14 @@ class QueryParserTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals(['{DAV:}getcontentlength'], $search->select); $this->assertEquals(['{DAV:}getcontentlength'], $search->select);
$this->assertEquals([ $this->assertEquals([
new Scope('/container1/', 'infinity') new Scope('/container1/', 'infinity'),
], $search->from); ], $search->from);
$this->assertEquals(new Operator(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, [ $this->assertEquals(new Operator(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, [
'{DAV:}getcontentlength', '{DAV:}getcontentlength',
new Literal(10000) new Literal(10000),
]), $search->where); ]), $search->where);
$this->assertEquals([ $this->assertEquals([
new Order('{DAV:}getcontentlength', \SearchDAV\Query\Order::DESC) new Order('{DAV:}getcontentlength', \SearchDAV\Query\Order::DESC),
], $search->orderBy); ], $search->orderBy);
} }
@ -105,12 +105,10 @@ class QueryParserTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals([], $search->orderBy); $this->assertEquals([], $search->orderBy);
} }
/**
* @expectedException \Sabre\XML\ParseException
*/
public function testParseNoFrom() { public function testParseNoFrom() {
$query = file_get_contents(__DIR__ . '/nofrom.xml'); $query = file_get_contents(__DIR__ . '/nofrom.xml');
$parser = new QueryParser(); $parser = new QueryParser();
$this->expectException(ParseException::class);
$parser->parse($query, null, $rootElementName); $parser->parse($query, null, $rootElementName);
} }
@ -142,4 +140,44 @@ class QueryParserTest extends \PHPUnit_Framework_TestCase {
$limit->maxResults = 10; $limit->maxResults = 10;
$this->assertEquals($limit, $search->limit); $this->assertEquals($limit, $search->limit);
} }
public function testParseComplexQuery() {
$query = file_get_contents(__DIR__ . '/complexquery.xml');
$parser = new QueryParser();
$xml = $parser->parse($query, null, $rootElementName);
$this->assertEquals('{DAV:}searchrequest', $rootElementName);
$this->assertArrayHasKey('{DAV:}basicsearch', $xml);
/** @var BasicSearch $search */
$search = $xml['{DAV:}basicsearch'];
$this->assertInstanceOf(BasicSearch::class, $search);
$this->assertEquals(['{DAV:}getcontentlength'], $search->select);
$this->assertEquals([
new Scope('/container1/', 'infinity'),
], $search->from);
$this->assertEquals(new Operator(\SearchDAV\Query\Operator::OPERATION_AND, [
new Operator(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, [
'{DAV:}getcontentlength',
new Literal(10000),
]),
new Operator(\SearchDAV\Query\Operator::OPERATION_LESS_THAN, [
'{DAV:}getcontentlength',
new Literal(90000),
]),
new Operator(\SearchDAV\Query\Operator::OPERATION_CONTAINS, [
'Peter Forsberg',
]),
]), $search->where);
$this->assertEquals([
new Order('{DAV:}getcontentlength', \SearchDAV\Query\Order::ASC),
], $search->orderBy);
}
public function testParseWhereBroken() {
$query = file_get_contents(__DIR__ . '/invalidwherebroken.xml');
$this->expectException(ParseException::class);
(new QueryParser())->parse($query, null, $rootElementName);
}
} }

View file

@ -21,7 +21,7 @@
namespace SearchDAV\Test; namespace SearchDAV\Test;
use PHPUnit\Framework\TestCase;
use Sabre\DAV\FS\Directory; use Sabre\DAV\FS\Directory;
use Sabre\DAV\INode; use Sabre\DAV\INode;
use Sabre\DAV\PropFind; use Sabre\DAV\PropFind;
@ -29,25 +29,21 @@ use Sabre\DAV\Server;
use Sabre\DAV\Xml\Service; use Sabre\DAV\Xml\Service;
use Sabre\HTTP\Request; use Sabre\HTTP\Request;
use Sabre\HTTP\Response; use Sabre\HTTP\Response;
use Sabre\VObject\Parser\XML;
use SearchDAV\Backend\ISearchBackend; use SearchDAV\Backend\ISearchBackend;
use SearchDAV\Backend\SearchPropertyDefinition; use SearchDAV\Backend\SearchPropertyDefinition;
use SearchDAV\Backend\SearchResult; use SearchDAV\Backend\SearchResult;
use SearchDAV\DAV\SearchPlugin; use SearchDAV\DAV\SearchPlugin;
use SearchDAV\Query\Query; use SearchDAV\Query\Query;
use SearchDAV\XML\BasicSearch;
use SearchDAV\XML\Limit; use SearchDAV\XML\Limit;
use SearchDAV\XML\Literal; use SearchDAV\XML\Literal;
use SearchDAV\XML\Operator;
use SearchDAV\XML\Order;
use SearchDAV\XML\Scope; use SearchDAV\XML\Scope;
use SearchDAV\XML\SupportedQueryGrammar; use SearchDAV\XML\SupportedQueryGrammar;
class SearchPluginTest extends \PHPUnit_Framework_TestCase { class SearchPluginTest extends TestCase {
/** @var ISearchBackend|\PHPUnit_Framework_MockObject_MockObject */ /** @var ISearchBackend|\PHPUnit_Framework_MockObject_MockObject */
private $searchBackend; private $searchBackend;
protected function setUp() { protected function setUp(): void {
parent::setUp(); parent::setUp();
$this->searchBackend = $this->getMockBuilder(ISearchBackend::class) $this->searchBackend = $this->getMockBuilder(ISearchBackend::class)
@ -60,7 +56,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
->willReturn('foo'); ->willReturn('foo');
$request = new Request('SEARCH', 'foo', [ $request = new Request('SEARCH', 'foo', [
'Content-Type' => 'text/plain' 'Content-Type' => 'text/plain',
], fopen(__DIR__ . '/nofrom.xml', 'r')); ], fopen(__DIR__ . '/nofrom.xml', 'r'));
$response = new Response(); $response = new Response();
@ -75,7 +71,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
->willReturn('foo'); ->willReturn('foo');
$request = new Request('SEARCH', 'bar', [ $request = new Request('SEARCH', 'bar', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
], fopen(__DIR__ . '/nofrom.xml', 'r')); ], fopen(__DIR__ . '/nofrom.xml', 'r'));
$response = new Response(); $response = new Response();
@ -90,7 +86,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
->willReturn('foo'); ->willReturn('foo');
$request = new Request('SEARCH', 'foo', [ $request = new Request('SEARCH', 'foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
], fopen(__DIR__ . '/invalidtype.xml', 'r')); ], fopen(__DIR__ . '/invalidtype.xml', 'r'));
$response = new Response(); $response = new Response();
@ -107,7 +103,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
->willReturn('foo'); ->willReturn('foo');
$request = new Request('SEARCH', 'foo', [ $request = new Request('SEARCH', 'foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
], fopen(__DIR__ . '/nofrom.xml', 'r')); ], fopen(__DIR__ . '/nofrom.xml', 'r'));
$response = new Response(); $response = new Response();
@ -171,7 +167,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/discover.xml', 'r')); $request->setBody(fopen(__DIR__ . '/discover.xml', 'r'));
@ -184,10 +180,22 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$this->searchBackend->expects($this->once()) $this->searchBackend->expects($this->once())
->method('getPropertyDefinitionsForScope') ->method('getPropertyDefinitionsForScope')
->willReturn([ ->willReturn([
new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), new SearchPropertyDefinition(
'{DAV:}getcontentlength',
true,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
),
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
new SearchPropertyDefinition('{DAV:}displayname', true, true, true), new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
new SearchPropertyDefinition('{http://ns.nextcloud.com:}fileid', false, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), new SearchPropertyDefinition(
'{http://ns.nextcloud.com:}fileid',
false,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
),
]); ]);
$plugin->searchHandler($request, $response); $plugin->searchHandler($request, $response);
@ -208,7 +216,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/discover.xml', 'r')); $request->setBody(fopen(__DIR__ . '/discover.xml', 'r'));
@ -239,7 +247,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/invaliddiscover.xml', 'r')); $request->setBody(fopen(__DIR__ . '/invaliddiscover.xml', 'r'));
@ -266,7 +274,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/basicquery.xml', 'r')); $request->setBody(fopen(__DIR__ . '/basicquery.xml', 'r'));
@ -276,17 +284,23 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
->method('isValidScope') ->method('isValidScope')
->willReturn(true); ->willReturn(true);
$lengthProp = new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER); $lengthProp = new SearchPropertyDefinition(
'{DAV:}getcontentlength',
true,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
);
$orderBy = [ $orderBy = [
new \SearchDAV\Query\Order($lengthProp, \SearchDAV\Query\Order::ASC) new \SearchDAV\Query\Order($lengthProp, \SearchDAV\Query\Order::ASC),
]; ];
$select = [$lengthProp]; $select = [$lengthProp];
$from = [ $from = [
new Scope('/container1/', 'infinity', '/container1/') new Scope('/container1/', 'infinity', '/container1/'),
]; ];
$where = new \SearchDAV\Query\Operator(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, [ $where = new \SearchDAV\Query\Operator(\SearchDAV\Query\Operator::OPERATION_GREATER_THAN, [
$lengthProp, $lengthProp,
new Literal(10000) new Literal(10000),
]); ]);
$limit = new Limit(); $limit = new Limit();
$query = new Query($select, $from, $where, $orderBy, $limit); $query = new Query($select, $from, $where, $orderBy, $limit);
@ -298,13 +312,13 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
new SearchResult( new SearchResult(
new Directory('/foo'), new Directory('/foo'),
'/foo' '/foo'
) ),
]); ]);
$this->searchBackend->expects($this->any()) $this->searchBackend->expects($this->any())
->method('getPropertyDefinitionsForScope') ->method('getPropertyDefinitionsForScope')
->willReturn([ ->willReturn([
$lengthProp $lengthProp,
]); ]);
$plugin->searchHandler($request, $response); $plugin->searchHandler($request, $response);
@ -325,7 +339,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/nofrom.xml', 'r')); $request->setBody(fopen(__DIR__ . '/nofrom.xml', 'r'));
@ -348,12 +362,19 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
->method('getArbiterPath') ->method('getArbiterPath')
->willReturn('foo'); ->willReturn('foo');
$lengthProp = new SearchPropertyDefinition(
'{DAV:}getcontentlength',
true,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
);
$plugin = new SearchPlugin($this->searchBackend); $plugin = new SearchPlugin($this->searchBackend);
$server = new Server(); $server = new Server();
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/nowhere.xml', 'r')); $request->setBody(fopen(__DIR__ . '/nowhere.xml', 'r'));
@ -363,12 +384,20 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
->method('isValidScope') ->method('isValidScope')
->willReturn(true); ->willReturn(true);
$this->searchBackend->expects($this->never()) $this->searchBackend->expects($this->any())
->method('search'); ->method('getPropertyDefinitionsForScope')
->willReturn([$lengthProp]);
$this->searchBackend->expects($this->once())
->method('search')
->willReturnCallback(function (Query $query) {
$this->assertNull($query->where);
return [];
});
$plugin->searchHandler($request, $response); $plugin->searchHandler($request, $response);
$this->assertEquals(400, $response->getStatus()); $this->assertEquals(207, $response->getStatus());
} }
public function testSearchQueryNoSelect() { public function testSearchQueryNoSelect() {
@ -381,7 +410,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/noselect.xml', 'r')); $request->setBody(fopen(__DIR__ . '/noselect.xml', 'r'));
@ -409,7 +438,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/invalid.xml', 'r')); $request->setBody(fopen(__DIR__ . '/invalid.xml', 'r'));
@ -458,7 +487,7 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$plugin->initialize($server); $plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [ $request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml' 'Content-Type' => 'text/xml',
]); ]);
$request->setBaseUrl('/index.php'); $request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/invalidwhere.xml', 'r')); $request->setBody(fopen(__DIR__ . '/invalidwhere.xml', 'r'));
@ -474,7 +503,13 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$this->searchBackend->expects($this->once()) $this->searchBackend->expects($this->once())
->method('getPropertyDefinitionsForScope') ->method('getPropertyDefinitionsForScope')
->willReturn([ ->willReturn([
new SearchPropertyDefinition('{http://ns.nextcloud.com:}fileid', false, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), new SearchPropertyDefinition(
'{http://ns.nextcloud.com:}fileid',
false,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
),
]); ]);
$plugin->searchHandler($request, $response); $plugin->searchHandler($request, $response);
@ -482,4 +517,97 @@ class SearchPluginTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals(400, $response->getStatus()); $this->assertEquals(400, $response->getStatus());
} }
public function testSearchQueryInvalidWhereNoProp() {
$this->searchBackend->expects($this->any())
->method('getArbiterPath')
->willReturn('foo');
$plugin = new SearchPlugin($this->searchBackend);
$server = new Server();
$plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml',
]);
$request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/invalidwherenoprop.xml', 'r'));
$response = new Response();
$this->searchBackend->expects($this->any())
->method('isValidScope')
->willReturn(true);
$this->searchBackend->expects($this->never())
->method('search');
$this->searchBackend->expects($this->any())
->method('getPropertyDefinitionsForScope')
->willReturn([
new SearchPropertyDefinition(
'{http://ns.nextcloud.com:}fileid',
false,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
),
new SearchPropertyDefinition(
'{DAV:}getcontentlength',
true,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
),
]);
$plugin->searchHandler($request, $response);
$this->assertEquals(400, $response->getStatus());
}
public function testSearchQueryInfiniteLoopEmptyLiteral() {
$this->searchBackend->expects($this->any())
->method('getArbiterPath')
->willReturn('foo');
$plugin = new SearchPlugin($this->searchBackend);
$server = new Server();
$plugin->initialize($server);
$request = new Request('SEARCH', '/index.php/foo', [
'Content-Type' => 'text/xml',
]);
$request->setBaseUrl('/index.php');
$request->setBody(fopen(__DIR__ . '/infiniteloopemptyliteral.xml', 'r'));
$response = new Response();
$this->searchBackend->expects($this->any())
->method('isValidScope')
->willReturn(true);
$this->searchBackend->expects($this->never())
->method('search');
$this->searchBackend->expects($this->any())
->method('getPropertyDefinitionsForScope')
->willReturn([
new SearchPropertyDefinition(
'{http://ns.nextcloud.com:}fileid',
false,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
),
new SearchPropertyDefinition(
'{DAV:}getcontentlength',
true,
true,
true,
SearchPropertyDefinition::DATATYPE_NON_NEGATIVE_INTEGER
),
]);
$plugin->searchHandler($request, $response);
$this->assertEquals(400, $response->getStatus());
}
} }

View file

@ -1,3 +1,3 @@
<?php <?php
require '../vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';

41
tests/complexquery.xml Normal file
View file

@ -0,0 +1,41 @@
<?xml version="1.0"?>
<d:searchrequest xmlns:d="DAV:">
<d:basicsearch>
<d:select>
<d:prop>
<d:getcontentlength/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/container1/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:and>
<d:gt>
<d:prop>
<d:getcontentlength/>
</d:prop>
<d:literal>10000</d:literal>
</d:gt>
<d:lt>
<d:prop>
<d:getcontentlength/>
</d:prop>
<d:literal>90000</d:literal>
</d:lt>
<d:contains>Peter Forsberg</d:contains>
</d:and>
</d:where>
<d:orderby>
<d:order>
<d:prop>
<d:getcontentlength/>
</d:prop>
<d:ascending/>
</d:order>
</d:orderby>
</d:basicsearch>
</d:searchrequest>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://nextcloud.com/ns">
<d:basicsearch>
<d:select>
<d:prop>
<d:displayname/>
<d:getcontenttype/>
<d:resourcetype/>
<d:getcontentlength/>
<d:getlastmodified/>
<d:creationdate/>
<d:getetag/>
<d:quota-used-bytes/>
<d:quota-available-bytes/>
<oc:permissions xmlns:oc="http://owncloud.org/ns"/>
<oc:id xmlns:oc="http://owncloud.org/ns"/>
<oc:size xmlns:oc="http://owncloud.org/ns"/>
<oc:favorite xmlns:oc="http://owncloud.org/ns"/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/files/naofumi</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where><d:like><d:prop/><d:literal/></d:like></d:where>
<d:orderby/>
</d:basicsearch>
</d:searchrequest>

View file

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://ns.nextcloud.com">
<d:basicsearch>
<d:select>
<d:prop>
<d:getcontentlength/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/container1/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:gt>
<d:prop>
<d:literal>5</d:literal>
</d:gt>
</d:where>
</d:basicsearch>
</d:searchrequest>

View file

@ -0,0 +1,30 @@
<?xml version="1.0"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://ns.nextcloud.com">
<d:basicsearch>
<d:select>
<d:prop>
<d:getcontentlength/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/container1/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:gt>
<d:prop />
<d:literal />
</d:gt>
</d:where>
<d:orderby>
<d:order>
<d:prop>
<d:getcontentlength/>
</d:prop>
<d:ascending/>
</d:order>
</d:orderby>
</d:basicsearch>
</d:searchrequest>