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