From 706131b394eef4d346f8019b4978f9a735139b03 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 2 Feb 2017 18:19:16 +0100 Subject: [PATCH 1/6] add icewind/searchdav Signed-off-by: Robin Appelman --- 3rdparty | 2 +- apps/dav/lib/Files/FileSearchBackend.php | 76 +++++++++ lib/private/Files/Cache/QuerySearchHelper.php | 155 ++++++++++++++++++ .../Files/Search/SearchBinaryOperator.php | 20 +++ lib/private/Files/Search/SearchComparison.php | 67 ++++++++ lib/private/Files/Search/SearchOrder.php | 57 +++++++ lib/private/Files/Search/SearchQuery.php | 23 +++ .../Files/Search/ISearchBinaryOperator.php | 46 ++++++ lib/public/Files/Search/ISearchComparison.php | 54 ++++++ lib/public/Files/Search/ISearchOperator.php | 26 +++ lib/public/Files/Search/ISearchOrder.php | 46 ++++++ lib/public/Files/Search/ISearchQuery.php | 33 ++++ .../lib/Files/Cache/QuerySearchHelperTest.php | 28 ++++ 13 files changed, 632 insertions(+), 1 deletion(-) create mode 100644 apps/dav/lib/Files/FileSearchBackend.php create mode 100644 lib/private/Files/Cache/QuerySearchHelper.php create mode 100644 lib/private/Files/Search/SearchBinaryOperator.php create mode 100644 lib/private/Files/Search/SearchComparison.php create mode 100644 lib/private/Files/Search/SearchOrder.php create mode 100644 lib/private/Files/Search/SearchQuery.php create mode 100644 lib/public/Files/Search/ISearchBinaryOperator.php create mode 100644 lib/public/Files/Search/ISearchComparison.php create mode 100644 lib/public/Files/Search/ISearchOperator.php create mode 100644 lib/public/Files/Search/ISearchOrder.php create mode 100644 lib/public/Files/Search/ISearchQuery.php create mode 100644 tests/lib/Files/Cache/QuerySearchHelperTest.php diff --git a/3rdparty b/3rdparty index 489bcf4f81..2838fa6777 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit 489bcf4f81e462f4d74f0b76f58caeabd58e75de +Subproject commit 2838fa6777b1427c6c912a5e599a96639ac2b31f diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php new file mode 100644 index 0000000000..2a89c756d3 --- /dev/null +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -0,0 +1,76 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Files; + +use OCA\DAV\Connector\Sabre\Directory; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Tree; +use SearchDAV\Backend\ISearchBackend; +use SearchDAV\Backend\SearchPropertyDefinition; + +class FileSearchBackend implements ISearchBackend { + /** @var Tree */ + private $tree; + + /** + * FileSearchBackend constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) { + $this->tree = $tree; + } + + /** + * Search endpoint will be remote.php/dav/files + * + * @return string + */ + public function getArbiterPath() { + return 'files'; + } + + public function isValidScope($href, $depth, $path) { + // only allow scopes inside the dav server + if (is_null($path)) { + return false; + } + + try { + $node = $this->tree->getNodeForPath($path); + return $node instanceof Directory; + } catch (NotFound $e) { + return false; + } + } + + public function getPropertyDefinitionsForScope($href, $path) { + // all valid scopes support the same schema + + return [ + new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), + new SearchPropertyDefinition('{DAV:}displayname', true, true, true), + new SearchPropertyDefinition('{http://ns.nextcloud.com:}fileid', false, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + ]; + } +} diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php new file mode 100644 index 0000000000..60f4e484da --- /dev/null +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -0,0 +1,155 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Files\Cache; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; + +/** + * Tools for transforming search queries into database queries + */ +class QuerySearchUtil { + static protected $searchOperatorMap = [ + ISearchComparison::COMPARE_LIKE => 'iLike', + ISearchComparison::COMPARE_EQUAL => 'eq', + ISearchComparison::COMPARE_GREATER_THAN => 'gt', + ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte', + ISearchComparison::COMPARE_LESS_THAN => 'lt', + ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte' + ]; + + static protected $searchOperatorNegativeMap = [ + ISearchComparison::COMPARE_LIKE => 'notLike', + ISearchComparison::COMPARE_EQUAL => 'neq', + ISearchComparison::COMPARE_GREATER_THAN => 'lte', + ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt', + ISearchComparison::COMPARE_LESS_THAN => 'gte', + ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt' + ]; + + /** @var IMimeTypeLoader */ + private $mimetypeLoader; + + /** + * QuerySearchUtil constructor. + * + * @param IMimeTypeLoader $mimetypeLoader + */ + public function __construct(IMimeTypeLoader $mimetypeLoader) { + $this->mimetypeLoader = $mimetypeLoader; + } + + public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) { + $expr = $builder->expr(); + if ($operator instanceof ISearchBinaryOperator) { + switch ($operator->getType()) { + case ISearchBinaryOperator::OPERATOR_NOT: + $negativeOperator = $operator->getArguments()[0]; + if ($negativeOperator instanceof ISearchComparison) { + return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap); + } else { + throw new \InvalidArgumentException('Binary operators inside "not" is not supported'); + } + case ISearchBinaryOperator::OPERATOR_AND: + return $expr->andX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1])); + case ISearchBinaryOperator::OPERATOR_OR: + return $expr->orX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1])); + default: + throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType()); + } + } else if ($operator instanceof ISearchComparison) { + return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap); + } else { + throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator)); + } + } + + private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) { + if (!$this->isValidComparison($comparison)) { + throw new \InvalidArgumentException('Invalid comparison ' . $operator->getType() . ' on field ' . $operator->getField()); + } + + list($field, $value) = $this->getOperatorFieldAndValue($comparison); + if (isset($operatorMap[$comparison->getType()])) { + $queryOperator = $operatorMap[$comparison->getType()]; + return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value)); + } else { + throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType()); + } + } + + private function getOperatorFieldAndValue(ISearchComparison $operator) { + $field = $operator->getField(); + $value = $operator->getValue(); + if ($field === 'mimetype') { + if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) { + $value = $this->mimetypeLoader->getId($value); + } else if ($operator->getType() === ISearchComparison::COMPARE_LIKE) { + // transform "mimetype='foo/%'" to "mimepart='foo'" + if (preg_match('|(.+)/%|', $value, $matches)) { + $field = 'mimepart'; + $value = $this->mimetypeLoader->getId($matches[1]); + } + } + } + return [$field, $value]; + } + + private function isValidComparison(ISearchComparison $operator) { + $types = [ + 'mimetype' => 'string', + 'mtime' => \DateTime::class, + 'name' => 'string', + 'size' => 'integer' + ]; + $comparisons = [ + 'mimetype' => ['eq', 'like'], + 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'], + 'name' => ['eq', 'like'], + 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'] + ]; + + if (!isset($types[$operator->getField()])) { + return false; + } + $type = $types[$operator->getField()]; + if (gettype($operator->getValue()) !== $type && !(is_a($operator->getValue(), $type))) { + return false; + } + return in_array($operator->getType(), $comparisons[$operator->getField()]); + } + + private function getParameterForValue(IQueryBuilder $builder, $value) { + if ($value instanceof \DateTime) { + $value = $value->getTimestamp(); + } + if (is_numeric($value)) { + $type = IQueryBuilder::PARAM_INT; + } else { + $type = IQueryBuilder::PARAM_STR; + } + return $builder->createNamedParameter($value, $type); + } +} diff --git a/lib/private/Files/Search/SearchBinaryOperator.php b/lib/private/Files/Search/SearchBinaryOperator.php new file mode 100644 index 0000000000..15944e2776 --- /dev/null +++ b/lib/private/Files/Search/SearchBinaryOperator.php @@ -0,0 +1,20 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ diff --git a/lib/private/Files/Search/SearchComparison.php b/lib/private/Files/Search/SearchComparison.php new file mode 100644 index 0000000000..32c4ad0e5a --- /dev/null +++ b/lib/private/Files/Search/SearchComparison.php @@ -0,0 +1,67 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Files\Search; + +use OCP\Files\Search\ISearchComparison; + +class SearchComparison implements ISearchComparison { + /** @var string */ + private $type; + /** @var string */ + private $field; + /** @var string|integer|\DateTime */ + private $value; + + /** + * SearchComparison constructor. + * + * @param string $type + * @param string $field + * @param \DateTime|int|string $value + */ + public function __construct($type, $field, $value) { + $this->type = $type; + $this->field = $field; + $this->value = $value; + } + + /** + * @return string + */ + public function getType() { + return $this->type; + } + + /** + * @return string + */ + public function getField() { + return $this->field; + } + + /** + * @return \DateTime|int|string + */ + public function getValue() { + return $this->value; + } +} diff --git a/lib/private/Files/Search/SearchOrder.php b/lib/private/Files/Search/SearchOrder.php new file mode 100644 index 0000000000..c76d6f2e25 --- /dev/null +++ b/lib/private/Files/Search/SearchOrder.php @@ -0,0 +1,57 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Files\Search; + + +use OCP\Files\Search\ISearchOrder; + +class SearchOrder implements ISearchOrder { + /** @var string */ + private $direction; + /** @var string */ + private $field; + + /** + * SearchOrder constructor. + * + * @param string $direction + * @param string $field + */ + public function __construct($direction, $field) { + $this->direction = $direction; + $this->field = $field; + } + + /** + * @return string + */ + public function getDirection() { + return $this->direction; + } + + /** + * @return string + */ + public function getField() { + return $this->field; + } +} diff --git a/lib/private/Files/Search/SearchQuery.php b/lib/private/Files/Search/SearchQuery.php new file mode 100644 index 0000000000..a017d3e245 --- /dev/null +++ b/lib/private/Files/Search/SearchQuery.php @@ -0,0 +1,23 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Files\Search; + diff --git a/lib/public/Files/Search/ISearchBinaryOperator.php b/lib/public/Files/Search/ISearchBinaryOperator.php new file mode 100644 index 0000000000..248e3de56b --- /dev/null +++ b/lib/public/Files/Search/ISearchBinaryOperator.php @@ -0,0 +1,46 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Files\Search; + +interface ISearchBinaryOperator extends ISearchOperator { + const OPERATOR_AND = 'and'; + const OPERATOR_OR = 'or'; + const OPERATOR_NOT = 'not'; + + /** + * The type of binary operator + * + * One of the ISearchBinaryOperator::OPERATOR_* constants + * + * @return string + */ + public function getType(); + + /** + * The arguments for the binary operator + * + * One argument for the 'not' operator and two for 'and' and 'or' + * + * @return ISearchOperator[] + */ + public function getArguments(); +} diff --git a/lib/public/Files/Search/ISearchComparison.php b/lib/public/Files/Search/ISearchComparison.php new file mode 100644 index 0000000000..0534221861 --- /dev/null +++ b/lib/public/Files/Search/ISearchComparison.php @@ -0,0 +1,54 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Files\Search; + +interface ISearchComparison extends ISearchOperator { + const COMPARE_EQUAL = 'eq'; + const COMPARE_GREATER_THAN = 'gt'; + const COMPARE_GREATER_THAN_EQUAL = 'gte'; + const COMPARE_LESS_THAN = 'lt'; + const COMPARE_LESS_THAN_EQUAL = 'lte'; + const COMPARE_LIKE = 'like'; + + /** + * Get the type of comparison, one of the ISearchComparison::COMPARE_* constants + * + * @return string + */ + public function getType(); + + /** + * Get the name of the field to compare with + * + * i.e. 'size', 'name' or 'mimetype' + * + * @return string + */ + public function getField(); + + /** + * Get the value to compare the field with + * + * @return string|integer|\DateTime + */ + public function getValue(); +} diff --git a/lib/public/Files/Search/ISearchOperator.php b/lib/public/Files/Search/ISearchOperator.php new file mode 100644 index 0000000000..fc1db57f4c --- /dev/null +++ b/lib/public/Files/Search/ISearchOperator.php @@ -0,0 +1,26 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Files\Search; + +interface ISearchCondition { + +} diff --git a/lib/public/Files/Search/ISearchOrder.php b/lib/public/Files/Search/ISearchOrder.php new file mode 100644 index 0000000000..1abfd7506d --- /dev/null +++ b/lib/public/Files/Search/ISearchOrder.php @@ -0,0 +1,46 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Files\Search; + +/** + * @since 12.0.0 + */ +interface ISearchOrder { + const DIRECTION_ASCENDING = 'asc'; + const DIRECTION_DESCENDING = 'desc'; + + /** + * The direction to sort in, either ISearchOrder::DIRECTION_ASCENDING or ISearchOrder::DIRECTION_DESCENDING + * + * @return string + * @since 12.0.0 + */ + public function getDirection(); + + /** + * The field to sort on + * + * @return string + * @since 12.0.0 + */ + public function getField(); +} diff --git a/lib/public/Files/Search/ISearchQuery.php b/lib/public/Files/Search/ISearchQuery.php new file mode 100644 index 0000000000..f9c07533a1 --- /dev/null +++ b/lib/public/Files/Search/ISearchQuery.php @@ -0,0 +1,33 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Files\Search; + +/** + * @since 12.0.0 + */ +interface ISearchQuery { + /** + * @return ISearchOperator + * @since 12.0.0 + */ + public function getSearchOperation(); +} diff --git a/tests/lib/Files/Cache/QuerySearchHelperTest.php b/tests/lib/Files/Cache/QuerySearchHelperTest.php new file mode 100644 index 0000000000..bdf3d79432 --- /dev/null +++ b/tests/lib/Files/Cache/QuerySearchHelperTest.php @@ -0,0 +1,28 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace Test\Files\Cache; + +use Test\TestCase; + +class QuerySearchHelperTest extends TestCase { + +} From df2063ee7b49d051f9081c6fd416dd8791358ada Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 2 Feb 2017 18:20:08 +0100 Subject: [PATCH 2/6] Implement webdav SEARCH Signed-off-by: Robin Appelman --- apps/dav/lib/Files/FileSearchBackend.php | 204 +++++++++++++++++- apps/dav/lib/Server.php | 8 + lib/private/Files/Cache/Cache.php | 45 +++- lib/private/Files/Cache/FailedCache.php | 5 + lib/private/Files/Cache/QuerySearchHelper.php | 37 ++-- lib/private/Files/Cache/Wrapper/CacheJail.php | 6 + .../Files/Cache/Wrapper/CacheWrapper.php | 6 + lib/private/Files/Node/Folder.php | 9 +- .../Files/Search/SearchBinaryOperator.php | 37 ++++ lib/private/Files/Search/SearchQuery.php | 57 +++++ lib/private/Lockdown/Filesystem/NullCache.php | 5 + lib/public/Files/Cache/ICache.php | 12 ++ lib/public/Files/Folder.php | 3 +- .../Files/Search/ISearchBinaryOperator.php | 5 + lib/public/Files/Search/ISearchComparison.php | 3 + lib/public/Files/Search/ISearchOperator.php | 5 +- lib/public/Files/Search/ISearchQuery.php | 24 +++ .../lib/Files/Cache/QuerySearchHelperTest.php | 173 +++++++++++++++ 18 files changed, 610 insertions(+), 34 deletions(-) diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index 2a89c756d3..3d87612aad 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -21,32 +21,73 @@ namespace OCA\DAV\Files; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchOrder; +use OC\Files\Search\SearchQuery; +use OC\Files\View; use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchOrder; +use OCP\Files\Search\ISearchQuery; +use OCP\IUser; +use OCP\Share\IManager; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Tree; use SearchDAV\Backend\ISearchBackend; use SearchDAV\Backend\SearchPropertyDefinition; +use SearchDAV\Backend\SearchResult; +use SearchDAV\XML\BasicSearch; +use SearchDAV\XML\Literal; +use SearchDAV\XML\Operator; +use SearchDAV\XML\Order; class FileSearchBackend implements ISearchBackend { /** @var Tree */ private $tree; + /** @var IUser */ + private $user; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var IManager */ + private $shareManager; + + /** @var View */ + private $view; + /** * FileSearchBackend constructor. * * @param Tree $tree + * @param IUser $user + * @param IRootFolder $rootFolder + * @param IManager $shareManager + * @param View $view + * @internal param IRootFolder $rootFolder */ - public function __construct(Tree $tree) { + public function __construct(Tree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) { $this->tree = $tree; + $this->user = $user; + $this->rootFolder = $rootFolder; + $this->shareManager = $shareManager; + $this->view = $view; } /** - * Search endpoint will be remote.php/dav/files + * Search endpoint will be remote.php/dav * * @return string */ public function getArbiterPath() { - return 'files'; + return ''; } public function isValidScope($href, $depth, $path) { @@ -66,11 +107,162 @@ class FileSearchBackend implements ISearchBackend { public function getPropertyDefinitionsForScope($href, $path) { // all valid scopes support the same schema + //todo dynamically load all propfind properties that are supported return [ - new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + // queryable properties + new SearchPropertyDefinition('{DAV:}displayname', true, false, true), new SearchPropertyDefinition('{DAV:}getcontenttype', 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('{DAV:}getlastmodifed', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), + new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + + // select only properties + new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false), + new SearchPropertyDefinition('{DAV:}getcontentlength', false, true, false), + new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_BOOLEAN), + new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), ]; } + + public function search(BasicSearch $search) { + if (count($search->from) !== 1) { + throw new \InvalidArgumentException('Searching more than one folder is not supported'); + } + $query = $this->transformQuery($search); + $scope = $search->from[0]; + if ($scope->path === null) { + throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead'); + } + $node = $this->tree->getNodeForPath($scope->path); + if (!$node instanceof Directory) { + throw new \InvalidArgumentException('Search is only supported on directories'); + } + + $fileInfo = $node->getFileInfo(); + $folder = $this->rootFolder->get($fileInfo->getPath()); + /** @var Folder $folder $results */ + $results = $folder->search($query); + + return array_map(function (Node $node) { + if ($node instanceof Folder) { + return new SearchResult(new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager), $this->getHrefForNode($node)); + } else { + return new SearchResult(new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager), $this->getHrefForNode($node)); + } + }, $results); + } + + /** + * @param Node $node + * @return string + */ + private function getHrefForNode(Node $node) { + $base = '/files/' . $this->user->getUID(); + return $base . $this->view->getRelativePath($node->getPath()); + } + + /** + * @param BasicSearch $query + * @return ISearchQuery + */ + private function transformQuery(BasicSearch $query) { + // TODO offset, limit + $orders = array_map([$this, 'mapSearchOrder'], $query->orderBy); + return new SearchQuery($this->transformSearchOperation($query->where), 0, 0, $orders); + } + + /** + * @param Order $order + * @return ISearchOrder + */ + private function mapSearchOrder(Order $order) { + return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToCollumn($order->property)); + } + + /** + * @param Operator $operator + * @return ISearchOperator + */ + private function transformSearchOperation(Operator $operator) { + list(, $trimmedType) = explode('}', $operator->type); + switch ($operator->type) { + case Operator::OPERATION_AND: + case Operator::OPERATION_OR: + case Operator::OPERATION_NOT: + $arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments); + return new SearchBinaryOperator($trimmedType, $arguments); + case Operator::OPERATION_EQUAL: + case Operator::OPERATION_GREATER_OR_EQUAL_THAN: + case Operator::OPERATION_GREATER_THAN: + case Operator::OPERATION_LESS_OR_EQUAL_THAN: + case Operator::OPERATION_LESS_THAN: + case Operator::OPERATION_IS_LIKE: + if (count($operator->arguments) !== 2) { + throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation'); + } + if (gettype($operator->arguments[0]) !== 'string') { + throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property'); + } + if (!($operator->arguments[1] instanceof Literal)) { + throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal'); + } + return new SearchComparison($trimmedType, $this->mapPropertyNameToCollumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value)); + case Operator::OPERATION_IS_COLLECTION: + return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE); + default: + throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType. ' (' . $operator->type . ')'); + } + } + + /** + * @param string $propertyName + * @return string + */ + private function mapPropertyNameToCollumn($propertyName) { + /** + * new SearchPropertyDefinition('{DAV:}displayname', true, false, true), + * new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), + * new SearchPropertyDefinition('{DAV:}getlastmodifed', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), + * new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + */ + + switch ($propertyName) { + case '{DAV:}displayname': + return 'name'; + case '{DAV:}getcontenttype': + return 'mimetype'; + case '{DAV:}getlastmodifed': + return 'mtime'; + case FilesPlugin::SIZE_PROPERTYNAME: + return 'size'; + default: + throw new \InvalidArgumentException('Unsupported property for search or order: ' . $propertyName); + } + } + + private function castValue($propertyName, $value) { + $allProps = $this->getPropertyDefinitionsForScope('', ''); + foreach ($allProps as $prop) { + if ($prop->name === $propertyName) { + $dataType = $prop->dataType; + switch ($dataType) { + case SearchPropertyDefinition::DATATYPE_BOOLEAN: + return $value === 'yes'; + case SearchPropertyDefinition::DATATYPE_DECIMAL: + case SearchPropertyDefinition::DATATYPE_INTEGER: + case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER: + return 0 + $value; + default: + return $value; + } + } + } + return $value; + } } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 79c4301a8d..57f89f708a 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -51,6 +51,7 @@ use OCP\SabrePluginEvent; use Sabre\CardDAV\VCFExportPlugin; use Sabre\DAV\Auth\Plugin; use OCA\DAV\Connector\Sabre\TagsPlugin; +use SearchDAV\DAV\SearchPlugin; class Server { @@ -222,6 +223,13 @@ class Server { \OC::$server->getGroupManager(), $userFolder )); + $this->server->addPlugin(new SearchPlugin(new \OCA\DAV\Files\FileSearchBackend( + $this->server->tree, + $user, + \OC::$server->getRootFolder(), + \OC::$server->getShareManager(), + $view + ))); } } }); diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 7e7ebd795a..b0527d801d 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -36,9 +36,11 @@ namespace OC\Files\Cache; +use Doctrine\DBAL\Driver\Statement; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use \OCP\Files\IMimeTypeLoader; +use OCP\Files\Search\ISearchQuery; use OCP\IDBConnection; /** @@ -79,6 +81,9 @@ class Cache implements ICache { */ protected $connection; + /** @var QuerySearchHelper */ + protected $querySearchHelper; + /** * @param \OC\Files\Storage\Storage|string $storage */ @@ -95,6 +100,7 @@ class Cache implements ICache { $this->storageCache = new Storage($storage); $this->mimetypeLoader = \OC::$server->getMimeTypeLoader(); $this->connection = \OC::$server->getDatabaseConnection(); + $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader); } /** @@ -350,7 +356,7 @@ class Cache implements ICache { $queryParts[] = '`mtime`'; } } elseif ($name === 'encrypted') { - if(isset($data['encryptedVersion'])) { + if (isset($data['encryptedVersion'])) { $value = $data['encryptedVersion']; } else { // Boolean to integer conversion @@ -599,9 +605,17 @@ class Cache implements ICache { [$this->getNumericStorageId(), $pattern] ); + return $this->searchResultToCacheEntries($result); + } + + /** + * @param Statement $result + * @return CacheEntry[] + */ + private function searchResultToCacheEntries(Statement $result) { $files = $result->fetchAll(); - return array_map(function(array $data) { + return array_map(function (array $data) { return self::cacheEntryFromData($data, $this->mimetypeLoader); }, $files); } @@ -624,14 +638,29 @@ class Cache implements ICache { $mimetype = $this->mimetypeLoader->getId($mimetype); $result = $this->connection->executeQuery($sql, array($mimetype, $this->getNumericStorageId())); - $files = $result->fetchAll(); - - return array_map(function (array $data) { - return self::cacheEntryFromData($data, $this->mimetypeLoader); - }, $files); + return $this->searchResultToCacheEntries($result); } - /** + public function searchQuery(ISearchQuery $searchQuery) { + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $query = $builder->select(['fileid', 'storage', 'path', 'parent', 'name', 'mimetype', 'mimepart', 'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum']) + ->from('filecache') + ->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->getNumericStorageId()))) + ->andWhere($this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation())); + + if ($searchQuery->getLimit()) { + $query->setMaxResults($searchQuery->getLimit()); + } + if ($searchQuery->getOffset()) { + $query->setFirstResult($searchQuery->getOffset()); + } + + $result = $query->execute(); + return $this->searchResultToCacheEntries($result); + } + + /** * Search for files by tag of a given users. * * Note that every user can tag files differently. diff --git a/lib/private/Files/Cache/FailedCache.php b/lib/private/Files/Cache/FailedCache.php index 3a0424b5e2..932a5e5181 100644 --- a/lib/private/Files/Cache/FailedCache.php +++ b/lib/private/Files/Cache/FailedCache.php @@ -24,6 +24,7 @@ namespace OC\Files\Cache; use OCP\Constants; use OCP\Files\Cache\ICache; +use OCP\Files\Search\ISearchQuery; /** * Storage placeholder to represent a missing precondition, storage unavailable @@ -125,6 +126,10 @@ class FailedCache implements ICache { return []; } + public function searchQuery(ISearchQuery $query) { + return []; + } + public function getAll() { return []; } diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 60f4e484da..931f258ec5 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -30,7 +30,7 @@ use OCP\Files\Search\ISearchOperator; /** * Tools for transforming search queries into database queries */ -class QuerySearchUtil { +class QuerySearchHelper { static protected $searchOperatorMap = [ ISearchComparison::COMPARE_LIKE => 'iLike', ISearchComparison::COMPARE_EQUAL => 'eq', @@ -73,9 +73,9 @@ class QuerySearchUtil { throw new \InvalidArgumentException('Binary operators inside "not" is not supported'); } case ISearchBinaryOperator::OPERATOR_AND: - return $expr->andX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1])); + return $expr->andX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1])); case ISearchBinaryOperator::OPERATOR_OR: - return $expr->orX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1])); + return $expr->orX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1])); default: throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType()); } @@ -87,13 +87,11 @@ class QuerySearchUtil { } private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) { - if (!$this->isValidComparison($comparison)) { - throw new \InvalidArgumentException('Invalid comparison ' . $operator->getType() . ' on field ' . $operator->getField()); - } + $this->validateComparison($comparison); - list($field, $value) = $this->getOperatorFieldAndValue($comparison); - if (isset($operatorMap[$comparison->getType()])) { - $queryOperator = $operatorMap[$comparison->getType()]; + list($field, $value, $type) = $this->getOperatorFieldAndValue($comparison); + if (isset($operatorMap[$type])) { + $queryOperator = $operatorMap[$type]; return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value)); } else { throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType()); @@ -103,6 +101,7 @@ class QuerySearchUtil { private function getOperatorFieldAndValue(ISearchComparison $operator) { $field = $operator->getField(); $value = $operator->getValue(); + $type = $operator->getType(); if ($field === 'mimetype') { if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) { $value = $this->mimetypeLoader->getId($value); @@ -111,16 +110,20 @@ class QuerySearchUtil { if (preg_match('|(.+)/%|', $value, $matches)) { $field = 'mimepart'; $value = $this->mimetypeLoader->getId($matches[1]); + $type = ISearchComparison::COMPARE_EQUAL; + } + if (strpos($value, '%') !== false) { + throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported'); } } } - return [$field, $value]; + return [$field, $value, $type]; } - private function isValidComparison(ISearchComparison $operator) { + private function validateComparison(ISearchComparison $operator) { $types = [ 'mimetype' => 'string', - 'mtime' => \DateTime::class, + 'mtime' => 'integer', 'name' => 'string', 'size' => 'integer' ]; @@ -132,13 +135,15 @@ class QuerySearchUtil { ]; if (!isset($types[$operator->getField()])) { - return false; + throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField()); } $type = $types[$operator->getField()]; - if (gettype($operator->getValue()) !== $type && !(is_a($operator->getValue(), $type))) { - return false; + if (gettype($operator->getValue()) !== $type) { + throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField()); + } + if (!in_array($operator->getType(), $comparisons[$operator->getField()])) { + throw new \InvalidArgumentException('Unsupported comparison for field ' . $operator->getField() . ': ' . $operator->getType()); } - return in_array($operator->getType(), $comparisons[$operator->getField()]); } private function getParameterForValue(IQueryBuilder $builder, $value) { diff --git a/lib/private/Files/Cache/Wrapper/CacheJail.php b/lib/private/Files/Cache/Wrapper/CacheJail.php index 894fbcc803..ebab20fbae 100644 --- a/lib/private/Files/Cache/Wrapper/CacheJail.php +++ b/lib/private/Files/Cache/Wrapper/CacheJail.php @@ -28,6 +28,7 @@ namespace OC\Files\Cache\Wrapper; use OC\Files\Cache\Cache; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Search\ISearchQuery; /** * Jail to a subdirectory of the wrapped cache @@ -218,6 +219,11 @@ class CacheJail extends CacheWrapper { return $this->formatSearchResults($results); } + public function searchQuery(ISearchQuery $query) { + $results = $this->getCache()->searchQuery($query); + return $this->formatSearchResults($results); + } + /** * search for files by mimetype * diff --git a/lib/private/Files/Cache/Wrapper/CacheWrapper.php b/lib/private/Files/Cache/Wrapper/CacheWrapper.php index 83fe7e5f43..1463d1467b 100644 --- a/lib/private/Files/Cache/Wrapper/CacheWrapper.php +++ b/lib/private/Files/Cache/Wrapper/CacheWrapper.php @@ -31,6 +31,7 @@ namespace OC\Files\Cache\Wrapper; use OC\Files\Cache\Cache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Cache\ICache; +use OCP\Files\Search\ISearchQuery; class CacheWrapper extends Cache { /** @@ -229,6 +230,11 @@ class CacheWrapper extends Cache { return array_map(array($this, 'formatCacheEntry'), $results); } + public function searchQuery(ISearchQuery $query) { + $results = $this->getCache()->searchQuery($query); + return array_map(array($this, 'formatCacheEntry'), $results); + } + /** * search for files by tag * diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index fd907f708f..45372d0fed 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -33,6 +33,7 @@ use OCP\Files\FileInfo; use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\Files\Search\ISearchOperator; class Folder extends Node implements \OCP\Files\Folder { /** @@ -190,11 +191,15 @@ class Folder extends Node implements \OCP\Files\Folder { /** * search for files with the name matching $query * - * @param string $query + * @param string|ISearchOperator $query * @return \OC\Files\Node\Node[] */ public function search($query) { - return $this->searchCommon('search', array('%' . $query . '%')); + if (is_string($query)) { + return $this->searchCommon('search', array('%' . $query . '%')); + } else { + return $this->searchCommon('searchQuery', array($query)); + } } /** diff --git a/lib/private/Files/Search/SearchBinaryOperator.php b/lib/private/Files/Search/SearchBinaryOperator.php index 15944e2776..c9466d8b9e 100644 --- a/lib/private/Files/Search/SearchBinaryOperator.php +++ b/lib/private/Files/Search/SearchBinaryOperator.php @@ -18,3 +18,40 @@ * along with this program. If not, see . * */ + +namespace OC\Files\Search; + +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchOperator; + +class SearchBinaryOperator implements ISearchBinaryOperator { + /** @var string */ + private $type; + /** @var ISearchOperator[] */ + private $arguments; + + /** + * SearchBinaryOperator constructor. + * + * @param string $type + * @param ISearchOperator[] $arguments + */ + public function __construct($type, array $arguments) { + $this->type = $type; + $this->arguments = $arguments; + } + + /** + * @return string + */ + public function getType() { + return $this->type; + } + + /** + * @return ISearchOperator[] + */ + public function getArguments() { + return $this->arguments; + } +} diff --git a/lib/private/Files/Search/SearchQuery.php b/lib/private/Files/Search/SearchQuery.php index a017d3e245..8a0478ae98 100644 --- a/lib/private/Files/Search/SearchQuery.php +++ b/lib/private/Files/Search/SearchQuery.php @@ -21,3 +21,60 @@ namespace OC\Files\Search; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchOrder; +use OCP\Files\Search\ISearchQuery; + +class SearchQuery implements ISearchQuery { + /** @var ISearchOperator */ + private $searchOperation; + /** @var integer */ + private $limit; + /** @var integer */ + private $offset; + /** @var ISearchOrder[] */ + private $order; + + /** + * SearchQuery constructor. + * + * @param ISearchOperator $searchOperation + * @param int $limit + * @param int $offset + * @param array $order + */ + public function __construct(ISearchOperator $searchOperation, $limit, $offset, array $order) { + $this->searchOperation = $searchOperation; + $this->limit = $limit; + $this->offset = $offset; + $this->order = $order; + } + + /** + * @return ISearchOperator + */ + public function getSearchOperation() { + return $this->searchOperation; + } + + /** + * @return int + */ + public function getLimit() { + return $this->limit; + } + + /** + * @return int + */ + public function getOffset() { + return $this->offset; + } + + /** + * @return ISearchOrder[] + */ + public function getOrder() { + return $this->order; + } +} diff --git a/lib/private/Lockdown/Filesystem/NullCache.php b/lib/private/Lockdown/Filesystem/NullCache.php index 8c6b5258aa..9cb8016194 100644 --- a/lib/private/Lockdown/Filesystem/NullCache.php +++ b/lib/private/Lockdown/Filesystem/NullCache.php @@ -24,6 +24,7 @@ use OCP\Constants; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\FileInfo; +use OCP\Files\Search\ISearchQuery; class NullCache implements ICache { public function getNumericStorageId() { @@ -103,6 +104,10 @@ class NullCache implements ICache { return []; } + public function searchQuery(ISearchQuery $query) { + return []; + } + public function searchByTag($tag, $userId) { return []; } diff --git a/lib/public/Files/Cache/ICache.php b/lib/public/Files/Cache/ICache.php index 7d01b1a290..63993d0a8c 100644 --- a/lib/public/Files/Cache/ICache.php +++ b/lib/public/Files/Cache/ICache.php @@ -21,6 +21,8 @@ */ namespace OCP\Files\Cache; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchQuery; /** * Metadata cache for a storage @@ -212,6 +214,16 @@ interface ICache { */ public function searchByMime($mimetype); + /** + * Search for files with a flexible query + * + * @param ISearchQuery $query + * @return ICacheEntry[] + * @throw \InvalidArgumentException if the cache is unable to perform the query + * @since 12.0.0 + */ + public function searchQuery(ISearchQuery $query); + /** * Search for files by tag of a given users. * diff --git a/lib/public/Files/Folder.php b/lib/public/Files/Folder.php index 8f8576d850..52a4b30319 100644 --- a/lib/public/Files/Folder.php +++ b/lib/public/Files/Folder.php @@ -30,6 +30,7 @@ // use OCP namespace for all classes that are considered public. // This means that they should be used by apps instead of the internal ownCloud classes namespace OCP\Files; +use OCP\Files\Search\ISearchQuery; /** * @since 6.0.0 @@ -115,7 +116,7 @@ interface Folder extends Node { /** * search for files with the name matching $query * - * @param string $query + * @param string|ISearchQuery $query * @return \OCP\Files\Node[] * @since 6.0.0 */ diff --git a/lib/public/Files/Search/ISearchBinaryOperator.php b/lib/public/Files/Search/ISearchBinaryOperator.php index 248e3de56b..d5a2d5dc02 100644 --- a/lib/public/Files/Search/ISearchBinaryOperator.php +++ b/lib/public/Files/Search/ISearchBinaryOperator.php @@ -21,6 +21,9 @@ namespace OCP\Files\Search; +/** + * @since 12.0.0 + */ interface ISearchBinaryOperator extends ISearchOperator { const OPERATOR_AND = 'and'; const OPERATOR_OR = 'or'; @@ -32,6 +35,7 @@ interface ISearchBinaryOperator extends ISearchOperator { * One of the ISearchBinaryOperator::OPERATOR_* constants * * @return string + * @since 12.0.0 */ public function getType(); @@ -41,6 +45,7 @@ interface ISearchBinaryOperator extends ISearchOperator { * One argument for the 'not' operator and two for 'and' and 'or' * * @return ISearchOperator[] + * @since 12.0.0 */ public function getArguments(); } diff --git a/lib/public/Files/Search/ISearchComparison.php b/lib/public/Files/Search/ISearchComparison.php index 0534221861..e5175ee311 100644 --- a/lib/public/Files/Search/ISearchComparison.php +++ b/lib/public/Files/Search/ISearchComparison.php @@ -33,6 +33,7 @@ interface ISearchComparison extends ISearchOperator { * Get the type of comparison, one of the ISearchComparison::COMPARE_* constants * * @return string + * @since 12.0.0 */ public function getType(); @@ -42,6 +43,7 @@ interface ISearchComparison extends ISearchOperator { * i.e. 'size', 'name' or 'mimetype' * * @return string + * @since 12.0.0 */ public function getField(); @@ -49,6 +51,7 @@ interface ISearchComparison extends ISearchOperator { * Get the value to compare the field with * * @return string|integer|\DateTime + * @since 12.0.0 */ public function getValue(); } diff --git a/lib/public/Files/Search/ISearchOperator.php b/lib/public/Files/Search/ISearchOperator.php index fc1db57f4c..047792bc78 100644 --- a/lib/public/Files/Search/ISearchOperator.php +++ b/lib/public/Files/Search/ISearchOperator.php @@ -21,6 +21,9 @@ namespace OCP\Files\Search; -interface ISearchCondition { +/** + * @since 12.0.0 + */ +interface ISearchOperator { } diff --git a/lib/public/Files/Search/ISearchQuery.php b/lib/public/Files/Search/ISearchQuery.php index f9c07533a1..5a701b321b 100644 --- a/lib/public/Files/Search/ISearchQuery.php +++ b/lib/public/Files/Search/ISearchQuery.php @@ -30,4 +30,28 @@ interface ISearchQuery { * @since 12.0.0 */ public function getSearchOperation(); + + /** + * Get the maximum number of results to return + * + * @return integer + * @since 12.0.0 + */ + public function getLimit(); + + /** + * Get the offset for returned results + * + * @return integer + * @since 12.0.0 + */ + public function getOffset(); + + /** + * The fields and directions to order by + * + * @return ISearchOrder[] + * @since 12.0.0 + */ + public function getOrder(); } diff --git a/tests/lib/Files/Cache/QuerySearchHelperTest.php b/tests/lib/Files/Cache/QuerySearchHelperTest.php index bdf3d79432..beb0981d2d 100644 --- a/tests/lib/Files/Cache/QuerySearchHelperTest.php +++ b/tests/lib/Files/Cache/QuerySearchHelperTest.php @@ -21,8 +21,181 @@ namespace Test\Files\Cache; +use OC\DB\QueryBuilder\Literal; +use OC\Files\Cache\QuerySearchHelper; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; use Test\TestCase; +/** + * @group DB + */ class QuerySearchHelperTest extends TestCase { + /** @var IQueryBuilder */ + private $builder; + /** @var IMimeTypeLoader|\PHPUnit_Framework_MockObject_MockObject */ + private $mimetypeLoader; + + /** @var QuerySearchHelper */ + private $querySearchHelper; + + /** @var integer */ + private $numericStorageId; + + public function setUp() { + parent::setUp(); + $this->builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $this->mimetypeLoader = $this->createMock(IMimeTypeLoader::class); + + $this->mimetypeLoader->expects($this->any()) + ->method('getId') + ->willReturnMap([ + ['text', 1], + ['text/plain', 2], + ['text/xml', 3], + ['image/jpg', 4], + ['image/png', 5], + ['image', 6], + ]); + + $this->mimetypeLoader->expects($this->any()) + ->method('getMimetypeById') + ->willReturnMap([ + [1, 'text'], + [2, 'text/plain'], + [3, 'text/xml'], + [4, 'image/jpg'], + [5, 'image/png'], + [6, 'image'] + ]); + + $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader); + $this->numericStorageId = 10000; + + $this->builder->select(['fileid']) + ->from('filecache') + ->where($this->builder->expr()->eq('storage', new Literal($this->numericStorageId))); + } + + public function tearDown() { + parent::tearDown(); + + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $builder->delete('filecache') + ->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->numericStorageId, IQueryBuilder::PARAM_INT))); + + $builder->execute(); + } + + private function addCacheEntry(array $data) { + $data['storage'] = $this->numericStorageId; + $data['etag'] = 'unimportant'; + $data['storage_mtime'] = $data['mtime']; + if (!isset($data['path'])) { + $data['path'] = 'random/' . $this->getUniqueID(); + } + $data['path_hash'] = md5($data['path']); + if (!isset($data['mtime'])) { + $data['mtime'] = 100; + } + if (!isset($data['size'])) { + $data['size'] = 100; + } + $data['name'] = basename($data['path']); + $data['parent'] = -1; + if (isset($data['mimetype'])) { + list($mimepart,) = explode('/', $data['mimetype']); + $data['mimepart'] = $this->mimetypeLoader->getId($mimepart); + $data['mimetype'] = $this->mimetypeLoader->getId($data['mimetype']); + } else { + $data['mimepart'] = 1; + $data['mimetype'] = 1; + } + + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $values = []; + foreach ($data as $key => $value) { + $values[$key] = $builder->createNamedParameter($value); + } + + $builder->insert('filecache') + ->values($values) + ->execute(); + } + + private function search(ISearchOperator $operator) { + $dbOperator = $this->querySearchHelper->searchOperatorToDBExpr($this->builder, $operator); + $this->builder->andWhere($dbOperator); + return $this->builder->execute()->fetchAll(\PDO::FETCH_COLUMN); + } + + public function comparisonProvider() { + return [ + [new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), [1002]], + [new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [1001]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 125), []], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), [1001, 1002]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), [1001]], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), [1001, 1002]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpg'), [1001]], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'image/%'), [1001, 1002]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), + new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [1001] + ]), [1001]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 100), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), + ]), [1001, 1002]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), + ]), [1001]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), + ]), [1001]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), + ]), [1002]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%bar'), + ]), [1002]], + + ]; + } + + /** + * @dataProvider comparisonProvider + * + * @param ISearchOperator $operator + * @param array $fileIds + */ + public function testComparison(ISearchOperator $operator, array $fileIds) { + $this->addCacheEntry([ + 'path' => 'foobar', + 'fileid' => 1001, + 'mtime' => 100, + 'size' => 50, + 'mimetype' => 'image/jpg' + ]); + + $this->addCacheEntry([ + 'path' => 'fooasd', + 'fileid' => 1002, + 'mtime' => 150, + 'size' => 50, + 'mimetype' => 'image/png' + ]); + + $results = $this->search($operator); + + $this->assertEquals($fileIds, $results); + } } From e6a896f2f076507c33a02962035b29afb24dad05 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 21 Feb 2017 16:14:11 +0100 Subject: [PATCH 3/6] add tests for searchQuery Signed-off-by: Robin Appelman --- lib/public/Files/Search/ISearchComparison.php | 3 + tests/lib/Files/Cache/CacheTest.php | 74 +++++++++++++++---- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/lib/public/Files/Search/ISearchComparison.php b/lib/public/Files/Search/ISearchComparison.php index e5175ee311..5468260f00 100644 --- a/lib/public/Files/Search/ISearchComparison.php +++ b/lib/public/Files/Search/ISearchComparison.php @@ -21,6 +21,9 @@ namespace OCP\Files\Search; +/** + * @since 12.0.0 + */ interface ISearchComparison extends ISearchOperator { const COMPARE_EQUAL = 'eq'; const COMPARE_GREATER_THAN = 'gt'; diff --git a/tests/lib/Files/Cache/CacheTest.php b/tests/lib/Files/Cache/CacheTest.php index 4c4f43d63d..1bcf8832c6 100644 --- a/tests/lib/Files/Cache/CacheTest.php +++ b/tests/lib/Files/Cache/CacheTest.php @@ -11,6 +11,9 @@ namespace Test\Files\Cache; use Doctrine\DBAL\Platforms\MySqlPlatform; use OC\Files\Cache\Cache; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchQuery; +use OCP\Files\Search\ISearchComparison; class LongId extends \OC\Files\Storage\Temporary { public function getId() { @@ -111,15 +114,15 @@ class CacheTest extends \Test\TestCase { * @dataProvider folderDataProvider */ public function testFolder($folder) { - if(strpos($folder, 'F09F9890')) { + if (strpos($folder, 'F09F9890')) { // 4 byte UTF doesn't work on mysql $params = \OC::$server->getDatabaseConnection()->getParams(); - if(\OC::$server->getDatabaseConnection()->getDatabasePlatform() instanceof MySqlPlatform && $params['charset'] !== 'utf8mb4') { + if (\OC::$server->getDatabaseConnection()->getDatabasePlatform() instanceof MySqlPlatform && $params['charset'] !== 'utf8mb4') { $this->markTestSkipped('MySQL doesn\'t support 4 byte UTF-8'); } } - $file2 = $folder.'/bar'; - $file3 = $folder.'/foo'; + $file2 = $folder . '/bar'; + $file3 = $folder . '/foo'; $data1 = array('size' => 100, 'mtime' => 50, 'mimetype' => 'httpd/unix-directory'); $fileData = array(); $fileData['bar'] = array('size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'); @@ -138,7 +141,7 @@ class CacheTest extends \Test\TestCase { } } - $file4 = $folder.'/unkownSize'; + $file4 = $folder . '/unkownSize'; $fileData['unkownSize'] = array('size' => -1, 'mtime' => 25, 'mimetype' => 'foo/file'); $this->cache->put($file4, $fileData['unkownSize']); @@ -155,8 +158,8 @@ class CacheTest extends \Test\TestCase { $this->assertEquals(0, $this->cache->calculateFolderSize($folder)); $this->cache->remove($folder); - $this->assertFalse($this->cache->inCache($folder.'/foo')); - $this->assertFalse($this->cache->inCache($folder.'/bar')); + $this->assertFalse($this->cache->inCache($folder . '/foo')); + $this->assertFalse($this->cache->inCache($folder . '/bar')); } public function testRemoveRecursive() { @@ -165,7 +168,7 @@ class CacheTest extends \Test\TestCase { $folders = ['folder', 'folder/subfolder', 'folder/sub2', 'folder/sub2/sub3']; $files = ['folder/foo.txt', 'folder/bar.txt', 'folder/subfolder/asd.txt', 'folder/sub2/qwerty.txt', 'folder/sub2/sub3/foo.txt']; - foreach($folders as $folder){ + foreach ($folders as $folder) { $this->cache->put($folder, $folderData); } foreach ($files as $file) { @@ -360,7 +363,9 @@ class CacheTest extends \Test\TestCase { $this->assertEquals(2, count($results)); - usort($results, function($value1, $value2) { return $value1['name'] >= $value2['name']; }); + usort($results, function ($value1, $value2) { + return $value1['name'] >= $value2['name']; + }); $this->assertEquals('folder', $results[0]['name']); $this->assertEquals('foo', $results[1]['name']); @@ -368,11 +373,15 @@ class CacheTest extends \Test\TestCase { // use tag id $tags = $tagManager->getTagsForUser($userId); $this->assertNotEmpty($tags); - $tags = array_filter($tags, function($tag) { return $tag->getName() === 'tag2'; }); + $tags = array_filter($tags, function ($tag) { + return $tag->getName() === 'tag2'; + }); $results = $this->cache->searchByTag(current($tags)->getId(), $userId); $this->assertEquals(3, count($results)); - usort($results, function($value1, $value2) { return $value1['name'] >= $value2['name']; }); + usort($results, function ($value1, $value2) { + return $value1['name'] >= $value2['name']; + }); $this->assertEquals('folder', $results[0]['name']); $this->assertEquals('foo2', $results[1]['name']); @@ -383,7 +392,42 @@ class CacheTest extends \Test\TestCase { $this->logout(); $user = \OC::$server->getUserManager()->get($userId); - if ($user !== null) { $user->delete(); } + if ($user !== null) { + $user->delete(); + } + } + + function testSearchByQuery() { + $file1 = 'folder'; + $file2 = 'folder/foobar'; + $file3 = 'folder/foo'; + $data1 = array('size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'); + $fileData = array(); + $fileData['foobar'] = array('size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file'); + $fileData['foo'] = array('size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file'); + + $this->cache->put($file1, $data1); + $this->cache->put($file2, $fileData['foobar']); + $this->cache->put($file3, $fileData['foo']); + + $this->assertCount(1, $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foo') + , 10, 0, []))); + $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%') + , 10, 0, []))); + $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'foo/file') + , 10, 0, []))); + $this->assertCount(3, $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'foo/%') + , 10, 0, []))); + $this->assertCount(1, $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'size', 100) + , 10, 0, []))); + $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', 100) + , 10, 0, []))); } function testMove() { @@ -626,9 +670,9 @@ class CacheTest extends \Test\TestCase { public function escapingProvider() { return [ - ['foo'], - ['o%'], - ['oth_r'], + ['foo'], + ['o%'], + ['oth_r'], ]; } From 709f64d39699331f197dd2d93b9d62980a2ba145 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 21 Feb 2017 18:18:41 +0100 Subject: [PATCH 4/6] add tests for filesearchbackend Signed-off-by: Robin Appelman --- apps/dav/lib/Files/FileSearchBackend.php | 11 +- .../unit/Files/FileSearchBackendTest.php | 299 ++++++++++++++++++ .../lib/Files/Cache/QuerySearchHelperTest.php | 3 + 3 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 apps/dav/tests/unit/Files/FileSearchBackendTest.php diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index 3d87612aad..c429a1727f 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -130,6 +130,10 @@ class FileSearchBackend implements ISearchBackend { ]; } + /** + * @param BasicSearch $search + * @return SearchResult[] + */ public function search(BasicSearch $search) { if (count($search->from) !== 1) { throw new \InvalidArgumentException('Searching more than one folder is not supported'); @@ -225,13 +229,6 @@ class FileSearchBackend implements ISearchBackend { * @return string */ private function mapPropertyNameToCollumn($propertyName) { - /** - * new SearchPropertyDefinition('{DAV:}displayname', true, false, true), - * new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), - * new SearchPropertyDefinition('{DAV:}getlastmodifed', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), - * new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), - */ - switch ($propertyName) { case '{DAV:}displayname': return 'name'; diff --git a/apps/dav/tests/unit/Files/FileSearchBackendTest.php b/apps/dav/tests/unit/Files/FileSearchBackendTest.php new file mode 100644 index 0000000000..24b9a9c51e --- /dev/null +++ b/apps/dav/tests/unit/Files/FileSearchBackendTest.php @@ -0,0 +1,299 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Tests\Files; + +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchQuery; +use OC\Files\View; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Files\FileSearchBackend; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Search\ISearchComparison; +use OCP\IUser; +use OCP\Share\IManager; +use Sabre\DAV\Tree; +use SearchDAV\XML\BasicSearch; +use SearchDAV\XML\Literal; +use SearchDAV\XML\Operator; +use SearchDAV\XML\Scope; +use Test\TestCase; + +class FileSearchBackendTest extends TestCase { + /** @var Tree|\PHPUnit_Framework_MockObject_MockObject */ + private $tree; + + /** @var IUser */ + private $user; + + /** @var IRootFolder|\PHPUnit_Framework_MockObject_MockObject */ + private $rootFolder; + + /** @var IManager|\PHPUnit_Framework_MockObject_MockObject */ + private $shareManager; + + /** @var View|\PHPUnit_Framework_MockObject_MockObject */ + private $view; + + /** @var Folder|\PHPUnit_Framework_MockObject_MockObject */ + private $searchFolder; + + /** @var FileSearchBackend */ + private $search; + + /** @var Directory|\PHPUnit_Framework_MockObject_MockObject */ + private $davFolder; + + protected function setUp() { + parent::setUp(); + + $this->user = $this->createMock(IUser::class); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('test'); + + $this->tree = $this->getMockBuilder(Tree::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->view = $this->getMockBuilder(View::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->view->expects($this->any()) + ->method('getRelativePath') + ->willReturnArgument(0); + + $this->rootFolder = $this->createMock(IRootFolder::class); + + $this->shareManager = $this->createMock(IManager::class); + + $this->searchFolder = $this->createMock(Folder::class); + + $fileInfo = $this->createMock(FileInfo::class); + + $this->davFolder = $this->createMock(Directory::class); + + $this->davFolder->expects($this->any()) + ->method('getFileInfo') + ->willReturn($fileInfo); + + $this->rootFolder->expects($this->any()) + ->method('get') + ->willReturn($this->searchFolder); + + $this->search = new FileSearchBackend($this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view); + } + + public function testSearchFilename() { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'name', + 'foo' + ), + 0, + 0, + [] + )) + ->will($this->returnValue([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') + ])); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo'); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchMimetype() { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'mimetype', + 'foo' + ), + 0, + 0, + [] + )) + ->will($this->returnValue([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') + ])); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo'); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchSize() { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_GREATER_THAN, + 'size', + 10 + ), + 0, + 0, + [] + )) + ->will($this->returnValue([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') + ])); + + $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchMtime() { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_GREATER_THAN, + 'mtime', + 10 + ), + 0, + 0, + [] + )) + ->will($this->returnValue([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') + ])); + + $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodifed', 10); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchIsCollection() { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'mimetype', + FileInfo::MIMETYPE_FOLDER + ), + 0, + 0, + [] + )) + ->will($this->returnValue([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') + ])); + + $query = $this->getBasicQuery(Operator::OPERATION_IS_COLLECTION, 'yes'); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testSearchInvalidProp() { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->never()) + ->method('search'); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo'); + $this->search->search($query); + } + + private function getBasicQuery($type, $property, $value = null) { + $query = new BasicSearch(); + $scope = new Scope('/', 'infinite'); + $scope->path = '/'; + $query->from = [$scope]; + $query->orderBy = []; + $query->select = []; + if (is_null($value)) { + $query->where = new Operator( + $type, + [new Literal($property)] + ); + } else { + $query->where = new Operator( + $type, + [$property, new Literal($value)] + ); + } + return $query; + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testSearchNonFolder() { + $davNode = $this->createMock(File::class); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($davNode); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo'); + $this->search->search($query); + } +} diff --git a/tests/lib/Files/Cache/QuerySearchHelperTest.php b/tests/lib/Files/Cache/QuerySearchHelperTest.php index beb0981d2d..f458ef039e 100644 --- a/tests/lib/Files/Cache/QuerySearchHelperTest.php +++ b/tests/lib/Files/Cache/QuerySearchHelperTest.php @@ -196,6 +196,9 @@ class QuerySearchHelperTest extends TestCase { $results = $this->search($operator); + sort($fileIds); + sort($results); + $this->assertEquals($fileIds, $results); } } From d8c89688f439d31fecfeb9e80dcf64b64aa06c91 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 21 Feb 2017 18:20:42 +0100 Subject: [PATCH 5/6] autoloader Signed-off-by: Robin Appelman --- lib/composer/composer/autoload_classmap.php | 10 ++++++++++ lib/composer/composer/autoload_static.php | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 9a1ede021a..f4f80ebdf1 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -141,6 +141,11 @@ return array( 'OCP\\Files\\Notify\\IRenameChange' => $baseDir . '/lib/public/Files/Notify/IRenameChange.php', 'OCP\\Files\\ObjectStore\\IObjectStore' => $baseDir . '/lib/public/Files/ObjectStore/IObjectStore.php', 'OCP\\Files\\ReservedWordException' => $baseDir . '/lib/public/Files/ReservedWordException.php', + 'OCP\\Files\\Search\\ISearchBinaryOperator' => $baseDir . '/lib/public/Files/Search/ISearchBinaryOperator.php', + 'OCP\\Files\\Search\\ISearchComparison' => $baseDir . '/lib/public/Files/Search/ISearchComparison.php', + 'OCP\\Files\\Search\\ISearchOperator' => $baseDir . '/lib/public/Files/Search/ISearchOperator.php', + 'OCP\\Files\\Search\\ISearchOrder' => $baseDir . '/lib/public/Files/Search/ISearchOrder.php', + 'OCP\\Files\\Search\\ISearchQuery' => $baseDir . '/lib/public/Files/Search/ISearchQuery.php', 'OCP\\Files\\SimpleFS\\ISimpleFile' => $baseDir . '/lib/public/Files/SimpleFS/ISimpleFile.php', 'OCP\\Files\\SimpleFS\\ISimpleFolder' => $baseDir . '/lib/public/Files/SimpleFS/ISimpleFolder.php', 'OCP\\Files\\SimpleFS\\ISimpleRoot' => $baseDir . '/lib/public/Files/SimpleFS/ISimpleRoot.php', @@ -509,6 +514,7 @@ return array( 'OC\\Files\\Cache\\HomePropagator' => $baseDir . '/lib/private/Files/Cache/HomePropagator.php', 'OC\\Files\\Cache\\MoveFromCacheTrait' => $baseDir . '/lib/private/Files/Cache/MoveFromCacheTrait.php', 'OC\\Files\\Cache\\Propagator' => $baseDir . '/lib/private/Files/Cache/Propagator.php', + 'OC\\Files\\Cache\\QuerySearchHelper' => $baseDir . '/lib/private/Files/Cache/QuerySearchHelper.php', 'OC\\Files\\Cache\\Scanner' => $baseDir . '/lib/private/Files/Cache/Scanner.php', 'OC\\Files\\Cache\\Storage' => $baseDir . '/lib/private/Files/Cache/Storage.php', 'OC\\Files\\Cache\\StorageGlobal' => $baseDir . '/lib/private/Files/Cache/StorageGlobal.php', @@ -548,6 +554,10 @@ return array( 'OC\\Files\\ObjectStore\\S3ConnectionTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php', 'OC\\Files\\ObjectStore\\StorageObjectStore' => $baseDir . '/lib/private/Files/ObjectStore/StorageObjectStore.php', 'OC\\Files\\ObjectStore\\Swift' => $baseDir . '/lib/private/Files/ObjectStore/Swift.php', + 'OC\\Files\\Search\\SearchBinaryOperator' => $baseDir . '/lib/private/Files/Search/SearchBinaryOperator.php', + 'OC\\Files\\Search\\SearchComparison' => $baseDir . '/lib/private/Files/Search/SearchComparison.php', + 'OC\\Files\\Search\\SearchOrder' => $baseDir . '/lib/private/Files/Search/SearchOrder.php', + 'OC\\Files\\Search\\SearchQuery' => $baseDir . '/lib/private/Files/Search/SearchQuery.php', 'OC\\Files\\SimpleFS\\SimpleFile' => $baseDir . '/lib/private/Files/SimpleFS/SimpleFile.php', 'OC\\Files\\SimpleFS\\SimpleFolder' => $baseDir . '/lib/private/Files/SimpleFS/SimpleFolder.php', 'OC\\Files\\Storage\\Common' => $baseDir . '/lib/private/Files/Storage/Common.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 329773cd6e..48923c9913 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -171,6 +171,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Files\\Notify\\IRenameChange' => __DIR__ . '/../../..' . '/lib/public/Files/Notify/IRenameChange.php', 'OCP\\Files\\ObjectStore\\IObjectStore' => __DIR__ . '/../../..' . '/lib/public/Files/ObjectStore/IObjectStore.php', 'OCP\\Files\\ReservedWordException' => __DIR__ . '/../../..' . '/lib/public/Files/ReservedWordException.php', + 'OCP\\Files\\Search\\ISearchBinaryOperator' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchBinaryOperator.php', + 'OCP\\Files\\Search\\ISearchComparison' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchComparison.php', + 'OCP\\Files\\Search\\ISearchOperator' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchOperator.php', + 'OCP\\Files\\Search\\ISearchOrder' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchOrder.php', + 'OCP\\Files\\Search\\ISearchQuery' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchQuery.php', 'OCP\\Files\\SimpleFS\\ISimpleFile' => __DIR__ . '/../../..' . '/lib/public/Files/SimpleFS/ISimpleFile.php', 'OCP\\Files\\SimpleFS\\ISimpleFolder' => __DIR__ . '/../../..' . '/lib/public/Files/SimpleFS/ISimpleFolder.php', 'OCP\\Files\\SimpleFS\\ISimpleRoot' => __DIR__ . '/../../..' . '/lib/public/Files/SimpleFS/ISimpleRoot.php', @@ -539,6 +544,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Files\\Cache\\HomePropagator' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/HomePropagator.php', 'OC\\Files\\Cache\\MoveFromCacheTrait' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/MoveFromCacheTrait.php', 'OC\\Files\\Cache\\Propagator' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Propagator.php', + 'OC\\Files\\Cache\\QuerySearchHelper' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/QuerySearchHelper.php', 'OC\\Files\\Cache\\Scanner' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Scanner.php', 'OC\\Files\\Cache\\Storage' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Storage.php', 'OC\\Files\\Cache\\StorageGlobal' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/StorageGlobal.php', @@ -578,6 +584,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Files\\ObjectStore\\S3ConnectionTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php', 'OC\\Files\\ObjectStore\\StorageObjectStore' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/StorageObjectStore.php', 'OC\\Files\\ObjectStore\\Swift' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/Swift.php', + 'OC\\Files\\Search\\SearchBinaryOperator' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchBinaryOperator.php', + 'OC\\Files\\Search\\SearchComparison' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchComparison.php', + 'OC\\Files\\Search\\SearchOrder' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchOrder.php', + 'OC\\Files\\Search\\SearchQuery' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchQuery.php', 'OC\\Files\\SimpleFS\\SimpleFile' => __DIR__ . '/../../..' . '/lib/private/Files/SimpleFS/SimpleFile.php', 'OC\\Files\\SimpleFS\\SimpleFolder' => __DIR__ . '/../../..' . '/lib/private/Files/SimpleFS/SimpleFolder.php', 'OC\\Files\\Storage\\Common' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Common.php', From a3e638709b4702156a3ddc0791de1d6ce9fb902e Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Wed, 1 Mar 2017 12:33:25 -0600 Subject: [PATCH 6/6] update 3rdparty submodule Signed-off-by: Morris Jobke --- 3rdparty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty b/3rdparty index 2838fa6777..98fa92c67d 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit 2838fa6777b1427c6c912a5e599a96639ac2b31f +Subproject commit 98fa92c67d735f82ae012786395e660f1513bef7