xref: /plugin/filelist/Crawler.php (revision 0aa34e23175a488654959797676dd61149b41609) !
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