Completed
Push — stable8.2 ( 696ee3...54f8ec )
by Roeland
70:33
created

Encryption::rmdir()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4
Metric Value
dl 0
loc 12
ccs 10
cts 10
cp 1
rs 9.2
cc 4
eloc 8
nc 2
nop 1
crap 4
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 OCP\Encryption\Exceptions\GenericEncryptionException;
34
use OCP\Encryption\IFile;
35
use OCP\Encryption\IManager;
36
use OCP\Encryption\Keys\IStorage;
37
use OCP\Files\Mount\IMountPoint;
38
use OCP\Files\Storage;
39
use OCP\ILogger;
40
41
class Encryption extends Wrapper {
42
43
	use LocalTempFileTrait;
44
45
	/** @var string */
46
	private $mountPoint;
47
48
	/** @var \OC\Encryption\Util */
49
	private $util;
50
51
	/** @var \OCP\Encryption\IManager */
52
	private $encryptionManager;
53
54
	/** @var \OCP\ILogger */
55
	private $logger;
56
57
	/** @var string */
58
	private $uid;
59
60
	/** @var array */
61
	protected $unencryptedSize;
62
63
	/** @var \OCP\Encryption\IFile */
64
	private $fileHelper;
65
66
	/** @var IMountPoint */
67
	private $mount;
68
69
	/** @var IStorage */
70
	private $keyStorage;
71
72
	/** @var Update */
73
	private $update;
74
75
	/** @var Manager */
76
	private $mountManager;
77
78
	/** @var array remember for which path we execute the repair step to avoid recursions */
79
	private $fixUnencryptedSizeOf = array();
80
81
	/**
82
	 * @param array $parameters
83
	 * @param IManager $encryptionManager
84
	 * @param Util $util
85
	 * @param ILogger $logger
86
	 * @param IFile $fileHelper
87
	 * @param string $uid
88
	 * @param IStorage $keyStorage
89
	 * @param Update $update
90
	 * @param Manager $mountManager
91
	 */
92 142
	public function __construct(
93
			$parameters,
94
			IManager $encryptionManager = null,
95
			Util $util = null,
96
			ILogger $logger = null,
97
			IFile $fileHelper = null,
98
			$uid = null,
99
			IStorage $keyStorage = null,
100
			Update $update = null,
101
			Manager $mountManager = null
102
		) {
103
		
104 142
		$this->mountPoint = $parameters['mountPoint'];
105 142
		$this->mount = $parameters['mount'];
106 142
		$this->encryptionManager = $encryptionManager;
107 142
		$this->util = $util;
108 142
		$this->logger = $logger;
109 142
		$this->uid = $uid;
110 142
		$this->fileHelper = $fileHelper;
111 142
		$this->keyStorage = $keyStorage;
112 142
		$this->unencryptedSize = array();
113 142
		$this->update = $update;
114 142
		$this->mountManager = $mountManager;
115 142
		parent::__construct($parameters);
116 142
	}
117
118
	/**
119
	 * see http://php.net/manual/en/function.filesize.php
120
	 * The result for filesize when called on a folder is required to be 0
121
	 *
122
	 * @param string $path
123
	 * @return int
124
	 */
125 13
	public function filesize($path) {
126 13
		$fullPath = $this->getFullPath($path);
127
128 13
		$info = $this->getCache()->get($path);
129 13
		if (isset($this->unencryptedSize[$fullPath])) {
130 5
			$size = $this->unencryptedSize[$fullPath];
131
			// update file cache
132 5
			$info['encrypted'] = true;
133 5
			$info['size'] = $size;
134 5
			$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 128 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...
135
136 5
			return $size;
137
		}
138
139 13
		if (isset($info['fileid']) && $info['encrypted']) {
140 2
			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 140 which is incompatible with the return type declared by the interface OCP\Files\Storage::filesize of type integer|false.
Loading history...
141
		}
142
143 12
		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 143 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...
144
	}
145
146
	/**
147
	 * @param string $path
148
	 * @return array
149
	 */
150 9
	public function getMetaData($path) {
151 9
		$data = $this->storage->getMetaData($path);
152 9
		if (is_null($data)) {
153 1
			return null;
154
		}
155 8
		$fullPath = $this->getFullPath($path);
156
157 8
		if (isset($this->unencryptedSize[$fullPath])) {
158 6
			$data['encrypted'] = true;
159 6
			$data['size'] = $this->unencryptedSize[$fullPath];
160 6
		} else {
161 7
			$info = $this->getCache()->get($path);
162 7
			if (isset($info['fileid']) && $info['encrypted']) {
163 1
				$data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
164 1
				$data['encrypted'] = true;
165 1
			}
166
		}
167
168 8
		return $data;
169
	}
170
	/**
171
	 * see http://php.net/manual/en/function.file_get_contents.php
172
	 *
173
	 * @param string $path
174
	 * @return string
175
	 */
176 57
	public function file_get_contents($path) {
177
178 57
		$encryptionModule = $this->getEncryptionModule($path);
179
180 57
		if ($encryptionModule) {
181 57
			$handle = $this->fopen($path, "r");
182 57
			if (!$handle) {
183
				return false;
184
			}
185 57
			$data = stream_get_contents($handle);
186 57
			fclose($handle);
187 57
			return $data;
188
		}
189 4
		return $this->storage->file_get_contents($path);
190
	}
191
192
	/**
193
	 * see http://php.net/manual/en/function.file_put_contents.php
194
	 *
195
	 * @param string $path
196
	 * @param string $data
197
	 * @return bool
198
	 */
199 66
	public function file_put_contents($path, $data) {
200
		// file put content will always be translated to a stream write
201 66
		$handle = $this->fopen($path, 'w');
202 66
		$written = fwrite($handle, $data);
203 66
		fclose($handle);
204 66
		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...
205
	}
206
207
	/**
208
	 * see http://php.net/manual/en/function.unlink.php
209
	 *
210
	 * @param string $path
211
	 * @return bool
212
	 */
213 27
	public function unlink($path) {
214 27
		$fullPath = $this->getFullPath($path);
215 27
		if ($this->util->isExcluded($fullPath)) {
216
			return $this->storage->unlink($path);
217
		}
218
219 27
		$encryptionModule = $this->getEncryptionModule($path);
220 27
		if ($encryptionModule) {
221 24
			$this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
222 24
		}
223
224 27
		return $this->storage->unlink($path);
225
	}
226
227
	/**
228
	 * see http://php.net/manual/en/function.rename.php
229
	 *
230
	 * @param string $path1
231
	 * @param string $path2
232
	 * @return bool
233
	 */
234 30
	public function rename($path1, $path2) {
235
236 30
		$result = $this->storage->rename($path1, $path2);
237
238 30
		if ($result &&
239
			// versions always use the keys from the original file, so we can skip
240
			// this step for versions
241 30
			$this->isVersion($path2) === false &&
242 30
			$this->encryptionManager->isEnabled()) {
243 9
			$source = $this->getFullPath($path1);
244 9
			if (!$this->util->isExcluded($source)) {
245 9
				$target = $this->getFullPath($path2);
246 9
				if (isset($this->unencryptedSize[$source])) {
247 5
					$this->unencryptedSize[$target] = $this->unencryptedSize[$source];
248 5
				}
249 9
				$this->keyStorage->renameKeys($source, $target);
250 9
			}
251 9
		}
252
253 30
		return $result;
254
	}
255
256
	/**
257
	 * see http://php.net/manual/en/function.rmdir.php
258
	 *
259
	 * @param string $path
260
	 * @return bool
261
	 */
262 16
	public function rmdir($path) {
263 16
		$result = $this->storage->rmdir($path);
264 16
		$fullPath = $this->getFullPath($path);
265 16
		if ($result &&
266 16
			$this->util->isExcluded($fullPath) === false &&
267 2
			$this->encryptionManager->isEnabled()
268 16
		) {
269 1
			$this->keyStorage->deleteAllFileKeys($fullPath);
270 1
		}
271
272 16
		return $result;
273
	}
274
275
	/**
276
	 * check if a file can be read
277
	 *
278
	 * @param string $path
279
	 * @return bool
280
	 */
281 12
	public function isReadable($path) {
282
283 12
		$isReadable = true;
284
285 12
		$metaData = $this->getMetaData($path);
286
		if (
287 12
			!$this->is_dir($path) &&
288 12
			isset($metaData['encrypted']) &&
289 1
			$metaData['encrypted'] === true
290 12
		) {
291 1
			$fullPath = $this->getFullPath($path);
292 1
			$module = $this->getEncryptionModule($path);
293 1
			$isReadable = $module->isReadable($fullPath, $this->uid);
294 1
		}
295
296 12
		return $this->storage->isReadable($path) && $isReadable;
297
	}
298
299
	/**
300
	 * see http://php.net/manual/en/function.copy.php
301
	 *
302
	 * @param string $path1
303
	 * @param string $path2
304
	 * @return bool
305
	 */
306 21
	public function copy($path1, $path2) {
307
308 21
		$source = $this->getFullPath($path1);
309
310 21
		if ($this->util->isExcluded($source)) {
311
			return $this->storage->copy($path1, $path2);
312
		}
313
314
		// need to stream copy file by file in case we copy between a encrypted
315
		// and a unencrypted storage
316 21
		$this->unlink($path2);
317 21
		$result = $this->copyFromStorage($this, $path1, $path2);
318
319 21
		return $result;
320
	}
321
322
	/**
323
	 * see http://php.net/manual/en/function.fopen.php
324
	 *
325
	 * @param string $path
326
	 * @param string $mode
327
	 * @return resource
328
	 * @throws GenericEncryptionException
329
	 * @throws ModuleDoesNotExistsException
330
	 */
331 71
	public function fopen($path, $mode) {
332
333 71
		$encryptionEnabled = $this->encryptionManager->isEnabled();
334 71
		$shouldEncrypt = false;
335 71
		$encryptionModule = null;
336 71
		$header = $this->getHeader($path);
337 71
		$fullPath = $this->getFullPath($path);
338 71
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
339
340 71
		if ($this->util->isExcluded($fullPath) === false) {
341
342 5
			$size = $unencryptedSize = 0;
343 5
			$realFile = $this->util->stripPartialFileExtension($path);
344 5
			$targetExists = $this->file_exists($realFile) || $this->file_exists($path);
345 5
			$targetIsEncrypted = false;
346 5
			if ($targetExists) {
347
				// in case the file exists we require the explicit module as
348
				// specified in the file header - otherwise we need to fail hard to
349
				// prevent data loss on client side
350 5
				if (!empty($encryptionModuleId)) {
351 5
					$targetIsEncrypted = true;
352 5
					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
353 5
				}
354
355 5
				if ($this->file_exists($path)) {
356 5
					$size = $this->storage->filesize($path);
357 5
					$unencryptedSize = $this->filesize($path);
358 5
				} else {
359 2
					$size = $unencryptedSize = 0;
360
				}
361 5
			}
362
363
			try {
364
365
				if (
366
					$mode === 'w'
367 5
					|| $mode === 'w+'
368 5
					|| $mode === 'wb'
369 5
					|| $mode === 'wb+'
370 5
				) {
371
					// don't overwrite encrypted files if encyption is not enabled
372 5
					if ($targetIsEncrypted && $encryptionEnabled === false) {
373
						throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled');
374
					}
375 5
					if ($encryptionEnabled) {
376
						// if $encryptionModuleId is empty, the default module will be used
377 5
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
378 5
						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
379 5
					}
380 5
				} else {
381 5
					$info = $this->getCache()->get($path);
382
					// only get encryption module if we found one in the header
383
					// or if file should be encrypted according to the file cache
384 5
					if (!empty($encryptionModuleId)) {
385 5
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
386 5
						$shouldEncrypt = true;
387 5
					} else if (empty($encryptionModuleId) && $info['encrypted'] === true) {
388
						// we come from a old installation. No header and/or no module defined
389
						// but the file is encrypted. In this case we need to use the
390
						// OC_DEFAULT_MODULE to read the file
391
						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
392
						$shouldEncrypt = true;
393
						$targetIsEncrypted = true;
394
					}
395
				}
396 5
			} catch (ModuleDoesNotExistsException $e) {
397
				$this->logger->warning('Encryption module "' . $encryptionModuleId .
398
					'" not found, file will be stored unencrypted (' . $e->getMessage() . ')');
399
			}
400
401
			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
402 5
			if (!$encryptionEnabled || !$this->mount->getOption('encrypt', true)) {
403
				if (!$targetExists || !$targetIsEncrypted) {
404
					$shouldEncrypt = false;
405
				}
406
			}
407
408 5
			if ($shouldEncrypt === true && $encryptionModule !== null) {
409 5
				$headerSize = $this->getHeaderSize($path);
410 5
				$source = $this->storage->fopen($path, $mode);
411 5
				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
412 5
					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
413 5
					$size, $unencryptedSize, $headerSize);
414 5
				return $handle;
415
			}
416
417 3
		}
418
419 71
		return $this->storage->fopen($path, $mode);
420
	}
