xref: /plugin/deletepageguard/admin.php (revision bacaf98395dfe5da6c87ae6ce4a6b1b35aa59a89)
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 '&nbsp;&nbsp;&nbsp;<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