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