1<?php
2
3namespace dokuwiki\plugin\filelist;
4
5class Path
6{
7    protected $paths = [];
8
9    /**
10     * @param string $pathConfig The path configuration ftom the plugin settings
11     */
12    public function __construct($pathConfig)
13    {
14        $this->paths = $this->parsePathConfig($pathConfig);
15    }
16
17    /**
18     * Access the parsed paths
19     *
20     * @return array
21     */
22    public function getPaths()
23    {
24        return $this->paths;
25    }
26
27    /**
28     * Parse the path configuration into an internal array
29     *
30     * roots (and aliases) are always saved with a trailing slash
31     *
32     * @return array
33     */
34    protected function parsePathConfig($pathConfig)
35    {
36        $paths = [];
37        $lines = explode("\n", $pathConfig);
38        $lastRoot = '';
39        foreach ($lines as $line) {
40            $line = trim($line);
41            if (empty($line)) {
42                continue;
43            }
44
45            if (str_starts_with($line, 'A>')) {
46                // this is an alias for the last read root
47                $line = trim(substr($line, 2));
48                if (!isset($paths[$lastRoot])) continue; // no last root, no alias
49                $alias = static::cleanPath($line);
50                $paths[$lastRoot]['alias'] = $alias;
51                $paths[$alias] = &$paths[$lastRoot]; // alias references the original
52            } elseif (str_starts_with($line, 'W>')) {
53                // this is a web path for the last read root
54                $line = trim(substr($line, 2));
55                if (!isset($paths[$lastRoot])) continue; // no last path, no web path
56                $paths[$lastRoot]['web'] = $line;
57            } else {
58                // this is a new path
59                $line = static::cleanPath($line);
60                $lastRoot = $line;
61                $paths[$line] = [
62                    'root' => $line,
63                    'web' => DOKU_BASE . 'lib/plugins/filelist/file.php?root=' . rawurlencode($line) . '&file=',
64                ];
65            }
66        }
67
68        return $paths;
69    }
70
71    /**
72     * Check if a given path is listable and return it's configuration
73     *
74     * @param string $path
75     * @param bool $addTrailingSlash
76     * @return array
77     * @throws \Exception if the given path is not allowed
78     */
79    public function getPathInfo($path, $addTrailingSlash = true)
80    {
81        $path = static::cleanPath($path, $addTrailingSlash);
82
83        $paths = $this->paths;
84        if ($paths === []) {
85            throw new \Exception('No paths configured');
86        }
87
88        $allowed = array_keys($paths);
89        usort($allowed, static fn($a, $b) => strlen($a) - strlen($b));
90        $allowed = array_map('preg_quote_cb', $allowed);
91        $regex = '/^(' . implode('|', $allowed) . ')/';
92
93        if (!preg_match($regex, $path, $matches)) {
94            throw new \Exception('Path not allowed: ' . $path);
95        }
96        $match = $matches[1];
97
98        $pathInfo = $paths[$match];
99        $pathInfo['local'] = substr($path, strlen($match));
100        $pathInfo['path'] = $pathInfo['root'] . $pathInfo['local'];
101
102
103        return $pathInfo;
104    }
105
106    /**
107     * Clean a path for better comparison
108     *
109     * Converts all backslashes to forward slashes
110     * Keeps leading double backslashes for UNC paths
111     * Ensure a single trailing slash unless disabled
112     *
113     * @param string $path
114     * @return string
115     */
116    public static function cleanPath($path, $addTrailingSlash = true)
117    {
118        if (str_starts_with($path, '\\\\')) {
119            $unc = '\\\\';
120        } else {
121            $unc = '';
122        }
123        $path = ltrim($path, '\\');
124        $path = str_replace('\\', '/', $path);
125        $path = self::realpath($path);
126        if ($addTrailingSlash) {
127            $path = rtrim($path, '/');
128            $path .= '/';
129        }
130
131        return $unc . $path;
132    }
133
134    /**
135     * Canonicalizes a given path. A bit like realpath, but without the resolving of symlinks.
136     *
137     * @author anonymous
138     * @see <http://www.php.net/manual/en/function.realpath.php#73563>
139     */
140    public static function realpath($path)
141    {
142        $path = explode('/', $path);
143        $output = [];
144        $counter = count($path);
145        for ($i = 0; $i < $counter; $i++) {
146            if ('.' == $path[$i]) continue;
147            if ('' === $path[$i] && $i > 0) continue;
148            if ('..' == $path[$i] && '..' != ($output[count($output) - 1] ?? '')) {
149                array_pop($output);
150                continue;
151            }
152            $output[] = $path[$i];
153        }
154        return implode('/', $output);
155    }
156
157    /**
158     * Check if the given path is within the data or dokuwiki dir
159     *
160     * This whould prevent accidental or deliberate circumvention of the ACLs
161     *
162     * @param string $path and already cleaned path
163     * @return bool
164     */
165    public static function isWikiControlled($path)
166    {
167        global $conf;
168        $dataPath = self::cleanPath($conf['savedir']);
169        if (str_starts_with($path, $dataPath)) {
170            return true;
171        }
172        $wikiDir = self::cleanPath(DOKU_INC);
173        if (str_starts_with($path, $wikiDir)) {
174            return true;
175        }
176        return false;
177    }
178}
179