Passed
Push — master ( 6f31d2...35b910 )
by Julius
34:16 queued 19:13
created

Folder::searchByTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Georg Ehrke <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Julius Härtl <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Robin McCorkell <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 *
16
 * @license AGPL-3.0
17
 *
18
 * This code is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License, version 3,
20
 * as published by the Free Software Foundation.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
 * GNU Affero General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU Affero General Public License, version 3,
28
 * along with this program. If not, see <http://www.gnu.org/licenses/>
29
 *
30
 */
31
32
namespace OC\Files\Node;
33
34
use OC\Files\Search\SearchBinaryOperator;
35
use OC\Files\Search\SearchComparison;
36
use OC\Files\Search\SearchOrder;
37
use OC\Files\Search\SearchQuery;
38
use OCP\Files\Cache\ICacheEntry;
39
use OCP\Files\Config\ICachedMountInfo;
40
use OCP\Files\FileInfo;
41
use OCP\Files\Mount\IMountPoint;
42
use OCP\Files\NotFoundException;
43
use OCP\Files\NotPermittedException;
44
use OCP\Files\Search\ISearchBinaryOperator;
45
use OCP\Files\Search\ISearchComparison;
46
use OCP\Files\Search\ISearchOperator;
47
use OCP\Files\Search\ISearchOrder;
48
use OCP\Files\Search\ISearchQuery;
49
use OCP\IUserManager;
50
51
class Folder extends Node implements \OCP\Files\Folder {
52
	/**
53
	 * Creates a Folder that represents a non-existing path
54
	 *
55
	 * @param string $path path
56
	 * @return string non-existing node class
57
	 */
58
	protected function createNonExistingNode($path) {
59
		return new NonExistingFolder($this->root, $this->view, $path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new OC\Files\Node...ot, $this->view, $path) returns the type OC\Files\Node\NonExistingFolder which is incompatible with the documented return type string.
Loading history...
60
	}
61
62
	/**
63
	 * @param string $path path relative to the folder
64
	 * @return string
65
	 * @throws \OCP\Files\NotPermittedException
66
	 */
67
	public function getFullPath($path) {
68
		if (!$this->isValidPath($path)) {
69
			throw new NotPermittedException('Invalid path');
70
		}
71
		return $this->path . $this->normalizePath($path);
72
	}
73
74
	/**
75
	 * @param string $path
76
	 * @return string|null
77
	 */
78
	public function getRelativePath($path) {
79
		if ($this->path === '' or $this->path === '/') {
80
			return $this->normalizePath($path);
81
		}
82
		if ($path === $this->path) {
83
			return '/';
84
		} elseif (strpos($path, $this->path . '/') !== 0) {
85
			return null;
86
		} else {
87
			$path = substr($path, strlen($this->path));
88
			return $this->normalizePath($path);
89
		}
90
	}
91
92
	/**
93
	 * check if a node is a (grand-)child of the folder
94
	 *
95
	 * @param \OC\Files\Node\Node $node
96
	 * @return bool
97
	 */
98
	public function isSubNode($node) {
99
		return strpos($node->getPath(), $this->path . '/') === 0;
100
	}
101
102
	/**
103
	 * get the content of this directory
104
	 *
105
	 * @return Node[]
106
	 * @throws \OCP\Files\NotFoundException
107
	 */
108
	public function getDirectoryListing() {
109
		$folderContent = $this->view->getDirectoryContent($this->path);
110
111
		return array_map(function (FileInfo $info) {
112
			if ($info->getMimetype() === 'httpd/unix-directory') {
113
				return new Folder($this->root, $this->view, $info->getPath(), $info);
114
			} else {
115
				return new File($this->root, $this->view, $info->getPath(), $info);
116
			}
117
		}, $folderContent);
118
	}
119
120
	/**
121
	 * @param string $path
122
	 * @param FileInfo $info
123
	 * @return File|Folder
124
	 */
125
	protected function createNode($path, FileInfo $info = null) {
126
		if (is_null($info)) {
127
			$isDir = $this->view->is_dir($path);
128
		} else {
129
			$isDir = $info->getType() === FileInfo::TYPE_FOLDER;
130
		}
131
		if ($isDir) {
132
			return new Folder($this->root, $this->view, $path, $info);
133
		} else {
134
			return new File($this->root, $this->view, $path, $info);
135
		}
136
	}
137
138
	/**
139
	 * Get the node at $path
140
	 *
141
	 * @param string $path
142
	 * @return \OC\Files\Node\Node
143
	 * @throws \OCP\Files\NotFoundException
144
	 */
145
	public function get($path) {
146
		return $this->root->get($this->getFullPath($path));
147
	}
148
149
	/**
150
	 * @param string $path
151
	 * @return bool
152
	 */
153
	public function nodeExists($path) {
154
		try {
155
			$this->get($path);
156
			return true;
157
		} catch (NotFoundException $e) {
158
			return false;
159
		}
160
	}
161
162
	/**
163
	 * @param string $path
164
	 * @return \OC\Files\Node\Folder
165
	 * @throws \OCP\Files\NotPermittedException
166
	 */
167
	public function newFolder($path) {
168
		if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
169
			$fullPath = $this->getFullPath($path);
170
			$nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath);
171
			$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
172
			if (!$this->view->mkdir($fullPath)) {
173
				throw new NotPermittedException('Could not create folder');
174
			}
175
			$node = new Folder($this->root, $this->view, $fullPath);
176
			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
177
			return $node;
178
		} else {
179
			throw new NotPermittedException('No create permission for folder');
180
		}
