Completed
Pull Request — master (#28003)
by Piotr
11:34
created

File::handleMetadataUpdate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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