Passed
Push — master ( 4c60ff...437d93 )
by Julius
15:25 queued 12s
created

Folder::getDirectoryListing()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 1
nop 0
dl 0
loc 10
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
namespace OC\Files\Node;
32
33
use OC\Files\Cache\QuerySearchHelper;
34
use OC\Files\Search\SearchBinaryOperator;
35
use OC\Files\Cache\Wrapper\CacheJail;
36
use OC\Files\Search\SearchComparison;
37
use OC\Files\Search\SearchOrder;
38
use OC\Files\Search\SearchQuery;
39
use OCP\Files\Cache\ICacheEntry;
40
use OCP\Files\Config\ICachedMountInfo;
41
use OCP\Files\FileInfo;
42
use OCP\Files\Mount\IMountPoint;
43
use OCP\Files\NotFoundException;
44
use OCP\Files\NotPermittedException;
45
use OCP\Files\Search\ISearchBinaryOperator;
46
use OCP\Files\Search\ISearchComparison;
47
use OCP\Files\Search\ISearchOperator;
48
use OCP\Files\Search\ISearchOrder;
49
use OCP\Files\Search\ISearchQuery;
50
use OCP\IUserManager;
51
52
class Folder extends Node implements \OCP\Files\Folder {
53
	/**
54
	 * Creates a Folder that represents a non-existing path
55
	 *
56
	 * @param string $path path
57
	 * @return string non-existing node class
58
	 */
59
	protected function createNonExistingNode($path) {
60
		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...
61
	}
62
63
	/**
64
	 * @param string $path path relative to the folder
65
	 * @return string
66
	 * @throws \OCP\Files\NotPermittedException
67
	 */
68
	public function getFullPath($path) {
69
		if (!$this->isValidPath($path)) {
70
			throw new NotPermittedException('Invalid path');
71
		}
72
		return $this->path . $this->normalizePath($path);
73
	}
74
75
	/**
76
	 * @param string $path
77
	 * @return string|null
78
	 */
79
	public function getRelativePath($path) {
80
		if ($this->path === '' or $this->path === '/') {
81
			return $this->normalizePath($path);
82
		}
83
		if ($path === $this->path) {
84
			return '/';
85
		} elseif (strpos($path, $this->path . '/') !== 0) {
86
			return null;
87
		} else {
88
			$path = substr($path, strlen($this->path));
89
			return $this->normalizePath($path);
90
		}
91
	}
92
93
	/**
94
	 * check if a node is a (grand-)child of the folder
95
	 *
96
	 * @param \OC\Files\Node\Node $node
97
	 * @return bool
98
	 */
99
	public function isSubNode($node) {
100
		return strpos($node->getPath(), $this->path . '/') === 0;
101
	}
102
103
	/**
104
	 * get the content of this directory
105
	 *
106
	 * @return Node[]
107
	 * @throws \OCP\Files\NotFoundException
108
	 */
109
	public function getDirectoryListing() {
110
		$folderContent = $this->view->getDirectoryContent($this->path);
111
112
		return array_map(function (FileInfo $info) {
113
			if ($info->getMimetype() === 'httpd/unix-directory') {
114
				return new Folder($this->root, $this->view, $info->getPath(), $info);
115
			} else {
116
				return new File($this->root, $this->view, $info->getPath(), $info);
117
			}
118
		}, $folderContent);
119
	}
120
121
	/**
122
	 * @param string $path
123
	 * @param FileInfo $info
124
	 * @return File|Folder
125
	 */
126
	protected function createNode($path, FileInfo $info = null) {
127
		if (is_null($info)) {
128
			$isDir = $this->view->is_dir($path);
129
		} else {
130
			$isDir = $info->getType() === FileInfo::TYPE_FOLDER;
131
		}
132
		if ($isDir) {
133
			return new Folder($this->root, $this->view, $path, $info);
134
		} else {
135
			return new File($this->root, $this->view, $path, $info);
136
		}
137
	}
138
139
	/**
140
	 * Get the node at $path
141
	 *
142
	 * @param string $path
143
	 * @return \OC\Files\Node\Node
144
	 * @throws \OCP\Files\NotFoundException
145
	 */
146
	public function get($path) {
147
		return $this->root->get($this->getFullPath($path));
148
	}
149
150
	/**
151
	 * @param string $path
152
	 * @return bool
153
	 */
154
	public function nodeExists($path) {
155
		try {
156
			$this->get($path);
157
			return true;
158
		} catch (NotFoundException $e) {
159
			return false;
160
		}
161
	}
162
163
	/**
164
	 * @param string $path
165
	 * @return \OC\Files\Node\Folder
166
	 * @throws \OCP\Files\NotPermittedException
167
	 */
168
	public function newFolder($path) {
169
		if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
170
			$fullPath = $this->getFullPath($path);
171
			$nonExisting = new NonExistingFolder($this->root, $this->view, $fullPath);
172
			$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
173
			if (!$this->view->mkdir($fullPath)) {
174
				throw new NotPermittedException('Could not create folder');
175
			}
176
			$node = new Folder($this->root, $this->view, $fullPath);
177
			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
178
			return $node;
179
		} else {
180
			throw new NotPermittedException('No create permission for folder');
181
		}
182
	}
