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