Completed
Push — master ( 81153d...2a6722 )
by Thomas
52s
created

File::get()   C

Complexity

Conditions 8
Paths 21

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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