1<?php 2 3namespace dokuwiki\plugin\bez\mdl; 4 5use dokuwiki\plugin\bez\meta\ConsistencyViolationException; 6use dokuwiki\plugin\bez\meta\PermissionDeniedException; 7use dokuwiki\plugin\bez\meta\ValidationException; 8 9class Thread extends Entity { 10 11 protected $id; 12 13 protected $original_poster, $coordinator, $closed_by; 14 15 protected $private, $lock; 16 17 protected $type, $state; 18 19 protected $create_date, $last_activity_date, $last_modification_date, $close_date; 20 21 protected $title, $content, $content_html; 22 23 protected $task_count, $task_count_closed, $task_sum_cost; 24 25 protected $corrective_count, $preventive_count; 26 27 public static function get_columns() { 28 return array('id', 29 'original_poster', 'coordinator', 'closed_by', 30 'private', 'lock', 31 'type', 'state', 32 'create_date', 'last_activity_date', 'last_modification_date', 'close_date', 33 'title', 'content', 'content_html', 34 'task_count', 'task_count_closed', 'task_sum_cost'); 35 } 36 37 public static function get_select_columns() { 38 $cols = parent::get_select_columns(); 39 array_push($cols, 'label_id', 'label_name', 'corrective_count', 'preventive_count'); 40 return $cols; 41 } 42 43 public static function get_states() { 44 return array('proposal', 'opened', 'done', 'closed', 'rejected'); 45 } 46 47 public function __get($property) { 48 if($property == 'priority') { 49 return $this->$property; 50 } 51 return parent::__get($property); 52 } 53 54 public function user_is_coordinator() { 55 if ($this->coordinator === $this->model->user_nick || 56 $this->model->get_level() >= BEZ_AUTH_ADMIN) { 57 return true; 58 } 59 return false; 60 } 61 62 public function __construct($model, $defaults=array()) { 63 parent::__construct($model); 64 65 $this->validator->set_rules(array( 66 'coordinator' => array(array('dw_user'), 'NULL'), 67 'title' => array(array('length', 200), 'NOT NULL'), 68 'content' => array(array('length', 10000), 'NOT NULL'), 69 'type' => array(array('select', array('issue', 'project')), 'NULL') 70 )); 71 72 //we've created empty object (new record) 73 if ($this->id === NULL) { 74 $this->original_poster = $this->model->user_nick; 75 $this->create_date = date('c'); 76 $this->last_activity_date = $this->create_date; 77 $this->last_modification_date = $this->create_date; 78 79 $this->state = 'proposal'; 80 81 $this->acl->grant('title', BEZ_PERMISSION_CHANGE); 82 $this->acl->grant('content', BEZ_PERMISSION_CHANGE); 83 $this->acl->grant('type', BEZ_PERMISSION_CHANGE); 84 85 86 if ($this->model->get_level() >= BEZ_AUTH_LEADER) { 87 88 $this->state = 'opened'; 89 90 $this->acl->grant('coordinator', BEZ_PERMISSION_CHANGE); 91 $this->acl->grant('label_id', BEZ_PERMISSION_CHANGE); 92 $this->acl->grant('private', BEZ_PERMISSION_CHANGE); 93 } 94 95 } else { 96 //private threads 97 if ($this->model->level < BEZ_AUTH_ADMIN && $this->private == '1') { 98 if ($this->get_participant($this->model->user_nick) === false) { 99 $this->acl->revoke(self::get_select_columns(), BEZ_AUTH_LEADER); 100 return; 101 } 102 } 103 104 if ($this->state == 'proposal' && $this->original_poster == $this->model->user_nick) { 105 $this->acl->grant('title', BEZ_PERMISSION_CHANGE); 106 $this->acl->grant('content', BEZ_PERMISSION_CHANGE); 107 $this->acl->grant('type', BEZ_PERMISSION_CHANGE); 108 } 109 110 if ($this->coordinator == $this->model->user_nick) { 111 $this->acl->grant('title', BEZ_PERMISSION_CHANGE); 112 $this->acl->grant('content', BEZ_PERMISSION_CHANGE); 113 $this->acl->grant('coordinator', BEZ_PERMISSION_CHANGE); 114 $this->acl->grant('label_id', BEZ_PERMISSION_CHANGE); 115 $this->acl->grant('private', BEZ_PERMISSION_CHANGE); 116 117 $this->acl->grant('state', BEZ_PERMISSION_CHANGE); 118 $this->acl->grant('type', BEZ_PERMISSION_CHANGE); 119 } 120 } 121 } 122 123 public function set_data($data, $filter=NULL) { 124 parent::set_data($data, $filter=NULL); 125 126 if (isset($data['coordinator']) && $this->state == 'proposal') { 127 $this->state = 'opened'; 128 } 129 130 //update cache 131 $this->purge(); 132 133 //update dates 134 $this->last_modification_date = date('c'); 135 $this->last_activity_date = $this->last_modification_date; 136 } 137 138 public function set_state($state) { 139 if ($this->acl_of('state') < BEZ_PERMISSION_CHANGE) { 140 throw new PermissionDeniedException(); 141 } 142 143 if (!in_array($state, array('opened', 'closed', 'rejected'))) { 144 throw new ValidationException('thread', array('state should be opened, closed or rejected')); 145 } 146 147 //nothing to do 148 if ($state == $this->state) { 149 return; 150 } 151 152 if ($state == 'closed' || $state == 'rejected') { 153 $this->state = $state; 154 $this->closed_by = $this->model->user_nick; 155 $this->close_date = date('c'); 156 157 $this->model->sqlite->query("UPDATE {$this->get_table_name()} SET state=?, closed_by=?, close_date=? WHERE id=?", 158 $state, 159 $this->closed_by, 160 $this->close_date, 161 $this->id); 162 //reopen the task 163 } else { 164 $this->state = $state; 165 166 $this->model->sqlite->query("UPDATE {$this->get_table_name()} SET state=? WHERE id=?", $state, $this->id); 167 } 168 169 $this->state = $state; 170 } 171 172 public function set_private_flag($flag) { 173 $private = '0'; 174 if ($flag) { 175 $private = '1'; 176 } 177 178 if ($private == $this->private) { 179 return; 180 } 181 182 //update thread 183 $this->model->sqlite->query("UPDATE {$this->get_table_name()} SET private=? WHERE id=?", $private, $this->id); 184 185 //update task 186 $this->model->sqlite->query("UPDATE task SET private=? WHERE thread_id=?", $private, $this->id); 187 } 188 189 public function update_last_activity() { 190 $this->last_activity_date = date('c'); 191 $this->model->sqlite->query('UPDATE thread SET last_activity_date=? WHERE id=?', 192 $this->last_activity_date, $this->id); 193 } 194 195 public function get_participants($filter='') { 196 if ($this->id === NULL) { 197 return array(); 198 } 199 200 $sql = 'SELECT * FROM thread_participant WHERE'; 201 $possible_flags = array('original_poster', 'coordinator', 'commentator', 'task_assignee', 'subscribent'); 202 if ($filter != '') { 203 if (!in_array($filter, $possible_flags)) { 204 throw new \Exception("unknown flag $filter"); 205 } 206 $sql .= " $filter=1 AND"; 207 } 208 $sql .= ' thread_id=? AND removed=0 ORDER BY user_id'; 209 210 $r = $this->model->sqlite->query($sql, $this->id); 211 $pars = $this->model->sqlite->res2arr($r); 212 $participants = array(); 213 foreach ($pars as $par) { 214 $participants[$par['user_id']] = $par; 215 } 216 217 return $participants; 218 } 219 220 public function get_participant($user_id, $can_be_removed=false) { 221 if ($this->id === NULL) { 222 return array(); 223 } 224 225 $q = 'SELECT * FROM thread_participant WHERE thread_id=? AND user_id=?'; 226 if (!$can_be_removed) { 227 $q .= ' AND removed=0'; 228 } 229 $r = $this->model->sqlite->query($q, $this->id, $user_id); 230 $par = $this->model->sqlite->res2row($r); 231 if (!is_array($par)) { 232 return false; 233 } 234 235 return $par; 236 } 237 238 public function is_subscribent($user_id=null) { 239 if ($user_id == null) { 240 $user_id = $this->model->user_nick; 241 } 242 $par = $this->get_participant($user_id); 243 if ($par['subscribent'] == 1) { 244 return true; 245 } 246 return false; 247 } 248 249 public function remove_participant_flags($user_id, $flags) { 250 //thread not saved yet 251 if ($this->id === NULL) { 252 throw new \Exception('cannot remove flags from not saved thread'); 253 } 254 255 $participant = $this->get_participant($user_id, true); 256 if ($participant === false) { 257 throw new ConsistencyViolationException("$user_id isn't participant"); 258 } 259 260 $possible_flags = array('original_poster', 'coordinator', 'commentator', 'task_assignee', 'subscribent'); 261 if (array_intersect($flags, $possible_flags) != $flags) { 262 throw new \Exception('unknown flags'); 263 } 264 265 $set = implode(',', array_map(function ($v) { return "$v=0"; }, $flags)); 266 267 $sql = "UPDATE thread_participant SET $set WHERE thread_id=? AND user_id=?"; 268 $this->model->sqlite->query($sql, $this->id, $user_id); 269 270 } 271 272 public function set_participant_flags($user_id, $flags=array()) { 273 //thread not saved yet 274 if ($this->id === NULL) { 275 throw new \Exception('cannot add flags to not saved thread'); 276 } 277 278 //validate user 279 if (!$this->model->userFactory->exists($user_id)) { 280 throw new \Exception("$user_id isn't dokuwiki user"); 281 } 282 283 $possible_flags = array('original_poster', 'coordinator', 'commentator', 'task_assignee', 'subscribent'); 284 if (array_intersect($flags, $possible_flags) != $flags) { 285 throw new \Exception('unknown flags'); 286 } 287 288 $participant = $this->get_participant($user_id, true); 289 if ($participant == false) { 290 $participant = array_fill_keys($possible_flags, 0); 291 292 $participant['thread_id'] = $this->id; 293 $participant['user_id'] = $user_id; 294 $participant['added_by'] = $this->model->user_nick; 295 $participant['added_date'] = date('c'); 296 297 $values = array_merge($participant, array_fill_keys($flags, 1)); 298 299 $this->model->sqlite->storeEntry('thread_participant', $values); 300 } else { 301 $set = implode(',', array_map(function($flag) { return "$flag=1"; }, $flags)); 302 303 if ($participant['removed'] == '1') { 304 $set .= ',removed=0'; 305 } 306 307 $q = "UPDATE thread_participant SET $set WHERE thread_id=? AND user_id=?"; 308 $this->model->sqlite->query($q, $this->id, $user_id); 309 } 310 311 } 312 313 public function remove_participant($user_id) { 314 //thread not saved yet 315 if ($this->id === NULL) { 316 throw new \Exception('cannot remove flags from not saved thread'); 317 } 318 319 $participant = $this->get_participant($user_id); 320 if ($participant === false) { 321 throw new ConsistencyViolationException("$user_id isn't participant"); 322 } 323 324 if ($participant['coordinator'] == '1') { 325 throw new ConsistencyViolationException("cannot remove coordinator"); 326 } 327 328 if ($participant['task_assignee'] == '1') { 329 throw new ConsistencyViolationException("cannot remove task_assignee"); 330 } 331 332 $q = "UPDATE thread_participant SET removed=1 WHERE thread_id=? AND user_id=?"; 333 $this->model->sqlite->query($q, $this->id, $user_id); 334 335 } 336 337 public function invite($client) { 338 $this->set_participant_flags($client, array('subscribent')); 339 $this->mail_notify_invite($client); 340 } 341 342 public function get_labels() { 343 //record not saved 344 if ($this->id === NULL) { 345 return array(); 346 } 347 348 $labels = array(); 349 $r = $this->model->sqlite->query('SELECT * FROM label JOIN thread_label ON label.id = thread_label.label_id 350 WHERE thread_label.thread_id=?', $this->id); 351 $arr = $this->model->sqlite->res2arr($r); 352 foreach ($arr as $label) { 353 $labels[$label['id']] = $label; 354 } 355 356 return $labels; 357 } 358 359 public function add_label($label_id) { 360 //issue not saved yet 361 if ($this->id === NULL) { 362 throw new \Exception('cannot add labels to not saved thread. use initial_save() instead'); 363 } 364 365 $r = $this->model->sqlite->query('SELECT id FROM label WHERE id=?', $label_id); 366 $label_id = $this->model->sqlite->res2single($r); 367 if (!$label_id) { 368 throw new \Exception("label($label_id) doesn't exist"); 369 } 370 371 372 $this->model->sqlite->storeEntry('thread_label', 373 array('thread_id' => $this->id, 374 'label_id' => $label_id)); 375 376 } 377 378 public function remove_label($label_id) { 379 //issue not saved yet 380 if ($this->id === NULL) { 381 throw new \Exception('cannot remove labels from not saved thread. use initial_save() instead'); 382 } 383 384 /** @var \PDOStatement $r */ 385 $r = $this->model->sqlite->query('DELETE FROM thread_label WHERE thread_id=? AND label_id=?',$this->id, $label_id); 386 if ($r->rowCount() != 1) { 387 throw new \Exception('label was not assigned to this thread'); 388 } 389 390 } 391 392 public function get_causes() { 393 $r = $this->model->sqlite->query("SELECT id FROM thread_comment WHERE (type='cause' OR type='risk' OR type='opportunity') AND thread_id=?", 394 $this->id); 395 $arr = $this->model->sqlite->res2arr($r); 396 $causes = array(); 397 foreach ($arr as $cause) { 398 $causes[] = $cause['id']; 399 } 400 401 return $causes; 402 } 403 404 public function can_add_comments() { 405 return in_array($this->state, array('proposal', 'opened', 'done')); 406 } 407 408 public function can_add_causes() { 409 return $this->type == 'issue' && in_array($this->state, array('opened', 'done')); 410 } 411 412 public function can_add_tasks() { 413 return in_array($this->state, array('opened', 'done')); 414 } 415 416 public function can_add_participants() { 417 return in_array($this->state, array('opened', 'done')); 418 } 419 420 public function count_opened_nopreventive_tasks() { 421 $res = $this->model->sqlite->query("SELECT state FROM task WHERE thread_id = ? 422 AND type != 'preventive' 423 AND state = 'opened'", $this->id); 424 return $this->model->sqlite->res2count($res); 425 } 426 427 public function can_be_closed() { 428 $res = $this->model->sqlite->query("SELECT thread_comment.id FROM thread_comment 429 LEFT JOIN task ON thread_comment.id = task.thread_comment_id 430 WHERE thread_comment.thread_id = ? AND 431 thread_comment.type = 'cause' AND task.id IS NULL", $this->id); 432 $causes_without_tasks = $this->model->sqlite->res2count($res); 433 434 return !in_array($this->state, array('closed', 'rejected')) 435 && $this->task_count > 0 436 && $this->count_opened_nopreventive_tasks() == 0 437 && $causes_without_tasks == 0; 438 } 439 440 public function can_be_rejected() { 441 return $this->state != 'rejected' && $this->task_count == 0; 442 } 443 444 public function can_be_reopened() { 445 return in_array($this->state, array('closed', 'rejected')); 446 } 447 448 public function closing_comment() { 449 $r = $this->model->thread_commentFactory->get_from_thread($this, array(), 'id DESC', 1); 450 $thread_comment = $r->fetch(); 451 452 return $thread_comment->content_html; 453 } 454 455 protected function html_link_url() { 456 $tpl = $this->model->action->get_tpl(); 457 return $tpl->url('thread', 'id', $this->id); 458 } 459 460 protected function html_link_content() { 461 return '#' . $this->id; 462 } 463 464 protected function getMailSubject() 465 { 466 return parent::getMailSubject() . ' #'.$this->id. ' ' .$this->title; 467 } 468 469 public function mail_thread_box(&$attachedImages) { 470 $tpl = $this->model->action->get_tpl(); 471 472 //render style 473 $less = new \lessc(); 474 $less->addImportDir(DOKU_PLUGIN . 'bez/style/'); 475 $style = $less->compileFile(DOKU_PLUGIN . 'bez/style/thread.less'); 476 477 //render content for mail 478 $old_content_html = $this->content_html; 479 $this->content_html = p_render('bez_xhtmlmail', p_get_instructions($this->content), $info); 480 $attachedImages = array_merge($attachedImages, $info['img']); 481 482 $tpl->set('thread', $this); 483 $tpl->set('style', $style); 484 $tpl->set('no_actions', true); 485 $thread_box = $this->model->action->bez_tpl_include('thread_box', true); 486 487 $this->content_html = $old_content_html; 488 489 return $thread_box; 490 } 491 492 public function mail_thread(&$attachedImages) { 493 $tpl = $this->model->action->get_tpl(); 494 495 $thread_box = $this->mail_thread_box($attachedImages); 496 497 $tpl->set('content', $thread_box); 498 $content = $this->model->action->bez_tpl_include('mail/thread', true); 499 500 return $content; 501 } 502 503 public function mail_notify_change_state($action='') { 504 if (!$action) { 505 $action = 'mail_mail_notify_change_state_action'; 506 } 507 $tpl = $this->model->action->get_tpl(); 508 509 $tpl->set('who', $this->model->user_nick); 510 $tpl->set('action', $action); 511 $attachedImages = array(); 512 $content = $this->mail_thread($attachedImages); 513 $this->mail_notify($content, false, $attachedImages); 514 } 515 516 public function mail_notify_invite($client) { 517 $tpl = $this->model->action->get_tpl(); 518 519 $tpl->set('who', $this->model->user_nick); 520 $tpl->set('action', 'mail_mail_notify_invite_action'); 521 $attachedImages = array(); 522 $content = $this->mail_thread($attachedImages); 523 $this->mail_notify($content, array($client), $attachedImages); 524 } 525 526 public function mail_inform_coordinator() { 527 $tpl = $this->model->action->get_tpl(); 528 529 $tpl->set('who', $this->model->user_nick); 530 $tpl->set('action', 'mail_mail_inform_coordinator_action'); 531 $attachedImages = array(); 532 $content = $this->mail_thread($attachedImages); 533 $this->mail_notify($content, array($this->coordinator), $attachedImages); 534 } 535 536 public function mail_inform_admins() { 537 $tpl = $this->model->action->get_tpl(); 538 539 $tpl->set('who', $this->model->user_nick); 540 $tpl->set('action', 'mail_mail_inform_admins_action'); 541 $attachedImages = array(); 542 $content = $this->mail_thread($attachedImages); 543 $this->mail_notify($content, $this->model->userFactory->users_of_group(array('admin', 'bez_admin')), $attachedImages); 544 } 545 546 public function mail_notify_inactive($users=false) { 547 $tpl = $this->model->action->get_tpl(); 548 549 $tpl->set('who', $this->model->user_nick); 550 $tpl->set('action', 'mail_mail_notify_issue_inactive'); 551 $attachedImages = array(); 552 $content = $this->mail_thread($attachedImages); 553 $this->mail_notify($content, $users, $attachedImages); 554 } 555 556 public function mail_notify_task_added(Task $task) { 557 $tpl = $this->model->action->get_tpl(); 558 559 //we don't want who 560 $tpl->set('who', $this->model->user_nick); 561 $tpl->set('action', 'mail_thread_task_added'); 562 $attachedImages = array(); 563 $task_box = $task->mail_task_box($attachedImages); 564 565 $tpl->set('thread', $this); 566 $tpl->set('content', $task_box); 567 $content = $this->model->action->bez_tpl_include('mail/thread', true); 568 569 $this->mail_notify($content, false, $attachedImages); 570 } 571 572 public function mail_notify_task_state_changed(Task $task) { 573 $tpl = $this->model->action->get_tpl(); 574 575 if ($task->state == 'done') { 576 $action = 'mail_thread_task_done'; 577 } else { 578 $action = 'mail_thread_task_reopened'; 579 } 580 581 //we don't want who 582 $tpl->set('who', $this->model->user_nick); 583 $tpl->set('action', $action); 584 $attachedImages = array(); 585 $task_box = $task->mail_task_box($attachedImages); 586 587 $tpl->set('thread', $this); 588 $tpl->set('content', $task_box); 589 $content = $this->model->action->bez_tpl_include('mail/thread', true); 590 591 $this->mail_notify($content, false, $attachedImages); 592 } 593 594 public function can_be_removed() { 595 $r = $this->model->sqlite->query("SELECT COUNT(*) FROM thread_comment WHERE thread_id=?", 596 $this->id); 597 $comments_count = $this->model->sqlite->res2single($r); 598 return $this->task_count == 0 && $comments_count == 0; 599 } 600} 601