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