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