Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/dav/lib/Files/FileSearchBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,23 @@ private function transformSearchOperation(Operator $operator) {
throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e);
}

if ($field === 'name') {
return new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [
new SearchComparison(
$trimmedType,
$field,
$castedValue,
$extra ?? ''
),
new SearchComparison(
$trimmedType,
'mount_point_name',
$castedValue,
$extra ?? ''
)
]);
}

return new SearchComparison(
$trimmedType,
$field,
Expand Down
18 changes: 13 additions & 5 deletions apps/dav/tests/unit/Files/FileSearchBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace OCA\DAV\Tests\unit\Files;

use OC\Files\Search\SearchBinaryOperator;
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
use OC\Files\View;
Expand Down Expand Up @@ -92,11 +93,18 @@ public function testSearchFilename(): void {
$this->searchFolder->expects($this->once())
->method('search')
->with(new SearchQuery(
new SearchComparison(
ISearchComparison::COMPARE_EQUAL,
'name',
'foo'
),
new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [
new SearchComparison(
ISearchComparison::COMPARE_EQUAL,
'name',
'foo'
),
new SearchComparison(
ISearchComparison::COMPARE_EQUAL,
'mount_point_name',
'foo'
),
]),
100,
0,
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
}
}

public function regexSubstring($input, $pattern): IQueryFunction {

Check failure on line 56 in lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

MissingOverrideAttribute

lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php:56:2: MissingOverrideAttribute: Method OC\DB\QueryBuilder\FunctionBuilder\FunctionBuilder::regexsubstring should have the "Override" attribute (see https://psalm.dev/358)
return new QueryFunction('REGEXP_SUBSTR(' . $this->helper->quoteColumnName($input) . ', ' . $this->helper->quoteColumnName($pattern) . ')');
}

#[\Override]
public function sum($field): IQueryFunction {
return new QueryFunction('SUM(' . $this->helper->quoteColumnName($field) . ')');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@
$separator = $this->connection->quote($separator);
return new QueryFunction('string_agg(' . $castedExpression . ', ' . $separator . ')');
}

public function regexSubstring($input, $pattern): IQueryFunction {

Check failure on line 37 in lib/private/DB/QueryBuilder/FunctionBuilder/PgSqlFunctionBuilder.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

MissingOverrideAttribute

lib/private/DB/QueryBuilder/FunctionBuilder/PgSqlFunctionBuilder.php:37:2: MissingOverrideAttribute: Method OC\DB\QueryBuilder\FunctionBuilder\PgSqlFunctionBuilder::regexsubstring should have the "Override" attribute (see https://psalm.dev/358)
return new QueryFunction('substring(' . $this->helper->quoteColumnName($input) . ' from ' . $this->helper->quoteColumnName($pattern) . ')');
}
}
20 changes: 20 additions & 0 deletions lib/private/DB/SQLiteSessionInit.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,30 @@ public function postConnect(ConnectionEventArgs $args): void {
/** @var \Doctrine\DBAL\Driver\PDO\Connection $connection */
$connection = $args->getConnection()->getWrappedConnection();
$pdo = $connection->getWrappedConnection();

$regexSubstr = function ($string, $pattern): string {
if (is_null($string) || is_null($pattern)) {
return '';
} else {
$string = (string)$string;
$pattern = str_replace('#', '\#', (string)$pattern);
}

$matches = [];
$result = preg_match("#$pattern#", $string, $matches);
if ($result === 0 || $result === false) {
return '';
} else {
return $matches[0];
}
};

if (PHP_VERSION_ID >= 80500 && method_exists($pdo, 'createFunction')) {
$pdo->createFunction('md5', 'md5', 1);
$pdo->createFunction('regexp_substr', $regexSubstr, 2);
} else {
$pdo->sqliteCreateFunction('md5', 'md5', 1);
$pdo->sqliteCreateFunction('regexp_substr', $regexSubstr, 2);
}
}

Expand Down
11 changes: 11 additions & 0 deletions lib/private/Files/Cache/QuerySearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@
));
}

protected function equipQueryForMounts(CacheQueryBuilder $query, IUser $user): void {
$query
->leftJoin('file', 'mounts', 'm', $query->expr()->andX(
$query->expr()->eq('m.root_id', 'file.fileid'),
$query->expr()->eq('m.user_id', $query->createNamedParameter($user->getUID()))
));
}

