1<?php 2/** 3 * DokuWiki Plugin watchcycle (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Szymon Olewniczak <dokuwiki@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) { 11 die(); 12} 13 14class action_plugin_watchcycle extends DokuWiki_Action_Plugin 15{ 16 17 /** 18 * Registers a callback function for a given event 19 * 20 * @param Doku_Event_Handler $controller DokuWiki's event controller object 21 * 22 * @return void 23 */ 24 public function register(Doku_Event_Handler $controller) 25 { 26 27 $controller->register_hook('PARSER_METADATA_RENDER', 'AFTER', $this, 'handle_parser_metadata_render'); 28 $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'handle_parser_cache_use'); 29 // ensure a page revision is created when summary changes: 30 $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handle_pagesave_before'); 31 $controller->register_hook('SEARCH_RESULT_PAGELOOKUP', 'BEFORE', $this, 'addIconToPageLookupResult'); 32 $controller->register_hook('SEARCH_RESULT_FULLPAGE', 'BEFORE', $this, 'addIconToFullPageResult'); 33 $controller->register_hook('FORM_SEARCH_OUTPUT', 'BEFORE', $this, 'addFilterToSearchForm'); 34 $controller->register_hook('FORM_QUICKSEARCH_OUTPUT', 'BEFORE', $this, 'handle_form_quicksearch_output'); 35 $controller->register_hook('SEARCH_QUERY_FULLPAGE', 'AFTER', $this, 'filterSearchResults'); 36 $controller->register_hook('SEARCH_QUERY_PAGELOOKUP', 'AFTER', $this, 'filterSearchResults'); 37 38 $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'handle_toolbar_define'); 39 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_get'); 40 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_validate'); 41 } 42 43 44 /** 45 * Register a new toolbar button 46 * 47 * @param Doku_Event $event event object by reference 48 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 49 * handler was registered] 50 * 51 * @return void 52 */ 53 public function handle_toolbar_define(Doku_Event $event, $param) 54 { 55 $event->data[] = [ 56 'type' => 'plugin_watchcycle', 57 'title' => $this->getLang('title toolbar button'), 58 'icon' => '../../plugins/watchcycle/images/eye-plus16Green.png', 59 ]; 60 } 61 62 /** 63 * Add a checkbox to the search form to allow limiting the search to maintained pages only 64 * 65 * @param Doku_Event $event 66 * @param $param 67 */ 68 public function addFilterToSearchForm(Doku_Event $event, $param) 69 { 70 /* @var \dokuwiki\Form\Form $searchForm */ 71 $searchForm = $event->data; 72 $advOptionsPos = $searchForm->findPositionByAttribute('class', 'advancedOptions'); 73 $searchForm->addCheckbox('watchcycle_only', $this->getLang('cb only maintained pages'), $advOptionsPos + 1) 74 ->addClass('plugin__watchcycle_searchform_cb'); 75 } 76 77 /** 78 * Handles the FORM_QUICKSEARCH_OUTPUT event 79 * 80 * @param Doku_Event $event event object by reference 81 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 82 * handler was registered] 83 * 84 * @return void 85 */ 86 public function handle_form_quicksearch_output(Doku_Event $event, $param) 87 { 88 /** @var \dokuwiki\Form\Form $qsearchForm */ 89 $qsearchForm = $event->data; 90 if ($this->getConf('default_maintained_only')) { 91 $qsearchForm->setHiddenField('watchcycle_only', '1'); 92 } 93 } 94 95 /** 96 * Filter the search results to show only maintained pages, if watchcycle_only is true in $INPUT 97 * 98 * @param Doku_Event $event 99 * @param $param 100 */ 101 public function filterSearchResults(Doku_Event $event, $param) 102 { 103 global $INPUT; 104 if (!$INPUT->bool('watchcycle_only')) { 105 return; 106 } 107 $event->result = array_filter($event->result, function ($key) { 108 $watchcycle = p_get_metadata($key, 'plugin watchcycle'); 109 return !empty($watchcycle); 110 }, ARRAY_FILTER_USE_KEY); 111 } 112 113 /** 114 * [Custom event handler which performs action] 115 * 116 * @param Doku_Event $event event object by reference 117 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 118 * handler was registered] 119 * 120 * @return void 121 */ 122 public function handle_parser_metadata_render(Doku_Event $event, $param) 123 { 124 global $ID; 125 126 /** @var \helper_plugin_sqlite $sqlite */ 127 $sqlite = plugin_load('helper', 'watchcycle_db')->getDB(); 128 if (!$sqlite) { 129 msg($this->getLang('error sqlite missing'), -1); 130 return; 131 } 132 /* @var \helper_plugin_watchcycle $helper */ 133 $helper = plugin_load('helper', 'watchcycle'); 134 135 $page = $event->data['current']['last_change']['id']; 136 137 if (isset($event->data['current']['plugin']['watchcycle'])) { 138 $watchcycle = $event->data['current']['plugin']['watchcycle']; 139 $res = $sqlite->query('SELECT * FROM watchcycle WHERE page=?', $page); 140 $row = $sqlite->res2row($res); 141 $changes = $this->getLastMaintainerRev($event->data, $watchcycle['maintainer'], $last_maintainer_rev); 142 //false if page needs checking 143 $uptodate = $helper->daysAgo($last_maintainer_rev) <= (int)$watchcycle['cycle']; 144 145 if ($uptodate === false) { 146 $this->informMaintainer($watchcycle['maintainer'], $ID); 147 } 148 149 if (!$row) { 150 $entry = $watchcycle; 151 $entry['page'] = $page; 152 $entry['last_maintainer_rev'] = $last_maintainer_rev; 153 // uptodate is an int in the database 154 $entry['uptodate'] = (int)$uptodate; 155 $sqlite->storeEntry('watchcycle', $entry); 156 } else { //check if we need to update something 157 $toupdate = []; 158 159 if ($row['cycle'] != $watchcycle['cycle']) { 160 $toupdate['cycle'] = $watchcycle['cycle']; 161 } 162 163 if ($row['maintainer'] != $watchcycle['maintainer']) { 164 $toupdate['maintainer'] = $watchcycle['maintainer']; 165 } 166 167 if ($row['last_maintainer_rev'] != $last_maintainer_rev) { 168 $toupdate['last_maintainer_rev'] = $last_maintainer_rev; 169 } 170 171 //uptodate value has changed? compare with the string we got from the database 172 if ($row['uptodate'] !== (string)(int)$uptodate) { 173 $toupdate['uptodate'] = (int)$uptodate; 174 } 175 176 if (count($toupdate) > 0) { 177 $set = implode(',', array_map(function ($v) { 178 return "$v=?"; 179 }, array_keys($toupdate))); 180 $toupdate[] = $page; 181 $sqlite->query("UPDATE watchcycle SET $set WHERE page=?", $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 Doku_Event $event 195 * @param string $param 196 */ 197 public function handle_ajax_get(Doku_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 Doku_Event $event 224 * @param $param 225 */ 226 public function handle_ajax_validate(Doku_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(function ($name, $user) use ($term) { 264 return ['label' => $user['name'] . " ($name)", 'value' => $name]; 265 }, array_keys($foundUsers), $foundUsers); 266 } 267 268 $groups = []; 269 270 // check cache 271 $cachedGroups = new cache('retrievedGroups', '.txt'); 272 if($cachedGroups->useCache(['age' => 30])) { 273 $foundGroups = unserialize($cachedGroups->retrieveCache()); 274 } else { 275 $foundGroups = $auth->retrieveGroups(); 276 $cachedGroups->storeCache(serialize($foundGroups)); 277 } 278 279 if (!empty($foundGroups)) { 280 $groups = array_filter( 281 array_map(function ($grp) use ($term) { 282 // filter groups 283 if (strpos($grp, $term) !== false) { 284 return ['label' => '@' . $grp, 'value' => '@' . $grp]; 285 } 286 }, $foundGroups) 287 ); 288 } 289 290 return array_merge($users, $groups); 291 } 292 293 /** 294 * @param array $meta metadata of the page 295 * @param string $maintainer 296 * @param int $rev revision of the last page edition by maintainer or -1 if no edition was made 297 * 298 * @return int number of changes since last maintainer's revision or -1 if no changes was made 299 */ 300 protected function getLastMaintainerRev($meta, $maintainer, &$rev) 301 { 302 $changes = 0; 303 304 /* @var \helper_plugin_watchcycle $helper */ 305 $helper = plugin_load('helper', 'watchcycle'); 306 307 if ($helper->isMaintainer($meta['current']['last_change']['user'], $maintainer)) { 308 $rev = $meta['current']['last_change']['date']; 309 return $changes; 310 } else { 311 $page = $meta['current']['last_change']['id']; 312 $changelog = new PageChangelog($page); 313 $first = 0; 314 $num = 100; 315 while (count($revs = $changelog->getRevisions($first, $num)) > 0) { 316 foreach ($revs as $rev) { 317 $changes += 1; 318 $revInfo = $changelog->getRevisionInfo($rev); 319 if ($helper->isMaintainer($revInfo['user'], $maintainer)) { 320 $rev = $revInfo['date']; 321 return $changes; 322 } 323 } 324 $first += $num; 325 } 326 } 327 328 $rev = -1; 329 return -1; 330 } 331 332 /** 333 * Inform all maintainers that the page needs checking 334 * 335 * @param string $def defined maintainers 336 * @param string $page that needs checking 337 */ 338 protected function informMaintainer($def, $page) 339 { 340 /* @var DokuWiki_Auth_Plugin $auth */ 341 global $auth; 342 343 /* @var \helper_plugin_watchcycle $helper */ 344 $helper = plugin_load('helper', 'watchcycle'); 345 $mails = $helper->getMaintainerMails($def); 346 foreach ($mails as $mail) { 347 $this->sendMail($mail, $page); 348 } 349 } 350 351 /** 352 * clean the cache every 24 hours 353 * 354 * @param Doku_Event $event event object by reference 355 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 356 * handler was registered] 357 * 358 * @return void 359 */ 360 public function handle_parser_cache_use(Doku_Event $event, $param) 361 { 362 /* @var \helper_plugin_watchcycle $helper */ 363 $helper = plugin_load('helper', 'watchcycle'); 364 365 if ($helper->daysAgo($event->data->_time) >= 1) { 366 $event->result = false; 367 } 368 } 369 370 /** 371 * Check if the page has to be changed 372 * 373 * @param Doku_Event $event event object by reference 374 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 375 * handler was registered] 376 * 377 * @return void 378 */ 379 public function handle_pagesave_before(Doku_Event $event, $param) 380 { 381 if ($event->data['contentChanged']) { 382 return; 383 } // will be saved for page changes 384 global $ACT; 385 386 //save page if summary is provided 387 if (!empty($event->data['summary'])) { 388 $event->data['contentChanged'] = true; 389 } 390 } 391 392 /** 393 * called for event SEARCH_RESULT_PAGELOOKUP 394 * 395 * @param Doku_Event $event 396 * @param $param 397 */ 398 public function addIconToPageLookupResult(Doku_Event $event, $param) 399 { 400 /* @var \helper_plugin_watchcycle $helper */ 401 $helper = plugin_load('helper', 'watchcycle'); 402 403 $icon = $helper->getSearchResultIconHTML($event->data['page']); 404 if ($icon) { 405 $event->data['listItemContent'][] = $icon; 406 } 407 } 408 409 /** 410 * called for event SEARCH_RESULT_FULLPAGE 411 * 412 * @param Doku_Event $event 413 * @param $param 414 */ 415 public function addIconToFullPageResult(Doku_Event $event, $param) 416 { 417 /* @var \helper_plugin_watchcycle $helper */ 418 $helper = plugin_load('helper', 'watchcycle'); 419 420 $icon = $helper->getSearchResultIconHTML($event->data['page']); 421 if ($icon) { 422 $event->data['resultHeader'][] = $icon; 423 } 424 } 425 426 /** 427 * Sends an email 428 * 429 * @param array $mail 430 * @param string $page 431 */ 432 protected function sendMail($mail, $page) 433 { 434 $mailer = new Mailer(); 435 $mailer->to($mail); 436 $mailer->subject($this->getLang('mail subject')); 437 $text = sprintf($this->getLang('mail body'), $page); 438 $link = '<a href="' . wl($page, '', true) . '">' . $page . '</a>'; 439 $html = sprintf($this->getLang('mail body'), $link); 440 $mailer->setBody($text, null, null, $html); 441 442 if (!$mailer->send()) { 443 msg($this->getLang('error mail'), -1); 444 } 445 } 446} 447 448// vim:ts=4:sw=4:et: 449