181
	}
182
183
	/**
184
	 * @param string $path
185
	 * @param string | resource | null $content
186
	 * @return \OC\Files\Node\File
187
	 * @throws \OCP\Files\NotPermittedException
188
	 */
189
	public function newFile($path, $content = null) {
190
		if (empty($path)) {
191
			throw new NotPermittedException('Could not create as provided path is empty');
192
		}
193
		if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
194
			$fullPath = $this->getFullPath($path);
195
			$nonExisting = new NonExistingFile($this->root, $this->view, $fullPath);
196
			$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
197
			if ($content !== null) {
198
				$result = $this->view->file_put_contents($fullPath, $content);
199
			} else {
200
				$result = $this->view->touch($fullPath);
201
			}
202
			if ($result === false) {
203
				throw new NotPermittedException('Could not create path');
204
			}
205
			$node = new File($this->root, $this->view, $fullPath);
206
			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
207
			return $node;
208
		}
209
		throw new NotPermittedException('No create permission for path');
210
	}
211
212
	private function queryFromOperator(ISearchOperator $operator, string $uid = null): ISearchQuery {
213
		if ($uid === null) {
214
			$user = null;
215
		} else {
216
			/** @var IUserManager $userManager */
217
			$userManager = \OC::$server->query(IUserManager::class);
218
			$user = $userManager->get($uid);
219
		}
220
		return new SearchQuery($operator, 0, 0, [], $user);
221
	}
222
223
	/**
224
	 * search for files with the name matching $query
225
	 *
226
	 * @param string|ISearchQuery $query
227
	 * @return \OC\Files\Node\Node[]
228
	 */
229
	public function search($query) {
230
		if (is_string($query)) {
231
			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'));
232
		}
233
234
		// Limit+offset for queries with ordering
235
		//
236
		// Because we currently can't do ordering between the results from different storages in sql
237
		// The only way to do ordering is requesting the $limit number of entries from all storages
238
		// sorting them and returning the first $limit entries.
239
		//
240
		// For offset we have the same problem, we don't know how many entries from each storage should be skipped
241
		// by a given $offset, so instead we query $offset + $limit from each storage and return entries $offset..($offset+$limit)
242
		// after merging and sorting them.
243
		//
244
		// This is suboptimal but because limit and offset tend to be fairly small in real world use cases it should
245
		// still be significantly better than disabling paging altogether
246
247
		$limitToHome = $query->limitToHome();
248
		if ($limitToHome && count(explode('/', $this->path)) !== 3) {
249
			throw new \InvalidArgumentException('searching by owner is only allows on the users home folder');
250
		}
251
252
		$rootLength = strlen($this->path);
253
		$mount = $this->root->getMount($this->path);
254
		$storage = $mount->getStorage();
255
		$internalPath = $mount->getInternalPath($this->path);
256
		$internalPath = rtrim($internalPath, '/');
257
		if ($internalPath !== '') {
258
			$internalPath = $internalPath . '/';
259
		}
260
261
		$subQueryLimit = $query->getLimit() > 0 ? $query->getLimit() + $query->getOffset() : 0;
262
		$rootQuery = new SearchQuery(
263
			new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [
264
				new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', $internalPath . '%'),
265
				$query->getSearchOperation(),
266
			]),
267
			$subQueryLimit,
268
			0,
269
			$query->getOrder(),
270
			$query->getUser()
271
		);
272
273
		$files = [];
274
275
		$cache = $storage->getCache('');
276
277
		$results = $cache->searchQuery($rootQuery);
