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