Compare commits

...

40 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
b3fde1c79b
Merge pull request #5 from icewind1991/scrutinizer-patch-1
Scrutinizer Auto-Fixes
2018-03-22 17:07:21 +01:00
Scrutinizer Auto-Fixer
6d421fc4ec Scrutinizer Auto-Fixes
This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com
2018-03-22 16:04:24 +00:00
aa91e9c193
Merge pull request #4 from rullzer/allow_404
No need to error out if the propery can't be found
2018-03-22 16:55:28 +01:00
Roeland Jago Douma
a292d9402a
No need to error out if the propery can't be found
Just don't search for it if it is not allowed. But we can still
continue. Webdav will give a 404 if it can't be found later anyway.

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
2018-03-22 16:50:59 +01:00
42 changed files with 714 additions and 229 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,29 +30,31 @@ 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 = [
'DAV:' => 'd', 'DAV:' => 'd',
'http://sabredav.org/ns' => 's', 'http://sabredav.org/ns' => 's',
'http://www.w3.org/2001/XMLSchema' => 'xs', 'http://www.w3.org/2001/XMLSchema' => 'xs',
SearchPlugin::SEARCHDAV_NS => 'sd' SearchPlugin::SEARCHDAV_NS => 'sd'
]; ];
public function __construct() { public function __construct() {
$this->elementMap = [ $this->elementMap = [
'{DAV:}literal' => Literal::class, '{DAV:}literal' => Literal::class,
'{DAV:}searchrequest' => Element\KeyValue::class, '{DAV:}searchrequest' => Element\KeyValue::class,
'{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) {
@ -61,20 +63,21 @@ class QueryParser extends Service {
}, $reader->parseGetElements()); }, $reader->parseGetElements());
return (isset($operators[0])) ? $operators[0] : null; 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,
'{DAV:}eq' => Operator::class, '{DAV:}eq' => Operator::class,
'{DAV:}gt' => Operator::class, '{DAV:}gt' => Operator::class,
'{DAV:}gte' => Operator::class, '{DAV:}gte' => Operator::class,
'{DAV:}lt' => Operator::class, '{DAV:}lt' => Operator::class,
'{DAV:}lte' => Operator::class, '{DAV:}lte' => Operator::class,
'{DAV:}and' => Operator::class, '{DAV:}and' => Operator::class,
'{DAV:}or' => Operator::class, '{DAV:}or' => Operator::class,
'{DAV:}like' => Operator::class, '{DAV:}like' => Operator::class,
'{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:}limit' => Limit::class, '{DAV:}is-defined' => Operator::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,19 +55,14 @@ 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 searchrequest, expected basicsearch'); $response->setBody('Unexpected xml content for search request, expected basicsearch');
return false; return false;
} }
/** @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])) {
throw new BadRequest('requested property is not a valid property for this scope'); return null;
} }
$prop = $allProps[$propName]; $prop = $allProps[$propName];
if (!$prop->selectable) { if (!$prop->selectable) {
@ -120,13 +120,20 @@ class SearchHandler {
} }
return $prop; return $prop;
}, $xml->select); }, $xml->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])) {
@ -137,10 +144,12 @@ 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) {
return $this->transformOperator($argument, $allProps);
} else { } else {
return $argument; if ($argument instanceof \SearchDAV\XML\Operator) {
return $this->transformOperator($argument, $allProps);
} else {
return $argument;
}
} }
}, $operator->arguments); }, $operator->arguments);
@ -157,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,10 +37,10 @@ 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',
'value' => $propDesc 'value' => $propDesc
]; ];
}, $this->properties); }, $this->properties);

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();
$literal->value = $reader->readText(); if ($reader->isEmptyElement) {
$literal->value = '';
} else {
$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]
]; ];
@ -62,7 +69,7 @@ class PropDesc implements XmlSerializable {
} }
$writer->write(array_map(function ($propName) { $writer->write(array_map(function ($propName) {
return [ return [
'name' => '{DAV:}prop', 'name' => '{DAV:}prop',
'value' => $propName 'value' => $propName
]; ];
}, $this->properties)); }, $this->properties));

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,21 +38,23 @@ 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', [
'{DAV:}basicsearchschema' => $this->schema '{DAV:}basicsearchschema' => $this->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,11 +21,12 @@
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 [
['/', '', ''], ['/', '', ''],
['/index.php/', 'foo', 'foo'], ['/index.php/', 'foo', 'foo'],

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>