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