xref: /plugin/deletepageguard/action.php (revision e39ccd63478fc57ef8a8eb17b92c646766ac43fd)
1*e39ccd63SJonny Dee<?php
2*e39ccd63SJonny Dee/**
3*e39ccd63SJonny Dee * Delete Guard Plugin for DokuWiki
4*e39ccd63SJonny Dee *
5*e39ccd63SJonny Dee * This action plugin prevents the deletion of pages by blocking "empty save"
6*e39ccd63SJonny Dee * operations on pages whose IDs or file paths match a set of user‑defined
7*e39ccd63SJonny Dee * regular expressions. Administrators (superusers) and optionally configured
8*e39ccd63SJonny Dee * exempt groups are allowed to delete pages regardless of these patterns.
9*e39ccd63SJonny Dee *
10*e39ccd63SJonny Dee * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
11*e39ccd63SJonny Dee * @author     ChatGPT Agent
12*e39ccd63SJonny Dee */
13*e39ccd63SJonny Dee
14*e39ccd63SJonny Deeuse dokuwiki\Extension\ActionPlugin;
15*e39ccd63SJonny Dee// Import the correct namespaced classes for event handling
16*e39ccd63SJonny Deeuse dokuwiki\Extension\Event;
17*e39ccd63SJonny Deeuse dokuwiki\Extension\EventHandler;
18*e39ccd63SJonny Dee
19*e39ccd63SJonny Dee// Protect against direct call
20*e39ccd63SJonny Deeif (!defined('DOKU_INC')) die();
21*e39ccd63SJonny Dee
22*e39ccd63SJonny Dee/**
23*e39ccd63SJonny Dee * Class action_plugin_deleteguard
24*e39ccd63SJonny Dee *
25*e39ccd63SJonny Dee * Registers a handler on COMMON_WIKIPAGE_SAVE to intercept page save
26*e39ccd63SJonny Dee * operations. When a deletion (empty save) is attempted on a protected page
27*e39ccd63SJonny Dee * by a non‑admin user, the save is prevented and an error message is shown.
28*e39ccd63SJonny Dee */
29*e39ccd63SJonny Deeclass action_plugin_deleteguard extends ActionPlugin {
30*e39ccd63SJonny Dee
31*e39ccd63SJonny Dee    /**
32*e39ccd63SJonny Dee     * Register the plugin events
33*e39ccd63SJonny Dee     *
34*e39ccd63SJonny Dee     * @param EventHandler $controller
35*e39ccd63SJonny Dee     * @return void
36*e39ccd63SJonny Dee     */
37*e39ccd63SJonny Dee    public function register(EventHandler $controller) {
38*e39ccd63SJonny Dee        // Run before the page is saved so we can abort the delete
39*e39ccd63SJonny Dee        $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handle_common_wikipage_save');
40*e39ccd63SJonny Dee    }
41*e39ccd63SJonny Dee
42*e39ccd63SJonny Dee    /**
43*e39ccd63SJonny Dee     * Handler for the COMMON_WIKIPAGE_SAVE event
44*e39ccd63SJonny Dee     *
45*e39ccd63SJonny Dee     * This method checks whether the save operation represents a deletion
46*e39ccd63SJonny Dee     * (i.e. the new content is empty) and whether the page matches one of
47*e39ccd63SJonny Dee     * the configured regular expressions. If so, and the current user is
48*e39ccd63SJonny Dee     * neither an administrator nor in one of the exempt groups, the
49*e39ccd63SJonny Dee     * deletion is prevented.
50*e39ccd63SJonny Dee     *
51*e39ccd63SJonny Dee     * @param Event      $event The event object
52*e39ccd63SJonny Dee     * @param mixed      $param Additional parameters (unused)
53*e39ccd63SJonny Dee     * @return void
54*e39ccd63SJonny Dee     */
55*e39ccd63SJonny Dee    public function handle_common_wikipage_save(Event $event, $param) {
56*e39ccd63SJonny Dee        global $USERINFO, $conf;
57*e39ccd63SJonny Dee
58*e39ccd63SJonny Dee        // Only take action when the event is preventable
59*e39ccd63SJonny Dee        if (!$event->canPreventDefault) {
60*e39ccd63SJonny Dee            return;
61*e39ccd63SJonny Dee        }
62*e39ccd63SJonny Dee
63*e39ccd63SJonny Dee        // Allow administrators to delete pages
64*e39ccd63SJonny Dee        if (function_exists('auth_isadmin') && auth_isadmin()) {
65*e39ccd63SJonny Dee            return;
66*e39ccd63SJonny Dee        }
67*e39ccd63SJonny Dee
68*e39ccd63SJonny Dee        // Check for exempt groups configuration
69*e39ccd63SJonny Dee        $exemptSetting = (string)$this->getConf('exempt_groups');
70*e39ccd63SJonny Dee        $exemptGroups  = array_filter(array_map('trim', explode(',', $exemptSetting)));
71*e39ccd63SJonny Dee
72*e39ccd63SJonny Dee        if (!empty($exemptGroups) && isset($USERINFO['grps']) && is_array($USERINFO['grps'])) {
73*e39ccd63SJonny Dee            foreach ($USERINFO['grps'] as $group) {
74*e39ccd63SJonny Dee                if (in_array($group, $exemptGroups, true)) {
75*e39ccd63SJonny Dee                    // User is in an exempt group, allow deletion
76*e39ccd63SJonny Dee                    return;
77*e39ccd63SJonny Dee                }
78*e39ccd63SJonny Dee            }
79*e39ccd63SJonny Dee        }
80*e39ccd63SJonny Dee
81*e39ccd63SJonny Dee        // Determine if the save represents a deletion
82*e39ccd63SJonny Dee        $newContent = isset($event->data['newContent']) ? $event->data['newContent'] : '';
83*e39ccd63SJonny Dee        $trimMode   = (bool)$this->getConf('trim_mode');
84*e39ccd63SJonny Dee        $isEmpty    = $trimMode ? trim($newContent) === '' : $newContent === '';
85*e39ccd63SJonny Dee
86*e39ccd63SJonny Dee        if (!$isEmpty) {
87*e39ccd63SJonny Dee            // Not empty – normal edit, allow saving
88*e39ccd63SJonny Dee            return;
89*e39ccd63SJonny Dee        }
90*e39ccd63SJonny Dee
91*e39ccd63SJonny Dee        // Determine the matching target: page ID or relative file path
92*e39ccd63SJonny Dee        $matchTarget = $this->getConf('match_target') === 'filepath' ?
93*e39ccd63SJonny Dee            $this->getRelativeFilePath($event->data['file'], $conf['datadir']) :
94*e39ccd63SJonny Dee            $event->data['id'];
95*e39ccd63SJonny Dee
96*e39ccd63SJonny Dee        // Retrieve regex patterns from configuration
97*e39ccd63SJonny Dee        $patternsSetting = (string)$this->getConf('patterns');
98*e39ccd63SJonny Dee        $patternLines    = preg_split('/\R+/', $patternsSetting, -1, PREG_SPLIT_NO_EMPTY);
99*e39ccd63SJonny Dee
100*e39ccd63SJonny Dee        foreach ($patternLines as $rawPattern) {
101*e39ccd63SJonny Dee            $pattern = trim($rawPattern);
102*e39ccd63SJonny Dee            if ($pattern === '') {
103*e39ccd63SJonny Dee                continue;
104*e39ccd63SJonny Dee            }
105*e39ccd63SJonny Dee            // Try to apply the regex; invalid patterns are ignored
106*e39ccd63SJonny Dee            if (@preg_match('/' . $pattern . '/u', '') === false) {
107*e39ccd63SJonny Dee                continue;
108*e39ccd63SJonny Dee            }
109*e39ccd63SJonny Dee            if (preg_match('/' . $pattern . '/u', $matchTarget)) {
110*e39ccd63SJonny Dee                // Match found – prevent deletion
111*e39ccd63SJonny Dee                $event->preventDefault();
112*e39ccd63SJonny Dee                $event->stopPropagation();
113*e39ccd63SJonny Dee                msg($this->getLang('deny_msg'), -1);
114*e39ccd63SJonny Dee                return;
115*e39ccd63SJonny Dee            }
116*e39ccd63SJonny Dee        }
117*e39ccd63SJonny Dee    }
118*e39ccd63SJonny Dee
119*e39ccd63SJonny Dee    /**
120*e39ccd63SJonny Dee     * Convert an absolute file path into a relative one below the data directory
121*e39ccd63SJonny Dee     *
122*e39ccd63SJonny Dee     * The COMMON_WIKIPAGE_SAVE event provides the absolute file path. When
123*e39ccd63SJonny Dee     * matching against the file path, we want a path relative to the base
124*e39ccd63SJonny Dee     * pages directory so users can write concise regular expressions.
125*e39ccd63SJonny Dee     *
126*e39ccd63SJonny Dee     * @param string $fullPath Absolute path to the file
127*e39ccd63SJonny Dee     * @param string $dataDir  Base data directory (usually $conf['datadir'])
128*e39ccd63SJonny Dee     * @return string Relative file path
129*e39ccd63SJonny Dee     */
130*e39ccd63SJonny Dee    protected function getRelativeFilePath($fullPath, $dataDir) {
131*e39ccd63SJonny Dee        $base = rtrim($dataDir, '/');
132*e39ccd63SJonny Dee        // DokuWiki stores pages in $datadir/pages
133*e39ccd63SJonny Dee        $pagesDir = $base . '/pages/';
134*e39ccd63SJonny Dee        if (strpos($fullPath, $pagesDir) === 0) {
135*e39ccd63SJonny Dee            return substr($fullPath, strlen($pagesDir));
136*e39ccd63SJonny Dee        }
137*e39ccd63SJonny Dee        // Fallback: attempt to strip base datadir
138*e39ccd63SJonny Dee        if (strpos($fullPath, $base . '/') === 0) {
139*e39ccd63SJonny Dee            return substr($fullPath, strlen($base) + 1);
140*e39ccd63SJonny Dee        }
141*e39ccd63SJonny Dee        return $fullPath;
142*e39ccd63SJonny Dee    }
143*e39ccd63SJonny Dee}