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