protected function equipQueryForShares(CacheQueryBuilder $query): void {
$query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source'));
}
Expand Down Expand Up @@ -168,6 +176,9 @@
if (in_array('owner', $requestedFields, true) || in_array('share_with', $requestedFields, true) || in_array('share_type', $requestedFields, true)) {
$this->equipQueryForShares($query);
}
if (in_array('mount_point_name', $requestedFields)) {

Check failure on line 179 in lib/private/Files/Cache/QuerySearchHelper.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UnrecognizedExpression

lib/private/Files/Cache/QuerySearchHelper.php:179:7: UnrecognizedExpression: in_array() must be called with an explicit $strict parameter (see https://psalm.dev/048)
$this->equipQueryForMounts($query, $this->requireUser($searchQuery));
}

$metadataQuery = $query->selectMetadata();

Expand Down
20 changes: 13 additions & 7 deletions lib/private/Files/Cache/SearchBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use OCP\DB\QueryBuilder\IParameter;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\IQueryFunction;
use OCP\Files\IMimeTypeLoader;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
Expand Down Expand Up @@ -67,6 +68,7 @@ class SearchBuilder {
'owner' => 'string',
'creation_time' => 'integer',
'upload_time' => 'integer',
'mount_point_name' => 'string',
];

/** @var array<string, int|string> */
Expand Down Expand Up @@ -162,7 +164,7 @@ private function searchComparisonToDBExpr(
if ($comparison->getExtra()) {
[$field, $value, $type, $paramType] = $this->getExtraOperatorField($comparison, $metadataQuery);
} else {
[$field, $value, $type, $paramType] = $this->getOperatorFieldAndValue($comparison);
[$field, $value, $type, $paramType] = $this->getOperatorFieldAndValue($builder, $comparison);
}

if (isset($operatorMap[$type])) {
Expand All @@ -175,29 +177,29 @@ private function searchComparisonToDBExpr(

/**
* @param ISearchComparison $operator
* @return array{string, ParamValue, string, string}
* @return array{string|IQueryFunction, ParamValue, string, string}
*/
private function getOperatorFieldAndValue(ISearchComparison $operator): array {
private function getOperatorFieldAndValue(IQueryBuilder $builder, ISearchComparison $operator): array {
$this->validateComparison($operator);
$field = $operator->getField();
$value = $operator->getValue();
$type = $operator->getType();
$pathEqHash = $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true);
return $this->getOperatorFieldAndValueInner($field, $value, $type, $pathEqHash);
return $this->getOperatorFieldAndValueInner($builder, $field, $value, $type, $pathEqHash);
}

/**
* @param ParamValue $value
* @return array{string, ParamValue, string, string}
* @return array{string|IQueryFunction, ParamValue, string, string}
*/
private function getOperatorFieldAndValueInner(string $field, mixed $value, string $type, bool $pathEqHash): array {
private function getOperatorFieldAndValueInner(IQueryBuilder $builder, string $field, mixed $value, string $type, bool $pathEqHash): array {
$paramType = self::FIELD_TYPES[$field];
if ($type === ISearchComparison::COMPARE_IN) {
$resultField = $field;
$values = [];
foreach ($value as $arrayValue) {
/** @var ParamSingleValue $arrayValue */
[$arrayField, $arrayValue] = $this->getOperatorFieldAndValueInner($field, $arrayValue, ISearchComparison::COMPARE_EQUAL, $pathEqHash);
[$arrayField, $arrayValue] = $this->getOperatorFieldAndValueInner($builder, $field, $arrayValue, ISearchComparison::COMPARE_EQUAL, $pathEqHash);
$resultField = $arrayField;
$values[] = $arrayValue;
}
Expand Down Expand Up @@ -237,6 +239,9 @@ private function getOperatorFieldAndValueInner(string $field, mixed $value, stri
$value = md5((string)$value);
} elseif ($field === 'owner') {
$field = 'uid_owner';
} elseif ($field === 'mount_point_name') {
$field = $builder->func()->regexSubstring('mount_point', $builder->createNamedParameter('[^/]+/$'));
$value = $value . '/';
}
return [$field, $value, $type, $paramType];
}
Expand All @@ -258,6 +263,7 @@ private function validateComparison(ISearchComparison $operator): void {
'owner' => ['eq'],
'creation_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
'upload_time' => ['eq', 'gt', 'lt', 'gte', 'lte'],
'mount_point_name' => ['eq', 'like', 'clike', 'in'],
];

if (!isset(self::FIELD_TYPES[$operator->getField()])) {
Expand Down
20 changes: 19 additions & 1 deletion lib/private/Files/Node/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,25 @@ private function queryFromOperator(ISearchOperator $operator, ?string $uid = nul
#[\Override]
public function search($query) {
if (is_string($query)) {
$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'));
$operator = new SearchComparison(
ISearchComparison::COMPARE_LIKE,
'name',
'%' . $query . '%',
);
$parts = explode('/', $this->path);
$uid = null;
if (count($parts) > 2) {
[, $uid] = $parts;
$operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [
$operator,
new SearchComparison(
ISearchComparison::COMPARE_LIKE,
'mount_point_name',
'%' . $query . '%',
)
]);
}
$query = $this->queryFromOperator($operator, $uid);
}

// search is handled by a single query covering all caches that this folder contains
Expand Down
11 changes: 11 additions & 0 deletions lib/public/DB/QueryBuilder/IFunctionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ public function groupConcat($expr, ?string $separator = ','): IQueryFunction;
*/
public function substring($input, $start, $length = null): IQueryFunction;

/**
* Takes a substring from the input string using a regex pattern
*
* @param string|ILiteral|IParameter|IQueryFunction $input The input string
* @param string|ILiteral|IParameter|IQueryFunction $pattern The pattern to match and return
*
* @return IQueryFunction
* @since 35.0.0
*/
public function regexSubstring($input, $pattern): IQueryFunction;

/**
* Takes the sum of all rows in a column
*
Expand Down
29 changes: 29 additions & 0 deletions tests/lib/DB/QueryBuilder/FunctionBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Server;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;

/**
Expand Down Expand Up @@ -487,4 +488,32 @@ public function testLeast(): void {
$result->closeCursor();
$this->assertEquals(1, $row);
}

public static function regexSubstringData(): array {
return [
['foobar', 'foo', 'foo'],
['foobar', 'b.+$', 'bar'],
['foo#bar', 'ba.+r$', null],
['foo#bar', 'o#.', 'o#b'],
['a/file/path', '[^/]+$', 'path'],
];
}

#[DataProvider('regexSubstringData')]
public function testRegexSubstring(string $input, string $pattern, ?string $expected): void {
error_reporting(E_ALL);
$query = $this->connection->getQueryBuilder();

$query->select($query->func()->regexSubstring(
$query->createNamedParameter($input),
$query->createNamedParameter($pattern),
));
$query->from('appconfig')
->setMaxResults(1);

$result = $query->executeQuery();
$row = $result->fetchOne();
$result->closeCursor();
$this->assertEquals($expected, $row);
}
}
10 changes: 10 additions & 0 deletions tests/lib/Files/Node/FolderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOrder;
use OCP\Files\Storage\IStorage;
use OCP\IUser;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\Traits\UserTrait;

/**
* Class FolderTest
Expand All @@ -50,6 +52,14 @@
*/
#[\PHPUnit\Framework\Attributes\Group('DB')]
class FolderTest extends NodeTestCase {
use UserTrait;

protected function setUp(): void {
parent::setUp();

$this->createUser('bar', 'bar');
}

#[\Override]
protected function createTestNode(IRootFolder $root, View&MockObject $view, string $path, array $data = [], string $internalPath = '', ?IStorage $storage = null): Folder {
$view->expects($this->any())
Expand Down
52 changes: 49 additions & 3 deletions tests/lib/Files/Search/SearchIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,45 @@
use OC\Files\Search\SearchComparison;
use OC\Files\Search\SearchQuery;
use OC\Files\Storage\Temporary;
use OCP\Files\Cache\ICache;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Search\ISearchBinaryOperator;
use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Storage\IStorage;
use OCP\IUser;
use OCP\Server;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use Test\TestCase;

#[\PHPUnit\Framework\Attributes\Group('DB')]
#[Group('DB')]
class SearchIntegrationTest extends TestCase {
private $cache;
private $storage;
private ICache $cache;
private IStorage $storage;
private string $mountPoint;
private IUserMountCache $mountCache;
private IUser $user;

#[\Override]
protected function setUp(): void {
parent::setUp();

$this->user = $this->createMock(IUser::class);
$this->user->method('getUID')
->willReturn('user');
$this->storage = new Temporary([]);
$this->cache = $this->storage->getCache();
$this->storage->getScanner()->scan('');
$this->mountCache = Server::get(IUserMountCache::class);
$this->mountPoint = '/user/files/search_test/';
$this->mountCache->addMount($this->user, $this->mountPoint, $this->cache->get(''), 'dummy');
}

protected function tearDown(): void {
$this->mountCache->removeMount($this->mountPoint);

parent::tearDown();
}

public function testThousandAndOneFilters(): void {
Expand All @@ -44,4 +67,27 @@ public function testThousandAndOneFilters(): void {
$this->assertCount(1, $results);
$this->assertEquals($id, $results[0]->getId());
}

public static function searchMountNameProvider(): array {
return [
[new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%search%'), ''],
[new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mount_point_name', 'search_test'), ''],
[new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%search_test%'), ''],
[new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%files%'), null],
];
}

#[DataProvider('searchMountNameProvider')]
public function testSearchMountName(ISearchOperator $operator, ?string $resultPath): void {
$query = new SearchQuery($operator, 10, 0, [], $this->user);

$results = $this->cache->searchQuery($query);

if (is_null($resultPath)) {
$this->assertCount(0, $results);
} else {
$this->assertCount(1, $results);
$this->assertEquals($this->cache->getId($resultPath), $results[0]->getId());
}
}
}
Loading