1<?php
2
3
4namespace ComboStrap;
5
6/**
7 * Class LocalPath
8 * @package ComboStrap
9 * A local file system path
10 */
11class LocalPath extends PathAbs
12{
13
14    /**
15     * For whatever reason, it seems that php uses always the / separator on windows also
16     * but not always (ie  https://www.php.net/manual/en/function.realpath.php output \ on windows)
17     *
18     * Because we want to be able to copy the path and to be able to use
19     * it directly, we {@link LocalPath::normalizedToOs() normalize} it to the OS separator
20     * at build time
21     */
22    public const PHP_SYSTEM_DIRECTORY_SEPARATOR = DIRECTORY_SEPARATOR;
23
24    /**
25     * The characters that cannot be in the path for windows
26     * @var string[]
27     */
28    public const RESERVED_WINDOWS_CHARACTERS = ["\\", "/", ":", "*", "?", "\"", "<", ">", "|"];
29
30    private $path;
31
32    /**
33     * LocalPath constructor.
34     * @param $path - relative or absolute
35     */
36    public function __construct($path)
37    {
38        $this->path = $path;
39    }
40
41
42    /**
43     * @param string $filePath
44     * @return LocalPath
45     * @deprecated for {@link LocalPath::createFromPath()}
46     */
47    public static function create(string $filePath): LocalPath
48    {
49        return new LocalPath($filePath);
50    }
51
52    public static function createFromPath(string $string): LocalPath
53    {
54        return new LocalPath($string);
55    }
56
57    function getScheme(): string
58    {
59        return LocalFs::SCHEME;
60    }
61
62    function getLastName()
63    {
64        $names = $this->getNames();
65        $sizeof = sizeof($names);
66        if ($sizeof === 0) {
67            return null;
68        }
69        return $names[$sizeof - 1];
70
71    }
72
73    public function getExtension()
74    {
75        return pathinfo($this->path, PATHINFO_EXTENSION);
76    }
77
78    function getNames()
79    {
80        $directorySeparator = $this->getDirectorySeparator();
81        return explode($directorySeparator, $this->path);
82    }
83
84    function getDokuwikiId()
85    {
86        throw new ExceptionComboRuntime("Not implemented");
87    }
88
89
90    function toString(): string
91    {
92        return $this->path;
93    }
94
95    public function getParent(): ?Path
96    {
97        $absolutePath = pathinfo($this->path, PATHINFO_DIRNAME);
98        if (empty($absolutePath)) {
99            return null;
100        }
101        return new LocalPath($absolutePath);
102    }
103
104    function toAbsolutePath(): Path
105    {
106
107        if ($this->isAbsolute()) {
108            return $this;
109        }
110
111        return $this->toCanonicalPath();
112
113    }
114
115    /**
116     * @return string
117     */
118    private function getDirectorySeparator(): string
119    {
120        $directorySeparator = self::PHP_SYSTEM_DIRECTORY_SEPARATOR;
121        if (
122            $directorySeparator === '\\'
123            &&
124            strpos($this->path, "/") !== false
125        ) {
126            $directorySeparator = "/";
127        }
128        return $directorySeparator;
129    }
130
131
132    /**
133     * @throws ExceptionCombo
134     */
135    public function toDokuPath(): DokuPath
136    {
137        $driveRoots = DokuPath::getDriveRoots();
138        foreach ($driveRoots as $driveRoot => $drivePath) {
139            try {
140                $relativePath = $this->relativize($drivePath);
141                return DokuPath::createDokuPath($relativePath->toString(), $driveRoot);
142            } catch (ExceptionCombo $e) {
143                // not a relative path
144            }
145
146        }
147        throw new ExceptionCombo("The local path ($this) is not inside a doku path drive");
148
149
150    }
151
152    public function resolve(string $name): LocalPath
153    {
154
155        $newPath = $this->toCanonicalPath()->toString() . self::PHP_SYSTEM_DIRECTORY_SEPARATOR . $name;
156        return self::createFromPath($newPath);
157
158    }
159
160    /**
161     * @throws ExceptionCombo
162     */
163    public function relativize(LocalPath $localPath): LocalPath
164    {
165        $actualPath = $this->toCanonicalPath();
166        $localPath = $localPath->toCanonicalPath();
167
168        if (!(strpos($actualPath->toString(), $localPath->toString()) === 0)) {
169            throw new ExceptionCombo("The path ($localPath) is not a parent path of the actual path ($actualPath)");
170        }
171        $sepCharacter = 1; // delete the sep characters
172        $relativePath = substr($actualPath->toString(), strlen($localPath->toString()) + $sepCharacter);
173        $relativePath = str_replace(self::PHP_SYSTEM_DIRECTORY_SEPARATOR, DokuPath::PATH_SEPARATOR, $relativePath);
174        return LocalPath::createFromPath($relativePath);
175
176    }
177
178    public function isAbsolute(): bool
179    {
180        /**
181         * /
182         * \
183         * or a:/
184         * or z:\
185         */
186        if (preg_match("/^\/|[a-z]:[\\\\\/]|\\\\/i", $this->path)) {
187            return true;
188        }
189        return false;
190
191    }
192
193    /**
194     * An absolute path may not be canonical
195     * (ie windows short name or the path separator is not consistent (ie / in place of \ on windows)
196     *
197     * This function makes the path canonical meaning that two canonical path can be compared.
198     */
199    public function toCanonicalPath(): LocalPath
200    {
201
202        /**
203         * realpath() is just a system/library call to actual realpath() function supported by OS.
204         * real path handle also the windows name ie USERNAME~
205         */
206        $realPath = realpath($this->path);
207        if ($realPath !== false) {
208            return LocalPath::createFromPath($realPath);
209        }
210
211        /**
212         * It returns false on on file that does not exists.
213         * The suggestion on the realpath man page
214         * is to look for an existing parent directory.
215         * https://man7.org/linux/man-pages/man3/realpath.3.html
216         */
217        $parts = null;
218        $isRoot = false;
219        $counter = 0; // breaker
220        $workingPath = $this->path;
221        while ($realPath === false) {
222            $counter++;
223            $parent = dirname($workingPath);
224            /**
225             * From the doc: https://www.php.net/manual/en/function.dirname.php
226             * dirname('.');    // Will return '.'.
227             * dirname('/');    // Will return `\` on Windows and '/' on *nix systems.
228             * dirname('\\');   // Will return `\` on Windows and '.' on *nix systems.
229             * dirname('C:\\'); // Will return 'C:\' on Windows and '.' on *nix systems.
230             * dirname('\');    // Will return `C:\` on Windows and ??? on *nix systems.
231             */
232            if (preg_match("/^(\.|\/|\\\\|[a-z]:\\\\)$/i", $parent)
233                || $parent === $workingPath
234                || $parent === "\\" // bug on regexp
235            ) {
236                $isRoot = true;
237            }
238            // root, no need to delete the last sep
239            $lastSep = 1;
240            if ($isRoot) {
241                $lastSep = 0;
242            }
243            $parts[] = substr($workingPath, strlen($parent) + $lastSep);
244
245            $realPath = realpath($parent);
246            if ($isRoot) {
247                break;
248            }
249            if ($counter > 200) {
250                $message = "Bad absolute local path file ($this->path)";
251                if (PluginUtility::isDevOrTest()) {
252                    throw new ExceptionComboRuntime($message);
253                } else {
254                    LogUtility::msg($message);
255                }
256                return $this;
257            }
258            if ($realPath === false) {
259                // loop
260                $workingPath = $parent;
261            }
262        }
263        if ($parts !== null) {
264            if (!$isRoot) {
265                $realPath .= self::PHP_SYSTEM_DIRECTORY_SEPARATOR;
266            }
267            $parts = array_reverse($parts);
268            $realPath .= implode(self::PHP_SYSTEM_DIRECTORY_SEPARATOR, $parts);
269        }
270        return LocalPath::createFromPath($realPath);
271    }
272
273
274}
275