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