1<?php 2/** 3 * DokuWiki Plugin cleandeletedusernames (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Michael Große <dokuwiki@cosmocode.de> 7 */ 8 9class action_plugin_gdpr_delusers extends DokuWiki_Action_Plugin 10{ 11 12 protected $didMeaningfulWork = false; 13 14 /** 15 * Registers a callback function for a given event 16 * 17 * @param Doku_Event_Handler $controller DokuWiki's event controller object 18 * 19 * @return void 20 */ 21 public function register(Doku_Event_Handler $controller) 22 { 23 $controller->register_hook('AUTH_USER_CHANGE', 'AFTER', $this, 'handleAuthUserChange'); 24 $controller->register_hook('INDEXER_TASKS_RUN', 'BEFORE', $this, 'handleIndexerTaskRun'); 25 } 26 27 /** 28 * [Custom event handler which performs action] 29 * 30 * Called for event: AUTH_USER_CHANGE 31 * 32 * @param Doku_Event $event event object by reference 33 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 34 * handler was registered] 35 * 36 * @return void 37 */ 38 public function handleAuthUserChange(Doku_Event $event, $param) 39 { 40 if ($event->data['type'] !== 'delete') { 41 return; 42 } 43 44 $username = $event->data['params'][0][0]; 45 $cacheFN = $this->getCacheFN('users'); 46 file_put_contents($cacheFN, $username . "\n", FILE_APPEND); 47 } 48 49 /** 50 * [Custom event handler which performs action] 51 * 52 * Called for event: INDEXER_TASKS_RUN 53 * 54 * @param Doku_Event $event event object by reference 55 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 56 * handler was registered] 57 * 58 * @return void 59 */ 60 public function handleIndexerTaskRun(Doku_Event $event, $param) 61 { 62 $deleteUserCacheFile = $this->getCacheFN('users'); 63 if (!file_exists($deleteUserCacheFile) || filesize($deleteUserCacheFile) === 0) { 64 return; 65 } 66 67 $usersToDelete = file($deleteUserCacheFile); 68 $this->cleanUsernameFromChangelogs(trim($usersToDelete[0])); 69 70 if ($this->didMeaningfulWork) { 71 $event->preventDefault(); 72 $event->stopPropagation(); 73 } 74 } 75 76 /** 77 * Clean the username form all changelogs and replace it with a unique placeholder 78 * 79 * @param $username 80 * 81 * @return void 82 */ 83 protected function cleanUsernameFromChangelogs($username) 84 { 85 $deletedUserCount = $this->getCleanedUserCount(); 86 87 while ($changelog = $this->getTopChangelog()) { 88 if (!$this->cleanChangelog($changelog, $username, $deletedUserCount)) { 89 return; 90 } 91 $this->removeTopChangelogFromList(); 92 $this->didMeaningfulWork = true; 93 } 94 $this->incrementCleanedUserCounter(); 95 $this->removeTopUsernameFromList(); 96 } 97 98 /** 99 * Remove the first username from the list of deleted usernames that have still to be cleaned 100 * 101 * @return void 102 */ 103 protected function removeTopUsernameFromList() 104 { 105 $deleteUserCacheFile = $this->getCacheFN('users'); 106 $usersToDelete = file($deleteUserCacheFile); 107 array_shift($usersToDelete); 108 file_put_contents($deleteUserCacheFile, $usersToDelete); 109 } 110 111 /** 112 * Increment the counter of users that have already been cleaned 113 */ 114 protected function incrementCleanedUserCounter() 115 { 116 $cacheFN = $this->getCacheFN('counter'); 117 $count = (int)file_get_contents($cacheFN); 118 file_put_contents($cacheFN, $count + 1); 119 } 120 121 /** 122 * Remove the first changelog from the list of changelogs that have still to be cleaned for the current deleted user 123 */ 124 protected function removeTopChangelogFromList() 125 { 126 $changelogCacheFN = $this->getCacheFN('changelogs'); 127 $lines = file($changelogCacheFN); 128 array_shift($lines); 129 file_put_contents($changelogCacheFN, implode('', $lines)); 130 } 131 132 /** 133 * Get the number of users that have already been cleaned 134 * 135 * @return bool|string 136 */ 137 protected function getCleanedUserCount() 138 { 139 $cacheFN = $this->getCacheFN('counter'); 140 if (!file_exists($cacheFN)) { 141 file_put_contents($cacheFN, '0'); 142 return '0'; 143 } 144 return file_get_contents($cacheFN); 145 } 146 147 148 /** 149 * Try to clean a username from a changelog and replace it with a placeholder, locks the page 150 * 151 * @param string $changelogFN path to the changelog to be cleaned 152 * @param string $nameToBeCleaned username to be cleaned 153 * @param string $count number of users that have already been cleaned, will be appended to the placeholder 154 * 155 * @return bool if the changelog has been successfully cleaned, false if page was locked and nothing was done 156 */ 157 protected function cleanChangelog($changelogFN, $nameToBeCleaned, $count) 158 { 159 $pageid = $this->getPageIDfromChangelogFN($changelogFN); 160 if ($pageid && checklock($pageid) !== false) { 161 return false; 162 } 163 164 $pageid && lock($pageid); 165 $cleanChangelogLines = []; 166 167 $handle = fopen($changelogFN, 'rb+'); 168 flock($handle, LOCK_EX); 169 while ($line = fgets($handle)) { 170 $parts = explode("\t", $line); 171 if ($parts[4] !== $nameToBeCleaned) { 172 $cleanChangelogLines[] = $line; 173 continue; 174 } 175 $parts[4] = '_deletedUser' . $count . '_'; 176 $cleanChangelogLines[] = implode("\t", $parts); 177 } 178 ftruncate($handle, 0); 179 fseek($handle, 0); 180 fwrite($handle, implode('', $cleanChangelogLines)); 181 flock($handle, LOCK_UN); 182 fclose($handle); 183 $pageid && unlock($pageid); 184 return true; 185 } 186 187 /** 188 * Parse the pageid from the changelog filename 189 * 190 * @param string $changelogFN 191 * 192 * @return false|string pageid or false if media changelog 193 */ 194 protected function getPageIDfromChangelogFN($changelogFN) 195 { 196 global $conf; 197 if (strpos($changelogFN, $conf['mediametadir']) === 0) { 198 return false; 199 } 200 $pageid = substr($changelogFN, strlen($conf['metadir']), -1 * strlen('.changes')); 201 return str_replace('/', ':', $pageid); 202 } 203 204 /** 205 * Get the next changelog to clean 206 * 207 * @return bool|string the next changelog or false if we are done 208 */ 209 protected function getTopChangelog() 210 { 211 $changelogCacheFN = $this->getCacheFN('changelogs'); 212 if (!file_exists($changelogCacheFN)) { 213 global $conf; 214 /** @var helper_plugin_gdpr_utils $gdprUtils */ 215 $gdprUtils = plugin_load('helper', 'gdpr_utils'); 216 217 $metaDir = $conf['metadir']; 218 $mediaMetaDir = $conf['mediametadir']; 219 220 $changelogs = $gdprUtils->collectChangelogs(dir($metaDir)); 221 $changelogs = array_merge($changelogs, $gdprUtils->collectChangelogs(dir($mediaMetaDir))); 222 223 file_put_contents($changelogCacheFN, implode("\n", $changelogs)); 224 $this->didMeaningfulWork = true; 225 return $changelogs[0]; 226 } 227 228 if (filesize($changelogCacheFN) > 0) { 229 $handle = fopen($changelogCacheFN, 'rb'); 230 $firstLine = fgets($handle); 231 fclose($handle); 232 return trim($firstLine); 233 } 234 235 unlink($changelogCacheFN); 236 return false; 237 } 238 239 /** 240 * Get the cache filname for a given key 241 * 242 * @param string $key 243 * 244 * @return string 245 */ 246 protected function getCacheFN($key) 247 { 248 switch ($key) { 249 case 'users': 250 return getCacheName('_cleandeletedusernames_users', ".$key.gdpr"); // 24603dd46ffc3f959fda54e307304714 251 case 'changelogs': 252 return getCacheName('_cleandeletedusernames_changelogs', ".$key.gdpr"); // c665cd8d3071e0e7c57ae12d97a869cd 253 case 'counter': 254 return getCacheName('_cleandeletedusernames_counter', ".$key.gdpr"); // 141cd02168fca864e927e12639b0272e 255 default: 256 throw new InvalidArgumentException('Unknown cache key provided: ' . $key); 257 } 258 } 259} 260