Issues (2553)

apps/dav/lib/Connector/Sabre/Directory.php (3 issues)

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Jakob Sack <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Julius Härtl <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 * @author Vincent Petry <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program. If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
namespace OCA\DAV\Connector\Sabre;
34
35
use OC\Files\Mount\MoveableMount;
36
use OC\Files\View;
37
use OC\Metadata\FileMetadata;
38
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
39
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
40
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
41
use OCA\DAV\Upload\FutureFile;
42
use OCP\Files\FileInfo;
43
use OCP\Files\Folder;
44
use OCP\Files\ForbiddenException;
45
use OCP\Files\InvalidPathException;
46
use OCP\Files\NotPermittedException;
47
use OCP\Files\StorageNotAvailableException;
48
use OCP\Lock\ILockingProvider;
49
use OCP\Lock\LockedException;
50
use Psr\Log\LoggerInterface;
51
use Sabre\DAV\Exception\BadRequest;
52
use Sabre\DAV\Exception\Locked;
53
use Sabre\DAV\Exception\NotFound;
54
use Sabre\DAV\Exception\ServiceUnavailable;
55
use Sabre\DAV\IFile;
56
use Sabre\DAV\INode;
57
use OCP\Share\IManager as IShareManager;
58
59
class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
60
	/**
61
	 * Cached directory content
62
	 * @var \OCP\Files\FileInfo[]
63
	 */
64
	private ?array $dirContent = null;
65
66
	/** Cached quota info */
67
	private ?array $quotaInfo = null;
68
	private ?CachingTree $tree = null;
69
70
	/** @var array<string, array<int, FileMetadata>> */
71
	private array $metadata = [];
72
73
	/**
74
	 * Sets up the node, expects a full path name
75
	 */
76
	public function __construct(View $view, FileInfo $info, ?CachingTree $tree = null, IShareManager $shareManager = null) {
77
		parent::__construct($view, $info, $shareManager);
78
		$this->tree = $tree;
79
	}
80
81
	/**
82
	 * Creates a new file in the directory
83
	 *
84
	 * Data will either be supplied as a stream resource, or in certain cases
85
	 * as a string. Keep in mind that you may have to support either.
86
	 *
87
	 * After successful creation of the file, you may choose to return the ETag
88
	 * of the new file here.
89
	 *
90
	 * The returned ETag must be surrounded by double-quotes (The quotes should
91
	 * be part of the actual string).
92
	 *
93
	 * If you cannot accurately determine the ETag, you should not return it.
94
	 * If you don't store the file exactly as-is (you're transforming it
95
	 * somehow) you should also not return an ETag.
96
	 *
97
	 * This means that if a subsequent GET to this new file does not exactly
98
	 * return the same contents of what was submitted here, you are strongly
99
	 * recommended to omit the ETag.
100
	 *
101
	 * @param string $name Name of the file
102
	 * @param resource|string $data Initial payload
103
	 * @return null|string
104
	 * @throws Exception\EntityTooLarge
105
	 * @throws Exception\UnsupportedMediaType
106
	 * @throws FileLocked
107
	 * @throws InvalidPath
108
	 * @throws \Sabre\DAV\Exception
109
	 * @throws \Sabre\DAV\Exception\BadRequest
110
	 * @throws \Sabre\DAV\Exception\Forbidden
111
	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
112
	 */