278
		foreach ($results as $result) {
279
			$files[] = $this->cacheEntryToFileInfo($mount, '', $internalPath, $result);
280
		}
281
282
		if (!$limitToHome) {
283
			$mounts = $this->root->getMountsIn($this->path);
284
			foreach ($mounts as $mount) {
285
				$subQuery = new SearchQuery(
286
					$query->getSearchOperation(),
287
					$subQueryLimit,
288
					0,
289
					$query->getOrder(),
290
					$query->getUser()
291
				);
292
293
				$storage = $mount->getStorage();
294
				if ($storage) {
295
					$cache = $storage->getCache('');
296
297
					$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
298
					$results = $cache->searchQuery($subQuery);
299
					foreach ($results as $result) {
300
						$files[] = $this->cacheEntryToFileInfo($mount, $relativeMountPoint, '', $result);
301
					}
302
				}
303
			}
304
		}
305
306
		$order = $query->getOrder();
307
		if ($order) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $order of type OCP\Files\Search\ISearchOrder[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
308
			usort($files, function (FileInfo $a, FileInfo $b) use ($order) {
309
				foreach ($order as $orderField) {
310
					$cmp = $orderField->sortFileInfo($a, $b);
311
					if ($cmp !== 0) {
312
						return $cmp;
313
					}
314
				}
315
				return 0;
316
			});
317
		}
318
		$files = array_values(array_slice($files, $query->getOffset(), $query->getLimit() > 0 ? $query->getLimit() : null));
319
320
		return array_map(function (FileInfo $file) {
321
			return $this->createNode($file->getPath(), $file);
322
		}, $files);
323
	}
324
325
	private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, string $trimRoot, ICacheEntry $cacheEntry): FileInfo {
326
		$trimLength = strlen($trimRoot);
327
		$cacheEntry['internalPath'] = $cacheEntry['path'];
328
		$cacheEntry['path'] = $appendRoot . substr($cacheEntry['path'], $trimLength);
329
		return new \OC\Files\FileInfo($this->path . '/' . $cacheEntry['path'], $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
330
	}
331
332
	/**
333
	 * search for files by mimetype
334
	 *
335
	 * @param string $mimetype
336
	 * @return Node[]
337
	 */
338
	public function searchByMime($mimetype) {
339
		if (strpos($mimetype, '/') === false) {
340
			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%'));
341
		} else {
342
			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype));
343
		}
344
		return $this->search($query);
345
	}
346
347
	/**
348
	 * search for files by tag
349
	 *
350
	 * @param string|int $tag name or tag id
351
	 * @param string $userId owner of the tags
352
	 * @return Node[]
353
	 */
354
	public function searchByTag($tag, $userId) {
355
		$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', $tag), $userId);
356
		return $this->search($query);
357
	}
358
359
	/**
360
	 * @param int $id
361
	 * @return \OC\Files\Node\Node[]
362
	 */
363
	public function getById($id) {
364
		$mountCache = $this->root->getUserMountCache();
365
		if (strpos($this->getPath(), '/', 1) > 0) {
366
			[, $user] = explode('/', $this->getPath());
367
		} else {
368
			$user = null;
369
		}
370
		$mountsContainingFile = $mountCache->getMountsForFileId((int)$id, $user);
371
		$mounts = $this->root->getMountsIn($this->path);
372
		$mounts[] = $this->root->getMount($this->path);
373
		/** @var IMountPoint[] $folderMounts */
374
		$folderMounts = array_combine(array_map(function (IMountPoint $mountPoint) {
375
			return $mountPoint->getMountPoint();
376
		}, $mounts), $mounts);
377
378
		/** @var ICachedMountInfo[] $mountsContainingFile */
379
		$mountsContainingFile = array_values(array_filter($mountsContainingFile, function (ICachedMountInfo $cachedMountInfo) use ($folderMounts) {
380
			return isset($folderMounts[$cachedMountInfo->getMountPoint()]);
381
		}));
382
383
		if (count($mountsContainingFile) === 0) {
384
			if ($user === $this->getAppDataDirectoryName()) {
385
				return $this->getByIdInRootMount((int)$id);
386
			}
387
			return [];
388
		}
389
390
		$nodes = array_map(function (ICachedMountInfo $cachedMountInfo) use ($folderMounts, $id) {
391
			$mount = $folderMounts[$cachedMountInfo->getMountPoint()];
392
			$cacheEntry = $mount->getStorage()->getCache()->get((int)$id);
393
			if (!$cacheEntry) {
394
				return null;
395
			}
396
397
			// cache jails will hide the "true" internal path
398
			$internalPath = ltrim($cachedMountInfo->getRootInternalPath() . '/' . $cacheEntry->getPath(), '/');
399
			$pathRelativeToMount = substr($internalPath, strlen($cachedMountInfo->getRootInternalPath()));
400
			$pathRelativeToMount = ltrim($pathRelativeToMount, '/');
401
			$absolutePath = rtrim($cachedMountInfo->getMountPoint() . $pathRelativeToMount, '/');
402
			return $this->root->createNode($absolutePath, new \OC\Files\FileInfo(
403
				$absolutePath, $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount,
404
				\OC::$server->getUserManager()->get($mount->getStorage()->getOwner($pathRelativeToMount))
405
			));
406
		}, $mountsContainingFile);
407
408
		$nodes = array_filter($nodes);
409
410
		return array_filter($nodes, function (Node $node) {
411
			return $this->getRelativePath($node->getPath());
412
		});
413
	}
