Skip to content

Commit 8daf30e

Browse files
authored
Merge pull request #26135 from nextcloud/backport/25136/stable19
[stable19] do cachejail search filtering in sql
2 parents f9c9e27 + b290306 commit 8daf30e

8 files changed

Lines changed: 238 additions & 23 deletions

File tree

apps/files_sharing/lib/Cache.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ protected function getRoot() {
8989
return $this->root;
9090
}
9191

92+
protected function getGetUnjailedRoot() {
93+
return $this->sourceRootInfo->getPath();
94+
}
95+
9296
public function getCache() {
9397
if (is_null($this->cache)) {
9498
$sourceStorage = $this->storage->getSourceStorage();

apps/files_sharing/tests/CacheTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,4 +517,40 @@ public function testShareJailedStorage() {
517517

518518
$this->assertTrue($sourceStorage->getCache()->inCache('jail/sub/bar.txt'));
519519
}
520+
521+
public function testSearchShareJailedStorage() {
522+
$sourceStorage = new Temporary();
523+
$sourceStorage->mkdir('jail');
524+
$sourceStorage->mkdir('jail/sub');
525+
$sourceStorage->file_put_contents('jail/sub/foo.txt', 'foo');
526+
$jailedSource = new Jail([
527+
'storage' => $sourceStorage,
528+
'root' => 'jail'
529+
]);
530+
$sourceStorage->getScanner()->scan('');
531+
$this->registerMount(self::TEST_FILES_SHARING_API_USER1, $jailedSource, '/' . self::TEST_FILES_SHARING_API_USER1 . '/files/foo');
532+
533+
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
534+
535+
$rootFolder = \OC::$server->getUserFolder(self::TEST_FILES_SHARING_API_USER1);
536+
$node = $rootFolder->get('foo/sub');
537+
$share = $this->shareManager->newShare();
538+
$share->setNode($node)
539+
->setShareType(IShare::TYPE_USER)
540+
->setSharedWith(self::TEST_FILES_SHARING_API_USER2)
541+
->setSharedBy(self::TEST_FILES_SHARING_API_USER1)
542+
->setPermissions(\OCP\Constants::PERMISSION_ALL);
543+
$share = $this->shareManager->createShare($share);
544+
$share->setStatus(IShare::STATUS_ACCEPTED);
545+
$this->shareManager->updateShare($share);
546+
\OC_Util::tearDownFS();
547+
548+
self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
549+
550+
/** @var SharedStorage $sharedStorage */
551+
list($sharedStorage) = \OC\Files\Filesystem::resolvePath('/' . self::TEST_FILES_SHARING_API_USER2 . '/files/sub');
552+
553+
$results = $sharedStorage->getCache()->search("foo.txt");
554+
$this->assertCount(1, $results);
555+
}
520556
}

lib/private/Files/Cache/Cache.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public function __construct(IStorage $storage) {
113113
$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
114114
}
115115

116-
private function getQueryBuilder() {
116+
protected function getQueryBuilder() {
117117
return new CacheQueryBuilder(
118118
$this->connection,
119119
\OC::$server->getSystemConfig(),

lib/private/Files/Cache/QuerySearchHelper.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ private function getOperatorFieldAndValue(ISearchComparison $operator) {
166166
$field = 'tag.category';
167167
} elseif ($field === 'fileid') {
168168
$field = 'file.fileid';
169+
} elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL) {
170+
$field = 'path_hash';
171+
$value = md5((string)$value);
169172
}
170173
return [$field, $value, $type];
171174
}
@@ -175,6 +178,7 @@ private function validateComparison(ISearchComparison $operator) {
175178
'mimetype' => 'string',
176179
'mtime' => 'integer',
177180
'name' => 'string',
181+
'path' => 'string',
178182
'size' => 'integer',
179183
'tagname' => 'string',
180184
'favorite' => 'boolean',
@@ -184,6 +188,7 @@ private function validateComparison(ISearchComparison $operator) {
184188
'mimetype' => ['eq', 'like'],
185189
'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
186190
'name' => ['eq', 'like'],
191+
'path' => ['eq', 'like'],
187192
'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
188193
'tagname' => ['eq', 'like'],
189194
'favorite' => ['eq'],

lib/private/Files/Cache/Wrapper/CacheJail.php

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@
3030
namespace OC\Files\Cache\Wrapper;
3131

3232
use OC\Files\Cache\Cache;
33+
use OC\Files\Search\SearchBinaryOperator;
34+
use OC\Files\Search\SearchComparison;
3335
use OC\Files\Search\SearchQuery;
36+
use OCP\DB\QueryBuilder\IQueryBuilder;
3437
use OCP\Files\Cache\ICacheEntry;
38+
use OCP\Files\Search\ISearchBinaryOperator;
39+
use OCP\Files\Search\ISearchComparison;
3540
use OCP\Files\Search\ISearchQuery;
3641

3742
/**
@@ -42,6 +47,7 @@ class CacheJail extends CacheWrapper {
4247
* @var string
4348
*/
4449
protected $root;
50+
protected $unjailedRoot;
4551

4652
/**
4753
* @param \OCP\Files\Cache\ICache $cache
@@ -50,12 +56,29 @@ class CacheJail extends CacheWrapper {
5056
public function __construct($cache, $root) {
5157
parent::__construct($cache);
5258
$this->root = $root;
59+
$this->connection = \OC::$server->getDatabaseConnection();
60+
$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
61+
62+
if ($cache instanceof CacheJail) {
63+
$this->unjailedRoot = $cache->getSourcePath($root);
64+
} else {
65+
$this->unjailedRoot = $root;
66+
}
5367
}
5468

5569
protected function getRoot() {
5670
return $this->root;
5771
}
5872

73+
/**
74+
* Get the root path with any nested jails resolved
75+
*
76+
* @return string
77+
*/
78+
protected function getGetUnjailedRoot() {
79+
return $this->unjailedRoot;
80+
}
81+
5982
protected function getSourcePath($path) {
6083
if ($path === '') {
6184
return $this->getRoot();
@@ -66,16 +89,20 @@ protected function getSourcePath($path) {
6689

6790
/**
6891
* @param string $path
92+
* @param null|string $root
6993
* @return null|string the jailed path or null if the path is outside the jail
7094
*/
71-
protected function getJailedPath($path) {
72-
if ($this->getRoot() === '') {
95+
protected function getJailedPath(string $path, string $root = null) {
96+
if ($root === null) {
97+
$root = $this->getRoot();
98+
}
99+
if ($root === '') {
73100
return $path;
74101
}
75-
$rootLength = strlen($this->getRoot()) + 1;
76-
if ($path === $this->getRoot()) {
102+
$rootLength = strlen($root) + 1;
103+
if ($path === $root) {
77104
return '';
78-
} elseif (substr($path, 0, $rootLength) === $this->getRoot() . '/') {
105+
} elseif (substr($path, 0, $rootLength) === $root . '/') {
79106
return substr($path, $rootLength);
80107
} else {
81108
return null;
@@ -93,11 +120,6 @@ protected function formatCacheEntry($entry) {
93120
return $entry;
94121
}
95122

96-
protected function filterCacheEntry($entry) {
97-
$rootLength = strlen($this->getRoot()) + 1;
98-
return $rootLength === 1 || ($entry['path'] === $this->getRoot()) || (substr($entry['path'], 0, $rootLength) === $this->getRoot() . '/');
99-
}
100-
101123
/**
102124
* get the stored metadata of a file or folder
103125
*
@@ -210,9 +232,10 @@ public function getStatus($file) {
210232
}
211233

212234
private function formatSearchResults($results) {
213-
$results = array_filter($results, [$this, 'filterCacheEntry']);
214-
$results = array_values($results);
215-
return array_map([$this, 'formatCacheEntry'], $results);
235+
return array_map(function ($entry) {
236+
$entry['path'] = $this->getJailedPath($entry['path'], $this->getGetUnjailedRoot());
237+
return $entry;
238+
}, $results);
216239
}
217240

218241
/**
@@ -222,7 +245,29 @@ private function formatSearchResults($results) {
222245
* @return array an array of file data
223246
*/
224247
public function search($pattern) {
225-
$results = $this->getCache()->search($pattern);
248+
// normalize pattern
249+
$pattern = $this->normalize($pattern);
250+
251+
if ($pattern === '%%') {
252+
return [];
253+
}
254+
255+
$query = $this->getQueryBuilder();
256+
$query->selectFileCache()
257+
->whereStorageId()
258+
->andWhere($query->expr()->orX(
259+
$query->expr()->like('path', $query->createNamedParameter($this->getGetUnjailedRoot() . '/%')),
260+
$query->expr()->eq('path_hash', $query->createNamedParameter(md5($this->getGetUnjailedRoot())))
261+
))
262+
->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
263+
264+
$result = $query->execute();
265+
$files = $result->fetchAll();
266+
$result->closeCursor();
267+
268+
$results = array_map(function (array $data) {
269+
return self::cacheEntryFromData($data, $this->mimetypeLoader);
270+
}, $files);
226271
return $this->formatSearchResults($results);
227272
}
228273

@@ -233,12 +278,48 @@ public function search($pattern) {
233278
* @return array
234279
*/
235280
public function searchByMime($mimetype) {
236-
$results = $this->getCache()->searchByMime($mimetype);
281+
$mimeId = $this->mimetypeLoader->getId($mimetype);
282+
283+
$query = $this->getQueryBuilder();
284+
$query->selectFileCache()
285+
->whereStorageId()
286+
->andWhere($query->expr()->orX(
287+
$query->expr()->like('path', $query->createNamedParameter($this->getGetUnjailedRoot() . '/%')),
288+
$query->expr()->eq('path_hash', $query->createNamedParameter(md5($this->getGetUnjailedRoot())))
289+
));
290+
291+
if (strpos($mimetype, '/')) {
292+
$query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
293+
} else {
294+
$query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
295+
}
296+
297+
$result = $query->execute();
298+
$files = $result->fetchAll();
299+
$result->closeCursor();
300+
301+
$results = array_map(function (array $data) {
302+
return self::cacheEntryFromData($data, $this->mimetypeLoader);
303+
}, $files);
237304
return $this->formatSearchResults($results);
238305
}
239306

240307
public function searchQuery(ISearchQuery $query) {
241-
$simpleQuery = new SearchQuery($query->getSearchOperation(), 0, 0, $query->getOrder(), $query->getUser());
308+
$prefixFilter = new SearchComparison(
309+
ISearchComparison::COMPARE_LIKE,
310+
'path',
311+
$this->getGetUnjailedRoot() . '/%'
312+
);
313+
$rootFilter = new SearchComparison(
314+
ISearchComparison::COMPARE_EQUAL,
315+
'path',
316+
$this->getGetUnjailedRoot()
317+
);
318+
$operation = new SearchBinaryOperator(
319+
ISearchBinaryOperator::OPERATOR_AND,
320+
[new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [$prefixFilter, $rootFilter]) , $query->getSearchOperation()]
321+
);
322+
$simpleQuery = new SearchQuery($operation, 0, 0, $query->getOrder(), $query->getUser());
242323
$results = $this->getCache()->searchQuery($simpleQuery);
243324
$results = $this->formatSearchResults($results);
244325

lib/private/Share20/Share.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030

3131
namespace OC\Share20;
3232

33-
use OCP\Files\Cache\ICacheEntry;
3433
use OCP\Files\File;
34+
use OCP\Files\Cache\ICacheEntry;
35+
use OCP\Files\FileInfo;
3536
use OCP\Files\IRootFolder;
3637
use OCP\Files\Node;
3738
use OCP\Files\NotFoundException;
@@ -233,8 +234,13 @@ public function setNodeType($type) {
233234
*/
234235
public function getNodeType() {
235236
if ($this->nodeType === null) {
236-
$node = $this->getNode();
237-
$this->nodeType = $node instanceof File ? 'file' : 'folder';
237+
if ($this->getNodeCacheEntry()) {
238+
$info = $this->getNodeCacheEntry();
239+
$this->nodeType = $info->getMimeType() === FileInfo::MIMETYPE_FOLDER ? 'folder' : 'file';
240+
} else {
241+
$node = $this->getNode();
242+
$this->nodeType = $node instanceof File ? 'file' : 'folder';
243+
}
238244
}
239245

240246
return $this->nodeType;

tests/lib/Files/Cache/Wrapper/CacheJailTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
namespace Test\Files\Cache\Wrapper;
1010

1111
use OC\Files\Cache\Wrapper\CacheJail;
12+
use OC\Files\Search\SearchComparison;
13+
use OC\Files\Search\SearchQuery;
14+
use OC\User\User;
15+
use OCP\Files\Search\ISearchComparison;
16+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1217
use Test\Files\Cache\CacheTest;
1318

1419
/**
@@ -32,6 +37,7 @@ protected function setUp(): void {
3237
}
3338

3439
public function testSearchOutsideJail() {
40+
$this->storage->getScanner()->scan('');
3541
$file1 = 'foo/foobar';
3642
$file2 = 'folder/foobar';
3743
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
@@ -44,6 +50,52 @@ public function testSearchOutsideJail() {
4450
$result = $this->cache->search('%foobar%');
4551
$this->assertCount(1, $result);
4652
$this->assertEquals('foobar', $result[0]['path']);
53+
54+
$result = $this->cache->search('%foo%');
55+
$this->assertCount(2, $result);
56+
usort($result, function ($a, $b) {
57+
return $a['path'] <=> $b['path'];
58+
});
59+
$this->assertEquals('', $result[0]['path']);
60+
$this->assertEquals('foobar', $result[1]['path']);
61+
}
62+
63+
public function testSearchMimeOutsideJail() {
64+
$this->storage->getScanner()->scan('');
65+
$file1 = 'foo/foobar';
66+
$file2 = 'folder/foobar';
67+
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
68+
69+
$this->sourceCache->put($file1, $data1);
70+
$this->sourceCache->put($file2, $data1);
71+
72+
$this->assertCount(2, $this->sourceCache->searchByMime('foo/folder'));
73+
74+
$result = $this->cache->search('%foobar%');
75+
$this->assertCount(1, $result);
76+
$this->assertEquals('foobar', $result[0]['path']);
77+
}
78+
79+
public function testSearchQueryOutsideJail() {
80+
$this->storage->getScanner()->scan('');
81+
$file1 = 'foo/foobar';
82+
$file2 = 'folder/foobar';
83+
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
84+
85+
$this->sourceCache->put($file1, $data1);
86+
$this->sourceCache->put($file2, $data1);
87+
88+
$user = new User('foo', null, $this->createMock(EventDispatcherInterface::class));
89+
$query = new SearchQuery(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), 10, 0, [], $user);
90+
$result = $this->cache->searchQuery($query);
91+
92+
$this->assertCount(1, $result);
93+
$this->assertEquals('foobar', $result[0]['path']);
94+
95+
$query = new SearchQuery(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foo'), 10, 0, [], $user);
96+
$result = $this->cache->searchQuery($query);
97+
$this->assertCount(1, $result);
98+
$this->assertEquals('', $result[0]['path']);
4799
}
48100

49101
public function testClearKeepEntriesOutsideJail() {
@@ -130,4 +182,22 @@ public function testMoveBetweenJail() {
130182
$this->assertTrue($this->sourceCache->inCache('target/foo'));
131183
$this->assertTrue($this->sourceCache->inCache('target/foo/bar'));
132184
}
185+
186+
public function testSearchNested() {
187+
$this->storage->getScanner()->scan('');
188+
$file1 = 'foo';
189+
$file2 = 'foo/bar';
190+
$file3 = 'foo/bar/asd';
191+
$data1 = ['size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder'];
192+
193+
$this->sourceCache->put($file1, $data1);
194+
$this->sourceCache->put($file2, $data1);
195+
$this->sourceCache->put($file3, $data1);
196+
197+
$nested = new \OC\Files\Cache\Wrapper\CacheJail($this->cache, 'bar');
198+
199+
$result = $nested->search('%asd%');
200+
$this->assertCount(1, $result);
201+
$this->assertEquals('asd', $result[0]['path']);
202+
}
133203
}

0 commit comments

Comments
 (0)