183
184
	/**
185
	 * @param string $path
186
	 * @param string | resource | null $content
187
	 * @return \OC\Files\Node\File
188
	 * @throws \OCP\Files\NotPermittedException
189
	 */
190
	public function newFile($path, $content = null) {
191
		if (empty($path)) {
192
			throw new NotPermittedException('Could not create as provided path is empty');
193
		}
194
		if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) {
195
			$fullPath = $this->getFullPath($path);
196
			$nonExisting = new NonExistingFile($this->root, $this->view, $fullPath);
197
			$this->sendHooks(['preWrite', 'preCreate'], [$nonExisting]);
198
			if ($content !== null) {
199
				$result = $this->view->file_put_contents($fullPath, $content);
200
			} else {
201
				$result = $this->view->touch($fullPath);
202
			}
203
			if ($result === false) {
204
				throw new NotPermittedException('Could not create path');
205
			}
206
			$node = new File($this->root, $this->view, $fullPath);
207
			$this->sendHooks(['postWrite', 'postCreate'], [$node]);
208
			return $node;
209
		}
210
		throw new NotPermittedException('No create permission for path');
211
	}
212
213
	private function queryFromOperator(ISearchOperator $operator, string $uid = null): ISearchQuery {
214
		if ($uid === null) {
215
			$user = null;
216
		} else {
217
			/** @var IUserManager $userManager */
218
			$userManager = \OC::$server->query(IUserManager::class);
219
			$user = $userManager->get($uid);
220
		}
221
		return new SearchQuery($operator, 0, 0, [], $user);
222
	}
223
224
	/**
225
	 * search for files with the name matching $query
226
	 *
227
	 * @param string|ISearchQuery $query
228
	 * @return \OC\Files\Node\Node[]
229
	 */
230
	public function search($query) {
231
		if (is_string($query)) {
232
			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%'));
233
		}
234
235
		// search is handled by a single query covering all caches that this folder contains
236
		// this is done by collect
237
238
		$limitToHome = $query->limitToHome();
239
		if ($limitToHome && count(explode('/', $this->path)) !== 3) {
240
			throw new \InvalidArgumentException('searching by owner is only allows on the users home folder');
241
		}
242
243
		$rootLength = strlen($this->path);
244
		$mount = $this->root->getMount($this->path);
245
		$storage = $mount->getStorage();
246
		$internalPath = $mount->getInternalPath($this->path);
247
248
		// collect all caches for this folder, indexed by their mountpoint relative to this folder
249
		// and save the mount which is needed later to construct the FileInfo objects
250
251
		if ($internalPath !== '') {
252
			// a temporary CacheJail is used to handle filtering down the results to within this folder
253
			$caches = ['' => new CacheJail($storage->getCache(''), $internalPath)];
254
		} else {
255
			$caches = ['' => $storage->getCache('')];
256
		}
257
		$mountByMountPoint = ['' => $mount];
258
259
		if (!$limitToHome) {
260
			$mounts = $this->root->getMountsIn($this->path);
261
			foreach ($mounts as $mount) {
262
				$storage = $mount->getStorage();
263
				if ($storage) {
264
					$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
265
					$caches[$relativeMountPoint] = $storage->getCache('');
266
					$mountByMountPoint[$relativeMountPoint] = $mount;
267
				}
268
			}
269
		}
270
271
		/** @var QuerySearchHelper $searchHelper */
272
		$searchHelper = \OC::$server->get(QuerySearchHelper::class);
273
		$resultsPerCache = $searchHelper->searchInCaches($query, $caches);
274
275
		// loop trough all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all
276
		$files = array_merge(...array_map(function (array $results, $relativeMountPoint) use ($mountByMountPoint) {
277
			$mount = $mountByMountPoint[$relativeMountPoint];
278
			return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) {
279
				return $this->cacheEntryToFileInfo($mount, $relativeMountPoint, $result);
280
			}, $results);
281
		}, array_values($resultsPerCache), array_keys($resultsPerCache)));
