11a97af9eSJohann Duscher<?php 21a97af9eSJohann Duscher/** 31a97af9eSJohann Duscher * Admin interface for Delete Page Guard pattern validation 41a97af9eSJohann Duscher * 51a97af9eSJohann Duscher * @license GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html) - see LICENSE.md 61a97af9eSJohann Duscher * @author Johann Duscher <jonny.dee@posteo.net> 71a97af9eSJohann Duscher * @copyright 2025 Johann Duscher 81a97af9eSJohann Duscher */ 91a97af9eSJohann Duscher 101a97af9eSJohann Duscheruse dokuwiki\Extension\AdminPlugin; 111a97af9eSJohann Duscher 121a97af9eSJohann Duscher// Protect against direct call 131a97af9eSJohann Duscherif (!defined('DOKU_INC')) die(); 141a97af9eSJohann Duscher 151a97af9eSJohann Duscher/** 161a97af9eSJohann Duscher * Class admin_plugin_deletepageguard 171a97af9eSJohann Duscher * 181a97af9eSJohann Duscher * Provides an admin interface for validating Delete Page Guard patterns 191a97af9eSJohann Duscher * and offering configuration guidance to administrators. 201a97af9eSJohann Duscher */ 211a97af9eSJohann Duscherclass admin_plugin_deletepageguard extends AdminPlugin { 221a97af9eSJohann Duscher 231a97af9eSJohann Duscher /** 24*bacaf983SJohann Duscher * Cached instance of the action plugin 25*bacaf983SJohann Duscher * @var action_plugin_deletepageguard|null 26*bacaf983SJohann Duscher */ 27*bacaf983SJohann Duscher private $actionPlugin = null; 28*bacaf983SJohann Duscher 29*bacaf983SJohann Duscher /** 30*bacaf983SJohann Duscher * Get the action plugin instance (cached) 31*bacaf983SJohann Duscher * @return action_plugin_deletepageguard|null 32*bacaf983SJohann Duscher */ 33*bacaf983SJohann Duscher private function getActionPlugin() { 34*bacaf983SJohann Duscher if ($this->actionPlugin === null) { 35*bacaf983SJohann Duscher $this->actionPlugin = plugin_load('action', 'deletepageguard'); 36*bacaf983SJohann Duscher } 37*bacaf983SJohann Duscher return $this->actionPlugin; 38*bacaf983SJohann Duscher } 39*bacaf983SJohann Duscher 40*bacaf983SJohann Duscher /** 411a97af9eSJohann Duscher * Return sort order for position in admin menu 421a97af9eSJohann Duscher * @return int 431a97af9eSJohann Duscher */ 441a97af9eSJohann Duscher public function getMenuSort() { 451a97af9eSJohann Duscher return 200; 461a97af9eSJohann Duscher } 471a97af9eSJohann Duscher 481a97af9eSJohann Duscher /** 499a383d51SJohann Duscher * Return the text to display in the admin menu 509a383d51SJohann Duscher * @return string 519a383d51SJohann Duscher */ 529a383d51SJohann Duscher public function getMenuText($language) { 539a383d51SJohann Duscher return $this->getLang('menu'); 549a383d51SJohann Duscher } 559a383d51SJohann Duscher 569a383d51SJohann Duscher /** 571a97af9eSJohann Duscher * Return true if access to this admin plugin is allowed 581a97af9eSJohann Duscher * @return bool 591a97af9eSJohann Duscher */ 601a97af9eSJohann Duscher public function forAdminOnly() { 611a97af9eSJohann Duscher return true; 621a97af9eSJohann Duscher } 631a97af9eSJohann Duscher 641a97af9eSJohann Duscher /** 651a97af9eSJohann Duscher * Handle user request 661a97af9eSJohann Duscher * @return void 671a97af9eSJohann Duscher */ 681a97af9eSJohann Duscher public function handle() { 699a383d51SJohann Duscher // Nothing to handle - validation is done in html() method 701a97af9eSJohann Duscher } 711a97af9eSJohann Duscher 721a97af9eSJohann Duscher /** 731a97af9eSJohann Duscher * Render HTML output 741a97af9eSJohann Duscher * @return void 751a97af9eSJohann Duscher */ 761a97af9eSJohann Duscher public function html() { 771a97af9eSJohann Duscher echo '<h1>' . $this->getLang('admin_title') . '</h1>'; 781a97af9eSJohann Duscher echo '<div class="level1">'; 791a97af9eSJohann Duscher 809a383d51SJohann Duscher // Determine which patterns to show - use POST data if available, otherwise config 819a383d51SJohann Duscher $patterns = $_POST['test_patterns'] ?? $this->getConf('patterns'); 829a383d51SJohann Duscher 83*bacaf983SJohann Duscher // Show validation results if "Validate" button was clicked 849a383d51SJohann Duscher if (isset($_POST['validate_patterns'])) { 859a383d51SJohann Duscher echo '<h2>' . $this->getLang('validation_results_title') . '</h2>'; 861a97af9eSJohann Duscher $this->showPatternValidation($patterns); 87*bacaf983SJohann Duscher } 88*bacaf983SJohann Duscher // Show matching pages if "Show Matches" button was clicked 89*bacaf983SJohann Duscher elseif (isset($_POST['show_matches'])) { 90*bacaf983SJohann Duscher echo '<h2>' . $this->getLang('validation_results_title') . '</h2>'; 91*bacaf983SJohann Duscher $this->showPatternValidation($patterns); 92*bacaf983SJohann Duscher echo '<h2>' . $this->getLang('matching_pages_title') . '</h2>'; 93*bacaf983SJohann Duscher $this->showMatchingPages($patterns); 94*bacaf983SJohann Duscher } 95*bacaf983SJohann Duscher // Initial load - just show validation 96*bacaf983SJohann Duscher else { 979a383d51SJohann Duscher $this->showPatternValidation($patterns); 989a383d51SJohann Duscher } 991a97af9eSJohann Duscher 1001a97af9eSJohann Duscher // Add validation form 1011a97af9eSJohann Duscher echo '<h2>' . $this->getLang('test_patterns_title') . '</h2>'; 1021a97af9eSJohann Duscher echo '<form method="post" accept-charset="utf-8">'; 1031a97af9eSJohann Duscher echo '<p>' . $this->getLang('test_patterns_help') . '</p>'; 1041a97af9eSJohann Duscher echo '<textarea name="test_patterns" rows="10" cols="80" class="edit">' . hsc($patterns) . '</textarea><br>'; 1051a97af9eSJohann Duscher echo '<input type="submit" name="validate_patterns" value="' . $this->getLang('validate_button') . '" class="button"> '; 106*bacaf983SJohann Duscher echo '<input type="submit" name="show_matches" value="' . $this->getLang('show_matches_button') . '" class="button">'; 1071a97af9eSJohann Duscher echo '</form>'; 1081a97af9eSJohann Duscher 1091a97af9eSJohann Duscher echo '</div>'; 1101a97af9eSJohann Duscher } 1111a97af9eSJohann Duscher 1121a97af9eSJohann Duscher /** 1131a97af9eSJohann Duscher * Display pattern validation results 1141a97af9eSJohann Duscher * @param string $patterns The patterns to validate 1151a97af9eSJohann Duscher * @return void 1161a97af9eSJohann Duscher */ 1171a97af9eSJohann Duscher private function showPatternValidation($patterns) { 1181a97af9eSJohann Duscher if (empty(trim($patterns))) { 1191a97af9eSJohann Duscher echo '<div class="info">' . $this->getLang('no_patterns') . '</div>'; 1201a97af9eSJohann Duscher return; 1211a97af9eSJohann Duscher } 1221a97af9eSJohann Duscher 1231a97af9eSJohann Duscher $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 1241a97af9eSJohann Duscher $hasErrors = false; 1251a97af9eSJohann Duscher $validCount = 0; 1261a97af9eSJohann Duscher 1271a97af9eSJohann Duscher echo '<div class="level2">'; 1281a97af9eSJohann Duscher echo '<h3>' . $this->getLang('validation_results') . '</h3>'; 1291a97af9eSJohann Duscher echo '<ul>'; 1301a97af9eSJohann Duscher 1311a97af9eSJohann Duscher foreach ($lines as $i => $line) { 1321a97af9eSJohann Duscher $pattern = trim($line); 1331a97af9eSJohann Duscher if ($pattern === '') continue; 1341a97af9eSJohann Duscher 1351a97af9eSJohann Duscher $lineNum = $i + 1; 1361a97af9eSJohann Duscher $status = $this->validateSinglePattern($pattern); 1371a97af9eSJohann Duscher 1381a97af9eSJohann Duscher if ($status === true) { 1391a97af9eSJohann Duscher echo '<li><span style="color: green; font-weight: bold;">✓</span> '; 1401a97af9eSJohann Duscher echo '<strong>Line ' . $lineNum . ':</strong> <code>' . hsc($pattern) . '</code></li>'; 1411a97af9eSJohann Duscher $validCount++; 1421a97af9eSJohann Duscher } else { 1431a97af9eSJohann Duscher echo '<li><span style="color: red; font-weight: bold;">✗</span> '; 1441a97af9eSJohann Duscher echo '<strong>Line ' . $lineNum . ':</strong> <code>' . hsc($pattern) . '</code><br>'; 1451a97af9eSJohann Duscher echo ' <em style="color: red;">' . hsc($status) . '</em></li>'; 1461a97af9eSJohann Duscher $hasErrors = true; 1471a97af9eSJohann Duscher } 1481a97af9eSJohann Duscher } 1491a97af9eSJohann Duscher 1501a97af9eSJohann Duscher echo '</ul>'; 1511a97af9eSJohann Duscher 1521a97af9eSJohann Duscher if (!$hasErrors && $validCount > 0) { 1531a97af9eSJohann Duscher echo '<div class="success">' . sprintf($this->getLang('all_patterns_valid'), $validCount) . '</div>'; 1541a97af9eSJohann Duscher } elseif ($hasErrors) { 1551a97af9eSJohann Duscher echo '<div class="error">' . $this->getLang('some_patterns_invalid') . '</div>'; 1561a97af9eSJohann Duscher } 1571a97af9eSJohann Duscher 1581a97af9eSJohann Duscher echo '</div>'; 1591a97af9eSJohann Duscher } 1601a97af9eSJohann Duscher 1611a97af9eSJohann Duscher /** 1629a383d51SJohann Duscher * Validate a single pattern by delegating to the action plugin's validator. 1639a383d51SJohann Duscher * This ensures consistent validation logic between admin UI and runtime checks. 1649a383d51SJohann Duscher * 1651a97af9eSJohann Duscher * @param string $pattern The pattern to validate 1661a97af9eSJohann Duscher * @return string|true True if valid, error message if invalid 1671a97af9eSJohann Duscher */ 1681a97af9eSJohann Duscher private function validateSinglePattern($pattern) { 1699a383d51SJohann Duscher // Load the action plugin to use its centralized validation logic 170*bacaf983SJohann Duscher $actionPlugin = $this->getActionPlugin(); 1719a383d51SJohann Duscher if (!$actionPlugin) { 1729a383d51SJohann Duscher return 'Error: Could not load validation service'; 1731a97af9eSJohann Duscher } 1741a97af9eSJohann Duscher 1759a383d51SJohann Duscher // Use the action plugin's validateRegexPattern method (without line number) 1769a383d51SJohann Duscher $result = $actionPlugin->validateRegexPattern($pattern, 0); 1779a383d51SJohann Duscher 1789a383d51SJohann Duscher // The action plugin returns true for valid, string for invalid 1799a383d51SJohann Duscher // We need to strip the "Line 0: " prefix if present 1809a383d51SJohann Duscher if (is_string($result)) { 1819a383d51SJohann Duscher $result = preg_replace('/^Line 0: /', '', $result); 1821a97af9eSJohann Duscher } 1831a97af9eSJohann Duscher 1849a383d51SJohann Duscher return $result; 1851a97af9eSJohann Duscher } 186*bacaf983SJohann Duscher 187*bacaf983SJohann Duscher /** 188*bacaf983SJohann Duscher * Show all wiki pages that match the given patterns 189*bacaf983SJohann Duscher * @param string $patterns The patterns to test 190*bacaf983SJohann Duscher * @return void 191*bacaf983SJohann Duscher */ 192*bacaf983SJohann Duscher private function showMatchingPages($patterns) { 193*bacaf983SJohann Duscher // Load action plugin for matching logic 194*bacaf983SJohann Duscher $actionPlugin = $this->getActionPlugin(); 195*bacaf983SJohann Duscher if (!$actionPlugin) { 196*bacaf983SJohann Duscher echo '<div class="error">Error: Could not load action plugin</div>'; 197*bacaf983SJohann Duscher return; 198*bacaf983SJohann Duscher } 199*bacaf983SJohann Duscher 200*bacaf983SJohann Duscher // Parse patterns 201*bacaf983SJohann Duscher $lines = preg_split('/\R+/', $patterns, -1, PREG_SPLIT_NO_EMPTY); 202*bacaf983SJohann Duscher $validPatterns = []; 203*bacaf983SJohann Duscher 204*bacaf983SJohann Duscher foreach ($lines as $line) { 205*bacaf983SJohann Duscher $pattern = trim($line); 206*bacaf983SJohann Duscher if ($pattern === '') continue; 207*bacaf983SJohann Duscher 208*bacaf983SJohann Duscher // Only use valid patterns 209*bacaf983SJohann Duscher if ($actionPlugin->validateRegexPattern($pattern, 0) === true) { 210*bacaf983SJohann Duscher $validPatterns[] = $pattern; 211*bacaf983SJohann Duscher } 212*bacaf983SJohann Duscher } 213*bacaf983SJohann Duscher 214*bacaf983SJohann Duscher if (empty($validPatterns)) { 215*bacaf983SJohann Duscher echo '<div class="info">' . $this->getLang('no_valid_patterns') . '</div>'; 216*bacaf983SJohann Duscher return; 217*bacaf983SJohann Duscher } 218*bacaf983SJohann Duscher 219*bacaf983SJohann Duscher // Get all pages using DokuWiki's search function 220*bacaf983SJohann Duscher global $conf; 221*bacaf983SJohann Duscher $allPages = []; 222*bacaf983SJohann Duscher 223*bacaf983SJohann Duscher // DokuWiki's search expects to search in the pages directory 224*bacaf983SJohann Duscher $pagesDir = $conf['datadir'] . '/pages'; 225*bacaf983SJohann Duscher search($allPages, $pagesDir, 'search_allpages', []); 226*bacaf983SJohann Duscher 227*bacaf983SJohann Duscher // Fallback: use indexer if search returns nothing 228*bacaf983SJohann Duscher if (empty($allPages)) { 229*bacaf983SJohann Duscher require_once(DOKU_INC . 'inc/indexer.php'); 230*bacaf983SJohann Duscher $indexer = idx_get_indexer(); 231*bacaf983SJohann Duscher $pagesList = $indexer->getPages(); 232*bacaf983SJohann Duscher 233*bacaf983SJohann Duscher // Convert simple page list to expected format 234*bacaf983SJohann Duscher if (!empty($pagesList)) { 235*bacaf983SJohann Duscher $allPages = []; 236*bacaf983SJohann Duscher foreach ($pagesList as $pageId) { 237*bacaf983SJohann Duscher $allPages[] = ['id' => $pageId]; 238*bacaf983SJohann Duscher } 239*bacaf983SJohann Duscher } 240*bacaf983SJohann Duscher } 241*bacaf983SJohann Duscher 242*bacaf983SJohann Duscher if (empty($allPages)) { 243*bacaf983SJohann Duscher echo '<div class="info">' . $this->getLang('no_pages_found') . '</div>'; 244*bacaf983SJohann Duscher return; 245*bacaf983SJohann Duscher } 246*bacaf983SJohann Duscher 247*bacaf983SJohann Duscher // Test each page against patterns 248*bacaf983SJohann Duscher $matchedPages = []; 249*bacaf983SJohann Duscher $testedCount = 0; 250*bacaf983SJohann Duscher foreach ($allPages as $page) { 251*bacaf983SJohann Duscher $pageId = $page['id']; 252*bacaf983SJohann Duscher $matchTarget = $actionPlugin->getMatchTarget($pageId); 253*bacaf983SJohann Duscher $testedCount++; 254*bacaf983SJohann Duscher 255*bacaf983SJohann Duscher foreach ($validPatterns as $pattern) { 256*bacaf983SJohann Duscher if ($actionPlugin->matchesPattern($pattern, $matchTarget)) { 257*bacaf983SJohann Duscher $matchedPages[] = [ 258*bacaf983SJohann Duscher 'id' => $pageId, 259*bacaf983SJohann Duscher 'target' => $matchTarget, 260*bacaf983SJohann Duscher 'pattern' => $pattern 261*bacaf983SJohann Duscher ]; 262*bacaf983SJohann Duscher break; // Only list each page once 263*bacaf983SJohann Duscher } 264*bacaf983SJohann Duscher } 265*bacaf983SJohann Duscher } 266*bacaf983SJohann Duscher 267*bacaf983SJohann Duscher // Display results 268*bacaf983SJohann Duscher echo '<div class="level2">'; 269*bacaf983SJohann Duscher 270*bacaf983SJohann Duscher if (empty($matchedPages)) { 271*bacaf983SJohann Duscher echo '<div class="info">' . sprintf($this->getLang('no_matching_pages'), count($allPages)) . '</div>'; 272*bacaf983SJohann Duscher } else { 273*bacaf983SJohann Duscher echo '<p>' . sprintf($this->getLang('found_matching_pages'), count($matchedPages), count($allPages)) . '</p>'; 274*bacaf983SJohann Duscher echo '<table class="inline">'; 275*bacaf983SJohann Duscher echo '<thead><tr>'; 276*bacaf983SJohann Duscher echo '<th>' . $this->getLang('page_id') . '</th>'; 277*bacaf983SJohann Duscher echo '<th>' . $this->getLang('match_target') . '</th>'; 278*bacaf983SJohann Duscher echo '<th>' . $this->getLang('matched_pattern') . '</th>'; 279*bacaf983SJohann Duscher echo '</tr></thead>'; 280*bacaf983SJohann Duscher echo '<tbody>'; 281*bacaf983SJohann Duscher 282*bacaf983SJohann Duscher foreach ($matchedPages as $match) { 283*bacaf983SJohann Duscher echo '<tr>'; 284*bacaf983SJohann Duscher echo '<td><a href="' . wl($match['id']) . '">' . hsc($match['id']) . '</a></td>'; 285*bacaf983SJohann Duscher echo '<td><code>' . hsc($match['target']) . '</code></td>'; 286*bacaf983SJohann Duscher echo '<td><code>' . hsc($match['pattern']) . '</code></td>'; 287*bacaf983SJohann Duscher echo '</tr>'; 288*bacaf983SJohann Duscher } 289*bacaf983SJohann Duscher 290*bacaf983SJohann Duscher echo '</tbody></table>'; 291*bacaf983SJohann Duscher } 292*bacaf983SJohann Duscher 293*bacaf983SJohann Duscher echo '</div>'; 294*bacaf983SJohann Duscher } 2951a97af9eSJohann Duscher} 296