421
422
423
	/**
424
	 * perform some plausibility checks if the the unencrypted size is correct.
425
	 * If not, we calculate the correct unencrypted size and return it
426
	 *
427
	 * @param string $path internal path relative to the storage root
428
	 * @param int $unencryptedSize size of the unencrypted file
429
	 *
430
	 * @return int unencrypted size
431
	 */
432 5
	protected function verifyUnencryptedSize($path, $unencryptedSize) {
433
434 5
		$size = $this->storage->filesize($path);
435 5
		$result = $unencryptedSize;
436
437 5
		if ($unencryptedSize < 0 ||
438 3
			($size > 0 && $unencryptedSize === $size)
439 5
		) {
440
			// check if we already calculate the unencrypted size for the
441
			// given path to avoid recursions
442 3
			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
443 3
				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
444
				try {
445 3
					$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 434 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...
446 3
				} catch (\Exception $e) {
447 1
					$this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
448 1
					$this->logger->logException($e);
449
				}
450 3
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
451 3
			}
452 3
		}
453
454 5
		return $result;
455
	}
456
457
	/**
458
	 * calculate the unencrypted size
459
	 *
460
	 * @param string $path internal path relative to the storage root
461
	 * @param int $size size of the physical file
462
	 * @param int $unencryptedSize size of the unencrypted file
463
	 *
464
	 * @return int calculated unencrypted size
465
	 */
