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