Completed
Push — stable8.2 ( 3b3780...cb9d0b )
by Lukas
29s
created

Encryption::copyBetweenStorage()   D

Complexity

Conditions 18
Paths 51

Size

Total Lines 82
Code Lines 51

Duplication

Lines 7
Ratio 8.54 %

Code Coverage

Tests 33
CRAP Score 32.2396

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 7
loc 82
ccs 33
cts 51
cp 0.6471
rs 4.9297
cc 18
eloc 51
nc 51
nop 5
crap 32.2396

How to fix   Long Method    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 Björn Schießle <[email protected]>
4
 * @author Joas Schilling <[email protected]>
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Thomas Müller <[email protected]>
7
 *
8
 * @copyright Copyright (c) 2015, ownCloud, Inc.
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
25
namespace OC\Files\Storage\Wrapper;
26
27
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
28
use OC\Encryption\Update;
29
use OC\Encryption\Util;
30
use OC\Files\Filesystem;
31
use OC\Files\Mount\Manager;
32
use OC\Files\Storage\LocalTempFileTrait;
33
use OC\Memcache\ArrayCache;
34
use OCP\Encryption\Exceptions\GenericEncryptionException;
35
use OCP\Encryption\IFile;
36
use OCP\Encryption\IManager;
37
use OCP\Encryption\Keys\IStorage;
38
use OCP\Files\Mount\IMountPoint;
39
use OCP\Files\Storage;
40
use OCP\ILogger;
41
42
class Encryption extends Wrapper {
43
44
	use LocalTempFileTrait;
45
46
	/** @var string */
47
	private $mountPoint;
48
49
	/** @var \OC\Encryption\Util */
50
	private $util;
51
52
	/** @var \OCP\Encryption\IManager */
53
	private $encryptionManager;
54
55
	/** @var \OCP\ILogger */
56
	private $logger;
57
58
	/** @var string */
59
	private $uid;
60
61
	/** @var array */
62
	protected $unencryptedSize;
63
64
	/** @var \OCP\Encryption\IFile */
65
	private $fileHelper;
66
67
	/** @var IMountPoint */
68
	private $mount;
69
70
	/** @var IStorage */
71
	private $keyStorage;
72
73
	/** @var Update */
74
	private $update;
75
76
	/** @var Manager */
77
	private $mountManager;
78
79
	/** @var array remember for which path we execute the repair step to avoid recursions */
80
	private $fixUnencryptedSizeOf = array();
81
82
	/** @var  ArrayCache */
83
	private $arrayCache;
84
85
	/**
86
	 * @param array $parameters
87
	 * @param IManager $encryptionManager
88
	 * @param Util $util
89
	 * @param ILogger $logger
90
	 * @param IFile $fileHelper
91
	 * @param string $uid
92 142
	 * @param IStorage $keyStorage
93
	 * @param Update $update
94
	 * @param Manager $mountManager
95
	 * @param ArrayCache $arrayCache
96
	 */
97
	public function __construct(
98
			$parameters,
99
			IManager $encryptionManager = null,
100
			Util $util = null,
101
			ILogger $logger = null,
102
			IFile $fileHelper = null,
103
			$uid = null,
104 142
			IStorage $keyStorage = null,
105 142
			Update $update = null,
106 142
			Manager $mountManager = null,
107 142
			ArrayCache $arrayCache = null
108 142
		) {
109 142
		
110 142
		$this->mountPoint = $parameters['mountPoint'];
111 142
		$this->mount = $parameters['mount'];
112 142
		$this->encryptionManager = $encryptionManager;
113 142
		$this->util = $util;
114 142
		$this->logger = $logger;
115 142
		$this->uid = $uid;
116 142
		$this->fileHelper = $fileHelper;
117
		$this->keyStorage = $keyStorage;
118
		$this->unencryptedSize = array();
119
		$this->update = $update;
120
		$this->mountManager = $mountManager;
121
		$this->arrayCache = $arrayCache;
122
		parent::__construct($parameters);
123
	}
124
125 13
	/**
126 13
	 * see http://php.net/manual/en/function.filesize.php
127
	 * The result for filesize when called on a folder is required to be 0
128 13
	 *
129 13
	 * @param string $path
130 5
	 * @return int
131
	 */
132 5
	public function filesize($path) {
133 5
		$fullPath = $this->getFullPath($path);
134 5
135
		$info = $this->getCache()->get($path);
136 5
		if (isset($this->unencryptedSize[$fullPath])) {
137
			$size = $this->unencryptedSize[$fullPath];
138
			// update file cache
139 13
			$info['encrypted'] = true;
140 2
			$info['size'] = $size;
141
			$this->getCache()->put($path, $info);
0 ignored issues
show
Security Bug introduced by
It seems like $info defined by $this->getCache()->get($path) on line 135 can also be of type false; however, OC\Files\Cache\Cache::put() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
142
143 12
			return $size;
144
		}
145
146
		if (isset($info['fileid']) && $info['encrypted']) {
147
			return $this->verifyUnencryptedSize($path, $info['size']);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->verifyUnencrypted...($path, $info['size']); of type integer|double adds the type double to the return on line 147 which is incompatible with the return type declared by the interface OCP\Files\Storage::filesize of type integer|false.
Loading history...
148
		}
149
150 9
		return $this->storage->filesize($path);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->storage->filesize($path); of type integer|false adds false to the return on line 150 which is incompatible with the return type documented by OC\Files\Storage\Wrapper\Encryption::filesize of type integer. It seems like you forgot to handle an error condition.
Loading history...
151 9
	}
152 9
153 1
	/**
154
	 * @param string $path
155 8
	 * @return array
156
	 */
157 8
	public function getMetaData($path) {
158 6
		$data = $this->storage->getMetaData($path);
159 6
		if (is_null($data)) {
160 6
			return null;
161 7
		}
162 7
		$fullPath = $this->getFullPath($path);
163 1
164 1
		if (isset($this->unencryptedSize[$fullPath])) {
165 1
			$data['encrypted'] = true;
166
			$data['size'] = $this->unencryptedSize[$fullPath];
167
		} else {
168 8
			$info = $this->getCache()->get($path);
169
			if (isset($info['fileid']) && $info['encrypted']) {
170
				$data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
171
				$data['encrypted'] = true;
172
			}
173
		}
174
175
		return $data;
176 57
	}
177
	/**
178 57
	 * see http://php.net/manual/en/function.file_get_contents.php
179
	 *
180 57
	 * @param string $path
181 57
	 * @return string
182 57
	 */
183
	public function file_get_contents($path) {
184
185 57
		$encryptionModule = $this->getEncryptionModule($path);
186 57
187 57
		if ($encryptionModule) {
188
			$handle = $this->fopen($path, "r");
189 4
			if (!$handle) {
190
				return false;
191
			}
192
			$data = stream_get_contents($handle);
193
			fclose($handle);
194
			return $data;
195
		}
196
		return $this->storage->file_get_contents($path);
197
	}
198
199 66
	/**
200
	 * see http://php.net/manual/en/function.file_put_contents.php
201 66
	 *
202 66
	 * @param string $path
203 66
	 * @param string $data
204 66
	 * @return bool
205
	 */
206
	public function file_put_contents($path, $data) {
207
		// file put content will always be translated to a stream write
208
		$handle = $this->fopen($path, 'w');
209
		$written = fwrite($handle, $data);
210
		fclose($handle);
211
		return $written;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $written; (integer) is incompatible with the return type declared by the interface OCP\Files\Storage::file_put_contents of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
212
	}
213 27
214 27
	/**
215 27
	 * see http://php.net/manual/en/function.unlink.php
216
	 *
217
	 * @param string $path
218
	 * @return bool
219 27
	 */
220 27
	public function unlink($path) {
221 24
		$fullPath = $this->getFullPath($path);
222 24
		if ($this->util->isExcluded($fullPath)) {
223
			return $this->storage->unlink($path);
224 27
		}
225
226
		$encryptionModule = $this->getEncryptionModule($path);
227
		if ($encryptionModule) {
228
			$this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
229
		}
230
231
		return $this->storage->unlink($path);
232
	}
233
234 30
	/**
235
	 * see http://php.net/manual/en/function.rename.php
236 30
	 *
237
	 * @param string $path1
238 30
	 * @param string $path2
239
	 * @return bool
240
	 */
241 30
	public function rename($path1, $path2) {
242 30
243 9
		$result = $this->storage->rename($path1, $path2);
244 9
245 9
		if ($result &&
246 9
			// versions always use the keys from the original file, so we can skip
247 5
			// this step for versions
248 5
			$this->isVersion($path2) === false &&
249 9
			$this->encryptionManager->isEnabled()) {
250 9
			$source = $this->getFullPath($path1);
251 9
			if (!$this->util->isExcluded($source)) {
252
				$target = $this->getFullPath($path2);
253 30
				if (isset($this->unencryptedSize[$source])) {
254
					$this->unencryptedSize[$target] = $this->unencryptedSize[$source];
255
				}
256
				$this->keyStorage->renameKeys($source, $target);
257
			}
258
		}
259
260
		return $result;
261
	}
262 16
263 16
	/**
264 16
	 * see http://php.net/manual/en/function.rmdir.php
265 16
	 *
266 16
	 * @param string $path
267 2
	 * @return bool
268 16
	 */
269 1
	public function rmdir($path) {
270 1
		$result = $this->storage->rmdir($path);
271
		$fullPath = $this->getFullPath($path);
272 16
		if ($result &&
273
			$this->util->isExcluded($fullPath) === false &&
274
			$this->encryptionManager->isEnabled()
275
		) {
276
			$this->keyStorage->deleteAllFileKeys($fullPath);
277
		}
278
279
		return $result;
280
	}
281 12
282
	/**
283 12
	 * check if a file can be read
284
	 *
285 12
	 * @param string $path
286
	 * @return bool
287 12
	 */
288 12
	public function isReadable($path) {
289 1
290 12
		$isReadable = true;
291 1
292 1
		$metaData = $this->getMetaData($path);
293 1
		if (
294 1
			!$this->is_dir($path) &&
295
			isset($metaData['encrypted']) &&
296 12
			$metaData['encrypted'] === true
297
		) {
298
			$fullPath = $this->getFullPath($path);
299
			$module = $this->getEncryptionModule($path);
300
			$isReadable = $module->isReadable($fullPath, $this->uid);
301
		}
302
303
		return $this->storage->isReadable($path) && $isReadable;
304
	}
305
306 21
	/**
307
	 * see http://php.net/manual/en/function.copy.php
308 21
	 *
309
	 * @param string $path1
310 21
	 * @param string $path2
311
	 * @return bool
312
	 */
313
	public function copy($path1, $path2) {
314
315
		$source = $this->getFullPath($path1);
316 21
317 21
		if ($this->util->isExcluded($source)) {
318
			return $this->storage->copy($path1, $path2);
319 21
		}
320
321
		// need to stream copy file by file in case we copy between a encrypted
322
		// and a unencrypted storage
323
		$this->unlink($path2);
324
		$result = $this->copyFromStorage($this, $path1, $path2);
325
326
		return $result;
327
	}
328
329
	/**
330
	 * see http://php.net/manual/en/function.fopen.php
331 71
	 *
332
	 * @param string $path
333 71
	 * @param string $mode
334 71
	 * @return resource
335 71
	 * @throws GenericEncryptionException
336 71
	 * @throws ModuleDoesNotExistsException
337 71
	 */
338 71
	public function fopen($path, $mode) {
339
340 71
		// check if the file is stored in the array cache, this means that we
341
		// copy a file over to the versions folder, in this case we don't want to
342 5
		// decrypt it
343 5
		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
344 5
			$this->arrayCache->remove('encryption_copy_version_' . $path);
345 5
			return $this->storage->fopen($path, $mode);
346 5
		}
347
348
		$encryptionEnabled = $this->encryptionManager->isEnabled();
349
		$shouldEncrypt = false;
350 5
		$encryptionModule = null;
351 5
		$header = $this->getHeader($path);
352 5
		$fullPath = $this->getFullPath($path);
353 5
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
354
355 5
		if ($this->util->isExcluded($fullPath) === false) {
356 5
357 5
			$size = $unencryptedSize = 0;
358 5
			$realFile = $this->util->stripPartialFileExtension($path);
359 2
			$targetExists = $this->file_exists($realFile) || $this->file_exists($path);
360
			$targetIsEncrypted = false;
361 5
			if ($targetExists) {
362
				// in case the file exists we require the explicit module as
363
				// specified in the file header - otherwise we need to fail hard to
364
				// prevent data loss on client side
365
				if (!empty($encryptionModuleId)) {
366
					$targetIsEncrypted = true;
367 5
					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
368 5
				}
369 5
370 5
				if ($this->file_exists($path)) {
371
					$size = $this->storage->filesize($path);
372 5
					$unencryptedSize = $this->filesize($path);
373
				} else {
374
					$size = $unencryptedSize = 0;
375 5
				}
376
			}
377 5
378 5
			try {
379 5
380 5
				if (
381 5
					$mode === 'w'
382
					|| $mode === 'w+'
383
					|| $mode === 'wb'
384 5
					|| $mode === 'wb+'
385 5
				) {
386 5
					// don't overwrite encrypted files if encyption is not enabled
387 5
					if ($targetIsEncrypted && $encryptionEnabled === false) {
388
						throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled');
389
					}
390
					if ($encryptionEnabled) {
391
						// if $encryptionModuleId is empty, the default module will be used
392
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
393
						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
394
					}
395
				} else {
396 5
					$info = $this->getCache()->get($path);
397
					// only get encryption module if we found one in the header
398
					// or if file should be encrypted according to the file cache
399
					if (!empty($encryptionModuleId)) {
400
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
401
						$shouldEncrypt = true;
402 5
					} else if (empty($encryptionModuleId) && $info['encrypted'] === true) {
403
						// we come from a old installation. No header and/or no module defined
404
						// but the file is encrypted. In this case we need to use the
405
						// OC_DEFAULT_MODULE to read the file
406
						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
407
						$shouldEncrypt = true;
408 5
						$targetIsEncrypted = true;
409 5
					}
410 5
				}
411 5
			} catch (ModuleDoesNotExistsException $e) {
412 5
				$this->logger->warning('Encryption module "' . $encryptionModuleId .
413 5
					'" not found, file will be stored unencrypted (' . $e->getMessage() . ')');
414 5
			}
415
416
			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
417 3
			if (!$encryptionEnabled || !$this->mount->getOption('encrypt', true)) {
418
				if (!$targetExists || !$targetIsEncrypted) {
419 71
					$shouldEncrypt = false;
420
				}
421
			}
422
423
			if ($shouldEncrypt === true && $encryptionModule !== null) {
424
				$headerSize = $this->getHeaderSize($path);
425
				$source = $this->storage->fopen($path, $mode);
426
				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
427
					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
428
					$size, $unencryptedSize, $headerSize);
429
				return $handle;
430
			}
431
432 5
		}
