1<?php
2/**
3 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
4 * @author     Esther Brunner <wikidesign@gmail.com>
5 */
6
7class helper_plugin_task extends DokuWiki_Plugin {
8
9    function getMethods() {
10        $result = array();
11        $result[] = array(
12                'name'   => 'th',
13                'desc'   => 'returns the header of the task column for pagelist',
14                'return' => array('header' => 'string'),
15                );
16        $result[] = array(
17                'name'   => 'td',
18                'desc'   => 'returns the status of the task',
19                'params' => array('id' => 'string'),
20                'return' => array('label' => 'string'),
21                );
22        $result[] = array(
23                'name'   => 'getTasks',
24                'desc'   => 'get task pages, sorted by priority',
25                'params' => array(
26                    'namespace' => 'string',
27                    'number (optional)' => 'integer',
28                    'filter (optional)' => 'string'),
29                'return' => array('pages' => 'array'),
30                );
31        $result[] = array(
32                'name'   => 'readTask',
33                'desc'   => 'get a single task metafile',
34                'params' => array('id' => 'string'),
35                'return' => array('task on success, else false' => 'array, (boolean)'),
36                );
37        $result[] = array(
38                'name'   => 'writeTask',
39                'desc'   => 'save task metdata in a file',
40                'params' => array(
41                    'id' => 'string',
42                    'task' => 'array'),
43                'return' => array('success' => 'boolean'),
44                );
45        $result[] = array(
46                'name'   => 'statusLabel',
47                'desc'   => 'returns the status label for a given integer',
48                'params' => array('status' => 'integer'),
49                'return' => array('label' => 'string'),
50                );
51        return $result;
52    }
53
54    /**
55     * Returns the column header for the Pagelist Plugin
56     */
57    function th() {
58        return $this->getLang('status');
59    }
60
61    /**
62     * Returns the status of the task
63     */
64    function td($id) {
65        $task = $this->readTask($id);
66        return $this->statusLabel($task['status']);
67    }
68
69    /**
70     * Returns an array of task pages, sorted by priority
71     */
72    function getTasks($ns, $num = NULL, $filter = '', $user = NULL) {
73        global $conf;
74
75        if (!$filter) $filter = strtolower($_REQUEST['filter']);
76
77        require_once(DOKU_INC.'inc/search.php');
78
79        $dir = $conf['datadir'].($ns ? '/'.str_replace(':', '/', $ns): '');
80
81        // returns the list of pages in the given namespace and it's subspaces
82        $items = array();
83        $opts = array();
84        $ns = utf8_encodeFN(str_replace(':', '/', $ns));
85        search($items, $conf['datadir'], 'search_allpages', $opts, $ns);
86
87        // add pages with comments to result
88        $result = array();
89        foreach ($items as $item) {
90            $id = $item['id'];
91
92            // skip pages without task
93            if (!$task = $this->readTask($id)) continue;
94
95            $date = $task['date']['due'];
96            $responsible = $this->_isResponsible($task['user']);
97
98            // Check status in detail if filter is not 'all'
99            if ($filter != 'all') {
100                if ($filter == 'rejected') {
101                    // Only show 'rejected'
102                    if ($task['status'] != -1) continue;
103                } else if ($filter == 'accepted') {
104                    // Only show 'accepted' and 'started'
105                    if ($task['status'] != 1 && $task['status'] != 2) continue;
106                } else if ($filter == 'started') {
107                    // Only show 'started'
108                    if ($task['status'] != 2) continue;
109                } else if ($filter == 'done') {
110                    // Only show 'done'
111                    if ($task['status'] != 3) continue;
112                } else if ($filter == 'verified') {
113                    // Only show 'verified'
114                    if ($task['status'] != 4) continue;
115                } else {
116                    // No pure status filter, skip done and closed tasks
117                    if (($task['status'] < 0) || ($task['status'] > 2)) continue;
118                }
119            }
120
121            // skip other's tasks if filter is 'my'
122            if (($filter == 'my') && (!$responsible)) continue;
123
124            // skip assigned and not new tasks if filter is 'new'
125            if (($filter == 'new') && ($task['user']['name'] || ($task['status'] != 0))) continue;
126
127            // filter is 'due' or 'overdue'
128            if (in_array($filter, array('due', 'overdue'))) {
129                if (!$date || ($date > time()) || ($task['status'] > 2)) continue;
130                elseif (($date + 86400 < time()) && ($filter == 'due')) continue;
131                elseif (($date + 86400 > time()) && ($filter == 'overdue')) continue;
132            }
133
134            $result[$task['key']] = array(
135                    'id'       => $id,
136                    'date'     => $date,
137                    'user'     => $task['user']['name'],
138                    'status'   => $this->statusLabel($task['status']),
139                    'priority' => $task['priority'],
140                    'perm'     => $perm,
141                    'file'     => $task['file'],
142                    'exists'   => true,
143                    );
144        }
145
146        // finally sort by time of last comment
147        krsort($result);
148
149        if (is_numeric($num)) $result = array_slice($result, 0, $num);
150
151        return $result;
152    }
153
154    /**
155     * Reads the .task metafile
156     */
157    function readTask($id) {
158        $file = metaFN($id, '.task');
159        if (!@file_exists($file)) {
160            $data = p_get_metadata($id, 'task');
161            if (is_array($data)) {
162                $data['date'] = array('due' => $data['date']);
163                $data['user'] = array('name' => $data['user']);
164                $meta = array('task' => NULL);
165                if ($this->writeTask($id, $data)) p_set_metadata($id, $meta);
166            }
167        } else {
168            $data = unserialize(io_readFile($file, false));
169        }
170        if (!is_array($data) || empty($data)) return false;
171        $data['file']   = $file;
172        $data['exists'] = true;
173        return $data;
174    }
175
176    /**
177     * Saves the .task metafile
178     */
179    function writeTask($id, $data) {
180        if (!is_array($data)) return false;
181        $file = ($data['file'] ? $data['file'] : metaFN($id, '.task'));
182
183        // remove file and exists keys
184        unset($data['file']);
185        unset($data['exists']);
186
187        // set creation or modification time
188        if (!is_array($data['date'])) $data['date'] = array('due' => $data['date']);
189        if (!@file_exists($file) || !$data['date']['created']) {
190            $data['date']['created'] = time();
191        } else {
192            $data['date']['modified'] = time();
193        }
194
195        if (!is_array($data['user'])) $data['user'] = array('name' => $data['user']);
196
197        if (!isset($data['status'])) {    // make sure we don't overwrite status
198            $current = unserialize(io_readFile($file, false));
199            $data['status'] = $current['status'];
200        } elseif ($data['status'] == 3) { // set task completion time
201            $data['date']['completed'] = time();
202        }
203
204        // generate vtodo for iCal file download
205        $data['vtodo'] = $this->_vtodo($id, $data);
206
207        // generate sortkey with priority and creation date
208        $data['key'] = chr($data['priority'] + 97).(2000000000 - $data['date']['created']);
209
210        // save task metadata
211        $ok = io_saveFile($file, serialize($data));
212
213        // and finally notify users
214        $this->_notify($data);
215        return $ok;
216    }
217
218    /**
219     * Returns the label of a status
220     */
221    function statusLabel($status) {
222        switch ($status) {
223            case -1:
224                return $this->getLang('rejected');
225            case 1:
226                return $this->getLang('accepted');
227            case 2:
228                return $this->getLang('started');
229            case 3:
230                return $this->getLang('done');
231            case 4:
232                return $this->getLang('verified');
233            default:
234                return $this->getLang('new');
235        }
236    }
237
238    /**
239     * Returns the label of a priority
240     */
241    function priorityLabel($priority) {
242        switch ($priority) {
243            case 1:
244                return $this->getLang('medium');
245            case 2:
246                return $this->getLang('high');
247            case 3:
248                return $this->getLang('critical');
249            default:
250                return $this->getLang('low');
251        }
252    }
253
254    /**
255     * Is the given task assigned to the current user?
256     */
257    function _isResponsible($user) {
258        global $INFO;
259
260        if (!$user) return false;
261
262        if (isset($user['id']) && $user['id'] == $_SERVER['REMOTE_USER'] || isset($user['name']) && $user['name'] == $INFO['userinfo']['name'] || $user == $INFO['userinfo']['name']) {
263            return true;
264        }
265
266        return false;
267    }
268
269    /**
270     * Interpret date with strtotime()
271     */
272    function _interpretDate($str) {
273        if (!$str) return NULL;
274
275        // only year given -> time till end of year
276        if (preg_match("/^\d{4}$/", $str)) {
277            $str .= '-12-31';
278
279            // only month given -> time till last of month
280        } elseif (preg_match("/^\d{4}-(\d{2})$/", $str, $month)) {
281            switch ($month[1]) {
282                case '01': case '03': case '05': case '07': case '08': case '10': case '12':
283                    $str .= '-31';
284                    break;
285                case '04': case '06': case '09': case '11':
286                    $str .= '-30';
287                    break;
288                case '02': // leap year isn't handled here
289                    $str .= '-28';
290                    break;
291            }
292        }
293
294        // convert to UNIX time
295        $date = strtotime($str);
296        if ($date === -1) $date = NULL;
297        return $date;
298    }
299
300    /**
301     * Sends a notify mail on new or changed task
302     *
303     * @param  array  $task  data array of the task
304     *
305     * @author Andreas Gohr <andi@splitbrain.org>
306     * @author Esther Brunner <wikidesign@gmail.com>
307     */
308    function _notify($task) {
309        global $conf;
310        global $ID;
311
312        if ((!$conf['subscribers']) && (!$conf['notify'])) return; //subscribers enabled?
313        $data = array('id' => $ID, 'addresslist' => '', 'self' => false);
314        trigger_event('COMMON_NOTIFY_ADDRESSLIST', $data, 'subscription_addresslist');
315        $bcc = $data['addresslist'];
316        if ((empty($bcc)) && (!$conf['notify'])) return;
317        $to   = $conf['notify'];
318        $text = io_readFile($this->localFN('subscribermail'));
319
320        $text = str_replace('@PAGE@', $ID, $text);
321        $text = str_replace('@TITLE@', $conf['title'], $text);
322        if(!empty($task['date']['due'])) {
323            $dformat = preg_replace('#%[HIMprRST]|:#', '', ($conf['dformat']));
324            $text    = str_replace('@DATE@', strftime($dformat, $task['date']['due']), $text);
325        } else {
326            $text = str_replace('@DATE@', '', $text);
327        }
328        $text = str_replace('@NAME@', $task['user']['name'], $text);
329        $text = str_replace('@STATUS@', $this->statusLabel($task['status']), $text);
330        $text = str_replace('@PRIORITY@', $this->priorityLabel($task['priority']), $text);
331        $text = str_replace('@UNSUBSCRIBE@', wl($ID, 'do=unsubscribe', true, '&'), $text);
332        $text = str_replace('@DOKUWIKIURL@', DOKU_URL, $text);
333
334        $subject = '['.$conf['title'].'] ';
335        if ($task['status'] == 0) $subject .= $this->getLang('mail_newtask');
336        else $subject .= $this->getLang('mail_changedtask');
337
338        mail_send($to, $subject, $text, $conf['mailfrom'], '', $bcc);
339    }
340
341    /**
342     * Generates a VTODO section for iCal file download
343     */
344    function _vtodo($id, $task) {
345        if (!defined('CRLF')) define('CRLF', "\r\n");
346
347        $meta = p_get_metadata($id);
348
349        $ret = 'BEGIN:VTODO'.CRLF.
350            'UID:'.$id.'@'.$_SERVER['SERVER_NAME'].CRLF.
351            'URL:'.wl($id, '', true, '&').CRLF.
352            'SUMMARY:'.$this->_vsc($meta['title']).CRLF;
353        if ($meta['description']['abstract'])
354            $ret .= 'DESCRIPTION:'.$this->_vsc($meta['description']['abstract']).CRLF;
355        if ($meta['subject'])
356            $ret .= 'CATEGORIES:'.$this->_vcategories($meta['subject']).CRLF;
357        if ($task['date']['created'])
358            $ret .= 'CREATED:'.$this->_vdate($task['date']['created']).CRLF;
359        if ($task['date']['modified'])
360            $ret .= 'LAST-MODIFIED:'.$this->_vdate($task['date']['modified']).CRLF;
361        if ($task['date']['due'])
362            $ret .= 'DUE:'.$this->_vdate($task['date']['due']).CRLF;
363        if ($task['date']['completed'])
364            $ret .= 'COMPLETED:'.$this->_vdate($task['date']['completed']).CRLF;
365        if ($task['user']) $ret .= 'ORGANIZER;CN="'.$this->_vsc($task['user']['name']).'":'.
366            'MAILTO:'.$task['user']['mail'].CRLF;
367        $ret .= 'STATUS:'.$this->_vstatus($task['status']).CRLF;
368        if (is_numeric($task['priority']))
369            $ret .= 'PRIORITY:'.(7 - ($task['priority'] * 2)).CRLF;
370        $ret .= 'CLASS:'.$this->_vclass($id).CRLF.
371            'END:VTODO'.CRLF;
372        return $ret;
373    }
374
375    /**
376     * Encodes vCard / iCal special characters
377     */
378    function _vsc($string) {
379        $search = array("\\", ",", ";", "\n", "\r");
380        $replace = array("\\\\", "\\,", "\\;", "\\n", "\\n");
381        return str_replace($search, $replace, $string);
382    }
383
384    /**
385     * Generates YYYYMMDD"T"hhmmss"Z" UTC time date format (ISO 8601 / RFC 3339)
386     */
387    function _vdate($date, $extended = false) {
388        if ($extended) return strftime('%Y-%m-%dT%H:%M:%SZ', $date);
389        else return strftime('%Y%m%dT%H%M%SZ', $date);
390    }
391
392    /**
393     * Returns VTODO status
394     */
395    function _vstatus($status) {
396        switch ($status) {
397            case -1:
398                return 'CANCELLED';
399            case 1:
400            case 2:
401                return 'IN-PROCESS';
402            case 3:
403            case 4:
404                return 'COMPLETED';
405            default:
406                return 'NEEDS-ACTION';
407        }
408    }
409
410    /**
411     * Returns VTODO categories
412     */
413    function _vcategories($cat) {
414        if (!is_array($cat)) $cat = explode(' ', $cat);
415        return join(',', $this->_vsc($cat));
416    }
417
418    /**
419     * Returns access classification for VTODO
420     */
421    function _vclass($id) {
422        global $USERINFO; // checks access rights for anonymous user
423        if (auth_aclcheck($id, '', $USERINFO['grps'])) return 'PUBLIC';
424        else return 'PRIVATE';
425    }
426
427    /**
428     * Show the form to create a new task.
429     * The function just forwards the call to the old or new function.
430     *
431     * @param string $ns              The DokuWiki namespace in which the new task
432     *                                page shall be created
433     * @param bool   $selectUser      If false then create a simple input line for the user field.
434     *                                If true then create a drop down list.
435     * @param bool   $selectUserGroup If not NULL and if $selectUser==true then the drop down list
436     *                                for the user field will only show users who are members of
437     *                                the user group given in $selectUserGroup.
438     */
439    function _newTaskForm($ns, $selectUser=false, $selectUserGroup=NULL) {
440        if (class_exists('dokuwiki\Form\Form')) {
441            return $this->_newTaskFormNew($ns, $selectUser, $selectUserGroup);
442        } else {
443            return $this->_newTaskFormOld($ns, $selectUser, $selectUserGroup);
444        }
445    }
446
447    /**
448     * Show the form to create a new task.
449     * This is the new version using class dokuwiki\Form\Form.
450     *
451     * @see _newTaskForm
452     */
453    protected function _newTaskFormNew($ns, $selectUser=false, $selectUserGroup=NULL) {
454        global $ID, $lang, $INFO, $auth;
455
456        $form = new dokuwiki\Form\Form(array('id' => 'task__newtask_form'));
457        $pos = 1;
458
459        // Open fieldset
460        $form->addFieldsetOpen($this->getLang('newtask'), $pos++);
461
462        // Set hidden fields
463        $form->setHiddenField ('id', $ID);
464        $form->setHiddenField ('do', 'newtask');
465        $form->setHiddenField ('ns', $ns);
466
467        // Set input filed for task title
468        $input = $form->addTextInput('title', NULL, $pos++);
469        $input->attr('id', 'task__newtask_title');
470        $input->attr('size', '40');
471
472        // Set input field for user (either text field or drop down box)
473        $form->addHTML('<table class="blind"><tr><th>'.$this->getLang('user').':</th><td>', $pos++);
474        if(!$selectUser) {
475            // Old way input field
476            $input = $form->addTextInput('user', NULL, $pos++);
477            $input->attr('value', hsc($INFO['userinfo']['name']));
478        } else {
479            // Select user from drop down list
480            $filter = array();
481            $filter['grps'] = $selectUserGroup;
482            $options = array();
483            if ($auth) {
484                foreach ($auth->retrieveUsers(0, 0, $filter) as $curr_user) {
485                    $options [] = $curr_user['name'];
486                }
487            }
488            $input = $form->addDropdown('user', $options, NULL, $pos++);
489            $input->val($INFO['userinfo']['name']);
490        }
491        $form->addHTML('</td></tr>', $pos++);
492
493        // Field for due date
494        if ($this->getConf('datefield')) {
495            $form->addHTML('<tr><th>'.$this->getLang('date').':</th><td>', $pos++);
496            $input = $form->addTextInput('date', NULL, $pos++);
497            $input->attr('value', date('Y-m-d'));
498            $form->addHTML('</td></tr>', $pos++);
499        }
500
501        // Select priority from drop down list
502        $form->addHTML('<tr><th>'.$this->getLang('priority').':</th><td>');
503        $filter = array();
504        $filter['grps'] = $selectUserGroup;
505        $options = array();
506        $options [''] = $this->getLang('low');
507        $options ['!'] = $this->getLang('medium');
508        $options ['!!'] = $this->getLang('high');
509        $options ['!!!'] = $this->getLang('critical');
510        $input = $form->addDropdown('priority', $options, NULL, $pos++);
511        $input->attr('size', '1');
512        $input->val($this->getLang('low'));
513        $form->addHTML('</td></tr>', $pos++);
514
515        $form->addHTML('</table>', $pos++);
516
517        // Add button
518        $form->addButton(NULL, $lang['btn_create'], $pos++);
519
520        // Close fieldset
521        $form->addFieldsetClose($pos++);
522
523        // Generate the HTML-Representation of the form
524        $ret = '<div class="newtask_form">';
525        $ret .= $form->toHTML();
526        $ret .= '</div>';
527
528        return $ret;
529    }
530
531    /**
532     * Show the form to create a new task.
533     * This is the old version, creating all HTML code on its own.
534     *
535     * @see _newTaskForm
536     */
537    protected function _newTaskFormOld($ns, $selectUser=false, $selectUserGroup=NULL) {
538        global $ID, $lang, $INFO, $auth;
539
540        $ret =  '<div class="newtask_form">';
541        $ret .= '<form id="task__newtask_form"  method="post" action="'.script().'" accept-charset="'.$lang['encoding'].'">';
542        $ret .= '<fieldset>';
543        $ret .= '<legend> '.$this->getLang('newtask').': </legend>';
544        $ret .= '<input type="hidden" name="id" value="'.$ID.'" />';
545        $ret .= '<input type="hidden" name="do" value="newtask" />';
546        $ret .= '<input type="hidden" name="ns" value="'.$ns.'" />';
547        $ret .= '<input class="edit" type="text" name="title" id="task__newtask_title" size="40" tabindex="1" />';
548        $ret .= '<table class="blind"><tr>';
549
550        if(!$selectUser) {
551            // Old way input field
552            $ret .= '<th>'.$this->getLang('user').':</th>';
553            $ret .= '<td><input type="text" name="user" value="'.hsc($INFO['userinfo']['name']).'" class="edit" tabindex="2" /></td>';
554        } else {
555            // Select user from drop down list
556            $ret .= '<th>'.$this->getLang('user').':</th>';
557            $ret .= '<td><select name="user">';
558
559            $filter = array();
560            $filter['grps'] = $selectUserGroup;
561            if ($auth) {
562                foreach ($auth->retrieveUsers(0, 0, $filter) as $curr_user) {
563                    $ret .= '<option' . ($curr_user['name'] == $INFO['userinfo']['name'] ? ' selected="selected"' : '') . '>' . $curr_user['name'] . '</option>';
564                }
565            }
566            $ret .= '</select></td>';
567        }
568
569        $ret .= '</tr>';
570        if ($this->getConf('datefield')) { // field for due date
571            $ret .= '<tr><th>'.$this->getLang('date').':</th>';
572            $ret .= '<td><input type="text" name="date" value="'.date('Y-m-d').'" class="edit" tabindex="3" /></td></tr>';
573        }
574        $ret .= '<tr><th>'.$this->getLang('priority').':</th><td>';
575        $ret .= '<select name="priority" size="1" tabindex="4" class="edit">';
576        $ret .= '<option value="" selected="selected">'.$this->getLang('low').'</option>';
577        $ret .= '<option value="!">'.$this->getLang('medium').'</option>';
578        $ret .= '<option value="!!">'.$this->getLang('high').'</option>';
579        $ret .= '<option value="!!!">'.$this->getLang('critical').'</option>';
580        $ret .= '</select>';
581        $ret .= '</td></tr></table>';
582        $ret .= '<input class="button" type="submit" value="'.$lang['btn_create'].'" tabindex="5" />';
583        $ret .= '</fieldset></form></div>'.DOKU_LF;
584        return $ret;
585    }
586}
587// vim:ts=4:sw=4:et:enc=utf-8:
588