1<?php
2
3namespace ComboStrap;
4
5
6use ComboStrap\Web\Url;
7
8/**
9 * Class DokuPath
10 * @package ComboStrap
11 * A dokuwiki path has the same structure than a windows path with a drive and a path
12 *
13 * The drive being a local path on the local file system
14 *
15 * Ultimately, this path is the application path and should be used everywhere.
16 * * for users input (ie in a link markup such as media and page link)
17 * * for output (ie creating the id for the url)
18 *
19 * Dokuwiki knows only two drives ({@link WikiPath::MARKUP_DRIVE} and {@link WikiPath::MEDIA_DRIVE}
20 * but we have added a couple more such as the {@link WikiPath::COMBO_DRIVE combo resources}
21 * and the {@link WikiPath::CACHE_DRIVE} to be able to serve resources
22 *
23 * TODO: because all {@link LocalPath} has at minium a drive (ie C:,D:, E: for windows or \ for linux)
24 *    A Wiki Path can be just a wrapper around every local path)
25 *    The {@link LocalPath::toWikiPath()} should not throw then but as not all drive
26 *    may be public, we need to add a drive functionality to get this information.
27 */
28class WikiPath extends PathAbs
29{
30
31    const MEDIA_DRIVE = "media";
32    const MARKUP_DRIVE = "markup";
33    const UNKNOWN_DRIVE = "unknown";
34    const NAMESPACE_SEPARATOR_DOUBLE_POINT = ":";
35
36    // https://www.dokuwiki.org/config:useslash
37    const NAMESPACE_SEPARATOR_SLASH = "/";
38
39    const SEPARATORS = [self::NAMESPACE_SEPARATOR_DOUBLE_POINT, self::NAMESPACE_SEPARATOR_SLASH];
40
41    /**
42     * For whatever reason, dokuwiki uses also on windows
43     * the linux separator
44     */
45    public const DIRECTORY_SEPARATOR = "/";
46    public const SLUG_SEPARATOR = "-";
47
48
49    /**
50     * Dokuwiki has a file system that starts at a page and/or media
51     * directory that depends on the used syntax.
52     *
53     * It's a little bit the same than as the icon library (we set it as library then)
54     *
55     * This parameters is an URL parameter
56     * that permits to set an another one
57     * when retrieving the file via HTTP
58     * For now, there is only one value: {@link WikiPath::COMBO_DRIVE}
59     */
60    public const DRIVE_ATTRIBUTE = "drive";
61
62    /**
63     * The interwiki scheme that points to the
64     * combo resources directory ie {@link WikiPath::COMBO_DRIVE}
65     * ie
66     *   combo>library:
67     *   combo>image:
68     */
69    const COMBO_DRIVE = "combo";
70    /**
71     * The home directory for all themes
72     */
73    const COMBO_DATA_THEME_DRIVE = "combo-theme";
74    const CACHE_DRIVE = "cache";
75    const MARKUP_DEFAULT_TXT_EXTENSION = "txt";
76    const MARKUP_MD_TXT_EXTENSION = "md";
77    const REV_ATTRIBUTE = "rev";
78    const CURRENT_PATH_CHARACTER = ".";
79    const CURRENT_PARENT_PATH_CHARACTER = "..";
80    const CANONICAL = "wiki-path";
81    const ALL_MARKUP_EXTENSIONS = [self::MARKUP_DEFAULT_TXT_EXTENSION, self::MARKUP_MD_TXT_EXTENSION];
82
83
84    /**
85     * @var string[]
86     */
87    private static $reservedWords;
88
89    /**
90     * @var string the path id passed to function (cleaned)
91     */
92    private $id;
93
94
95    /**
96     * @var string
97     */
98    private $drive;
99    /**
100     * @var string|null - ie mtime
101     */
102    private $rev;
103
104
105    /**
106     * The separator from the {@link WikiPath::getDrive()}
107     */
108    const DRIVE_SEPARATOR = ">";
109    /**
110     * @var string - the absolute path (we use it for now to handle directory by adding a separator at the end)
111     */
112    protected $absolutePath;
113
114    /**
115     * DokuPath constructor.
116     *
117     * A path for the Dokuwiki File System
118     *
119     * @param string $path - the path (may be relative)
120     * @param string $drive - the drive (media, page, combo) - same as in windows for the drive prefix (c, d, ...)
121     * @param string|null $rev - the revision (mtime)
122     *
123     * Thee path should be a qualified/absolute path because in Dokuwiki, a link to a {@link MarkupPath}
124     * that ends with the {@link WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT} points to a start page
125     * and not to a namespace. The qualification occurs in the transformation
126     * from ref to page.
127     *   For a page: in {@link MarkupRef::getInternalPage()}
128     *   For a media: in the {@link MediaLink::createMediaLinkFromId()}
129     * Because this class is mostly the file representation, it should be able to
130     * represents also a namespace
131     */
132    protected function __construct(string $path, string $drive, string $rev = null)
133    {
134
135        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
136
137        /**
138         * Due to the fact that the request environment is set on the setup in test,
139         * the path may be not normalized
140         */
141        $path = self::normalizeWikiPath($path);
142
143        if (trim($path) === "") {
144            try {
145                $path = WikiPath::getContextPath()->toAbsoluteId();
146            } catch (ExceptionNotFound $e) {
147                throw new ExceptionRuntimeInternal("The context path is unknwon. The empty path string needs it.");
148            }
149        }
150
151        /**
152         * Relative Path ?
153         */
154        $this->absolutePath = $path;
155        $firstCharacter = substr($path, 0, 1);
156        if ($drive === self::MARKUP_DRIVE && $firstCharacter !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) {
157            $parts = preg_split('/' . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . '/', $path);
158            switch ($parts[0]) {
159                case WikiPath::CURRENT_PATH_CHARACTER:
160                    // delete the relative character
161                    $parts = array_splice($parts, 1);
162                    try {
163                        $rootRelativePath = $executionContext->getContextNamespacePath();
164                    } catch (ExceptionNotFound $e) {
165                        // Root case: the relative path is in the root
166                        // the root has no parent
167                        LogUtility::error("The current relative path ({$this->absolutePath}) returns an error: {$e->getMessage()}", self::CANONICAL);
168                        $rootRelativePath = WikiPath::createMarkupPathFromPath(WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT);
169                    }
170                    break;
171                case WikiPath::CURRENT_PARENT_PATH_CHARACTER:
172                    // delete the relative character
173                    $parts = array_splice($parts, 1);
174
175                    $currentPagePath = $executionContext->getContextNamespacePath();
176                    try {
177                        $rootRelativePath = $currentPagePath->getParent();
178                    } catch (ExceptionNotFound $e) {
179                        LogUtility::error("The parent relative path ({$this->absolutePath}) returns an error: {$e->getMessage()}", self::CANONICAL);
180                        $rootRelativePath = $executionContext->getContextNamespacePath();
181                    }
182
183                    break;
184                default:
185                    /**
186                     * just a relative name path
187                     * (ie hallo)
188                     */
189                    $rootRelativePath = $executionContext->getContextNamespacePath();
190                    break;
191            }
192            // is relative directory path ?
193            // ie ..: or .:
194            $isRelativeDirectoryPath = false;
195            $countParts = sizeof($parts);
196            if ($countParts > 0 && $parts[$countParts - 1] === "") {
197                $isRelativeDirectoryPath = true;
198                $parts = array_splice($parts, 0, $countParts - 1);
199            }
200            foreach ($parts as $part) {
201                $rootRelativePath = $rootRelativePath->resolve($part);
202            }
203            $absolutePathString = $rootRelativePath->getAbsolutePath();
204            if ($isRelativeDirectoryPath && !WikiPath::isNamespacePath($absolutePathString)) {
205                $absolutePathString = $absolutePathString . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT;
206            }
207            $this->absolutePath = $absolutePathString;
208        }
209
210
211        /**
212         * ACL check does not care about the type of id
213         * https://www.dokuwiki.org/devel:event:auth_acl_check
214         * https://github.com/splitbrain/dokuwiki/issues/3476
215         *
216         * We check if there is an extension
217         * If this is the case, this is a media
218         */
219        if ($drive === self::UNKNOWN_DRIVE) {
220            $lastPosition = StringUtility::lastIndexOf($path, ".");
221            if ($lastPosition === FALSE) {
222                $drive = self::MARKUP_DRIVE;
223            } else {
224                $drive = self::MEDIA_DRIVE;
225            }
226        }
227        $this->drive = $drive;
228
229
230        /**
231         * We use interwiki to define the combo resources
232         * (Internal use only)
233         */
234        $comboInterWikiScheme = "combo>";
235        if (strpos($this->absolutePath, $comboInterWikiScheme) === 0) {
236            $pathPart = substr($this->absolutePath, strlen($comboInterWikiScheme));
237            $this->id = $this->toDokuWikiIdDriveContextual($pathPart);
238            $this->drive = self::COMBO_DRIVE;
239        } else {
240            WikiPath::addRootSeparatorIfNotPresent($this->absolutePath);
241            $this->id = $this->toDokuWikiIdDriveContextual($this->absolutePath);
242        }
243
244
245        $this->rev = $rev;
246
247    }
248
249
250    /**
251     * For a Markup drive path, a file path should have an extension
252     * if it's not a namespace
253     *
254     * This function checks that
255     *
256     * @param string $parameterPath - the path in a wiki form that may be relative - if the path is blank, it's the current markup (the requested markup)
257     * @param string|null $rev - the revision (ie timestamp in number format)
258     * @return WikiPath - the wiki path
259     * @throws ExceptionBadArgument - if a relative path is given and the context path does not have any parent
260     */
261    public static function createMarkupPathFromPath(string $parameterPath, string $rev = null): WikiPath
262    {
263        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
264
265        if ($parameterPath == "") {
266            return $executionContext->getContextPath();
267        }
268        if (WikiPath::isNamespacePath($parameterPath)) {
269
270            if ($parameterPath[0] !== self::CURRENT_PATH_CHARACTER) {
271                /**
272                 * Not a relative path
273                 */
274                return new WikiPath($parameterPath, self::MARKUP_DRIVE, $rev);
275            }
276            /**
277             * A relative path
278             */
279            $contextPath = $executionContext->getContextPath();
280            if ($parameterPath === self::CURRENT_PARENT_PATH_CHARACTER . self::NAMESPACE_SEPARATOR_DOUBLE_POINT) {
281                /**
282                 * ie processing `..:`
283                 */
284                try {
285                    return $contextPath->getParent()->getParent();
286                } catch (ExceptionNotFound $e) {
287                    throw new ExceptionBadArgument("The context path ($contextPath) does not have a grand parent, therefore the relative path ($parameterPath) is invalid.", $e);
288                }
289            }
290            /**
291             * ie processing `.:`
292             */
293            try {
294                return $contextPath->getParent();
295            } catch (ExceptionNotFound $e) {
296                LogUtility::internalError("A context path is a page and should therefore have a parent", $e);
297            }
298
299        }
300
301        /**
302         * Default Path
303         * (we add the txt extension if not present)
304         */
305        $defaultPath = $parameterPath;
306        $lastName = $parameterPath;
307        $lastSeparator = strrpos($parameterPath, self::NAMESPACE_SEPARATOR_DOUBLE_POINT);
308        if ($lastSeparator !== false) {
309            $lastName = substr($parameterPath, $lastSeparator);
310        }
311        $lastPoint = strpos($lastName, ".");
312        if ($lastPoint === false) {
313            $defaultPath = $defaultPath . '.' . self::MARKUP_DEFAULT_TXT_EXTENSION;
314        } else {
315            /**
316             * Case such as file `1.22`
317             */
318            $parameterPathExtension = substr($lastName, $lastPoint + 1);
319            if (!in_array($parameterPathExtension, self::ALL_MARKUP_EXTENSIONS)) {
320                $defaultPath = $defaultPath . '.' . self::MARKUP_DEFAULT_TXT_EXTENSION;
321            }
322        }
323        $defaultWikiPath = new WikiPath($defaultPath, self::MARKUP_DRIVE, $rev);
324        if (FileSystems::exists($defaultWikiPath)) {
325            return $defaultWikiPath;
326        }
327
328        /**
329         * Markup extension (Markdown, ...)
330         */
331        if (!isset($parameterPathExtension)) {
332            foreach (self::ALL_MARKUP_EXTENSIONS as $markupExtension) {
333                if ($markupExtension == self::MARKUP_DEFAULT_TXT_EXTENSION) {
334                    continue;
335                }
336                $markupWikiPath = new WikiPath($parameterPath . '.' . $markupExtension, self::MARKUP_DRIVE, $rev);
337                if (FileSystems::exists($markupWikiPath)) {
338                    return $markupWikiPath;
339                }
340            }
341        }
342
343        /**
344         * Return the non-existen default wiki path
345         */
346        return $defaultWikiPath;
347
348    }
349
350
351    public
352    static function createMediaPathFromPath($path, $rev = null): WikiPath
353    {
354        return new WikiPath($path, WikiPath::MEDIA_DRIVE, $rev);
355    }
356
357    /**
358     * If the media may come from the
359     * dokuwiki media or combo resources media,
360     * you should use this function
361     *
362     * The constructor will determine the type based on
363     * the id structure.
364     * @param $id
365     * @return WikiPath
366     */
367    public
368    static function createFromUnknownRoot($id): WikiPath
369    {
370        return new WikiPath($id, WikiPath::UNKNOWN_DRIVE);
371    }
372
373    /**
374     * @param $url - a URL path http://whatever/hello/my/lord (The canonical)
375     * @return WikiPath - a dokuwiki Id hello:my:lord
376     * @deprecated for {@link FetcherPage::createPageFragmentFetcherFromUrl()}
377     */
378    public
379    static function createFromUrl($url): WikiPath
380    {
381        // Replace / by : and suppress the first : because the global $ID does not have it
382        $parsedQuery = parse_url($url, PHP_URL_QUERY);
383        $parsedQueryArray = [];
384        parse_str($parsedQuery, $parsedQueryArray);
385        $queryId = 'id';
386        if (array_key_exists($queryId, $parsedQueryArray)) {
387            // Doku form (ie doku.php?id=)
388            $id = $parsedQueryArray[$queryId];
389        } else {
390            // Slash form ie (/my/id)
391            $urlPath = parse_url($url, PHP_URL_PATH);
392            $id = substr(str_replace("/", ":", $urlPath), 1);
393        }
394        return self::createMarkupPathFromPath(":$id");
395    }
396
397    /**
398     * Static don't ask why
399     * @param $pathId
400     * @return false|string
401     */
402    public
403    static function getLastPart($pathId)
404    {
405        $endSeparatorLocation = StringUtility::lastIndexOf($pathId, WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT);
406        if ($endSeparatorLocation === false) {
407            $endSeparatorLocation = StringUtility::lastIndexOf($pathId, WikiPath::NAMESPACE_SEPARATOR_SLASH);
408        }
409        if ($endSeparatorLocation === false) {
410            $lastPathPart = $pathId;
411        } else {
412            $lastPathPart = substr($pathId, $endSeparatorLocation + 1);
413        }
414        return $lastPathPart;
415    }
416
417    /**
418     * @param $id
419     * @return string
420     * Return an path from a id
421     */
422    public
423    static function IdToAbsolutePath($id)
424    {
425        if (is_null($id)) {
426            LogUtility::msg("The id passed should not be null");
427        }
428        return WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . $id;
429    }
430
431    function toDokuWikiIdDriveContextual($path): string
432    {
433        /**
434         * Delete the first separator
435         */
436        $id = self::removeRootSepIfPresent($path);
437
438        /**
439         * If this is a markup, we delete the txt extension if any
440         */
441        if ($this->getDrive() === self::MARKUP_DRIVE) {
442            StringUtility::rtrim($id, '.' . self::MARKUP_DEFAULT_TXT_EXTENSION);
443        }
444        return $id;
445
446    }
447
448    public
449    static function createMediaPathFromId($id, $rev = null): WikiPath
450    {
451        WikiPath::addRootSeparatorIfNotPresent($id);
452        return self::createMediaPathFromPath($id, $rev);
453    }
454
455    public static function getComboCustomThemeHomeDirectory(): WikiPath
456    {
457        return new WikiPath(self::NAMESPACE_SEPARATOR_DOUBLE_POINT, self::COMBO_DATA_THEME_DRIVE);
458    }
459
460    /**
461     * @throws ExceptionBadArgument
462     */
463    public
464    static function createFromUri(string $uri): WikiPath
465    {
466
467        $schemeQualified = WikiFileSystem::SCHEME . "://";
468        $lengthSchemeQualified = strlen($schemeQualified);
469        $uriScheme = substr($uri, 0, $lengthSchemeQualified);
470        if ($uriScheme !== $schemeQualified) {
471            throw new ExceptionBadArgument("The uri ($uri) is not a wiki uri");
472        }
473        $uriWithoutScheme = substr($uri, $lengthSchemeQualified);
474        $locationQuestionMark = strpos($uriWithoutScheme, "?");
475        if ($locationQuestionMark === false) {
476            $pathAndDrive = $uriWithoutScheme;
477            $rev = '';
478        } else {
479            $pathAndDrive = substr($uriWithoutScheme, 0, $locationQuestionMark);
480            $query = substr($uriWithoutScheme, $locationQuestionMark + 1);
481            parse_str($query, $queryKeys);
482            $queryKeys = new ArrayCaseInsensitive($queryKeys);
483            $rev = $queryKeys['rev'];
484        }
485        $locationGreaterThan = strpos($pathAndDrive, ">");
486        if ($locationGreaterThan === false) {
487            $path = $pathAndDrive;
488            $locationLastPoint = strrpos($pathAndDrive, ".");
489            if ($locationLastPoint === false) {
490                $drive = WikiPath::MARKUP_DRIVE;
491            } else {
492                $extension = substr($pathAndDrive, $locationLastPoint + 1);
493                if (in_array($extension, WikiPath::ALL_MARKUP_EXTENSIONS)) {
494                    $drive = WikiPath::MARKUP_DRIVE;
495                } else {
496                    $drive = WikiPath::MEDIA_DRIVE;
497                }
498            }
499        } else {
500            $drive = substr($pathAndDrive, 0, $locationGreaterThan);
501            $path = substr($pathAndDrive, $locationGreaterThan + 1);
502        }
503        return new WikiPath(":$path", $drive, $rev);
504    }
505
506
507    public
508    static function createMarkupPathFromId($id, $rev = null): WikiPath
509    {
510        if (strpos($id, WikiFileSystem::SCHEME . "://") !== false) {
511            return WikiPath::createFromUri($id);
512        }
513        WikiPath::addRootSeparatorIfNotPresent($id);
514        return self::createMarkupPathFromPath($id);
515    }
516
517    /**
518     * If the id does not have a root separator,
519     * it's added (ie to transform an id to a path)
520     * @param string $path
521     */
522    public
523    static function addRootSeparatorIfNotPresent(string &$path)
524    {
525        $firstCharacter = substr($path, 0, 1);
526        if (!in_array($firstCharacter, [WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, WikiPath::CURRENT_PATH_CHARACTER])) {
527            $path = WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . $path;
528        }
529    }
530
531    /**
532     * @param string $relativePath
533     * @return string - a dokuwiki path (replacing the windows or linux path separator to the dokuwiki separator)
534     */
535    public
536    static function toDokuWikiSeparator(string $relativePath): string
537    {
538        return preg_replace('/[\\\\\/]/', ":", $relativePath);
539    }
540
541
542    /**
543     * @param $path - a manual path value
544     * @return string -  a valid path
545     */
546    public
547    static function toValidAbsolutePath($path): string
548    {
549        $path = cleanID($path);
550        WikiPath::addRootSeparatorIfNotPresent($path);
551        return $path;
552    }
553
554    /**
555     */
556    public
557    static function createComboResource($stringPath): WikiPath
558    {
559        return new WikiPath($stringPath, self::COMBO_DRIVE);
560    }
561
562
563    /**
564     * @param $path - relative or absolute path
565     * @param $drive - the drive
566     * @param string $rev - the revision
567     * @return WikiPath
568     */
569    public
570    static function createWikiPath($path, $drive, string $rev = ''): WikiPath
571    {
572        return new WikiPath($path, $drive, $rev);
573    }
574
575    /**
576     * The executing markup
577     * @throws ExceptionNotFound
578     */
579    public
580    static function createExecutingMarkupWikiPath(): WikiPath
581    {
582        return ExecutionContext::getActualOrCreateFromEnv()
583            ->getExecutingWikiPath();
584
585    }
586
587
588    /**
589     * @throws ExceptionNotFound
590     */
591    public
592    static function createRequestedPagePathFromRequest(): WikiPath
593    {
594        return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath();
595    }
596
597    /**
598     * @throws ExceptionBadArgument - if the path is not a local path or is not in a known drive
599     */
600    public
601    static function createFromPathObject(Path $path): WikiPath
602    {
603        if ($path instanceof WikiPath) {
604            return $path;
605        }
606        if (!($path instanceof LocalPath)) {
607            throw new ExceptionBadArgument("The path ($path) is not a local path and cannot be converted to a wiki path");
608        }
609        $driveRoots = WikiPath::getDriveRoots();
610
611        foreach ($driveRoots as $driveRoot => $drivePath) {
612
613            try {
614                $relativePath = $path->relativize($drivePath);
615            } catch (ExceptionBadArgument $e) {
616                /**
617                 * The drive may be a symlink link
618                 * (not the path)
619                 */
620                if (!$drivePath->isSymlink()) {
621                    continue;
622                }
623                try {
624                    $drivePath = $drivePath->toCanonicalAbsolutePath();
625                    $relativePath = $path->relativize($drivePath);
626                } catch (ExceptionBadArgument $e) {
627                    // not a relative path
628                    continue;
629                }
630            }
631            $wikiId = $relativePath->toAbsoluteId();
632            if (FileSystems::isDirectory($path)) {
633                WikiPath::addNamespaceEndSeparatorIfNotPresent($wikiId);
634            }
635            WikiPath::addRootSeparatorIfNotPresent($wikiId);
636            return WikiPath::createWikiPath($wikiId, $driveRoot);
637
638        }
639        throw new ExceptionBadArgument("The local path ($path) is not inside a wiki path drive");
640
641    }
642
643    /**
644     * @return LocalPath[]
645     */
646    public
647    static function getDriveRoots(): array
648    {
649        return [
650            self::MEDIA_DRIVE => Site::getMediaDirectory(),
651            self::MARKUP_DRIVE => Site::getPageDirectory(),
652            self::COMBO_DRIVE => DirectoryLayout::getComboResourcesDirectory(),
653            self::COMBO_DATA_THEME_DRIVE => Site::getDataDirectory()->resolve("combo")->resolve("theme"),
654            self::CACHE_DRIVE => Site::getCacheDirectory()
655        ];
656    }
657
658    /**
659     *
660     * Wiki path system cannot make the difference between a txt file
661     * and a directory natively because there is no extension.
662     *
663     * ie `ns:name` is by default the file `ns:name.txt`
664     *
665     * To make this distinction, we add a `:` at the end
666     *
667     * TODO: May be ? We may also just check if the txt file exists
668     *   and if not if the directory exists
669     *
670     * Also related {@link WikiPath::addNamespaceEndSeparatorIfNotPresent()}
671     *
672     * @param string $namespacePath
673     * @return bool
674     */
675    public
676    static function isNamespacePath(string $namespacePath): bool
677    {
678        if (substr($namespacePath, -1) !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) {
679            return false;
680        }
681        return true;
682
683    }
684
685    /**
686     * @throws ExceptionBadSyntax
687     */
688    public
689    static function checkNamespacePath(string $namespacePath)
690    {
691        if (!self::isNamespacePath($namespacePath)) {
692            throw new ExceptionBadSyntax("The path ($namespacePath) is not a namespace path");
693        }
694    }
695
696    /**
697     * Add a end separator to the wiki path to pass the fact that this is a directory/namespace
698     * See {@link WikiPath::isNamespacePath()} for more info
699     *
700     * @param string $namespaceAttribute
701     * @return void
702     */
703    public
704    static function addNamespaceEndSeparatorIfNotPresent(string &$namespaceAttribute)
705    {
706        if (substr($namespaceAttribute, -1) !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) {
707            $namespaceAttribute = $namespaceAttribute . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT;
708        }
709    }
710
711    /**
712     * @param string $path - path or id
713     * @param string $drive
714     * @param string|null $rev
715     * @return WikiPath
716     */
717    public
718    static function createFromPath(string $path, string $drive, string $rev = null): WikiPath
719    {
720        return new WikiPath($path, $drive, $rev);
721    }
722
723
724    public
725    static function getContextPath(): WikiPath
726    {
727        return ExecutionContext::getActualOrCreateFromEnv()->getContextPath();
728    }
729
730    /**
731     * Normalize a valid id
732     * (ie from / to :)
733     *
734     * @param string $id
735     * @return array|string|string[]
736     *
737     * This is not the same than {@link MarkupRef::normalizePath()}
738     * because there is no relativity or any reserved character in a id
739     *
740     * as an {@link WikiPath::getWikiId() id} is a validated absolute path without root character
741     */
742    public
743    static function normalizeWikiPath(string $id)
744    {
745        return str_replace(WikiPath::NAMESPACE_SEPARATOR_SLASH, WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $id);
746    }
747
748    public
749    static function createRootNamespacePathOnMarkupDrive(): WikiPath
750    {
751        return WikiPath::createMarkupPathFromPath(self::NAMESPACE_SEPARATOR_DOUBLE_POINT);
752    }
753
754    /**
755     * @param $path
756     * @return string with the root path
757     */
758    public
759    static function removeRootSepIfPresent($path): string
760    {
761        $id = $path;
762        if ($id[0] === WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) {
763            return substr($id, 1);
764        }
765        return $id;
766    }
767
768
769    /**
770     * The last part of the path
771     * @throws ExceptionNotFound
772     */
773    public
774    function getLastName(): string
775    {
776        /**
777         * See also {@link noNSorNS}
778         */
779        $names = $this->getNames();
780        $lastName = $names[sizeOf($names) - 1] ?? null;
781        if ($lastName === null) {
782            throw new ExceptionNotFound("This path ($this) does not have any last name");
783        }
784        return $lastName;
785    }
786
787    public
788    function getNames(): array
789    {
790
791        $actualNames = explode(self::NAMESPACE_SEPARATOR_DOUBLE_POINT, $this->absolutePath);
792
793        /**
794         * First element can be an empty string
795         * Case of only one string without path separator
796         * the first element returned is an empty string
797         * Last element can be empty (namespace split, ie :ns:)
798         */
799        $names = [];
800        foreach ($actualNames as $name) {
801            /**
802             * Don't use the {@link empty()} function
803             * In the cache, we may have the directory '0'
804             * and it's empty but is valid name
805             */
806            if ($name !== "") {
807                $names[] = $name;
808            }
809        }
810
811        return $names;
812    }
813
814    /**
815     * @return bool true if this id represents a page
816     */
817    public
818    function isPage(): bool
819    {
820
821        if (
822            $this->drive === self::MARKUP_DRIVE
823            &&
824            !$this->isGlob()
825        ) {
826            return true;
827        } else {
828            return false;
829        }
830
831    }
832
833
834    public
835    function isGlob(): bool
836    {
837        /**
838         * {@link search_universal} triggers ACL check
839         * with id of the form :path:*
840         * (for directory ?)
841         */
842        return StringUtility::endWiths($this->getWikiId(), ":*");
843    }
844
845    public
846    function __toString()
847    {
848        return $this->toUriString();
849    }
850
851    /**
852     *
853     *
854     * @return string - the wiki id is the absolute path
855     * without the root separator (ie normalized)
856     *
857     * The index stores needs this value
858     * And most of the function that are not links related
859     * use this format (What fucked up is fucked up)
860     *
861     * The id is a validated absolute path without any root character.
862     *
863     * Heavily used inside Dokuwiki
864     */
865    public
866    function getWikiId(): string
867    {
868
869        return $this->id;
870
871    }
872
873    public
874    function getPath(): string
875    {
876
877        return $this->absolutePath;
878
879    }
880
881
882    public
883    function getScheme(): string
884    {
885
886        return WikiFileSystem::SCHEME;
887
888    }
889
890    /**
891     * The wiki revision value
892     * as seen in the {@link basicinfo()} function
893     * is the {@link File::getModifiedTime()} of the file
894     *
895     * Let op passing a revision to Dokuwiki will
896     * make it search to the history
897     * The actual file will then not be found
898     *
899     * @return string|null
900     * @throws ExceptionNotFound
901     */
902    public
903    function getRevision(): string
904    {
905        /**
906         * Empty because the value may be null or empty string
907         */
908        if (empty($this->rev)) {
909            throw new ExceptionNotFound("The rev was not set");
910        }
911        return $this->rev;
912    }
913
914    /**
915     *
916     * @throws ExceptionNotFound - if the revision is not set and the path does not exist
917     */
918    public
919    function getRevisionOrDefault()
920    {
921        try {
922            return $this->getRevision();
923        } catch (ExceptionNotFound $e) {
924            // same as $INFO['lastmod'];
925            return FileSystems::getModifiedTime($this)->getTimestamp();
926        }
927
928    }
929
930
931    /**
932     * @return string
933     *
934     * This is the local absolute path WITH the root separator.
935     * It's used in ref present in {@link MarkupRef link} or {@link MediaLink}
936     * when creating test, otherwise the ref is considered as relative
937     *
938     *
939     * Otherwise everywhere in Dokuwiki, they use the {@link WikiPath::getWikiId()} absolute value that does not have any root separator
940     * and is absolute (internal index, function, ...)
941     *
942     */
943    public
944    function getAbsolutePath(): string
945    {
946
947        return $this->absolutePath;
948
949    }
950
951    /**
952     * @return array the pages where the wiki file (page or media) is used
953     *   * backlinks for page
954     *   * page with media for media
955     */
956    public
957    function getReferencedBy(): array
958    {
959        $absoluteId = $this->getWikiId();
960        if ($this->drive == self::MEDIA_DRIVE) {
961            return idx_get_indexer()->lookupKey('relation_media', $absoluteId);
962        } else {
963            return idx_get_indexer()->lookupKey('relation_references', $absoluteId);
964        }
965    }
966
967
968    /**
969     * Return the path relative to the base directory
970     * (ie $conf[basedir])
971     * @return string
972     */
973    public
974    function toRelativeFileSystemPath(): string
975    {
976        $relativeSystemPath = ".";
977        if (!empty($this->getWikiId())) {
978            $relativeSystemPath .= "/" . utf8_encodeFN(str_replace(':', '/', $this->getWikiId()));
979        }
980        return $relativeSystemPath;
981
982    }
983
984    public
985    function isPublic(): bool
986    {
987        return $this->getAuthAclValue() >= AUTH_READ;
988    }
989
990    /**
991     * @return int - An AUTH_ value for this page for the current logged user
992     * See the file defines.php
993     *
994     */
995    public
996    function getAuthAclValue(): int
997    {
998        return auth_quickaclcheck($this->getWikiId());
999    }
1000
1001
1002    public
1003    static function getReservedWords(): array
1004    {
1005        if (self::$reservedWords == null) {
1006            self::$reservedWords = array_merge(Url::RESERVED_WORDS, LocalPath::RESERVED_WINDOWS_CHARACTERS);
1007        }
1008        return self::$reservedWords;
1009    }
1010
1011
1012    /**
1013     * The absolute path for a wiki path
1014     * @return string - the wiki id with a root separator
1015     */
1016    function toAbsoluteId(): string
1017    {
1018        return self::NAMESPACE_SEPARATOR_DOUBLE_POINT . $this->getWikiId();
1019    }
1020
1021
1022    function toAbsolutePath(): Path
1023    {
1024        return new WikiPath($this->absolutePath, $this->drive, $this->rev);
1025    }
1026
1027    /**
1028     * The parent path is a directory (namespace)
1029     * The root path throw an errors
1030     *
1031     * @return WikiPath
1032     * @throws ExceptionNotFound when the root
1033     */
1034    function getParent(): Path
1035    {
1036        /**
1037         * Same as {@link getNS()}
1038         */
1039        $names = $this->getNames();
1040        switch (sizeof($names)) {
1041            case 0:
1042                throw new ExceptionNotFound("The path `{$this}` does not have any parent");
1043            case 1:
1044                return new WikiPath(WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $this->drive, $this->rev);
1045            default:
1046                $names = array_slice($names, 0, sizeof($names) - 1);
1047                $path = implode(WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $names);
1048                /**
1049                 * Because DokuPath does not have the notion of extension
1050                 * if this is a page, we don't known if this is a directory
1051                 * or a page. To make the difference, we add a separator at the end
1052                 */
1053                $sep = self::NAMESPACE_SEPARATOR_DOUBLE_POINT;
1054                $path = "$sep$path$sep";
1055                return new WikiPath($path, $this->drive, $this->rev);
1056        }
1057
1058    }
1059
1060    /**
1061     * @throws ExceptionNotFound
1062     */
1063    function getMime(): Mime
1064    {
1065        if ($this->drive === self::MARKUP_DRIVE) {
1066            return new Mime(Mime::PLAIN_TEXT);
1067        }
1068        return FileSystems::getMime($this);
1069
1070    }
1071
1072
1073    public
1074    function getDrive(): string
1075    {
1076        return $this->drive;
1077    }
1078
1079    public
1080    function resolve(string $name): WikiPath
1081    {
1082
1083        // Directory path have already separator at the end, don't add it
1084        if ($this->absolutePath[strlen($this->absolutePath) - 1] !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) {
1085            $path = $this->absolutePath . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . $name;
1086        } else {
1087            $path = $this->absolutePath . $name;
1088        }
1089        return new WikiPath($path, $this->getDrive());
1090
1091    }
1092
1093
1094    function toUriString(): string
1095    {
1096        $driveSep = self::DRIVE_SEPARATOR;
1097        $absolutePath = self::removeRootSepIfPresent($this->absolutePath);
1098        $uri = "{$this->getScheme()}://$this->drive$driveSep$absolutePath";
1099        if (!empty($this->rev)) {
1100            $uri = "$uri?rev={$this->rev}";
1101        }
1102        return $uri;
1103
1104    }
1105
1106    function getUrl(): Url
1107    {
1108        return $this->toLocalPath()->getUrl();
1109    }
1110
1111    function getHost(): string
1112    {
1113        return "localhost";
1114    }
1115
1116    public
1117    function resolveId($markupId): WikiPath
1118    {
1119        if ($this->getDrive() !== self::MARKUP_DRIVE) {
1120            return $this->resolve($markupId);
1121        }
1122        if (!WikiPath::isNamespacePath($this->absolutePath)) {
1123            try {
1124                $contextId = $this->getParent()->getWikiId() . self::NAMESPACE_SEPARATOR_DOUBLE_POINT;
1125            } catch (ExceptionNotFound $e) {
1126                $contextId = "";
1127            }
1128        } else {
1129            $contextId = $this->getWikiId();
1130        }
1131        return WikiPath::createMarkupPathFromId($contextId . $markupId);
1132
1133    }
1134
1135    /**
1136     * @return LocalPath
1137     * TODO: change it for a constructor on LocalPath
1138     * @throws ExceptionCast
1139     */
1140    public
1141    function toLocalPath(): LocalPath
1142    {
1143        /**
1144         * File path
1145         */
1146        $isNamespacePath = self::isNamespacePath($this->absolutePath);
1147        if ($isNamespacePath) {
1148            /**
1149             * Namespace
1150             * (Fucked up is fucked up)
1151             * We qualify for the namespace here
1152             * because there is no link or media for a namespace
1153             */
1154            global $conf;
1155            switch ($this->drive) {
1156                case self::MEDIA_DRIVE:
1157                    $localPath = LocalPath::createFromPathString($conf['mediadir']);
1158                    break;
1159                case self::MARKUP_DRIVE:
1160                    $localPath = LocalPath::createFromPathString($conf['datadir']);
1161                    break;
1162                default:
1163                    $localPath = WikiPath::getDriveRoots()[$this->drive];
1164                    break;
1165            }
1166
1167            foreach ($this->getNames() as $name) {
1168                $localPath = $localPath->resolve($name);
1169            }
1170            return $localPath;
1171        }
1172
1173        // File
1174        switch ($this->drive) {
1175            case self::MEDIA_DRIVE:
1176                if (!empty($rev)) {
1177                    $filePathString = mediaFN($this->id, $rev);
1178                } else {
1179                    $filePathString = mediaFN($this->id);
1180                }
1181                break;
1182            case self::MARKUP_DRIVE:
1183                /**
1184                 * Adaptation of {@link WikiFN}
1185                 */
1186                global $conf;
1187                try {
1188                    $extension = $this->getExtension();
1189                } catch (ExceptionNotFound $e) {
1190                    LogUtility::internalError("For a markup path file, the extension should have been set. This is not the case for ($this)");
1191                    $extension = self::MARKUP_DEFAULT_TXT_EXTENSION;
1192                }
1193                $idFileSystem = str_replace(':', '/', $this->id);
1194                if (empty($this->rev)) {
1195                    $filePathString = Site::getPageDirectory()->resolve(utf8_encodeFN($idFileSystem) . '.' . $extension)->toAbsoluteId();
1196                } else {
1197                    $filePathString = Site::getOldDirectory()->resolve(utf8_encodeFN($idFileSystem) . '.' . $this->rev . '.' . $extension)->toAbsoluteId();
1198                    if ($conf['compression']) {
1199                        //test for extensions here, we want to read both compressions
1200                        if (file_exists($filePathString . '.gz')) {
1201                            $filePathString .= '.gz';
1202                        } elseif (file_exists($filePathString . '.bz2')) {
1203                            $filePathString .= '.bz2';
1204                        } else {
1205                            // File doesnt exist yet, so we take the configured extension
1206                            $filePathString .= '.' . $conf['compression'];
1207                        }
1208                    }
1209                }
1210
1211                break;
1212            default:
1213                $baseDirectory = WikiPath::getDriveRoots()[$this->drive];
1214                if ($baseDirectory === null) {
1215                    // We don't throw, the file will just not exist
1216                    // this is metadata
1217                    throw new ExceptionCast("The drive ($this->drive) is unknown, the local file system path could not be found");
1218                }
1219                $filePath = $baseDirectory;
1220                foreach ($this->getNames() as $name) {
1221                    $filePath = $filePath->resolve($name);
1222                }
1223                $filePathString = $filePath->toAbsoluteId();
1224                break;
1225        }
1226        return LocalPath::createFromPathString($filePathString);
1227
1228    }
1229
1230    public function hasRevision(): bool
1231    {
1232        try {
1233            $this->getRevision();
1234            return true;
1235        } catch (ExceptionNotFound $e) {
1236            return false;
1237        }
1238    }
1239
1240}
1241