Passed
Pull Request — master (#77)
by Daniel
36:15
created

Website::assertValidTheme()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
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(',', $groupAccess);
151
			}
152
153
			foreach ($groupAccess 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
			/** @var OCFolder $viewerOCFolder */
178
			$viewerOCFolder = \OC::$server->getUserFolder($this->getViewer());
179
			$viewerAccessClosure = function (OCNode $node) use ($viewerOCFolder) {
180
				$nodeId = $node->getId();
181
182
				$viewerNodes = $viewerOCFolder->getById($nodeId);
183
				foreach ($viewerNodes as $viewerNode) {
184
					if ($viewerNode->isReadable()) {
185
						return true;
186
					}
187
				}
188
189
				return false;
190
			};
191
192
			$websiteFolder = $this->getWebsiteFolder();
193
194
			$path = $this->miscService->normalizePath($path);
195
			while ($path && ($path !== '.')) {
196
				try {
197
					/** @var StorageFile|StorageFolder $file */
198
					$file = $websiteFolder->get($path);
199
				} catch (NotFoundException $e) {
200
					$file = null;
201
				}
202
203
				if ($file) {
204
					if ($viewerAccessClosure($file->getOCNode())) {
205
						return;
206
					}
207
208
					throw new $exceptionClass();
209
				}
210
211
				$path = dirname($path);
212
			}
213
214
			if ($viewerAccessClosure($websiteFolder->getOCNode())) {
215
				return;
216
			}
217
		}
218
219
		throw new $exceptionClass();
220
	}
221
222
	/**
223
	 * @throws WebsiteInvalidOwnerException
224
	 */
225 5
	public function assertValidOwner()
226
	{
227 5
		$user = $this->userManager->get($this->getUserId());
228 5
		if ($user === null) {
229
			throw new WebsiteInvalidOwnerException();
230
		}
231 5
		if (!$user->isEnabled()) {
232
			throw new WebsiteInvalidOwnerException();
233
		}
234 5
		if (!$this->websitesService->isUserAllowed($this->getUserId())) {
235
			throw new WebsiteInvalidOwnerException();
236
		}
237 5
	}
238
239
	/**
240
	 * @throws WebsiteInvalidDataException
241
	 */
242 4
	public function assertValidName()
243
	{
244 4
		if (strlen($this->getName()) < self::NAME_LENGTH_MIN) {
245
			throw new WebsiteInvalidDataException('name', $this->l10n->t('The name of the website must be longer.'));
246
		}
247 4
		if (strlen($this->getName()) > self::NAME_LENGTH_MAX) {
248
			throw new WebsiteInvalidDataException('name', $this->l10n->t('The name of the website is too long.'));
249
		}
250 4
	}
251
252
	/**
253
	 * @throws WebsiteInvalidDataException
254
	 */
255 4
	public function assertValidSite()
256
	{
257 4
		if (strlen($this->getSite()) < self::SITE_LENGTH_MIN) {
258
			$errorMessage = $this->l10n->t('The identifier of the website must be longer.');
259
			throw new WebsiteInvalidDataException('site', $errorMessage);
260
		}
261 4
		if (strlen($this->getSite()) > self::SITE_LENGTH_MAX) {
262
			$errorMessage = $this->l10n->t('The identifier of the website is too long.');
263
			throw new WebsiteInvalidDataException('site', $errorMessage);
264
		}
265 4
		if (preg_match('/' . self::SITE_REGEX . '/', $this->getSite()) !== 1) {
266
			$errorMessage = $this->l10n->t('The identifier of the website can only contains alpha numeric chars.');
267
			throw new WebsiteInvalidDataException('site', $errorMessage);
268
		}
269 4
	}
270
271
	/**
272
	 * @throws WebsiteInvalidDataException
273
	 */
274 3
	public function assertValidPath()