466
	protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
467
468
		$headerSize = $this->getHeaderSize($path);
469
		$header = $this->getHeader($path);
470
		$encryptionModule = $this->getEncryptionModule($path);
471
472
		$stream = $this->storage->fopen($path, 'r');
473
474
		// if we couldn't open the file we return the old unencrypted size
475
		if (!is_resource($stream)) {
476
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
477
			return $unencryptedSize;
478
		}
479
480
		$newUnencryptedSize = 0;
481
		$size -= $headerSize;
482
		$blockSize = $this->util->getBlockSize();
483
484
		// if a header exists we skip it
485
		if ($headerSize > 0) {
486
			fread($stream, $headerSize);
487
		}
488
489
		// fast path, else the calculation for $lastChunkNr is bogus
490
		if ($size === 0) {
491
			return 0;
492
		}
493
494
		$signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
495
		$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...
496
497
		// calculate last chunk nr
498
		// next highest is end of chunks, one subtracted is last one
499
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
500
501
		$lastChunkNr = ceil($size/ $blockSize)-1;
502
		// calculate last chunk position
503
		$lastChunkPos = ($lastChunkNr * $blockSize);
504
		// try to fseek to the last chunk, if it fails we have to read the whole file
505
		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
506
			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
507
		}
508
509
		$lastChunkContentEncrypted='';
