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