414
415
	protected function getAppDataDirectoryName(): string {
416
		$instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
417
		return 'appdata_' . $instanceId;
418
	}
419
420
	/**
421
	 * In case the path we are currently in is inside the appdata_* folder,
422
	 * the original getById method does not work, because it can only look inside
423
	 * the user's mount points. But the user has no mount point for the root storage.
424
	 *
425
	 * So in that case we directly check the mount of the root if it contains
426
	 * the id. If it does we check if the path is inside the path we are working
427
	 * in.
428
	 *
429
	 * @param int $id
430
	 * @return array
431
	 */
432
	protected function getByIdInRootMount(int $id): array {
433
		$mount = $this->root->getMount('');
434
		$cacheEntry = $mount->getStorage()->getCache($this->path)->get($id);
435
		if (!$cacheEntry) {
436
			return [];
437
		}
438
439
		$absolutePath = '/' . ltrim($cacheEntry->getPath(), '/');
440
		$currentPath = rtrim($this->path, '/') . '/';
441
442
		if (strpos($absolutePath, $currentPath) !== 0) {
443
			return [];
444
		}
445
446
		return [$this->root->createNode(
447
			$absolutePath, new \OC\Files\FileInfo(
448
			$absolutePath,
449
			$mount->getStorage(),
450
			$cacheEntry->getPath(),
451
			$cacheEntry,
452
			$mount
453
		))];
454
	}
455
456
	public function getFreeSpace() {
457
		return $this->view->free_space($this->path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->view->free_space($this->path) also could return the type boolean which is incompatible with the return type mandated by OCP\Files\Folder::getFreeSpace() of integer.
Loading history...
458
	}
459
460
	public function delete() {
461
		if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) {
462
			$this->sendHooks(['preDelete']);
463
			$fileInfo = $this->getFileInfo();
464
			$this->view->rmdir($this->path);
465
			$nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo);
466
			$this->sendHooks(['postDelete'], [$nonExisting]);
467
			$this->exists = false;
0 ignored issues
show
Bug Best Practice introduced by
The property exists does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
468
		} else {
469
			throw new NotPermittedException('No delete permission for path');
470
		}
471
	}
472
473
	/**
474
	 * Add a suffix to the name in case the file exists
475
	 *
476
	 * @param string $name
477
	 * @return string
478
	 * @throws NotPermittedException
479
	 */
480
	public function getNonExistingName($name) {
481
		$uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view);
482
		return trim($this->getRelativePath($uniqueName), '/');
483
	}
484
485
	/**
486
	 * @param int $limit
487
	 * @param int $offset
488
	 * @return \OCP\Files\Node[]
489
	 */
490
	public function getRecent($limit, $offset = 0) {
491
		$query = new SearchQuery(
492
			new SearchBinaryOperator(
493
				// filter out non empty folders
494
				ISearchBinaryOperator::OPERATOR_OR,
495
				[
496
					new SearchBinaryOperator(
497
						ISearchBinaryOperator::OPERATOR_NOT,
498
						[
499
							new SearchComparison(
500
								ISearchComparison::COMPARE_EQUAL,
501
								'mimetype',
502
								FileInfo::MIMETYPE_FOLDER
503
							),
504
						]
505
					),
506
					new SearchComparison(
507
						ISearchComparison::COMPARE_EQUAL,
508
						'size',
509
						0
510
					),
511
				]
512
			),
513
			$limit,
514
			$offset,
515
			[
516
				new SearchOrder(
517
					ISearchOrder::DIRECTION_DESCENDING,
518
					'mtime'
519
				),
520
			]
521
		);
522
		return $this->search($query);
523
	}
524
}
525