510
		$count = $blockSize;
511
512
		while ($count > 0) {
513
			$data=fread($stream, $blockSize);
514
			$count=strlen($data);
515
			$lastChunkContentEncrypted .= $data;
516
			if(strlen($lastChunkContentEncrypted) > $blockSize) {
517
				$newUnencryptedSize += $unencryptedBlockSize;
518
				$lastChunkContentEncrypted=substr($lastChunkContentEncrypted, $blockSize);
519
			}
520
		}
521
522
		fclose($stream);
523
524
		// we have to decrypt the last chunk to get it actual size
525
		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
526
		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted);
527
		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path));
528
529
		// calc the real file size with the size of the last chunk
530
		$newUnencryptedSize += strlen($decryptedLastChunk);
531
532
		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
533
534
		// write to cache if applicable
535
		$cache = $this->storage->getCache();
536
		if ($cache) {
537
			$entry = $cache->get($path);
538
			$cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
539
		}
540
541
		return $newUnencryptedSize;
542
	}
543
544
	/**
545
	 * @param Storage $sourceStorage
546
	 * @param string $sourceInternalPath
547
	 * @param string $targetInternalPath
548
	 * @param bool $preserveMtime
549
	 * @return bool
550
	 */
551 5 View Code Duplication
	public function moveFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
