Passed
Push — master ( 037411...507fac )
by Roeland
28:26 queued 12:26
created

Manager   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 252
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 136
dl 0
loc 252
rs 8.96
c 0
b 0
f 0
wmc 43

19 Methods

Rating   Name   Duplication   Size   Complexity  
A cleanup() 0 5 1
A invokeTokenScope() 0 3 1
A getTemplates() 0 29 5
A refreshToken() 0 7 1
A open() 0 12 3
A findEditorForFile() 0 7 3
A create() 0 22 5
A getToken() 0 9 2
A editSecure() 0 1 1
A getFileForToken() 0 10 3
A __construct() 0 14 2
A accessToken() 0 8 1
A getEditors() 0 2 1
A createToken() 0 15 2
A invalidateToken() 0 6 1
A edit() 0 14 3
A getEditor() 0 5 2
A registerDirectEditor() 0 2 1
A isEnabled() 0 16 5

How to fix   Complexity   

Complex Class

Complex classes like Manager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Manager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2019 Julius Härtl <[email protected]>
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Julius Härtl <[email protected]>
7
 * @author Robin Appelman <[email protected]>
8
 * @author Tobias Kaminsky <[email protected]>
9
 *
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OC\DirectEditing;
28
29
use Doctrine\DBAL\FetchMode;
30
use OC\Files\Node\Folder;
31
use OCP\AppFramework\Http\NotFoundResponse;
32
use OCP\AppFramework\Http\Response;
33
use OCP\AppFramework\Http\TemplateResponse;
34
use OCP\DB\QueryBuilder\IQueryBuilder;
35
use OCP\DirectEditing\ACreateFromTemplate;
36
use OCP\DirectEditing\IEditor;
37
use \OCP\DirectEditing\IManager;
38
use OCP\DirectEditing\IToken;
39
use OCP\Encryption\IManager as EncryptionManager;
40
use OCP\Files\File;
41
use OCP\Files\IRootFolder;
42
use OCP\Files\Node;
43
use OCP\Files\NotFoundException;
44
use OCP\IDBConnection;
45
use OCP\IL10N;
46
use OCP\IUserSession;
47
use OCP\L10N\IFactory;
48
use OCP\Security\ISecureRandom;
49
use OCP\Share\IShare;
50
use Throwable;
51
use function array_key_exists;
52
use function in_array;
53
54
class Manager implements IManager {
55
	private const TOKEN_CLEANUP_TIME = 12 * 60 * 60 ;
56
57
	public const TABLE_TOKENS = 'direct_edit';
58
59
	/** @var IEditor[] */
60
	private $editors = [];
61
	/** @var IDBConnection */
62
	private $connection;
63
	/** @var ISecureRandom */
64
	private $random;
65
	/** @var string|null */
66
	private $userId;
67
	/** @var IRootFolder */
68
	private $rootFolder;
69
	/** @var IL10N */
70
	private $l10n;
71
	/** @var EncryptionManager */
72
	private $encryptionManager;
73
74
	public function __construct(
75
		ISecureRandom $random,
76
		IDBConnection $connection,
77
		IUserSession $userSession,
78
		IRootFolder $rootFolder,
79
		IFactory $l10nFactory,
80
		EncryptionManager $encryptionManager
81
	) {
82
		$this->random = $random;
83
		$this->connection = $connection;
84
		$this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
85
		$this->rootFolder = $rootFolder;
86
		$this->l10n = $l10nFactory->get('core');
87
		$this->encryptionManager = $encryptionManager;
88
	}
89
90
	public function registerDirectEditor(IEditor $directEditor): void {
91
		$this->editors[$directEditor->getId()] = $directEditor;
92
	}
93
94
	public function getEditors(): array {
95
		return $this->editors;
96
	}
97
98
	public function getTemplates(string $editor, string $type): array {
99
		if (!array_key_exists($editor, $this->editors)) {
100
			throw new \RuntimeException('No matching editor found');
101
		}
102
		$templates = [];
103
		foreach ($this->editors[$editor]->getCreators() as $creator) {
104
			if ($creator->getId() === $type) {
105
				$templates = [
106
					'empty' => [
107
						'id' => 'empty',
108
						'title' => $this->l10n->t('Empty file'),
109
						'preview' => null
110
					]
111
				];
112
113
				if ($creator instanceof ACreateFromTemplate) {
114
					$templates = $creator->getTemplates();
115
				}
116
117
				$templates = array_map(function ($template) use ($creator) {
118
					$template['extension'] = $creator->getExtension();
119
					$template['mimetype'] = $creator->getMimetype();
120
					return $template;
121
				}, $templates);
122
			}
123
		}
124
		$return = [];
125
		$return['templates'] = $templates;
126
		return $return;
127
	}
128
129
	public function create(string $path, string $editorId, string $creatorId, $templateId = null): string {
130
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
131
		if ($userFolder->nodeExists($path)) {
132
			throw new \RuntimeException('File already exists');
133
		} else {
134
			if (!$userFolder->nodeExists(dirname($path))) {
135
				throw new \RuntimeException('Invalid path');
136
			}
137
			/** @var Folder $folder */
138
			$folder = $userFolder->get(dirname($path));
139
			$file = $folder->newFile(basename($path));
140
			$editor = $this->getEditor($editorId);
141
			$creators = $editor->getCreators();
142
			foreach ($creators as $creator) {
143
				if ($creator->getId() === $creatorId) {
144
					$creator->create($file, $creatorId, $templateId);
145
					return $this->createToken($editorId, $file, $path);
146
				}
147
			}
148
		}
149
150
		throw new \RuntimeException('No creator found');
151
	}
152
153
	public function open(string $filePath, string $editorId = null): string {
154
		/** @var File $file */
155
		$file = $this->rootFolder->getUserFolder($this->userId)->get($filePath);
156
157
		if ($editorId === null) {
158
			$editorId = $this->findEditorForFile($file);
159
		}
160
		if (!array_key_exists($editorId, $this->editors)) {
161
			throw new \RuntimeException("Editor $editorId is unknown");
162
		}
163
164
		return $this->createToken($editorId, $file, $filePath);
165
	}
166
167
	private function findEditorForFile(File $file) {
168
		foreach ($this->editors as $editor) {
169
			if (in_array($file->getMimeType(), $editor->getMimetypes())) {
170
				return $editor->getId();
171
			}
172
		}
173
		throw new \RuntimeException('No default editor found for files mimetype');
174
	}
175
176
	public function edit(string $token): Response {
177
		try {
178
			/** @var IEditor $editor */
179
			$tokenObject = $this->getToken($token);
180
			if ($tokenObject->hasBeenAccessed()) {
181
				throw new \RuntimeException('Token has already been used and can only be used for followup requests');
182
			}
183
			$editor = $this->getEditor($tokenObject->getEditor());
184
			$this->accessToken($token);
185
		} catch (Throwable $throwable) {
186
			$this->invalidateToken($token);
187
			return new NotFoundResponse();
188
		}
189
		return $editor->open($tokenObject);
190
	}
191
192
	public function editSecure(File $file, string $editorId): TemplateResponse {
0 ignored issues
show
Unused Code introduced by
The parameter $editorId is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

192
	public function editSecure(File $file, /** @scrutinizer ignore-unused */ string $editorId): TemplateResponse {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $file is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

192
	public function editSecure(/** @scrutinizer ignore-unused */ File $file, string $editorId): TemplateResponse {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
193
		// TODO: Implementation in follow up
194
	}
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return OCP\AppFramework\Http\TemplateResponse. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
195
196
	private function getEditor($editorId): IEditor {
197
		if (!array_key_exists($editorId, $this->editors)) {
198
			throw new \RuntimeException('No editor found');
199
		}
200
		return $this->editors[$editorId];
201
	}
202
203
	public function getToken(string $token): IToken {
204
		$query = $this->connection->getQueryBuilder();
205
		$query->select('*')->from(self::TABLE_TOKENS)
206
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
207
		$result = $query->execute();
208
		if ($tokenRow = $result->fetch(FetchMode::ASSOCIATIVE)) {
209
			return new Token($this, $tokenRow);
210
		}
211
		throw new \RuntimeException('Failed to validate the token');
212
	}
213
214
	public function cleanup(): int {
215
		$query = $this->connection->getQueryBuilder();
216
		$query->delete(self::TABLE_TOKENS)
217
			->where($query->expr()->lt('timestamp', $query->createNamedParameter(time() - self::TOKEN_CLEANUP_TIME)));
218
		return $query->execute();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->execute() could return the type OCP\DB\IResult which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
219
	}
220
221
	public function refreshToken(string $token): bool {
222
		$query = $this->connection->getQueryBuilder();
223
		$query->update(self::TABLE_TOKENS)
224
			->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
225
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
226
		$result = $query->execute();
227
		return $result !== 0;
228
	}
229
230
231
	public function invalidateToken(string $token): bool {
232
		$query = $this->connection->getQueryBuilder();
233
		$query->delete(self::TABLE_TOKENS)
234
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
235
		$result = $query->execute();
236
		return $result !== 0;
237
	}
238
239
	public function accessToken(string $token): bool {
240
		$query = $this->connection->getQueryBuilder();
241
		$query->update(self::TABLE_TOKENS)
242
			->set('accessed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
243
			->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
244
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
245
		$result = $query->execute();
246
		return $result !== 0;
247
	}
248
249
	public function invokeTokenScope($userId): void {
250
		\OC_User::setIncognitoMode(true);
251
		\OC_User::setUserId($userId);
252
	}
253
254
	public function createToken($editorId, File $file, string $filePath, IShare $share = null): string {
255
		$token = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
256
		$query = $this->connection->getQueryBuilder();
257
		$query->insert(self::TABLE_TOKENS)
258
			->values([
259
				'token' => $query->createNamedParameter($token),
260
				'editor_id' => $query->createNamedParameter($editorId),
261
				'file_id' => $query->createNamedParameter($file->getId()),
262
				'file_path' => $query->createNamedParameter($filePath),
263
				'user_id' => $query->createNamedParameter($this->userId),
264
				'share_id' => $query->createNamedParameter($share !== null ? $share->getId(): null),
265
				'timestamp' => $query->createNamedParameter(time())
266
			]);
267
		$query->execute();
268
		return $token;
269
	}
270
271
	/**
272
	 * @param $userId
273
	 * @param $fileId
274
	 * @param null $filePath
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $filePath is correct as it would always require null to be passed?
Loading history...
275
	 * @return Node
276
	 * @throws NotFoundException
277
	 */
278
	public function getFileForToken($userId, $fileId, $filePath = null): Node {
279
		$userFolder = $this->rootFolder->getUserFolder($userId);
280
		if ($filePath !== null) {
0 ignored issues
show
introduced by
The condition $filePath !== null is always false.
Loading history...
281
			return $userFolder->get($filePath);
282
		}
283
		$files = $userFolder->getById($fileId);
284
		if (count($files) === 0) {
285
			throw new NotFoundException('File nound found by id ' . $fileId);
286
		}
287
		return $files[0];
288
	}
289
290
	public function isEnabled(): bool {
291
		if (!$this->encryptionManager->isEnabled()) {
292
			return true;
293
		}
294
295
		try {
296
			$moduleId = $this->encryptionManager->getDefaultEncryptionModuleId();
297
			$module = $this->encryptionManager->getEncryptionModule($moduleId);
298
			/** @var \OCA\Encryption\Util $util */
299
			$util = \OC::$server->get(\OCA\Encryption\Util::class);
300
			if ($module->isReadyForUser($this->userId) && $util->isMasterKeyEnabled()) {
301
				return true;
302
			}
303
		} catch (Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
304
		}
305
		return false;
306
	}
307
}
308