Completed
Push — master ( 76ba1c...91e148 )
by Thomas
23:00 queued 09:19
created

File::convertToSabreException()   C

Complexity

Conditions 11
Paths 10

Size

Total Lines 39
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 20
nc 10
nop 1
dl 0
loc 39
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @author Bart Visscher <[email protected]>
4
 * @author Björn Schießle <[email protected]>
5
 * @author Jakob Sack <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author Jörn Friedrich Dreyer <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Owen Winkler <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Semih Serhat Karakaya <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 *
17
 * @copyright Copyright (c) 2017, ownCloud GmbH
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
34
namespace OCA\DAV\Connector\Sabre;
35
36
use OC\Files\Filesystem;
37
use OC\Files\Storage\Storage;
38
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
39
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
40
use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException;
41
use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
42
use OCP\Encryption\Exceptions\GenericEncryptionException;
43
use OCP\Files\EntityTooLargeException;
44
use OCP\Files\ForbiddenException;
45
use OCP\Files\InvalidContentException;
46
use OCP\Files\InvalidPathException;
47
use OCP\Files\LockNotAcquiredException;
48
use OCP\Files\NotPermittedException;
49
use OCP\Files\StorageNotAvailableException;
50
use OCP\Lock\ILockingProvider;
51
use OCP\Lock\LockedException;
52
use Sabre\DAV\Exception;
53
use Sabre\DAV\Exception\BadRequest;
54
use Sabre\DAV\Exception\Forbidden;
55
use Sabre\DAV\Exception\NotImplemented;
56
use Sabre\DAV\Exception\ServiceUnavailable;
57
use Sabre\DAV\IFile;
58
use Sabre\DAV\Exception\NotFound;
59
use OC\AppFramework\Http\Request;
60
61
class File extends Node implements IFile {
62
63
	protected $request;
64
	
65
	/**
66
	 * Sets up the node, expects a full path name
67
	 *
68
	 * @param \OC\Files\View $view
69
	 * @param \OCP\Files\FileInfo $info
70
	 * @param \OCP\Share\IManager $shareManager
71
	 */
72
	public function __construct($view, $info, $shareManager = null, Request $request = null) {
73
		if (isset($request)) {
74
			$this->request = $request;
75
		} else {
76
			$this->request = \OC::$server->getRequest();
77
		}
78
		parent::__construct($view, $info, $shareManager);
79
	}
80
	
81
	/**
82
	 * Updates the data
83
	 *
84
	 * The data argument is a readable stream resource.
85
	 *
86
	 * After a successful put operation, you may choose to return an ETag. The
87
	 * etag must always be surrounded by double-quotes. These quotes must
88
	 * appear in the actual string you're returning.
89
	 *
90
	 * Clients may use the ETag from a PUT request to later on make sure that
91
	 * when they update the file, the contents haven't changed in the mean
92
	 * time.
93
	 *
94
	 * If you don't plan to store the file byte-by-byte, and you return a
95
	 * different object on a subsequent GET you are strongly recommended to not
96
	 * return an ETag, and just return null.
97
	 *
98
	 * @param resource|string $data
99
	 *
100
	 * @throws Forbidden
101
	 * @throws UnsupportedMediaType
102
	 * @throws BadRequest
103
	 * @throws Exception
104
	 * @throws EntityTooLarge
105
	 * @throws ServiceUnavailable
106
	 * @throws FileLocked
107
	 * @return string|null
108
	 */
109
	public function put($data) {
110
		try {
111
			$exists = $this->fileView->file_exists($this->path);
112
			if ($this->info && $exists && !$this->info->isUpdateable()) {
113
				throw new Forbidden();
114
			}
115
		} catch (StorageNotAvailableException $e) {
116
			throw new ServiceUnavailable("File is not updatable: " . $e->getMessage());
117
		}
118
119
		// verify path of the target
120
		$this->verifyPath();
121
122
		// chunked handling
123
		if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
124
			try {
125
				return $this->createFileChunked($data);
0 ignored issues
show
Bug introduced by
It seems like $data defined by parameter $data on line 109 can also be of type string; however, OCA\DAV\Connector\Sabre\File::createFileChunked() does only seem to accept resource, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
126
			} catch (\Exception $e) {
127
				$this->convertToSabreException($e);
128
			}
129
		}
130
131
		list($partStorage) = $this->fileView->resolvePath($this->path);
132
		$needsPartFile = $this->needsPartFile($partStorage) && (strlen($this->path) > 1);
133
134
		if ($needsPartFile) {
135
			// mark file as partial while uploading (ignored by the scanner)
136
			$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
137
		} else {
138
			// upload file directly as the final path
139
			$partFilePath = $this->path;
140
		}
141
142
		// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
143
		/** @var \OC\Files\Storage\Storage $partStorage */
144
		list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
145
		/** @var \OC\Files\Storage\Storage $storage */
146
		list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
147
		try {
148
			$target = $partStorage->fopen($internalPartPath, 'wb');
149
			if ($target === false) {
150
				\OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::fopen() failed', \OCP\Util::ERROR);
151
				// because we have no clue about the cause we can only throw back a 500/Internal Server Error
152
				throw new Exception('Could not write file contents');
153
			}
154
			list($count, $result) = \OC_Helper::streamCopy($data, $target);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type string; however, OC_Helper::streamCopy() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
155
			fclose($target);
156
157
			if (!self::isChecksumValid($partStorage, $internalPartPath)) {
158
				throw new BadRequest('The computed checksum does not match the one received from the client.');
159
			}
160
161
			if ($result === false) {
162
				$expected = -1;
163
				if (isset($_SERVER['CONTENT_LENGTH'])) {
164
					$expected = $_SERVER['CONTENT_LENGTH'];
165
				}
166
				throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )');
167
			}
168
169
			// if content length is sent by client:
170
			// double check if the file was fully received
171
			// compare expected and actual size
172
			if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
173
				$expected = $_SERVER['CONTENT_LENGTH'];
174
				if ($count != $expected) {
175
					throw new BadRequest('expected filesize ' . $expected . ' got ' . $count);
176
				}
177
			}