433
434 5
		return $this->storage->fopen($path, $mode);
435 5
	}
436
437 5
438 3
	/**
439 5
	 * perform some plausibility checks if the the unencrypted size is correct.
440
	 * If not, we calculate the correct unencrypted size and return it
441
	 *
442 3
	 * @param string $path internal path relative to the storage root
443 3
	 * @param int $unencryptedSize size of the unencrypted file
444
	 *
445 3
	 * @return int unencrypted size
446 3
	 */
447 1
	protected function verifyUnencryptedSize($path, $unencryptedSize) {
448 1
449
		$size = $this->storage->filesize($path);
450 3
		$result = $unencryptedSize;
451 3
452 3
		if ($unencryptedSize < 0 ||
453
			($size > 0 && $unencryptedSize === $size)
454 5
		) {
455
			// check if we already calculate the unencrypted size for the
456
			// given path to avoid recursions
457
			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
458
				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
459
				try {
460
					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
0 ignored issues
show
Security Bug introduced by
It seems like $size defined by $this->storage->filesize($path) on line 449 can also be of type false; however, OC\Files\Storage\Wrapper...n::fixUnencryptedSize() does only seem to accept integer, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
461
				} catch (\Exception $e) {
462
					$this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
463
					$this->logger->logException($e);
464
				}
465
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
466
			}
467
		}
