Explorar el Código

Allow filtering the search results to the users home storage

This is done by adding a

```xml
<d:eq>
    <d:prop>
        <oc:owner-id/>
    </d:prop>
    <d:literal>$userId</d:literal>
</d:eq>
```

clause to the search query.

Searching by `owner-id` can only be done with the current user id
and the comparison can not be inside a `<d:not>` or `<d:or>` statement

Signed-off-by: Robin Appelman <robin@icewind.nl>
tags/v18.0.0beta1
Robin Appelman hace 4 años
padre
commit
c62637da8b
No account linked to committer's email address

+ 75
- 7
apps/dav/lib/Files/FileSearchBackend.php Ver fichero

@@ -119,6 +119,7 @@ class FileSearchBackend implements ISearchBackend {
new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN),
new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, true, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, false),

// select only properties
new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false),
@@ -126,7 +127,6 @@ class FileSearchBackend implements ISearchBackend {
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),
@@ -169,10 +169,12 @@ class FileSearchBackend implements ISearchBackend {
return new SearchResult($davNode, $path);
}, $results);

// Sort again, since the result from multiple storages is appended and not sorted
usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
return $this->sort($a, $b, $search->orderBy);
});
if (!$query->limitToHome()) {
// Sort again, since the result from multiple storages is appended and not sorted
usort($nodes, function (SearchResult $a, SearchResult $b) use ($search) {
return $this->sort($a, $b, $search->orderBy);
});
}

