Passed
Pull Request — master (#99)
by Daniel
26:12 queued 03:19
created

Website::getWebsiteFolder()   A

Complexity

Conditions 5
Paths 18

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.7283

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 27
ccs 9
cts 13
cp 0.6923
rs 9.4888
cc 5
nc 18
nop 0
crap 5.7283
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
	public const SITE_LENGTH_MIN = 3;
59
60
	/** @var int */
61
	public const SITE_LENGTH_MAX = 255;
62
63
	/** @var string */
64
	public const SITE_REGEX = '^[a-z0-9][a-z0-9_-]+[a-z0-9]$';
65
66
	/** @var int */
67
	public const NAME_LENGTH_MIN = 3;
68
69
	/** @var int */
70
	public 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 = []): void
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
			$groupPageAccess = $meta['access'];
149
			if (!is_array($groupPageAccess)) {
150
				$groupPageAccess = explode(',', $groupPageAccess);
151
			}
152
153
			foreach ($groupPageAccess as $group) {
154
				$group = trim($group);
155
156
				if ($group === 'public') {
157
					return;
158
				} elseif ($group === 'private') {
159
					continue;
160
				}
161
162
				if ($this->getViewer() && $this->groupManager->groupExists($group)) {
163
					if ($this->groupManager->isInGroup($this->getViewer(), $group)) {
164
						return;
165
					}
166
				}
167
			}
168
169
			$exceptionClass = NotPermittedException::class;
170
		}
171
172
		if ($this->getViewer()) {
173
			if ($this->getViewer() === $this->getUserId()) {
174
				return;
175
			}
176
177
			$groupAccess = $this->getOption('group_access') ?? [];
178
			foreach ($groupAccess as $group) {
179
				if ($this->groupManager->groupExists($group)) {
180
					if ($this->groupManager->isInGroup($this->getViewer(), $group)) {
181
						return;
182
					}
183
				}
184
			}
185
186
			/** @var OCFolder $viewerOCFolder */
187
			$viewerOCFolder = \OC::$server->getUserFolder($this->getViewer());
188
			$viewerAccessClosure = function (OCNode $node) use ($viewerOCFolder) {
189
				$nodeId = $node->getId();
190
191
				$viewerNodes = $viewerOCFolder->getById($nodeId);
192
				foreach ($viewerNodes as $viewerNode) {
193
					if ($viewerNode->isReadable()) {
194
						return true;
195
					}
196
				}
197
198
				return false;
199
			};
200
201
			$websiteFolder = $this->getWebsiteFolder();
202
203
			$path = $this->miscService->normalizePath($path);
204
			while ($path && ($path !== '.')) {
205
				try {
206
					/** @var StorageFile|StorageFolder $file */
207
					$file = $websiteFolder->get($path);
208
				} catch (NotFoundException $e) {
209
					$file = null;
210
				}
211
212
				if ($file) {
213
					if ($viewerAccessClosure($file->getOCNode())) {
214
						return;
215
					}
216
217
					throw new $exceptionClass();
218
				}
219
220
				$path = dirname($path);
221
			}
222
223
			if ($viewerAccessClosure($websiteFolder->getOCNode())) {
224
				return;
225
			}
226
		}
227
228
		throw new $exceptionClass();
229
	}
230
231
	/**
232
	 * @throws WebsiteInvalidOwnerException
233
	 */
234 5
	public function assertValidOwner(): void
235
	{
236 5
		$user = $this->userManager->get($this->getUserId());
237 5
		if ($user === null) {
238
			throw new WebsiteInvalidOwnerException();
239
		}
240 5
		if (!$user->isEnabled()) {
241
			throw new WebsiteInvalidOwnerException();
242
		}
243 5
		if (!$this->websitesService->isUserAllowed($this->getUserId())) {
244
			throw new WebsiteInvalidOwnerException();
245
		}
246 5
	}
247
248
	/**
249
	 * @throws WebsiteInvalidDataException
250
	 */
251 4
	public function assertValidName(): void
252
	{
253 4
		if (strlen($this->getName()) < self::NAME_LENGTH_MIN) {
254
			throw new WebsiteInvalidDataException('name', $this->l10n->t('The name of the website must be longer.'));
255
		}
256 4
		if (strlen($this->getName()) > self::NAME_LENGTH_MAX) {
257
			throw new WebsiteInvalidDataException('name', $this->l10n->t('The name of the website is too long.'));
258
		}
259 4
	}
260
261
	/**
262
	 * @throws WebsiteInvalidDataException
263
	 */
264 4
	public function assertValidSite(): void
265
	{
266 4
		if (strlen($this->getSite()) < self::SITE_LENGTH_MIN) {
267
			$error = $this->l10n->t('The identifier of the website must be longer.');
268
			throw new WebsiteInvalidDataException('site', $error);
269
		}
270 4
		if (strlen($this->getSite()) > self::SITE_LENGTH_MAX) {
271
			$error = $this->l10n->t('The identifier of the website is too long.');
272
			throw new WebsiteInvalidDataException('site', $error);
273
		}
274 4
		if (preg_match('/' . self::SITE_REGEX . '/', $this->getSite()) !== 1) {
275
			$error = $this->l10n->t('The identifier of the website can only contain lowercase alpha numeric chars.');
276
			throw new WebsiteInvalidDataException('site', $error);
277
		}
278 4
	}
