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

MiscService::isBinaryFile()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 10.3696

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
dl 0
loc 29
ccs 10
cts 15
cp 0.6667
rs 8.4444
cc 8
nc 32
nop 1
crap 10.3696
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
	 * @throws ComposerException
210
	 */
211
	public function checkComposer(): void
212
	{
213
		$appPath = Application::getAppPath();
214
		if (!is_file($appPath . '/vendor/autoload.php')) {
215
			try {
216
				$relativeAppPath = $this->getRelativePath($appPath) . '/';
217
			} catch (InvalidPathException $e) {
218
				$relativeAppPath = 'apps/' . Application::APP_NAME . '/';
219
			}
220
221
			throw new ComposerException($this->l10n->t(
222
				'Failed to enable Pico CMS for Nextcloud: Couldn\'t find "%s". Make sure to install the app\'s '
223
				. 'dependencies by executing `composer install` in the app\'s install directory below "%s". '
224
				. 'Then try again enabling Pico CMS for Nextcloud.',
225
				[ $relativeAppPath . 'vendor/autoload.php', $relativeAppPath ]
226
			));
227
		}
228
	}
229
230
	/**
231
	 * @throws FilesystemNotWritableException
232
	 */
233
	public function checkPublicFolder(): void
234
	{
235
		$publicFolder = $this->fileService->getPublicFolder();
236
237
		try {
238
			try {
239
				$publicThemesFolder = $publicFolder->newFolder(PicoService::DIR_THEMES);
240
			} catch (AlreadyExistsException $e) {
241
				$publicThemesFolder = $publicFolder->getFolder(PicoService::DIR_THEMES);
242
			}
243
244
			$publicThemesTestFileName = $this->getRandom(10, 'tmp', Application::APP_NAME . '-test');
245
			$publicThemesTestFile = $publicThemesFolder->newFile($publicThemesTestFileName);
246
			$publicThemesTestFile->delete();
247
248
			try {
249
				$publicPluginsFolder = $publicFolder->newFolder(PicoService::DIR_PLUGINS);
250
			} catch (AlreadyExistsException $e) {
251
				$publicPluginsFolder = $publicFolder->getFolder(PicoService::DIR_PLUGINS);
252
			}
253
254
			$publicPluginsTestFileName = $this->getRandom(10, 'tmp', Application::APP_NAME . '-test');
255
			$publicPluginsTestFile = $publicPluginsFolder->newFile($publicPluginsTestFileName);
256
			$publicPluginsTestFile->delete();
257
		} catch (NotPermittedException $e) {
258
			try {
259
				$appDataPublicPath = Application::getAppPath() . '/appdata_public';
260
				$appDataPublicPath = $this->getRelativePath($appDataPublicPath) . '/';
261
			} catch (InvalidPathException $e) {
262
				$appDataPublicPath = 'apps/' . Application::APP_NAME . '/appdata_public/';
263
			}
264
265
			try {
266
				$dataPath = $this->configService->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data');
267
				$dataPath = $this->getRelativePath($dataPath) . '/';
268
			} catch (InvalidPathException $e) {
269
				$dataPath = 'data/';
270
			}
271
272
			throw new FilesystemNotWritableException($this->l10n->t(
273
				'Failed to enable Pico CMS for Nextcloud: The webserver has no permission to create files and '
274
				. 'folders below "%s". Make sure to give the webserver write access to this directory by '
275
				. 'changing its permissions and ownership to the same as of your "%s" directory. Then try '
276
				. 'again enabling Pico CMS for Nextcloud.',
277
				[ $appDataPublicPath, $dataPath ]
278
			));
279
		}
280
	}
281
}
282