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