Test Setup Failed
Push — master ( e9c97c...d2f071 )
by
unknown
44:25
created

MiscService::dropFileExtension()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 9
ccs 5
cts 6
cp 0.8333
rs 10
cc 3
nc 2
nop 2
crap 3.0416
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
namespace OCA\CMSPico\Service;
25
26
use OCA\CMSPico\AppInfo\Application;
27
use OCA\CMSPico\Exceptions\ComposerException;
28
use OCA\CMSPico\Exceptions\FilesystemNotWritableException;
29
use OCA\CMSPico\Files\FileInterface;
30
use OCP\Files\AlreadyExistsException;
31
use OCP\Files\GenericFileException;
32
use OCP\Files\InvalidPathException;
33
use OCP\Files\NotPermittedException;
34
use OCP\IL10N;
35
use OCP\Security\ISecureRandom;
36
37
class MiscService
38
{
39
	/** @var IL10N */
40
	private $l10n;
41
42
	/** @var ConfigService */
43
	private $configService;
44
45
	/** @var FileService */
46
	private $fileService;
47
48
	/** @var string[] */
49
	private $textFileMagic;
50
51
	/** @var string[] */
52
	private $binaryFileMagic;
53
54
	/**
55
	 * MiscService constructor.
56
	 *
57
	 * @param IL10N         $l10n
58
	 * @param ConfigService $configService
59
	 * @param FileService   $fileService
60
	 */
61 1
	public function __construct(IL10N $l10n, ConfigService $configService, FileService $fileService)
62
	{
63 1
		$this->l10n = $l10n;
64 1
		$this->configService = $configService;
65 1
		$this->fileService = $fileService;
66
67 1
		$this->textFileMagic = [
68 1
			hex2bin('EFBBBF'),
69 1
			hex2bin('0000FEFF'),
70 1
			hex2bin('FFFE0000'),
71 1
			hex2bin('FEFF'),
72 1
			hex2bin('FFFE')
73
		];
74
75 1
		$this->binaryFileMagic = [
76 1
			'%PDF',
77 1
			hex2bin('89') . 'PNG'
78
		];
79 1
	}
80
81
	/**
82
	 * @param string $path
83
	 *
84
	 * @return string
85
	 * @throws InvalidPathException
86
	 */
87 7
	public function normalizePath(string $path): string
88
	{
89 7
		$path = str_replace('\\', '/', $path);
90 7
		$pathParts = explode('/', $path);
91
92 7
		$resultParts = [];
93 7
		foreach ($pathParts as $pathPart) {
94 7
			if (($pathPart === '') || ($pathPart === '.')) {
95 7
				continue;
96 7
			} elseif ($pathPart === '..') {
97
				if (empty($resultParts)) {
98
					throw new InvalidPathException();
99
				}
100
101
				array_pop($resultParts);
102
				continue;
103
			}
104
105 7
			$resultParts[] = $pathPart;
106
		}
107
108 7
		return implode('/', $resultParts);
109
	}
110
111
	/**
112
	 * @param string      $path
113
	 * @param string|null $basePath
114
	 *
115
	 * @return string
116
	 * @throws InvalidPathException
117
	 */
118 2
	public function getRelativePath(string $path, string $basePath = null): string
119
	{
120 2
		if (!$basePath) {
121
			$basePath = \OC::$SERVERROOT;
122
		}
123
124 2
		$basePath = $this->normalizePath($basePath);
125 2
		$basePathLength = strlen($basePath);
126
127 2
		$path = $this->normalizePath($path);
128
129 2
		if ($path === $basePath) {
130
			return '';
131 2
		} elseif (substr($path, 0, $basePathLength + 1) === $basePath . '/') {
132 2
			return substr($path, $basePathLength + 1);
133
		} else {
134 2
			throw new InvalidPathException();
135
		}
136
	}
137
138
	/**
139
	 * @param string $path
140
	 * @param string $fileExtension
141
	 *
142
	 * @return false|string
143
	 * @throws InvalidPathException
144
	 */
145 2
	public function dropFileExtension(string $path, string $fileExtension): string
146
	{
147 2
		$fileName = basename($path);
148 2
		$fileExtensionPos = strrpos($fileName, '.');
149 2
		if (($fileExtensionPos === false) || (substr($fileName, $fileExtensionPos) !== $fileExtension)) {
150
			throw new InvalidPathException();
151
		}
152
153 2
		return substr($path, 0, strlen($path) - strlen($fileExtension));
154
	}
155
156
	/**
157
	 * @param FileInterface $file
158
	 *
159
	 * @return bool
160
	 * @throws NotPermittedException
161
	 * @throws GenericFileException
162
	 */
163 3
	public function isBinaryFile(FileInterface $file): bool
164
	{
165
		try {
166 3
			$buffer = file_get_contents($file->getLocalPath(), false, null, 0, 1024);
167
		} catch (\Exception $e) {
168
			$buffer = false;
169
		}
170
171 3
		if ($buffer === false) {
172
			$buffer = substr($file->getContent(), 0, 1024);
173
		}
174
175 3
		if ($buffer === '') {
176
			return false;
177
		}
178
179 3
		foreach ($this->textFileMagic as $textFileMagic) {
180 3
			if (substr_compare($buffer, $textFileMagic, 0, strlen($textFileMagic)) === 0) {
181
				return false;
182
			}
183
		}
184
185 3
		foreach ($this->binaryFileMagic as $binaryFileMagic) {
186 3
			if (substr_compare($buffer, $binaryFileMagic, 0, strlen($binaryFileMagic)) === 0) {
187 3
				return true;
188
			}
189
		}
190
191 3
		return (strpos($buffer, "\0") !== false);
192
	}
193
194
	/**
195
	 * @param int    $length
196
	 * @param string $prefix
197
	 * @param string $suffix
198
	 *
199
	 * @return string
200
	 */
201 1
	public function getRandom(int $length = 10, string $prefix = '', string $suffix = ''): string
202
	{
203 1
		$randomChars = ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS;
204 1
		$random = \OC::$server->getSecureRandom()->generate($length, $randomChars);
205 1
		return ($prefix ? $prefix . '.' : '') . $random . ($suffix ? '.' . $suffix : '');
206
	}
207
208
	/**
209
	 * @param \Exception $e
210
	 * @param string     ...$classNames
211
	 *
212
	 * @throws \Exception
213
	 */
214 1
	public function consumeException(\Exception $e, string ...$classNames)
215
	{
216 1
		foreach ($classNames as $className) {
217 1
			if (is_a($e, $className)) {
218 1
				return;
219
			}
220
		}
221
222
		throw $e;
223
	}
224
225
	/**
226
	 * @throws ComposerException
227
	 */
228
	public function checkComposer()
229
	{
230
		$appPath = Application::getAppPath();
231
		if (!is_file($appPath . '/vendor/autoload.php')) {
232
			try {
233
				$relativeAppPath = $this->getRelativePath($appPath) . '/';
234
			} catch (InvalidPathException $e) {
235
				$relativeAppPath = 'apps/' . Application::APP_NAME . '/';
236
			}
237
238
			throw new ComposerException($this->l10n->t(
239
				'Failed to enable Pico CMS for Nextcloud: Couldn\'t find "%s". Make sure to install the app\'s '
240
				. 'dependencies by executing `composer install` in the app\'s install directory below "%s". '
241
				. 'Then try again enabling Pico CMS for Nextcloud.',
242
				[ $relativeAppPath . 'vendor/autoload.php', $relativeAppPath ]
243
			));
244
		}
245
	}
246
247
	/**
248
	 * @throws FilesystemNotWritableException
249
	 */
250
	public function checkPublicFolder()
251
	{
252
		$publicFolder = $this->fileService->getPublicFolder();
253
254
		try {
255
			try {
256
				$publicThemesFolder = $publicFolder->newFolder(PicoService::DIR_THEMES);
257
			} catch (AlreadyExistsException $e) {
258
				$publicThemesFolder = $publicFolder->getFolder(PicoService::DIR_THEMES);
259
			}
260
261
			$publicThemesTestFileName = $this->getRandom(10, 'tmp', Application::APP_NAME . '-test');
262
			$publicThemesTestFile = $publicThemesFolder->newFile($publicThemesTestFileName);
263
			$publicThemesTestFile->delete();
264
265
			try {
266
				$publicPluginsFolder = $publicFolder->newFolder(PicoService::DIR_PLUGINS);
267
			} catch (AlreadyExistsException $e) {
268
				$publicPluginsFolder = $publicFolder->getFolder(PicoService::DIR_PLUGINS);
269
			}
270
271
			$publicPluginsTestFileName = $this->getRandom(10, 'tmp', Application::APP_NAME . '-test');
272
			$publicPluginsTestFile = $publicPluginsFolder->newFile($publicPluginsTestFileName);
273
			$publicPluginsTestFile->delete();
274
		} catch (NotPermittedException $e) {
275
			try {
276
				$appDataPublicPath = Application::getAppPath() . '/appdata_public';
277
				$appDataPublicPath = $this->getRelativePath($appDataPublicPath) . '/';
278
			} catch (InvalidPathException $e) {
279
				$appDataPublicPath = 'apps/' . Application::APP_NAME . '/appdata_public/';
280
			}
281
282
			try {
283
				$dataPath = $this->configService->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data');
284
				$dataPath = $this->getRelativePath($dataPath) . '/';
285
			} catch (InvalidPathException $e) {
286
				$dataPath = 'data/';
287
			}
288
289
			throw new FilesystemNotWritableException($this->l10n->t(
290
				'Failed to enable Pico CMS for Nextcloud: The webserver has no permission to create files and '
291
				. 'folders below "%s". Make sure to give the webserver write access to this directory by '
292
				. 'changing its permissions and ownership to the same as of your "%s" directory. Then try '
293
				. 'again enabling Pico CMS for Nextcloud.',
294
				[ $appDataPublicPath, $dataPath ]
295
			));
296
		}
297
	}
298
}
299