178
179
		} catch (\Exception $e) {
180
			if ($needsPartFile) {
181
				$partStorage->unlink($internalPartPath);
182
			}
183
			$this->convertToSabreException($e);
184
		}
185
186
		try {
187
			$view = \OC\Files\Filesystem::getView();
188
			if ($view) {
189
				$run = $this->emitPreHooks($exists);
190
			} else {
191
				$run = true;
192
			}
193
194
			try {
195
				$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
196
			} catch (LockedException $e) {
197
				if ($needsPartFile) {
198
					$partStorage->unlink($internalPartPath);
199
				}
200
				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
201
			}
202
203
			if ($needsPartFile) {
204
				// rename to correct path
205
				try {
206
					if ($run) {
207
						$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
208
						$fileExists = $storage->file_exists($internalPath);
209
					}
210
					if (!$run || $renameOkay === false || $fileExists === false) {
0 ignored issues
show
Bug introduced by
The variable $renameOkay does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $fileExists does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
211
						\OCP\Util::writeLog('webdav', 'renaming part file to final file failed', \OCP\Util::ERROR);
212
						throw new Exception('Could not rename part file to final file');
213
					}
214
				} catch (ForbiddenException $ex) {
215
					throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
216
				} catch (\Exception $e) {
217
					$partStorage->unlink($internalPartPath);
218
					$this->convertToSabreException($e);
219
				}
220
			}
221
222
			// since we skipped the view we need to scan and emit the hooks ourselves
223
			$storage->getUpdater()->update($internalPath);
224
225
			try {
226
				$this->changeLock(ILockingProvider::LOCK_SHARED);
227
			} catch (LockedException $e) {
228
				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
229
			}
230
231
			// allow sync clients to send the mtime along in a header
232
			if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
233
				$mtime = $this->sanitizeMtime($this->request->server ['HTTP_X_OC_MTIME']);
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
234
				if ($this->fileView->touch($this->path, $mtime)) {
235
					header('X-OC-MTime: accepted');
236
				}
237
			}
238
			
239
			if ($view) {
240
				$this->emitPostHooks($exists);
241
			}
242
			
243
			$this->refreshInfo();
244
		} catch (StorageNotAvailableException $e) {
245
			throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage());
246
		}
247
248
		return '"' . $this->info->getEtag() . '"';
249
	}