552 5
		if ($sourceStorage === $this) {
553 5
			return $this->rename($sourceInternalPath, $targetInternalPath);
554
		}
555
556
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
557
		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
558
		// - copy the file cache update from  $this->copyBetweenStorage to this method
559
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
560
		// - remove $this->copyBetweenStorage
561
562
		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
563
		if ($result) {
564
			if ($sourceStorage->is_dir($sourceInternalPath)) {
565
				$result &= $sourceStorage->rmdir($sourceInternalPath);
566
			} else {
567
				$result &= $sourceStorage->unlink($sourceInternalPath);
568
			}
569
		}
570
		return $result;
571
	}
572
573
574
	/**
575
	 * @param Storage $sourceStorage
576
	 * @param string $sourceInternalPath
577
	 * @param string $targetInternalPath
578
	 * @param bool $preserveMtime
579
	 * @return bool
580
	 */
581 29
	public function copyFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
582
583
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
584
		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
585
		// - copy the file cache update from  $this->copyBetweenStorage to this method
586
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
587
		// - remove $this->copyBetweenStorage
588
589 29
		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, false);
590
	}
591
592
	/**
593
	 * copy file between two storages
594
	 *
595
	 * @param Storage $sourceStorage
596
	 * @param string $sourceInternalPath
597
	 * @param string $targetInternalPath
598
	 * @param bool $preserveMtime
599
	 * @param bool $isRename
600
	 * @return bool
601
	 * @throws \Exception
602
	 */
603 41
	private function copyBetweenStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
604
605
		// for versions we have nothing to do, because versions should always use the
606
		// key from the original file. Just create a 1:1 copy and done
607 41
		if ($this->isVersion($targetInternalPath) ||
608 41
			$this->isVersion($sourceInternalPath)) {
609 8
			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
610 8
			if ($result) {
611 4
				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
612
				// make sure that we update the unencrypted size for the version
613 4
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
614 2
					$this->updateUnencryptedSize(
615 2
						$this->getFullPath($targetInternalPath),
616 2
						$info['size']
617 2
					);
618 2
				}
619 4
			}
620 8
			return $result;
621
		}
622
623
		// first copy the keys that we reuse the existing file key on the target location
624
		// and don't create a new one which would break versions for example.
625 33
		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
626 33
		if (count($mount) === 1) {
627
			$mountPoint = $mount[0]->getMountPoint();
628
			$source = $mountPoint . '/' . $sourceInternalPath;
629
			$target = $this->getFullPath($targetInternalPath);
630
			$this->copyKeys($source, $target);
631
		} else {
632 33
			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
633
		}
634
635 33
		if ($sourceStorage->is_dir($sourceInternalPath)) {
636 3
			$dh = $sourceStorage->opendir($sourceInternalPath);
637 3
			$result = $this->mkdir($targetInternalPath);
638 3 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...
639 3
				while ($result and ($file = readdir($dh)) !== false) {
640 3
					if (!Filesystem::isIgnoredDir($file)) {
641 3
						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
642 3
					}
643 3
				}
644 3
			}
