Passed
Push — master ( 6b97f6...a6ae80 )
by Blizzz
13:11 queued 11s
created

Manager   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 36
eloc 101
c 3
b 0
f 0
dl 0
loc 189
rs 9.52

18 Methods

Rating   Name   Duplication   Size   Complexity  
A cleanup() 0 5 1
A invokeTokenScope() 0 3 1
A getTemplates() 0 13 5
A refreshToken() 0 7 1
A open() 0 13 5
A findEditorForFile() 0 7 3
A create() 0 12 3
A getToken() 0 9 2
A editSecure() 0 1 1
A getFileForToken() 0 3 1
A __construct() 0 10 2
A accessToken() 0 8 1
A getEditors() 0 2 1
A createToken() 0 14 2
A invalidateToken() 0 6 1
A edit() 0 15 3
A getEditor() 0 5 2
A registerDirectEditor() 0 2 1
1
<?php
2
/**
3
 * @copyright Copyright (c) 2019 Julius Härtl <[email protected]>
4
 *
5
 * @author Julius Härtl <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OC\DirectEditing;
25
26
use Doctrine\DBAL\FetchMode;
27
use OCP\AppFramework\Http\NotFoundResponse;
28
use OCP\AppFramework\Http\Response;
29
use OCP\AppFramework\Http\TemplateResponse;
30
use OCP\DB\QueryBuilder\IQueryBuilder;
31
use OCP\DirectEditing\ACreateFromTemplate;
32
use OCP\DirectEditing\IEditor;
33
use \OCP\DirectEditing\IManager;
34
use OCP\DirectEditing\IToken;
35
use OCP\DirectEditing\RegisterDirectEditorEvent;
36
use OCP\EventDispatcher\IEventDispatcher;
37
use OCP\Files\File;
38
use OCP\Files\IRootFolder;
39
use OCP\Files\NotFoundException;
40
use OCP\IDBConnection;
41
use OCP\IUserSession;
42
use OCP\Security\ISecureRandom;
43
use OCP\Share\IShare;
44
45
class Manager implements IManager {
46
47
	private const TOKEN_CLEANUP_TIME = 12 * 60 * 60 ;
48
49
	public const TABLE_TOKENS = 'direct_edit';
50
51
	/** @var IEditor[] */
52
	private $editors = [];
53
54
	/** @var IDBConnection */
55
	private $connection;
56
	/**
57
	 * @var ISecureRandom
58
	 */
59
	private $random;
60
	private $userId;
61
	private $rootFolder;
62
63
	public function __construct(
64
		ISecureRandom $random,
65
		IDBConnection $connection,
66
		IUserSession $userSession,
67
		IRootFolder $rootFolder
68
	) {
69
		$this->random = $random;
70
		$this->connection = $connection;
71
		$this->userId = $userSession->getUser() ? $userSession->getUser()->getUID() : null;
72
		$this->rootFolder = $rootFolder;
73
	}
74
75
	public function registerDirectEditor(IEditor $directEditor): void {
76
		$this->editors[$directEditor->getId()] = $directEditor;
77
	}
78
79
	public function getEditors(): array {
80
		return $this->editors;
81
	}
82
83
	public function getTemplates(string $editor, string $type): array {
84
		if (!array_key_exists($editor, $this->editors)) {
85
			throw new \RuntimeException('No matching editor found');
86
		}
87
		$templates = [];
88
		foreach ($this->editors[$editor]->getCreators() as $creator) {
89
			if ($creator instanceof ACreateFromTemplate && $creator->getId() === $type) {
90
				$templates = $creator->getTemplates();
91
			}
92
		}
93
		$return = [];
94
		$return['templates'] =  $templates;
95
		return $return;
96
	}
97
98
	public function create(string $path, string $editorId, string $creatorId, $templateId = null): string {
99
		$userFolder = $this->rootFolder->getUserFolder($this->userId);
100
		$file = $userFolder->newFile($path);
101
		$editor = $this->getEditor($editorId);
102
		$creators = $editor->getCreators();
103
		foreach ($creators as $creator) {
104
			if ($creator->getId() === $creatorId) {
105
				$creator->create($file, $creatorId, $templateId);
106
				return $this->createToken($editorId, $file);
107
			}
108
		}
109
		throw new \RuntimeException('No creator found');
110
	}
