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 Assignments */ 23 protected static $instance = null; 24 25 /** 26 * Get the singleton instance of the Assignments 27 * 28 * @param bool $forcereload create a new instace to reload the assignment data 29 * @return Assignments 30 */ 31 public static function getInstance($forcereload = false) 32 { 33 if (is_null(self::$instance) or $forcereload) { 34 $class = get_called_class(); 35 self::$instance = new $class(); 36 } 37 return self::$instance; 38 } 39 40 /** 41 * Assignments constructor. 42 * 43 * Not public. Use Assignments::getInstance() instead 44 */ 45 protected function __construct() 46 { 47 /** @var \helper_plugin_struct_db $helper */ 48 $helper = plugin_load('helper', 'struct_db'); 49 $this->sqlite = $helper->getDB(); 50 51 $this->loadPatterns(); 52 } 53 54 55 /** 56 * Load existing assignment patterns 57 */ 58 protected function loadPatterns() 59 { 60 $sql = 'SELECT * FROM schema_assignments_patterns ORDER BY pattern'; 61 $res = $this->sqlite->query($sql); 62 $this->patterns = $this->sqlite->res2arr($res); 63 $this->sqlite->res_close($res); 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, array($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, array($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 $res = $this->sqlite->query($sql, $table); 106 $pagerows = $this->sqlite->res2arr($res); 107 $this->sqlite->res_close($res); 108 109 // reevalute the pages and unassign when needed 110 foreach ($pagerows as $row) { 111 $tables = $this->getPageAssignments($row['pid'], true); 112 if (!in_array($table, $tables)) { 113 $this->deassignPageSchema($row['pid'], $table); 114 } 115 } 116 117 return $ok; 118 } 119 120 /** 121 * Rechecks all assignments of a given page against the current patterns 122 * 123 * @param string $pid 124 */ 125 public function reevaluatePageAssignments($pid) 126 { 127 // reload patterns 128 $this->loadPatterns(); 129 $tables = $this->getPageAssignments($pid, true); 130 131 // fetch possibly affected tables 132 $sql = 'SELECT tbl FROM schema_assignments WHERE pid = ?'; 133 $res = $this->sqlite->query($sql, $pid); 134 $tablerows = $this->sqlite->res2arr($res); 135 $this->sqlite->res_close($res); 136 137 // reevalute the tables and apply assignments 138 foreach ($tablerows as $row) { 139 if (in_array($row['tbl'], $tables)) { 140 $this->assignPageSchema($pid, $row['tbl']); 141 } else { 142 $this->deassignPageSchema($pid, $row['tbl']); 143 } 144 } 145 } 146 147 /** 148 * Clear all patterns - deassigns all pages 149 * 150 * This is mostly useful for testing and not used in the interface currently 151 * 152 * @param bool $full fully delete all previous assignments 153 * @return bool 154 */ 155 public function clear($full = false) 156 { 157 $sql = 'DELETE FROM schema_assignments_patterns'; 158 $ok = (bool)$this->sqlite->query($sql); 159 160 if ($full) { 161 $sql = 'DELETE FROM schema_assignments'; 162 } else { 163 $sql = 'UPDATE schema_assignments SET assigned = 0'; 164 } 165 $ok = $ok && (bool)$this->sqlite->query($sql); 166 167 // reload patterns 168 $this->loadPatterns(); 169 170 return $ok; 171 } 172 173 /** 174 * Add page to assignments 175 * 176 * @param string $page 177 * @param string $table 178 * @return bool 179 */ 180 public function assignPageSchema($page, $table) 181 { 182 $sql = 'REPLACE INTO schema_assignments (pid, tbl, assigned) VALUES (?, ?, 1)'; 183 return (bool)$this->sqlite->query($sql, array($page, $table)); 184 } 185 186 /** 187 * Remove page from assignments 188 * 189 * @param string $page 190 * @param string $table 191 * @return bool 192 */ 193 public function deassignPageSchema($page, $table) 194 { 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 { 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 { 218 $tables = array(); 219 $page = cleanID($page); 220 221 if ($checkpatterns) { 222 // evaluate patterns 223 $pns = ':' . getNS($page) . ':'; 224 foreach ($this->patterns as $row) { 225 if ($this->matchPagePattern($row['pattern'], $page, $pns)) { 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 $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 { 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 { 290 if (trim($pattern, ':') == '**') return true; // match all 291 292 // regex patterns 293 if ($pattern[0] == '/') { 294 return (bool)preg_match($pattern, ":$page"); 295 } 296 297 if (is_null($pns)) { 298 $pns = ':' . getNS($page) . ':'; 299 } 300 301 $ans = ':' . cleanID($pattern) . ':'; 302 if (substr($pattern, -2) == '**') { 303 // upper namespaces match 304 if (strpos($pns, $ans) === 0) { 305 return true; 306 } 307 } elseif (substr($pattern, -1) == '*') { 308 // namespaces match exact 309 if ($ans == $pns) { 310 return true; 311 } 312 } else { 313 // exact match 314 if (cleanID($pattern) == $page) { 315 return true; 316 } 317 } 318 319 return false; 320 } 321 322 /** 323 * Returns all tables of schemas that existed and stored data for the page back then 324 * 325 * @deprecated because we're always only interested in the current state of affairs, even when restoring. 326 * 327 * @param string $page 328 * @param string $ts 329 * @return array 330 */ 331 public function getHistoricAssignments($page, $ts) 332 { 333 $sql = "SELECT DISTINCT tbl FROM schemas WHERE ts <= ? ORDER BY ts DESC"; 334 $res = $this->sqlite->query($sql, $ts); 335 $tables = $this->sqlite->res2arr($res); 336 $this->sqlite->res_close($res); 337 338 $assigned = array(); 339 foreach ($tables as $row) { 340 $table = $row['tbl']; 341 /** @noinspection SqlResolve */ 342 $sql = "SELECT pid FROM data_$table WHERE pid = ? AND rev <= ? LIMIT 1"; 343 $res = $this->sqlite->query($sql, $page, $ts); 344 $found = $this->sqlite->res2arr($res); 345 $this->sqlite->res_close($res); 346 347 if ($found) $assigned[] = $table; 348 } 349 350 return $assigned; 351 } 352 353 /** 354 * fetch all pages where the schema isn't assigned, yet 355 * and reevaluate the page assignments for those pages and assign when needed 356 * 357 * @param $table 358 */ 359 public function propagatePageAssignments($table) 360 { 361 $sql = 'SELECT pid FROM schema_assignments WHERE tbl != ? OR assigned != 1'; 362 $res = $this->sqlite->query($sql, $table); 363 $pagerows = $this->sqlite->res2arr($res); 364 $this->sqlite->res_close($res); 365 366 foreach ($pagerows as $row) { 367 $tables = $this->getPageAssignments($row['pid'], true); 368 if (in_array($table, $tables)) { 369 $this->assignPageSchema($row['pid'], $table); 370 } 371 } 372 } 373} 374