645 3
		} else {
646
			try {
647 33
				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
648 33
				$target = $this->fopen($targetInternalPath, 'w');
649 33
				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...
650 33
				fclose($source);
651 33
				fclose($target);
652 33
			} catch (\Exception $e) {
653
				fclose($source);
654
				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...
655
				throw $e;
656
			}
657 33
			if($result) {
658 33
				if ($preserveMtime) {
659
					$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...
660
				}
661 33
				$isEncrypted = $this->encryptionManager->isEnabled() && $this->mount->getOption('encrypt', true) ? 1 : 0;
662
663
				// in case of a rename we need to manipulate the source cache because
664
				// this information will be kept for the new target
665 33
				if ($isRename) {
666
					$sourceStorage->getCache()->put($sourceInternalPath, ['encrypted' => $isEncrypted]);
667
				} else {
668 33
					$this->getCache()->put($targetInternalPath, ['encrypted' => $isEncrypted]);
669
				}
670 33
			} else {
671
				// delete partially written target file
672
				$this->unlink($targetInternalPath);
673
				// delete cache entry that was created by fopen
674
				$this->getCache()->remove($targetInternalPath);
675
			}
676
		}
677 33
		return (bool)$result;
678
679
	}
680
681
	/**
682
	 * get the path to a local version of the file.
683
	 * The local version of the file can be temporary and doesn't have to be persistent across requests
684
	 *
685
	 * @param string $path
686
	 * @return string
687
	 */
688 1
	public function getLocalFile($path) {
689 1
		if ($this->encryptionManager->isEnabled()) {
690
			$cachedFile = $this->getCachedFile($path);
691
			if (is_string($cachedFile)) {
692
				return $cachedFile;
693
			}
694
		}
695 1
		return $this->storage->getLocalFile($path);
696
	}
697
698
	/**
699
	 * Returns the wrapped storage's value for isLocal()
700
	 *
701
	 * @return bool wrapped storage's isLocal() value
702
	 */
703 1
	public function isLocal() {
704 1
		if ($this->encryptionManager->isEnabled()) {
705 1
			return false;
706
		}
707
		return $this->storage->isLocal();
708
	}
709
710
	/**
711
	 * see http://php.net/manual/en/function.stat.php
712
	 * only the following keys are required in the result: size and mtime
713
	 *
714
	 * @param string $path
715
	 * @return array
716
	 */
717 1
	public function stat($path) {
718 1
		$stat = $this->storage->stat($path);
719 1
		$fileSize = $this->filesize($path);
720 1
		$stat['size'] = $fileSize;
721 1
		$stat[7] = $fileSize;
722 1
		return $stat;
723
	}
724
725
	/**
726
	 * see http://php.net/manual/en/function.hash.php
727
	 *
728
	 * @param string $type
729
	 * @param string $path
730
	 * @param bool $raw
731
	 * @return string
732
	 */
733 3 View Code Duplication
	public function hash($type, $path, $raw = false) {
734 3
		$fh = $this->fopen($path, 'rb');
735 3
		$ctx = hash_init($type);
736 3
		hash_update_stream($ctx, $fh);
737 3
		fclose($fh);
738 3
		return hash_final($ctx, $raw);
739
	}
740
741
	/**
742
	 * return full path, including mount point
743
	 *
744
	 * @param string $path relative to mount point
745
	 * @return string full path including mount point
746
	 */
747 99
	protected function getFullPath($path) {
748 99
		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
749
	}
750
751
	/**
752
	 * read first block of encrypted file, typically this will contain the
753
	 * encryption header
754
	 *
755
	 * @param string $path
756
	 * @return string
757
	 */
758 71
	protected function readFirstBlock($path) {
759 71
		$firstBlock = '';
760 71
		if ($this->storage->file_exists($path)) {
761 61
			$handle = $this->storage->fopen($path, 'r');
762 61
			$firstBlock = fread($handle, $this->util->getHeaderSize());
763 61
			fclose($handle);
764 61
		}
765 71
		return $firstBlock;
766
	}
767
768
	/**
769
	 * return header size of given file
770
	 *
771
	 * @param string $path
772
	 * @return int
773
	 */
