1<?php
2/**
3 * DokuWiki Plugin do (Helper Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr <gohr@cosmocode.de>
7 * @author  Adrian Lang <lang@cosmocode.de>
8 * @author  Dominik Eckelmann <eckelmann@cosmocode.de>
9 */
10
11// must be run within Dokuwiki
12if (!defined('DOKU_INC')) {
13    die();
14}
15
16class helper_plugin_do extends DokuWiki_Plugin
17{
18
19    /** @var helper_plugin_sqlite */
20    private $db = null;
21
22    /**
23     * Constructor. Initializes the SQLite DB Connection
24     */
25    public function __construct()
26    {
27        $this->db = plugin_load('helper', 'sqlite');
28        if (!$this->db) {
29            msg('The do plugin requires the sqlite plugin. Please install it');
30            return;
31        }
32        if (!$this->db->init('do', dirname(__FILE__) . '/db/')) {
33            $this->db = null;
34        }
35    }
36
37    /**
38     * Delete the all tasks from a given page id
39     *
40     * @param string $id page id
41     */
42    public function cleanPageTasks($id)
43    {
44        if (!$this->db) {
45            return;
46        }
47        $this->db->query('DELETE FROM tasks WHERE page = ?', $id);
48        $this->db->query('DELETE FROM task_assignees WHERE page = ?', $id);
49    }
50
51    /**
52     * Save a task.
53     *
54     * @param array $data task informations as key value array.
55     *                    keys are: page, md5, date, user, text, creator
56     */
57    public function saveTask($data)
58    {
59        if (!$this->db) {
60            return;
61        }
62
63        $date = !empty($data['date']) ? $data['date'] : null;
64
65        $this->db->query(
66            'INSERT INTO tasks (page,md5,date,text,creator,pos)
67             VALUES (?, ?, ?, ?, ?, ?)',
68            $data['page'],
69            $data['md5'],
70            $date,
71            $data['text'],
72            $data['creator'],
73            $data['pos']
74        );
75        foreach ((array)$data['users'] as $userName) {
76            $this->db->query(
77                'INSERT INTO task_assignees (page,md5,user)
78                 VALUES (?,?,?)',
79                $data['page'],
80                $data['md5'],
81                $userName
82            );
83        }
84    }
85
86    /**
87     * Load all tasks with given filters.
88     *
89     * Filters are:
90     *  - ns        for namespace filters
91     *  - id
92     *  - status    can be done or undone to filter for (un)completed tasks
93     *  - limit     limit the results to a given number of results
94     *  - user      all tasks to a given user
95     *  - md5       a single task
96     *
97     * @param array $args        filters to apply
98     * @param bool  $checkAccess yes: check if item is hidden or blocked by ACL, false: skip this check
99     *
100     * @return array filtered result.
101     */
102    public function loadTasks($args = null, $checkAccess = true)
103    {
104        if (!$this->db) {
105            return array();
106        }
107        $where = ' WHERE 1=1';
108        $limit = '';
109        if (isset($args)) {
110            if (isset($args['ns'])) {
111                // Whatever you do here, test it against the following mappings
112                // (current ID has to be NS1:NS2:PAGE):
113                // '..:..' => ''
114                // '.'     => 'NS1:NS2:'
115                // 'NS3'  => 'NS1:NS2:NS3'
116                // ':NS4'  => 'NS4'
117                // ':'     => ''
118
119                global $ID;
120                $ns = trim(resolve_id(getNS($ID), $args['ns'], false), ':');
121                if (strlen($ns) > 0) {
122                    // Do not match NSbla with NS, but only NS:bla
123                    $ns .= ':';
124                }
125
126                $where .= sprintf(' AND A.page LIKE %s', $this->db->quote_string($ns . '%'));
127            }
128
129            if (isset($args['id'])) {
130                global $ID;
131                if (!is_array($args['id'])) {
132                    $args['id'] = array($args['id']);
133                }
134                $exists = false;
135                resolve_pageid(getNS($ID), $args['id'][0], $exists);
136                $where .= sprintf(' AND A.page = %s', $this->db->quote_string($args['id'][0]));
137            }
138
139            if (isset($args['status'])) {
140                $status = utf8_strtolower($args['status'][0]);
141                if ($status == 'done') {
142                    $where .= ' AND B.status IS NOT null';
143                } elseif ($status == 'undone') {
144                    $where .= ' AND B.status IS null';
145                }
146            }
147
148            if (isset($args['from']) && isset($args['to'])) {
149                // regex to match YYYY-MM-DD
150                $dateRegex = '/^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])$/';
151                if (!preg_match($dateRegex, $args['from'][0]) || !preg_match($dateRegex, $args['to'][0])) {
152                    return array();
153                }
154                $where .= sprintf(" AND A.date >= '%s' AND  A.date <= '%s' ", $args['from'][0], $args['to'][0]);
155            }
156
157            if (isset($args['limit'])) {
158                $limit = ' LIMIT ' . intval($args['limit'][0]);
159            }
160
161            if (isset($args['md5'])) {
162                if (!is_array($args['md5'])) {
163                    $args['md5'] = array($args['md5']);
164                }
165                $where .= ' AND A.md5 = ' . $this->db->quote_string($args['md5'][0]);
166            }
167
168            $argn = array('user', 'creator');
169            foreach ($argn as $n) {
170                if (isset($args[$n])) {
171                    if (!is_array($args[$n])) {
172                        $args[$n] = array($args[$n]);
173                    }
174
175                    // replace current user's placeholder
176                    $args[$n] = array_map(
177                        function ($user) {
178                            return (strtolower($user) === '@user@' && $_SERVER['REMOTE_USER']) ?
179                                $_SERVER['REMOTE_USER'] :
180                                $user;
181                        },
182                        $args[$n]
183                    );
184
185                    $search = $n;
186
187                    /** @var DokuWiki_Auth_Plugin $auth */
188                    global $auth;
189                    if ($auth && !$auth->isCaseSensitive()) {
190                        $search = "lower($search)";
191                        $args[$n] = array_map('utf8_strtolower', $args[$n]);
192                    }
193                    $args[$n] = $this->db->quote_and_join($args[$n]);
194
195                    $where .= sprintf(' AND %s in (%s)', $search, $args[$n]);
196                }
197            }
198
199        }
200        if ($checkAccess) {
201            $where .= ' AND GETACCESSLEVEL(A.page) >= ' . AUTH_READ;
202        }
203        $query = 'SELECT A.page     AS page,
204                        A.md5      AS md5,
205                        A.date     AS date,
206                        A.text     AS text,
207                        A.creator  AS creator,
208                        B.msg      AS msg,
209                        B.status   AS status,
210                        B.closedby AS closedby,
211                        C.user     AS user
212                   FROM tasks A LEFT JOIN task_status B
213                     ON A.page = B.page
214                     AND A.md5 = B.md5
215                   LEFT JOIN task_assignees C
216                     ON A.page = C.page
217                     AND A.md5 = C.md5
218                     ' . $where . '
219                   ORDER BY A.page, A.pos' . $limit;
220        $res = $this->db->query($query);
221        $res = $this->db->res2arr($res);
222
223        // merge assignees into users array
224        $result = array();
225        foreach ($res as $row) {
226            $key = $row['page'] . $row['md5'];
227            if (!isset($result[$key])) {
228                $result[$key] = $row;
229                unset($result[$key]['user']);
230                $result[$key]['users'] = array();
231            }
232
233            if ($row['user'] !== null) {
234                $result[$key]['users'][] = $row['user'];
235            }
236        }
237
238        return array_values($result);
239    }
240
241    /**
242     * Toggles a tasks status.
243     *
244     * @param string $page      page id of the task
245     * @param string $md5       tasks md5 hash
246     * @param string $commitmsg a optional message to the task completion
247     *
248     * @return bool|string|int
249     *          false on undone a task
250     *          or timestamp on task completion
251     *          or -2 if not allowed
252     */
253    public function toggleTaskStatus($page, $md5, $commitmsg = '')
254    {
255        global $ID;
256
257        if (!$this->db) {
258            return -2;
259        } //not allowed
260        $md5 = trim($md5);
261        if (!$page || !$md5) {
262            return -2;
263        } //not allowed
264
265        $commitmsg = strip_tags($commitmsg);
266
267        $res = $this->db->query(
268            'SELECT A.page AS page,
269                    B.status AS status
270               FROM tasks A LEFT JOIN task_status B
271                 ON A.page = B.page
272                 AND A.md5 = B.md5
273              WHERE A.page = ?
274                AND A.md5  = ?',
275            $page, $md5
276        );
277        $stat = $this->db->res2row($res);
278        if ($stat == false) {
279            return -2; //not allowed, task don't exist
280        }
281        $stat = $stat['status'];
282
283        // load task details and determine notify receivers
284        $task = $this->loadTasks(array('id' => $ID, 'md5' => $md5))[0];
285        $recs = (array)$task['users'];
286        $recs[] = $task['creator'];
287        $recs = array_unique($recs);
288        $recs = array_diff($recs, array($_SERVER['REMOTE_USER']));
289
290        $name = $_SERVER['REMOTE_USER'];
291        if (!$stat) {
292            // close the task
293            $stat = date('Y-m-d', time());
294            $this->db->query(
295                'INSERT INTO task_status
296                     (page, md5, status, closedby, msg)
297                 VALUES
298                     (?, ?, ?, ?, ?)',
299                $page, $md5, $stat, $name, $commitmsg
300            );
301
302            $this->sendMail($recs, 'close', $task, $name, $commitmsg);
303            return $stat;
304        } else {
305            // reopen the task
306            $this->db->query(
307                'DELETE FROM task_status
308                 WHERE page = ?
309                   AND md5  = ?',
310                $page, $md5
311            );
312            $this->sendMail($recs, 'reopen', $task, $name);
313            return false;
314        }
315    }
316
317    /**
318     * Notify assignees or creators of new tasks and status changes
319     *
320     * @param array  $receivers list of user names to notify
321     * @param string $type      type of notification (open|reopen|close)
322     * @param array  $task
323     * @param string $user      user who triggered the notification
324     * @param string $msg       the closing message if any
325     */
326    public function sendMail($receivers, $type, $task, $user = '', $msg = '')
327    {
328        global $conf;
329        /** @var DokuWiki_Auth_Plugin $auth */
330        global $auth;
331
332        if (!$auth) {
333            return;
334        }
335        if (!$this->getConf('notify_assignee')) {
336            return;
337        }
338        $receivers = (array)$receivers;
339        if (!count($receivers)) {
340            return;
341        }
342
343        // prepare subject
344        $subj = '[' . $conf['title'] . '] ';
345        $subj .= sprintf($this->getLang('mail_' . $type), $task['text']);
346
347        // prepare text
348        $text = file_get_contents($this->localFN('mail_' . $type));
349        $text = str_replace(
350            array(
351                '@USER@',
352                '@DATE@',
353                '@TASK@',
354                '@TASKURL@',
355                '@MSG@',
356                '@DOKUWIKIURL@'
357            ),
358            array(
359                isset($user) ? $user : $this->getLang('someone'),
360                isset($task['date']) ? $task['date'] : $this->getLang('nodue'),
361                $task['text'],
362                wl($task['page'], '', true, '&') . '#plgdo__' . $task['md5'],
363                $msg,
364                DOKU_URL
365            ),
366            $text
367        );
368
369        // send mails
370        foreach ($receivers as $receiver) {
371            $info = $auth->getUserData($receiver);
372            if (!$info['mail']) {
373                continue;
374            }
375            $to = $info['name'] . ' <' . $info['mail'] . '>';
376			$mail = new Mailer();
377			$mail->to($to);
378			$mail->from($conf['mailfrom']);
379			$mail->subject($subj);
380			$mail->setBody($text);
381			$mail->send();
382        }
383    }
384
385    /**
386     * load all page stats from a given page.
387     *
388     * Note: doesn't check ACL
389     *
390     * @param string $page page id
391     *
392     * @return array
393     */
394    public function getAllPageStatuses($page)
395    {
396        if (!$this->db) {
397            return array();
398        }
399        if (!$page) {
400            return array();
401        }
402
403        $res = $this->db->query(
404           'SELECT
405                A.page     AS page,
406                A.creator  AS creator,
407                A.md5      AS md5,
408                B.status   AS status,
409                B.closedby AS closedby,
410                B.msg      AS msg
411            FROM tasks A
412            LEFT JOIN task_status B
413            ON A.page = B.page
414            AND A.md5 = B.md5
415            WHERE A.page = ?',
416            $page
417        );
418
419        return $this->db->res2arr($res);
420    }
421
422    /**
423     * Get information about the number of tasks on a specefic id.
424     *
425     * result keys are
426     *   count  - number of all tasks
427     *   done   - number of all finished tasks
428     *   undone - number of all tasks to do
429     *
430     * @param string $id String  Id of the wiki page - if no id is given the current page will be used.
431     *
432     * @return array
433     */
434    public function getPageTaskCount($id = '')
435    {
436        if (!$id) {
437            global $ID;
438            $id = $ID;
439        }
440        if (auth_quickaclcheck($id) < AUTH_READ) {
441            $tasks = array();
442        } else {
443            //for improving performance skip the access check in the query
444            $tasks = $this->loadTasks(array('id' => $id), $checkAccess = false);
445        }
446
447        $result = array(
448            'count' => count($tasks),
449            'done' => 0,
450            'undone' => 0,
451            'late' => 0,
452        );
453
454        foreach ($tasks as $task) {
455            if (empty($task['status'])) {
456                $result['undone']++;
457            } else {
458                $result['done']++;
459            }
460            if (!empty($task['date']) && empty($task['status'])) {
461                if (strtotime($task['date']) < time()) {
462                    $result['late']++;
463                }
464            }
465        }
466
467        return $result;
468    }
469
470    /**
471     * displays a small page task status view
472     *
473     * @param string $id     page id
474     * @param bool   $return if true return html, otherwise print the html
475     *
476     * @return string|void
477     */
478    public function tpl_pageTasks($id = '', $return = false)
479    {
480        $count = $this->getPageTaskCount($id);
481        if ($count['count'] == 0) {
482            return;
483        }
484
485        if ($count['undone'] == 0) { // all tasks done
486            $class = 'do_done';
487            $title = $this->getLang('title_alldone');
488        } elseif ($count['late'] == 0) { // open tasks - no late
489            $class = 'do_undone';
490            $title = sprintf($this->getLang('title_intime'), $count['undone']);
491        } else { // late tasks
492            $class = 'do_late';
493            $title = sprintf($this->getLang('title_late'), $count['undone'], $count['late']);
494        }
495
496        $out = '<div class="plugin__do_pagetasks" title="' . $title . '"><span class="' . $class . '">';
497        $out .= $count['undone'];
498        $out .= '</span></div>';
499
500        if ($return) {
501            return $out;
502        }
503        echo $out;
504    }
505
506    /**
507     * Get the html for an icon showing the user's open tasks
508     *
509     * If the user has open tasks, a js-overlay is shown on click.
510     *
511     * @return string the icon-html
512     */
513    public function tpl_getUserTasksIconHTML()
514    {
515        global $INPUT;
516        if (!$INPUT->server->has('REMOTE_USER')) {
517            return '';
518        }
519        $user = $INPUT->server->str('REMOTE_USER');
520        $tasks = $this->loadTasks(array('status' => array('undone'), 'user' => $user));
521        $num = count($tasks);
522
523        $svg = inlineSVG(__DIR__ . '/pix/clipboard-text.svg');
524
525        $doInner = '<span class="a11y">' . $this->getLang('prefix_tasks_user') . " </span>$svg<span class=\"num\">" . count($tasks) . '</span>';
526        if ($user && $num > 0) {
527            $title = sprintf($this->getLang('tasks_user_intime'), $num);
528            $link = '<button class="plugin__do_usertasks" title="' . $title . '">' . $doInner . '</button>';
529        } else {
530            $title = $this->getLang('tasks_user_none');
531            $link = '<span class="plugin__do_usertasks noopentasks" title="' . $title . '">' . $doInner . '</span>';
532        }
533
534        return $link;
535    }
536
537    /**
538     * Get a pretty userlink
539     *
540     * @param string $user users loginname
541     *
542     * @return string username with possible links
543     */
544    public function getPrettyUser($user)
545    {
546        $userpage = $this->getConf('userpage');
547        if ($userpage !== '' && $user !== '') {
548            return p_get_renderer('xhtml')->internallink(
549                sprintf($userpage, $user),
550                '', '', true, 'navigation'
551            );
552
553        } else {
554            return editorinfo($user);
555        }
556    }
557}
558
559