468
469
		return $result;
470
	}
471
472
	/**
473
	 * calculate the unencrypted size
474
	 *
475
	 * @param string $path internal path relative to the storage root
476
	 * @param int $size size of the physical file
477
	 * @param int $unencryptedSize size of the unencrypted file
478
	 *
479
	 * @return int calculated unencrypted size
480
	 */
481
	protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
482
483
		$headerSize = $this->getHeaderSize($path);
484
		$header = $this->getHeader($path);
485
		$encryptionModule = $this->getEncryptionModule($path);
486
487
		$stream = $this->storage->fopen($path, 'r');
488
489
		// if we couldn't open the file we return the old unencrypted size
490
		if (!is_resource($stream)) {
491
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
492
			return $unencryptedSize;
493
		}
494
495
		$newUnencryptedSize = 0;
496
		$size -= $headerSize;
497
		$blockSize = $this->util->getBlockSize();
498
499
		// if a header exists we skip it
500
		if ($headerSize > 0) {
501
			fread($stream, $headerSize);
502
		}
503
504
		// fast path, else the calculation for $lastChunkNr is bogus
505
		if ($size === 0) {
506
			return 0;
507
		}
508
509
		$signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
510
		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
0 ignored issues
show
Unused Code introduced by
The call to IEncryptionModule::getUnencryptedBlockSize() has too many arguments starting with $signed.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
511
512
		// calculate last chunk nr