774 5
	protected function getHeaderSize($path) {
775 5
		$headerSize = 0;
776 5
		$realFile = $this->util->stripPartialFileExtension($path);
777 5
		if ($this->storage->file_exists($realFile)) {
778 5
			$path = $realFile;
779 5
		}
780 5
		$firstBlock = $this->readFirstBlock($path);
781
782 5
		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
783 5
			$headerSize = strlen($firstBlock);
784 5
		}
785
786 5
		return $headerSize;
787
	}
788
789
	/**
790
	 * parse raw header to array
791
	 *
792
	 * @param string $rawHeader
793
	 * @return array
794
	 */
795 77
	protected function parseRawHeader($rawHeader) {
796 77
		$result = array();
797 77
		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
798 8
			$header = $rawHeader;
799 8
			$endAt = strpos($header, Util::HEADER_END);
800 8
			if ($endAt !== false) {
801 7
				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
802
803
				// +1 to not start with an ':' which would result in empty element at the beginning
804 7
				$exploded = explode(':', substr($header, strlen(Util::HEADER_START)+1));
805
806 7
				$element = array_shift($exploded);
807 7 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...
808 7
					$result[$element] = array_shift($exploded);
809 7
					$element = array_shift($exploded);
810 7
				}
811 7
			}
812 8
		}
813
814 77
		return $result;
815
	}
816
817
	/**
818
	 * read header from file
819
	 *
820
	 * @param string $path
821
	 * @return array
822
	 */
823 79
	protected function getHeader($path) {
824 79
		$realFile = $this->util->stripPartialFileExtension($path);
825 79
		if ($this->storage->file_exists($realFile)) {
826 63
			$path = $realFile;
827 63
		}
828
829 79
		$firstBlock = $this->readFirstBlock($path);
830 79
		$result = $this->parseRawHeader($firstBlock);
831
832
		// if the header doesn't contain a encryption module we check if it is a
833
		// legacy file. If true, we add the default encryption module
834 79
		if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
835 74
			if (!empty($result)) {
836 1
				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
837 1
			} else {
838
				// if the header was empty we have to check first if it is a encrypted file at all
839 73
				$info = $this->getCache()->get($path);
840 73
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
841 1
					$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
842 1
				}
843
			}
844 74
		}
845
846 79
		return $result;
847
	}
848
849
	/**
850
	 * read encryption module needed to read/write the file located at $path
851
	 *
852
	 * @param string $path
853
	 * @return null|\OCP\Encryption\IEncryptionModule
854
	 * @throws ModuleDoesNotExistsException
855
	 * @throws \Exception
856
	 */
857 5
	protected function getEncryptionModule($path) {
858 5
		$encryptionModule = null;
859 5
		$header = $this->getHeader($path);
860 5
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
861 5
		if (!empty($encryptionModuleId)) {
862
			try {
863 5
				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
864 5
			} catch (ModuleDoesNotExistsException $e) {
865
				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
866
				throw $e;
867
			}
868 5
		}
869 5
		return $encryptionModule;
870
	}
871
872
	/**
873
	 * @param string $path
874
	 * @param int $unencryptedSize
875
	 */
876 5
	public function updateUnencryptedSize($path, $unencryptedSize) {
877 5
		$this->unencryptedSize[$path] = $unencryptedSize;
878 5
	}
879
880
	/**
881
	 * copy keys to new location
882
	 *
883
	 * @param string $source path relative to data/
884
	 * @param string $target path relative to data/
885
	 * @return bool
886
	 */
887 2
	protected function copyKeys($source, $target) {
888 2
		if (!$this->util->isExcluded($source)) {
889 1
			return $this->keyStorage->copyKeys($source, $target);
890
		}
891
892 1
		return false;
893
	}
894
895
	/**
896
	 * check if path points to a files version
897
	 *
898
	 * @param $path
899
	 * @return bool
900
	 */
901 77
	protected function isVersion($path) {
902 77
		$normalized = Filesystem::normalizePath($path);
903 77
		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
904
	}
905
906
}
907