Passed
Push — master ( 2e803d...3ba33c )
by Morris
09:46 queued 10s
created
apps/dav/lib/Connector/Sabre/File.php 1 patch
Indentation   +577 added lines, -577 removed lines patch added patch discarded remove patch
@@ -69,581 +69,581 @@
 block discarded – undo
69 69
 
70 70
 class File extends Node implements IFile {
71 71
 
72
-	protected $request;
73
-
74
-	/**
75
-	 * Sets up the node, expects a full path name
76
-	 *
77
-	 * @param \OC\Files\View $view
78
-	 * @param \OCP\Files\FileInfo $info
79
-	 * @param \OCP\Share\IManager $shareManager
80
-	 * @param \OC\AppFramework\Http\Request $request
81
-	 */
82
-	public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) {
83
-		parent::__construct($view, $info, $shareManager);
84
-
85
-		if (isset($request)) {
86
-			$this->request = $request;
87
-		} else {
88
-			$this->request = \OC::$server->getRequest();
89
-		}
90
-	}
91
-
92
-	/**
93
-	 * Updates the data
94
-	 *
95
-	 * The data argument is a readable stream resource.
96
-	 *
97
-	 * After a successful put operation, you may choose to return an ETag. The
98
-	 * etag must always be surrounded by double-quotes. These quotes must
99
-	 * appear in the actual string you're returning.
100
-	 *
101
-	 * Clients may use the ETag from a PUT request to later on make sure that
102
-	 * when they update the file, the contents haven't changed in the mean
103
-	 * time.
104
-	 *
105
-	 * If you don't plan to store the file byte-by-byte, and you return a
106
-	 * different object on a subsequent GET you are strongly recommended to not
107
-	 * return an ETag, and just return null.
108
-	 *
109
-	 * @param resource $data
110
-	 *
111
-	 * @throws Forbidden
112
-	 * @throws UnsupportedMediaType
113
-	 * @throws BadRequest
114
-	 * @throws Exception
115
-	 * @throws EntityTooLarge
116
-	 * @throws ServiceUnavailable
117
-	 * @throws FileLocked
118
-	 * @return string|null
119
-	 */
120
-	public function put($data) {
121
-		try {
122
-			$exists = $this->fileView->file_exists($this->path);
123
-			if ($this->info && $exists && !$this->info->isUpdateable()) {
124
-				throw new Forbidden();
125
-			}
126
-		} catch (StorageNotAvailableException $e) {
127
-			throw new ServiceUnavailable("File is not updatable: " . $e->getMessage());
128
-		}
129
-
130
-		// verify path of the target
131
-		$this->verifyPath();
132
-
133
-		// chunked handling
134
-		if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
135
-			try {
136
-				return $this->createFileChunked($data);
137
-			} catch (\Exception $e) {
138
-				$this->convertToSabreException($e);
139
-			}
140
-		}
141
-
142
-		/** @var Storage $partStorage */
143
-		list($partStorage) = $this->fileView->resolvePath($this->path);
144
-		$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
145
-
146
-		$view = \OC\Files\Filesystem::getView();
147
-
148
-		if ($needsPartFile) {
149
-			// mark file as partial while uploading (ignored by the scanner)
150
-			$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
151
-
152
-			if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
153
-				$needsPartFile = false;
154
-			}
155
-		}
156
-		if (!$needsPartFile) {
157
-			// upload file directly as the final path
158
-			$partFilePath = $this->path;
159
-
160
-			if ($view && !$this->emitPreHooks($exists)) {
161
-				throw new Exception('Could not write to final file, canceled by hook');
162
-			}
163
-		}
164
-
165
-		// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
166
-		/** @var \OC\Files\Storage\Storage $partStorage */
167
-		list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
168
-		/** @var \OC\Files\Storage\Storage $storage */
169
-		list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
170
-		try {
171
-			if (!$needsPartFile) {
172
-				$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
173
-			}
174
-
175
-			if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
176
-
177
-				if (!is_resource($data)) {
178
-					$tmpData = fopen('php://temp', 'r+');
179
-					if ($data !== null) {
180
-						fwrite($tmpData, $data);
181
-						rewind($tmpData);
182
-					}
183
-					$data = $tmpData;
184
-				}
185
-
186
-				$isEOF = false;
187
-				$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
188
-					$isEOF = feof($stream);
189
-				});
190
-
191
-				$count = $partStorage->writeStream($internalPartPath, $wrappedData);
192
-				$result = $count > 0;
193
-
194
-				if ($result === false) {
195
-					$result = $isEOF;
196
-					if (is_resource($wrappedData)) {
197
-						$result = feof($wrappedData);
198
-					}
199
-				}
200
-
201
-			} else {
202
-				$target = $partStorage->fopen($internalPartPath, 'wb');
203
-				if ($target === false) {
204
-					\OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
205
-					// because we have no clue about the cause we can only throw back a 500/Internal Server Error
206
-					throw new Exception('Could not write file contents');
207
-				}
208
-				list($count, $result) = \OC_Helper::streamCopy($data, $target);
209
-				fclose($target);
210
-			}
211
-
212
-			if ($result === false) {
213
-				$expected = -1;
214
-				if (isset($_SERVER['CONTENT_LENGTH'])) {
215
-					$expected = $_SERVER['CONTENT_LENGTH'];
216
-				}
217
-				if ($expected !== "0") {
218
-					throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )');
219
-				}
220
-			}
221
-
222
-			// if content length is sent by client:
223
-			// double check if the file was fully received
224
-			// compare expected and actual size
225
-			if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
226
-				$expected = (int)$_SERVER['CONTENT_LENGTH'];
227
-				if ($count !== $expected) {
228
-					throw new BadRequest('expected filesize ' . $expected . ' got ' . $count);
229
-				}
230
-			}
231
-
232
-		} catch (\Exception $e) {
233
-			$context = [];
234
-
235
-			if ($e instanceof LockedException) {
236
-				$context['level'] = ILogger::DEBUG;
237
-			}
238
-
239
-			\OC::$server->getLogger()->logException($e, $context);
240
-			if ($needsPartFile) {
241
-				$partStorage->unlink($internalPartPath);
242
-			}
243
-			$this->convertToSabreException($e);
244
-		}
245
-
246
-		try {
247
-			if ($needsPartFile) {
248
-				if ($view && !$this->emitPreHooks($exists)) {
249
-					$partStorage->unlink($internalPartPath);
250
-					throw new Exception('Could not rename part file to final file, canceled by hook');
251
-				}
252
-				try {
253
-					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
254
-				} catch (LockedException $e) {
255
-					if ($needsPartFile) {
256
-						$partStorage->unlink($internalPartPath);
257
-					}
258
-					throw new FileLocked($e->getMessage(), $e->getCode(), $e);
259
-				}
260
-
261
-				// rename to correct path
262
-				try {
263
-					$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
264
-					$fileExists = $storage->file_exists($internalPath);
265
-					if ($renameOkay === false || $fileExists === false) {
266
-						\OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
267
-						throw new Exception('Could not rename part file to final file');
268
-					}
269
-				} catch (ForbiddenException $ex) {
270
-					throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
271
-				} catch (\Exception $e) {
272
-					$partStorage->unlink($internalPartPath);
273
-					$this->convertToSabreException($e);
274
-				}
275
-			}
276
-
277
-			// since we skipped the view we need to scan and emit the hooks ourselves
278
-			$storage->getUpdater()->update($internalPath);
279
-
280
-			try {
281
-				$this->changeLock(ILockingProvider::LOCK_SHARED);
282
-			} catch (LockedException $e) {
283
-				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
284
-			}
285
-
286
-			// allow sync clients to send the mtime along in a header
287
-			if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
288
-				$mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
289
-				if ($this->fileView->touch($this->path, $mtime)) {
290
-					$this->header('X-OC-MTime: accepted');
291
-				}
292
-			}
293
-
294
-			if ($view) {
295
-				$this->emitPostHooks($exists);
296
-			}
297
-
298
-			$this->refreshInfo();
299
-
300
-			if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
301
-				$checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
302
-				$this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
303
-				$this->refreshInfo();
304
-			} else if ($this->getChecksum() !== null && $this->getChecksum() !== '') {
305
-				$this->fileView->putFileInfo($this->path, ['checksum' => '']);
306
-				$this->refreshInfo();
307
-			}
308
-
309
-		} catch (StorageNotAvailableException $e) {
310
-			throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage(), 0, $e);
311
-		}
312
-
313
-		return '"' . $this->info->getEtag() . '"';
314
-	}
315
-
316
-	private function getPartFileBasePath($path) {
317
-		$partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
318
-		if ($partFileInStorage) {
319
-			return $path;
320
-		} else {
321
-			return md5($path); // will place it in the root of the view with a unique name
322
-		}
323
-	}
324
-
325
-	/**
326
-	 * @param string $path
327
-	 */
328
-	private function emitPreHooks($exists, $path = null) {
329
-		if (is_null($path)) {
330
-			$path = $this->path;
331
-		}
332
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
333
-		$run = true;
334
-
335
-		if (!$exists) {
336
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array(
337
-				\OC\Files\Filesystem::signal_param_path => $hookPath,
338
-				\OC\Files\Filesystem::signal_param_run => &$run,
339
-			));
340
-		} else {
341
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array(
342
-				\OC\Files\Filesystem::signal_param_path => $hookPath,
343
-				\OC\Files\Filesystem::signal_param_run => &$run,
344
-			));
345
-		}
346
-		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array(
347
-			\OC\Files\Filesystem::signal_param_path => $hookPath,
348
-			\OC\Files\Filesystem::signal_param_run => &$run,
349
-		));
350
-		return $run;
351
-	}
352
-
353
-	/**
354
-	 * @param string $path
355
-	 */
356
-	private function emitPostHooks($exists, $path = null) {
357
-		if (is_null($path)) {
358
-			$path = $this->path;
359
-		}
360
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
361
-		if (!$exists) {
362
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
363
-				\OC\Files\Filesystem::signal_param_path => $hookPath
364
-			));
365
-		} else {
366
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array(
367
-				\OC\Files\Filesystem::signal_param_path => $hookPath
368
-			));
369
-		}
370
-		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array(
371
-			\OC\Files\Filesystem::signal_param_path => $hookPath
372
-		));
373
-	}
374
-
375
-	/**
376
-	 * Returns the data
377
-	 *
378
-	 * @return resource
379
-	 * @throws Forbidden
380
-	 * @throws ServiceUnavailable
381
-	 */
382
-	public function get() {
383
-		//throw exception if encryption is disabled but files are still encrypted
384
-		try {
385
-			if (!$this->info->isReadable()) {
386
-				// do a if the file did not exist
387
-				throw new NotFound();
388
-			}
389
-			try {
390
-				$res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
391
-			} catch (\Exception $e) {
392
-				$this->convertToSabreException($e);
393
-			}
394
-			if ($res === false) {
395
-				throw new ServiceUnavailable("Could not open file");
396
-			}
397
-			return $res;
398
-		} catch (GenericEncryptionException $e) {
399
-			// returning 503 will allow retry of the operation at a later point in time
400
-			throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage());
401
-		} catch (StorageNotAvailableException $e) {
402
-			throw new ServiceUnavailable("Failed to open file: " . $e->getMessage());
403
-		} catch (ForbiddenException $ex) {
404
-			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
405
-		} catch (LockedException $e) {
406
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
407
-		}
408
-	}
409
-
410
-	/**
411
-	 * Delete the current file
412
-	 *
413
-	 * @throws Forbidden
414
-	 * @throws ServiceUnavailable
415
-	 */
416
-	public function delete() {
417
-		if (!$this->info->isDeletable()) {
418
-			throw new Forbidden();
419
-		}
420
-
421
-		try {
422
-			if (!$this->fileView->unlink($this->path)) {
423
-				// assume it wasn't possible to delete due to permissions
424
-				throw new Forbidden();
425
-			}
426
-		} catch (StorageNotAvailableException $e) {
427
-			throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage());
428
-		} catch (ForbiddenException $ex) {
429
-			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
430
-		} catch (LockedException $e) {
431
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
432
-		}
433
-	}
434
-
435
-	/**
436
-	 * Returns the mime-type for a file
437
-	 *
438
-	 * If null is returned, we'll assume application/octet-stream
439
-	 *
440
-	 * @return string
441
-	 */
442
-	public function getContentType() {
443
-		$mimeType = $this->info->getMimetype();
444
-
445
-		// PROPFIND needs to return the correct mime type, for consistency with the web UI
446
-		if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
447
-			return $mimeType;
448
-		}
449
-		return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
450
-	}
451
-
452
-	/**
453
-	 * @return array|false
454
-	 */
455
-	public function getDirectDownload() {
456
-		if (\OCP\App::isEnabled('encryption')) {
457
-			return [];
458
-		}
459
-		/** @var \OCP\Files\Storage $storage */
460
-		list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
461
-		if (is_null($storage)) {
462
-			return [];
463
-		}
464
-
465
-		return $storage->getDirectDownload($internalPath);
466
-	}
467
-
468
-	/**
469
-	 * @param resource $data
470
-	 * @return null|string
471
-	 * @throws Exception
472
-	 * @throws BadRequest
473
-	 * @throws NotImplemented
474
-	 * @throws ServiceUnavailable
475
-	 */
476
-	private function createFileChunked($data) {
477
-		list($path, $name) = \Sabre\Uri\split($this->path);
478
-
479
-		$info = \OC_FileChunking::decodeName($name);
480
-		if (empty($info)) {
481
-			throw new NotImplemented('Invalid chunk name');
482
-		}
483
-
484
-		$chunk_handler = new \OC_FileChunking($info);
485
-		$bytesWritten = $chunk_handler->store($info['index'], $data);
486
-
487
-		//detect aborted upload
488
-		if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
489
-			if (isset($_SERVER['CONTENT_LENGTH'])) {
490
-				$expected = (int)$_SERVER['CONTENT_LENGTH'];
491
-				if ($bytesWritten !== $expected) {
492
-					$chunk_handler->remove($info['index']);
493
-					throw new BadRequest(
494
-						'expected filesize ' . $expected . ' got ' . $bytesWritten);
495
-				}
496
-			}
497
-		}
498
-
499
-		if ($chunk_handler->isComplete()) {
500
-			/** @var Storage $storage */
501
-			list($storage,) = $this->fileView->resolvePath($path);
502
-			$needsPartFile = $storage->needsPartFile();
503
-			$partFile = null;
504
-
505
-			$targetPath = $path . '/' . $info['name'];
506
-			/** @var \OC\Files\Storage\Storage $targetStorage */
507
-			list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
508
-
509
-			$exists = $this->fileView->file_exists($targetPath);
510
-
511
-			try {
512
-				$this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
513
-
514
-				$this->emitPreHooks($exists, $targetPath);
515
-				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
516
-				/** @var \OC\Files\Storage\Storage $targetStorage */
517
-				list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
518
-
519
-				if ($needsPartFile) {
520
-					// we first assembly the target file as a part file
521
-					$partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
522
-					/** @var \OC\Files\Storage\Storage $targetStorage */
523
-					list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile);
524
-
525
-
526
-					$chunk_handler->file_assemble($partStorage, $partInternalPath);
527
-
528
-					// here is the final atomic rename
529
-					$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
530
-					$fileExists = $targetStorage->file_exists($targetInternalPath);
531
-					if ($renameOkay === false || $fileExists === false) {
532
-						\OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
533
-						// only delete if an error occurred and the target file was already created
534
-						if ($fileExists) {
535
-							// set to null to avoid double-deletion when handling exception
536
-							// stray part file
537
-							$partFile = null;
538
-							$targetStorage->unlink($targetInternalPath);
539
-						}
540
-						$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
541
-						throw new Exception('Could not rename part file assembled from chunks');
542
-					}
543
-				} else {
544
-					// assemble directly into the final file
545
-					$chunk_handler->file_assemble($targetStorage, $targetInternalPath);
546
-				}
547
-
548
-				// allow sync clients to send the mtime along in a header
549
-				if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
550
-					$mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
551
-					if ($targetStorage->touch($targetInternalPath, $mtime)) {
552
-						$this->header('X-OC-MTime: accepted');
553
-					}
554
-				}
555
-
556
-				// since we skipped the view we need to scan and emit the hooks ourselves
557
-				$targetStorage->getUpdater()->update($targetInternalPath);
558
-
559
-				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
560
-
561
-				$this->emitPostHooks($exists, $targetPath);
562
-
563
-				// FIXME: should call refreshInfo but can't because $this->path is not the of the final file
564
-				$info = $this->fileView->getFileInfo($targetPath);
565
-
566
-				if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
567
-					$checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
568
-					$this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
569
-				} else if ($info->getChecksum() !== null && $info->getChecksum() !== '') {
570
-					$this->fileView->putFileInfo($this->path, ['checksum' => '']);
571
-				}
572
-
573
-				$this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
574
-
575
-				return $info->getEtag();
576
-			} catch (\Exception $e) {
577
-				if ($partFile !== null) {
578
-					$targetStorage->unlink($targetInternalPath);
579
-				}
580
-				$this->convertToSabreException($e);
581
-			}
582
-		}
583
-
584
-		return null;
585
-	}
586
-
587
-	/**
588
-	 * Convert the given exception to a SabreException instance
589
-	 *
590
-	 * @param \Exception $e
591
-	 *
592
-	 * @throws \Sabre\DAV\Exception
593
-	 */
594
-	private function convertToSabreException(\Exception $e) {
595
-		if ($e instanceof \Sabre\DAV\Exception) {
596
-			throw $e;
597
-		}
598
-		if ($e instanceof NotPermittedException) {
599
-			// a more general case - due to whatever reason the content could not be written
600
-			throw new Forbidden($e->getMessage(), 0, $e);
601
-		}
602
-		if ($e instanceof ForbiddenException) {
603
-			// the path for the file was forbidden
604
-			throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
605
-		}
606
-		if ($e instanceof EntityTooLargeException) {
607
-			// the file is too big to be stored
608
-			throw new EntityTooLarge($e->getMessage(), 0, $e);
609
-		}
610
-		if ($e instanceof InvalidContentException) {
611
-			// the file content is not permitted
612
-			throw new UnsupportedMediaType($e->getMessage(), 0, $e);
613
-		}
614
-		if ($e instanceof InvalidPathException) {
615
-			// the path for the file was not valid
616
-			// TODO: find proper http status code for this case
617
-			throw new Forbidden($e->getMessage(), 0, $e);
618
-		}
619
-		if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
620
-			// the file is currently being written to by another process
621
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
622
-		}
623
-		if ($e instanceof GenericEncryptionException) {
624
-			// returning 503 will allow retry of the operation at a later point in time
625
-			throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e);
626
-		}
627
-		if ($e instanceof StorageNotAvailableException) {
628
-			throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e);
629
-		}
630
-		if ($e instanceof NotFoundException) {
631
-			throw new NotFound('File not found: ' . $e->getMessage(), 0, $e);
632
-		}
633
-
634
-		throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
635
-	}
636
-
637
-	/**
638
-	 * Get the checksum for this file
639
-	 *
640
-	 * @return string
641
-	 */
642
-	public function getChecksum() {
643
-		return $this->info->getChecksum();
644
-	}
645
-
646
-	protected function header($string) {
647
-		\header($string);
648
-	}
72
+    protected $request;
73
+
74
+    /**
75
+     * Sets up the node, expects a full path name
76
+     *
77
+     * @param \OC\Files\View $view
78
+     * @param \OCP\Files\FileInfo $info
79
+     * @param \OCP\Share\IManager $shareManager
80
+     * @param \OC\AppFramework\Http\Request $request
81
+     */
82
+    public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) {
83
+        parent::__construct($view, $info, $shareManager);
84
+
85
+        if (isset($request)) {
86
+            $this->request = $request;
87
+        } else {
88
+            $this->request = \OC::$server->getRequest();
89
+        }
90
+    }
91
+
92
+    /**
93
+     * Updates the data
94
+     *
95
+     * The data argument is a readable stream resource.
96
+     *
97
+     * After a successful put operation, you may choose to return an ETag. The
98
+     * etag must always be surrounded by double-quotes. These quotes must
99
+     * appear in the actual string you're returning.
100
+     *
101
+     * Clients may use the ETag from a PUT request to later on make sure that
102
+     * when they update the file, the contents haven't changed in the mean
103
+     * time.
104
+     *
105
+     * If you don't plan to store the file byte-by-byte, and you return a
106
+     * different object on a subsequent GET you are strongly recommended to not
107
+     * return an ETag, and just return null.
108
+     *
109
+     * @param resource $data
110
+     *
111
+     * @throws Forbidden
112
+     * @throws UnsupportedMediaType
113
+     * @throws BadRequest
114
+     * @throws Exception
115
+     * @throws EntityTooLarge
116
+     * @throws ServiceUnavailable
117
+     * @throws FileLocked
118
+     * @return string|null
119
+     */
120
+    public function put($data) {
121
+        try {
122
+            $exists = $this->fileView->file_exists($this->path);
123
+            if ($this->info && $exists && !$this->info->isUpdateable()) {
124
+                throw new Forbidden();
125
+            }
126
+        } catch (StorageNotAvailableException $e) {
127
+            throw new ServiceUnavailable("File is not updatable: " . $e->getMessage());
128
+        }
129
+
130
+        // verify path of the target
131
+        $this->verifyPath();
132
+
133
+        // chunked handling
134
+        if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
135
+            try {
136
+                return $this->createFileChunked($data);
137
+            } catch (\Exception $e) {
138
+                $this->convertToSabreException($e);
139
+            }
140
+        }
141
+
142
+        /** @var Storage $partStorage */
143
+        list($partStorage) = $this->fileView->resolvePath($this->path);
144
+        $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
145
+
146
+        $view = \OC\Files\Filesystem::getView();
147
+
148
+        if ($needsPartFile) {
149
+            // mark file as partial while uploading (ignored by the scanner)
150
+            $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
151
+
152
+            if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
153
+                $needsPartFile = false;
154
+            }
155
+        }
156
+        if (!$needsPartFile) {
157
+            // upload file directly as the final path
158
+            $partFilePath = $this->path;
159
+
160
+            if ($view && !$this->emitPreHooks($exists)) {
161
+                throw new Exception('Could not write to final file, canceled by hook');
162
+            }
163
+        }
164
+
165
+        // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
166
+        /** @var \OC\Files\Storage\Storage $partStorage */
167
+        list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
168
+        /** @var \OC\Files\Storage\Storage $storage */
169
+        list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
170
+        try {
171
+            if (!$needsPartFile) {
172
+                $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
173
+            }
174
+
175
+            if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
176
+
177
+                if (!is_resource($data)) {
178
+                    $tmpData = fopen('php://temp', 'r+');
179
+                    if ($data !== null) {
180
+                        fwrite($tmpData, $data);
181
+                        rewind($tmpData);
182
+                    }
183
+                    $data = $tmpData;
184
+                }
185
+
186
+                $isEOF = false;
187
+                $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
188
+                    $isEOF = feof($stream);
189
+                });
190
+
191
+                $count = $partStorage->writeStream($internalPartPath, $wrappedData);
192
+                $result = $count > 0;
193
+
194
+                if ($result === false) {
195
+                    $result = $isEOF;
196
+                    if (is_resource($wrappedData)) {
197
+                        $result = feof($wrappedData);
198
+                    }
199
+                }
200
+
201
+            } else {
202
+                $target = $partStorage->fopen($internalPartPath, 'wb');
203
+                if ($target === false) {
204
+                    \OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
205
+                    // because we have no clue about the cause we can only throw back a 500/Internal Server Error
206
+                    throw new Exception('Could not write file contents');
207
+                }
208
+                list($count, $result) = \OC_Helper::streamCopy($data, $target);
209
+                fclose($target);
210
+            }
211
+
212
+            if ($result === false) {
213
+                $expected = -1;
214
+                if (isset($_SERVER['CONTENT_LENGTH'])) {
215
+                    $expected = $_SERVER['CONTENT_LENGTH'];
216
+                }
217
+                if ($expected !== "0") {
218
+                    throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )');
219
+                }
220
+            }
221
+
222
+            // if content length is sent by client:
223
+            // double check if the file was fully received
224
+            // compare expected and actual size
225
+            if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
226
+                $expected = (int)$_SERVER['CONTENT_LENGTH'];
227
+                if ($count !== $expected) {
228
+                    throw new BadRequest('expected filesize ' . $expected . ' got ' . $count);
229
+                }
230
+            }
231
+
232
+        } catch (\Exception $e) {
233
+            $context = [];
234
+
235
+            if ($e instanceof LockedException) {
236
+                $context['level'] = ILogger::DEBUG;
237
+            }
238
+
239
+            \OC::$server->getLogger()->logException($e, $context);
240
+            if ($needsPartFile) {
241
+                $partStorage->unlink($internalPartPath);
242
+            }
243
+            $this->convertToSabreException($e);
244
+        }
245
+
246
+        try {
247
+            if ($needsPartFile) {
248
+                if ($view && !$this->emitPreHooks($exists)) {
249
+                    $partStorage->unlink($internalPartPath);
250
+                    throw new Exception('Could not rename part file to final file, canceled by hook');
251
+                }
252
+                try {
253
+                    $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
254
+                } catch (LockedException $e) {
255
+                    if ($needsPartFile) {
256
+                        $partStorage->unlink($internalPartPath);
257
+                    }
258
+                    throw new FileLocked($e->getMessage(), $e->getCode(), $e);
259
+                }
260
+
261
+                // rename to correct path
262
+                try {
263
+                    $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
264
+                    $fileExists = $storage->file_exists($internalPath);
265
+                    if ($renameOkay === false || $fileExists === false) {
266
+                        \OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
267
+                        throw new Exception('Could not rename part file to final file');
268
+                    }
269
+                } catch (ForbiddenException $ex) {
270
+                    throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
271
+                } catch (\Exception $e) {
272
+                    $partStorage->unlink($internalPartPath);
273
+                    $this->convertToSabreException($e);
274
+                }
275
+            }
276
+
277
+            // since we skipped the view we need to scan and emit the hooks ourselves
278
+            $storage->getUpdater()->update($internalPath);
279
+
280
+            try {
281
+                $this->changeLock(ILockingProvider::LOCK_SHARED);
282
+            } catch (LockedException $e) {
283
+                throw new FileLocked($e->getMessage(), $e->getCode(), $e);
284
+            }
285
+
286
+            // allow sync clients to send the mtime along in a header
287
+            if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
288
+                $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
289
+                if ($this->fileView->touch($this->path, $mtime)) {
290
+                    $this->header('X-OC-MTime: accepted');
291
+                }
292
+            }
293
+
294
+            if ($view) {
295
+                $this->emitPostHooks($exists);
296
+            }
297
+
298
+            $this->refreshInfo();
299
+
300
+            if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
301
+                $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
302
+                $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
303
+                $this->refreshInfo();
304
+            } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') {
305
+                $this->fileView->putFileInfo($this->path, ['checksum' => '']);
306
+                $this->refreshInfo();
307
+            }
308
+
309
+        } catch (StorageNotAvailableException $e) {
310
+            throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage(), 0, $e);
311
+        }
312
+
313
+        return '"' . $this->info->getEtag() . '"';
314
+    }
315
+
316
+    private function getPartFileBasePath($path) {
317
+        $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
318
+        if ($partFileInStorage) {
319
+            return $path;
320
+        } else {
321
+            return md5($path); // will place it in the root of the view with a unique name
322
+        }
323
+    }
324
+
325
+    /**
326
+     * @param string $path
327
+     */
328
+    private function emitPreHooks($exists, $path = null) {
329
+        if (is_null($path)) {
330
+            $path = $this->path;
331
+        }
332
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
333
+        $run = true;
334
+
335
+        if (!$exists) {
336
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array(
337
+                \OC\Files\Filesystem::signal_param_path => $hookPath,
338
+                \OC\Files\Filesystem::signal_param_run => &$run,
339
+            ));
340
+        } else {
341
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array(
342
+                \OC\Files\Filesystem::signal_param_path => $hookPath,
343
+                \OC\Files\Filesystem::signal_param_run => &$run,
344
+            ));
345
+        }
346
+        \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array(
347
+            \OC\Files\Filesystem::signal_param_path => $hookPath,
348
+            \OC\Files\Filesystem::signal_param_run => &$run,
349
+        ));
350
+        return $run;
351
+    }
352
+
353
+    /**
354
+     * @param string $path
355
+     */
356
+    private function emitPostHooks($exists, $path = null) {
357
+        if (is_null($path)) {
358
+            $path = $this->path;
359
+        }
360
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
361
+        if (!$exists) {
362
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
363
+                \OC\Files\Filesystem::signal_param_path => $hookPath
364
+            ));
365
+        } else {
366
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array(
367
+                \OC\Files\Filesystem::signal_param_path => $hookPath
368
+            ));
369
+        }
370
+        \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array(
371
+            \OC\Files\Filesystem::signal_param_path => $hookPath
372
+        ));
373
+    }
374
+
375
+    /**
376
+     * Returns the data
377
+     *
378
+     * @return resource
379
+     * @throws Forbidden
380
+     * @throws ServiceUnavailable
381
+     */
382
+    public function get() {
383
+        //throw exception if encryption is disabled but files are still encrypted
384
+        try {
385
+            if (!$this->info->isReadable()) {
386
+                // do a if the file did not exist
387
+                throw new NotFound();
388
+            }
389
+            try {
390
+                $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
391
+            } catch (\Exception $e) {
392
+                $this->convertToSabreException($e);
393
+            }
394
+            if ($res === false) {
395
+                throw new ServiceUnavailable("Could not open file");
396
+            }
397
+            return $res;
398
+        } catch (GenericEncryptionException $e) {
399
+            // returning 503 will allow retry of the operation at a later point in time
400
+            throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage());
401
+        } catch (StorageNotAvailableException $e) {
402
+            throw new ServiceUnavailable("Failed to open file: " . $e->getMessage());
403
+        } catch (ForbiddenException $ex) {
404
+            throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
405
+        } catch (LockedException $e) {
406
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
407
+        }
408
+    }
409
+
410
+    /**
411
+     * Delete the current file
412
+     *
413
+     * @throws Forbidden
414
+     * @throws ServiceUnavailable
415
+     */
416
+    public function delete() {
417
+        if (!$this->info->isDeletable()) {
418
+            throw new Forbidden();
419
+        }
420
+
421
+        try {
422
+            if (!$this->fileView->unlink($this->path)) {
423
+                // assume it wasn't possible to delete due to permissions
424
+                throw new Forbidden();
425
+            }
426
+        } catch (StorageNotAvailableException $e) {
427
+            throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage());
428
+        } catch (ForbiddenException $ex) {
429
+            throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
430
+        } catch (LockedException $e) {
431
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
432
+        }
433
+    }
434
+
435
+    /**
436
+     * Returns the mime-type for a file
437
+     *
438
+     * If null is returned, we'll assume application/octet-stream
439
+     *
440
+     * @return string
441
+     */
442
+    public function getContentType() {
443
+        $mimeType = $this->info->getMimetype();
444
+
445
+        // PROPFIND needs to return the correct mime type, for consistency with the web UI
446
+        if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
447
+            return $mimeType;
448
+        }
449
+        return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
450
+    }
451
+
452
+    /**
453
+     * @return array|false
454
+     */
455
+    public function getDirectDownload() {
456
+        if (\OCP\App::isEnabled('encryption')) {
457
+            return [];
458
+        }
459
+        /** @var \OCP\Files\Storage $storage */
460
+        list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
461
+        if (is_null($storage)) {
462
+            return [];
463
+        }
464
+
465
+        return $storage->getDirectDownload($internalPath);
466
+    }
467
+
468
+    /**
469
+     * @param resource $data
470
+     * @return null|string
471
+     * @throws Exception
472
+     * @throws BadRequest
473
+     * @throws NotImplemented
474
+     * @throws ServiceUnavailable
475
+     */
476
+    private function createFileChunked($data) {
477
+        list($path, $name) = \Sabre\Uri\split($this->path);
478
+
479
+        $info = \OC_FileChunking::decodeName($name);
480
+        if (empty($info)) {
481
+            throw new NotImplemented('Invalid chunk name');
482
+        }
483
+
484
+        $chunk_handler = new \OC_FileChunking($info);
485
+        $bytesWritten = $chunk_handler->store($info['index'], $data);
486
+
487
+        //detect aborted upload
488
+        if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
489
+            if (isset($_SERVER['CONTENT_LENGTH'])) {
490
+                $expected = (int)$_SERVER['CONTENT_LENGTH'];
491
+                if ($bytesWritten !== $expected) {
492
+                    $chunk_handler->remove($info['index']);
493
+                    throw new BadRequest(
494
+                        'expected filesize ' . $expected . ' got ' . $bytesWritten);
495
+                }
496
+            }
497
+        }
498
+
499
+        if ($chunk_handler->isComplete()) {
500
+            /** @var Storage $storage */
501
+            list($storage,) = $this->fileView->resolvePath($path);
502
+            $needsPartFile = $storage->needsPartFile();
503
+            $partFile = null;
504
+
505
+            $targetPath = $path . '/' . $info['name'];
506
+            /** @var \OC\Files\Storage\Storage $targetStorage */
507
+            list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
508
+
509
+            $exists = $this->fileView->file_exists($targetPath);
510
+
511
+            try {
512
+                $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
513
+
514
+                $this->emitPreHooks($exists, $targetPath);
515
+                $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
516
+                /** @var \OC\Files\Storage\Storage $targetStorage */
517
+                list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
518
+
519
+                if ($needsPartFile) {
520
+                    // we first assembly the target file as a part file
521
+                    $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
522
+                    /** @var \OC\Files\Storage\Storage $targetStorage */
523
+                    list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile);
524
+
525
+
526
+                    $chunk_handler->file_assemble($partStorage, $partInternalPath);
527
+
528
+                    // here is the final atomic rename
529
+                    $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
530
+                    $fileExists = $targetStorage->file_exists($targetInternalPath);
531
+                    if ($renameOkay === false || $fileExists === false) {
532
+                        \OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
533
+                        // only delete if an error occurred and the target file was already created
534
+                        if ($fileExists) {
535
+                            // set to null to avoid double-deletion when handling exception
536
+                            // stray part file
537
+                            $partFile = null;
538
+                            $targetStorage->unlink($targetInternalPath);
539
+                        }
540
+                        $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
541
+                        throw new Exception('Could not rename part file assembled from chunks');
542
+                    }
543
+                } else {
544
+                    // assemble directly into the final file
545
+                    $chunk_handler->file_assemble($targetStorage, $targetInternalPath);
546
+                }
547
+
548
+                // allow sync clients to send the mtime along in a header
549
+                if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
550
+                    $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
551
+                    if ($targetStorage->touch($targetInternalPath, $mtime)) {
552
+                        $this->header('X-OC-MTime: accepted');
553
+                    }
554
+                }
555
+
556
+                // since we skipped the view we need to scan and emit the hooks ourselves
557
+                $targetStorage->getUpdater()->update($targetInternalPath);
558
+
559
+                $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
560
+
561
+                $this->emitPostHooks($exists, $targetPath);
562
+
563
+                // FIXME: should call refreshInfo but can't because $this->path is not the of the final file
564
+                $info = $this->fileView->getFileInfo($targetPath);
565
+
566
+                if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
567
+                    $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
568
+                    $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
569
+                } else if ($info->getChecksum() !== null && $info->getChecksum() !== '') {
570
+                    $this->fileView->putFileInfo($this->path, ['checksum' => '']);
571
+                }
572
+
573
+                $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
574
+
575
+                return $info->getEtag();
576
+            } catch (\Exception $e) {
577
+                if ($partFile !== null) {
578
+                    $targetStorage->unlink($targetInternalPath);
579
+                }
580
+                $this->convertToSabreException($e);
581
+            }
582
+        }
583
+
584
+        return null;
585
+    }
586
+
587
+    /**
588
+     * Convert the given exception to a SabreException instance
589
+     *
590
+     * @param \Exception $e
591
+     *
592
+     * @throws \Sabre\DAV\Exception
593
+     */
594
+    private function convertToSabreException(\Exception $e) {
595
+        if ($e instanceof \Sabre\DAV\Exception) {
596
+            throw $e;
597
+        }
598
+        if ($e instanceof NotPermittedException) {
599
+            // a more general case - due to whatever reason the content could not be written
600
+            throw new Forbidden($e->getMessage(), 0, $e);
601
+        }
602
+        if ($e instanceof ForbiddenException) {
603
+            // the path for the file was forbidden
604
+            throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
605
+        }
606
+        if ($e instanceof EntityTooLargeException) {
607
+            // the file is too big to be stored
608
+            throw new EntityTooLarge($e->getMessage(), 0, $e);
609
+        }
610
+        if ($e instanceof InvalidContentException) {
611
+            // the file content is not permitted
612
+            throw new UnsupportedMediaType($e->getMessage(), 0, $e);
613
+        }
614
+        if ($e instanceof InvalidPathException) {
615
+            // the path for the file was not valid
616
+            // TODO: find proper http status code for this case
617
+            throw new Forbidden($e->getMessage(), 0, $e);
618
+        }
619
+        if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
620
+            // the file is currently being written to by another process
621
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
622
+        }
623
+        if ($e instanceof GenericEncryptionException) {
624
+            // returning 503 will allow retry of the operation at a later point in time
625
+            throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e);
626
+        }
627
+        if ($e instanceof StorageNotAvailableException) {
628
+            throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e);
629
+        }
630
+        if ($e instanceof NotFoundException) {
631
+            throw new NotFound('File not found: ' . $e->getMessage(), 0, $e);
632
+        }
633
+
634
+        throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
635
+    }
636
+
637
+    /**
638
+     * Get the checksum for this file
639
+     *
640
+     * @return string
641
+     */
642
+    public function getChecksum() {
643
+        return $this->info->getChecksum();
644
+    }
645
+
646
+    protected function header($string) {
647
+        \header($string);
648
+    }
649 649
 }
Please login to merge, or discard this patch.