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