282
283
		// since results were returned per-cache, they are no longer fully sorted
284
		$order = $query->getOrder();
285
		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...
286
			usort($files, function (FileInfo $a, FileInfo $b) use ($order) {
287
				foreach ($order as $orderField) {
288
					$cmp = $orderField->sortFileInfo($a, $b);
289
					if ($cmp !== 0) {
290
						return $cmp;
291
					}
292
				}
293
				return 0;
294
			});
295
		}
296
297
		return array_map(function (FileInfo $file) {
298
			return $this->createNode($file->getPath(), $file);
299
		}, $files);
300
	}
301
302
	private function cacheEntryToFileInfo(IMountPoint $mount, string $appendRoot, ICacheEntry $cacheEntry): FileInfo {
303
		$cacheEntry['internalPath'] = $cacheEntry['path'];
304
		$cacheEntry['path'] = $appendRoot . $cacheEntry->getPath();
305
		return new \OC\Files\FileInfo($this->path . '/' . $cacheEntry['path'], $mount->getStorage(), $cacheEntry['internalPath'], $cacheEntry, $mount);
306
	}
307
308
	/**
309
	 * search for files by mimetype
310
	 *
311
	 * @param string $mimetype
312
	 * @return Node[]
313
	 */
314
	public function searchByMime($mimetype) {
315
		if (strpos($mimetype, '/') === false) {
316
			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%'));
317
		} else {
318
			$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype));
319
		}
320
		return $this->search($query);
321
	}
322
323
	/**
324
	 * search for files by tag
325
	 *
326
	 * @param string|int $tag name or tag id
327
	 * @param string $userId owner of the tags
328
	 * @return Node[]
329
	 */
330
	public function searchByTag($tag, $userId) {
331
		$query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', $tag), $userId);
332
		return $this->search($query);
333
	}
334
335
	/**
336
	 * @param int $id
337
	 * @return \OC\Files\Node\Node[]
338
	 */
339
	public function getById($id) {
340
		$mountCache = $this->root->getUserMountCache();
341
		if (strpos($this->getPath(), '/', 1) > 0) {
342
			[, $user] = explode('/', $this->getPath());
343
		} else {
344
			$user = null;
345
		}
346
		$mountsContainingFile = $mountCache->getMountsForFileId((int)$id, $user);
347
		$mounts = $this->root->getMountsIn($this->path);
348
		$mounts[] = $this->root->getMount($this->path);
349
		/** @var IMountPoint[] $folderMounts */
350
		$folderMounts = array_combine(array_map(function (IMountPoint $mountPoint) {
351
			return $mountPoint->getMountPoint();
352
		}, $mounts), $mounts);
353
354
		/** @var ICachedMountInfo[] $mountsContainingFile */
355
		$mountsContainingFile = array_values(array_filter($mountsContainingFile, function (ICachedMountInfo $cachedMountInfo) use ($folderMounts) {
356
			return isset($folderMounts[$cachedMountInfo->getMountPoint()]);
357
		}));
358
359
		if (count($mountsContainingFile) === 0) {
360
			if ($user === $this->getAppDataDirectoryName()) {
361
				return $this->getByIdInRootMount((int)$id);
362
			}
363
			return [];
364
		}
365
366
		$nodes = array_map(function (ICachedMountInfo $cachedMountInfo) use ($folderMounts, $id) {
367
			$mount = $folderMounts[$cachedMountInfo->getMountPoint()];
368
			$cacheEntry = $mount->getStorage()->getCache()->get((int)$id);
369
			if (!$cacheEntry) {
370
				return null;
371
			}
372
373
			// cache jails will hide the "true" internal path
374
			$internalPath = ltrim($cachedMountInfo->getRootInternalPath() . '/' . $cacheEntry->getPath(), '/');
375
			$pathRelativeToMount = substr($internalPath, strlen($cachedMountInfo->getRootInternalPath()));
376
			$pathRelativeToMount = ltrim($pathRelativeToMount, '/');
377
			$absolutePath = rtrim($cachedMountInfo->getMountPoint() . $pathRelativeToMount, '/');
378
			return $this->root->createNode($absolutePath, new \OC\Files\FileInfo(
379
				$absolutePath, $mount->getStorage(), $cacheEntry->getPath(), $cacheEntry, $mount,
380
				\OC::$server->getUserManager()->get($mount->getStorage()->getOwner($pathRelativeToMount))
381
			));
382
		}, $mountsContainingFile);
383
384
		$nodes = array_filter($nodes);
385
386
		return array_filter($nodes, function (Node $node) {
387
			return $this->getRelativePath($node->getPath());
388
		});
389
	}
