xref: /plugin/filelist/Path.php (revision 0aa34e23175a488654959797676dd61149b41609)
128af4b67SAndreas Gohr<?php
228af4b67SAndreas Gohr
328af4b67SAndreas Gohrnamespace dokuwiki\plugin\filelist;
428af4b67SAndreas Gohr
528af4b67SAndreas Gohrclass Path
628af4b67SAndreas Gohr{
728af4b67SAndreas Gohr    protected $paths = [];
828af4b67SAndreas Gohr
928af4b67SAndreas Gohr    /**
1028af4b67SAndreas Gohr     * @param string $pathConfig The path configuration ftom the plugin settings
1128af4b67SAndreas Gohr     */
1228af4b67SAndreas Gohr    public function __construct($pathConfig)
1328af4b67SAndreas Gohr    {
1428af4b67SAndreas Gohr        $this->paths = $this->parsePathConfig($pathConfig);
1528af4b67SAndreas Gohr    }
1628af4b67SAndreas Gohr
1728af4b67SAndreas Gohr    /**
181eeb87d7SAndreas Gohr     * Access the parsed paths
191eeb87d7SAndreas Gohr     *
201eeb87d7SAndreas Gohr     * @return array
211eeb87d7SAndreas Gohr     */
221eeb87d7SAndreas Gohr    public function getPaths()
231eeb87d7SAndreas Gohr    {
241eeb87d7SAndreas Gohr        return $this->paths;
251eeb87d7SAndreas Gohr    }
261eeb87d7SAndreas Gohr
271eeb87d7SAndreas Gohr    /**
2828af4b67SAndreas Gohr     * Parse the path configuration into an internal array
2928af4b67SAndreas Gohr     *
3028af4b67SAndreas Gohr     * roots (and aliases) are always saved with a trailing slash
3128af4b67SAndreas Gohr     *
3228af4b67SAndreas Gohr     * @return array
3328af4b67SAndreas Gohr     */
3428af4b67SAndreas Gohr    protected function parsePathConfig($pathConfig)
3528af4b67SAndreas Gohr    {
3628af4b67SAndreas Gohr        $paths = [];
3728af4b67SAndreas Gohr        $lines = explode("\n", $pathConfig);
3828af4b67SAndreas Gohr        $lastRoot = '';
3928af4b67SAndreas Gohr        foreach ($lines as $line) {
4028af4b67SAndreas Gohr            $line = trim($line);
4128af4b67SAndreas Gohr            if (empty($line)) {
4228af4b67SAndreas Gohr                continue;
4328af4b67SAndreas Gohr            }
4428af4b67SAndreas Gohr
4528af4b67SAndreas Gohr            if (str_starts_with($line, 'A>')) {
4628af4b67SAndreas Gohr                // this is an alias for the last read root
4728af4b67SAndreas Gohr                $line = trim(substr($line, 2));
4828af4b67SAndreas Gohr                if (!isset($paths[$lastRoot])) continue; // no last root, no alias
4905f444a4SAndreas Gohr                $alias = static::cleanPath($line);
5028af4b67SAndreas Gohr                $paths[$lastRoot]['alias'] = $alias;
5128af4b67SAndreas Gohr                $paths[$alias] = &$paths[$lastRoot]; // alias references the original
5228af4b67SAndreas Gohr            } elseif (str_starts_with($line, 'W>')) {
5328af4b67SAndreas Gohr                // this is a web path for the last read root
5428af4b67SAndreas Gohr                $line = trim(substr($line, 2));
5528af4b67SAndreas Gohr                if (!isset($paths[$lastRoot])) continue; // no last path, no web path
5628af4b67SAndreas Gohr                $paths[$lastRoot]['web'] = $line;
5728af4b67SAndreas Gohr            } else {
5828af4b67SAndreas Gohr                // this is a new path
5905f444a4SAndreas Gohr                $line = static::cleanPath($line);
6028af4b67SAndreas Gohr                $lastRoot = $line;
6128af4b67SAndreas Gohr                $paths[$line] = [
6228af4b67SAndreas Gohr                    'root' => $line,
6328af4b67SAndreas Gohr                    'web' => DOKU_BASE . 'lib/plugins/filelist/file.php?root=' . rawurlencode($line) . '&file=',
6428af4b67SAndreas Gohr                ];
6528af4b67SAndreas Gohr            }
6628af4b67SAndreas Gohr        }
6728af4b67SAndreas Gohr
6828af4b67SAndreas Gohr        return $paths;
6928af4b67SAndreas Gohr    }
7028af4b67SAndreas Gohr
7128af4b67SAndreas Gohr    /**
7228af4b67SAndreas Gohr     * Check if a given path is listable and return it's configuration
7328af4b67SAndreas Gohr     *
7428af4b67SAndreas Gohr     * @param string $path
7528af4b67SAndreas Gohr     * @param bool $addTrailingSlash
7628af4b67SAndreas Gohr     * @return array
7728af4b67SAndreas Gohr     * @throws \Exception if the given path is not allowed
7828af4b67SAndreas Gohr     */
7928af4b67SAndreas Gohr    public function getPathInfo($path, $addTrailingSlash = true)
8028af4b67SAndreas Gohr    {
8105f444a4SAndreas Gohr        $path = static::cleanPath($path, $addTrailingSlash);
8228af4b67SAndreas Gohr
8328af4b67SAndreas Gohr        $paths = $this->paths;
8423c781f8SAndreas Gohr        if ($paths === []) {
8523c781f8SAndreas Gohr            throw new \Exception('No paths configured');
8623c781f8SAndreas Gohr        }
8723c781f8SAndreas Gohr
8828af4b67SAndreas Gohr        $allowed = array_keys($paths);
8905f444a4SAndreas Gohr        usort($allowed, static fn($a, $b) => strlen($a) - strlen($b));
9028af4b67SAndreas Gohr        $allowed = array_map('preg_quote_cb', $allowed);
9128af4b67SAndreas Gohr        $regex = '/^(' . implode('|', $allowed) . ')/';
9228af4b67SAndreas Gohr
9328af4b67SAndreas Gohr        if (!preg_match($regex, $path, $matches)) {
9428af4b67SAndreas Gohr            throw new \Exception('Path not allowed: ' . $path);
9528af4b67SAndreas Gohr        }
9628af4b67SAndreas Gohr        $match = $matches[1];
9728af4b67SAndreas Gohr
9828af4b67SAndreas Gohr        $pathInfo = $paths[$match];
9928af4b67SAndreas Gohr        $pathInfo['local'] = substr($path, strlen($match));
100*0aa34e23SAndreas Gohr        $pathInfo['path'] = static::toAbsolute($pathInfo['root'] . $pathInfo['local']);
10128af4b67SAndreas Gohr
10228af4b67SAndreas Gohr
10328af4b67SAndreas Gohr        return $pathInfo;
10428af4b67SAndreas Gohr    }
10528af4b67SAndreas Gohr
10628af4b67SAndreas Gohr    /**
10728af4b67SAndreas Gohr     * Clean a path for better comparison
10828af4b67SAndreas Gohr     *
10928af4b67SAndreas Gohr     * Converts all backslashes to forward slashes
11028af4b67SAndreas Gohr     * Keeps leading double backslashes for UNC paths
11128af4b67SAndreas Gohr     * Ensure a single trailing slash unless disabled
11228af4b67SAndreas Gohr     *
11328af4b67SAndreas Gohr     * @param string $path
11428af4b67SAndreas Gohr     * @return string
11528af4b67SAndreas Gohr     */
11628af4b67SAndreas Gohr    public static function cleanPath($path, $addTrailingSlash = true)
11728af4b67SAndreas Gohr    {
11828af4b67SAndreas Gohr        if (str_starts_with($path, '\\\\')) {
11928af4b67SAndreas Gohr            $unc = '\\\\';
12028af4b67SAndreas Gohr        } else {
12128af4b67SAndreas Gohr            $unc = '';
12228af4b67SAndreas Gohr        }
12328af4b67SAndreas Gohr        $path = ltrim($path, '\\');
12428af4b67SAndreas Gohr        $path = str_replace('\\', '/', $path);
12528af4b67SAndreas Gohr        $path = self::realpath($path);
12628af4b67SAndreas Gohr        if ($addTrailingSlash) {
12728af4b67SAndreas Gohr            $path = rtrim($path, '/');
12828af4b67SAndreas Gohr            $path .= '/';
12928af4b67SAndreas Gohr        }
13028af4b67SAndreas Gohr
13128af4b67SAndreas Gohr        return $unc . $path;
13228af4b67SAndreas Gohr    }
13328af4b67SAndreas Gohr
13428af4b67SAndreas Gohr    /**
13528af4b67SAndreas Gohr     * Canonicalizes a given path. A bit like realpath, but without the resolving of symlinks.
13628af4b67SAndreas Gohr     *
13728af4b67SAndreas Gohr     * @author anonymous
13828af4b67SAndreas Gohr     * @see <http://www.php.net/manual/en/function.realpath.php#73563>
13928af4b67SAndreas Gohr     */
14028af4b67SAndreas Gohr    public static function realpath($path)
14128af4b67SAndreas Gohr    {
14228af4b67SAndreas Gohr        $path = explode('/', $path);
14328af4b67SAndreas Gohr        $output = [];
14428af4b67SAndreas Gohr        $counter = count($path);
14528af4b67SAndreas Gohr        for ($i = 0; $i < $counter; $i++) {
14628af4b67SAndreas Gohr            if ('.' == $path[$i]) continue;
14728af4b67SAndreas Gohr            if ('' === $path[$i] && $i > 0) continue;
1481eeb87d7SAndreas Gohr            if ('..' == $path[$i] && '..' != ($output[count($output) - 1] ?? '')) {
14928af4b67SAndreas Gohr                array_pop($output);
15028af4b67SAndreas Gohr                continue;
15128af4b67SAndreas Gohr            }
15228af4b67SAndreas Gohr            $output[] = $path[$i];
15328af4b67SAndreas Gohr        }
15428af4b67SAndreas Gohr        return implode('/', $output);
15528af4b67SAndreas Gohr    }
156e82754c5SAndreas Gohr
157e82754c5SAndreas Gohr    /**
158*0aa34e23SAndreas Gohr     * Check if the given (already cleaned) path is absolute
159*0aa34e23SAndreas Gohr     *
160*0aa34e23SAndreas Gohr     * Recognizes unix paths, Windows drive letters and UNC paths.
161*0aa34e23SAndreas Gohr     *
162*0aa34e23SAndreas Gohr     * @param string $path an already cleaned path
163*0aa34e23SAndreas Gohr     * @return bool
164*0aa34e23SAndreas Gohr     */
165*0aa34e23SAndreas Gohr    public static function isAbsolute($path)
166*0aa34e23SAndreas Gohr    {
167*0aa34e23SAndreas Gohr        if (str_starts_with($path, '/')) return true; // unix
168*0aa34e23SAndreas Gohr        if (str_starts_with($path, '\\\\')) return true; // UNC
169*0aa34e23SAndreas Gohr        if (preg_match('/^[a-zA-Z]:/', $path)) return true; // windows drive letter
170*0aa34e23SAndreas Gohr        return false;
171*0aa34e23SAndreas Gohr    }
172*0aa34e23SAndreas Gohr
173*0aa34e23SAndreas Gohr    /**
174*0aa34e23SAndreas Gohr     * Resolve a (cleaned) path to an absolute one
175*0aa34e23SAndreas Gohr     *
176*0aa34e23SAndreas Gohr     * Relative paths are resolved against the DokuWiki directory (DOKU_INC), the same way
177*0aa34e23SAndreas Gohr     * DokuWiki itself treats relative config paths. This makes path handling independent of
178*0aa34e23SAndreas Gohr     * the current working directory, which otherwise differs between the wiki renderer
179*0aa34e23SAndreas Gohr     * (doku.php, cwd = wiki root) and the file delivery script (file.php, cwd = plugin dir).
180*0aa34e23SAndreas Gohr     *
181*0aa34e23SAndreas Gohr     * Absolute paths (unix, Windows drive letters, UNC) are returned unchanged.
182*0aa34e23SAndreas Gohr     *
183*0aa34e23SAndreas Gohr     * @param string $path an already cleaned path
184*0aa34e23SAndreas Gohr     * @return string
185*0aa34e23SAndreas Gohr     */
186*0aa34e23SAndreas Gohr    public static function toAbsolute($path)
187*0aa34e23SAndreas Gohr    {
188*0aa34e23SAndreas Gohr        if (self::isAbsolute($path)) {
189*0aa34e23SAndreas Gohr            return $path;
190*0aa34e23SAndreas Gohr        }
191*0aa34e23SAndreas Gohr        return self::cleanPath(DOKU_INC, false) . '/' . $path;
192*0aa34e23SAndreas Gohr    }
193*0aa34e23SAndreas Gohr
194*0aa34e23SAndreas Gohr    /**
195e82754c5SAndreas Gohr     * Check if the given path is within the data or dokuwiki dir
196e82754c5SAndreas Gohr     *
197e82754c5SAndreas Gohr     * This whould prevent accidental or deliberate circumvention of the ACLs
198e82754c5SAndreas Gohr     *
199*0aa34e23SAndreas Gohr     * Both the given path and the wiki/data directories are resolved to absolute paths
200*0aa34e23SAndreas Gohr     * before comparison. Without this, a relatively configured root (e.g. "firmware") would
201*0aa34e23SAndreas Gohr     * never match the absolute DOKU_INC and the check would be silently bypassed.
202*0aa34e23SAndreas Gohr     *
203e82754c5SAndreas Gohr     * @param string $path and already cleaned path
204e82754c5SAndreas Gohr     * @return bool
205e82754c5SAndreas Gohr     */
206e82754c5SAndreas Gohr    public static function isWikiControlled($path)
207e82754c5SAndreas Gohr    {
208e82754c5SAndreas Gohr        global $conf;
209*0aa34e23SAndreas Gohr        $path = self::toAbsolute($path);
210*0aa34e23SAndreas Gohr
211*0aa34e23SAndreas Gohr        $dataPath = self::toAbsolute(self::cleanPath($conf['savedir']));
212e82754c5SAndreas Gohr        if (str_starts_with($path, $dataPath)) {
213e82754c5SAndreas Gohr            return true;
214e82754c5SAndreas Gohr        }
215*0aa34e23SAndreas Gohr        $wikiDir = self::toAbsolute(self::cleanPath(DOKU_INC));
216e82754c5SAndreas Gohr        if (str_starts_with($path, $wikiDir)) {
217e82754c5SAndreas Gohr            return true;
218e82754c5SAndreas Gohr        }
219e82754c5SAndreas Gohr        return false;
220e82754c5SAndreas Gohr    }
22128af4b67SAndreas Gohr}
222