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