Passed
Push — master ( 90d2cb...d07838 )
by Julius
16:46 queued 13s
created

Manager::revertTokenScope()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
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
namespace OC\DirectEditing;
27
28
use Doctrine\DBAL\FetchMode;
29
use OC\Files\Node\Folder;
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;
0 ignored issues
show
Bug introduced by
The type \OCP\DirectEditing\IManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 IUserSession */
63
	private $userSession;
64
	/** @var ISecureRandom */
65
	private $random;
66
	/** @var string|null */
67
	private $userId;
68
	/** @var IRootFolder */
69
	private $rootFolder;
70
	/** @var IL10N */
71
	private $l10n;
72
	/** @var EncryptionManager */
73
	private $encryptionManager;
74
75
	public function __construct(
76
		ISecureRandom $random,
77
		IDBConnection $connection,
78
		IUserSession $userSession,
79
		IRootFolder $rootFolder,
80
		IFactory $l10nFactory,
81
		EncryptionManager $encryptionManager
82
	) {
83
		$this->random = $random;
84
		$this->connection = $connection;
85
		$this->userSession = $userSession;
86
		$this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
87
		$this->rootFolder = $rootFolder;
88
		$this->l10n = $l10nFactory->get('lib');
89
		$this->encryptionManager = $encryptionManager;
90
	}
91
92
	public function registerDirectEditor(IEditor $directEditor): void {
93
		$this->editors[$directEditor->getId()] = $directEditor;
94
	}
95
96
	public function getEditors(): array {
97
		return $this->editors;
98
	}
99
100
	public function getTemplates(string $editor, string $type): array {
101
		if (!array_key_exists($editor, $this->editors)) {
102
			throw new \RuntimeException('No matching editor found');
103
		}
104
		$templates = [];
105
		foreach ($this->editors[$editor]->getCreators() as $creator) {
106
			if ($creator->getId() === $type) {
107
				$templates = [
108
					'empty' => [
109
						'id' => 'empty',
110
						'title' => $this->l10n->t('Empty file'),
111
						'preview' => null
112
					]
113
				];
114
115
				if ($creator instanceof ACreateFromTemplate) {
116
					$templates = $creator->getTemplates();
117
				}
118
119
				$templates = array_map(function ($template) use ($creator) {
120
					$template['extension'] = $creator->getExtension();
121
					$template['mimetype'] = $creator->getMimetype();
122
					return $template;
123
				}, $templates);
124
			}
125
		}
126
		$return = [];
127
		$return['templates'] = $templates;
128
		return $return;
129
	}
130
131
	public function create(string $path, string $editorId, string $creatorId, $templateId = null): string {
132
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
133
		if ($userFolder->nodeExists($path)) {
134
			throw new \RuntimeException('File already exists');
135
		} else {
136
			if (!$userFolder->nodeExists(dirname($path))) {
137
				throw new \RuntimeException('Invalid path');
138
			}
139
			/** @var Folder $folder */
140
			$folder = $userFolder->get(dirname($path));
141
			$file = $folder->newFile(basename($path));
142
			$editor = $this->getEditor($editorId);
143
			$creators = $editor->getCreators();
144
			foreach ($creators as $creator) {
145
				if ($creator->getId() === $creatorId) {
146
					$creator->create($file, $creatorId, $templateId);
147
					return $this->createToken($editorId, $file, $path);
148
				}
149
			}
150
		}
151
152
		throw new \RuntimeException('No creator found');
153
	}
154
155
	public function open(string $filePath, string $editorId = null): string {
156
		/** @var File $file */
157
		$file = $this->rootFolder->getUserFolder($this->userId)->get($filePath);
158
159
		if ($editorId === null) {
160
			$editorId = $this->findEditorForFile($file);
161
		}
162
		if (!array_key_exists($editorId, $this->editors)) {
163
			throw new \RuntimeException("Editor $editorId is unknown");
164
		}
165
166
		return $this->createToken($editorId, $file, $filePath);
167
	}
168
169
	private function findEditorForFile(File $file) {
170
		foreach ($this->editors as $editor) {
171
			if (in_array($file->getMimeType(), $editor->getMimetypes())) {
172
				return $editor->getId();
173
			}
174
		}
175
		throw new \RuntimeException('No default editor found for files mimetype');
176
	}
177
178
	public function edit(string $token): Response {
179
		try {
180
			/** @var IEditor $editor */
181
			$tokenObject = $this->getToken($token);
182
			if ($tokenObject->hasBeenAccessed()) {
183
				throw new \RuntimeException('Token has already been used and can only be used for followup requests');
184
			}
185
			$editor = $this->getEditor($tokenObject->getEditor());
186
			$this->accessToken($token);
187
		} catch (Throwable $throwable) {
188
			$this->invalidateToken($token);
189
			return new NotFoundResponse();
190
		}
191
192
		try {
193
			$this->invokeTokenScope($tokenObject->getUser());
194
			return $editor->open($tokenObject);
195
		} finally {
196
			$this->revertTokenScope();
197
		}
198
	}