113
	public function createFile($name, $data = null) {
114
		try {
115
			// for chunked upload also updating a existing file is a "createFile"
116
			// because we create all the chunks before re-assemble them to the existing file.
117
			if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
118
				// exit if we can't create a new file and we don't updatable existing file
119
				$chunkInfo = \OC_FileChunking::decodeName($name);
120
				if (!$this->fileView->isCreatable($this->path) &&
121
					!$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
122
				) {
123
					throw new \Sabre\DAV\Exception\Forbidden();
124
				}
125
			} else {
126
				// For non-chunked upload it is enough to check if we can create a new file
127
				if (!$this->fileView->isCreatable($this->path)) {
128
					throw new \Sabre\DAV\Exception\Forbidden();
129
				}
130
			}
131
132
			$this->fileView->verifyPath($this->path, $name);
133
134
			$path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
135
			// in case the file already exists/overwriting
136
			$info = $this->fileView->getFileInfo($this->path . '/' . $name);
137
			if (!$info) {
138
				// use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
139
				$info = new \OC\Files\FileInfo($path, null, null, [
140
					'type' => FileInfo::TYPE_FILE
141
				], null);
142
			}
143
			$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
144
145
			// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
146
			$node->acquireLock(ILockingProvider::LOCK_SHARED);
147
			$this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
148
149
			$result = $node->put($data);
150
151
			$this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
152
			$node->releaseLock(ILockingProvider::LOCK_SHARED);
153
			return $result;
154
		} catch (\OCP\Files\StorageNotAvailableException $e) {
155
			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
156
		} catch (InvalidPathException $ex) {
157
			throw new InvalidPath($ex->getMessage(), false, $ex);
158
		} catch (ForbiddenException $ex) {
159
			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
160
		} catch (LockedException $e) {
161
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
162
		}
163
	}
164
165
	/**
166
	 * Creates a new subdirectory
167
	 *
168
	 * @param string $name
169
	 * @throws FileLocked
170
	 * @throws InvalidPath
171
	 * @throws \Sabre\DAV\Exception\Forbidden
172
	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
173
	 */
174
	public function createDirectory($name) {
175
		try {
176
			if (!$this->info->isCreatable()) {
177
				throw new \Sabre\DAV\Exception\Forbidden();
178
			}
179
180
			$this->fileView->verifyPath($this->path, $name);
181
			$newPath = $this->path . '/' . $name;
182
			if (!$this->fileView->mkdir($newPath)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fileView->mkdir($newPath) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
183
				throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
184
			}
185
		} catch (\OCP\Files\StorageNotAvailableException $e) {
186
			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
187
		} catch (InvalidPathException $ex) {
188
			throw new InvalidPath($ex->getMessage());
189
		} catch (ForbiddenException $ex) {
190
			throw new Forbidden($ex->getMessage(), $ex->getRetry());
191
		} catch (LockedException $e) {
192
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
193
		}
194
	}
195
196
	/**
197
	 * Returns a specific child node, referenced by its name
198
	 *
199
	 * @param string $name
200
	 * @param \OCP\Files\FileInfo $info
201
	 * @return \Sabre\DAV\INode
202
	 * @throws InvalidPath
203
	 * @throws \Sabre\DAV\Exception\NotFound
204
	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
205
	 */
206
	public function getChild($name, $info = null) {
207
		if (!$this->info->isReadable()) {
208
			// avoid detecting files through this way
209
			throw new NotFound();
210
		}
211
212
		$path = $this->path . '/' . $name;
213
		if (is_null($info)) {
214
			try {
215
				$this->fileView->verifyPath($this->path, $name);
216
				$info = $this->fileView->getFileInfo($path);
217
			} catch (\OCP\Files\StorageNotAvailableException $e) {
218
				throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
219
			} catch (InvalidPathException $ex) {
220
				throw new InvalidPath($ex->getMessage());
221
			} catch (ForbiddenException $e) {
222
				throw new \Sabre\DAV\Exception\Forbidden();
223
			}
224
		}
225
226
		if (!$info) {
227
			throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
228
		}
229
230
		if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
231
			$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
232
		} else {
233
			$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager);
234
		}
235
		if ($this->tree) {
236
			$this->tree->cacheNode($node);
237
		}
238
		return $node;
239
	}
240
241
	/**
242
	 * Returns an array with all the child nodes
243
	 *
244
	 * @return \Sabre\DAV\INode[]
245
	 * @throws \Sabre\DAV\Exception\Locked
246
	 * @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden
247
	 */
