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