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