513
		// next highest is end of chunks, one subtracted is last one
514
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
515
516
		$lastChunkNr = ceil($size/ $blockSize)-1;
517
		// calculate last chunk position
518
		$lastChunkPos = ($lastChunkNr * $blockSize);
519
		// try to fseek to the last chunk, if it fails we have to read the whole file
520
		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
521
			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
522
		}
523
524
		$lastChunkContentEncrypted='';
525
		$count = $blockSize;
526
527
		while ($count > 0) {
528
			$data=fread($stream, $blockSize);
529
			$count=strlen($data);
530
			$lastChunkContentEncrypted .= $data;
531
			if(strlen($lastChunkContentEncrypted) > $blockSize) {
532
				$newUnencryptedSize += $unencryptedBlockSize;
533
				$lastChunkContentEncrypted=substr($lastChunkContentEncrypted, $blockSize);
534
			}
535
		}
536
537
		fclose($stream);
538
539
		// we have to decrypt the last chunk to get it actual size
540
		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
541
		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted);
542
		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path));
543
544
		// calc the real file size with the size of the last chunk
545
		$newUnencryptedSize += strlen($decryptedLastChunk);
546
547
		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
548
549
		// write to cache if applicable
550
		$cache = $this->storage->getCache();
551 5
		if ($cache) {
552 5
			$entry = $cache->get($path);
553 5
			$cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
554
		}
