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