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