555
556
		return $newUnencryptedSize;
557
	}
558
559
	/**
560
	 * @param Storage $sourceStorage
561
	 * @param string $sourceInternalPath
562
	 * @param string $targetInternalPath
563
	 * @param bool $preserveMtime
564
	 * @return bool
565
	 */
566 View Code Duplication
	public function moveFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
567
		if ($sourceStorage === $this) {
568
			return $this->rename($sourceInternalPath, $targetInternalPath);
569
		}
570
571
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
572
		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
573
		// - copy the file cache update from  $this->copyBetweenStorage to this method
574
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
575
		// - remove $this->copyBetweenStorage
576
577
		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
578
		if ($result) {
579
			if ($sourceStorage->is_dir($sourceInternalPath)) {
580
				$result &= $sourceStorage->rmdir($sourceInternalPath);
581 29
			} else {
582
				$result &= $sourceStorage->unlink($sourceInternalPath);
583
			}
584
		}
585
		return $result;
586
	}
587
588
589 29
	/**
590
	 * @param Storage $sourceStorage
591
	 * @param string $sourceInternalPath
592
	 * @param string $targetInternalPath
593
	 * @param bool $preserveMtime
594
	 * @return bool
595
	 */
596
	public function copyFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
597
598
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
599
		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
600
		// - copy the file cache update from  $this->copyBetweenStorage to this method
