Passed
Push — master ( 192e53...75f728 )
by Julius
30:42 queued 15:22
created

FixEncryptedVersion::correctEncryptedVersion()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 69
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 9
eloc 41
nc 7
nop 2
dl 0
loc 69
rs 7.7084
c 2
b 1
f 0

How to fix   Long Method   

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 Sujith Haridasan <[email protected]>
4
 * @author Ilja Neumann <[email protected]>
5
 *
6
 * @copyright Copyright (c) 2019, ownCloud GmbH
7
 * @license AGPL-3.0
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
23
namespace OCA\Encryption\Command;
24
25
use OC\Files\View;
26
use OC\HintException;
27
use OCA\Encryption\Util;
28
use OCP\Files\IRootFolder;
29
use OCP\IConfig;
30
use OCP\ILogger;
31
use OCP\IUserManager;
32
use Symfony\Component\Console\Command\Command;
33
use Symfony\Component\Console\Input\InputArgument;
34
use Symfony\Component\Console\Input\InputInterface;
35
use Symfony\Component\Console\Output\OutputInterface;
36
37
class FixEncryptedVersion extends Command {
38
	/** @var IConfig */
39
	private $config;
40
41
	/** @var ILogger */
42
	private $logger;
43
44
	/** @var IRootFolder  */
45
	private $rootFolder;
46
47
	/** @var IUserManager  */
48
	private $userManager;
49
50
	/** @var Util */
51
	private $util;
52
53
	/** @var View  */
54
	private $view;
55
56
	public function __construct(
57
		IConfig $config,
58
		ILogger $logger,
59
		IRootFolder $rootFolder,
60
		IUserManager $userManager,
61
		Util $util,
62
		View $view
63
	) {
64
		$this->config = $config;
65
		$this->logger = $logger;
66
		$this->rootFolder = $rootFolder;
67
		$this->userManager = $userManager;
68
		$this->util = $util;
69
		$this->view = $view;
70
		parent::__construct();
71
	}
72
73
	protected function configure(): void {
74
		parent::configure();
75
76
		$this
77
			->setName('encryption:fix-encrypted-version')
78
			->setDescription('Fix the encrypted version if the encrypted file(s) are not downloadable.')
79
			->addArgument(
80
				'user',
81
				InputArgument::REQUIRED,
82
				'The id of the user whose files need fixing'
83
			)->addOption(
84
				'path',
85
				'p',
86
				InputArgument::OPTIONAL,
87
				'Limit files to fix with path, e.g., --path="/Music/Artist". If path indicates a directory, all the files inside directory will be fixed.'
88
			);
89
	}
90
91
	/**
92
	 * @param InputInterface $input
93
	 * @param OutputInterface $output
94
	 * @return int
95
	 */
96
	protected function execute(InputInterface $input, OutputInterface $output): int {
97
		$skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false);
98
99
		if ($skipSignatureCheck) {
100
			$output->writeln("<error>Repairing is not possible when \"encryption_skip_signature_check\" is set. Please disable this flag in the configuration.</error>\n");
101
			return 1;
102
		}
103
104
		if (!$this->util->isMasterKeyEnabled()) {
105
			$output->writeln("<error>Repairing only works with master key encryption.</error>\n");
106
			return 1;
107
		}
108
109
		$user = (string)$input->getArgument('user');
110
		$pathToWalk = "/$user/files";
111
112
		/**
113
		 * trim() returns an empty string when the argument is an unset/null
114
		 */
115
		$pathOption = \trim($input->getOption('path'), '/');
116
		if ($pathOption !== "") {
117
			$pathToWalk = "$pathToWalk/$pathOption";
118
		}
119
120
		if ($user === null) {
0 ignored issues
show
introduced by
The condition $user === null is always false.
Loading history...
121
			$output->writeln("<error>No user id provided.</error>\n");
122
			return 1;
123
		}
124
125
		if ($this->userManager->get($user) === null) {
126
			$output->writeln("<error>User id $user does not exist. Please provide a valid user id</error>");
127
			return 1;
128
		}
129
		return $this->walkPathOfUser($user, $pathToWalk, $output);
130
	}
131
132
	/**
133
	 * @param string $user
134
	 * @param string $path
135
	 * @param OutputInterface $output
136
	 * @return int 0 for success, 1 for error
137
	 */
138
	private function walkPathOfUser($user, $path, OutputInterface $output): int {
139
		$this->setupUserFs($user);
140
		if (!$this->view->file_exists($path)) {
141
			$output->writeln("<error>Path \"$path\" does not exist. Please provide a valid path.</error>");
142
			return 1;
143
		}
144
145
		if ($this->view->is_file($path)) {
146
			$output->writeln("Verifying the content of file \"$path\"");
147
			$this->verifyFileContent($path, $output);
148
			return 0;
149
		}
150
		$directories = [];
151
		$directories[] = $path;
152
		while ($root = \array_pop($directories)) {
153
			$directoryContent = $this->view->getDirectoryContent($root);
154
			foreach ($directoryContent as $file) {
155
				$path = $root . '/' . $file['name'];
156
				if ($this->view->is_dir($path)) {
157
					$directories[] = $path;
158
				} else {
159
					$output->writeln("Verifying the content of file \"$path\"");
160
					$this->verifyFileContent($path, $output);
161
				}
162
			}
163
		}
164
		return 0;
165
	}
