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