601
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
602
		// - remove $this->copyBetweenStorage
603 41
604
		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, false);
605
	}
606
607 41
	/**
608 41
	 * copy file between two storages
609 8
	 *
610 8
	 * @param Storage $sourceStorage
611 4
	 * @param string $sourceInternalPath
612
	 * @param string $targetInternalPath
613 4
	 * @param bool $preserveMtime
614 2
	 * @param bool $isRename
615 2
	 * @return bool
616 2
	 * @throws \Exception
617 2
	 */
618 2
	private function copyBetweenStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
619 4
620 8
		// for versions we have nothing to do, because versions should always use the
621
		// key from the original file. Just create a 1:1 copy and done
622
		if ($this->isVersion($targetInternalPath) ||
623
			$this->isVersion($sourceInternalPath)) {
624
			// remember that we try to create a version so that we can detect it during
625 33
			// fopen($sourceInternalPath) and by-pass the encryption in order to
626 33
			// create a 1:1 copy of the file
627
			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
628
			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
629
			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
630
			if ($result) {
631
				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
632 33
				// make sure that we update the unencrypted size for the version
633
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
634
					$this->updateUnencryptedSize(
635 33
						$this->getFullPath($targetInternalPath),
636 3
						$info['size']
637 3
					);
638 3
				}
639 3
			}
640 3
			return $result;
641 3
		}
642 3
643 3
		// first copy the keys that we reuse the existing file key on the target location
644 3
		// and don't create a new one which would break versions for example.
645 3
		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
646
		if (count($mount) === 1) {
647 33
			$mountPoint = $mount[0]->getMountPoint();
648 33
			$source = $mountPoint . '/' . $sourceInternalPath;
649 33
			$target = $this->getFullPath($targetInternalPath);
650 33
			$this->copyKeys($source, $target);
651 33
		} else {
652 33
			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
653
		}
654
655
		if ($sourceStorage->is_dir($sourceInternalPath)) {
656
			$dh = $sourceStorage->opendir($sourceInternalPath);
657 33
			$result = $this->mkdir($targetInternalPath);
658 33 View Code Duplication
			if (is_resource($dh)) {
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...
659
				while ($result and ($file = readdir($dh)) !== false) {
660
					if (!Filesystem::isIgnoredDir($file)) {
661 33
						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
662
					}
663
				}
664
			}
665 33
		} else {
666
			try {
667
				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
668 33
				$target = $this->fopen($targetInternalPath, 'w');
669
				list(, $result) = \OC_Helper::streamCopy($source, $target);
0 ignored issues
show
Security Bug introduced by
It seems like $source can also be of type false; however, OC_Helper::streamCopy() does only seem to accept resource, did you maybe forget to handle an error condition?
Loading history...
Security Bug introduced by
It seems like $target can also be of type false; however, OC_Helper::streamCopy() does only seem to accept resource, did you maybe forget to handle an error condition?
Loading history...
670 33
				fclose($source);
671
				fclose($target);
672
			} catch (\Exception $e) {
673
				fclose($source);
674
				fclose($target);
0 ignored issues
show
Bug introduced by
The variable $target 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...
675
				throw $e;
676
			}
677 33
			if($result) {
678
				if ($preserveMtime) {
679
					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
0 ignored issues
show
Security Bug introduced by
It seems like $sourceStorage->filemtime($sourceInternalPath) targeting OCP\Files\Storage::filemtime() can also be of type false; however, OC\Files\Storage\Wrapper\Wrapper::touch() does only seem to accept integer|null, did you maybe forget to handle an error condition?
Loading history...
680
				}
681
				$isEncrypted = $this->encryptionManager->isEnabled() && $this->mount->getOption('encrypt', true) ? 1 : 0;
682
683
				// in case of a rename we need to manipulate the source cache because
684
				// this information will be kept for the new target
685
				if ($isRename) {
686
					$sourceStorage->getCache()->put($sourceInternalPath, ['encrypted' => $isEncrypted]);
687
				} else {
688 1
					$this->getCache()->put($targetInternalPath, ['encrypted' => $isEncrypted]);
689 1
				}
690
			} else {
691
				// delete partially written target file
692
				$this->unlink($targetInternalPath);
693
				// delete cache entry that was created by fopen
694
				$this->getCache()->remove($targetInternalPath);
695 1
			}
696
		}
