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}