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