1<?php 2 3namespace dokuwiki\plugin\extendpage\meta; 4 5/** 6 * Class Assignments 7 * 8 * Manages the assignment of header, footer pages to pages and namespaces 9 * 10 * This is a singleton. Assignment data is only loaded once per request. 11 * 12 * @package dokuwiki\plugin\extendpage\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_extendpage_db $helper */ 49 $helper = plugin_load('helper', 'extendpage_db'); 50 $this->sqlite = $helper->getDB(); 51 52 $this->loadPatterns(); 53 } 54 55 /** 56 * Load existing assignment patterns 57 */ 58 protected function loadPatterns() 59 { 60 $sql = 'SELECT * FROM assignments_patterns ORDER BY pattern'; 61 $res = $this->sqlite->query($sql); 62 $this->patterns = $this->sqlite->res2arr($res); 63 $this->sqlite->res_close($res); 64 } 65 66 /** 67 * Add a new assignment pattern to the pattern table 68 * 69 * @param string $pattern 70 * @param string $page 71 * @return bool 72 */ 73 public function addPattern($pattern, $page, $pos) 74 { 75 // add the pattern 76 $sql = 'REPLACE INTO assignments_patterns (pattern, page, pos) VALUES (?,?,?)'; 77 $ok = (bool) $this->sqlite->query($sql, array($pattern, $page, $pos)); 78 79 $sql = 'SELECT last_insert_rowid()'; 80 $res = $this->sqlite->query($sql); 81 82 // reload patterns 83 $this->loadPatterns(); 84 $this->propagatePageAssignments($this->sqlite->res2single($res)); 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 $page 94 * @return bool 95 */ 96 public function removePattern($id) 97 { 98 // remove the pattern 99 $sql = 'DELETE FROM assignments_patterns WHERE id = ?'; 100 $ok = (bool) $this->sqlite->query($sql, array($id)); 101 102 // reload patterns 103 $this->loadPatterns(); 104 105 // fetch possibly affected pages 106 $sql = 'SELECT pid FROM assignments WHERE pattern_id = ?'; 107 $res = $this->sqlite->query($sql, $id); 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 $pages = $this->getPageAssignments($row['pid'], $row['pos'], true); 114 if (!in_array($page, $pages)) { 115 $this->deassignPageExtension($row['pid'], $row['pos']); 116 } 117 } 118 119 return $ok; 120 } 121 122 /** 123 * Clear all patterns - deassigns all pages 124 * 125 * This is mostly useful for testing and not used in the interface currently 126 * 127 * @param bool $full fully delete all previous assignments 128 * @return bool 129 */ 130 public function clear($full = false) 131 { 132 $sql = 'DELETE FROM assignments_patterns'; 133 $ok = (bool) $this->sqlite->query($sql); 134 135 if ($full) { 136 $sql = 'DELETE FROM assignments'; 137 } else { 138 $sql = 'UPDATE assignments SET assigned = 0'; 139 } 140 $ok = $ok && (bool) $this->sqlite->query($sql); 141 142 // reload patterns 143 $this->loadPatterns(); 144 145 return $ok; 146 } 147 148 /** 149 * Add page to assignments 150 * 151 * @param string $page 152 * @param string $ext 153 * @return bool 154 */ 155 public function assignPageExtension($page, $pattern) 156 { 157 $sql = 'REPLACE INTO assignments (pid, pattern_id, assigned) VALUES (?, ?, 1)'; 158 return (bool) $this->sqlite->query($sql, array($page, $pattern)); 159 } 160 161 /** 162 * Remove page from assignments 163 * 164 * @param string $page 165 * @param string $ext 166 * @return bool 167 */ 168 public function deassignPageExtension($page, $pattern) 169 { 170 $sql = 'REPLACE INTO assignments (pid, pattern_id, assigned) VALUES (?, ?, 0)'; 171 return (bool) $this->sqlite->query($sql, array($page, $pattern)); 172 } 173 174 /** 175 * Get the whole pattern table 176 * 177 * @return array 178 */ 179 public function getAllPatterns() 180 { 181 return $this->patterns; 182 } 183 184 /** 185 * Returns a list of extension page names assigned to the given page and position 186 * 187 * @param string $page 188 * @param string $pos 189 * @param bool $checkpatterns Should the current patterns be re-evaluated? 190 * @return \string[] extensions assigned 191 */ 192 public function getPageAssignments($page, $pos, $checkpatterns = true) 193 { 194 $extensions = 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 ($row['pos'] === $pos)) { 203 $extensions[] = array('page' => $row['page']); 204 } 205 } 206 } else { 207 // just select 208 $sql = 'SELECT assignments_patterns.page 209 FROM assignments, assignments_patterns 210 WHERE assignments.pattern_id = assignments_patterns.id 211 AND assignments.pid = ? 212 AND assignments_patterns.pos = ? 213 AND assignments.assigned = 1'; 214 $res = $this->sqlite->query($sql, array($page, $pos)); 215 $list = $this->sqlite->res2arr($res); 216 $this->sqlite->res_close($res); 217 foreach ($list as $row) { 218 $extensions[] = array( 219 'page' => $row['assignments_patterns.page'] 220 ); 221 } 222 } 223 224 return array_unique($extensions); 225 } 226 227 /** 228 * Check if the given pattern matches the given page 229 * 230 * @param string $pattern the pattern to check against 231 * @param string $page the cleaned pageid to check 232 * @param string|null $pns optimization, the colon wrapped namespace of the page, set null for automatic 233 * @return bool 234 */ 235 protected function matchPagePattern($pattern, $page, $pns = null) 236 { 237 if (trim($pattern, ':') == '**') return true; // match all 238 239 // regex patterns 240 if ($pattern[0] == '/') { 241 return (bool) preg_match($pattern, ":$page"); 242 } 243 244 if (is_null($pns)) { 245 $pns = ':' . getNS($page) . ':'; 246 } 247 248 $ans = ':' . cleanID($pattern) . ':'; 249 if (substr($pattern, -2) == '**') { 250 // upper namespaces match 251 if (strpos($pns, $ans) === 0) { 252 return true; 253 } 254 } elseif (substr($pattern, -1) == '*') { 255 // namespaces match exact 256 if ($ans == $pns) { 257 return true; 258 } 259 } else { 260 // exact match 261 if (cleanID($pattern) == $page) { 262 return true; 263 } 264 } 265 266 return false; 267 } 268 269 /** 270 * fetch all pages where the extension page isn't assigned, yet and reevaluate the page assignments for those pages and assign when needed 271 * 272 * @param $page 273 */ 274 public function propagatePageAssignments($pattern) 275 { 276 $sql = 'SELECT assignments.pid, assignments.pattern_id, assignments_patterns.pos 277 FROM assignments, assignments_patterns 278 WHERE assignments.pattern_id = assignments_patterns.id 279 AND assignments.assigned != 1 AND assignments.pattern_id = ?'; 280 $res = $this->sqlite->query($sql, $pattern); 281 $pagerows = $this->sqlite->res2arr($res); 282 $this->sqlite->res_close($res); 283 284 foreach ($pagerows as $row) { 285 $pages = $this->getPageAssignments( 286 $row['assignments.pid'], 287 $row['assignments_patterns.pos'], true 288 ); 289 if (in_array($row['assignments_patterns.page'], $pages)) { 290 $this->assignPageExtension($row['assignments.pid'], $pattern); 291 } 292 } 293 } 294} 295