166
167
	/**
168
	 * @param string $path
169
	 * @param OutputInterface $output
170
	 * @param bool $ignoreCorrectEncVersionCall, setting this variable to false avoids recursion
171
	 */
172
	private function verifyFileContent($path, OutputInterface $output, $ignoreCorrectEncVersionCall = true): bool {
173
		try {
174
			/**
175
			 * In encryption, the files are read in a block size of 8192 bytes
176
			 * Read block size of 8192 and a bit more (808 bytes)
177
			 * If there is any problem, the first block should throw the signature
178
			 * mismatch error. Which as of now, is enough to proceed ahead to
179
			 * correct the encrypted version.
180
			 */
181
			$handle = $this->view->fopen($path, 'rb');
182
183
			if (\fread($handle, 9001) !== false) {
184
				$output->writeln("<info>The file \"$path\" is: OK</info>");
185
			}
186
187
			\fclose($handle);
188
189
			return true;
190
		} catch (HintException $e) {
191
			$this->logger->warning("Issue: " . $e->getMessage());
192
			//If allowOnce is set to false, this becomes recursive.
193
			if ($ignoreCorrectEncVersionCall === true) {
194
				//Lets rectify the file by correcting encrypted version
195
				$output->writeln("<info>Attempting to fix the path: \"$path\"</info>");
196
				return $this->correctEncryptedVersion($path, $output);
197
			}
198
			return false;
199
		}
200
	}
201
202
	/**
203
	 * @param string $path
204
	 * @param OutputInterface $output
205
	 * @return bool
206
	 */
207
	private function correctEncryptedVersion($path, OutputInterface $output): bool {
208
		$fileInfo = $this->view->getFileInfo($path);
209
		if (!$fileInfo) {
210
			$output->writeln("<warning>File info not found for file: \"$path\"</warning>");
211
			return true;
212
		}
213
		$fileId = $fileInfo->getId();
214
		$encryptedVersion = $fileInfo->getEncryptedVersion();
215
		$wrongEncryptedVersion = $encryptedVersion;
216
217
		$storage = $fileInfo->getStorage();
218
219
		$cache = $storage->getCache();
220
		$fileCache = $cache->get($fileId);
221
		if (!$fileCache) {
222
			$output->writeln("<warning>File cache entry not found for file: \"$path\"</warning>");
223
			return true;
224
		}
225
226
		if ($storage->instanceOfStorage('OCA\Files_Sharing\ISharedStorage')) {
227
			$output->writeln("<info>The file: \"$path\" is a share. Please also run the script for the owner of the share</info>");
228
			return true;
229
		}
230
231
		// Save original encrypted version so we can restore it if decryption fails with all version
232
		$originalEncryptedVersion = $encryptedVersion;
233
		if ($encryptedVersion >= 0) {
234
			//test by decrementing the value till 1 and if nothing works try incrementing
235
			$encryptedVersion--;
236
			while ($encryptedVersion > 0) {
237
				$cacheInfo = ['encryptedVersion' => $encryptedVersion, 'encrypted' => $encryptedVersion];
238
				$cache->put($fileCache->getPath(), $cacheInfo);
239
				$output->writeln("<info>Decrement the encrypted version to $encryptedVersion</info>");
240
				if ($this->verifyFileContent($path, $output, false) === true) {
241
					$output->writeln("<info>Fixed the file: \"$path\" with version " . $encryptedVersion . "</info>");
242
					return true;
243
				}
244
				$encryptedVersion--;
245
			}
246
247
			//So decrementing did not work. Now lets increment. Max increment is till 5
248
			$increment = 1;
249
			while ($increment <= 5) {
250
				/**
251
				 * The wrongEncryptedVersion would not be incremented so nothing to worry about here.
252
				 * Only the newEncryptedVersion is incremented.
253
				 * For example if the wrong encrypted version is 4 then
254
				 * cycle1 -> newEncryptedVersion = 5 ( 4 + 1)
255
				 * cycle2 -> newEncryptedVersion = 6 ( 4 + 2)
256
				 * cycle3 -> newEncryptedVersion = 7 ( 4 + 3)
257
				 */
258
				$newEncryptedVersion = $wrongEncryptedVersion + $increment;
259
260
				$cacheInfo = ['encryptedVersion' => $newEncryptedVersion, 'encrypted' => $newEncryptedVersion];
261
				$cache->put($fileCache->getPath(), $cacheInfo);
262
				$output->writeln("<info>Increment the encrypted version to $newEncryptedVersion</info>");
263
				if ($this->verifyFileContent($path, $output, false) === true) {
264
					$output->writeln("<info>Fixed the file: \"$path\" with version " . $newEncryptedVersion . "</info>");
265
					return true;
266
				}
267
				$increment++;
268
			}
269
		}
270
271
		$cacheInfo = ['encryptedVersion' => $originalEncryptedVersion, 'encrypted' => $originalEncryptedVersion];
272
		$cache->put($fileCache->getPath(), $cacheInfo);
273
		$output->writeln("<info>No fix found for \"$path\", restored version to original: $originalEncryptedVersion</info>");
274
275
		return false;
276
	}
277
278
	/**
279
	 * Setup user file system
280
	 * @param string $uid
281
	 */
282
	private function setupUserFs($uid): void {
283
		\OC_Util::tearDownFS();
284
		\OC_Util::setupFS($uid);
285
	}
286
}
287