390
391
	protected function getAppDataDirectoryName(): string {
392
		$instanceId = \OC::$server->getConfig()->getSystemValueString('instanceid');
393
		return 'appdata_' . $instanceId;
394
	}
395
396
	/**
397
	 * In case the path we are currently in is inside the appdata_* folder,
398
	 * the original getById method does not work, because it can only look inside
399
	 * the user's mount points. But the user has no mount point for the root storage.
400
	 *
401
	 * So in that case we directly check the mount of the root if it contains
402
	 * the id. If it does we check if the path is inside the path we are working
403
	 * in.
404
	 *
405
	 * @param int $id
406
	 * @return array
407
	 */
408
	protected function getByIdInRootMount(int $id): array {
409
		$mount = $this->root->getMount('');
410
		$cacheEntry = $mount->getStorage()->getCache($this->path)->get($id);
411
		if (!$cacheEntry) {
412
			return [];
413
		}
414
415
		$absolutePath = '/' . ltrim($cacheEntry->getPath(), '/');
416
		$currentPath = rtrim($this->path, '/') . '/';
417
418
		if (strpos($absolutePath, $currentPath) !== 0) {
419
			return [];
420
		}
421
422
		return [$this->root->createNode(
423
			$absolutePath, new \OC\Files\FileInfo(
424
			$absolutePath,
425
			$mount->getStorage(),
426
			$cacheEntry->getPath(),
427
			$cacheEntry,
428
			$mount
429
		))];
430
	}
431
432
	public function getFreeSpace() {
433
		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...
434
	}
435
436
	public function delete() {
437
		if ($this->checkPermissions(\OCP\Constants::PERMISSION_DELETE)) {
438
			$this->sendHooks(['preDelete']);
439
			$fileInfo = $this->getFileInfo();
440
			$this->view->rmdir($this->path);
441
			$nonExisting = new NonExistingFolder($this->root, $this->view, $this->path, $fileInfo);
442
			$this->sendHooks(['postDelete'], [$nonExisting]);
443
			$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...
444
		} else {
445
			throw new NotPermittedException('No delete permission for path');
446
		}
447
	}
448
449
	/**
450
	 * Add a suffix to the name in case the file exists
451
	 *
452
	 * @param string $name
453
	 * @return string
454
	 * @throws NotPermittedException
455
	 */
456
	public function getNonExistingName($name) {
457
		$uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view);
458
		return trim($this->getRelativePath($uniqueName), '/');
459
	}
460
461
	/**
462
	 * @param int $limit
463
	 * @param int $offset
464
	 * @return \OCP\Files\Node[]
465
	 */
466
	public function getRecent($limit, $offset = 0) {
467
		$query = new SearchQuery(
468
			new SearchBinaryOperator(
469
				// filter out non empty folders
470
				ISearchBinaryOperator::OPERATOR_OR,
471
				[
472
					new SearchBinaryOperator(
473
						ISearchBinaryOperator::OPERATOR_NOT,
474
						[
475
							new SearchComparison(
476
								ISearchComparison::COMPARE_EQUAL,
477
								'mimetype',
478
								FileInfo::MIMETYPE_FOLDER
479
							),
480
						]
481
					),
482
					new SearchComparison(
483
						ISearchComparison::COMPARE_EQUAL,
484
						'size',
485
						0
486
					),
487
				]
488
			),
489
			$limit,
490
			$offset,
491
			[
492
				new SearchOrder(
493
					ISearchOrder::DIRECTION_DESCENDING,
494
					'mtime'
495
				),
496
			]
497
		);
498
		return $this->search($query);
499
	}
500
}
501