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