1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5/** 6 * Class Assignments 7 * 8 * Manages the assignment of schemas (table names) to pages and namespaces 9 * 10 * This is a singleton. Assignment data is only loaded once per request. 11 * 12 * @package dokuwiki\plugin\struct\meta 13 */ 14class Assignments 15{ 16 /** @var \helper_plugin_sqlite|null */ 17 protected $sqlite; 18 19 /** @var array All the assignments patterns */ 20 protected $patterns; 21 22 /** @var Assignments */ 23 protected static $instance = null; 24 25 /** 26 * Get the singleton instance of the Assignments 27 * 28 * @param bool $forcereload create a new instace to reload the assignment data 29 * @return Assignments 30 */ 31 public static function getInstance($forcereload = false) 32 { 33 if (is_null(self::$instance) or $forcereload) { 34 $class = get_called_class(); 35 self::$instance = new $class(); 36 } 37 return self::$instance; 38 } 39 40 /** 41 * Assignments constructor. 42 * 43 * Not public. Use Assignments::getInstance() instead 44 */ 45 protected function __construct() 46 { 47 /** @var \helper_plugin_struct_db $helper */ 48 $helper = plugin_load('helper', 'struct_db'); 49 $this->sqlite = $helper->getDB(); 50 51 $this->loadPatterns(); 52 } 53 54 55 /** 56 * Load existing assignment patterns 57 */ 58 protected function loadPatterns() 59 { 60 $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern'; 61 $this->patterns = $this->sqlite->queryAll($sql); 62 } 63 64 /** 65 * Add a new assignment pattern to the pattern table 66 * 67 * @param string $pattern 68 * @param string $table 69 * @return bool 70 */ 71 public function addPattern($pattern, $table) 72 { 73 // add the pattern 74 $sql = 'REPLACE INTO schema_assignments_patterns (pattern, tbl) VALUES (?,?)'; 75 $ok = (bool)$this->sqlite->query($sql, [$pattern, $table]); 76 77 // reload patterns 78 $this->loadPatterns(); 79 $this->propagatePageAssignments($table); 80 81 82 return $ok; 83 } 84 85 /** 86 * Remove an existing assignment pattern from the pattern table 87 * 88 * @param string $pattern 89 * @param string $table 90 * @return bool 91 */ 92 public function removePattern($pattern, $table) 93 { 94 // remove the pattern 95 $sql = 'DELETE FROM schema_assignments_patterns WHERE pattern = ? AND tbl = ?'; 96 $ok = (bool)$this->sqlite->query($sql, [$pattern, $table]); 97 98 // reload patterns 99 $this->loadPatterns(); 100 101 // fetch possibly affected pages 102 $sql = 'SELECT pid FROM schema_assignments WHERE tbl = ?'; 103 $pagerows = $this->sqlite->queryAll($sql, [$table]); 104 105 // reevalute the pages and unassign when needed 106 foreach ($pagerows as $row) { 107 $tables = $this->getPageAssignments($row['pid'], true); 108 if (!in_array($table, $tables)) { 109 $this->deassignPageSchema($row['pid'], $table); 110 } 111 } 112 113 return $ok; 114 } 115 116 /** 117 * Rechecks all assignments of a given page against the current patterns 118 * 119 * @param string $pid 120 */ 121 public function reevaluatePageAssignments($pid) 122 { 123 // reload patterns 124 $this->loadPatterns(); 125 $tables = $this->getPageAssignments($pid, true); 126 127 // fetch possibly affected tables 128 $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?'; 129 $tablerows = $this->sqlite->queryAll($sql, [$pid]); 130 131 // reevalute the tables and apply assignments 132 foreach ($tablerows as $row) { 133 if (in_array($row['tbl'], $tables)) { 134 $this->assignPageSchema($pid, $row['tbl']); 135 } else { 136 $this->deassignPageSchema($pid, $row['tbl']); 137 } 138 } 139 } 140 141 /** 142 * Clear all patterns - deassigns all pages 143 * 144 * This is mostly useful for testing and not used in the interface currently 145 * 146 * @param bool $full fully delete all previous assignments 147 * @return bool 148 */ 149 public function clear($full = false) 150 { 151 $sql = 'DELETE FROM schema_assignments_patterns'; 152 $ok = (bool)$this->sqlite->query($sql); 153 154 if ($full) { 155 $sql = 'DELETE FROM schema_assignments'; 156 } else { 157 $sql = 'UPDATE schema_assignments SET assigned = 0'; 158 } 159 $ok = $ok && (bool)$this->sqlite->query($sql); 160 161 // reload patterns 162 $this->loadPatterns(); 163 164 return $ok; 165 } 166 167 /** 168 * Add page to assignments 169 * 170 * @param string $page 171 * @param string $table 172 * @return bool 173 */ 174 public function assignPageSchema($page, $table) 175 { 176 $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)'; 177 return (bool)$this->sqlite->query($sql, array($page, $table)); 178 } 179 180 /** 181 * Remove page from assignments 182 * 183 * @param string $page 184 * @param string $table 185 * @return bool 186 */ 187 public function deassignPageSchema($page, $table) 188 { 189 $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 0)'; 190 return (bool)$this->sqlite->query($sql, array($page, $table)); 191 } 192 193 /** 194 * Get the whole pattern table 195 * 196 * @return array 197 */ 198 public function getAllPatterns() 199 { 200 return $this->patterns; 201 } 202 203 /** 204 * Returns a list of table names assigned to the given page 205 * 206 * @param string $page 207 * @param bool $checkpatterns Should the current patterns be re-evaluated? 208 * @return \string[] tables assigned 209 */ 210 public function getPageAssignments($page, $checkpatterns = true) 211 { 212 $tables = array(); 213 $page = cleanID($page); 214 215 if ($checkpatterns) { 216 // evaluate patterns 217 $pns = ':' . getNS($page) . ':'; 218 foreach ($this->patterns as $row) { 219 if ($this->matchPagePattern($row['pattern'], $page, $pns)) { 220 $tables[] = $row['tbl']; 221 } 222 } 223 } else { 224 // just select 225 $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ? AND assigned = 1'; 226 $list = $this->sqlite->queryAll($sql, [$page]); 227 foreach ($list as $row) { 228 $tables[] = $row['tbl']; 229 } 230 } 231 232 return array_unique($tables); 233 } 234 235 /** 236 * Get the pages known to struct and their assignment state 237 * 238 * @param null|string $schema limit results to the given schema 239 * @param bool $assignedonly limit results to currently assigned only 240 * @return array 241 */ 242 public function getPages($schema = null, $assignedonly = false) 243 { 244 $sql = 'SELECT pid, tbl, assigned FROM schema_assignments WHERE 1=1'; 245 246 $opts = array(); 247 if ($schema) { 248 $sql .= ' AND tbl = ?'; 249 $opts[] = $schema; 250 } 251 if ($assignedonly) { 252 $sql .= ' AND assigned = 1'; 253 } 254 255 $sql .= ' ORDER BY pid, tbl'; 256 257 $list = $this->sqlite->queryAll($sql, $opts); 258 259 $result = array(); 260 foreach ($list as $row) { 261 $pid = $row['pid']; 262 $tbl = $row['tbl']; 263 if (!isset($result[$pid])) $result[$pid] = array(); 264 $result[$pid][$tbl] = (bool)$row['assigned']; 265 } 266 267 return $result; 268 } 269 270 /** 271 * Check if the given pattern matches the given page 272 * 273 * @param string $pattern the pattern to check against 274 * @param string $page the cleaned pageid to check 275 * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic 276 * @return bool 277 */ 278 protected function matchPagePattern($pattern, $page, $pns = null) 279 { 280 if (trim($pattern, ':') == '**') return true; // match all 281 282 // regex patterns 283 if ($pattern[0] == '/') { 284 return (bool)preg_match($pattern, ":$page"); 285 } 286 287 if (is_null($pns)) { 288 $pns = ':' . getNS($page) . ':'; 289 } 290 291 $ans = ':' . cleanID($pattern) . ':'; 292 if (substr($pattern, -2) == '**') { 293 // upper namespaces match 294 if (strpos($pns, $ans) === 0) { 295 return true; 296 } 297 } elseif (substr($pattern, -1) == '*') { 298 // namespaces match exact 299 if ($ans == $pns) { 300 return true; 301 } 302 } else { 303 // exact match 304 if (cleanID($pattern) == $page) { 305 return true; 306 } 307 } 308 309 return false; 310 } 311 312 /** 313 * Returns all tables of schemas that existed and stored data for the page back then 314 * 315 * @deprecated because we're always only interested in the current state of affairs, even when restoring. 316 * 317 * @param string $page 318 * @param string $ts 319 * @return array 320 */ 321 public function getHistoricAssignments($page, $ts) 322 { 323 $sql = "SELECT DISTINCT tbl FROM schemas WHERE ts <= ? ORDER BY ts DESC"; 324 $tables = $this->sqlite->queryAll($sql, [$ts]); 325 326 $assigned = array(); 327 foreach ($tables as $row) { 328 $table = $row['tbl']; 329 /** @noinspection SqlResolve */ 330 $sql = "SELECT pid FROM data_$table WHERE pid = ? AND rev <= ? LIMIT 1"; 331 $found = $this->sqlite->queryAll($sql, [$page, $ts]); 332 333 if ($found) $assigned[] = $table; 334 } 335 336 return $assigned; 337 } 338 339 /** 340 * fetch all pages where the schema isn't assigned, yet 341 * and reevaluate the page assignments for those pages and assign when needed 342 * 343 * @param $table 344 */ 345 public function propagatePageAssignments($table) 346 { 347 $sql = 'SELECT pid FROM schema_assignments WHERE tbl != ? OR assigned != 1'; 348 $pagerows = $this->sqlite->queryAll($sql, [$table]); 349 350 foreach ($pagerows as $row) { 351 $tables = $this->getPageAssignments($row['pid'], true); 352 if (in_array($table, $tables)) { 353 $this->assignPageSchema($row['pid'], $table); 354 } 355 } 356 } 357} 358