248
	public function getChildren() {
249
		if (!is_null($this->dirContent)) {
250
			return $this->dirContent;
251
		}
252
		try {
253
			if (!$this->info->isReadable()) {
254
				// return 403 instead of 404 because a 404 would make
255
				// the caller believe that the collection itself does not exist
256
				if (\OCP\Server::get(\OCP\App\IAppManager::class)->isInstalled('files_accesscontrol')) {
257
					throw new Forbidden('No read permissions. This might be caused by files_accesscontrol, check your configured rules');
258
				} else {
259
					throw new Forbidden('No read permissions');
260
				}
261
			}
262
			$folderContent = $this->getNode()->getDirectoryListing();
263
		} catch (LockedException $e) {
264
			throw new Locked();
265
		}
266
267
		$nodes = [];
268
		foreach ($folderContent as $info) {
269
			$node = $this->getChild($info->getName(), $info);
270
			$nodes[] = $node;
271
		}
272
		$this->dirContent = $nodes;
273
		return $this->dirContent;
274
	}
275
276
	/**
277
	 * Checks if a child exists.
278
	 *
279
	 * @param string $name
280
	 * @return bool
281
	 */
282
	public function childExists($name) {
283
		// note: here we do NOT resolve the chunk file name to the real file name
284
		// to make sure we return false when checking for file existence with a chunk
285
		// file name.
286
		// This is to make sure that "createFile" is still triggered
287
		// (required old code) instead of "updateFile".
288
		//
289
		// TODO: resolve chunk file name here and implement "updateFile"
290
		$path = $this->path . '/' . $name;
291
		return $this->fileView->file_exists($path);
292
	}
293
294
	/**
295
	 * Deletes all files in this directory, and then itself
296
	 *
297
	 * @return void
298
	 * @throws FileLocked
299
	 * @throws \Sabre\DAV\Exception\Forbidden
300
	 */
301
	public function delete() {
302
		if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
303
			throw new \Sabre\DAV\Exception\Forbidden();
304
		}
305
306
		try {
307
			if (!$this->fileView->rmdir($this->path)) {
308
				// assume it wasn't possible to remove due to permission issue
309
				throw new \Sabre\DAV\Exception\Forbidden();
310
			}
311
		} catch (ForbiddenException $ex) {
312
			throw new Forbidden($ex->getMessage(), $ex->getRetry());
313
		} catch (LockedException $e) {
314
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
315
		}
316
	}
317
318
	/**
319
	 * Returns available diskspace information
320
	 *
321
	 * @return array
322
	 */
323
	public function getQuotaInfo() {
324
		/** @var LoggerInterface $logger */
325
		$logger = \OC::$server->get(LoggerInterface::class);
326
		if ($this->quotaInfo) {
327
			return $this->quotaInfo;
328
		}
329
		$relativePath = $this->fileView->getRelativePath($this->info->getPath());
330
		if ($relativePath === null) {
331
			$logger->warning("error while getting quota as the relative path cannot be found");
332
			return [0, 0];
333
		}
334
335
		try {
336
			$storageInfo = \OC_Helper::getStorageInfo($relativePath, $this->info, false);
337
			if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
338
				$free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
339
			} else {
340
				$free = $storageInfo['free'];
341
			}
342
			$this->quotaInfo = [
343
				$storageInfo['used'],
344
				$free
345
			];
346
			return $this->quotaInfo;
347
		} catch (\OCP\Files\NotFoundException $e) {
348
			$logger->warning("error while getting quota into", ['exception' => $e]);
349
			return [0, 0];
350
		} catch (\OCP\Files\StorageNotAvailableException $e) {
351
			$logger->warning("error while getting quota into", ['exception' => $e]);
352
			return [0, 0];
353
		} catch (NotPermittedException $e) {
354
			$logger->warning("error while getting quota into", ['exception' => $e]);
355
			return [0, 0];
356
		}
357
	}
358
359
	/**
360
	 * Moves a node into this collection.
361
	 *
362
	 * It is up to the implementors to:
363
	 *   1. Create the new resource.
364
	 *   2. Remove the old resource.
365
	 *   3. Transfer any properties or other data.
366
	 *
367
	 * Generally you should make very sure that your collection can easily move
368
	 * the move.
369
	 *
370
	 * If you don't, just return false, which will trigger sabre/dav to handle
371
	 * the move itself. If you return true from this function, the assumption
372
	 * is that the move was successful.
373
	 *
374
	 * @param string $targetName New local file/collection name.
375
	 * @param string $fullSourcePath Full path to source node
376
	 * @param INode $sourceNode Source node itself
377
	 * @return bool
378
	 * @throws BadRequest
379
	 * @throws ServiceUnavailable
380
	 * @throws Forbidden
381
	 * @throws FileLocked
382
	 * @throws \Sabre\DAV\Exception\Forbidden
383
	 */