275
	{
276
		try {
277 3
			$path = $this->miscService->normalizePath($this->getPath());
278 3
			if ($path === '') {
279 3
				throw new InvalidPathException();
280
			}
281
		} catch (InvalidPathException $e) {
282
			throw new WebsiteInvalidDataException(
283
				'path',
284
				$this->l10n->t('The path of the website is invalid.')
285
			);
286
		}
287
288 3
		$userFolder = new StorageFolder(\OC::$server->getUserFolder($this->getUserId()));
289
290
		try {
291 3
			$websiteBaseFolder = $userFolder->getFolder(dirname($path));
292
293
			try {
294 3
				$websiteFolder = $websiteBaseFolder->getFolder(basename($path));
295
296 1
				if (!$websiteFolder->isLocal()) {
297
					throw new WebsiteInvalidDataException(
298
						'path',
299 1
						$this->l10n->t('The website\'s path is stored on a non-local storage.')
300
					);
301
				}
302 3
			} catch (NotFoundException $e) {
303 3
				if (!$websiteBaseFolder->isLocal()) {
304
					throw new WebsiteInvalidDataException(
305
						'path',
306 3
						$this->l10n->t('The website\'s path is stored on a non-local storage.')
307
					);
308
				}
309
			}
310
		} catch (InvalidPathException $e) {
311
			throw new WebsiteInvalidDataException(
312
				'path',
313
				$this->l10n->t('Parent folder of the website\'s path not found.')
314
			);
315
		} catch (NotFoundException $e) {
316
			throw new WebsiteInvalidDataException(
317
				'path',
318
				$this->l10n->t('Parent folder of the website\'s path not found.')
319
			);
320
		}
321 3
	}
322
323
	/**
324
	 * @throws ThemeNotFoundException
325
	 * @throws ThemeNotCompatibleException
326
	 */
327 3
	public function assertValidTheme()
328
	{
329 3
		$this->themesService->assertValidTheme($this->getTheme());
330 3
	}
331
332
	/**
333
	 * @throws TemplateNotFoundException
334
	 * @throws TemplateNotCompatibleException
335
	 */
336 3
	public function assertValidTemplate()
337
	{
338 3
		$this->templatesService->assertValidTemplate($this->getTemplateSource());
339 3
	}
340
341
	/**
342
	 * @param string $userId
343
	 *
344
	 * @throws WebsiteForeignOwnerException
345
	 */
346 2
	public function assertOwnedBy($userId)
347
	{
348 2
		if ($this->getUserId() !== $userId) {
349 1
			throw new WebsiteForeignOwnerException();
350
		}
351 2
	}
352
353
	/**
354
	 * @return StorageFolder
355
	 * @throws WebsiteInvalidFilesystemException
356
	 */
357 2
	public function getWebsiteFolder(): StorageFolder
358
	{
359 2
		if ($this->folder !== null) {
360
			try {
361
				// NC doesn't guarantee that mounts are present for the whole request lifetime
362
				// for example, if you call \OC\Files\Utils\Scanner::scan(), all mounts are reset
363
				// this makes OCNode instances, which rely on mounts of different users than the current, unusable
364
				// by calling OCFolder::get('') we can detect this situation and re-init the required mounts
365 2
				$this->folder->get('');
366
			} catch (\Exception $e) {
367
				$this->folder = null;
368
			}
369
		}
370
371 2
		if ($this->folder === null) {
372
			try {
373 2
				$ocUserFolder = \OC::$server->getUserFolder($this->getUserId());
374 2
				$userFolder = new StorageFolder($ocUserFolder);
375
376 2
				$websiteFolder = $userFolder->getFolder($this->getPath());
377 2
				$this->folder = $websiteFolder->fakeRoot();
378
			} catch (InvalidPathException $e) {
379
				throw new WebsiteInvalidFilesystemException($e);
380
			} catch (NotFoundException $e) {
381
				throw new WebsiteInvalidFilesystemException($e);
382
			}
383
		}
384
385 2
		return $this->folder;
386
	}
387
388
	/**
389
	 * @return string
390
	 * @throws WebsiteInvalidFilesystemException
391
	 */
392 2
	public function getWebsitePath(): string
393
	{
394
		try {
395 2
			return $this->getWebsiteFolder()->getLocalPath() . '/';
396
		} catch (InvalidPathException $e) {
397
			throw new WebsiteInvalidFilesystemException($e);
398
		} catch (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