1<?php
2
3namespace dokuwiki\plugin\versionswitch;
4
5use dokuwiki\File\PageResolver;
6
7/**
8 * Manage the current page's version
9 */
10class Version
11{
12    protected $regex = '';
13    protected $version = '';
14    protected $namespace = '';
15    protected $idpart = '';
16
17    public const DEFAULT_REGEX = '[^:]+';
18
19    /**
20     * @param string $conf The configuration string containing the namespaces and regexes
21     * @param string $id The current page id
22     */
23    public function __construct($conf, $id)
24    {
25        $this->match($this->conf2List($conf), $id);
26    }
27
28    /**
29     * The regex that applies to the current namespace
30     *
31     * This will be empty if the current page is not in a versioned namespace
32     *
33     * @return string
34     */
35    public function getRegex(): string
36    {
37        return $this->regex;
38    }
39
40    /**
41     * The base namespace that applies to the current page
42     *
43     * This will be empty if the current page is not in a versioned namespace
44     *
45     * @return string
46     */
47    public function getBaseNamespace(): string
48    {
49        return $this->namespace;
50    }
51
52    /**
53     * The version that applies to the current page
54     *
55     * This will be empty if the current page is not in a versioned namespace
56     *
57     * @return string
58     */
59    public function getVersion(): string
60    {
61        return $this->version;
62    }
63
64    /**
65     * The part of the id that comes after the version namespace
66     *
67     * @return string
68     */
69    public function getIdPart(): string
70    {
71        return $this->idpart;
72    }
73
74
75    /**
76     * Convert the list of namespaces and regexes into an associative array
77     *
78     * @param string $conf
79     * @return array
80     */
81    protected function conf2List($conf)
82    {
83        $result = [];
84        $list = explode("\n", $conf);
85        foreach ($list as $line) {
86            $line = trim($line);
87            if ($line == '') {
88                continue;
89            }
90            if ($line[0] == '#') {
91                continue;
92            }
93            [$ns, $re] = sexplode(' ', $line, 2, '');
94            $ns = ':' . cleanID($ns);
95            $re = trim($re);
96            if ($re === '') $re = self::DEFAULT_REGEX; // default is direct namespaces
97
98            $result[$ns] = $re;
99        }
100
101        return $result;
102    }
103
104    /**
105     * Try the given regexes against the namespace of the given id
106     *
107     * @param array $regexes
108     * @param string $id
109     * @return bool true if a match was found
110     */
111    protected function match($regexes, $id)
112    {
113        $namespace = ':' . getNS($id);
114
115        foreach ($regexes as $base => $re) {
116            $regex = "/$re/i";
117
118            // match against base namespace first
119            if (str_starts_with($namespace, $base)) {
120                $namespace = substr($namespace, strlen($base));
121
122                // match remainder against regex
123                if (preg_match($regex, $namespace, $matches)) {
124                    $this->namespace = $base;
125                    $this->regex = $re;
126                    $this->version = $matches[0];
127                    $this->idpart = substr($id, strlen($base) + strlen($matches[0]) + 1);
128
129                    return true;
130                }
131            }
132        }
133
134        return false;
135    }
136
137    /**
138     * Get all versions and their titles
139     *
140     * @return array
141     */
142    public function getVersions()
143    {
144        $versions = $this->readVersionDirs();
145
146        // get titles for the versions
147        $resolver = new PageResolver('start'); // context doesn't matter, we always use absolute IDs
148        foreach (array_keys($versions) as $ns) {
149            $startPage = $resolver->resolveId($this->namespace . ':' . $ns . ':');
150            $title = p_get_first_heading($startPage,);
151            if ($title) $versions[$ns] = $title;
152        }
153        uksort($versions, [$this, 'sortByDepthAndVersion']);
154
155        return $versions;
156    }
157
158    /**
159     * Sort by depth of the namespace and by version
160     *
161     * @param string $a
162     * @param string $b
163     * @return int
164     */
165    public function sortByDepthAndVersion($a, $b)
166    {
167        $countA = substr_count($a, ':');
168        $countB = substr_count($b, ':');
169        if ($countA !== $countB) return $countA - $countB;
170        return version_compare($b, $a); // reverse order
171    }
172
173    /**
174     * Traverse the version directories and find all versions
175     *
176     * @param string $dir The base directory to start in, defaults to the set namespace
177     * @param string $sub The currently traversed sub directory
178     * @return array
179     */
180    protected function readVersionDirs($dir = '', $sub = '')
181    {
182        if ($dir === '') $dir = dirname(wikiFN($this->namespace . ':somthing', '', false));
183        $subns = utf8_decodeFN($sub);
184        $regex = '/^' . $this->regex . '$/i'; // anchored regex
185        $versions = [];
186
187        $fh = @opendir($dir . '/' . $sub);
188        if (!$fh) return [];
189        while (($item = readdir($fh)) !== false) {
190            if ($item[0] == '.') continue;
191            if (!is_dir($dir . '/' . $sub . '/' . $item)) continue;
192
193            $itemid = utf8_decodeFN($item);
194
195            // check if this is a version namespace
196            if (preg_match($regex, ltrim("$subns:$itemid", ':'), $match)) {
197                $versions[ltrim("$subns:$itemid", ':')] = $match[0];
198            }
199
200            // traverse into sub namespace unless default regex is used
201            if ($this->regex !== self::DEFAULT_REGEX) {
202                $versions = array_merge($versions, $this->readVersionDirs($dir, ltrim("$sub/$item", '/')));
203            }
204        }
205        closedir($fh);
206        return $versions;
207    }
208}
209