279
280
	/**
281
	 * @throws WebsiteInvalidDataException
282
	 */
283 3
	public function assertValidPath(): void
284
	{
285
		try {
286 3
			$path = $this->miscService->normalizePath($this->getPath());
287 3
			if ($path === '') {
288 3
				throw new InvalidPathException();
289
			}
290
		} catch (InvalidPathException $e) {
291
			throw new WebsiteInvalidDataException(
292
				'path',
293
				$this->l10n->t('The path of the website is invalid.')
294
			);
295
		}
296
297 3
		$userFolder = new StorageFolder(\OC::$server->getUserFolder($this->getUserId()));
298
299
		try {
300 3
			$websiteBaseFolder = $userFolder->getFolder(dirname($path));
301
302
			try {
303 3
				$websiteFolder = $websiteBaseFolder->getFolder(basename($path));
304
305 1
				if (!$websiteFolder->isLocal()) {
306
					throw new WebsiteInvalidDataException(
307
						'path',
308 1
						$this->l10n->t('The website\'s path is stored on a non-local storage.')
309
					);
310
				}
311 3
			} catch (NotFoundException $e) {
312 3
				if (!$websiteBaseFolder->isLocal()) {
313
					throw new WebsiteInvalidDataException(
314
						'path',
315 3
						$this->l10n->t('The website\'s path is stored on a non-local storage.')
316
					);
317
				}
318
			}
319
		} catch (InvalidPathException | NotFoundException $e) {
320
			throw new WebsiteInvalidDataException(
321
				'path',
322
				$this->l10n->t('Parent folder of the website\'s path not found.')
323
			);
324
		}
325 3
	}
326
327
	/**
328
	 * @throws ThemeNotFoundException
329
	 * @throws ThemeNotCompatibleException
330
	 */
331 3
	public function assertValidTheme(): void
332
	{
333 3
		$this->themesService->assertValidTheme($this->getTheme());
334 3
	}
335
336
	/**
337
	 * @throws TemplateNotFoundException
338
	 * @throws TemplateNotCompatibleException
339
	 */
340 3
	public function assertValidTemplate(): void
341
	{
342 3
		$this->templatesService->assertValidTemplate($this->getTemplateSource());
343 3
	}
344
345
	/**
346
	 * @param string $userId
347
	 *
348
	 * @throws WebsiteForeignOwnerException
349
	 */
350 2
	public function assertOwnedBy(string $userId): void
351
	{
352 2
		if ($this->getUserId() !== $userId) {
353 1
			throw new WebsiteForeignOwnerException();
354
		}
355 2
	}
356
357
	/**
358
	 * @return StorageFolder
359
	 * @throws WebsiteInvalidFilesystemException
360
	 */
361 2
	public function getWebsiteFolder(): StorageFolder
362
	{
363 2
		if ($this->folder !== null) {
364
			try {
365
				// NC doesn't guarantee that mounts are present for the whole request lifetime
366
				// for example, if you call \OC\Files\Utils\Scanner::scan(), all mounts are reset
367
				// this makes OCNode instances, which rely on mounts of different users than the current, unusable
368
				// by calling OCFolder::get('') we can detect this situation and re-init the required mounts
369 2
				$this->folder->get('');
370
			} catch (\Exception $e) {
371
				$this->folder = null;
372
			}
373
		}
374
375 2
		if ($this->folder === null) {
376
			try {
377 2
				$ocUserFolder = \OC::$server->getUserFolder($this->getUserId());
378 2
				$userFolder = new StorageFolder($ocUserFolder);
379
380 2
				$websiteFolder = $userFolder->getFolder($this->getPath());
381 2
				$this->folder = $websiteFolder->fakeRoot();
382
			} catch (InvalidPathException | NotFoundException $e) {
383
				throw new WebsiteInvalidFilesystemException($e);
384
			}
385
		}
386
387 2
		return $this->folder;
388
	}
389
390
	/**
391
	 * @return string
392
	 * @throws WebsiteInvalidFilesystemException
393
	 */
394 2
	public function getWebsitePath(): string
395
	{
396
		try {
397 2
			return $this->getWebsiteFolder()->getLocalPath() . '/';
398
		} catch (InvalidPathException | NotFoundException $e) {
399
			throw new WebsiteInvalidFilesystemException($e);
400
		}
401
	}
402
403
	/**
404
	 * @return string
405
	 */
406 2
	public function getWebsiteUrl(): string
407
	{
408 2
		if (!$this->getProxyRequest()) {
409 2
			$route = Application::APP_NAME . '.Pico.getPage';
410 2
			$parameters = [ 'site' => $this->getSite(), 'page' => '' ];
411 2
			return $this->urlGenerator->linkToRoute($route, $parameters) . '/';
412
		} else {
413
			return \OC::$WEBROOT . '/sites/' . urlencode($this->getSite()) . '/';
414
		}
415
	}
416
}
417