250
251
	private function getPartFileBasePath($path) {
252
		$partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
253
		if ($partFileInStorage) {
254
			return $path;
255
		} else {
256
			return md5($path); // will place it in the root of the view with a unique name
257
		}
258
	}
259
260
	/**
261
	 * @param string $path
262
	 */
263
	private function emitPreHooks($exists, $path = null) {
264
		if (is_null($path)) {
265
			$path = $this->path;
266
		}
267
		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
268
		$run = true;
269
270 View Code Duplication
		if (!$exists) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
271
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [
272
				\OC\Files\Filesystem::signal_param_path => $hookPath,
273
				\OC\Files\Filesystem::signal_param_run => &$run,
274
			]);
275
		} else {
276
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [
277
				\OC\Files\Filesystem::signal_param_path => $hookPath,
278
				\OC\Files\Filesystem::signal_param_run => &$run,
279
			]);
280
		}
281
		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [
282
			\OC\Files\Filesystem::signal_param_path => $hookPath,
283
			\OC\Files\Filesystem::signal_param_run => &$run,
284
		]);
285
		return $run;
286
	}
287
288
	/**
289
	 * @param string $path
290
	 */
291
	private function emitPostHooks($exists, $path = null) {
292
		if (is_null($path)) {
293
			$path = $this->path;
294
		}
295
		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
296 View Code Duplication
		if (!$exists) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
297
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [
298
				\OC\Files\Filesystem::signal_param_path => $hookPath
299
			]);
300
		} else {
301
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [
302
				\OC\Files\Filesystem::signal_param_path => $hookPath
303
			]);
304
		}
305
		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [
306
			\OC\Files\Filesystem::signal_param_path => $hookPath
307
		]);
308
	}
309
310
	/**
311
	 * Returns the data
312
	 *
313
	 * @return resource
314
	 * @throws Forbidden
315
	 * @throws ServiceUnavailable
316
	 */
317
	public function get() {
318
		//throw exception if encryption is disabled but files are still encrypted
319
		try {
320
			$viewPath = ltrim($this->path, '/');
321
			if (!$this->info->isReadable() || !$this->fileView->file_exists($viewPath)) {
322
				// do a if the file did not exist
323
				throw new NotFound();
324
			}
325
			$res = $this->fileView->fopen($viewPath, 'rb');
326
			if ($res === false) {
327
				throw new ServiceUnavailable("Could not open file");
328
			}
329
			return $res;
330
		} catch (GenericEncryptionException $e) {
331
			// returning 503 will allow retry of the operation at a later point in time
332
			throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage());
333
		} catch (StorageNotAvailableException $e) {
334
			throw new ServiceUnavailable("Failed to open file: " . $e->getMessage());
335
		} catch (ForbiddenException $ex) {
336
			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
337
		} catch (LockedException $e) {
338
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
339
		}
340
	}
341
342
	/**
343
	 * Delete the current file
344
	 *
345
	 * @throws Forbidden
346
	 * @throws ServiceUnavailable
347
	 */
348
	public function delete() {
349
		if (!$this->info->isDeletable()) {
350
			throw new Forbidden();
351
		}
352
353
		try {
354
			if (!$this->fileView->unlink($this->path)) {
355
				// assume it wasn't possible to delete due to permissions
356
				throw new Forbidden();
357
			}
358
		} catch (StorageNotAvailableException $e) {
359
			throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage());
360
		} catch (ForbiddenException $ex) {
361
			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
362
		} catch (LockedException $e) {
363
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
364
		}
365
	}
366
367
	/**
368
	 * Returns the mime-type for a file
369
	 *
370
	 * If null is returned, we'll assume application/octet-stream
371
	 *
372
	 * @return string
373
	 */
374
	public function getContentType() {
375
		$mimeType = $this->info->getMimetype();
376
377
		// PROPFIND needs to return the correct mime type, for consistency with the web UI
378
		if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
379
			return $mimeType;
380
		}
381
		return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
382
	}
383
384
	/**
385
	 * @return array|false
386
	 */
387
	public function getDirectDownload() {
388
		if (\OCP\App::isEnabled('encryption')) {
389
			return [];
390
		}
391
		/** @var \OCP\Files\Storage $storage */
392
		list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
393
		if (is_null($storage)) {
394
			return [];
395
		}
396
397
		return $storage->getDirectDownload($internalPath);
398
	}
