1<?php
2
3namespace ComboStrap;
4
5require_once(__DIR__ . '/PluginUtility.php');
6
7/**
8 * Class DokuPath
9 * @package ComboStrap
10 * A dokuwiki path
11 *
12 */
13class DokuPath extends PathAbs
14{
15    const MEDIA_TYPE = "media";
16    const PAGE_TYPE = "page";
17    const UNKNOWN_TYPE = "unknown";
18    const PATH_SEPARATOR = ":";
19
20    // https://www.dokuwiki.org/config:useslash
21    const SEPARATOR_SLASH = "/";
22
23    const SEPARATORS = [self::PATH_SEPARATOR, self::SEPARATOR_SLASH];
24
25    /**
26     * For whatever reason, dokuwiki uses also on windows
27     * the linux separator
28     */
29    public const DIRECTORY_SEPARATOR = "/";
30    public const SLUG_SEPARATOR = "-";
31
32    const RESOURCE_TYPE = "resource";
33    /**
34     * Dokuwiki know as file system starts at page and media
35     * This parameters permits to add another one
36     * that starts at the resource directory
37     */
38    public const WIKI_FS_TYPE = "wiki-fs-type";
39
40    /**
41     * @var string[]
42     */
43    private static $reservedWords;
44
45    /**
46     * @var string the path id passed to function (cleaned)
47     */
48    private $id;
49
50    /**
51     * @var string the absolute id with the root separator
52     * See {@link $id} for the absolute id without root separator for the index
53     */
54    private $absolutePath;
55
56    /**
57     * @var string
58     */
59    private $finalType;
60    /**
61     * @var string|null - ie mtime
62     */
63    private $rev;
64
65    /**
66     * @var string a value with an absolute id without the root
67     * used in the index (ie the id)
68     */
69    private $qualifiedId;
70
71    /**
72     * @var string the path scheme one constant that starts with SCHEME
73     * ie
74     * {@link DokuFs::SCHEME},
75     * {@link InternetPath::scheme},
76     * {@link InterWikiPath::scheme}
77     */
78    private $scheme;
79    private $filePath;
80
81
82    /**
83     * DokuPath constructor.
84     *
85     * A path for the Dokuwiki File System
86     *
87     * @param string $absolutePath - the dokuwiki absolute path (may not be relative but may be a namespace)
88     * @param string $type - the type (media, page)
89     * @param string|null $rev - the revision (mtime)
90     *
91     * Thee path should be a qualified/absolute path because in Dokuwiki, a link to a {@link Page}
92     * that ends with the {@link DokuPath::PATH_SEPARATOR} points to a start page
93     * and not to a namespace. The qualification occurs in the transformation
94     * from ref to page.
95     *   For a page: in {@link LinkUtility::getInternalPage()}
96     *   For a media: in the {@link MediaLink::createMediaLinkFromId()}
97     * Because this class is mostly the file representation, it should be able to
98     * represents also a namespace
99     */
100    protected function __construct(string $absolutePath, string $type, string $rev = null)
101    {
102        DokuPath::addRootSeparatorIfNotPresent($absolutePath);
103        if (empty($absolutePath)) {
104            LogUtility::msg("A null path was given", LogUtility::LVL_MSG_WARNING);
105        }
106        $this->absolutePath = $absolutePath;
107
108
109        // Check whether this is a local or remote image or interwiki
110        if (media_isexternal($absolutePath)) {
111
112            $this->scheme = InternetPath::scheme;
113
114        } else if (link_isinterwiki($absolutePath)) {
115
116            $this->scheme = InterWikiPath::scheme;
117
118        } else {
119
120            if (substr($absolutePath, 0, 1) !== DokuPath::PATH_SEPARATOR) {
121                if (PluginUtility::isDevOrTest()) {
122                    // Feel too much the log, test are not seeing anything, may be minimap ?
123                    LogUtility::msg("The path given ($absolutePath) is not qualified", LogUtility::LVL_MSG_ERROR);
124                }
125                $this->absolutePath = ":" . $absolutePath;
126            }
127            if (substr($absolutePath, 1, 1) === DokuPath::PATH_SEPARATOR) {
128                /**
129                 * path given is `::path`
130                 */
131                if (PluginUtility::isDevOrTest()) {
132                    LogUtility::msg("The path given ($absolutePath) has too much separator", LogUtility::LVL_MSG_ERROR);
133                }
134            }
135            $this->scheme = DokuFs::SCHEME;
136
137        }
138
139
140        /**
141         * ACL check does not care about the type of id
142         * https://www.dokuwiki.org/devel:event:auth_acl_check
143         * https://github.com/splitbrain/dokuwiki/issues/3476
144         *
145         * We check if there is an extension
146         * If this is the case, this is a media
147         */
148        if ($type == self::UNKNOWN_TYPE) {
149            $lastPosition = StringUtility::lastIndexOf($absolutePath, ".");
150            if ($lastPosition === FALSE) {
151                $type = self::PAGE_TYPE;
152            } else {
153                $type = self::MEDIA_TYPE;
154            }
155        }
156        $this->finalType = $type;
157        $this->rev = $rev;
158
159        /**
160         * File path
161         */
162        $filePath = $this->absolutePath;
163        if ($this->scheme == DokuFs::SCHEME) {
164
165            $this->id = DokuPath::toDokuwikiId($this->absolutePath);
166            $isNamespacePath = false;
167            if (\mb_substr($this->absolutePath, -1) == self::PATH_SEPARATOR) {
168                $isNamespacePath = true;
169            }
170
171            global $ID;
172
173            if (!$isNamespacePath) {
174
175                switch ($type) {
176
177                    case self::MEDIA_TYPE:
178                        if (!empty($rev)) {
179                            $filePath = mediaFN($this->id, $rev);
180                        } else {
181                            $filePath = mediaFN($this->id);
182                        }
183                        break;
184                    case self::PAGE_TYPE:
185                        if (!empty($rev)) {
186                            $filePath = wikiFN($this->id, $rev);
187                        } else {
188                            $filePath = wikiFN($this->id);
189                        }
190                        break;
191                    case self::RESOURCE_TYPE:
192                        $relativeFsPath = DokuPath::toFileSystemSeparator($this->id);
193                        $filePath = Resources::getAbsoluteResourcesDirectory() . DIRECTORY_SEPARATOR . $relativeFsPath;
194                        break;
195                    default:
196                        LogUtility::msg("Wiki Path Type ($type) is unknown, the local file system path could not be found", LogUtility::LVL_MSG_ERROR);
197
198                }
199            } else {
200                /**
201                 * Namespace
202                 * (Fucked up is fucked up)
203                 * We qualify for the namespace here
204                 * because there is no link or media for a namespace
205                 */
206                $this->id = resolve_id(getNS($ID), $this->id, true);
207                global $conf;
208                if ($type == self::MEDIA_TYPE) {
209                    $filePath = $conf['mediadir'] . '/' . utf8_encodeFN($this->id);
210                } else {
211                    $filePath = $conf['datadir'] . '/' . utf8_encodeFN($this->id);
212                }
213            }
214        }
215        $this->filePath = $filePath;
216    }
217
218
219    /**
220     *
221     * @param $absolutePath
222     * @return DokuPath
223     */
224    public static function createPagePathFromPath($absolutePath): DokuPath
225    {
226        return new DokuPath($absolutePath, DokuPath::PAGE_TYPE);
227    }
228
229    public static function createMediaPathFromAbsolutePath($absolutePath, $rev = ''): DokuPath
230    {
231        return new DokuPath($absolutePath, DokuPath::MEDIA_TYPE, $rev);
232    }
233
234    public static function createUnknownFromIdOrPath($id): DokuPath
235    {
236        DokuPath::addRootSeparatorIfNotPresent($id);
237        return new DokuPath($id, DokuPath::UNKNOWN_TYPE);
238    }
239
240    /**
241     * @param $url - a URL path http://whatever/hello/my/lord (The canonical)
242     * @return DokuPath - a dokuwiki Id hello:my:lord
243     */
244    public static function createFromUrl($url): DokuPath
245    {
246        // Replace / by : and suppress the first : because the global $ID does not have it
247        $parsedQuery = parse_url($url, PHP_URL_QUERY);
248        $parsedQueryArray = [];
249        parse_str($parsedQuery, $parsedQueryArray);
250        $queryId = 'id';
251        if (array_key_exists($queryId, $parsedQueryArray)) {
252            // Doku form (ie doku.php?id=)
253            $id = $parsedQueryArray[$queryId];
254        } else {
255            // Slash form ie (/my/id)
256            $urlPath = parse_url($url, PHP_URL_PATH);
257            $id = substr(str_replace("/", ":", $urlPath), 1);
258        }
259        return self::createPagePathFromPath(":$id");
260    }
261
262    /**
263     * Static don't ask why
264     * @param $pathId
265     * @return false|string
266     */
267    public static function getLastPart($pathId)
268    {
269        $endSeparatorLocation = StringUtility::lastIndexOf($pathId, DokuPath::PATH_SEPARATOR);
270        if ($endSeparatorLocation === false) {
271            $endSeparatorLocation = StringUtility::lastIndexOf($pathId, DokuPath::SEPARATOR_SLASH);
272        }
273        if ($endSeparatorLocation === false) {
274            $lastPathPart = $pathId;
275        } else {
276            $lastPathPart = substr($pathId, $endSeparatorLocation + 1);
277        }
278        return $lastPathPart;
279    }
280
281    /**
282     * @param $id
283     * @return string
284     * Return an path from a id
285     */
286    public static function IdToAbsolutePath($id)
287    {
288        if (is_null($id)) {
289            LogUtility::msg("The id passed should not be null");
290        }
291        return DokuPath::PATH_SEPARATOR . $id;
292    }
293
294    public
295    static function toDokuwikiId($absolutePath)
296    {
297        // Root ?
298        if ($absolutePath == DokuPath::PATH_SEPARATOR) {
299            return "";
300        }
301        if ($absolutePath[0] === DokuPath::PATH_SEPARATOR) {
302            return substr($absolutePath, 1);
303        }
304        return $absolutePath;
305
306    }
307
308    public static function createMediaPathFromId($id, $rev = ''): DokuPath
309    {
310        DokuPath::addRootSeparatorIfNotPresent($id);
311        return self::createMediaPathFromAbsolutePath($id, $rev);
312    }
313
314
315    public static function createPagePathFromId($id): DokuPath
316    {
317        return new DokuPath(DokuPath::PATH_SEPARATOR . $id, self::PAGE_TYPE);
318    }
319
320    /**
321     * If the path does not have a root separator,
322     * it's added (ie to transform an id to a path)
323     * @param string $path
324     */
325    public static function addRootSeparatorIfNotPresent(string &$path)
326    {
327        if (substr($path, 0, 1) !== ":") {
328            $path = DokuPath::PATH_SEPARATOR . $path;
329        }
330    }
331
332    /**
333     * @param string $relativePath
334     * @return string - a dokuwiki path (replacing the windows or linux path separator to the dokuwiki separator)
335     */
336    public static function toDokuWikiSeparator(string $relativePath): string
337    {
338        return preg_replace('/[\\\\\/]/', ":", $relativePath);
339    }
340
341    public static function toFileSystemSeparator($dokuPath)
342    {
343        return str_replace(":", self::DIRECTORY_SEPARATOR, $dokuPath);
344    }
345
346    /**
347     * @param $path - a manual path value
348     * @return string -  a valid path
349     */
350    public static function toValidAbsolutePath($path): string
351    {
352        $path = cleanID($path);
353        DokuPath::addRootSeparatorIfNotPresent($path);
354        return $path;
355    }
356
357    public static function createResource($dokuwikiId): DokuPath
358    {
359        return new DokuPath($dokuwikiId, self::RESOURCE_TYPE);
360    }
361
362    public static function createDokuPath($mediaId, $type, $rev = ''): DokuPath
363    {
364        return new DokuPath($mediaId, $type, $rev);
365    }
366
367
368    /**
369     * The last part of the path
370     */
371    public
372    function getLastName()
373    {
374        /**
375         * See also {@link noNSorNS}
376         */
377        $names = $this->getNames();
378        return $names[sizeOf($names) - 1];
379    }
380
381    /**
382     * @return string
383     */
384    public function getLastNameWithoutExtension(): string
385    {
386        /**
387         * A page doku path has no extension for now
388         */
389        if ($this->finalType === self::PAGE_TYPE) {
390            return $this->getLastName();
391        }
392        return parent::getLastNameWithoutExtension();
393    }
394
395
396    public
397    function getNames()
398    {
399
400        $names = explode(self::PATH_SEPARATOR, $this->getDokuwikiId());
401
402        if ($names[0] === "") {
403            /**
404             * Case of only one string without path separator
405             * the first element returned is an empty string
406             */
407            $names = array_splice($names, 1);
408        }
409        return $names;
410    }
411
412    /**
413     * @return bool true if this id represents a page
414     */
415    public function isPage(): bool
416    {
417
418        if (
419            $this->finalType === self::PAGE_TYPE
420            &&
421            !$this->isGlob()
422        ) {
423            return true;
424        } else {
425            return false;
426        }
427
428    }
429
430
431    public function isGlob(): bool
432    {
433        /**
434         * {@link search_universal} triggers ACL check
435         * with id of the form :path:*
436         * (for directory ?)
437         */
438        return StringUtility::endWiths($this->getDokuwikiId(), ":*");
439    }
440
441    public
442    function __toString()
443    {
444        return $this->getDokuwikiId();
445    }
446
447    /**
448     *
449     *
450     * @return string - the id of dokuwiki is the absolute path
451     * without the root separator (ie normalized)
452     *
453     * The index stores needs this value
454     * And most of the function that are not links related
455     * use this format (What fucked up is fucked up)
456     * /**
457     * The absolute path without root separator
458     * Heavily used inside Dokuwiki
459     */
460    public
461    function getDokuwikiId(): string
462    {
463
464        if ($this->getScheme() == DokuFs::SCHEME) {
465            return $this->id;
466        } else {
467            // the url (it's stored as id in the metadata)
468            return $this->getPath();
469        }
470
471    }
472
473    public
474    function getPath(): string
475    {
476
477        return $this->absolutePath;
478
479    }
480
481    public
482    function getScheme()
483    {
484
485        return $this->scheme;
486
487    }
488
489    /**
490     * The dokuwiki revision value
491     * as seen in the {@link basicinfo()} function
492     * is the {@link File::getModifiedTime()} of the file
493     *
494     * Let op passing a revision to Dokuwiki will
495     * make ti search to the history
496     * The actual file will then not be found
497     *
498     * @return string|null
499     */
500    public
501    function getRevision()
502    {
503        return $this->rev;
504    }
505
506
507    /**
508     * @return string
509     *
510     * This is the local absolute path WITH the root separator.
511     * It's used in ref present in {@link LinkUtility link} or {@link MediaLink}
512     * when creating test, otherwise the ref is considered as relative
513     *
514     *
515     * Otherwise everywhere in Dokuwiki, they use the {@link DokuPath::getDokuwikiId()} absolute value that does not have any root separator
516     * and is absolute (internal index, function, ...)
517     *
518     */
519    public
520    function getAbsolutePath(): string
521    {
522
523        return $this->absolutePath;
524
525    }
526
527    /**
528     * @return array the pages where the dokuwiki file (page or media) is used
529     *   * backlinks for page
530     *   * page with media for media
531     */
532    public
533    function getReferencedBy(): array
534    {
535        $absoluteId = $this->getDokuwikiId();
536        if ($this->finalType == self::MEDIA_TYPE) {
537            return idx_get_indexer()->lookupKey('relation_media', $absoluteId);
538        } else {
539            return idx_get_indexer()->lookupKey('relation_references', $absoluteId);
540        }
541    }
542
543
544    /**
545     * Return the path relative to the base directory
546     * (ie $conf[basedir])
547     * @return string
548     */
549    public
550    function toRelativeFileSystemPath()
551    {
552        $relativeSystemPath = ".";
553        if (!empty($this->getDokuwikiId())) {
554            $relativeSystemPath .= "/" . utf8_encodeFN(str_replace(':', '/', $this->getDokuwikiId()));
555        }
556        return $relativeSystemPath;
557
558    }
559
560    public function isPublic(): bool
561    {
562        return $this->getAuthAclValue() >= AUTH_READ;
563    }
564
565    /**
566     * @return int - An AUTH_ value for this page for the current logged user
567     * See the file defines.php
568     *
569     */
570    public function getAuthAclValue(): int
571    {
572        return auth_quickaclcheck($this->getDokuwikiId());
573    }
574
575
576    public static function getReservedWords(): array
577    {
578        if (self::$reservedWords == null) {
579            $reservedWords = array_merge(Url::RESERVED_WORDS, LocalPath::RESERVED_WINDOWS_CHARACTERS);
580            self::$reservedWords = array_filter($reservedWords, function ($e) {
581                return $e != DokuPath::PATH_SEPARATOR;
582            });
583        }
584        return self::$reservedWords;
585    }
586
587    /**
588     * @return string - a label from a path (used in link when their is no label available)
589     * The path is separated in words and every word gets an uppercase letter
590     */
591    public function toLabel(): string
592    {
593        $words = preg_split("/\s/", preg_replace("/-|_|:/", " ", $this->getPath()));
594        $wordsUc = [];
595        foreach ($words as $word) {
596            $wordsUc[] = ucfirst($word);
597        }
598        return implode(" ", $wordsUc);
599    }
600
601    public function toLocalPath(): LocalPath
602    {
603        return LocalPath::create($this->filePath);
604    }
605
606    function toString(): string
607    {
608        return $this->absolutePath;
609    }
610
611    function toUriString(): string
612    {
613        $string = "{$this->scheme}://$this->finalType/$this->id";
614        if ($this->rev !== null) {
615            return "$string?rev={$this->rev}";
616        }
617        return $string;
618    }
619
620    function toAbsolutePath(): Path
621    {
622        return new DokuPath($this->absolutePath, $this->finalType, $this->rev);
623    }
624
625    /**
626     * The parent path is a directory (namespace)
627     * The parent of page in the root does return the root namespace.
628     *
629     * @return DokuPath|null
630     */
631    function getParent(): ?Path
632    {
633
634        /**
635         * Same as {@link getNS()}
636         */
637        $names = $this->getNames();
638        switch (sizeof($names)) {
639            case 0:
640                return null;
641            case 1:
642                return new DokuPath(DokuPath::PATH_SEPARATOR, $this->finalType, $this->rev);
643            default:
644                $names = array_slice($names, 0, sizeof($names) - 1);
645                $path = implode(DokuPath::PATH_SEPARATOR, $names);
646                return new DokuPath($path, $this->finalType, $this->rev);
647        }
648
649    }
650
651    function getMime(): ?Mime
652    {
653        if ($this->finalType === self::PAGE_TYPE) {
654            return new Mime(Mime::PLAIN_TEXT);
655        }
656        return parent::getMime();
657
658    }
659
660    public function getType(): string
661    {
662        return $this->finalType;
663    }
664}
665