384
	public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
385
		if (!$sourceNode instanceof Node) {
386
			// it's a file of another kind, like FutureFile
387
			if ($sourceNode instanceof IFile) {
388
				// fallback to default copy+delete handling
389
				return false;
390
			}
391
			throw new BadRequest('Incompatible node types');
392
		}
393
394
		if (!$this->fileView) {
395
			throw new ServiceUnavailable('filesystem not setup');
396
		}
397
398
		$destinationPath = $this->getPath() . '/' . $targetName;
399
400
401
		$targetNodeExists = $this->childExists($targetName);
402
403
		// at getNodeForPath we also check the path for isForbiddenFileOrDir
404
		// with that we have covered both source and destination
405
		if ($sourceNode instanceof Directory && $targetNodeExists) {
406
			throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
407
		}
408
409
		[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
410
		$destinationDir = $this->getPath();
411
412
		$sourcePath = $sourceNode->getPath();
413
414
		$isMovableMount = false;
415
		$sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath));
416
		$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
417
		if ($sourceMount instanceof MoveableMount && $internalPath === '') {
418
			$isMovableMount = true;
419
		}
420
421
		try {
422
			$sameFolder = ($sourceDir === $destinationDir);
423
			// if we're overwriting or same folder
424
			if ($targetNodeExists || $sameFolder) {
425
				// note that renaming a share mount point is always allowed
426
				if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
427
					throw new \Sabre\DAV\Exception\Forbidden();
428
				}
429
			} else {
430
				if (!$this->fileView->isCreatable($destinationDir)) {
431
					throw new \Sabre\DAV\Exception\Forbidden();
432
				}
433
			}
434
435
			if (!$sameFolder) {
436
				// moving to a different folder, source will be gone, like a deletion
437
				// note that moving a share mount point is always allowed
438
				if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
439
					throw new \Sabre\DAV\Exception\Forbidden();
440
				}
441
			}
442
443
			$fileName = basename($destinationPath);
444
			try {
445
				$this->fileView->verifyPath($destinationDir, $fileName);
446
			} catch (InvalidPathException $ex) {
447
				throw new InvalidPath($ex->getMessage());
448
			}
449
450
			$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
451
			if (!$renameOkay) {
452
				throw new \Sabre\DAV\Exception\Forbidden('');
453
			}
454
		} catch (StorageNotAvailableException $e) {
455
			throw new ServiceUnavailable($e->getMessage());
456
		} catch (ForbiddenException $ex) {
457
			throw new Forbidden($ex->getMessage(), $ex->getRetry());
458
		} catch (LockedException $e) {
459
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
460
		}
461
462
		return true;
463
	}
464
465
466
	public function copyInto($targetName, $sourcePath, INode $sourceNode) {
467
		if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
468
			$destinationPath = $this->getPath() . '/' . $targetName;
469
			$sourcePath = $sourceNode->getPath();
470
471
			if (!$this->fileView->isCreatable($this->getPath())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fileView->isCreatable($this->getPath()) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
472
				throw new \Sabre\DAV\Exception\Forbidden();
473
			}
474
475
			try {
476
				$this->fileView->verifyPath($this->getPath(), $targetName);
477
			} catch (InvalidPathException $ex) {
478
				throw new InvalidPath($ex->getMessage());
479
			}
480
481
			return $this->fileView->copy($sourcePath, $destinationPath);
482
		}
483
484
		return false;
485
	}
486
487
	public function getNode(): Folder {
488
		return $this->node;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->node returns the type OCP\Files\Node which includes types incompatible with the type-hinted return OCP\Files\Folder.
Loading history...
489
	}
490
}
491