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