xref: /plugin/struct/meta/Assignments.php (revision 025cb9da4274aac00be48202c8eb49058f2dd283)
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 *
10*025cb9daSAndreas Gohr * This is a singleton. Assignment data is only loaded once per request.
11*025cb9daSAndreas Gohr *
12ba766201SAndreas Gohr * @package dokuwiki\plugin\struct\meta
131a8d1235SAndreas Gohr */
14fb31ca9fSAndreas Gohrclass Assignments {
15fb31ca9fSAndreas Gohr
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
22fc26989eSAndreas Gohr    /** @var  string[] All lookup schemas for error checking */
23fc26989eSAndreas Gohr    protected $lookups;
24fc26989eSAndreas Gohr
25*025cb9daSAndreas Gohr    /** @var Assignments */
26*025cb9daSAndreas Gohr    protected static $instance = null;
27*025cb9daSAndreas Gohr
28*025cb9daSAndreas Gohr    /**
29*025cb9daSAndreas Gohr     * Get the singleton instance of the Assignments
30*025cb9daSAndreas Gohr     *
31*025cb9daSAndreas Gohr     * @param bool $forcereload create a new instace to reload the assignment data
32*025cb9daSAndreas Gohr     * @return Assignments
33*025cb9daSAndreas Gohr     */
34*025cb9daSAndreas Gohr    public static function getInstance($forcereload = false) {
35*025cb9daSAndreas Gohr        if(is_null(self::$instance) or $forcereload) {
36*025cb9daSAndreas Gohr            $class = get_called_class();
37*025cb9daSAndreas Gohr            self::$instance = new $class();
38*025cb9daSAndreas Gohr        }
39*025cb9daSAndreas Gohr        return self::$instance;
40*025cb9daSAndreas Gohr    }
41*025cb9daSAndreas Gohr
42fb31ca9fSAndreas Gohr    /**
43fb31ca9fSAndreas Gohr     * Assignments constructor.
44*025cb9daSAndreas Gohr     *
45*025cb9daSAndreas Gohr     * Not public. Use Assignments::getInstance() instead
46fb31ca9fSAndreas Gohr     */
47*025cb9daSAndreas Gohr    protected function __construct() {
48fb31ca9fSAndreas Gohr        /** @var \helper_plugin_struct_db $helper */
49fb31ca9fSAndreas Gohr        $helper = plugin_load('helper', 'struct_db');
50fb31ca9fSAndreas Gohr        $this->sqlite = $helper->getDB();
51fc26989eSAndreas Gohr        if(!$this->sqlite) return;
52fb31ca9fSAndreas Gohr
53fc26989eSAndreas Gohr        $this->loadPatterns();
54fc26989eSAndreas Gohr        $this->lookups = Schema::getAll('lookup');
55fb31ca9fSAndreas Gohr    }
56fb31ca9fSAndreas Gohr
57*025cb9daSAndreas Gohr
58*025cb9daSAndreas Gohr
59fb31ca9fSAndreas Gohr    /**
6049d38573SAndreas Gohr     * Load existing assignment patterns
61fb31ca9fSAndreas Gohr     */
6233d7be6aSAndreas Gohr    protected function loadPatterns() {
6349d38573SAndreas Gohr        $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern';
64fb31ca9fSAndreas Gohr        $res = $this->sqlite->query($sql);
6549d38573SAndreas Gohr        $this->patterns = $this->sqlite->res2arr($res);
66fb31ca9fSAndreas Gohr        $this->sqlite->res_close($res);
67fb31ca9fSAndreas Gohr    }
68fb31ca9fSAndreas Gohr
69fb31ca9fSAndreas Gohr    /**
7049d38573SAndreas Gohr     * Add a new assignment pattern to the pattern table
711a8d1235SAndreas Gohr     *
7249d38573SAndreas Gohr     * @param string $pattern
731a8d1235SAndreas Gohr     * @param string $table
741a8d1235SAndreas Gohr     * @return bool
751a8d1235SAndreas Gohr     */
7633d7be6aSAndreas Gohr    public function addPattern($pattern, $table) {
77fc26989eSAndreas Gohr        if(in_array($table, $this->lookups)) {
78fc26989eSAndreas Gohr            throw new StructException('nolookupassign');
79fc26989eSAndreas Gohr        }
80fc26989eSAndreas Gohr
81ed60c3b3SAndreas Gohr        // add the pattern
8249d38573SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments_patterns (pattern, tbl) VALUES (?,?)';
83ed60c3b3SAndreas Gohr        $ok = (bool) $this->sqlite->query($sql, array($pattern, $table));
84ed60c3b3SAndreas Gohr
85ed60c3b3SAndreas Gohr        // reload patterns
86ed60c3b3SAndreas Gohr        $this->loadPatterns();
87b25bb9feSMichael Grosse        $this->propagatePageAssignments($table);
88ed60c3b3SAndreas Gohr
89ed60c3b3SAndreas Gohr
90ed60c3b3SAndreas Gohr        return $ok;
911a8d1235SAndreas Gohr    }
921a8d1235SAndreas Gohr
931a8d1235SAndreas Gohr    /**
9449d38573SAndreas Gohr     * Remove an existing assignment pattern from the pattern table
951a8d1235SAndreas Gohr     *
9649d38573SAndreas Gohr     * @param string $pattern
971a8d1235SAndreas Gohr     * @param string $table
981a8d1235SAndreas Gohr     * @return bool
991a8d1235SAndreas Gohr     */
10033d7be6aSAndreas Gohr    public function removePattern($pattern, $table) {
101ed60c3b3SAndreas Gohr        // remove the pattern
10249d38573SAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns WHERE pattern = ? AND tbl = ?';
103ed60c3b3SAndreas Gohr        $ok = (bool) $this->sqlite->query($sql, array($pattern, $table));
104ed60c3b3SAndreas Gohr
105ed60c3b3SAndreas Gohr        // reload patterns
106ed60c3b3SAndreas Gohr        $this->loadPatterns();
107ed60c3b3SAndreas Gohr
108ed60c3b3SAndreas Gohr        // fetch possibly affected pages
109ed60c3b3SAndreas Gohr        $sql = 'SELECT pid FROM schema_assignments WHERE tbl = ?';
110ed60c3b3SAndreas Gohr        $res = $this->sqlite->query($sql, $table);
1110e9e058fSAndreas Gohr        $pagerows = $this->sqlite->res2arr($res);
112ed60c3b3SAndreas Gohr        $this->sqlite->res_close($res);
113ed60c3b3SAndreas Gohr
114ed60c3b3SAndreas Gohr        // reevalute the pages and unassign when needed
1150e9e058fSAndreas Gohr        foreach($pagerows as $row) {
116be94e9d9SAndreas Gohr            $tables = $this->getPageAssignments($row['pid'], true);
117ed60c3b3SAndreas Gohr            if(!in_array($table, $tables)) {
118be94e9d9SAndreas Gohr                $this->deassignPageSchema($row['pid'], $table);
119ed60c3b3SAndreas Gohr            }
120ed60c3b3SAndreas Gohr        }
121ed60c3b3SAndreas Gohr
122ed60c3b3SAndreas Gohr        return $ok;
123ed60c3b3SAndreas Gohr    }
124ed60c3b3SAndreas Gohr
125ed60c3b3SAndreas Gohr    /**
1260173e75dSAndreas Gohr     * Rechecks all assignments of a given page against the current patterns
1270173e75dSAndreas Gohr     *
1280173e75dSAndreas Gohr     * @param string $pid
1290173e75dSAndreas Gohr     */
1300173e75dSAndreas Gohr    public function reevaluatePageAssignments($pid) {
1310173e75dSAndreas Gohr        // reload patterns
1320173e75dSAndreas Gohr        $this->loadPatterns();
1330173e75dSAndreas Gohr        $tables = $this->getPageAssignments($pid, true);
1340173e75dSAndreas Gohr
1350173e75dSAndreas Gohr        // fetch possibly affected tables
1360173e75dSAndreas Gohr        $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?';
1370173e75dSAndreas Gohr        $res = $this->sqlite->query($sql, $pid);
1380173e75dSAndreas Gohr        $tablerows = $this->sqlite->res2arr($res);
1390173e75dSAndreas Gohr        $this->sqlite->res_close($res);
1400173e75dSAndreas Gohr
1410173e75dSAndreas Gohr        // reevalute the tables and apply assignments
1420173e75dSAndreas Gohr        foreach($tablerows as $row) {
1430173e75dSAndreas Gohr            if(in_array($row['tbl'], $tables)) {
1440173e75dSAndreas Gohr                $this->assignPageSchema($pid, $row['tbl']);
1450173e75dSAndreas Gohr            } else {
1460173e75dSAndreas Gohr                $this->deassignPageSchema($pid, $row['tbl']);
1470173e75dSAndreas Gohr            }
1480173e75dSAndreas Gohr        }
1490173e75dSAndreas Gohr    }
1500173e75dSAndreas Gohr
1510173e75dSAndreas Gohr    /**
1520e9e058fSAndreas Gohr     * Clear all patterns - deassigns all pages
1530e9e058fSAndreas Gohr     *
1540e9e058fSAndreas Gohr     * This is mostly useful for testing and not used in the interface currently
1550e9e058fSAndreas Gohr     *
156153400c7SAndreas Gohr     * @param bool $full fully delete all previous assignments
1570e9e058fSAndreas Gohr     * @return bool
1580e9e058fSAndreas Gohr     */
159153400c7SAndreas Gohr    public function clear($full=false) {
1600e9e058fSAndreas Gohr        $sql = 'DELETE FROM schema_assignments_patterns';
1610e9e058fSAndreas Gohr        $ok = (bool) $this->sqlite->query($sql);
1620e9e058fSAndreas Gohr
163153400c7SAndreas Gohr        if($full) {
164153400c7SAndreas Gohr            $sql = 'DELETE FROM schema_assignments';
165153400c7SAndreas Gohr        } else {
1660e9e058fSAndreas Gohr            $sql = 'UPDATE schema_assignments SET assigned = 0';
167153400c7SAndreas Gohr        }
1680e9e058fSAndreas Gohr        $ok = $ok && (bool) $this->sqlite->query($sql);
1690e9e058fSAndreas Gohr
1700e9e058fSAndreas Gohr        // reload patterns
1710e9e058fSAndreas Gohr        $this->loadPatterns();
1720e9e058fSAndreas Gohr
1730e9e058fSAndreas Gohr        return $ok;
1740e9e058fSAndreas Gohr    }
1750e9e058fSAndreas Gohr
1760e9e058fSAndreas Gohr    /**
177ed60c3b3SAndreas Gohr     * Add page to assignments
178ed60c3b3SAndreas Gohr     *
179ed60c3b3SAndreas Gohr     * @param string $page
180ed60c3b3SAndreas Gohr     * @param string $table
181ed60c3b3SAndreas Gohr     * @return bool
182ed60c3b3SAndreas Gohr     */
183ed713594SAndreas Gohr    public function assignPageSchema($page, $table) {
184ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)';
185ed60c3b3SAndreas Gohr        return (bool) $this->sqlite->query($sql, array($page, $table));
186ed60c3b3SAndreas Gohr    }
187ed60c3b3SAndreas Gohr
188ed60c3b3SAndreas Gohr    /**
189ed60c3b3SAndreas Gohr     * Remove page from assignments
190ed60c3b3SAndreas Gohr     *
191ed60c3b3SAndreas Gohr     * @param string $page
192ed60c3b3SAndreas Gohr     * @param string $table
193ed60c3b3SAndreas Gohr     * @return bool
194ed60c3b3SAndreas Gohr     */
195ed713594SAndreas Gohr    public function deassignPageSchema($page, $table) {
196ed60c3b3SAndreas Gohr        $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 0)';
197ed60c3b3SAndreas Gohr        return (bool) $this->sqlite->query($sql, array($page, $table));
1981a8d1235SAndreas Gohr    }
1991a8d1235SAndreas Gohr
2001a8d1235SAndreas Gohr    /**
20149d38573SAndreas Gohr     * Get the whole pattern table
2021a8d1235SAndreas Gohr     *
2031a8d1235SAndreas Gohr     * @return array
2041a8d1235SAndreas Gohr     */
20533d7be6aSAndreas Gohr    public function getAllPatterns() {
20649d38573SAndreas Gohr        return $this->patterns;
2071a8d1235SAndreas Gohr    }
2081a8d1235SAndreas Gohr
2091a8d1235SAndreas Gohr    /**
210fb31ca9fSAndreas Gohr     * Returns a list of table names assigned to the given page
211fb31ca9fSAndreas Gohr     *
212fb31ca9fSAndreas Gohr     * @param string $page
2139ff81b7fSAndreas Gohr     * @param bool $checkpatterns Should the current patterns be re-evaluated?
2149ff81b7fSAndreas Gohr     * @return \string[] tables assigned
215fb31ca9fSAndreas Gohr     */
2169ff81b7fSAndreas Gohr    public function getPageAssignments($page, $checkpatterns=true) {
217fb31ca9fSAndreas Gohr        $tables = array();
218fb31ca9fSAndreas Gohr        $page = cleanID($page);
219fb31ca9fSAndreas Gohr
2209ff81b7fSAndreas Gohr        if($checkpatterns) {
2219ff81b7fSAndreas Gohr            // evaluate patterns
2229ff81b7fSAndreas Gohr            $pns = ':' . getNS($page) . ':';
22349d38573SAndreas Gohr            foreach($this->patterns as $row) {
224ed60c3b3SAndreas Gohr                if($this->matchPagePattern($row['pattern'], $page, $pns)) {
225fc26989eSAndreas Gohr                    if(in_array($row['tbl'], $this->lookups)) continue; // wrong assignment
226ed60c3b3SAndreas Gohr                    $tables[] = $row['tbl'];
227fb31ca9fSAndreas Gohr                }
228fb31ca9fSAndreas Gohr            }
2299ff81b7fSAndreas Gohr        } else {
2309ff81b7fSAndreas Gohr            // just select
2319ff81b7fSAndreas Gohr            $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ? AND assigned = 1';
2329ff81b7fSAndreas Gohr            $res = $this->sqlite->query($sql, array($page));
2339ff81b7fSAndreas Gohr            $list = $this->sqlite->res2arr($res);
2349ff81b7fSAndreas Gohr            $this->sqlite->res_close($res);
2359ff81b7fSAndreas Gohr            foreach($list as $row) {
236fc26989eSAndreas Gohr                if(in_array($row['tbl'], $this->lookups)) continue; // wrong assignment
2379ff81b7fSAndreas Gohr                $tables[] = $row['tbl'];
2389ff81b7fSAndreas Gohr            }
2399ff81b7fSAndreas Gohr        }
240fb31ca9fSAndreas Gohr
241fb31ca9fSAndreas Gohr        return array_unique($tables);
242fb31ca9fSAndreas Gohr    }
24356672c36SAndreas Gohr
24456672c36SAndreas Gohr    /**
245153400c7SAndreas Gohr     * Get the pages known to struct and their assignment state
246153400c7SAndreas Gohr     *
247153400c7SAndreas Gohr     * @param null|string $schema limit results to the given schema
248153400c7SAndreas Gohr     * @param bool $assignedonly limit results to currently assigned only
249153400c7SAndreas Gohr     * @return array
250153400c7SAndreas Gohr     */
251153400c7SAndreas Gohr    public function getPages($schema = null, $assignedonly = false) {
252153400c7SAndreas Gohr        $sql = 'SELECT pid, tbl, assigned FROM schema_assignments WHERE 1=1';
253153400c7SAndreas Gohr
254153400c7SAndreas Gohr        $opts = array();
255153400c7SAndreas Gohr        if($schema) {
256153400c7SAndreas Gohr            $sql .= ' AND tbl = ?';
257153400c7SAndreas Gohr            $opts[] = $schema;
258153400c7SAndreas Gohr        }
259153400c7SAndreas Gohr        if($assignedonly) {
260153400c7SAndreas Gohr            $sql .= ' AND assigned = 1';
261153400c7SAndreas Gohr        }
262153400c7SAndreas Gohr
263153400c7SAndreas Gohr        $sql .= ' ORDER BY pid, tbl';
264153400c7SAndreas Gohr
265153400c7SAndreas Gohr        $res = $this->sqlite->query($sql, $opts);
266153400c7SAndreas Gohr        $list = $this->sqlite->res2arr($res);
267153400c7SAndreas Gohr        $this->sqlite->res_close($res);
268153400c7SAndreas Gohr
269153400c7SAndreas Gohr        $result = array();
270153400c7SAndreas Gohr        foreach($list as $row) {
271153400c7SAndreas Gohr            $pid = $row['pid'];
272153400c7SAndreas Gohr            $tbl = $row['tbl'];
273153400c7SAndreas Gohr            if(!isset($result[$pid])) $result[$pid] = array();
274153400c7SAndreas Gohr            $result[$pid][$tbl] = (bool) $row['assigned'];
275153400c7SAndreas Gohr        }
276153400c7SAndreas Gohr
277153400c7SAndreas Gohr        return $result;
278153400c7SAndreas Gohr    }
279153400c7SAndreas Gohr
280153400c7SAndreas Gohr    /**
281ed60c3b3SAndreas Gohr     * Check if the given pattern matches the given page
282ed60c3b3SAndreas Gohr     *
283ed60c3b3SAndreas Gohr     * @param string $pattern the pattern to check against
284ed60c3b3SAndreas Gohr     * @param string $page the cleaned pageid to check
285ed60c3b3SAndreas Gohr     * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic
286ed60c3b3SAndreas Gohr     * @return bool
287ed60c3b3SAndreas Gohr     */
288ed60c3b3SAndreas Gohr    protected function matchPagePattern($pattern, $page, $pns = null) {
2890e9e058fSAndreas Gohr        if(trim($pattern,':') == '**') return true; // match all
2900e9e058fSAndreas Gohr
2919914e87eSAndreas Gohr        // regex patterns
2929914e87eSAndreas Gohr        if($pattern{0} == '/') {
2939914e87eSAndreas Gohr            return (bool) preg_match($pattern, ":$page");
2949914e87eSAndreas Gohr        }
2959914e87eSAndreas Gohr
296ed60c3b3SAndreas Gohr        if(is_null($pns)) {
297ed60c3b3SAndreas Gohr            $pns = ':' . getNS($page) . ':';
298ed60c3b3SAndreas Gohr        }
299ed60c3b3SAndreas Gohr
300ed60c3b3SAndreas Gohr        $ans = ':' . cleanID($pattern) . ':';
301ed60c3b3SAndreas Gohr        if(substr($pattern, -2) == '**') {
302ed60c3b3SAndreas Gohr            // upper namespaces match
303ed60c3b3SAndreas Gohr            if(strpos($pns, $ans) === 0) {
304ed60c3b3SAndreas Gohr                return true;
305ed60c3b3SAndreas Gohr            }
306ed60c3b3SAndreas Gohr        } else if(substr($pattern, -1) == '*') {
307ed60c3b3SAndreas Gohr            // namespaces match exact
308ed60c3b3SAndreas Gohr            if($ans == $pns) {
309ed60c3b3SAndreas Gohr                return true;
310ed60c3b3SAndreas Gohr            }
311ed60c3b3SAndreas Gohr        } else {
312ed60c3b3SAndreas Gohr            // exact match
313ed60c3b3SAndreas Gohr            if(cleanID($pattern) == $page) {
314ed60c3b3SAndreas Gohr                return true;
315ed60c3b3SAndreas Gohr            }
316ed60c3b3SAndreas Gohr        }
317ed60c3b3SAndreas Gohr
318ed60c3b3SAndreas Gohr        return false;
319ed60c3b3SAndreas Gohr    }
320ed60c3b3SAndreas Gohr
321ed60c3b3SAndreas Gohr    /**
32256672c36SAndreas Gohr     * Returns all tables of schemas that existed and stored data for the page back then
32356672c36SAndreas Gohr     *
3240e9e058fSAndreas Gohr     * @deprecated because we're always only interested in the current state of affairs, even when restoring.
32556672c36SAndreas Gohr     *
32656672c36SAndreas Gohr     * @param string $page
32756672c36SAndreas Gohr     * @param string $ts
32856672c36SAndreas Gohr     * @return array
32956672c36SAndreas Gohr     */
33056672c36SAndreas Gohr    public function getHistoricAssignments($page, $ts) {
33156672c36SAndreas Gohr        $sql = "SELECT DISTINCT tbl FROM schemas WHERE ts <= ? ORDER BY ts DESC";
33256672c36SAndreas Gohr        $res = $this->sqlite->query($sql, $ts);
33356672c36SAndreas Gohr        $tables = $this->sqlite->res2arr($res);
33456672c36SAndreas Gohr        $this->sqlite->res_close($res);
33556672c36SAndreas Gohr
33656672c36SAndreas Gohr        $assigned = array();
33756672c36SAndreas Gohr        foreach($tables as $row) {
33856672c36SAndreas Gohr            $table = $row['tbl'];
339ed60c3b3SAndreas Gohr            /** @noinspection SqlResolve */
34056672c36SAndreas Gohr            $sql = "SELECT pid FROM data_$table WHERE pid = ? AND rev <= ? LIMIT 1";
34156672c36SAndreas Gohr            $res = $this->sqlite->query($sql, $page, $ts);
34256672c36SAndreas Gohr            $found = $this->sqlite->res2arr($res);
34356672c36SAndreas Gohr            $this->sqlite->res_close($res);
34456672c36SAndreas Gohr
34556672c36SAndreas Gohr            if($found) $assigned[] = $table;
34656672c36SAndreas Gohr        }
34756672c36SAndreas Gohr
34856672c36SAndreas Gohr        return $assigned;
34956672c36SAndreas Gohr    }
350b25bb9feSMichael Grosse
351b25bb9feSMichael Grosse    /**
352b25bb9feSMichael Grosse     * fetch all pages where the schema isn't assigned, yet and reevaluate the page assignments for those pages and assign when needed
353b25bb9feSMichael Grosse     *
354b25bb9feSMichael Grosse     * @param $table
355b25bb9feSMichael Grosse     */
356b25bb9feSMichael Grosse    public function propagatePageAssignments($table) {
357b25bb9feSMichael Grosse        $sql = 'SELECT pid FROM schema_assignments WHERE tbl != ? OR assigned != 1';
358b25bb9feSMichael Grosse        $res = $this->sqlite->query($sql, $table);
359b25bb9feSMichael Grosse        $pagerows = $this->sqlite->res2arr($res);
360b25bb9feSMichael Grosse        $this->sqlite->res_close($res);
361b25bb9feSMichael Grosse
362b25bb9feSMichael Grosse        foreach ($pagerows as $row) {
363b25bb9feSMichael Grosse            $tables = $this->getPageAssignments($row['pid'], true);
364b25bb9feSMichael Grosse            if (in_array($table, $tables)) {
365b25bb9feSMichael Grosse                $this->assignPageSchema($row['pid'], $table);
366b25bb9feSMichael Grosse            }
367b25bb9feSMichael Grosse        }
368b25bb9feSMichael Grosse    }
369fb31ca9fSAndreas Gohr}
370