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 specific 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