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