xref: /plugin/struct/meta/Assignments.php (revision fc26989e4442674ded604c12f445cb8ca3510cf9)
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 *
10ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\meta
111a8d1235SAndreas Gohr */
12fb31ca9fSAndreas Gohrclass Assignments {
13fb31ca9fSAndreas Gohr
14fb31ca9fSAndreas Gohr    /** @var \helper_plugin_sqlite|null */
15fb31ca9fSAndreas Gohr    protected $sqlite;
16fb31ca9fSAndreas Gohr
1733d7be6aSAndreas Gohr    /** @var  array All the assignments patterns */
1849d38573SAndreas Gohr    protected $patterns;
19fb31ca9fSAndreas Gohr
20*fc26989eSAndreas Gohr    /** @var  string[] All lookup schemas for error checking */
21*fc26989eSAndreas Gohr    protected $lookups;
22*fc26989eSAndreas Gohr
23fb31ca9fSAndreas Gohr    /**
24fb31ca9fSAndreas Gohr     * Assignments constructor.
25fb31ca9fSAndreas Gohr     */
26fb31ca9fSAndreas Gohr    public function __construct() {
27fb31ca9fSAndreas Gohr        /** @var \helper_plugin_struct_db $helper */
28fb31ca9fSAndreas Gohr        $helper = plugin_load('helper', 'struct_db');
29fb31ca9fSAndreas Gohr        $this->sqlite = $helper->getDB();
30*fc26989eSAndreas Gohr        if(!$this->sqlite) return;
31fb31ca9fSAndreas Gohr
32*fc26989eSAndreas Gohr        $this->loadPatterns();
33*fc26989eSAndreas Gohr        $this->lookups = Schema::getAll('lookup');
34fb31ca9fSAndreas Gohr    }
35fb31ca9fSAndreas Gohr
36fb31ca9fSAndreas Gohr    /**
3749d38573SAndreas Gohr     * Load existing assignment patterns
38fb31ca9fSAndreas Gohr     */
3933d7be6aSAndreas Gohr    protected function loadPatterns() {
4049d38573SAndreas Gohr        $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern';
41fb31ca9fSAndreas Gohr        $res = $this->sqlite->query($sql);
4249d38573SAndreas Gohr        $this->patterns = $this->sqlite->res2arr($res);
43fb31ca9fSAndreas Gohr        $this->sqlite->res_close($res);
44fb31ca9fSAndreas Gohr    }
45fb31ca9fSAndreas Gohr
46fb31ca9fSAndreas Gohr    /**
4749d38573SAndreas Gohr     * Add a new assignment pattern to the pattern table
481a8d1235SAndreas Gohr     *
4949d38573SAndreas Gohr     * @param string $pattern
501a8d1235SAndreas Gohr     * @param string $table
511a8d1235SAndreas Gohr     * @return bool
521a8d1235SAndreas Gohr     */
5333d7be6aSAndreas Gohr    public function addPattern($pattern, $table) {
54*fc26989eSAndreas Gohr        if(in_array($table, $this->lookups)) {
55*fc26989eSAndreas Gohr            throw new StructException('nolookupassign');
56*fc26989eSAndreas Gohr        }
57*fc26989eSAndreas Gohr
58ed60c3b3SAndreas Gohr        // add the pattern
5949d38573SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments_patterns (pattern, tbl) VALUES (?,?)';
60ed60c3b3SAndreas Gohr        $ok = (bool) $this->sqlite->query($sql, array($pattern, $table));
61ed60c3b3SAndreas Gohr
62ed60c3b3SAndreas Gohr        // reload patterns
63ed60c3b3SAndreas Gohr        $this->loadPatterns();
64ed60c3b3SAndreas Gohr
65ed60c3b3SAndreas Gohr        // fetch all pages where the schema isn't assigned, yet
66ed60c3b3SAndreas Gohr        $sql = 'SELECT pid FROM schema_assignments WHERE tbl != ? OR assigned != 1';
67ed60c3b3SAndreas Gohr        $res = $this->sqlite->query($sql, $table);
680e9e058fSAndreas Gohr        $pagerows = $this->sqlite->res2arr($res);
69ed60c3b3SAndreas Gohr        $this->sqlite->res_close($res);
70ed60c3b3SAndreas Gohr
71ed60c3b3SAndreas Gohr        // reevalute the pages and assign when needed
720e9e058fSAndreas Gohr        foreach($pagerows as $row) {
730e9e058fSAndreas Gohr            $tables = $this->getPageAssignments($row['pid'], true);
74ed60c3b3SAndreas Gohr            if(in_array($table, $tables)) {
750e9e058fSAndreas Gohr                $this->assignPageSchema($row['pid'], $table);
76ed60c3b3SAndreas Gohr            }
77ed60c3b3SAndreas Gohr        }
78ed60c3b3SAndreas Gohr
79ed60c3b3SAndreas Gohr        return $ok;
801a8d1235SAndreas Gohr    }
811a8d1235SAndreas Gohr
821a8d1235SAndreas Gohr    /**
8349d38573SAndreas Gohr     * Remove an existing assignment pattern from the pattern table
841a8d1235SAndreas Gohr     *
8549d38573SAndreas Gohr     * @param string $pattern
861a8d1235SAndreas Gohr     * @param string $table
871a8d1235SAndreas Gohr     * @return bool
881a8d1235SAndreas Gohr     */
8933d7be6aSAndreas Gohr    public function removePattern($pattern, $table) {
90ed60c3b3SAndreas Gohr        // remove the pattern
9149d38573SAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns WHERE pattern = ? AND tbl = ?';
92ed60c3b3SAndreas Gohr        $ok = (bool) $this->sqlite->query($sql, array($pattern, $table));
93ed60c3b3SAndreas Gohr
94ed60c3b3SAndreas Gohr        // reload patterns
95ed60c3b3SAndreas Gohr        $this->loadPatterns();
96ed60c3b3SAndreas Gohr
97ed60c3b3SAndreas Gohr        // fetch possibly affected pages
98ed60c3b3SAndreas Gohr        $sql = 'SELECT pid FROM schema_assignments WHERE tbl = ?';
99ed60c3b3SAndreas Gohr        $res = $this->sqlite->query($sql, $table);
1000e9e058fSAndreas Gohr        $pagerows = $this->sqlite->res2arr($res);
101ed60c3b3SAndreas Gohr        $this->sqlite->res_close($res);
102ed60c3b3SAndreas Gohr
103ed60c3b3SAndreas Gohr        // reevalute the pages and unassign when needed
1040e9e058fSAndreas Gohr        foreach($pagerows as $row) {
105be94e9d9SAndreas Gohr            $tables = $this->getPageAssignments($row['pid'], true);
106ed60c3b3SAndreas Gohr            if(!in_array($table, $tables)) {
107be94e9d9SAndreas Gohr                $this->deassignPageSchema($row['pid'], $table);
108ed60c3b3SAndreas Gohr            }
109ed60c3b3SAndreas Gohr        }
110ed60c3b3SAndreas Gohr
111ed60c3b3SAndreas Gohr        return $ok;
112ed60c3b3SAndreas Gohr    }
113ed60c3b3SAndreas Gohr
114ed60c3b3SAndreas Gohr    /**
1150173e75dSAndreas Gohr     * Rechecks all assignments of a given page against the current patterns
1160173e75dSAndreas Gohr     *
1170173e75dSAndreas Gohr     * @param string $pid
1180173e75dSAndreas Gohr     */
1190173e75dSAndreas Gohr    public function reevaluatePageAssignments($pid) {
1200173e75dSAndreas Gohr        // reload patterns
1210173e75dSAndreas Gohr        $this->loadPatterns();
1220173e75dSAndreas Gohr        $tables = $this->getPageAssignments($pid, true);
1230173e75dSAndreas Gohr
1240173e75dSAndreas Gohr        // fetch possibly affected tables
1250173e75dSAndreas Gohr        $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?';
1260173e75dSAndreas Gohr        $res = $this->sqlite->query($sql, $pid);
1270173e75dSAndreas Gohr        $tablerows = $this->sqlite->res2arr($res);
1280173e75dSAndreas Gohr        $this->sqlite->res_close($res);
1290173e75dSAndreas Gohr
1300173e75dSAndreas Gohr        // reevalute the tables and apply assignments
1310173e75dSAndreas Gohr        foreach($tablerows as $row) {
1320173e75dSAndreas Gohr            if(in_array($row['tbl'], $tables)) {
1330173e75dSAndreas Gohr                $this->assignPageSchema($pid, $row['tbl']);
1340173e75dSAndreas Gohr            } else {
1350173e75dSAndreas Gohr                $this->deassignPageSchema($pid, $row['tbl']);
1360173e75dSAndreas Gohr            }
1370173e75dSAndreas Gohr        }
1380173e75dSAndreas Gohr    }
1390173e75dSAndreas Gohr
1400173e75dSAndreas Gohr    /**
1410e9e058fSAndreas Gohr     * Clear all patterns - deassigns all pages
1420e9e058fSAndreas Gohr     *
1430e9e058fSAndreas Gohr     * This is mostly useful for testing and not used in the interface currently
1440e9e058fSAndreas Gohr     *
145153400c7SAndreas Gohr     * @param bool $full fully delete all previous assignments
1460e9e058fSAndreas Gohr     * @return bool
1470e9e058fSAndreas Gohr     */
148153400c7SAndreas Gohr    public function clear($full=false) {
1490e9e058fSAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns';
1500e9e058fSAndreas Gohr        $ok = (bool) $this->sqlite->query($sql);
1510e9e058fSAndreas Gohr
152153400c7SAndreas Gohr        if($full) {
153153400c7SAndreas Gohr            $sql = 'DELETE FROM schema_assignments';
154153400c7SAndreas Gohr        } else {
1550e9e058fSAndreas Gohr            $sql = 'UPDATE schema_assignments SET assigned = 0';
156153400c7SAndreas Gohr        }
1570e9e058fSAndreas Gohr        $ok = $ok && (bool) $this->sqlite->query($sql);
1580e9e058fSAndreas Gohr
1590e9e058fSAndreas Gohr        // reload patterns
1600e9e058fSAndreas Gohr        $this->loadPatterns();
1610e9e058fSAndreas Gohr
1620e9e058fSAndreas Gohr        return $ok;
1630e9e058fSAndreas Gohr    }
1640e9e058fSAndreas Gohr
1650e9e058fSAndreas Gohr    /**
166ed60c3b3SAndreas Gohr     * Add page to assignments
167ed60c3b3SAndreas Gohr     *
168ed60c3b3SAndreas Gohr     * @param string $page
169ed60c3b3SAndreas Gohr     * @param string $table
170ed60c3b3SAndreas Gohr     * @return bool
171ed60c3b3SAndreas Gohr     */
172ed713594SAndreas Gohr    public function assignPageSchema($page, $table) {
173ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)';
174ed60c3b3SAndreas Gohr        return (bool) $this->sqlite->query($sql, array($page, $table));
175ed60c3b3SAndreas Gohr    }
176ed60c3b3SAndreas Gohr
177ed60c3b3SAndreas Gohr    /**
178ed60c3b3SAndreas Gohr     * Remove page from assignments
179ed60c3b3SAndreas Gohr     *
180ed60c3b3SAndreas Gohr     * @param string $page
181ed60c3b3SAndreas Gohr     * @param string $table
182ed60c3b3SAndreas Gohr     * @return bool
183ed60c3b3SAndreas Gohr     */
184ed713594SAndreas Gohr    public function deassignPageSchema($page, $table) {
185ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 0)';
186ed60c3b3SAndreas Gohr        return (bool) $this->sqlite->query($sql, array($page, $table));
1871a8d1235SAndreas Gohr    }
1881a8d1235SAndreas Gohr
1891a8d1235SAndreas Gohr    /**
19049d38573SAndreas Gohr     * Get the whole pattern table
1911a8d1235SAndreas Gohr     *
1921a8d1235SAndreas Gohr     * @return array
1931a8d1235SAndreas Gohr     */
19433d7be6aSAndreas Gohr    public function getAllPatterns() {
19549d38573SAndreas Gohr        return $this->patterns;
1961a8d1235SAndreas Gohr    }
1971a8d1235SAndreas Gohr
1981a8d1235SAndreas Gohr    /**
199fb31ca9fSAndreas Gohr     * Returns a list of table names assigned to the given page
200fb31ca9fSAndreas Gohr     *
201fb31ca9fSAndreas Gohr     * @param string $page
2029ff81b7fSAndreas Gohr     * @param bool $checkpatterns Should the current patterns be re-evaluated?
2039ff81b7fSAndreas Gohr     * @return \string[] tables assigned
204fb31ca9fSAndreas Gohr     */
2059ff81b7fSAndreas Gohr    public function getPageAssignments($page, $checkpatterns=true) {
206fb31ca9fSAndreas Gohr        $tables = array();
207fb31ca9fSAndreas Gohr        $page = cleanID($page);
208fb31ca9fSAndreas Gohr
2099ff81b7fSAndreas Gohr        if($checkpatterns) {
2109ff81b7fSAndreas Gohr            // evaluate patterns
2119ff81b7fSAndreas Gohr            $pns = ':' . getNS($page) . ':';
21249d38573SAndreas Gohr            foreach($this->patterns as $row) {
213ed60c3b3SAndreas Gohr                if($this->matchPagePattern($row['pattern'], $page, $pns)) {
214*fc26989eSAndreas Gohr                    if(in_array($row['tbl'], $this->lookups)) continue; // wrong assignment
215ed60c3b3SAndreas Gohr                    $tables[] = $row['tbl'];
216fb31ca9fSAndreas Gohr                }
217fb31ca9fSAndreas Gohr            }
2189ff81b7fSAndreas Gohr        } else {
2199ff81b7fSAndreas Gohr            // just select
2209ff81b7fSAndreas Gohr            $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ? AND assigned = 1';
2219ff81b7fSAndreas Gohr            $res = $this->sqlite->query($sql, array($page));
2229ff81b7fSAndreas Gohr            $list = $this->sqlite->res2arr($res);
2239ff81b7fSAndreas Gohr            $this->sqlite->res_close($res);
2249ff81b7fSAndreas Gohr            foreach($list as $row) {
225*fc26989eSAndreas Gohr                if(in_array($row['tbl'], $this->lookups)) continue; // wrong assignment
2269ff81b7fSAndreas Gohr                $tables[] = $row['tbl'];
2279ff81b7fSAndreas Gohr            }
2289ff81b7fSAndreas Gohr        }
229fb31ca9fSAndreas Gohr
230fb31ca9fSAndreas Gohr        return array_unique($tables);
231fb31ca9fSAndreas Gohr    }
23256672c36SAndreas Gohr
23356672c36SAndreas Gohr    /**
234153400c7SAndreas Gohr     * Get the pages known to struct and their assignment state
235153400c7SAndreas Gohr     *
236153400c7SAndreas Gohr     * @param null|string $schema limit results to the given schema
237153400c7SAndreas Gohr     * @param bool $assignedonly limit results to currently assigned only
238153400c7SAndreas Gohr     * @return array
239153400c7SAndreas Gohr     */
240153400c7SAndreas Gohr    public function getPages($schema = null, $assignedonly = false) {
241153400c7SAndreas Gohr        $sql = 'SELECT pid, tbl, assigned FROM schema_assignments WHERE 1=1';
242153400c7SAndreas Gohr
243153400c7SAndreas Gohr        $opts = array();
244153400c7SAndreas Gohr        if($schema) {
245153400c7SAndreas Gohr            $sql .= ' AND tbl = ?';
246153400c7SAndreas Gohr            $opts[] = $schema;
247153400c7SAndreas Gohr        }
248153400c7SAndreas Gohr        if($assignedonly) {
249153400c7SAndreas Gohr            $sql .= ' AND assigned = 1';
250153400c7SAndreas Gohr        }
251153400c7SAndreas Gohr
252153400c7SAndreas Gohr        $sql .= ' ORDER BY pid, tbl';
253153400c7SAndreas Gohr
254153400c7SAndreas Gohr        $res = $this->sqlite->query($sql, $opts);
255153400c7SAndreas Gohr        $list = $this->sqlite->res2arr($res);
256153400c7SAndreas Gohr        $this->sqlite->res_close($res);
257153400c7SAndreas Gohr
258153400c7SAndreas Gohr        $result = array();
259153400c7SAndreas Gohr        foreach($list as $row) {
260153400c7SAndreas Gohr            $pid = $row['pid'];
261153400c7SAndreas Gohr            $tbl = $row['tbl'];
262153400c7SAndreas Gohr            if(!isset($result[$pid])) $result[$pid] = array();
263153400c7SAndreas Gohr            $result[$pid][$tbl] = (bool) $row['assigned'];
264153400c7SAndreas Gohr        }
265153400c7SAndreas Gohr
266153400c7SAndreas Gohr        return $result;
267153400c7SAndreas Gohr    }
268153400c7SAndreas Gohr
269153400c7SAndreas Gohr    /**
270ed60c3b3SAndreas Gohr     * Check if the given pattern matches the given page
271ed60c3b3SAndreas Gohr     *
272ed60c3b3SAndreas Gohr     * @param string $pattern the pattern to check against
273ed60c3b3SAndreas Gohr     * @param string $page the cleaned pageid to check
274ed60c3b3SAndreas Gohr     * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic
275ed60c3b3SAndreas Gohr     * @return bool
276ed60c3b3SAndreas Gohr     */
277ed60c3b3SAndreas Gohr    protected function matchPagePattern($pattern, $page, $pns = null) {
2780e9e058fSAndreas Gohr        if(trim($pattern,':') == '**') return true; // match all
2790e9e058fSAndreas Gohr
2809914e87eSAndreas Gohr        // regex patterns
2819914e87eSAndreas Gohr        if($pattern{0} == '/') {
2829914e87eSAndreas Gohr            return (bool) preg_match($pattern, ":$page");
2839914e87eSAndreas Gohr        }
2849914e87eSAndreas Gohr
285ed60c3b3SAndreas Gohr        if(is_null($pns)) {
286ed60c3b3SAndreas Gohr            $pns = ':' . getNS($page) . ':';
287ed60c3b3SAndreas Gohr        }
288ed60c3b3SAndreas Gohr
289ed60c3b3SAndreas Gohr        $ans = ':' . cleanID($pattern) . ':';
290ed60c3b3SAndreas Gohr        if(substr($pattern, -2) == '**') {
291ed60c3b3SAndreas Gohr            // upper namespaces match
292ed60c3b3SAndreas Gohr            if(strpos($pns, $ans) === 0) {
293ed60c3b3SAndreas Gohr                return true;
294ed60c3b3SAndreas Gohr            }
295ed60c3b3SAndreas Gohr        } else if(substr($pattern, -1) == '*') {
296ed60c3b3SAndreas Gohr            // namespaces match exact
297ed60c3b3SAndreas Gohr            if($ans == $pns) {
298ed60c3b3SAndreas Gohr                return true;
299ed60c3b3SAndreas Gohr            }
300ed60c3b3SAndreas Gohr        } else {
301ed60c3b3SAndreas Gohr            // exact match
302ed60c3b3SAndreas Gohr            if(cleanID($pattern) == $page) {
303ed60c3b3SAndreas Gohr                return true;
304ed60c3b3SAndreas Gohr            }
305ed60c3b3SAndreas Gohr        }
306ed60c3b3SAndreas Gohr
307ed60c3b3SAndreas Gohr        return false;
308ed60c3b3SAndreas Gohr    }
309ed60c3b3SAndreas Gohr
310ed60c3b3SAndreas Gohr    /**
31156672c36SAndreas Gohr     * Returns all tables of schemas that existed and stored data for the page back then
31256672c36SAndreas Gohr     *
3130e9e058fSAndreas Gohr     * @deprecated because we're always only interested in the current state of affairs, even when restoring.
31456672c36SAndreas Gohr     *
31556672c36SAndreas Gohr     * @param string $page
31656672c36SAndreas Gohr     * @param string $ts
31756672c36SAndreas Gohr     * @return array
31856672c36SAndreas Gohr     */
31956672c36SAndreas Gohr    public function getHistoricAssignments($page, $ts) {
32056672c36SAndreas Gohr        $sql = "SELECT DISTINCT tbl FROM schemas WHERE ts <= ? ORDER BY ts DESC";
32156672c36SAndreas Gohr        $res = $this->sqlite->query($sql, $ts);
32256672c36SAndreas Gohr        $tables = $this->sqlite->res2arr($res);
32356672c36SAndreas Gohr        $this->sqlite->res_close($res);
32456672c36SAndreas Gohr
32556672c36SAndreas Gohr        $assigned = array();
32656672c36SAndreas Gohr        foreach($tables as $row) {
32756672c36SAndreas Gohr            $table = $row['tbl'];
328ed60c3b3SAndreas Gohr            /** @noinspection SqlResolve */
32956672c36SAndreas Gohr            $sql = "SELECT pid FROM data_$table WHERE pid = ? AND rev <= ? LIMIT 1";
33056672c36SAndreas Gohr            $res = $this->sqlite->query($sql, $page, $ts);
33156672c36SAndreas Gohr            $found = $this->sqlite->res2arr($res);
33256672c36SAndreas Gohr            $this->sqlite->res_close($res);
33356672c36SAndreas Gohr
33456672c36SAndreas Gohr            if($found) $assigned[] = $table;
33556672c36SAndreas Gohr        }
33656672c36SAndreas Gohr
33756672c36SAndreas Gohr        return $assigned;
33856672c36SAndreas Gohr    }
339fb31ca9fSAndreas Gohr}
340