199
200
	public function editSecure(File $file, string $editorId): TemplateResponse {
201
		// TODO: Implementation in follow up
202
	}
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...
203
204
	private function getEditor($editorId): IEditor {
205
		if (!array_key_exists($editorId, $this->editors)) {
206
			throw new \RuntimeException('No editor found');
207
		}
208
		return $this->editors[$editorId];
209
	}
210
211
	public function getToken(string $token): IToken {
212
		$query = $this->connection->getQueryBuilder();
213
		$query->select('*')->from(self::TABLE_TOKENS)
214
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
215
		$result = $query->execute();
216
		if ($tokenRow = $result->fetch(FetchMode::ASSOCIATIVE)) {
217
			return new Token($this, $tokenRow);
218
		}
219
		throw new \RuntimeException('Failed to validate the token');
220
	}
221
222
	public function cleanup(): int {
223
		$query = $this->connection->getQueryBuilder();
224
		$query->delete(self::TABLE_TOKENS)
225
			->where($query->expr()->lt('timestamp', $query->createNamedParameter(time() - self::TOKEN_CLEANUP_TIME)));
226
		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...
227
	}
228
229
	public function refreshToken(string $token): bool {
230
		$query = $this->connection->getQueryBuilder();
231
		$query->update(self::TABLE_TOKENS)
232
			->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
233
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
234
		$result = $query->execute();
235
		return $result !== 0;
236
	}
237
238
239
	public function invalidateToken(string $token): bool {
240
		$query = $this->connection->getQueryBuilder();
241
		$query->delete(self::TABLE_TOKENS)
242
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
243
		$result = $query->execute();
244
		return $result !== 0;
245
	}
246
247
	public function accessToken(string $token): bool {
248
		$query = $this->connection->getQueryBuilder();
249
		$query->update(self::TABLE_TOKENS)
250
			->set('accessed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
251
			->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
252
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
253
		$result = $query->execute();
254
		return $result !== 0;
255
	}
256
257
	public function invokeTokenScope($userId): void {
258
		\OC_User::setIncognitoMode(true);
259
		\OC_User::setUserId($userId);
260
	}
261
262
	public function revertTokenScope(): void {
263
		$this->userSession->setUser(null);
264
		\OC_User::setIncognitoMode(false);
265
	}
266
267
	public function createToken($editorId, File $file, string $filePath, IShare $share = null): string {
268
		$token = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
269
		$query = $this->connection->getQueryBuilder();
270
		$query->insert(self::TABLE_TOKENS)
271
			->values([
272
				'token' => $query->createNamedParameter($token),
273
				'editor_id' => $query->createNamedParameter($editorId),
274
				'file_id' => $query->createNamedParameter($file->getId()),
275
				'file_path' => $query->createNamedParameter($filePath),
276
				'user_id' => $query->createNamedParameter($this->userId),
277
				'share_id' => $query->createNamedParameter($share !== null ? $share->getId(): null),
278
				'timestamp' => $query->createNamedParameter(time())
279
			]);
280
		$query->execute();
281
		return $token;
282
	}
283
284
	/**
285
	 * @param $userId
286
	 * @param $fileId
287
	 * @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...
288
	 * @return Node
289
	 * @throws NotFoundException
290
	 */
291
	public function getFileForToken($userId, $fileId, $filePath = null): Node {
292
		$userFolder = $this->rootFolder->getUserFolder($userId);
293
		if ($filePath !== null) {
0 ignored issues
show
introduced by
The condition $filePath !== null is always false.
Loading history...
294
			return $userFolder->get($filePath);
295
		}
296
		$files = $userFolder->getById($fileId);
297
		if (count($files) === 0) {
298
			throw new NotFoundException('File nound found by id ' . $fileId);
299
		}
300
		return $files[0];
301
	}
302
303
	public function isEnabled(): bool {
304
		if (!$this->encryptionManager->isEnabled()) {
305
			return true;
306
		}
307
308
		try {
309
			$moduleId = $this->encryptionManager->getDefaultEncryptionModuleId();
310
			$module = $this->encryptionManager->getEncryptionModule($moduleId);
311
			/** @var \OCA\Encryption\Util $util */
312
			$util = \OC::$server->get(\OCA\Encryption\Util::class);
313
			if ($module->isReadyForUser($this->userId) && $util->isMasterKeyEnabled()) {
314
				return true;
315
			}
316
		} catch (Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
317
		}
318
		return false;
319
	}
320
}
321