1<?php
2namespace TYPO3\PharStreamWrapper;
3
4/*
5 * This file is part of the TYPO3 project.
6 *
7 * It is free software; you can redistribute it and/or modify it under the terms
8 * of the MIT License (MIT). For the full copyright and license information,
9 * please read the LICENSE file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13
14/**
15 * Helper provides low-level tools on file name resolving. However it does not
16 * (and should not) maintain any runtime state information. In order to resolve
17 * Phar archive paths according resolvers have to be used.
18 *
19 * @see \TYPO3\PharStreamWrapper\Resolvable::resolve()
20 */
21class Helper
22{
23    /*
24     * Resets PHP's OPcache if enabled as work-around for issues in `include()`
25     * or `require()` calls and OPcache delivering wrong results.
26     *
27     * @see https://bugs.php.net/bug.php?id=66569
28     */
29    public static function resetOpCache()
30    {
31        if (function_exists('opcache_reset')
32            && function_exists('opcache_get_status')
33        ) {
34            $status = opcache_get_status();
35            if (!empty($status['opcache_enabled'])) {
36                opcache_reset();
37            }
38        }
39    }
40
41    /**
42     * Determines base file that can be accessed using the regular file system.
43     * For e.g. "phar:///home/user/bundle.phar/content.txt" that would result
44     * into "/home/user/bundle.phar".
45     *
46     * @param string $path
47     * @return string|null
48     */
49    public static function determineBaseFile($path)
50    {
51        $parts = explode('/', static::normalizePath($path));
52
53        while (count($parts)) {
54            $currentPath = implode('/', $parts);
55            if (@is_file($currentPath) && realpath($currentPath) !== false) {
56                return $currentPath;
57            }
58            array_pop($parts);
59        }
60
61        return null;
62    }
63
64    /**
65     * @param string $path
66     * @return bool
67     */
68    public static function hasPharPrefix($path)
69    {
70        return stripos($path, 'phar://') === 0;
71    }
72
73    /**
74     * @param string $path
75     * @return string
76     */
77    public static function removePharPrefix($path)
78    {
79        $path = trim($path);
80        if (!static::hasPharPrefix($path)) {
81            return $path;
82        }
83        return substr($path, 7);
84    }
85
86    /**
87     * Normalizes a path, removes phar:// prefix, fixes Windows directory
88     * separators. Result is without trailing slash.
89     *
90     * @param string $path
91     * @return string
92     */
93    public static function normalizePath($path)
94    {
95        return rtrim(
96            static::normalizeWindowsPath(
97                static::removePharPrefix($path)
98            ),
99            '/'
100        );
101    }
102
103    /**
104     * Fixes a path for windows-backslashes and reduces double-slashes to single slashes
105     *
106     * @param string $path File path to process
107     * @return string
108     */
109    public static function normalizeWindowsPath($path)
110    {
111        return str_replace('\\', '/', $path);
112    }
113
114    /**
115     * Resolves all dots, slashes and removes spaces after or before a path...
116     *
117     * @param string $path Input string
118     * @return string Canonical path, always without trailing slash
119     */
120    private static function getCanonicalPath($path)
121    {
122        $path = static::normalizeWindowsPath($path);
123
124        $absolutePathPrefix = '';
125        if (static::isAbsolutePath($path)) {
126            if (static::isWindows() && strpos($path, ':/') === 1) {
127                $absolutePathPrefix = substr($path, 0, 3);
128                $path = substr($path, 3);
129            } else {
130                $path = ltrim($path, '/');
131                $absolutePathPrefix = '/';
132            }
133        }
134
135        $pathParts = explode('/', $path);
136        $pathPartsLength = count($pathParts);
137        for ($partCount = 0; $partCount < $pathPartsLength; $partCount++) {
138            // double-slashes in path: remove element
139            if ($pathParts[$partCount] === '') {
140                array_splice($pathParts, $partCount, 1);
141                $partCount--;
142                $pathPartsLength--;
143            }
144            // "." in path: remove element
145            if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '.') {
146                array_splice($pathParts, $partCount, 1);
147                $partCount--;
148                $pathPartsLength--;
149            }
150            // ".." in path:
151            if ((isset($pathParts[$partCount]) ? $pathParts[$partCount] : '') === '..') {
152                if ($partCount === 0) {
153                    array_splice($pathParts, $partCount, 1);
154                    $partCount--;
155                    $pathPartsLength--;
156                } elseif ($partCount >= 1) {
157                    // Rremove this and previous element
158                    array_splice($pathParts, $partCount - 1, 2);
159                    $partCount -= 2;
160                    $pathPartsLength -= 2;
161                } elseif ($absolutePathPrefix) {
162                    // can't go higher than root dir
163                    // simply remove this part and continue
164                    array_splice($pathParts, $partCount, 1);
165                    $partCount--;
166                    $pathPartsLength--;
167                }
168            }
169        }
170
171        return $absolutePathPrefix . implode('/', $pathParts);
172    }
173
174    /**
175     * Checks if the $path is absolute or relative (detecting either '/' or
176     * 'x:/' as first part of string) and returns TRUE if so.
177     *
178     * @param string $path File path to evaluate
179     * @return bool
180     */
181    private static function isAbsolutePath($path)
182    {
183        // Path starting with a / is always absolute, on every system
184        // On Windows also a path starting with a drive letter is absolute: X:/
185        return (isset($path[0]) ? $path[0] : null) === '/'
186            || static::isWindows() && (
187                strpos($path, ':/') === 1
188                || strpos($path, ':\\') === 1
189            );
190    }
191
192    /**
193     * @return bool
194     */
195    private static function isWindows()
196    {
197        return stripos(PHP_OS, 'WIN') === 0;
198    }
199}