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