Passed
Pull Request — master (#77)
by Daniel
24:04
created

Website::getWebsitePath()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 5.667

Importance

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