xref: /plugin/struct/meta/Assignments.php (revision 79b29326ae29dcbf2571b932f1b531c400b9460b)
1fb31ca9fSAndreas Gohr<?php
2fb31ca9fSAndreas Gohr
3ba766201SAndreas Gohrnamespace dokuwiki\plugin\struct\meta;
4fb31ca9fSAndreas Gohr
51a8d1235SAndreas Gohr/**
61a8d1235SAndreas Gohr * Class Assignments
71a8d1235SAndreas Gohr *
81a8d1235SAndreas Gohr * Manages the assignment of schemas (table names) to pages and namespaces
91a8d1235SAndreas Gohr *
10025cb9daSAndreas Gohr * This is a singleton. Assignment data is only loaded once per request.
11025cb9daSAndreas Gohr *
12ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\meta
131a8d1235SAndreas Gohr */
14d6d97f60SAnna Dabrowskaclass Assignments
15d6d97f60SAnna Dabrowska{
16fb31ca9fSAndreas Gohr    /** @var \helper_plugin_sqlite|null */
17fb31ca9fSAndreas Gohr    protected $sqlite;
18fb31ca9fSAndreas Gohr
1933d7be6aSAndreas Gohr    /** @var  array All the assignments patterns */
2049d38573SAndreas Gohr    protected $patterns;
21fb31ca9fSAndreas Gohr
22025cb9daSAndreas Gohr    /** @var Assignments */
23025cb9daSAndreas Gohr    protected static $instance = null;
24025cb9daSAndreas Gohr
25025cb9daSAndreas Gohr    /**
26025cb9daSAndreas Gohr     * Get the singleton instance of the Assignments
27025cb9daSAndreas Gohr     *
28025cb9daSAndreas Gohr     * @param bool $forcereload create a new instace to reload the assignment data
29025cb9daSAndreas Gohr     * @return Assignments
30025cb9daSAndreas Gohr     */
31d6d97f60SAnna Dabrowska    public static function getInstance($forcereload = false)
32d6d97f60SAnna Dabrowska    {
33025cb9daSAndreas Gohr        if (is_null(self::$instance) or $forcereload) {
34025cb9daSAndreas Gohr            $class = get_called_class();
35025cb9daSAndreas Gohr            self::$instance = new $class();
36025cb9daSAndreas Gohr        }
37025cb9daSAndreas Gohr        return self::$instance;
38025cb9daSAndreas Gohr    }
39025cb9daSAndreas Gohr
40fb31ca9fSAndreas Gohr    /**
41fb31ca9fSAndreas Gohr     * Assignments constructor.
42025cb9daSAndreas Gohr     *
43025cb9daSAndreas Gohr     * Not public. Use Assignments::getInstance() instead
44fb31ca9fSAndreas Gohr     */
45d6d97f60SAnna Dabrowska    protected function __construct()
46d6d97f60SAnna Dabrowska    {
47fb31ca9fSAndreas Gohr        /** @var \helper_plugin_struct_db $helper */
48fb31ca9fSAndreas Gohr        $helper = plugin_load('helper', 'struct_db');
49fb31ca9fSAndreas Gohr        $this->sqlite = $helper->getDB();
50fb31ca9fSAndreas Gohr
51fc26989eSAndreas Gohr        $this->loadPatterns();
52fb31ca9fSAndreas Gohr    }
53fb31ca9fSAndreas Gohr
54025cb9daSAndreas Gohr
55fb31ca9fSAndreas Gohr    /**
5649d38573SAndreas Gohr     * Load existing assignment patterns
57fb31ca9fSAndreas Gohr     */
58d6d97f60SAnna Dabrowska    protected function loadPatterns()
59d6d97f60SAnna Dabrowska    {
6049d38573SAndreas Gohr        $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern';
61*79b29326SAnna Dabrowska        $this->patterns = $this->sqlite->queryAll($sql);
62fb31ca9fSAndreas Gohr    }
63fb31ca9fSAndreas Gohr
64fb31ca9fSAndreas Gohr    /**
6549d38573SAndreas Gohr     * Add a new assignment pattern to the pattern table
661a8d1235SAndreas Gohr     *
6749d38573SAndreas Gohr     * @param string $pattern
681a8d1235SAndreas Gohr     * @param string $table
691a8d1235SAndreas Gohr     * @return bool
701a8d1235SAndreas Gohr     */
71d6d97f60SAnna Dabrowska    public function addPattern($pattern, $table)
72d6d97f60SAnna Dabrowska    {
73ed60c3b3SAndreas Gohr        // add the pattern
7449d38573SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments_patterns (pattern, tbl) VALUES (?,?)';
75*79b29326SAnna Dabrowska        $ok = (bool)$this->sqlite->query($sql, [$pattern, $table]);
76ed60c3b3SAndreas Gohr
77ed60c3b3SAndreas Gohr        // reload patterns
78ed60c3b3SAndreas Gohr        $this->loadPatterns();
79b25bb9feSMichael Grosse        $this->propagatePageAssignments($table);
80ed60c3b3SAndreas Gohr
81ed60c3b3SAndreas Gohr
82ed60c3b3SAndreas Gohr        return $ok;
831a8d1235SAndreas Gohr    }
841a8d1235SAndreas Gohr
851a8d1235SAndreas Gohr    /**
8649d38573SAndreas Gohr     * Remove an existing assignment pattern from the pattern table
871a8d1235SAndreas Gohr     *
8849d38573SAndreas Gohr     * @param string $pattern
891a8d1235SAndreas Gohr     * @param string $table
901a8d1235SAndreas Gohr     * @return bool
911a8d1235SAndreas Gohr     */
92d6d97f60SAnna Dabrowska    public function removePattern($pattern, $table)
93d6d97f60SAnna Dabrowska    {
94ed60c3b3SAndreas Gohr        // remove the pattern
9549d38573SAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns WHERE pattern = ? AND tbl = ?';
96*79b29326SAnna Dabrowska        $ok = (bool)$this->sqlite->query($sql, [$pattern, $table]);
97ed60c3b3SAndreas Gohr
98ed60c3b3SAndreas Gohr        // reload patterns
99ed60c3b3SAndreas Gohr        $this->loadPatterns();
100ed60c3b3SAndreas Gohr
101ed60c3b3SAndreas Gohr        // fetch possibly affected pages
102ed60c3b3SAndreas Gohr        $sql = 'SELECT pid FROM schema_assignments WHERE tbl = ?';
103*79b29326SAnna Dabrowska        $pagerows = $this->sqlite->queryAll($sql, [$table]);
104ed60c3b3SAndreas Gohr
105ed60c3b3SAndreas Gohr        // reevalute the pages and unassign when needed
1060e9e058fSAndreas Gohr        foreach ($pagerows as $row) {
107be94e9d9SAndreas Gohr            $tables = $this->getPageAssignments($row['pid'], true);
108ed60c3b3SAndreas Gohr            if (!in_array($table, $tables)) {
109be94e9d9SAndreas Gohr                $this->deassignPageSchema($row['pid'], $table);
110ed60c3b3SAndreas Gohr            }
111ed60c3b3SAndreas Gohr        }
112ed60c3b3SAndreas Gohr
113ed60c3b3SAndreas Gohr        return $ok;
114ed60c3b3SAndreas Gohr    }
115ed60c3b3SAndreas Gohr
116ed60c3b3SAndreas Gohr    /**
1170173e75dSAndreas Gohr     * Rechecks all assignments of a given page against the current patterns
1180173e75dSAndreas Gohr     *
1190173e75dSAndreas Gohr     * @param string $pid
1200173e75dSAndreas Gohr     */
121d6d97f60SAnna Dabrowska    public function reevaluatePageAssignments($pid)
122d6d97f60SAnna Dabrowska    {
1230173e75dSAndreas Gohr        // reload patterns
1240173e75dSAndreas Gohr        $this->loadPatterns();
1250173e75dSAndreas Gohr        $tables = $this->getPageAssignments($pid, true);
1260173e75dSAndreas Gohr
1270173e75dSAndreas Gohr        // fetch possibly affected tables
1280173e75dSAndreas Gohr        $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?';
129*79b29326SAnna Dabrowska        $tablerows = $this->sqlite->queryAll($sql, [$pid]);
1300173e75dSAndreas Gohr
1310173e75dSAndreas Gohr        // reevalute the tables and apply assignments
1320173e75dSAndreas Gohr        foreach ($tablerows as $row) {
1330173e75dSAndreas Gohr            if (in_array($row['tbl'], $tables)) {
1340173e75dSAndreas Gohr                $this->assignPageSchema($pid, $row['tbl']);
1350173e75dSAndreas Gohr            } else {
1360173e75dSAndreas Gohr                $this->deassignPageSchema($pid, $row['tbl']);
1370173e75dSAndreas Gohr            }
1380173e75dSAndreas Gohr        }
1390173e75dSAndreas Gohr    }
1400173e75dSAndreas Gohr
1410173e75dSAndreas Gohr    /**
1420e9e058fSAndreas Gohr     * Clear all patterns - deassigns all pages
1430e9e058fSAndreas Gohr     *
1440e9e058fSAndreas Gohr     * This is mostly useful for testing and not used in the interface currently
1450e9e058fSAndreas Gohr     *
146153400c7SAndreas Gohr     * @param bool $full fully delete all previous assignments
1470e9e058fSAndreas Gohr     * @return bool
1480e9e058fSAndreas Gohr     */
149d6d97f60SAnna Dabrowska    public function clear($full = false)
150d6d97f60SAnna Dabrowska    {
1510e9e058fSAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns';
1520e9e058fSAndreas Gohr        $ok = (bool)$this->sqlite->query($sql);
1530e9e058fSAndreas Gohr
154153400c7SAndreas Gohr        if ($full) {
155153400c7SAndreas Gohr            $sql = 'DELETE FROM schema_assignments';
156153400c7SAndreas Gohr        } else {
1570e9e058fSAndreas Gohr            $sql = 'UPDATE schema_assignments SET assigned = 0';
158153400c7SAndreas Gohr        }
1590e9e058fSAndreas Gohr        $ok = $ok && (bool)$this->sqlite->query($sql);
1600e9e058fSAndreas Gohr
1610e9e058fSAndreas Gohr        // reload patterns
1620e9e058fSAndreas Gohr        $this->loadPatterns();
1630e9e058fSAndreas Gohr
1640e9e058fSAndreas Gohr        return $ok;
1650e9e058fSAndreas Gohr    }
1660e9e058fSAndreas Gohr
1670e9e058fSAndreas Gohr    /**
168ed60c3b3SAndreas Gohr     * Add page to assignments
169ed60c3b3SAndreas Gohr     *
170ed60c3b3SAndreas Gohr     * @param string $page
171ed60c3b3SAndreas Gohr     * @param string $table
172ed60c3b3SAndreas Gohr     * @return bool
173ed60c3b3SAndreas Gohr     */
174d6d97f60SAnna Dabrowska    public function assignPageSchema($page, $table)
175d6d97f60SAnna Dabrowska    {
176ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)';
177ed60c3b3SAndreas Gohr        return (bool)$this->sqlite->query($sql, array($page, $table));
178ed60c3b3SAndreas Gohr    }
179ed60c3b3SAndreas Gohr
180ed60c3b3SAndreas Gohr    /**
181ed60c3b3SAndreas Gohr     * Remove page from assignments
182ed60c3b3SAndreas Gohr     *
183ed60c3b3SAndreas Gohr     * @param string $page
184ed60c3b3SAndreas Gohr     * @param string $table
185ed60c3b3SAndreas Gohr     * @return bool
186ed60c3b3SAndreas Gohr     */
187d6d97f60SAnna Dabrowska    public function deassignPageSchema($page, $table)
188d6d97f60SAnna Dabrowska    {
189ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 0)';
190ed60c3b3SAndreas Gohr        return (bool)$this->sqlite->query($sql, array($page, $table));
1911a8d1235SAndreas Gohr    }
1921a8d1235SAndreas Gohr
1931a8d1235SAndreas Gohr    /**
19449d38573SAndreas Gohr     * Get the whole pattern table
1951a8d1235SAndreas Gohr     *
1961a8d1235SAndreas Gohr     * @return array
1971a8d1235SAndreas Gohr     */
198d6d97f60SAnna Dabrowska    public function getAllPatterns()
199d6d97f60SAnna Dabrowska    {
20049d38573SAndreas Gohr        return $this->patterns;
2011a8d1235SAndreas Gohr    }
2021a8d1235SAndreas Gohr
2031a8d1235SAndreas Gohr    /**
204fb31ca9fSAndreas Gohr     * Returns a list of table names assigned to the given page
205fb31ca9fSAndreas Gohr     *
206fb31ca9fSAndreas Gohr     * @param string $page
2079ff81b7fSAndreas Gohr     * @param bool $checkpatterns Should the current patterns be re-evaluated?
2089ff81b7fSAndreas Gohr     * @return \string[] tables assigned
209fb31ca9fSAndreas Gohr     */
210d6d97f60SAnna Dabrowska    public function getPageAssignments($page, $checkpatterns = true)
211d6d97f60SAnna Dabrowska    {
212fb31ca9fSAndreas Gohr        $tables = array();
213fb31ca9fSAndreas Gohr        $page = cleanID($page);
214fb31ca9fSAndreas Gohr
2159ff81b7fSAndreas Gohr        if ($checkpatterns) {
2169ff81b7fSAndreas Gohr            // evaluate patterns
2179ff81b7fSAndreas Gohr            $pns = ':' . getNS($page) . ':';
21849d38573SAndreas Gohr            foreach ($this->patterns as $row) {
219ed60c3b3SAndreas Gohr                if ($this->matchPagePattern($row['pattern'], $page, $pns)) {
220ed60c3b3SAndreas Gohr                    $tables[] = $row['tbl'];
221fb31ca9fSAndreas Gohr                }
222fb31ca9fSAndreas Gohr            }
2239ff81b7fSAndreas Gohr        } else {
2249ff81b7fSAndreas Gohr            // just select
2259ff81b7fSAndreas Gohr            $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ? AND assigned = 1';
226*79b29326SAnna Dabrowska            $list = $this->sqlite->queryAll($sql, [$page]);
2279ff81b7fSAndreas Gohr            foreach ($list as $row) {
2289ff81b7fSAndreas Gohr                $tables[] = $row['tbl'];
2299ff81b7fSAndreas Gohr            }
2309ff81b7fSAndreas Gohr        }
231fb31ca9fSAndreas Gohr
232fb31ca9fSAndreas Gohr        return array_unique($tables);
233fb31ca9fSAndreas Gohr    }
23456672c36SAndreas Gohr
23556672c36SAndreas Gohr    /**
236153400c7SAndreas Gohr     * Get the pages known to struct and their assignment state
237153400c7SAndreas Gohr     *
238153400c7SAndreas Gohr     * @param null|string $schema limit results to the given schema
239153400c7SAndreas Gohr     * @param bool $assignedonly limit results to currently assigned only
240153400c7SAndreas Gohr     * @return array
241153400c7SAndreas Gohr     */
242d6d97f60SAnna Dabrowska    public function getPages($schema = null, $assignedonly = false)
243d6d97f60SAnna Dabrowska    {
244153400c7SAndreas Gohr        $sql = 'SELECT pid, tbl, assigned FROM schema_assignments WHERE 1=1';
245153400c7SAndreas Gohr
246153400c7SAndreas Gohr        $opts = array();
247153400c7SAndreas Gohr        if ($schema) {
248153400c7SAndreas Gohr            $sql .= ' AND tbl = ?';
249153400c7SAndreas Gohr            $opts[] = $schema;
250153400c7SAndreas Gohr        }
251153400c7SAndreas Gohr        if ($assignedonly) {
252153400c7SAndreas Gohr            $sql .= ' AND assigned = 1';
253153400c7SAndreas Gohr        }
254153400c7SAndreas Gohr
255153400c7SAndreas Gohr        $sql .= ' ORDER BY pid, tbl';
256153400c7SAndreas Gohr
257*79b29326SAnna Dabrowska        $list = $this->sqlite->queryAll($sql, $opts);
258153400c7SAndreas Gohr
259153400c7SAndreas Gohr        $result = array();
260153400c7SAndreas Gohr        foreach ($list as $row) {
261153400c7SAndreas Gohr            $pid = $row['pid'];
262153400c7SAndreas Gohr            $tbl = $row['tbl'];
263153400c7SAndreas Gohr            if (!isset($result[$pid])) $result[$pid] = array();
264153400c7SAndreas Gohr            $result[$pid][$tbl] = (bool)$row['assigned'];
265153400c7SAndreas Gohr        }
266153400c7SAndreas Gohr
267153400c7SAndreas Gohr        return $result;
268153400c7SAndreas Gohr    }
269153400c7SAndreas Gohr
270153400c7SAndreas Gohr    /**
271ed60c3b3SAndreas Gohr     * Check if the given pattern matches the given page
272ed60c3b3SAndreas Gohr     *
273ed60c3b3SAndreas Gohr     * @param string $pattern the pattern to check against
274ed60c3b3SAndreas Gohr     * @param string $page the cleaned pageid to check
275ed60c3b3SAndreas Gohr     * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic
276ed60c3b3SAndreas Gohr     * @return bool
277ed60c3b3SAndreas Gohr     */
278d6d97f60SAnna Dabrowska    protected function matchPagePattern($pattern, $page, $pns = null)
279d6d97f60SAnna Dabrowska    {
2800e9e058fSAndreas Gohr        if (trim($pattern, ':') == '**') return true; // match all
2810e9e058fSAndreas Gohr
2829914e87eSAndreas Gohr        // regex patterns
2838da1363aSAndreas Gohr        if ($pattern[0] == '/') {
2849914e87eSAndreas Gohr            return (bool)preg_match($pattern, ":$page");
2859914e87eSAndreas Gohr        }
2869914e87eSAndreas Gohr
287ed60c3b3SAndreas Gohr        if (is_null($pns)) {
288ed60c3b3SAndreas Gohr            $pns = ':' . getNS($page) . ':';
289ed60c3b3SAndreas Gohr        }
290ed60c3b3SAndreas Gohr
291ed60c3b3SAndreas Gohr        $ans = ':' . cleanID($pattern) . ':';
292ed60c3b3SAndreas Gohr        if (substr($pattern, -2) == '**') {
293ed60c3b3SAndreas Gohr            // upper namespaces match
294ed60c3b3SAndreas Gohr            if (strpos($pns, $ans) === 0) {
295ed60c3b3SAndreas Gohr                return true;
296ed60c3b3SAndreas Gohr            }
297ed60c3b3SAndreas Gohr        } elseif (substr($pattern, -1) == '*') {
298ed60c3b3SAndreas Gohr            // namespaces match exact
299ed60c3b3SAndreas Gohr            if ($ans == $pns) {
300ed60c3b3SAndreas Gohr                return true;
301ed60c3b3SAndreas Gohr            }
302ed60c3b3SAndreas Gohr        } else {
303ed60c3b3SAndreas Gohr            // exact match
304ed60c3b3SAndreas Gohr            if (cleanID($pattern) == $page) {
305ed60c3b3SAndreas Gohr                return true;
306ed60c3b3SAndreas Gohr            }
307ed60c3b3SAndreas Gohr        }
308ed60c3b3SAndreas Gohr
309ed60c3b3SAndreas Gohr        return false;
310ed60c3b3SAndreas Gohr    }
311ed60c3b3SAndreas Gohr
312ed60c3b3SAndreas Gohr    /**
31356672c36SAndreas Gohr     * Returns all tables of schemas that existed and stored data for the page back then
31456672c36SAndreas Gohr     *
3150e9e058fSAndreas Gohr     * @deprecated because we're always only interested in the current state of affairs, even when restoring.
31656672c36SAndreas Gohr     *
31756672c36SAndreas Gohr     * @param string $page
31856672c36SAndreas Gohr     * @param string $ts
31956672c36SAndreas Gohr     * @return array
32056672c36SAndreas Gohr     */
321d6d97f60SAnna Dabrowska    public function getHistoricAssignments($page, $ts)
322d6d97f60SAnna Dabrowska    {
32356672c36SAndreas Gohr        $sql = "SELECT DISTINCT tbl FROM schemas WHERE ts <= ? ORDER BY ts DESC";
324*79b29326SAnna Dabrowska        $tables = $this->sqlite->queryAll($sql, [$ts]);
32556672c36SAndreas Gohr
32656672c36SAndreas Gohr        $assigned = array();
32756672c36SAndreas Gohr        foreach ($tables as $row) {
32856672c36SAndreas Gohr            $table = $row['tbl'];
329ed60c3b3SAndreas Gohr            /** @noinspection SqlResolve */
33056672c36SAndreas Gohr            $sql = "SELECT pid FROM data_$table WHERE pid = ? AND rev <= ? LIMIT 1";
331*79b29326SAnna Dabrowska            $found = $this->sqlite->queryAll($sql, [$page, $ts]);
33256672c36SAndreas Gohr
33356672c36SAndreas Gohr            if ($found) $assigned[] = $table;
33456672c36SAndreas Gohr        }
33556672c36SAndreas Gohr
33656672c36SAndreas Gohr        return $assigned;
33756672c36SAndreas Gohr    }
338b25bb9feSMichael Grosse
339b25bb9feSMichael Grosse    /**
34017a3a578SAndreas Gohr     * fetch all pages where the schema isn't assigned, yet
34117a3a578SAndreas Gohr     * and reevaluate the page assignments for those pages and assign when needed
342b25bb9feSMichael Grosse     *
343b25bb9feSMichael Grosse     * @param $table
344b25bb9feSMichael Grosse     */
345d6d97f60SAnna Dabrowska    public function propagatePageAssignments($table)
346d6d97f60SAnna Dabrowska    {
347b25bb9feSMichael Grosse        $sql = 'SELECT pid FROM schema_assignments WHERE tbl != ? OR assigned != 1';
348*79b29326SAnna Dabrowska        $pagerows = $this->sqlite->queryAll($sql, [$table]);
349b25bb9feSMichael Grosse
350b25bb9feSMichael Grosse        foreach ($pagerows as $row) {
351b25bb9feSMichael Grosse            $tables = $this->getPageAssignments($row['pid'], true);
352b25bb9feSMichael Grosse            if (in_array($table, $tables)) {
353b25bb9feSMichael Grosse                $this->assignPageSchema($row['pid'], $table);
354b25bb9feSMichael Grosse            }
355b25bb9feSMichael Grosse        }
356b25bb9feSMichael Grosse    }
357fb31ca9fSAndreas Gohr}
358