1<?php 2/** 3 * Admin interface for Delete Page Guard pattern validation 4 * 5 * @license GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html) - see LICENSE.md 6 * @author Johann Duscher <jonny.dee@posteo.net> 7 * @copyright 2025 Johann Duscher 8 */ 9 10use dokuwiki\Extension\AdminPlugin; 11 12// Protect against direct call 13if (!defined('DOKU_INC')) die(); 14 15/** 16 * Class admin_plugin_deletepageguard 17 * 18 * Provides an admin interface for validating Delete Page Guard patterns 19 * and offering configuration guidance to administrators. 20 */ 21class admin_plugin_deletepageguard extends AdminPlugin { 22 23 /** 24 * Cached instance of the action plugin 25 * @var action_plugin_deletepageguard|null 26 */ 27 private $actionPlugin = null; 28 29 /** 30 * Get the action plugin instance (cached) 31 * @return action_plugin_deletepageguard|null 32 */ 33 private function getActionPlugin() { 34 if ($this->actionPlugin === null) { 35 $this->actionPlugin = plugin_load('action', 'deletepageguard'); 36 } 37 return $this->actionPlugin; 38 } 39 40 /** 41 * Return sort order for position in admin menu 42 * @return int 43 */ 44 public function getMenuSort() { 45 return 200; 46 } 47 48 /** 49 * Return the text to display in the admin menu 50 * @return string 51 */ 52 public function getMenuText($language) { 53 return $this->getLang('menu'); 54 } 55 56 /** 57 * Return true if access to this admin plugin is allowed 58 * @return bool 59 */ 60 public function forAdminOnly() { 61 return true; 62 } 63 64 /** 65 * Handle user request 66 * @return void 67 */ 68 public function handle() { 69 // Nothing to handle - validation is done in html() method 70 } 71 72 /** 73 * Render HTML output 74 * @return void 75 */ 76 public function html() { 77 echo '<h1>' . $this->getLang('admin_title') . '</h1>'; 78 echo '<div class="level1">'; 79 80 // Determine which patterns to show - use POST data if available, otherwise config 81 $patterns = $_POST['test_patterns'] ?? $this->getConf('patterns'); 82 83 // Show validation results if "Validate" button was clicked 84 if (isset($_POST['validate_patterns'])) { 85 echo '<h2>' . $this->getLang('validation_results_title') . '</h2>'; 86 $this->showPatternValidation($patterns); 87 } 88 // Show matching pages if "Show Matches" button was clicked 89 elseif (isset($_POST['show_matches'])) { 90 echo '<h2>' . $this->getLang('validation_results_title') . '</h2>'; 91 $this->showPatternValidation($patterns); 92 echo '<h2>' . $this->getLang('matching_pages_title') . '</h2>'; 93 $this->showMatchingPages($patterns); 94 } 95 // Initial load - just show validation 96 else { 97 $this->showPatternValidation($patterns); 98 } 99 100 // Add validation form 101 echo '<h2>' . $this->getLang('test_patterns_title') . '</h2>'; 102 echo '<form method="post" accept-charset="utf-8">'; 103 echo '<p>' . $this->getLang('test_patterns_help') . '</p>'; 104 echo '<textarea name="test_patterns" rows="10" cols="80" class="edit">' . hsc($patterns) . '</textarea><br>'; 105 echo '<input type="submit" name="validate_patterns" value="' . $this->getLang('validate_button') . '" class="button"> '; 106 echo '<input type="submit" name="show_matches" value="' . $this->getLang('show_matches_button') . '" class="button">'; 107 echo '</form>'; 108 109 echo '</div>'; 110 } 111 112 /** 113 * Display pattern validation results 114 * @param string $patterns The patterns to validate 115 * @return void 116 */ 117 private function showPatternValidation($patterns) { 118 if (empty(trim($patterns))) { 119 echo '<div class="info">' . $this->getLang('no_patterns') . '</div>'; 120 return; 121 } 122 123 $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 124 $hasErrors = false; 125 $validCount = 0; 126 127 echo '<div class="level2">'; 128 echo '<h3>' . $this->getLang('validation_results') . '</h3>'; 129 echo '<ul>'; 130 131 foreach ($lines as $i => $line) { 132 $pattern = trim($line); 133 if ($pattern === '') continue; 134 135 $lineNum = $i + 1; 136 $status = $this->validateSinglePattern($pattern); 137 138 if ($status === true) { 139 echo '<li><span style="color: green; font-weight: bold;">✓</span> '; 140 echo '<strong>Line ' . $lineNum . ':</strong> <code>' . hsc($pattern) . '</code></li>'; 141 $validCount++; 142 } else { 143 echo '<li><span style="color: red; font-weight: bold;">✗</span> '; 144 echo '<strong>Line ' . $lineNum . ':</strong> <code>' . hsc($pattern) . '</code><br>'; 145 echo ' <em style="color: red;">' . hsc($status) . '</em></li>'; 146 $hasErrors = true; 147 } 148 } 149 150 echo '</ul>'; 151 152 if (!$hasErrors && $validCount > 0) { 153 echo '<div class="success">' . sprintf($this->getLang('all_patterns_valid'), $validCount) . '</div>'; 154 } elseif ($hasErrors) { 155 echo '<div class="error">' . $this->getLang('some_patterns_invalid') . '</div>'; 156 } 157 158 echo '</div>'; 159 } 160 161 /** 162 * Validate a single pattern by delegating to the action plugin's validator. 163 * This ensures consistent validation logic between admin UI and runtime checks. 164 * 165 * @param string $pattern The pattern to validate 166 * @return string|true True if valid, error message if invalid 167 */ 168 private function validateSinglePattern($pattern) { 169 // Load the action plugin to use its centralized validation logic 170 $actionPlugin = $this->getActionPlugin(); 171 if (!$actionPlugin) { 172 return 'Error: Could not load validation service'; 173 } 174 175 // Use the action plugin's validateRegexPattern method (without line number) 176 $result = $actionPlugin->validateRegexPattern($pattern, 0); 177 178 // The action plugin returns true for valid, string for invalid 179 // We need to strip the "Line 0: " prefix if present 180 if (is_string($result)) { 181 $result = preg_replace('/^Line 0: /', '', $result); 182 } 183 184 return $result; 185 } 186 187 /** 188 * Show all wiki pages that match the given patterns 189 * @param string $patterns The patterns to test 190 * @return void 191 */ 192 private function showMatchingPages($patterns) { 193 // Load action plugin for matching logic 194 $actionPlugin = $this->getActionPlugin(); 195 if (!$actionPlugin) { 196 echo '<div class="error">Error: Could not load action plugin</div>'; 197 return; 198 } 199 200 // Parse patterns 201 $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 202 $validPatterns = []; 203 204 foreach ($lines as $line) { 205 $pattern = trim($line); 206 if ($pattern === '') continue; 207 208 // Only use valid patterns 209 if ($actionPlugin->validateRegexPattern($pattern, 0) === true) { 210 $validPatterns[] = $pattern; 211 } 212 } 213 214 if (empty($validPatterns)) { 215 echo '<div class="info">' . $this->getLang('no_valid_patterns') . '</div>'; 216 return; 217 } 218 219 // Get all pages using DokuWiki's search function 220 global $conf; 221 $allPages = []; 222 223 // DokuWiki's search expects to search in the pages directory 224 $pagesDir = $conf['datadir'] . '/pages'; 225 search($allPages, $pagesDir, 'search_allpages', []); 226 227 // Fallback: use indexer if search returns nothing 228 if (empty($allPages)) { 229 require_once(DOKU_INC . 'inc/indexer.php'); 230 $indexer = idx_get_indexer(); 231 $pagesList = $indexer->getPages(); 232 233 // Convert simple page list to expected format 234 if (!empty($pagesList)) { 235 $allPages = []; 236 foreach ($pagesList as $pageId) { 237 $allPages[] = ['id' => $pageId]; 238 } 239 } 240 } 241 242 if (empty($allPages)) { 243 echo '<div class="info">' . $this->getLang('no_pages_found') . '</div>'; 244 return; 245 } 246 247 // Test each page against patterns 248 $matchedPages = []; 249 $testedCount = 0; 250 foreach ($allPages as $page) { 251 $pageId = $page['id']; 252 $matchTarget = $actionPlugin->getMatchTarget($pageId); 253 $testedCount++; 254 255 foreach ($validPatterns as $pattern) { 256 if ($actionPlugin->matchesPattern($pattern, $matchTarget)) { 257 $matchedPages[] = [ 258 'id' => $pageId, 259 'target' => $matchTarget, 260 'pattern' => $pattern 261 ]; 262 break; // Only list each page once 263 } 264 } 265 } 266 267 // Display results 268 echo '<div class="level2">'; 269 270 if (empty($matchedPages)) { 271 echo '<div class="info">' . sprintf($this->getLang('no_matching_pages'), count($allPages)) . '</div>'; 272 } else { 273 echo '<p>' . sprintf($this->getLang('found_matching_pages'), count($matchedPages), count($allPages)) . '</p>'; 274 echo '<table class="inline">'; 275 echo '<thead><tr>'; 276 echo '<th>' . $this->getLang('page_id') . '</th>'; 277 echo '<th>' . $this->getLang('match_target') . '</th>'; 278 echo '<th>' . $this->getLang('matched_pattern') . '</th>'; 279 echo '</tr></thead>'; 280 echo '<tbody>'; 281 282 foreach ($matchedPages as $match) { 283 echo '<tr>'; 284 echo '<td><a href="' . wl($match['id']) . '">' . hsc($match['id']) . '</a></td>'; 285 echo '<td><code>' . hsc($match['target']) . '</code></td>'; 286 echo '<td><code>' . hsc($match['pattern']) . '</code></td>'; 287 echo '</tr>'; 288 } 289 290 echo '</tbody></table>'; 291 } 292 293 echo '</div>'; 294 } 295} 296