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