// If a limit is provided use only return that number of files
if ($search->limit->maxResults !== 0) {
@@ -267,11 +269,29 @@ class FileSearchBackend implements ISearchBackend {
* @param Query $query
* @return ISearchQuery
*/
private function transformQuery(Query $query) {
private function transformQuery(Query $query): ISearchQuery {
// TODO offset
$limit = $query->limit;
$orders = array_map([$this, 'mapSearchOrder'], $query->orderBy);
return new SearchQuery($this->transformSearchOperation($query->where), (int)$limit->maxResults, 0, $orders, $this->user);

$limitHome = false;
$ownerProp = $this->extractWhereValue($query->where, FilesPlugin::OWNER_ID_PROPERTYNAME, Operator::OPERATION_EQUAL);
if ($ownerProp !== null) {
if ($ownerProp === $this->user->getUID()) {
$limitHome = true;
} else {
throw new \InvalidArgumentException("Invalid search value for '{http://owncloud.org/ns}owner-id', only the current user id is allowed");
}
}

return new SearchQuery(
$this->transformSearchOperation($query->where),
(int)$limit->maxResults,
0,
$orders,
$this->user,
$limitHome
);
}

/**
@@ -360,4 +380,52 @@ class FileSearchBackend implements ISearchBackend {
return $value;
}
}

/**
* Get a specific property from the were clause
*/
private function extractWhereValue(Operator &$operator, string $propertyName, string $comparison, bool $acceptableLocation = true): ?string {
switch ($operator->type) {
case Operator::OPERATION_AND:
case Operator::OPERATION_OR:
case Operator::OPERATION_NOT:
foreach ($operator->arguments as &$argument) {
$value = $this->extractWhereValue($argument, $propertyName, $comparison, $acceptableLocation && $operator->type === Operator::OPERATION_AND);
if ($value !== null) {
return $value;
}
}
return null;
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 ($operator->arguments[0]->name === $propertyName) {
if ($operator->type === $comparison) {
if ($acceptableLocation) {
if ($operator->arguments[1] instanceof Literal) {
$value = $operator->arguments[1]->value;

// to remove the comparison from the query, we replace it with an empty AND
$operator = new Operator(Operator::OPERATION_AND);

return $value;
} else {
throw new \InvalidArgumentException("searching by '$propertyName' is only allowed with a literal value");
}
} else{
throw new \InvalidArgumentException("searching by '$propertyName' is not allowed inside a '{DAV:}or' or '{DAV:}not'");
}
} else {
throw new \InvalidArgumentException("searching by '$propertyName' is only allowed inside a '$comparison'");
}
} else {
return null;
}
default:
return null;
}
}
}

+ 79
- 0
apps/dav/tests/unit/Files/FileSearchBackendTest.php Ver fichero

@@ -35,7 +35,9 @@ use OCA\DAV\Files\FileSearchBackend;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchQuery;
use OCP\IUser;
use OCP\Share\IManager;
use SearchDAV\Backend\SearchPropertyDefinition;
@@ -308,4 +310,81 @@ class FileSearchBackendTest extends TestCase {
$query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
$this->search->search($query);
}

public function testSearchLimitOwnerBasic() {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);

/** @var ISearchQuery|null $receivedQuery */
$receivedQuery = null;
$this->searchFolder
->method('search')
->will($this->returnCallback(function ($query) use (&$receivedQuery) {
$receivedQuery = $query;
return [
new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
];
}));

$query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
$this->search->search($query);

$this->assertNotNull($receivedQuery);
$this->assertTrue($receivedQuery->limitToHome());

/** @var ISearchBinaryOperator $operator */
$operator = $receivedQuery->getSearchOperation();
$this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
$this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
$this->assertEmpty($operator->getArguments());
}

public function testSearchLimitOwnerNested() {
$this->tree->expects($this->any())
->method('getNodeForPath')
->willReturn($this->davFolder);

/** @var ISearchQuery|null $receivedQuery */
$receivedQuery = null;
$this->searchFolder
->method('search')
->will($this->returnCallback(function ($query) use (&$receivedQuery) {
$receivedQuery = $query;
return [
new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
];
}));

$query = $this->getBasicQuery(\SearchDAV\Query\Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID());
$query->where = new \SearchDAV\Query\Operator(
\SearchDAV\Query\Operator::OPERATION_AND,
[
new \SearchDAV\Query\Operator(
\SearchDAV\Query\Operator::OPERATION_EQUAL,
[new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new \SearchDAV\Query\Literal('image/png')]
),
new \SearchDAV\Query\Operator(
\SearchDAV\Query\Operator::OPERATION_EQUAL,
[new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, true), new \SearchDAV\Query\Literal($this->user->getUID())]
)
]
);
$this->search->search($query);

$this->assertNotNull($receivedQuery);
$this->assertTrue($receivedQuery->limitToHome());

/** @var ISearchBinaryOperator $operator */
$operator = $receivedQuery->getSearchOperation();
$this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
$this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
$this->assertCount(2, $operator->getArguments());

/** @var ISearchBinaryOperator $operator */
$operator = $operator->getArguments()[1];
$this->assertInstanceOf(ISearchBinaryOperator::class, $operator);
$this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType());
$this->assertEmpty($operator->getArguments());
}
}

+ 4
- 1
lib/private/Files/Cache/Cache.php Ver fichero

@@ -793,7 +793,10 @@ class Cache implements ICache {
->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
}

$query->andWhere($this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation()));
$searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
if ($searchExpr) {
$query->andWhere($searchExpr);
}

$this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());


+ 11
- 2
lib/private/Files/Cache/QuerySearchHelper.php Ver fichero

@@ -88,14 +88,18 @@ class QuerySearchHelper {
* @param ISearchOperator $operator
*/
public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) {
return array_map(function ($operator) use ($builder) {
return array_filter(array_map(function ($operator) use ($builder) {
return $this->searchOperatorToDBExpr($builder, $operator);
}, $operators);
}, $operators));
}

