1<?php 2 3use dokuwiki\Cache\Cache; 4use dokuwiki\Extension\ActionPlugin; 5use dokuwiki\Extension\EventHandler; 6use dokuwiki\Extension\Event; 7use dokuwiki\Form\Form; 8use dokuwiki\ChangeLog\PageChangeLog; 9use dokuwiki\plugin\sqlite\SQLiteDB; 10 11/** 12 * DokuWiki Plugin watchcycle (Action Component) 13 * 14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 15 * @author Szymon Olewniczak <dokuwiki@cosmocode.de> 16 */ 17 18class action_plugin_watchcycle extends ActionPlugin 19{ 20 /** 21 * Registers a callback function for a given event 22 * 23 * @param EventHandler $controller DokuWiki's event controller object 24 * 25 * @return void 26 */ 27 public function register(EventHandler $controller) 28 { 29 30 $controller->register_hook('PARSER_METADATA_RENDER', 'AFTER', $this, 'handleParserMetadataRender'); 31 $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'handleParserCacheUse'); 32 // ensure a page revision is created when summary changes: 33 $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handlePagesaveBefore'); 34 $controller->register_hook('SEARCH_RESULT_PAGELOOKUP', 'BEFORE', $this, 'addIconToPageLookupResult'); 35 $controller->register_hook('SEARCH_RESULT_FULLPAGE', 'BEFORE', $this, 'addIconToFullPageResult'); 36 $controller->register_hook('FORM_SEARCH_OUTPUT', 'BEFORE', $this, 'addFilterToSearchForm'); 37 $controller->register_hook('FORM_QUICKSEARCH_OUTPUT', 'BEFORE', $this, 'handleFormQuicksearchOutput'); 38 $controller->register_hook('SEARCH_QUERY_FULLPAGE', 'AFTER', $this, 'filterSearchResults'); 39 $controller->register_hook('SEARCH_QUERY_PAGELOOKUP', 'AFTER', $this, 'filterSearchResults'); 40 41 $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'handleToolbarDefine'); 42 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxGet'); 43 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxValidate'); 44 } 45 46 47 /** 48 * Register a new toolbar button 49 * 50 * @param Event $event event object by reference 51 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 52 * handler was registered] 53 * 54 * @return void 55 */ 56 public function handleToolbarDefine(Event $event, $param) 57 { 58 $event->data[] = [ 59 'type' => 'plugin_watchcycle', 60 'title' => $this->getLang('title toolbar button'), 61 'icon' => '../../plugins/watchcycle/images/eye-plus16Green.png', 62 ]; 63 } 64 65 /** 66 * Add a checkbox to the search form to allow limiting the search to maintained pages only 67 * 68 * @param Event $event 69 * @param $param 70 */ 71 public function addFilterToSearchForm(Event $event, $param) 72 { 73 /* @var \dokuwiki\Form\Form $searchForm */ 74 $searchForm = $event->data; 75 $advOptionsPos = $searchForm->findPositionByAttribute('class', 'advancedOptions'); 76 $searchForm->addCheckbox('watchcycle_only', $this->getLang('cb only maintained pages'), $advOptionsPos + 1) 77 ->addClass('plugin__watchcycle_searchform_cb'); 78 } 79 80 /** 81 * Handles the FORM_QUICKSEARCH_OUTPUT event 82 * 83 * @param Event $event event object by reference 84 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 85 * handler was registered] 86 * 87 * @return void 88 */ 89 public function handleFormQuicksearchOutput(Event $event, $param) 90 { 91 /** @var Form $qsearchForm */ 92 $qsearchForm = $event->data; 93 if ($this->getConf('default_maintained_only')) { 94 $qsearchForm->setHiddenField('watchcycle_only', '1'); 95 } 96 } 97 98 /** 99 * Filter the search results to show only maintained pages, if watchcycle_only is true in $INPUT 100 * 101 * @param Event $event 102 * @param $param 103 */ 104 public function filterSearchResults(Event $event, $param) 105 { 106 global $INPUT; 107 if (!$INPUT->bool('watchcycle_only')) { 108 return; 109 } 110 $event->result = array_filter($event->result, function ($key) { 111 $watchcycle = p_get_metadata($key, 'plugin watchcycle'); 112 return !empty($watchcycle); 113 }, ARRAY_FILTER_USE_KEY); 114 } 115 116 /** 117 * [Custom event handler which performs action] 118 * 119 * @param Event $event event object by reference 120 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 121 * handler was registered] 122 * 123 * @return void 124 */ 125 public function handleParserMetadataRender(Event $event, $param) 126 { 127 global $ID; 128 129 /** @var \helper_plugin_watchcycle_db $dbHelper */ 130 $dbHelper = plugin_load('helper', 'watchcycle_db'); 131 132 /** @var SQLiteDB */ 133 $sqlite = $dbHelper->getDB(); 134 135 /* @var \helper_plugin_watchcycle $helper */ 136 $helper = plugin_load('helper', 'watchcycle'); 137 138 $page = $event->data['current']['last_change']['id']; 139 140 if (isset($event->data['current']['plugin']['watchcycle'])) { 141 $watchcycle = $event->data['current']['plugin']['watchcycle']; 142 $row = $sqlite->queryRecord('SELECT * FROM watchcycle WHERE page=?', $page); 143 $changes = $this->getLastMaintainerRev($event->data, $watchcycle['maintainer'], $last_maintainer_rev); 144 //false if page needs checking 145 $uptodate = $helper->daysAgo($last_maintainer_rev) <= (int)$watchcycle['cycle']; 146 147 if ($uptodate === false) { 148 $helper->informMaintainer($watchcycle['maintainer'], $ID); 149 } 150 151 if (!$row) { 152 $entry = $watchcycle; 153 $entry['page'] = $page; 154 $entry['last_maintainer_rev'] = $last_maintainer_rev; 155 // uptodate is an int in the database 156 $entry['uptodate'] = (int)$uptodate; 157 $sqlite->saveRecord('watchcycle', $entry); 158 } else { //check if we need to update something 159 $toupdate = []; 160 161 if ($row['cycle'] != $watchcycle['cycle']) { 162 $toupdate['cycle'] = $watchcycle['cycle']; 163 } 164 165 if ($row['maintainer'] != $watchcycle['maintainer']) { 166 $toupdate['maintainer'] = $watchcycle['maintainer']; 167 } 168 169 if ($row['last_maintainer_rev'] != $last_maintainer_rev) { 170 $toupdate['last_maintainer_rev'] = $last_maintainer_rev; 171 } 172 173 //uptodate value has changed? 174 if ($row['uptodate'] !== (int)$uptodate) { 175 $toupdate['uptodate'] = (int)$uptodate; 176 } 177 178 if ($toupdate !== []) { 179 $set = implode(',', array_map(static fn($v) => "$v=?", array_keys($toupdate))); 180 $toupdate[] = $page; 181 $sqlite->query("UPDATE watchcycle SET $set WHERE page=?", array_values($toupdate)); 182 } 183 } 184 $event->data['current']['plugin']['watchcycle']['last_maintainer_rev'] = $last_maintainer_rev; 185 $event->data['current']['plugin']['watchcycle']['changes'] = $changes; 186 } else { //maybe we've removed the syntax -> delete from the database 187 $sqlite->query('DELETE FROM watchcycle WHERE page=?', $page); 188 } 189 } 190 191 /** 192 * Returns JSON with filtered users and groups 193 * 194 * @param Event $event 195 * @param string $param 196 */ 197 public function handleAjaxGet(Event $event, $param) 198 { 199 if ($event->data != 'plugin_watchcycle_get') return; 200 $event->preventDefault(); 201 $event->stopPropagation(); 202 global $conf; 203 204 header('Content-Type: application/json'); 205 try { 206 $result = $this->fetchUsersAndGroups(); 207 } catch (\Exception $e) { 208 $result = [ 209 'error' => $e->getMessage() . ' ' . basename($e->getFile()) . ':' . $e->getLine() 210 ]; 211 if ($conf['allowdebug']) { 212 $result['stacktrace'] = $e->getTraceAsString(); 213 } 214 http_status(500); 215 } 216 217 echo json_encode($result); 218 } 219 220 /** 221 * JSON result of validation of maintainers definition 222 * 223 * @param Event $event 224 * @param $param 225 */ 226 public function handleAjaxValidate(Event $event, $param) 227 { 228 if ($event->data != 'plugin_watchcycle_validate') return; 229 $event->preventDefault(); 230 $event->stopPropagation(); 231 232 global $INPUT; 233 $maintainers = $INPUT->str('param'); 234 235 if (empty($maintainers)) return; 236 237 header('Content-Type: application/json'); 238 239 /* @var \helper_plugin_watchcycle $helper */ 240 $helper = plugin_load('helper', 'watchcycle'); 241 242 echo json_encode($helper->validateMaintainerString($maintainers)); 243 } 244 245 /** 246 * Returns filtered users and groups, if supported by the current authentication 247 * 248 * @return array 249 */ 250 protected function fetchUsersAndGroups() 251 { 252 global $INPUT; 253 $term = $INPUT->str('param'); 254 255 if (empty($term)) return []; 256 257 /* @var DokuWiki_Auth_Plugin $auth */ 258 global $auth; 259 260 $users = []; 261 $foundUsers = $auth->retrieveUsers(0, 50, ['user' => $term]); 262 if (!empty($foundUsers)) { 263 $users = array_map( 264 static fn($name, $user) => ['label' => $user['name'] . " ($name)", 'value' => $name], 265 array_keys($foundUsers), 266 $foundUsers 267 ); 268 } 269 270 $groups = []; 271 272 // check cache 273 $cachedGroups = new Cache('retrievedGroups', '.txt'); 274 if ($cachedGroups->useCache(['age' => 30])) { 275 $foundGroups = unserialize($cachedGroups->retrieveCache()); 276 } else { 277 $foundGroups = $auth->retrieveGroups(); 278 $cachedGroups->storeCache(serialize($foundGroups)); 279 } 280 281 if (!empty($foundGroups)) { 282 $groups = array_filter( 283 array_map(function ($grp) use ($term) { 284 // filter groups 285 if (strpos($grp, (string) $term) !== false) { 286 return ['label' => '@' . $grp, 'value' => '@' . $grp]; 287 } 288 }, $foundGroups) 289 ); 290 } 291 292 return array_merge($users, $groups); 293 } 294 295 /** 296 * @param array $meta metadata of the page 297 * @param string $maintainer 298 * @param int $rev revision of the last page edition by maintainer or -1 if no edition was made 299 * 300 * @return int number of changes since last maintainer's revision or -1 if no changes was made 301 */ 302 protected function getLastMaintainerRev($meta, $maintainer, &$rev) 303 { 304 $changes = 0; 305 306 /* @var \helper_plugin_watchcycle $helper */ 307 $helper = plugin_load('helper', 'watchcycle'); 308 309 if ($helper->isMaintainer($meta['current']['last_change']['user'], $maintainer)) { 310 $rev = $meta['current']['last_change']['date']; 311 return $changes; 312 } else { 313 $page = $meta['current']['last_change']['id']; 314 $changelog = new PageChangeLog($page); 315 $first = 0; 316 $num = 100; 317 while (count($revs = $changelog->getRevisions($first, $num)) > 0) { 318 foreach ($revs as $rev) { 319 ++$changes; 320 $revInfo = $changelog->getRevisionInfo($rev); 321 if ($helper->isMaintainer($revInfo['user'], $maintainer)) { 322 $rev = $revInfo['date']; 323 return $changes; 324 } 325 } 326 $first += $num; 327 } 328 } 329 330 $rev = -1; 331 return -1; 332 } 333 334 /** 335 * clean the cache every 24 hours 336 * 337 * @param Event $event event object by reference 338 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 339 * handler was registered] 340 * 341 * @return void 342 */ 343 public function handleParserCacheUse(Event $event, $param) 344 { 345 /* @var \helper_plugin_watchcycle $helper */ 346 $helper = plugin_load('helper', 'watchcycle'); 347 348 if ($helper->daysAgo($event->data->_time) >= 1) { 349 $event->result = false; 350 } 351 } 352 353 /** 354 * Check if the page has to be changed 355 * 356 * @param Event $event event object by reference 357 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 358 * handler was registered] 359 * 360 * @return void 361 */ 362 public function handlePagesaveBefore(Event $event, $param) 363 { 364 if ($event->data['contentChanged']) { 365 return; 366 } // will be saved for page changes 367 368 //save page if summary is provided 369 if (!empty($event->data['summary'])) { 370 $event->data['contentChanged'] = true; 371 } 372 } 373 374 /** 375 * called for event SEARCH_RESULT_PAGELOOKUP 376 * 377 * @param Event $event 378 * @param $param 379 */ 380 public function addIconToPageLookupResult(Event $event, $param) 381 { 382 /* @var \helper_plugin_watchcycle $helper */ 383 $helper = plugin_load('helper', 'watchcycle'); 384 385 $icon = $helper->getSearchResultIconHTML($event->data['page']); 386 if ($icon) { 387 $event->data['listItemContent'][] = $icon; 388 } 389 } 390 391 /** 392 * called for event SEARCH_RESULT_FULLPAGE 393 * 394 * @param Event $event 395 * @param $param 396 */ 397 public function addIconToFullPageResult(Event $event, $param) 398 { 399 /* @var \helper_plugin_watchcycle $helper */ 400 $helper = plugin_load('helper', 'watchcycle'); 401 402 $icon = $helper->getSearchResultIconHTML($event->data['page']); 403 if ($icon) { 404 $event->data['resultHeader'][] = $icon; 405 } 406 } 407} 408