Passed
Pull Request — master (#77)
by Daniel
28:07
created

Website::assertOwnedBy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * CMS Pico - Create websites using Pico CMS for Nextcloud.
4
 *
5
 * @copyright Copyright (c) 2017, Maxence Lange (<[email protected]>)
6
 * @copyright Copyright (c) 2019, Daniel Rudolf (<[email protected]>)
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
24
declare(strict_types=1);
25
26
namespace OCA\CMSPico\Model;
27
28
use OCA\CMSPico\AppInfo\Application;
29
use OCA\CMSPico\Exceptions\TemplateNotCompatibleException;
30
use OCA\CMSPico\Exceptions\TemplateNotFoundException;
31
use OCA\CMSPico\Exceptions\ThemeNotCompatibleException;
32
use OCA\CMSPico\Exceptions\ThemeNotFoundException;
33
use OCA\CMSPico\Exceptions\WebsiteForeignOwnerException;
34
use OCA\CMSPico\Exceptions\WebsiteInvalidDataException;
35
use OCA\CMSPico\Exceptions\WebsiteInvalidFilesystemException;
36
use OCA\CMSPico\Exceptions\WebsiteInvalidOwnerException;
37
use OCA\CMSPico\Exceptions\WebsiteNotPermittedException;
38
use OCA\CMSPico\Files\StorageFile;
39
use OCA\CMSPico\Files\StorageFolder;
40
use OCA\CMSPico\Service\MiscService;
41
use OCA\CMSPico\Service\TemplatesService;
42
use OCA\CMSPico\Service\ThemesService;
43
use OCA\CMSPico\Service\WebsitesService;
44
use OCP\Files\Folder as OCFolder;
45
use OCP\Files\InvalidPathException;
46
use OCP\Files\Node as OCNode;
47
use OCP\Files\NotFoundException;
48
use OCP\Files\NotPermittedException;
49
use OCP\IConfig;
50
use OCP\IGroupManager;
51
use OCP\IL10N;
52
use OCP\IURLGenerator;
53
use OCP\IUserManager;
54
55
class Website extends WebsiteCore
56
{
57
	/** @var int */
58
	const SITE_LENGTH_MIN = 3;
59
60
	/** @var int */
61
	const SITE_LENGTH_MAX = 255;
62
63
	/** @var string */
64
	const SITE_REGEX = '^[a-z][a-z0-9_-]+[a-z0-9]$';
65
66
	/** @var int */
67
	const NAME_LENGTH_MIN = 3;
68
69
	/** @var int */
70
	const NAME_LENGTH_MAX = 255;
71
72
	/** @var IConfig */
73
	private $config;
74
75
	/** @var IL10N */
76
	private $l10n;
77
78
	/** @var IUserManager */
79
	private $userManager;
80
81
	/** @var IGroupManager */
82
	private $groupManager;
83
84
	/** @var IURLGenerator */
85
	private $urlGenerator;
86
87
	/** @var WebsitesService */
88
	private $websitesService;
89
90
	/** @var ThemesService */
91
	private $themesService;
92
93
	/** @var TemplatesService */
94
	private $templatesService;
95
96
	/** @var MiscService */
97
	private $miscService;
98
99
	/** @var StorageFolder */
100
	private $folder;
101
102
	/**
103
	 * Website constructor.
104
	 *
105
	 * @param array|string|null $data
106
	 */
107 10
	public function __construct($data = null)
108
	{
109 10
		$this->config = \OC::$server->getConfig();
110 10
		$this->l10n = \OC::$server->getL10N(Application::APP_NAME);
111 10
		$this->userManager = \OC::$server->getUserManager();
112 10
		$this->groupManager = \OC::$server->getGroupManager();
113 10
		$this->urlGenerator = \OC::$server->getURLGenerator();
114 10
		$this->websitesService = \OC::$server->query(WebsitesService::class);
115 10
		$this->themesService = \OC::$server->query(ThemesService::class);
116 10
		$this->templatesService = \OC::$server->query(TemplatesService::class);
117 10
		$this->miscService = \OC::$server->query(MiscService::class);
118
119 10
		parent::__construct($data);
120 10
	}
121
122
	/**
123
	 * @return string
124
	 */
125 2
	public function getTimeZone(): string
126
	{
127 2
		$serverTimeZone = date_default_timezone_get() ?: 'UTC';
128 2
		return $this->config->getUserValue($this->getUserId(), 'core', 'timezone', $serverTimeZone);
129
	}
130
131
	/**
132
	 * @param string $path
133
	 * @param array  $meta
134
	 *
135
	 * @throws InvalidPathException
136
	 * @throws WebsiteInvalidFilesystemException
137
	 * @throws WebsiteNotPermittedException
138
	 * @throws NotPermittedException
139
	 */
140 2
	public function assertViewerAccess(string $path, array $meta = [])
141
	{
142 2
		$exceptionClass = WebsiteNotPermittedException::class;
143 2
		if ($this->getType() === self::TYPE_PUBLIC) {
144 2
			if (empty($meta['access'])) {
145 2
				return;
146
			}
147
148
			$groupAccess = $meta['access'];
149
			if (!is_array($groupAccess)) {
150
				$groupAccess = explode(',', strtolower($groupAccess));
151
			}
152
153
			foreach ($groupAccess as $group) {
154
				if ($group === 'public') {
155
					return;
156
				} elseif ($group === 'private') {
157
					continue;
158
				}
159
160
				if ($this->getViewer() && $this->groupManager->groupExists($group)) {
161
					if ($this->groupManager->isInGroup($this->getViewer(), $group)) {
162
						return;
163
					}
164
				}
165
			}
166
167
			$exceptionClass = NotPermittedException::class;
168
		}
169
170
		if ($this->getViewer()) {
171
			if ($this->getViewer() === $this->getUserId()) {
172
				return;
173
			}
174
175
			/** @var OCFolder $viewerOCFolder */
176
			$viewerOCFolder = \OC::$server->getUserFolder($this->getViewer());
177
			$viewerAccessClosure = function (OCNode $node) use ($viewerOCFolder) {
178
				$nodeId = $node->getId();
179
180
				$viewerNodes = $viewerOCFolder->getById($nodeId);
181
				foreach ($viewerNodes as $viewerNode) {
182
					if ($viewerNode->isReadable()) {
183
						return true;
184
					}
185
				}
186
187
				return false;
188
			};
189
190
			$websiteFolder = $this->getWebsiteFolder();
191
192
			$path = $this->miscService->normalizePath($path);
193
			while ($path && ($path !== '.')) {
194
				try {
195
					/** @var StorageFile|StorageFolder $file */
196
					$file = $websiteFolder->get($path);
197
				} catch (NotFoundException $e) {
198
					$file = null;
199
				}
200
201
				if ($file) {
202
					if ($viewerAccessClosure($file->getOCNode())) {
203
						return;
204
					}
205
206
					throw new $exceptionClass();
207
				}
208
209
				$path = dirname($path);
210
			}
211
212
			if ($viewerAccessClosure($websiteFolder->getOCNode())) {
213
				return;
214
			}
215
		}
216
217
		throw new $exceptionClass();
218
	}
219
220
	/**
221
	 * @throws WebsiteInvalidOwnerException
222
	 */
223 5
	public function assertValidOwner()
224
	{
225 5
		$user = $this->userManager->get($this->getUserId());
226 5
		if ($user === null) {
227
			throw new WebsiteInvalidOwnerException();
228
		}
229 5
		if (!$user->isEnabled()) {
230
			throw new WebsiteInvalidOwnerException();
231
		}
232 5
		if (!$this->websitesService->isUserAllowed($this->getUserId())) {
233
			throw new WebsiteInvalidOwnerException();
234
		}
235 5
	}
236
237
	/**
238
	 * @throws WebsiteInvalidDataException
239
	 */
240 4
	public function assertValidName()
241
	{
242 4
		if (strlen($this->getName()) < self::NAME_LENGTH_MIN) {
243
			throw new WebsiteInvalidDataException('name', $this->l10n->t('The name of the website must be longer.'));
244
		}
245 4
		if (strlen($this->getName()) > self::NAME_LENGTH_MAX) {
246
			throw new WebsiteInvalidDataException('name', $this->l10n->t('The name of the website is too long.'));
247
		}
248 4
	}
249
250
	/**
251
	 * @throws WebsiteInvalidDataException
252
	 */
253 4
	public function assertValidSite()
254
	{
255 4
		if (strlen($this->getSite()) < self::SITE_LENGTH_MIN) {
256
			$errorMessage = $this->l10n->t('The identifier of the website must be longer.');
257
			throw new WebsiteInvalidDataException('site', $errorMessage);
258
		}
259 4
		if (strlen($this->getSite()) > self::SITE_LENGTH_MAX) {
260
			$errorMessage = $this->l10n->t('The identifier of the website is too long.');
261
			throw new WebsiteInvalidDataException('site', $errorMessage);
262
		}
263 4
		if (preg_match('/' . self::SITE_REGEX . '/', $this->getSite()) !== 1) {
264
			$errorMessage = $this->l10n->t('The identifier of the website can only contains alpha numeric chars.');
265
			throw new WebsiteInvalidDataException('site', $errorMessage);
266
		}
267 4
	}
268
269
	/**
270
	 * @throws WebsiteInvalidDataException
271
	 */
272 3
	public function assertValidPath()
273
	{
274
		try {
275 3
			$path = $this->miscService->normalizePath($this->getPath());
276 3
			if ($path === '') {
277 3
				throw new InvalidPathException();
278
			}
279
		} catch (InvalidPathException $e) {
280
			throw new WebsiteInvalidDataException(
281
				'path',
282
				$this->l10n->t('The path of the website is invalid.')
283
			);
284
		}
285
286 3
		$userFolder = new StorageFolder(\OC::$server->getUserFolder($this->getUserId()));
287
288
		try {
289 3
			$websiteBaseFolder = $userFolder->getFolder(dirname($path));
290
291
			try {
292 3
				$websiteFolder = $websiteBaseFolder->getFolder(basename($path));
293
294 1
				if (!$websiteFolder->isLocal()) {
295
					throw new WebsiteInvalidDataException(
296
						'path',
297 1
						$this->l10n->t('The website\'s path is stored on a non-local storage.')
298
					);
299
				}
300 3
			} catch (NotFoundException $e) {
301 3
				if (!$websiteBaseFolder->isLocal()) {
302
					throw new WebsiteInvalidDataException(
303
						'path',
304 3
						$this->l10n->t('The website\'s path is stored on a non-local storage.')
305
					);
306
				}
307
			}
308
		} catch (InvalidPathException $e) {
309
			throw new WebsiteInvalidDataException(
310
				'path',
311
				$this->l10n->t('Parent folder of the website\'s path not found.')
312
			);
313
		} catch (NotFoundException $e) {
314
			throw new WebsiteInvalidDataException(
315
				'path',
316
				$this->l10n->t('Parent folder of the website\'s path not found.')
317
			);
318
		}
319 3
	}
320
321
	/**
322
	 * @throws ThemeNotFoundException
323
	 * @throws ThemeNotCompatibleException
324
	 */
325 3
	public function assertValidTheme()
326
	{
327 3
		$this->themesService->assertValidTheme($this->getTheme());
328 3
	}
329
330
	/**
331
	 * @throws TemplateNotFoundException
332
	 * @throws TemplateNotCompatibleException
333
	 */
334 3
	public function assertValidTemplate()
335
	{
336 3
		$this->templatesService->assertValidTemplate($this->getTemplateSource());
337 3
	}
338
339
	/**
340
	 * @param string $userId
341
	 *
342
	 * @throws WebsiteForeignOwnerException
343
	 */
344 2
	public function assertOwnedBy($userId)
345
	{
346 2
		if ($this->getUserId() !== $userId) {
347 1
			throw new WebsiteForeignOwnerException();
348
		}
349 2
	}
350
351
	/**
352
	 * @return StorageFolder
353
	 * @throws WebsiteInvalidFilesystemException
354
	 */
355 2
	public function getWebsiteFolder(): StorageFolder
356
	{
357 2
		if ($this->folder !== null) {
358
			try {
359
				// NC doesn't guarantee that mounts are present for the whole request lifetime
360
				// for example, if you call \OC\Files\Utils\Scanner::scan(), all mounts are reset
361
				// this makes OCNode instances, which rely on mounts of different users than the current, unusable
362
				// by calling OCFolder::get('') we can detect this situation and re-init the required mounts
363 2
				$this->folder->get('');
364
			} catch (\Exception $e) {
365
				$this->folder = null;
366
			}
367
		}
368
369 2
		if ($this->folder === null) {
370
			try {
371 2
				$ocUserFolder = \OC::$server->getUserFolder($this->getUserId());
372 2
				$userFolder = new StorageFolder($ocUserFolder);
373
374 2
				$websiteFolder = $userFolder->getFolder($this->getPath());
375 2
				$this->folder = $websiteFolder->fakeRoot();
376
			} catch (InvalidPathException $e) {
377
				throw new WebsiteInvalidFilesystemException($e);
378
			} catch (NotFoundException $e) {
379
				throw new WebsiteInvalidFilesystemException($e);
380
			}
381
		}
382
383 2
		return $this->folder;
384
	}
385
386
	/**
387
	 * @return string
388
	 * @throws WebsiteInvalidFilesystemException
389
	 */
390 2
	public function getWebsitePath(): string
391
	{
392
		try {
393 2
			return $this->getWebsiteFolder()->getLocalPath() . '/';
394
		} catch (InvalidPathException $e) {
395
			throw new WebsiteInvalidFilesystemException($e);
396
		} catch (NotFoundException $e) {
397
			throw new WebsiteInvalidFilesystemException($e);
398
		}
399
	}
400
401
	/**
402
	 * @return string
403
	 */
404 2
	public function getWebsiteUrl(): string
405
	{
406 2
		if (!$this->getProxyRequest()) {
407 2
			$route = Application::APP_NAME . '.Pico.getPage';
408 2
			$parameters = [ 'site' => $this->getSite(), 'page' => '' ];
409 2
			return $this->urlGenerator->linkToRoute($route, $parameters) . '/';
410
		} else {
411
			return \OC::$WEBROOT . '/sites/' . urlencode($this->getSite()) . '/';
412
		}
413
	}
414
}
415