128af4b67SAndreas Gohr<?php 228af4b67SAndreas Gohr 328af4b67SAndreas Gohrnamespace dokuwiki\plugin\filelist; 428af4b67SAndreas Gohr 528af4b67SAndreas Gohrclass Crawler 628af4b67SAndreas Gohr{ 728af4b67SAndreas Gohr /** @var string regexp to check extensions */ 828af4b67SAndreas Gohr protected $ext; 928af4b67SAndreas Gohr 1028af4b67SAndreas Gohr /** @var string */ 1128af4b67SAndreas Gohr protected $sortby = 'name'; 1228af4b67SAndreas Gohr 1328af4b67SAndreas Gohr /** @var bool */ 1428af4b67SAndreas Gohr protected $sortreverse = false; 1528af4b67SAndreas Gohr 16d6a45e5cSAndreas Gohr /** @var string[] patterns to ignore */ 17d6a45e5cSAndreas Gohr protected $ignore = []; 18d6a45e5cSAndreas Gohr 1928af4b67SAndreas Gohr /** 2028af4b67SAndreas Gohr * Initializes the crawler 2128af4b67SAndreas Gohr * 2228af4b67SAndreas Gohr * @param string $extensions The extensions to allow (comma separated list) 2328af4b67SAndreas Gohr */ 2428af4b67SAndreas Gohr public function __construct($extensions) 2528af4b67SAndreas Gohr { 2628af4b67SAndreas Gohr $this->ext = explode(',', $extensions); 2728af4b67SAndreas Gohr $this->ext = array_map('trim', $this->ext); 2828af4b67SAndreas Gohr $this->ext = array_map('preg_quote_cb', $this->ext); 2928af4b67SAndreas Gohr $this->ext = implode('|', $this->ext); 30d6a45e5cSAndreas Gohr 31d6a45e5cSAndreas Gohr $this->ignore = $this->loadIgnores(); 3228af4b67SAndreas Gohr } 3328af4b67SAndreas Gohr 3428af4b67SAndreas Gohr public function setSortBy($sortby) 3528af4b67SAndreas Gohr { 3628af4b67SAndreas Gohr $this->sortby = $sortby; 3728af4b67SAndreas Gohr } 3828af4b67SAndreas Gohr 3928af4b67SAndreas Gohr public function setSortReverse($sortreverse) 4028af4b67SAndreas Gohr { 4128af4b67SAndreas Gohr $this->sortreverse = $sortreverse; 4228af4b67SAndreas Gohr } 4328af4b67SAndreas Gohr 4428af4b67SAndreas Gohr /** 4528af4b67SAndreas Gohr * Does a (recursive) crawl for finding files based on a given pattern. 4628af4b67SAndreas Gohr * Based on a safe glob reimplementation using fnmatch and opendir. 4728af4b67SAndreas Gohr * 4828af4b67SAndreas Gohr * @param string $path the path to search in 4928af4b67SAndreas Gohr * @param string $pattern the pattern to match to 5028af4b67SAndreas Gohr * @param bool $recursive whether to search recursively 5128af4b67SAndreas Gohr * @param string $titlefile the name of the title file 5228af4b67SAndreas Gohr * @return array a hierarchical filelist or false if nothing could be found 5328af4b67SAndreas Gohr * 5428af4b67SAndreas Gohr * @see http://www.php.net/manual/en/function.glob.php#71083 5528af4b67SAndreas Gohr */ 5628af4b67SAndreas Gohr public function crawl($root, $local, $pattern, $recursive, $titlefile) 5728af4b67SAndreas Gohr { 58*0aa34e23SAndreas Gohr $path = Path::toAbsolute($root . $local); 5928af4b67SAndreas Gohr 60e82754c5SAndreas Gohr // do not descent into wiki or data directories 61e82754c5SAndreas Gohr if (Path::isWikiControlled($path)) return []; 62e82754c5SAndreas Gohr 6328af4b67SAndreas Gohr if (($dir = opendir($path)) === false) return []; 6428af4b67SAndreas Gohr $result = []; 6528af4b67SAndreas Gohr while (($file = readdir($dir)) !== false) { 6628af4b67SAndreas Gohr if ($file[0] == '.' || $file == $titlefile) { 6728af4b67SAndreas Gohr // ignore hidden, system and title files 6828af4b67SAndreas Gohr continue; 6928af4b67SAndreas Gohr } 70*0aa34e23SAndreas Gohr // join without introducing leading or doubled slashes (local may be empty or end in /) 71*0aa34e23SAndreas Gohr $self = ($local === '') ? $file : rtrim($local, '/') . '/' . $file; 72*0aa34e23SAndreas Gohr $filepath = rtrim($path, '/') . '/' . $file; 7328af4b67SAndreas Gohr if (!is_readable($filepath)) continue; 7428af4b67SAndreas Gohr 7528af4b67SAndreas Gohr if ($this->fnmatch($pattern, $file) || (is_dir($filepath) && $recursive)) { 7628af4b67SAndreas Gohr if (!is_dir($filepath) && !$this->isExtensionAllowed($file)) { 7728af4b67SAndreas Gohr continue; 7828af4b67SAndreas Gohr } 79d6a45e5cSAndreas Gohr if ($this->isFileIgnored($file)) { 80d6a45e5cSAndreas Gohr continue; 81d6a45e5cSAndreas Gohr } 8228af4b67SAndreas Gohr 8328af4b67SAndreas Gohr // get title file 8428af4b67SAndreas Gohr $filename = $file; 8528af4b67SAndreas Gohr if (is_dir($filepath)) { 8628af4b67SAndreas Gohr $title = $filepath . '/' . $titlefile; 8728af4b67SAndreas Gohr if (is_readable($title)) { 8828af4b67SAndreas Gohr $filename = io_readFile($title, false); 8928af4b67SAndreas Gohr } 9028af4b67SAndreas Gohr } 9128af4b67SAndreas Gohr 9228af4b67SAndreas Gohr // prepare entry 9328af4b67SAndreas Gohr if (!is_dir($filepath) || $recursive) { 9428af4b67SAndreas Gohr $entry = [ 9528af4b67SAndreas Gohr 'name' => $filename, 9628af4b67SAndreas Gohr 'local' => $self, 9728af4b67SAndreas Gohr 'path' => $filepath, 9828af4b67SAndreas Gohr 'mtime' => filemtime($filepath), 9928af4b67SAndreas Gohr 'ctime' => filectime($filepath), 10028af4b67SAndreas Gohr 'size' => filesize($filepath), 10128af4b67SAndreas Gohr 'children' => ((is_dir($filepath) && $recursive) ? 10228af4b67SAndreas Gohr $this->crawl($root, $self, $pattern, $recursive, $titlefile) : 10328af4b67SAndreas Gohr false 10428af4b67SAndreas Gohr ), 10528af4b67SAndreas Gohr 'treesize' => 0, 10628af4b67SAndreas Gohr ]; 10728af4b67SAndreas Gohr 10828af4b67SAndreas Gohr // calculate tree size 10928af4b67SAndreas Gohr if ($entry['children'] !== false) { 11028af4b67SAndreas Gohr foreach ($entry['children'] as $child) { 11128af4b67SAndreas Gohr $entry['treesize'] += $child['treesize']; 11228af4b67SAndreas Gohr } 11328af4b67SAndreas Gohr } else { 11428af4b67SAndreas Gohr $entry['treesize'] = 1; 11528af4b67SAndreas Gohr } 11628af4b67SAndreas Gohr 11728af4b67SAndreas Gohr // add entry to result 11828af4b67SAndreas Gohr $result[] = $entry; 11928af4b67SAndreas Gohr } 12028af4b67SAndreas Gohr } 12128af4b67SAndreas Gohr } 12228af4b67SAndreas Gohr closedir($dir); 12328af4b67SAndreas Gohr return $this->sortItems($result); 12428af4b67SAndreas Gohr } 12528af4b67SAndreas Gohr 12628af4b67SAndreas Gohr /** 12728af4b67SAndreas Gohr * Sort the given items by the current sortby and sortreverse settings 12828af4b67SAndreas Gohr * 12928af4b67SAndreas Gohr * @param array $items 13028af4b67SAndreas Gohr * @return array 13128af4b67SAndreas Gohr */ 13228af4b67SAndreas Gohr protected function sortItems($items) 13328af4b67SAndreas Gohr { 13428af4b67SAndreas Gohr $callback = [$this, 'compare' . ucfirst($this->sortby)]; 13528af4b67SAndreas Gohr if (!is_callable($callback)) return $items; 13628af4b67SAndreas Gohr 13728af4b67SAndreas Gohr usort($items, $callback); 13828af4b67SAndreas Gohr if ($this->sortreverse) { 13928af4b67SAndreas Gohr $items = array_reverse($items); 14028af4b67SAndreas Gohr } 14128af4b67SAndreas Gohr return $items; 14228af4b67SAndreas Gohr } 14328af4b67SAndreas Gohr 14428af4b67SAndreas Gohr /** 14528af4b67SAndreas Gohr * Check if a file is allowed by the configured extensions 14628af4b67SAndreas Gohr * 14728af4b67SAndreas Gohr * @param string $file 14828af4b67SAndreas Gohr * @return bool 14928af4b67SAndreas Gohr */ 15028af4b67SAndreas Gohr protected function isExtensionAllowed($file) 15128af4b67SAndreas Gohr { 15228af4b67SAndreas Gohr if ($this->ext === '') return true; // no restriction 15328af4b67SAndreas Gohr return preg_match('/(' . $this->ext . ')$/i', $file); 15428af4b67SAndreas Gohr } 15528af4b67SAndreas Gohr 156d6a45e5cSAndreas Gohr /** 157d6a45e5cSAndreas Gohr * Check if a file is ignored by the ignore patterns 158d6a45e5cSAndreas Gohr * 159d6a45e5cSAndreas Gohr * @param string $file 160d6a45e5cSAndreas Gohr * @return bool 161d6a45e5cSAndreas Gohr */ 162d6a45e5cSAndreas Gohr protected function isFileIgnored($file) 163d6a45e5cSAndreas Gohr { 164d6a45e5cSAndreas Gohr foreach ($this->ignore as $pattern) { 165d6a45e5cSAndreas Gohr if ($this->fnmatch($pattern, $file)) return true; 166d6a45e5cSAndreas Gohr } 167d6a45e5cSAndreas Gohr return false; 168d6a45e5cSAndreas Gohr } 169d6a45e5cSAndreas Gohr 170d6a45e5cSAndreas Gohr /** 171d6a45e5cSAndreas Gohr * Load the ignore patterns from the ignore.txt file 172d6a45e5cSAndreas Gohr * 173d6a45e5cSAndreas Gohr * @return string[] 174d6a45e5cSAndreas Gohr */ 175d6a45e5cSAndreas Gohr protected function loadIgnores() 176d6a45e5cSAndreas Gohr { 177d6a45e5cSAndreas Gohr $file = __DIR__ . '/conf/ignore.txt'; 178d6a45e5cSAndreas Gohr $ignore = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 17946bd41a8Ssplitbrain $ignore = array_map(static fn($line) => trim(preg_replace('/\s*#.*$/', '', $line)), $ignore); 180d6a45e5cSAndreas Gohr $ignore = array_filter($ignore); 181d6a45e5cSAndreas Gohr return $ignore; 182d6a45e5cSAndreas Gohr } 18328af4b67SAndreas Gohr 18428af4b67SAndreas Gohr /** 18528af4b67SAndreas Gohr * Replacement for fnmatch() for windows systems. 18628af4b67SAndreas Gohr * 18728af4b67SAndreas Gohr * @author jk at ricochetsolutions dot com 18828af4b67SAndreas Gohr * @link http://www.php.net/manual/en/function.fnmatch.php#71725 18928af4b67SAndreas Gohr */ 19028af4b67SAndreas Gohr protected function fnmatch($pattern, $string) 19128af4b67SAndreas Gohr { 19228af4b67SAndreas Gohr return preg_match( 19328af4b67SAndreas Gohr "#^" . strtr( 19428af4b67SAndreas Gohr preg_quote($pattern, '#'), 19528af4b67SAndreas Gohr [ 19628af4b67SAndreas Gohr '\*' => '.*', 19728af4b67SAndreas Gohr '\?' => '.', 19828af4b67SAndreas Gohr '\[' => '[', 19928af4b67SAndreas Gohr '\]' => ']' 20028af4b67SAndreas Gohr ] 20128af4b67SAndreas Gohr ) . "$#i", 20228af4b67SAndreas Gohr $string 20328af4b67SAndreas Gohr ); 20428af4b67SAndreas Gohr } 20528af4b67SAndreas Gohr 20628af4b67SAndreas Gohr public function compareName($a, $b) 20728af4b67SAndreas Gohr { 20828af4b67SAndreas Gohr return strcmp($a['name'], $b['name']); 20928af4b67SAndreas Gohr } 21028af4b67SAndreas Gohr 21128af4b67SAndreas Gohr public function compareIname($a, $b) 21228af4b67SAndreas Gohr { 21328af4b67SAndreas Gohr return strcmp(strtolower($a['name']), strtolower($b['name'])); 21428af4b67SAndreas Gohr } 21528af4b67SAndreas Gohr 21628af4b67SAndreas Gohr public function compareCtime($a, $b) 21728af4b67SAndreas Gohr { 21828af4b67SAndreas Gohr return $a['ctime'] <=> $b['ctime']; 21928af4b67SAndreas Gohr } 22028af4b67SAndreas Gohr 22128af4b67SAndreas Gohr public function compareMtime($a, $b) 22228af4b67SAndreas Gohr { 22328af4b67SAndreas Gohr return $a['mtime'] <=> $b['mtime']; 22428af4b67SAndreas Gohr } 22528af4b67SAndreas Gohr 22628af4b67SAndreas Gohr public function compareSize($a, $b) 22728af4b67SAndreas Gohr { 22828af4b67SAndreas Gohr return $a['size'] <=> $b['size']; 22928af4b67SAndreas Gohr } 23028af4b67SAndreas Gohr} 231