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