697
		return (bool)$result;
698
699
	}
700
701
	/**
702
	 * get the path to a local version of the file.
703 1
	 * The local version of the file can be temporary and doesn't have to be persistent across requests
704 1
	 *
705 1
	 * @param string $path
706
	 * @return string
707
	 */
708
	public function getLocalFile($path) {
709
		if ($this->encryptionManager->isEnabled()) {
710
			$cachedFile = $this->getCachedFile($path);
711
			if (is_string($cachedFile)) {
712
				return $cachedFile;
713
			}
714
		}
715
		return $this->storage->getLocalFile($path);
716
	}
717 1
718 1
	/**
719 1
	 * Returns the wrapped storage's value for isLocal()
720 1
	 *
721 1
	 * @return bool wrapped storage's isLocal() value
722 1
	 */
723
	public function isLocal() {
724
		if ($this->encryptionManager->isEnabled()) {
725
			return false;
726
		}
727
		return $this->storage->isLocal();
728
	}
729
730
	/**
731
	 * see http://php.net/manual/en/function.stat.php
732
	 * only the following keys are required in the result: size and mtime
733 3
	 *
734 3
	 * @param string $path
735 3
	 * @return array
736 3
	 */
737 3
	public function stat($path) {
738 3
		$stat = $this->storage->stat($path);
739
		$fileSize = $this->filesize($path);
740
		$stat['size'] = $fileSize;
741
		$stat[7] = $fileSize;
742
		return $stat;
743
	}
744
745
	/**
746
	 * see http://php.net/manual/en/function.hash.php
747 99
	 *
748 99
	 * @param string $type
749
	 * @param string $path
750
	 * @param bool $raw
751
	 * @return string
752
	 */
753 View Code Duplication
	public function hash($type, $path, $raw = false) {
754
		$fh = $this->fopen($path, 'rb');
755
		$ctx = hash_init($type);
756
		hash_update_stream($ctx, $fh);
757
		fclose($fh);
758 71
		return hash_final($ctx, $raw);
759 71
	}
760 71
761 61
	/**
762 61
	 * return full path, including mount point
763 61
	 *
764 61
	 * @param string $path relative to mount point
765 71
	 * @return string full path including mount point
766
	 */
767
	protected function getFullPath($path) {
768
		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
769
	}
770
771
	/**
772
	 * read first block of encrypted file, typically this will contain the
773
	 * encryption header
774 5
	 *
775 5
	 * @param string $path
776 5
	 * @return string
777 5
	 */
778 5
	protected function readFirstBlock($path) {
779 5
		$firstBlock = '';
780 5
		if ($this->storage->file_exists($path)) {
781
			$handle = $this->storage->fopen($path, 'r');
782 5
			$firstBlock = fread($handle, $this->util->getHeaderSize());
783 5
			fclose($handle);
784 5
		}
785
		return $firstBlock;
786 5
	}
787
788
	/**
789
	 * return header size of given file
790
	 *
791
	 * @param string $path
792
	 * @return int
793
	 */
