[BUGFIX] Avoid PHP warning when using Phar archive with open_basedir
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / IO / PharStreamWrapper.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Core\IO;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Core\Environment;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Core\Utility\PathUtility;
21
22 class PharStreamWrapper
23 {
24 /**
25 * Internal stream constants that are not exposed to PHP, but used...
26 * @see https://github.com/php/php-src/blob/e17fc0d73c611ad0207cac8a4a01ded38251a7dc/main/php_streams.h
27 */
28 protected const STREAM_OPEN_FOR_INCLUDE = 128;
29
30 /**
31 * @var resource
32 */
33 public $context;
34
35 /**
36 * @var resource
37 */
38 protected $internalResource;
39
40 /**
41 * @return bool
42 */
43 public function dir_closedir(): bool
44 {
45 if (!is_resource($this->internalResource)) {
46 return false;
47 }
48
49 $this->invokeInternalStreamWrapper(
50 'closedir',
51 $this->internalResource
52 );
53 return !is_resource($this->internalResource);
54 }
55
56 /**
57 * @param string $path
58 * @param int $options
59 * @return bool
60 */
61 public function dir_opendir(string $path, int $options): bool
62 {
63 $this->assertPath($path);
64 $this->internalResource = $this->invokeInternalStreamWrapper(
65 'opendir',
66 $path,
67 $this->context
68 );
69 return is_resource($this->internalResource);
70 }
71
72 /**
73 * @return string|false
74 */
75 public function dir_readdir()
76 {
77 return $this->invokeInternalStreamWrapper(
78 'readdir',
79 $this->internalResource
80 );
81 }
82
83 /**
84 * @return bool
85 */
86 public function dir_rewinddir(): bool
87 {
88 if (!is_resource($this->internalResource)) {
89 return false;
90 }
91
92 $this->invokeInternalStreamWrapper(
93 'rewinddir',
94 $this->internalResource
95 );
96 return is_resource($this->internalResource);
97 }
98
99 /**
100 * @param string $path
101 * @param int $mode
102 * @param int $options
103 * @return bool
104 */
105 public function mkdir(string $path, int $mode, int $options): bool
106 {
107 $this->assertPath($path);
108 return $this->invokeInternalStreamWrapper(
109 'mkdir',
110 $path,
111 $mode,
112 (bool)($options & STREAM_MKDIR_RECURSIVE),
113 $this->context
114 );
115 }
116
117 /**
118 * @param string $path_from
119 * @param string $path_to
120 * @return bool
121 */
122 public function rename(string $path_from, string $path_to): bool
123 {
124 $this->assertPath($path_from);
125 $this->assertPath($path_to);
126 return $this->invokeInternalStreamWrapper(
127 'rename',
128 $path_from,
129 $path_to,
130 $this->context
131 );
132 }
133
134 public function rmdir(string $path, int $options): bool
135 {
136 $this->assertPath($path);
137 return $this->invokeInternalStreamWrapper(
138 'rmdir',
139 $path,
140 $this->context
141 );
142 }
143
144 /**
145 * @param int $cast_as
146 */
147 public function stream_cast(int $cast_as)
148 {
149 throw new PharStreamWrapperException(
150 'Method stream_select() cannot be used',
151 1530103999
152 );
153 }
154
155 public function stream_close()
156 {
157 $this->invokeInternalStreamWrapper(
158 'fclose',
159 $this->internalResource
160 );
161 }
162
163 /**
164 * @return bool
165 */
166 public function stream_eof(): bool
167 {
168 return $this->invokeInternalStreamWrapper(
169 'feof',
170 $this->internalResource
171 );
172 }
173
174 /**
175 * @return bool
176 */
177 public function stream_flush(): bool
178 {
179 return $this->invokeInternalStreamWrapper(
180 'fflush',
181 $this->internalResource
182 );
183 }
184
185 /**
186 * @param int $operation
187 * @return bool
188 */
189 public function stream_lock(int $operation): bool
190 {
191 return $this->invokeInternalStreamWrapper(
192 'flock',
193 $this->internalResource,
194 $operation
195 );
196 }
197
198 /**
199 * @param string $path
200 * @param int $option
201 * @param string|int $value
202 * @return bool
203 */
204 public function stream_metadata(string $path, int $option, $value): bool
205 {
206 $this->assertPath($path);
207 if ($option === STREAM_META_TOUCH) {
208 return $this->invokeInternalStreamWrapper(
209 'touch',
210 $path,
211 ...$value
212 );
213 }
214 if ($option === STREAM_META_OWNER_NAME || $option === STREAM_META_OWNER) {
215 return $this->invokeInternalStreamWrapper(
216 'chown',
217 $path,
218 $value
219 );
220 }
221 if ($option === STREAM_META_GROUP_NAME || $option === STREAM_META_GROUP) {
222 return $this->invokeInternalStreamWrapper(
223 'chgrp',
224 $path,
225 $value
226 );
227 }
228 if ($option === STREAM_META_ACCESS) {
229 return $this->invokeInternalStreamWrapper(
230 'chmod',
231 $path,
232 $value
233 );
234 }
235 return false;
236 }
237
238 /**
239 * @param string $path
240 * @param string $mode
241 * @param int $options
242 * @param string|null $opened_path
243 * @return bool
244 */
245 public function stream_open(
246 string $path,
247 string $mode,
248 int $options,
249 string &$opened_path = null
250 ): bool {
251 $this->assertPath($path);
252 $arguments = [$path, $mode, (bool)($options & STREAM_USE_PATH)];
253 // only add stream context for non include/require calls
254 if (!($options & static::STREAM_OPEN_FOR_INCLUDE)) {
255 $arguments[] = $this->context;
256 // work around https://bugs.php.net/bug.php?id=66569
257 // for including files from Phar stream with OPcache enabled
258 } else {
259 $this->resetOpCache();
260 }
261 $this->internalResource = $this->invokeInternalStreamWrapper(
262 'fopen',
263 ...$arguments
264 );
265 if (!is_resource($this->internalResource)) {
266 return false;
267 }
268 if ($opened_path !== null) {
269 $metaData = stream_get_meta_data($this->internalResource);
270 $opened_path = $metaData['uri'];
271 }
272 return true;
273 }
274
275 /**
276 * @param int $count
277 * @return string
278 */
279 public function stream_read(int $count): string
280 {
281 return $this->invokeInternalStreamWrapper(
282 'fread',
283 $this->internalResource,
284 $count
285 );
286 }
287
288 /**
289 * @param int $offset
290 * @param int $whence
291 * @return bool
292 */
293 public function stream_seek(int $offset, int $whence = SEEK_SET): bool
294 {
295 return $this->invokeInternalStreamWrapper(
296 'fseek',
297 $this->internalResource,
298 $offset,
299 $whence
300 ) !== -1;
301 }
302
303 /**
304 * @param int $option
305 * @param int $arg1
306 * @param int $arg2
307 * @return bool
308 */
309 public function stream_set_option(int $option, int $arg1, int $arg2): bool
310 {
311 if ($option === STREAM_OPTION_BLOCKING) {
312 return $this->invokeInternalStreamWrapper(
313 'stream_set_blocking',
314 $this->internalResource,
315 $arg1
316 );
317 }
318 if ($option === STREAM_OPTION_READ_TIMEOUT) {
319 return $this->invokeInternalStreamWrapper(
320 'stream_set_timeout',
321 $this->internalResource,
322 $arg1,
323 $arg2
324 );
325 }
326 if ($option === STREAM_OPTION_WRITE_BUFFER) {
327 return $this->invokeInternalStreamWrapper(
328 'stream_set_write_buffer',
329 $this->internalResource,
330 $arg2
331 ) === 0;
332 }
333 return false;
334 }
335
336 /**
337 * @return array
338 */
339 public function stream_stat(): array
340 {
341 return $this->invokeInternalStreamWrapper(
342 'fstat',
343 $this->internalResource
344 );
345 }
346
347 /**
348 * @return int
349 */
350 public function stream_tell(): int
351 {
352 return $this->invokeInternalStreamWrapper(
353 'ftell',
354 $this->internalResource
355 );
356 }
357
358 /**
359 * @param int $new_size
360 * @return bool
361 */
362 public function stream_truncate(int $new_size): bool
363 {
364 return $this->invokeInternalStreamWrapper(
365 'ftruncate',
366 $this->internalResource,
367 $new_size
368 );
369 }
370
371 /**
372 * @param string $data
373 * @return int
374 */
375 public function stream_write(string $data): int
376 {
377 return $this->invokeInternalStreamWrapper(
378 'fwrite',
379 $this->internalResource,
380 $data
381 );
382 }
383
384 /**
385 * @param string $path
386 * @return bool
387 */
388 public function unlink(string $path): bool
389 {
390 $this->assertPath($path);
391 return $this->invokeInternalStreamWrapper(
392 'unlink',
393 $path,
394 $this->context
395 );
396 }
397
398 /**
399 * @param string $path
400 * @param int $flags
401 * @return array|false
402 */
403 public function url_stat(string $path, int $flags)
404 {
405 $this->assertPath($path);
406 $functionName = $flags & STREAM_URL_STAT_QUIET ? '@stat' : 'stat';
407 return $this->invokeInternalStreamWrapper($functionName, $path);
408 }
409
410 /**
411 * @param string $path
412 * @return bool
413 */
414 protected function isAllowed(string $path): bool
415 {
416 $path = $this->determineBaseFile($path);
417 if (!GeneralUtility::isAbsPath($path)) {
418 $path = Environment::getPublicPath() . '/' . $path;
419 }
420
421 if (GeneralUtility::validPathStr($path)
422 && GeneralUtility::isFirstPartOfStr(
423 $path,
424 Environment::getPublicPath() . '/typo3conf/ext/'
425 )
426 ) {
427 return true;
428 }
429
430 return false;
431 }
432
433 /**
434 * Normalizes a path, removes phar:// prefix, fixes Windows directory
435 * separators. Result is without trailing slash.
436 *
437 * @param string $path
438 * @return string
439 */
440 protected function normalizePath(string $path): string
441 {
442 return rtrim(
443 PathUtility::getCanonicalPath(
444 GeneralUtility::fixWindowsFilePath(
445 $this->removePharPrefix($path)
446 )
447 ),
448 '/'
449 );
450 }
451
452 /**
453 * @param string $path
454 * @return string
455 */
456 protected function removePharPrefix(string $path): string
457 {
458 return preg_replace('#^phar://#i', '', $path);
459 }
460
461 /**
462 * Determines base file that can be accessed using the regular file system.
463 * For e.g. "phar:///home/user/bundle.phar/content.txt" that would result
464 * into "/home/user/bundle.phar".
465 *
466 * @param string $path
467 * @return string|null
468 */
469 protected function determineBaseFile(string $path)
470 {
471 $parts = explode('/', $this->normalizePath($path));
472
473 while (count($parts)) {
474 $currentPath = implode('/', $parts);
475 if (@file_exists($currentPath)) {
476 return $currentPath;
477 }
478 array_pop($parts);
479 }
480
481 return null;
482 }
483
484 /**
485 * Determines whether the requested path is the base file.
486 *
487 * @param string $path
488 * @return bool
489 * @deprecated Currently not used
490 */
491 protected function isBaseFile(string $path): bool
492 {
493 $path = $this->normalizePath($path);
494 $baseFile = $this->determineBaseFile($path);
495 return $path === $baseFile;
496 }
497
498 /**
499 * Asserts the given path to a Phar file.
500 *
501 * @param string $path
502 * @throws PharStreamWrapperException
503 */
504 protected function assertPath(string $path)
505 {
506 if (!$this->isAllowed($path)) {
507 throw new PharStreamWrapperException(
508 sprintf('Executing %s is denied', $path),
509 1530103998
510 );
511 }
512 }
513
514 protected function resetOpCache()
515 {
516 if (function_exists('opcache_reset')
517 && function_exists('opcache_get_status')
518 && !empty(opcache_get_status()['opcache_enabled'])
519 ) {
520 opcache_reset();
521 }
522 }
523
524 /**
525 * Invokes commands on the native PHP Phar stream wrapper.
526 *
527 * @param string $functionName
528 * @param mixed ...$arguments
529 * @return mixed
530 */
531 protected function invokeInternalStreamWrapper(string $functionName, ...$arguments)
532 {
533 $silentExecution = $functionName{0} === '@';
534 $functionName = ltrim($functionName, '@');
535 $this->restoreInternalSteamWrapper();
536
537 if ($silentExecution) {
538 $result = @call_user_func_array($functionName, $arguments);
539 } else {
540 $result = call_user_func_array($functionName, $arguments);
541 }
542
543 $this->registerStreamWrapper();
544 return $result;
545 }
546
547 protected function restoreInternalSteamWrapper()
548 {
549 stream_wrapper_restore('phar');
550 }
551
552 protected function registerStreamWrapper()
553 {
554 stream_wrapper_unregister('phar');
555 stream_wrapper_register('phar', static::class);
556 }
557 }