xref: /plugin/struct/meta/Assignments.php (revision ba662a609884bbbecde8cffec91014be306b652b)
1fb31ca9fSAndreas Gohr<?php
2fb31ca9fSAndreas Gohr
3ba766201SAndreas Gohrnamespace dokuwiki\plugin\struct\meta;
4fb31ca9fSAndreas Gohr
51a8d1235SAndreas Gohr/**
61a8d1235SAndreas Gohr * Class Assignments
71a8d1235SAndreas Gohr *
86636674bSAnna Dabrowska * Manages the assignment of schemas (table names) to pages and namespaces.
96636674bSAnna Dabrowska * An assignment is created when actual struct data is attached to the page.
106636674bSAnna Dabrowska * Assignment are never deleted, only their "assigned" status is changed.
111a8d1235SAndreas Gohr *
12025cb9daSAndreas Gohr * This is a singleton. Assignment data is only loaded once per request.
13025cb9daSAndreas Gohr *
14ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\meta
151a8d1235SAndreas Gohr */
16d6d97f60SAnna Dabrowskaclass Assignments
17d6d97f60SAnna Dabrowska{
18fb31ca9fSAndreas Gohr    /** @var \helper_plugin_sqlite|null */
19fb31ca9fSAndreas Gohr    protected $sqlite;
20fb31ca9fSAndreas Gohr
2133d7be6aSAndreas Gohr    /** @var  array All the assignments patterns */
2249d38573SAndreas Gohr    protected $patterns;
23fb31ca9fSAndreas Gohr
24025cb9daSAndreas Gohr    /** @var Assignments */
257234bfb1Ssplitbrain    protected static $instance;
26025cb9daSAndreas Gohr
27025cb9daSAndreas Gohr    /**
28025cb9daSAndreas Gohr     * Get the singleton instance of the Assignments
29025cb9daSAndreas Gohr     *
30025cb9daSAndreas Gohr     * @param bool $forcereload create a new instace to reload the assignment data
31025cb9daSAndreas Gohr     * @return Assignments
32025cb9daSAndreas Gohr     */
33d6d97f60SAnna Dabrowska    public static function getInstance($forcereload = false)
34d6d97f60SAnna Dabrowska    {
357234bfb1Ssplitbrain        if (is_null(self::$instance) || $forcereload) {
367234bfb1Ssplitbrain            $class = static::class;
37025cb9daSAndreas Gohr            self::$instance = new $class();
38025cb9daSAndreas Gohr        }
39025cb9daSAndreas Gohr        return self::$instance;
40025cb9daSAndreas Gohr    }
41025cb9daSAndreas Gohr
42fb31ca9fSAndreas Gohr    /**
43fb31ca9fSAndreas Gohr     * Assignments constructor.
44025cb9daSAndreas Gohr     *
45025cb9daSAndreas Gohr     * Not public. Use Assignments::getInstance() instead
46fb31ca9fSAndreas Gohr     */
47d6d97f60SAnna Dabrowska    protected function __construct()
48d6d97f60SAnna Dabrowska    {
49fb31ca9fSAndreas Gohr        /** @var \helper_plugin_struct_db $helper */
50fb31ca9fSAndreas Gohr        $helper = plugin_load('helper', 'struct_db');
51fb31ca9fSAndreas Gohr        $this->sqlite = $helper->getDB();
52fb31ca9fSAndreas Gohr
53fc26989eSAndreas Gohr        $this->loadPatterns();
54fb31ca9fSAndreas Gohr    }
55fb31ca9fSAndreas Gohr
56025cb9daSAndreas Gohr
57fb31ca9fSAndreas Gohr    /**
5849d38573SAndreas Gohr     * Load existing assignment patterns
59fb31ca9fSAndreas Gohr     */
60d6d97f60SAnna Dabrowska    protected function loadPatterns()
61d6d97f60SAnna Dabrowska    {
6249d38573SAndreas Gohr        $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern';
6379b29326SAnna Dabrowska        $this->patterns = $this->sqlite->queryAll($sql);
64fb31ca9fSAndreas Gohr    }
65fb31ca9fSAndreas Gohr
66fb31ca9fSAndreas Gohr    /**
6749d38573SAndreas Gohr     * Add a new assignment pattern to the pattern table
681a8d1235SAndreas Gohr     *
6949d38573SAndreas Gohr     * @param string $pattern
701a8d1235SAndreas Gohr     * @param string $table
711a8d1235SAndreas Gohr     * @return bool
721a8d1235SAndreas Gohr     */
73d6d97f60SAnna Dabrowska    public function addPattern($pattern, $table)
74d6d97f60SAnna Dabrowska    {
75ed60c3b3SAndreas Gohr        // add the pattern
7649d38573SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments_patterns (pattern, tbl) VALUES (?,?)';
7779b29326SAnna Dabrowska        $ok = (bool)$this->sqlite->query($sql, [$pattern, $table]);
78ed60c3b3SAndreas Gohr
79ed60c3b3SAndreas Gohr        // reload patterns
80ed60c3b3SAndreas Gohr        $this->loadPatterns();
81b25bb9feSMichael Grosse        $this->propagatePageAssignments($table);
82ed60c3b3SAndreas Gohr
83ed60c3b3SAndreas Gohr
84ed60c3b3SAndreas Gohr        return $ok;
851a8d1235SAndreas Gohr    }
861a8d1235SAndreas Gohr
871a8d1235SAndreas Gohr    /**
8849d38573SAndreas Gohr     * Remove an existing assignment pattern from the pattern table
891a8d1235SAndreas Gohr     *
9049d38573SAndreas Gohr     * @param string $pattern
911a8d1235SAndreas Gohr     * @param string $table
921a8d1235SAndreas Gohr     * @return bool
931a8d1235SAndreas Gohr     */
94d6d97f60SAnna Dabrowska    public function removePattern($pattern, $table)
95d6d97f60SAnna Dabrowska    {
96ed60c3b3SAndreas Gohr        // remove the pattern
9749d38573SAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns WHERE pattern = ? AND tbl = ?';
9879b29326SAnna Dabrowska        $ok = (bool)$this->sqlite->query($sql, [$pattern, $table]);
99ed60c3b3SAndreas Gohr
100ed60c3b3SAndreas Gohr        // reload patterns
101ed60c3b3SAndreas Gohr        $this->loadPatterns();
102ed60c3b3SAndreas Gohr
103ed60c3b3SAndreas Gohr        // fetch possibly affected pages
104ed60c3b3SAndreas Gohr        $sql = 'SELECT pid FROM schema_assignments WHERE tbl = ?';
10579b29326SAnna Dabrowska        $pagerows = $this->sqlite->queryAll($sql, [$table]);
106ed60c3b3SAndreas Gohr
107ed60c3b3SAndreas Gohr        // reevalute the pages and unassign when needed
1080e9e058fSAndreas Gohr        foreach ($pagerows as $row) {
109be94e9d9SAndreas Gohr            $tables = $this->getPageAssignments($row['pid'], true);
110ed60c3b3SAndreas Gohr            if (!in_array($table, $tables)) {
111be94e9d9SAndreas Gohr                $this->deassignPageSchema($row['pid'], $table);
112ed60c3b3SAndreas Gohr            }
113ed60c3b3SAndreas Gohr        }
114ed60c3b3SAndreas Gohr
115ed60c3b3SAndreas Gohr        return $ok;
116ed60c3b3SAndreas Gohr    }
117ed60c3b3SAndreas Gohr
118ed60c3b3SAndreas Gohr    /**
1190173e75dSAndreas Gohr     * Rechecks all assignments of a given page against the current patterns
1200173e75dSAndreas Gohr     *
1210173e75dSAndreas Gohr     * @param string $pid
1220173e75dSAndreas Gohr     */
123d6d97f60SAnna Dabrowska    public function reevaluatePageAssignments($pid)
124d6d97f60SAnna Dabrowska    {
1250173e75dSAndreas Gohr        // reload patterns
1260173e75dSAndreas Gohr        $this->loadPatterns();
1270173e75dSAndreas Gohr        $tables = $this->getPageAssignments($pid, true);
1280173e75dSAndreas Gohr
1290173e75dSAndreas Gohr        // fetch possibly affected tables
1300173e75dSAndreas Gohr        $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?';
13179b29326SAnna Dabrowska        $tablerows = $this->sqlite->queryAll($sql, [$pid]);
1320173e75dSAndreas Gohr
1330173e75dSAndreas Gohr        // reevalute the tables and apply assignments
1340173e75dSAndreas Gohr        foreach ($tablerows as $row) {
1350173e75dSAndreas Gohr            if (in_array($row['tbl'], $tables)) {
1360173e75dSAndreas Gohr                $this->assignPageSchema($pid, $row['tbl']);
1370173e75dSAndreas Gohr            } else {
1380173e75dSAndreas Gohr                $this->deassignPageSchema($pid, $row['tbl']);
1390173e75dSAndreas Gohr            }
1400173e75dSAndreas Gohr        }
1410173e75dSAndreas Gohr    }
1420173e75dSAndreas Gohr
1430173e75dSAndreas Gohr    /**
1440e9e058fSAndreas Gohr     * Clear all patterns - deassigns all pages
1450e9e058fSAndreas Gohr     *
1460e9e058fSAndreas Gohr     * This is mostly useful for testing and not used in the interface currently
1470e9e058fSAndreas Gohr     *
148153400c7SAndreas Gohr     * @param bool $full fully delete all previous assignments
1490e9e058fSAndreas Gohr     * @return bool
1500e9e058fSAndreas Gohr     */
151d6d97f60SAnna Dabrowska    public function clear($full = false)
152d6d97f60SAnna Dabrowska    {
1530e9e058fSAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns';
1540e9e058fSAndreas Gohr        $ok = (bool)$this->sqlite->query($sql);
1550e9e058fSAndreas Gohr
156153400c7SAndreas Gohr        if ($full) {
157153400c7SAndreas Gohr            $sql = 'DELETE FROM schema_assignments';
158153400c7SAndreas Gohr        } else {
1590e9e058fSAndreas Gohr            $sql = 'UPDATE schema_assignments SET assigned = 0';
160153400c7SAndreas Gohr        }
1610e9e058fSAndreas Gohr        $ok = $ok && (bool)$this->sqlite->query($sql);
1620e9e058fSAndreas Gohr
1630e9e058fSAndreas Gohr        // reload patterns
1640e9e058fSAndreas Gohr        $this->loadPatterns();
1650e9e058fSAndreas Gohr
1660e9e058fSAndreas Gohr        return $ok;
1670e9e058fSAndreas Gohr    }
1680e9e058fSAndreas Gohr
1690e9e058fSAndreas Gohr    /**
170ed60c3b3SAndreas Gohr     * Add page to assignments
171ed60c3b3SAndreas Gohr     *
172ed60c3b3SAndreas Gohr     * @param string $page
173ed60c3b3SAndreas Gohr     * @param string $table
174ed60c3b3SAndreas Gohr     * @return bool
175ed60c3b3SAndreas Gohr     */
176d6d97f60SAnna Dabrowska    public function assignPageSchema($page, $table)
177d6d97f60SAnna Dabrowska    {
178ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)';
1797234bfb1Ssplitbrain        return (bool)$this->sqlite->query($sql, [$page, $table]);
180ed60c3b3SAndreas Gohr    }
181ed60c3b3SAndreas Gohr
182ed60c3b3SAndreas Gohr    /**
183ed60c3b3SAndreas Gohr     * Remove page from assignments
184ed60c3b3SAndreas Gohr     *
185ed60c3b3SAndreas Gohr     * @param string $page
186ed60c3b3SAndreas Gohr     * @param string $table
187ed60c3b3SAndreas Gohr     * @return bool
188ed60c3b3SAndreas Gohr     */
189d6d97f60SAnna Dabrowska    public function deassignPageSchema($page, $table)
190d6d97f60SAnna Dabrowska    {
191ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 0)';
1927234bfb1Ssplitbrain        return (bool)$this->sqlite->query($sql, [$page, $table]);
1931a8d1235SAndreas Gohr    }
1941a8d1235SAndreas Gohr
1951a8d1235SAndreas Gohr    /**
19649d38573SAndreas Gohr     * Get the whole pattern table
1971a8d1235SAndreas Gohr     *
1981a8d1235SAndreas Gohr     * @return array
1991a8d1235SAndreas Gohr     */
200d6d97f60SAnna Dabrowska    public function getAllPatterns()
201d6d97f60SAnna Dabrowska    {
20249d38573SAndreas Gohr        return $this->patterns;
2031a8d1235SAndreas Gohr    }
2041a8d1235SAndreas Gohr
2051a8d1235SAndreas Gohr    /**
206fb31ca9fSAndreas Gohr     * Returns a list of table names assigned to the given page
207fb31ca9fSAndreas Gohr     *
208fb31ca9fSAndreas Gohr     * @param string $page
2099ff81b7fSAndreas Gohr     * @param bool $checkpatterns Should the current patterns be re-evaluated?
2109ff81b7fSAndreas Gohr     * @return \string[] tables assigned
211fb31ca9fSAndreas Gohr     */
212d6d97f60SAnna Dabrowska    public function getPageAssignments($page, $checkpatterns = true)
213d6d97f60SAnna Dabrowska    {
2147234bfb1Ssplitbrain        $tables = [];
215fb31ca9fSAndreas Gohr        $page = cleanID($page);
216fb31ca9fSAndreas Gohr
2179ff81b7fSAndreas Gohr        if ($checkpatterns) {
2189ff81b7fSAndreas Gohr            // evaluate patterns
2199ff81b7fSAndreas Gohr            $pns = ':' . getNS($page) . ':';
22049d38573SAndreas Gohr            foreach ($this->patterns as $row) {
221ed60c3b3SAndreas Gohr                if ($this->matchPagePattern($row['pattern'], $page, $pns)) {
222ed60c3b3SAndreas Gohr                    $tables[] = $row['tbl'];
223fb31ca9fSAndreas Gohr                }
224fb31ca9fSAndreas Gohr            }
2259ff81b7fSAndreas Gohr        } else {
2269ff81b7fSAndreas Gohr            // just select
2279ff81b7fSAndreas Gohr            $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ? AND assigned = 1';
22879b29326SAnna Dabrowska            $list = $this->sqlite->queryAll($sql, [$page]);
2299ff81b7fSAndreas Gohr            foreach ($list as $row) {
2309ff81b7fSAndreas Gohr                $tables[] = $row['tbl'];
2319ff81b7fSAndreas Gohr            }
2329ff81b7fSAndreas Gohr        }
233fb31ca9fSAndreas Gohr
234fb31ca9fSAndreas Gohr        return array_unique($tables);
235fb31ca9fSAndreas Gohr    }
23656672c36SAndreas Gohr
23756672c36SAndreas Gohr    /**
238153400c7SAndreas Gohr     * Get the pages known to struct and their assignment state
239153400c7SAndreas Gohr     *
240153400c7SAndreas Gohr     * @param null|string $schema limit results to the given schema
241153400c7SAndreas Gohr     * @param bool $assignedonly limit results to currently assigned only
242153400c7SAndreas Gohr     * @return array
243153400c7SAndreas Gohr     */
244d6d97f60SAnna Dabrowska    public function getPages($schema = null, $assignedonly = false)
245d6d97f60SAnna Dabrowska    {
246153400c7SAndreas Gohr        $sql = 'SELECT pid, tbl, assigned FROM schema_assignments WHERE 1=1';
247153400c7SAndreas Gohr
2487234bfb1Ssplitbrain        $opts = [];
249153400c7SAndreas Gohr        if ($schema) {
250153400c7SAndreas Gohr            $sql .= ' AND tbl = ?';
251153400c7SAndreas Gohr            $opts[] = $schema;
252153400c7SAndreas Gohr        }
253153400c7SAndreas Gohr        if ($assignedonly) {
254153400c7SAndreas Gohr            $sql .= ' AND assigned = 1';
255153400c7SAndreas Gohr        }
256153400c7SAndreas Gohr
257153400c7SAndreas Gohr        $sql .= ' ORDER BY pid, tbl';
258153400c7SAndreas Gohr
25979b29326SAnna Dabrowska        $list = $this->sqlite->queryAll($sql, $opts);
260153400c7SAndreas Gohr
2617234bfb1Ssplitbrain        $result = [];
262153400c7SAndreas Gohr        foreach ($list as $row) {
263153400c7SAndreas Gohr            $pid = $row['pid'];
264153400c7SAndreas Gohr            $tbl = $row['tbl'];
2657234bfb1Ssplitbrain            if (!isset($result[$pid])) $result[$pid] = [];
266153400c7SAndreas Gohr            $result[$pid][$tbl] = (bool)$row['assigned'];
267153400c7SAndreas Gohr        }
268153400c7SAndreas Gohr
269153400c7SAndreas Gohr        return $result;
270153400c7SAndreas Gohr    }
271153400c7SAndreas Gohr
272153400c7SAndreas Gohr    /**
273ed60c3b3SAndreas Gohr     * Check if the given pattern matches the given page
274ed60c3b3SAndreas Gohr     *
275ed60c3b3SAndreas Gohr     * @param string $pattern the pattern to check against
276ed60c3b3SAndreas Gohr     * @param string $page the cleaned pageid to check
277ed60c3b3SAndreas Gohr     * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic
278ed60c3b3SAndreas Gohr     * @return bool
279ed60c3b3SAndreas Gohr     */
280d6d97f60SAnna Dabrowska    protected function matchPagePattern($pattern, $page, $pns = null)
281d6d97f60SAnna Dabrowska    {
2820e9e058fSAndreas Gohr        if (trim($pattern, ':') == '**') return true; // match all
2830e9e058fSAndreas Gohr
2849914e87eSAndreas Gohr        // regex patterns
2858da1363aSAndreas Gohr        if ($pattern[0] == '/') {
2869914e87eSAndreas Gohr            return (bool)preg_match($pattern, ":$page");
2879914e87eSAndreas Gohr        }
2889914e87eSAndreas Gohr
289ed60c3b3SAndreas Gohr        if (is_null($pns)) {
290ed60c3b3SAndreas Gohr            $pns = ':' . getNS($page) . ':';
291ed60c3b3SAndreas Gohr        }
292ed60c3b3SAndreas Gohr
293ed60c3b3SAndreas Gohr        $ans = ':' . cleanID($pattern) . ':';
294*ba662a60SAndreas Gohr        if (str_ends_with($pattern, '**')) {
295ed60c3b3SAndreas Gohr            // upper namespaces match
296*ba662a60SAndreas Gohr            if (str_starts_with($pns, $ans)) {
297ed60c3b3SAndreas Gohr                return true;
298ed60c3b3SAndreas Gohr            }
299*ba662a60SAndreas Gohr        } elseif (str_ends_with($pattern, '*')) {
300ed60c3b3SAndreas Gohr            // namespaces match exact
301ed60c3b3SAndreas Gohr            if ($ans == $pns) {
302ed60c3b3SAndreas Gohr                return true;
303ed60c3b3SAndreas Gohr            }
3047234bfb1Ssplitbrain        } elseif (cleanID($pattern) == $page) {
305ed60c3b3SAndreas Gohr            // exact match
306ed60c3b3SAndreas Gohr            return true;
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";
32479b29326SAnna Dabrowska        $tables = $this->sqlite->queryAll($sql, [$ts]);
32556672c36SAndreas Gohr
3267234bfb1Ssplitbrain        $assigned = [];
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";
33179b29326SAnna 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';
34879b29326SAnna 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