paths = $this->parsePathConfig($pathConfig); } /** * Access the parsed paths * * @return array */ public function getPaths() { return $this->paths; } /** * Parse the path configuration into an internal array * * roots (and aliases) are always saved with a trailing slash * * @return array */ protected function parsePathConfig($pathConfig) { $paths = []; $lines = explode("\n", $pathConfig); $lastRoot = ''; foreach ($lines as $line) { $line = trim($line); if (empty($line)) { continue; } if (str_starts_with($line, 'A>')) { // this is an alias for the last read root $line = trim(substr($line, 2)); if (!isset($paths[$lastRoot])) continue; // no last root, no alias $alias = static::cleanPath($line); $paths[$lastRoot]['alias'] = $alias; $paths[$alias] = &$paths[$lastRoot]; // alias references the original } elseif (str_starts_with($line, 'W>')) { // this is a web path for the last read root $line = trim(substr($line, 2)); if (!isset($paths[$lastRoot])) continue; // no last path, no web path $paths[$lastRoot]['web'] = $line; } else { // this is a new path $line = static::cleanPath($line); $lastRoot = $line; $paths[$line] = [ 'root' => $line, 'web' => DOKU_BASE . 'lib/plugins/filelist/file.php?root=' . rawurlencode($line) . '&file=', ]; } } return $paths; } /** * Check if a given path is listable and return it's configuration * * @param string $path * @param bool $addTrailingSlash * @return array * @throws \Exception if the given path is not allowed */ public function getPathInfo($path, $addTrailingSlash = true) { $path = static::cleanPath($path, $addTrailingSlash); $paths = $this->paths; if ($paths === []) { throw new \Exception('No paths configured'); } $allowed = array_keys($paths); usort($allowed, static fn($a, $b) => strlen($a) - strlen($b)); $allowed = array_map('preg_quote_cb', $allowed); $regex = '/^(' . implode('|', $allowed) . ')/'; if (!preg_match($regex, $path, $matches)) { throw new \Exception('Path not allowed: ' . $path); } $match = $matches[1]; $pathInfo = $paths[$match]; $pathInfo['local'] = substr($path, strlen($match)); $pathInfo['path'] = static::toAbsolute($pathInfo['root'] . $pathInfo['local']); return $pathInfo; } /** * Clean a path for better comparison * * Converts all backslashes to forward slashes * Keeps leading double backslashes for UNC paths * Ensure a single trailing slash unless disabled * * @param string $path * @return string */ public static function cleanPath($path, $addTrailingSlash = true) { if (str_starts_with($path, '\\\\')) { $unc = '\\\\'; } else { $unc = ''; } $path = ltrim($path, '\\'); $path = str_replace('\\', '/', $path); $path = self::realpath($path); if ($addTrailingSlash) { $path = rtrim($path, '/'); $path .= '/'; } return $unc . $path; } /** * Canonicalizes a given path. A bit like realpath, but without the resolving of symlinks. * * @author anonymous * @see */ public static function realpath($path) { $path = explode('/', $path); $output = []; $counter = count($path); for ($i = 0; $i < $counter; $i++) { if ('.' == $path[$i]) continue; if ('' === $path[$i] && $i > 0) continue; if ('..' == $path[$i] && '..' != ($output[count($output) - 1] ?? '')) { array_pop($output); continue; } $output[] = $path[$i]; } return implode('/', $output); } /** * Check if the given (already cleaned) path is absolute * * Recognizes unix paths, Windows drive letters and UNC paths. * * @param string $path an already cleaned path * @return bool */ public static function isAbsolute($path) { if (str_starts_with($path, '/')) return true; // unix if (str_starts_with($path, '\\\\')) return true; // UNC if (preg_match('/^[a-zA-Z]:/', $path)) return true; // windows drive letter return false; } /** * Resolve a (cleaned) path to an absolute one * * Relative paths are resolved against the DokuWiki directory (DOKU_INC), the same way * DokuWiki itself treats relative config paths. This makes path handling independent of * the current working directory, which otherwise differs between the wiki renderer * (doku.php, cwd = wiki root) and the file delivery script (file.php, cwd = plugin dir). * * Absolute paths (unix, Windows drive letters, UNC) are returned unchanged. * * @param string $path an already cleaned path * @return string */ public static function toAbsolute($path) { if (self::isAbsolute($path)) { return $path; } return self::cleanPath(DOKU_INC, false) . '/' . $path; } /** * Check if the given path is within the data or dokuwiki dir * * This whould prevent accidental or deliberate circumvention of the ACLs * * Both the given path and the wiki/data directories are resolved to absolute paths * before comparison. Without this, a relatively configured root (e.g. "firmware") would * never match the absolute DOKU_INC and the check would be silently bypassed. * * @param string $path and already cleaned path * @return bool */ public static function isWikiControlled($path) { global $conf; $path = self::toAbsolute($path); $dataPath = self::toAbsolute(self::cleanPath($conf['savedir'])); if (str_starts_with($path, $dataPath)) { return true; } $wikiDir = self::toAbsolute(self::cleanPath(DOKU_INC)); if (str_starts_with($path, $wikiDir)) { return true; } return false; } }