794
	protected function getHeaderSize($path) {
795 77
		$headerSize = 0;
796 77
		$realFile = $this->util->stripPartialFileExtension($path);
797 77
		if ($this->storage->file_exists($realFile)) {
798 8
			$path = $realFile;
799 8
		}
800 8
		$firstBlock = $this->readFirstBlock($path);
801 7
802
		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
803
			$headerSize = strlen($firstBlock);
804 7
		}
805
806 7
		return $headerSize;
807 7
	}
808 7
809 7
	/**
810 7
	 * parse raw header to array
811 7
	 *
812 8
	 * @param string $rawHeader
813
	 * @return array
814 77
	 */
815
	protected function parseRawHeader($rawHeader) {
816
		$result = array();
817
		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
818
			$header = $rawHeader;
819
			$endAt = strpos($header, Util::HEADER_END);
820
			if ($endAt !== false) {
821
				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
822
823 79
				// +1 to not start with an ':' which would result in empty element at the beginning
824 79
				$exploded = explode(':', substr($header, strlen(Util::HEADER_START)+1));
825 79
826 63
				$element = array_shift($exploded);
827 63 View Code Duplication
				while ($element !== Util::HEADER_END) {
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...
828
					$result[$element] = array_shift($exploded);
829 79
					$element = array_shift($exploded);
830 79
				}
831
			}
832
		}
833
834 79
		return $result;
835 74
	}
836 1
837 1
	/**
838
	 * read header from file
839 73
	 *
840 73
	 * @param string $path
841 1
	 * @return array
842 1
	 */
843
	protected function getHeader($path) {
844 74
		$realFile = $this->util->stripPartialFileExtension($path);
845
		if ($this->storage->file_exists($realFile)) {
846 79
			$path = $realFile;
847
		}
848
849
		$firstBlock = $this->readFirstBlock($path);
850
		$result = $this->parseRawHeader($firstBlock);
851
852
		// if the header doesn't contain a encryption module we check if it is a
853
		// legacy file. If true, we add the default encryption module
854
		if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
855
			if (!empty($result)) {
856
				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
857 5
			} else {
858 5
				// if the header was empty we have to check first if it is a encrypted file at all
859 5
				$info = $this->getCache()->get($path);
860 5
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
861 5
					$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
862
				}
863 5
			}
864 5
		}
865
866
		return $result;
867
	}
868 5
869 5
	/**
870
	 * read encryption module needed to read/write the file located at $path
871
	 *
872
	 * @param string $path
873
	 * @return null|\OCP\Encryption\IEncryptionModule
874
	 * @throws ModuleDoesNotExistsException
875
	 * @throws \Exception
876 5
	 */
877 5
	protected function getEncryptionModule($path) {
878 5
		$encryptionModule = null;
879
		$header = $this->getHeader($path);
880
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
881
		if (!empty($encryptionModuleId)) {
882
			try {
883
				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
884
			} catch (ModuleDoesNotExistsException $e) {
885
				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
886
				throw $e;
887 2
			}
888 2
		}
889 1
		return $encryptionModule;
890
	}
891
892 1
	/**
893
	 * @param string $path
894
	 * @param int $unencryptedSize
895
	 */
896
	public function updateUnencryptedSize($path, $unencryptedSize) {
897
		$this->unencryptedSize[$path] = $unencryptedSize;
898
	}
899
900
	/**
901 77
	 * copy keys to new location
902 77
	 *
903 77
	 * @param string $source path relative to data/
904
	 * @param string $target path relative to data/
905
	 * @return bool
906
	 */
907
	protected function copyKeys($source, $target) {
908
		if (!$this->util->isExcluded($source)) {
909
			return $this->keyStorage->copyKeys($source, $target);
910
		}
911
912
		return false;
913
	}
914
915
	/**
916
	 * check if path points to a files version
917
	 *
918
	 * @param $path
919
	 * @return bool
920
	 */
921
	protected function isVersion($path) {
922
		$normalized = Filesystem::normalizePath($path);
923
		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
924
	}
925
926
}
927