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
|
|||
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
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 $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
|
|||
489 | } |
||
490 | } |
||
491 |
If an expression can have both
false
, andnull
as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.