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