Passed
Push — master ( 2b192e...6bda2c )
by Roeland
10:49 queued 10s
created

Manager::isEnabled()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 7
nop 0
dl 0
loc 16
rs 9.6111
c 0
b 0
f 0
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 OCP\AppFramework\Http\NotFoundResponse;
31
use OCP\AppFramework\Http\Response;
32
use OCP\AppFramework\Http\TemplateResponse;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
use OCP\DirectEditing\ACreateFromTemplate;
35
use OCP\DirectEditing\IEditor;
36
use \OCP\DirectEditing\IManager;
37
use OCP\DirectEditing\IToken;
38
use OCP\Encryption\IManager as EncryptionManager;
39
use OCP\Files\File;
40
use OCP\Files\IRootFolder;
41
use OCP\Files\Node;
42
use OCP\Files\NotFoundException;
43
use OCP\IDBConnection;
44
use OCP\IL10N;
45
use OCP\IUserSession;
46
use OCP\L10N\IFactory;
47
use OCP\Security\ISecureRandom;
48
use OCP\Share\IShare;
49
use Throwable;
50
use function array_key_exists;
51
use function in_array;
52
53
class Manager implements IManager {
54
	private const TOKEN_CLEANUP_TIME = 12 * 60 * 60 ;
55
56
	public const TABLE_TOKENS = 'direct_edit';
57
58
	/** @var IEditor[] */
59
	private $editors = [];
60
	/** @var IDBConnection */
61
	private $connection;
62
	/** @var ISecureRandom */
63
	private $random;
64
	/** @var string|null */
65
	private $userId;
66
	/** @var IRootFolder */
67
	private $rootFolder;
68
	/** @var IL10N */
69
	private $l10n;
70
	/** @var EncryptionManager */
71
	private $encryptionManager;
72
73
	public function __construct(
74
		ISecureRandom $random,
75
		IDBConnection $connection,
76
		IUserSession $userSession,
77
		IRootFolder $rootFolder,
78
		IFactory $l10nFactory,
79
		EncryptionManager $encryptionManager
80
	) {
81
		$this->random = $random;
82
		$this->connection = $connection;
83
		$this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
84
		$this->rootFolder = $rootFolder;
85
		$this->l10n = $l10nFactory->get('core');
86
		$this->encryptionManager = $encryptionManager;
87
	}
88
89
	public function registerDirectEditor(IEditor $directEditor): void {
90
		$this->editors[$directEditor->getId()] = $directEditor;
91
	}
92
93
	public function getEditors(): array {
94
		return $this->editors;
95
	}
96
97
	public function getTemplates(string $editor, string $type): array {
98
		if (!array_key_exists($editor, $this->editors)) {
99
			throw new \RuntimeException('No matching editor found');
100
		}
101
		$templates = [];
102
		foreach ($this->editors[$editor]->getCreators() as $creator) {
103
			if ($creator->getId() === $type) {
104
				$templates = [
105
					'empty' => [
106
						'id' => 'empty',
107
						'title' => $this->l10n->t('Empty file'),
108
						'preview' => null
109
					]
110
				];
111
112
				if ($creator instanceof ACreateFromTemplate) {
113
					$templates = $creator->getTemplates();
114
				}
115
116
				$templates = array_map(function ($template) use ($creator) {
117
					$template['extension'] = $creator->getExtension();
118
					$template['mimetype'] = $creator->getMimetype();
119
					return $template;
120
				}, $templates);
121
			}
122
		}
123
		$return = [];
124
		$return['templates'] =  $templates;
125
		return $return;
126
	}
127
128
	public function create(string $path, string $editorId, string $creatorId, $templateId = null): string {
129
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
130
		if ($userFolder->nodeExists($path)) {
131
			throw new \RuntimeException('File already exists');
132
		} else {
133
			$file = $userFolder->newFile($path);
134
			$editor = $this->getEditor($editorId);
135
			$creators = $editor->getCreators();
136
			foreach ($creators as $creator) {
137
				if ($creator->getId() === $creatorId) {
138
					$creator->create($file, $creatorId, $templateId);
139
					return $this->createToken($editorId, $file, $path);
140
				}
141
			}
142
		}
143
144
		throw new \RuntimeException('No creator found');
145
	}
146
147
	public function open(string $filePath, string $editorId = null): string {
148
		/** @var File $file */
149
		$file = $this->rootFolder->getUserFolder($this->userId)->get($filePath);
150
151
		if ($editorId === null) {
152
			$editorId = $this->findEditorForFile($file);
153
		}
154
		if (!array_key_exists($editorId, $this->editors)) {
155
			throw new \RuntimeException("Editor $editorId is unknown");
156
		}
157
158
		return $this->createToken($editorId, $file, $filePath);
159
	}
160
161
	private function findEditorForFile(File $file) {
162
		foreach ($this->editors as $editor) {
163
			if (in_array($file->getMimeType(), $editor->getMimetypes())) {
164
				return $editor->getId();
165
			}
166
		}
167
		throw new \RuntimeException('No default editor found for files mimetype');
168
	}
169
170
	public function edit(string $token): Response {
171
		try {
172
			/** @var IEditor $editor */
173
			$tokenObject = $this->getToken($token);
174
			if ($tokenObject->hasBeenAccessed()) {
175
				throw new \RuntimeException('Token has already been used and can only be used for followup requests');
176
			}
177
			$editor = $this->getEditor($tokenObject->getEditor());
178
			$this->accessToken($token);
179
		} catch (Throwable $throwable) {
180
			$this->invalidateToken($token);
181
			return new NotFoundResponse();
182
		}
183
		return $editor->open($tokenObject);
184
	}
185
186
	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

186
	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

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