Completed
Pull Request — master (#132)
by Julius
96:58 queued 95:31
created

Operation::onEvent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 2
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Morris Jobke <[email protected]>
4
 *
5
 * @license GNU AGPL version 3 or any later version
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as
9
 * published by the Free Software Foundation, either version 3 of the
10
 * License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
 *
20
 */
21
22
namespace OCA\FilesAccessControl;
23
24
25
use OCA\WorkflowEngine\Entity\File;
26
use OCP\Files\ForbiddenException;
27
use OCP\Files\Storage\IStorage;
28
use OCP\IL10N;
29
use OCP\IURLGenerator;
30
use OCP\WorkflowEngine\IComplexOperation;
31
use OCP\WorkflowEngine\IManager;
32
use OCP\WorkflowEngine\IOperation;
33
use OCP\WorkflowEngine\IRuleMatcher;
34
use OCP\WorkflowEngine\ISpecificOperation;
35
use Symfony\Component\EventDispatcher\GenericEvent;
36
37
class Operation implements IOperation, IComplexOperation, ISpecificOperation {
38
	/** @var IManager */
39
	protected $manager;
40
41
	/** @var IL10N */
42
	protected $l;
43
44
	/** @var IURLGenerator */
45
	protected $urlGenerator;
46
47
	/** @var int */
48
	protected $nestingLevel = 0;
49
50
	/**
51
	 * @param IManager $manager
52
	 * @param IL10N $l
53
	 */
54
	public function __construct(IManager $manager, IL10N $l, IURLGenerator $urlGenerator) {
55
		$this->manager = $manager;
56
		$this->l = $l;
57
		$this->urlGenerator = $urlGenerator;
58
	}
59
60
	/**
61
	 * @param IStorage $storage
62
	 * @param string $path
63
	 * @throws ForbiddenException
64
	 */
65
	public function checkFileAccess(IStorage $storage, $path) {
66
		if (!$this->isBlockablePath($storage, $path) || $this->isCreatingSkeletonFiles() || $this->nestingLevel !== 0) {
67
			// Allow creating skeletons and theming
68
			// https://github.com/nextcloud/files_accesscontrol/issues/5
69
			// https://github.com/nextcloud/files_accesscontrol/issues/12
70
			return;
71
		}
72
73
		$this->nestingLevel++;
74
75
		$filePath = $this->translatePath($storage, $path);
76
		$ruleMatcher = $this->manager->getRuleMatcher();
77
		$ruleMatcher->setFileInfo($storage, $filePath);
78
		$match = $ruleMatcher->getMatchingOperations(self::class);
79
80
		$this->nestingLevel--;
81
82
		if (!empty($match)) {
83
			// All Checks of one operation matched: prevent access
84
			throw new ForbiddenException('Access denied', true);
85
		}
86
	}
87
88
	/**
89
	 * @param IStorage $storage
90
	 * @param string $path
91
	 * @return bool
92
	 */
93
	protected function isBlockablePath(IStorage $storage, $path) {
94
		if (property_exists($storage, 'mountPoint')) {
95
			$hasMountPoint = $storage instanceof StorageWrapper;
96
			if (!$hasMountPoint) {
97
				$ref = new \ReflectionClass($storage);
98
				$prop = $ref->getProperty('mountPoint');
99
				$hasMountPoint = $prop->isPublic();
100
			}
101
102
			if ($hasMountPoint) {
103
				/** @var StorageWrapper $storage */
104
				$fullPath = $storage->mountPoint . $path;
105
			} else {
106
				$fullPath = $path;
107
			}
108
		} else {
109
			$fullPath = $path;
110
		}
111
112
		if (substr_count($fullPath, '/') < 3) {
113
			return false;
114
		}
115
116
		// '', admin, 'files', 'path/to/file.txt'
117
		$segment = explode('/', $fullPath, 4);
118
119
		return isset($segment[2]) && in_array($segment[2], [
120
			'files',
121
			'thumbnails',
122
			'files_versions',
123
		]);
124
	}
125
126
	/**
127
	 * For thumbnails and versions we want to check the tags of the original file
128
	 *
129
	 * @param IStorage $storage
130
	 * @param string $path
131
	 * @return bool
132
	 */
133
	protected function translatePath(IStorage $storage, $path) {
134
		if (substr_count($path, '/') < 1) {
135
			return $path;
136
		}
137
138
		// 'files', 'path/to/file.txt'
139
		list($folder, $innerPath) = explode('/', $path, 2);
140
141
		if ($folder === 'files_versions') {
142
			$innerPath = substr($innerPath, 0, strrpos($innerPath, '.v'));
143
			return 'files/' . $innerPath;
144
		} else if ($folder === 'thumbnails') {
145
			list($fileId,) = explode('/', $innerPath, 2);
146
			$innerPath = $storage->getCache()->getPathById($fileId);
147
148
			if ($innerPath !== null) {
149
				return 'files/' . $innerPath;
150
			}
151
		}
152
153
		return $path;
154
	}
155
156
	/**
157
	 * Check if we are in the LoginController and if so, ignore the firewall
158
	 * @return bool
159
	 */
160
	protected function isCreatingSkeletonFiles() {
161
		$exception = new \Exception();
162
		$trace = $exception->getTrace();
163
164
		foreach ($trace as $step) {
165
			if (isset($step['class']) && $step['class'] === 'OC\Core\Controller\LoginController' &&
166
				isset($step['function']) && $step['function'] === 'tryLogin') {
167
				return true;
168
			}
169
		}
170
171
		return false;
172
	}
173
174
	/**
175
	 * @param string $name
176
	 * @param array[] $checks
177
	 * @param string $operation
178
	 * @throws \UnexpectedValueException
179
	 */
180
	public function validateOperation(string $name, array $checks, string $operation): void {
181
		if (empty($checks)) {
182
			throw new \UnexpectedValueException($this->l->t('No rule given'));
183
		}
184
	}
185
186
	/**
187
	 * returns a translated name to be presented in the web interface
188
	 *
189
	 * Example: "Automated tagging" (en), "Aŭtomata etikedado" (eo)
190
	 *
191
	 * @since 18.0.0
192
	 */
193
	public function getDisplayName(): string {
194
		return $this->l->t('Files access control');
195
	}
196
197
	/**
198
	 * returns a translated, descriptive text to be presented in the web interface.
199
	 *
200
	 * It should be short and precise.
201
	 *
202
	 * Example: "Tag based automatic deletion of files after a given time." (en)
203
	 *
204
	 * @since 18.0.0
205
	 */
206
	public function getDescription(): string {
207
		return $this->l->t('Block access to a file');
208
	}
209
210
	/**
211
	 * returns the URL to the icon of the operator for display in the web interface.
212
	 *
213
	 * Usually, the implementation would utilize the `imagePath()` method of the
214
	 * `\OCP\IURLGenerator` instance and simply return its result.
215
	 *
216
	 * Example implementation: return $this->urlGenerator->imagePath('myApp', 'cat.svg');
217
	 *
218
	 * @since 18.0.0
219
	 */
220
	public function getIcon(): string {
221
		return $this->urlGenerator->imagePath('files_accesscontrol', 'app.svg');
222
	}
223
224
	/**
225
	 * returns whether the operation can be used in the requested scope.
226
	 *
227
	 * Scope IDs are defined as constants in OCP\WorkflowEngine\IManager. At
228
	 * time of writing these are SCOPE_ADMIN and SCOPE_USER.
229
	 *
230
	 * For possibly unknown future scopes the recommended behaviour is: if
231
	 * user scope is permitted, the default behaviour should return `true`,
232
	 * otherwise `false`.
233
	 *
234
	 * @since 18.0.0
235
	 */
236
	public function isAvailableForScope(int $scope): bool {
237
		return $scope === IManager::SCOPE_ADMIN;
238
	}
239
240
	/**
241
	 * returns the id of the entity the operator is designed for
242
	 *
243
	 * Example: 'WorkflowEngine_Entity_File'
244
	 *
245
	 * @since 18.0.0
246
	 */
247
	public function getEntityId(): string {
248
		return File::class;
249
	}
250
251
	/**
252
	 * As IComplexOperation chooses the triggering events itself, a hint has
253
	 * to be shown to the user so make clear when this operation is becoming
254
	 * active. This method returns such a translated string.
255
	 *
256
	 * Example: "When a file is accessed" (en)
257
	 *
258
	 * @since 18.0.0
259
	 */
260
	public function getTriggerHint(): string {
261
		return $this->l->t('File is accessed');
262
	}
263
264
	public function onEvent(string $eventName, GenericEvent $event, IRuleMatcher $ruleMatcher): void {
265
		// Noop
266
	}
267
}
268