1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5/**
6 * Class Assignments
7 *
8 * Manages the assignment of schemas (table names) to pages and namespaces
9 *
10 * This is a singleton. Assignment data is only loaded once per request.
11 *
12 * @package dokuwiki\plugin\struct\meta
13 */
14class Assignments
15{
16    /** @var \helper_plugin_sqlite|null */
17    protected $sqlite;
18
19    /** @var  array All the assignments patterns */
20    protected $patterns;
21
22    /** @var Assignments */
23    protected static $instance = null;
24
25    /**
26     * Get the singleton instance of the Assignments
27     *
28     * @param bool $forcereload create a new instace to reload the assignment data
29     * @return Assignments
30     */
31    public static function getInstance($forcereload = false)
32    {
33        if (is_null(self::$instance) or $forcereload) {
34            $class = get_called_class();
35            self::$instance = new $class();
36        }
37        return self::$instance;
38    }
39
40    /**
41     * Assignments constructor.
42     *
43     * Not public. Use Assignments::getInstance() instead
44     */
45    protected function __construct()
46    {
47        /** @var \helper_plugin_struct_db $helper */
48        $helper = plugin_load('helper', 'struct_db');
49        $this->sqlite = $helper->getDB();
50
51        $this->loadPatterns();
52    }
53
54
55    /**
56     * Load existing assignment patterns
57     */
58    protected function loadPatterns()
59    {
60        $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern';
61        $res = $this->sqlite->query($sql);
62        $this->patterns = $this->sqlite->res2arr($res);
63        $this->sqlite->res_close($res);
64    }
65
66    /**
67     * Add a new assignment pattern to the pattern table
68     *
69     * @param string $pattern
70     * @param string $table
71     * @return bool
72     */
73    public function addPattern($pattern, $table)
74    {
75        // add the pattern
76        $sql = 'REPLACE INTO schema_assignments_patterns (pattern, tbl) VALUES (?,?)';
77        $ok = (bool)$this->sqlite->query($sql, array($pattern, $table));
78
79        // reload patterns
80        $this->loadPatterns();
81        $this->propagatePageAssignments($table);
82
83
84        return $ok;
85    }
86
87    /**
88     * Remove an existing assignment pattern from the pattern table
89     *
90     * @param string $pattern
91     * @param string $table
92     * @return bool
93     */
94    public function removePattern($pattern, $table)
95    {
96        // remove the pattern
97        $sql = 'DELETE FROM schema_assignments_patterns WHERE pattern = ? AND tbl = ?';
98        $ok = (bool)$this->sqlite->query($sql, array($pattern, $table));
99
100        // reload patterns
101        $this->loadPatterns();
102
103        // fetch possibly affected pages
104        $sql = 'SELECT pid FROM schema_assignments WHERE tbl = ?';
105        $res = $this->sqlite->query($sql, $table);
106        $pagerows = $this->sqlite->res2arr($res);
107        $this->sqlite->res_close($res);
108
109        // reevalute the pages and unassign when needed
110        foreach ($pagerows as $row) {
111            $tables = $this->getPageAssignments($row['pid'], true);
112            if (!in_array($table, $tables)) {
113                $this->deassignPageSchema($row['pid'], $table);
114            }
115        }
116
117        return $ok;
118    }
119
120    /**
121     * Rechecks all assignments of a given page against the current patterns
122     *
123     * @param string $pid
124     */
125    public function reevaluatePageAssignments($pid)
126    {
127        // reload patterns
128        $this->loadPatterns();
129        $tables = $this->getPageAssignments($pid, true);
130
131        // fetch possibly affected tables
132        $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?';
133        $res = $this->sqlite->query($sql, $pid);
134        $tablerows = $this->sqlite->res2arr($res);
135        $this->sqlite->res_close($res);
136
137        // reevalute the tables and apply assignments
138        foreach ($tablerows as $row) {
139            if (in_array($row['tbl'], $tables)) {
140                $this->assignPageSchema($pid, $row['tbl']);
141            } else {
142                $this->deassignPageSchema($pid, $row['tbl']);
143            }
144        }
145    }
146
147    /**
148     * Clear all patterns - deassigns all pages
149     *
150     * This is mostly useful for testing and not used in the interface currently
151     *
152     * @param bool $full fully delete all previous assignments
153     * @return bool
154     */
155    public function clear($full = false)
156    {
157        $sql = 'DELETE FROM schema_assignments_patterns';
158        $ok = (bool)$this->sqlite->query($sql);
159
160        if ($full) {
161            $sql = 'DELETE FROM schema_assignments';
162        } else {
163            $sql = 'UPDATE schema_assignments SET assigned = 0';
164        }
165        $ok = $ok && (bool)$this->sqlite->query($sql);
166
167        // reload patterns
168        $this->loadPatterns();
169
170        return $ok;
171    }
172
173    /**
174     * Add page to assignments
175     *
176     * @param string $page
177     * @param string $table
178     * @return bool
179     */
180    public function assignPageSchema($page, $table)
181    {
182        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)';
183        return (bool)$this->sqlite->query($sql, array($page, $table));
184    }
185
186    /**
187     * Remove page from assignments
188     *
189     * @param string $page
190     * @param string $table
191     * @return bool
192     */
193    public function deassignPageSchema($page, $table)
194    {
195        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 0)';
196        return (bool)$this->sqlite->query($sql, array($page, $table));
197    }
198
199    /**
200     * Get the whole pattern table
201     *
202     * @return array
203     */
204    public function getAllPatterns()
205    {
206        return $this->patterns;
207    }
208
209    /**
210     * Returns a list of table names assigned to the given page
211     *
212     * @param string $page
213     * @param bool $checkpatterns Should the current patterns be re-evaluated?
214     * @return \string[] tables assigned
215     */
216    public function getPageAssignments($page, $checkpatterns = true)
217    {
218        $tables = array();
219        $page = cleanID($page);
220
221        if ($checkpatterns) {
222            // evaluate patterns
223            $pns = ':' . getNS($page) . ':';
224            foreach ($this->patterns as $row) {
225                if ($this->matchPagePattern($row['pattern'], $page, $pns)) {
226                    $tables[] = $row['tbl'];
227                }
228            }
229        } else {
230            // just select
231            $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ? AND assigned = 1';
232            $res = $this->sqlite->query($sql, array($page));
233            $list = $this->sqlite->res2arr($res);
234            $this->sqlite->res_close($res);
235            foreach ($list as $row) {
236                $tables[] = $row['tbl'];
237            }
238        }
239
240        return array_unique($tables);
241    }
242
243    /**
244     * Get the pages known to struct and their assignment state
245     *
246     * @param null|string $schema limit results to the given schema
247     * @param bool $assignedonly limit results to currently assigned only
248     * @return array
249     */
250    public function getPages($schema = null, $assignedonly = false)
251    {
252        $sql = 'SELECT pid, tbl, assigned FROM schema_assignments WHERE 1=1';
253
254        $opts = array();
255        if ($schema) {
256            $sql .= ' AND tbl = ?';
257            $opts[] = $schema;
258        }
259        if ($assignedonly) {
260            $sql .= ' AND assigned = 1';
261        }
262
263        $sql .= ' ORDER BY pid, tbl';
264
265        $res = $this->sqlite->query($sql, $opts);
266        $list = $this->sqlite->res2arr($res);
267        $this->sqlite->res_close($res);
268
269        $result = array();
270        foreach ($list as $row) {
271            $pid = $row['pid'];
272            $tbl = $row['tbl'];
273            if (!isset($result[$pid])) $result[$pid] = array();
274            $result[$pid][$tbl] = (bool)$row['assigned'];
275        }
276
277        return $result;
278    }
279
280    /**
281     * Check if the given pattern matches the given page
282     *
283     * @param string $pattern the pattern to check against
284     * @param string $page the cleaned pageid to check
285     * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic
286     * @return bool
287     */
288    protected function matchPagePattern($pattern, $page, $pns = null)
289    {
290        if (trim($pattern, ':') == '**') return true; // match all
291
292        // regex patterns
293        if ($pattern[0] == '/') {
294            return (bool)preg_match($pattern, ":$page");
295        }
296
297        if (is_null($pns)) {
298            $pns = ':' . getNS($page) . ':';
299        }
300
301        $ans = ':' . cleanID($pattern) . ':';
302        if (substr($pattern, -2) == '**') {
303            // upper namespaces match
304            if (strpos($pns, $ans) === 0) {
305                return true;
306            }
307        } elseif (substr($pattern, -1) == '*') {
308            // namespaces match exact
309            if ($ans == $pns) {
310                return true;
311            }
312        } else {
313            // exact match
314            if (cleanID($pattern) == $page) {
315                return true;
316            }
317        }
318
319        return false;
320    }
321
322    /**
323     * Returns all tables of schemas that existed and stored data for the page back then
324     *
325     * @deprecated because we're always only interested in the current state of affairs, even when restoring.
326     *
327     * @param string $page
328     * @param string $ts
329     * @return array
330     */
331    public function getHistoricAssignments($page, $ts)
332    {
333        $sql = "SELECT DISTINCT tbl FROM schemas WHERE ts <= ? ORDER BY ts DESC";
334        $res = $this->sqlite->query($sql, $ts);
335        $tables = $this->sqlite->res2arr($res);
336        $this->sqlite->res_close($res);
337
338        $assigned = array();
339        foreach ($tables as $row) {
340            $table = $row['tbl'];
341            /** @noinspection SqlResolve */
342            $sql = "SELECT pid FROM data_$table WHERE pid = ? AND rev <= ? LIMIT 1";
343            $res = $this->sqlite->query($sql, $page, $ts);
344            $found = $this->sqlite->res2arr($res);
345            $this->sqlite->res_close($res);
346
347            if ($found) $assigned[] = $table;
348        }
349
350        return $assigned;
351    }
352
353    /**
354     * fetch all pages where the schema isn't assigned, yet
355     * and reevaluate the page assignments for those pages and assign when needed
356     *
357     * @param $table
358     */
359    public function propagatePageAssignments($table)
360    {
361        $sql = 'SELECT pid FROM schema_assignments WHERE tbl != ? OR assigned != 1';
362        $res = $this->sqlite->query($sql, $table);
363        $pagerows = $this->sqlite->res2arr($res);
364        $this->sqlite->res_close($res);
365
366        foreach ($pagerows as $row) {
367            $tables = $this->getPageAssignments($row['pid'], true);
368            if (in_array($table, $tables)) {
369                $this->assignPageSchema($row['pid'], $table);
370            }
371        }
372    }
373}
374