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}