399
400
	/**
401
	 * @param resource $data
402
	 * @return null|string
403
	 * @throws Exception
404
	 * @throws BadRequest
405
	 * @throws NotImplemented
406
	 * @throws ServiceUnavailable
407
	 */
408
	private function createFileChunked($data) {
409
		list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($this->path);
410
411
		$info = \OC_FileChunking::decodeName($name);
412
		if (empty($info)) {
413
			throw new NotImplemented('Invalid chunk name');
414
		}
415
416
		$chunk_handler = new \OC_FileChunking($info);
417
		$bytesWritten = $chunk_handler->store($info['index'], $data);
418
419
		//detect aborted upload
420
		if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
421
			if (isset($_SERVER['CONTENT_LENGTH'])) {
422
				$expected = $_SERVER['CONTENT_LENGTH'];
423
				if ($bytesWritten != $expected) {
424
					$chunk_handler->remove($info['index']);
425
					throw new BadRequest(
426
						'expected filesize ' . $expected . ' got ' . $bytesWritten);
427
				}
428
			}
429
		}
430
431
		if ($chunk_handler->isComplete()) {
432
			list($storage,) = $this->fileView->resolvePath($path);
433
			$needsPartFile = $this->needsPartFile($storage);
434
			$partFile = null;
435
436
			$targetPath = $path . '/' . $info['name'];
437
			/** @var \OC\Files\Storage\Storage $targetStorage */
438
			list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
439
440
			$exists = $this->fileView->file_exists($targetPath);
441
442
			try {
443
				$this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
444
445
				$this->emitPreHooks($exists, $targetPath);
446
				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
447
				/** @var \OC\Files\Storage\Storage $targetStorage */
448
				list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
449
450
				if ($needsPartFile) {
451
					// we first assembly the target file as a part file
452
					$partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
453
					/** @var \OC\Files\Storage\Storage $targetStorage */
454
					list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile);
455
456
457
					$chunk_handler->file_assemble($partStorage, $partInternalPath);
458
459
					if (!self::isChecksumValid($partStorage, $partInternalPath)) {
460
						throw new BadRequest('The computed checksum does not match the one received from the client.');
461
					}
462
463
					// here is the final atomic rename
464
					$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
465
					$fileExists = $targetStorage->file_exists($targetInternalPath);
466
					if ($renameOkay === false || $fileExists === false) {
467
						\OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::rename() failed', \OCP\Util::ERROR);
468
						// only delete if an error occurred and the target file was already created
469
						if ($fileExists) {
470
							// set to null to avoid double-deletion when handling exception
471
							// stray part file
472
							$partFile = null;
473
							$targetStorage->unlink($targetInternalPath);
474
						}
475
						$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
476
						throw new Exception('Could not rename part file assembled from chunks');
477
					}
478
				} else {
479
					// assemble directly into the final file
480
					$chunk_handler->file_assemble($targetStorage, $targetInternalPath);
481
				}
482
483
				// allow sync clients to send the mtime along in a header
484
				if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
485
					$mtime = $this->sanitizeMtime(
486
						$this->request->server ['HTTP_X_OC_MTIME']
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
487
					);
488
					if ($targetStorage->touch($targetInternalPath, $mtime)) {
489
						header('X-OC-MTime: accepted');
490
					}
491
				}
492
493
				// since we skipped the view we need to scan and emit the hooks ourselves
494
				$targetStorage->getUpdater()->update($targetInternalPath);
495
496
				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
497
498
				$this->emitPostHooks($exists, $targetPath);
499
500
				// FIXME: should call refreshInfo but can't because $this->path is not the of the final file
501
				$info = $this->fileView->getFileInfo($targetPath);
502
503
504
				if (isset($partStorage) && isset($partInternalPath)) {
505
					$checksums = $partStorage->getMetaData($partInternalPath)['checksum'];
506
				} else {
507
					$checksums = $targetStorage->getMetaData($targetInternalPath)['checksum'];
508
				}
509
510
				$this->fileView->putFileInfo(
511
					$targetPath,
512
					['checksum' => $checksums]
513
				);
514
515
				$this->refreshInfo();
516
517
				$this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
518
519
				return $info->getEtag();
