Completed
Push — stable8.1 ( e0a38c...1aa653 )
by
unknown
80:54
created

Encryption::verifyUnencryptedSize()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

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