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