520
			} catch (\Exception $e) {
521
				if ($partFile !== null) {
522
					$targetStorage->unlink($targetInternalPath);
523
				}
524
				$this->convertToSabreException($e);
525
			}
526
		}
527
528
		return null;
529
	}
530
531
	/**
532
	 * will return true if checksum was not provided in request
533
	 *
534
	 * @param Storage $storage
535
	 * @param $path
536
	 * @return bool
537
	 */
538
	private static function isChecksumValid(Storage $storage, $path) {
539
		$meta = $storage->getMetaData($path);
540
		$request = \OC::$server->getRequest();
541
542
		if (!isset($request->server['HTTP_OC_CHECKSUM']) || !isset($meta['checksum'])) {
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
543
			// No comparison possible, skip the check
544
			return true;
545
		}
546
547
		$expectedChecksum = trim($request->server['HTTP_OC_CHECKSUM']);
0 ignored issues
show
Bug introduced by
Accessing server on the interface OCP\IRequest suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
548
		$computedChecksums = $meta['checksum'];
549
550
		return strpos($computedChecksums, $expectedChecksum) !== false;
551
552
	}
553
554
	/**
555
	 * Returns whether a part file is needed for the given storage
556
	 * or whether the file can be assembled/uploaded directly on the
557
	 * target storage.
558
	 *
559
	 * @param \OCP\Files\Storage $storage
560
	 * @return bool true if the storage needs part file handling
561
	 */
562
	private function needsPartFile($storage) {
563
		// TODO: in the future use ChunkHandler provided by storage
564
		// and/or add method on Storage called "needsPartFile()"
565
		return !$storage->instanceOfStorage('OCA\Files_Sharing\External\Storage') &&
566
			!$storage->instanceOfStorage('OC\Files\Storage\OwnCloud') &&
567
			!$storage->instanceOfStorage('OC\Files\ObjectStore\ObjectStoreStorage');
568
	}
569
570
	/**
571
	 * Convert the given exception to a SabreException instance
572
	 *
573
	 * @param \Exception $e
574
	 *
575
	 * @throws \Sabre\DAV\Exception
576
	 */
577
	private function convertToSabreException(\Exception $e) {
578
		if ($e instanceof \Sabre\DAV\Exception) {
579
			throw $e;
580
		}
581
		if ($e instanceof NotPermittedException) {
582
			// a more general case - due to whatever reason the content could not be written
583
			throw new Forbidden($e->getMessage(), 0, $e);
584
		}
585
		if ($e instanceof ForbiddenException) {
586
			// the path for the file was forbidden
587
			throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
588
		}
589
		if ($e instanceof EntityTooLargeException) {
590
			// the file is too big to be stored
591
			throw new EntityTooLarge($e->getMessage(), 0, $e);
592
		}
593
		if ($e instanceof InvalidContentException) {
594
			// the file content is not permitted
595
			throw new UnsupportedMediaType($e->getMessage(), 0, $e);
596
		}
597
		if ($e instanceof InvalidPathException) {
598
			// the path for the file was not valid
599
			// TODO: find proper http status code for this case
600
			throw new Forbidden($e->getMessage(), 0, $e);
601
		}
602
		if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
603
			// the file is currently being written to by another process
604
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
605
		}
606
		if ($e instanceof GenericEncryptionException) {
607
			// returning 503 will allow retry of the operation at a later point in time
608
			throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e);
609
		}
610
		if ($e instanceof StorageNotAvailableException) {
611
			throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e);
612
		}
613
614
		throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
615
	}
616
617
	/**
618
	 * Set $algo to get a specific checksum, leave null to get all checksums
619
	 * (space seperated)
620
	 * @param null $algo
621
	 * @return string
622
	 */
623
	public function getChecksum($algo = null) {
624
		$allChecksums = $this->info->getChecksum();
625
626
		if (!$algo) {
627
			return $allChecksums;
628
		}
629
630
		$checksums = explode(' ', $allChecksums);
631
		$algoPrefix = strtoupper($algo) . ':';
632
633
		foreach ($checksums as $checksum) {
634
			// starts with $algoPrefix
635
			if (substr($checksum, 0, strlen($algoPrefix)) === $algoPrefix) {
636
				return $checksum;
637
			}
638
		}
639
640
		return '';
641
	}
642
}
643