public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
$expr = $builder->expr();
if ($operator instanceof ISearchBinaryOperator) {
if (count($operator->getArguments()) === 0) {
return null;
}

switch ($operator->getType()) {
case ISearchBinaryOperator::OPERATOR_NOT:
$negativeOperator = $operator->getArguments()[0];
@@ -121,6 +125,11 @@ class QuerySearchHelper {
private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
$this->validateComparison($comparison);

// "owner" search is done by limiting the storages queries and should not be put in the sql
if ($comparison->getField() === 'owner') {
return null;
}

list($field, $value, $type) = $this->getOperatorFieldAndValue($comparison);
if (isset($operatorMap[$type])) {
$queryOperator = $operatorMap[$type];

+ 23
- 15
lib/private/Files/Node/Folder.php Ver fichero

@@ -34,7 +34,7 @@ use OCP\Files\FileInfo;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchQuery;

class Folder extends Node implements \OCP\Files\Folder {
/**
@@ -191,7 +191,7 @@ class Folder extends Node implements \OCP\Files\Folder {
/**
* search for files with the name matching $query
*
* @param string|ISearchOperator $query
* @param string|ISearchQuery $query
* @return \OC\Files\Node\Node[]
*/
public function search($query) {
@@ -229,6 +229,11 @@ class Folder extends Node implements \OCP\Files\Folder {
* @return \OC\Files\Node\Node[]
*/
private function searchCommon($method, $args) {
$limitToHome = ($method === 'searchQuery')? $args[0]->limitToHome(): false;
if ($limitToHome && count(explode('/', $this->path)) !== 3) {
throw new \InvalidArgumentException('searching by owner is only allows on the users home folder');
}

$files = array();
$rootLength = strlen($this->path);
$mount = $this->root->getMount($this->path);
@@ -252,19 +257,22 @@ class Folder extends Node implements \OCP\Files\Folder {
}
}

$mounts = $this->root->getMountsIn($this->path);
foreach ($mounts as $mount) {
$storage = $mount->getStorage();
if ($storage) {
$cache = $storage->getCache('');

$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
$results = call_user_func_array(array($cache, $method), $args);
foreach ($results as $result) {
$result['internalPath'] = $result['path'];
$result['path'] = $relativeMountPoint . $result['path'];
$result['storage'] = $storage;
$files[] = new \OC\Files\FileInfo($this->path . '/' . $result['path'], $storage, $result['internalPath'], $result, $mount);
if (!$limitToHome) {
$mounts = $this->root->getMountsIn($this->path);
foreach ($mounts as $mount) {
$storage = $mount->getStorage();
if ($storage) {
$cache = $storage->getCache('');

$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
$results = call_user_func_array([$cache, $method], $args);
foreach ($results as $result) {
$result['internalPath'] = $result['path'];
$result['path'] = $relativeMountPoint . $result['path'];
$result['storage'] = $storage;
$files[] = new \OC\Files\FileInfo($this->path . '/' . $result['path'], $storage,
$result['internalPath'], $result, $mount);
}
}
}
}

+ 15
- 1
lib/private/Files/Search/SearchQuery.php Ver fichero

@@ -39,6 +39,7 @@ class SearchQuery implements ISearchQuery {
private $order;
/** @var IUser */
private $user;
private $limitToHome;

/**
* SearchQuery constructor.
@@ -48,13 +49,22 @@ class SearchQuery implements ISearchQuery {
* @param int $offset
* @param array $order
* @param IUser $user
* @param bool $limitToHome
*/
public function __construct(ISearchOperator $searchOperation, $limit, $offset, array $order, IUser $user) {
public function __construct(
ISearchOperator $searchOperation,
int $limit,
int $offset,
array $order,
IUser $user,
bool $limitToHome = false
) {
$this->searchOperation = $searchOperation;
$this->limit = $limit;
$this->offset = $offset;
$this->order = $order;
$this->user = $user;
$this->limitToHome = $limitToHome;
}

/**
@@ -91,4 +101,8 @@ class SearchQuery implements ISearchQuery {
public function getUser() {
return $this->user;
}

public function limitToHome(): bool {
return $this->limitToHome;
}
}

+ 7
- 0
lib/public/Files/Search/ISearchQuery.php Ver fichero

@@ -66,4 +66,11 @@ interface ISearchQuery {
* @since 12.0.0
*/
public function getUser();

/**
* Whether or not the search should be limited to the users home storage
*
* @return bool
*/
public function limitToHome(): bool;
}

Cargando…
Cancelar
Guardar