111
112
	public function open(int $fileId, string $editorId = null): string {
113
		$file = $this->rootFolder->getUserFolder($this->userId)->getById($fileId);
114
		if (count($file) === 0 || !($file[0] instanceof File) || $file === null) {
115
			throw new NotFoundException();
116
		}
117
		/** @var File $file */
118
		$file = $file[0];
119
120
		if ($editorId === null) {
121
			$editorId = $this->findEditorForFile($file);
122
		}
123
124
		return $this->createToken($editorId, $file);
125
	}
126
127
	private function findEditorForFile(File $file) {
128
		foreach ($this->editors as $editor) {
129
			if (in_array($file->getMimeType(), $editor->getMimetypes())) {
130
				return $editor->getId();
131
			}
132
		}
133
		throw new \RuntimeException('No default editor found for files mimetype');
134
	}
135
136
	public function edit(string $token): Response {
137
		try {
138
			/** @var IEditor $editor */
139
			$tokenObject = $this->getToken($token);
140
			if ($tokenObject->hasBeenAccessed()) {
141
				throw new \RuntimeException('Token has already been used and can only be used for followup requests');
142
			}
143
			$editor = $this->getEditor($tokenObject->getEditor());
144
			$this->accessToken($token);
145
146
		} catch (\Throwable $throwable) {
147
			$this->invalidateToken($token);
148
			return new NotFoundResponse();
149
		}
150
		return $editor->open($tokenObject);
151
	}
152
153
	public function editSecure(File $file, string $editorId): TemplateResponse {
0 ignored issues
show
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

153
	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...
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

153
	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...
154
		// TODO: Implementation in follow up
155
	}
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...
156
157
	private function getEditor($editorId): IEditor {
158
		if (!array_key_exists($editorId, $this->editors)) {
159
			throw new \RuntimeException('No editor found');
160
		}
161
		return $this->editors[$editorId];
162
	}
163
164
	public function getToken(string $token): IToken {
165
		$query = $this->connection->getQueryBuilder();
166
		$query->select('*')->from(self::TABLE_TOKENS)
167
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
168
		$result = $query->execute();
169
		if ($tokenRow = $result->fetch(FetchMode::ASSOCIATIVE)) {
170
			return new Token($this, $tokenRow);
171
		}
172
		throw new \RuntimeException('Failed to validate the token');
173
	}
174
175
	public function cleanup(): int {
176
		$query = $this->connection->getQueryBuilder();
177
		$query->delete(self::TABLE_TOKENS)
178
			->where($query->expr()->lt('timestamp', $query->createNamedParameter(time() - self::TOKEN_CLEANUP_TIME)));
179
		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...
180
	}
181
182
	public function refreshToken(string $token): bool {
183
		$query = $this->connection->getQueryBuilder();
184
		$query->update(self::TABLE_TOKENS)
185
			->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
186
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
187
		$result = $query->execute();
188
		return $result !== 0;
189
	}
190
191
192
	public function invalidateToken(string $token): bool {
193
		$query = $this->connection->getQueryBuilder();
194
		$query->delete(self::TABLE_TOKENS)
195
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
196
		$result = $query->execute();
197
		return $result !== 0;
198
	}
199
200
	public function accessToken(string $token): bool {
201
		$query = $this->connection->getQueryBuilder();
202
		$query->update(self::TABLE_TOKENS)
203
			->set('accessed', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
204
			->set('timestamp', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
205
			->where($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)));
206
		$result = $query->execute();
207
		return $result !== 0;
208
	}
209
210
	public function invokeTokenScope($userId): void {
211
		\OC_User::setIncognitoMode(true);
212
		\OC_User::setUserId($userId);
213
	}
214
215
	public function createToken($editorId, File $file, IShare $share = null): string {
216
		$token = $this->random->generate(64, ISecureRandom::CHAR_HUMAN_READABLE);
217
		$query = $this->connection->getQueryBuilder();
218
		$query->insert(self::TABLE_TOKENS)
219
			->values([
220
				'token' => $query->createNamedParameter($token),
221
				'editor_id' => $query->createNamedParameter($editorId),
222
				'file_id' => $query->createNamedParameter($file->getId()),
223
				'user_id' => $query->createNamedParameter($this->userId),
224
				'share_id' => $query->createNamedParameter($share !== null ? $share->getId(): null),
225
				'timestamp' => $query->createNamedParameter(time())
226
			]);
227
		$query->execute();
228
		return $token;
229
	}
230
231
	public function getFileForToken($userId, $fileId) {
232
		$userFolder = $this->rootFolder->getUserFolder($userId);
233
		return $userFolder->getById($fileId)[0];
234
	}
235
236
}
237