1<?php 2/** 3 * Tracker - Universal tracker (bugs, feature requests, ...) with voting and bounties 4 * 5 * @link http://www.egroupware.org 6 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> 7 * @package tracker 8 * @copyright (c) 2006-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de> 9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 10 * @version $Id$ 11 */ 12 13use EGroupware\Api; 14use EGroupware\Api\Link; 15use EGroupware\Api\Framework; 16use EGroupware\Api\Egw; 17use EGroupware\Api\Acl; 18use EGroupware\Api\Etemplate; 19 20/** 21 * User Interface of the tracker 22 */ 23class tracker_ui extends tracker_bo 24{ 25 /** 26 * Functions callable via menuaction 27 * 28 * @var array 29 */ 30 var $public_functions = array( 31 'edit' => true, 32 'index' => true, 33 'tprint'=> true, 34 'mail_import' => True, 35 ); 36 /** 37 * Displayed instead of the '@' in email-addresses 38 * 39 * @var string 40 */ 41 var $mangle_at = ' -at- '; 42 /** 43 * reference to the preferences of the user 44 * 45 * @var array 46 */ 47 var $prefs; 48 49 /** 50 * allowed units and hours per day, can be overwritten by the projectmanager configuration, default all units, 8h 51 * 52 * @var string 53 */ 54 var $duration_format = ','; // comma is necessary! 55 56 /** 57 * Etemplate used for rendering 58 * 59 * @var Etemplate 60 */ 61 public $template; 62 63 /** 64 * Constructor 65 * 66 * @return tracker_ui 67 */ 68 function __construct() 69 { 70 parent::__construct(); 71 $this->prefs =& $GLOBALS['egw_info']['user']['preferences']['tracker']; 72 73 // read the duration format from project-manager 74 if ($GLOBALS['egw_info']['apps']['projectmanager']) 75 { 76 $pm_config = Api\Config::read('projectmanager'); 77 $this->duration_format = str_replace(',','',implode('', (array)$pm_config['duration_units'])).','.$pm_config['hours_per_workday']; 78 unset($pm_config); 79 } 80 } 81 82 /** 83 * Print a tracker item 84 * 85 * @return string html-content, if sitemgr otherwise null 86 */ 87 function tprint() 88 { 89 // Check if exists 90 if ((int)$_GET['tr_id']) 91 { 92 if (!$this->read($_GET['tr_id'])) 93 { 94 return lang('Tracker item not found !!!'); 95 } 96 } 97 else // new item 98 { 99 return lang('Tracker item not found !!!'); 100 } 101 if (!is_object($this->tracking)) 102 { 103 $this->tracking = new tracker_tracking($this); 104 } 105 106 if ($this->data['tr_edit_mode'] == 'html') 107 { 108 $this->tracking->html_content_allow = true; 109 } 110 111 $details = $this->tracking->get_body(true,$this->data,$this->data); 112 if (!$details) 113 { 114 return implode(', ',$this->tracking->errors); 115 } 116 $GLOBALS['egw']->framework->render($details,'',false); 117 } 118 119 /** 120 * Edit a tracker item in a popup 121 * 122 * @param array $content =null eTemplate content 123 * @param string $msg ='' 124 * @param boolean $popup =true use or not use a popup 125 * @return string html-content, if sitemgr otherwise null 126 */ 127 function edit($content=null,$msg='',$popup=true) 128 { 129 if ($this->htmledit || (isset($content['tr_edit_mode']) && $content['tr_edit_mode']=='html')) 130 { 131 $tr_editor_mode = 'html'; 132 } 133 else 134 { 135 $tr_editor_mode = 'ascii'; 136 } 137 138 //_debug_array($content); 139 if (!is_array($content)) 140 { 141 if ($_GET['msg']) $msg = strip_tags($_GET['msg']); 142 143 // edit or new? 144 if ((int)$_GET['tr_id']) 145 { 146 $own_referer = Api\Header\Referer::get(); 147 if (!$this->read($_GET['tr_id'])) 148 { 149 Framework::window_close(lang('Tracker item not found !!!')); 150 } 151 else 152 { 153 // Set the ticket as seen by this user 154 self::seen($this->data, true); 155 156 // editing, preventing/fixing mixed ascii-html 157 if ($this->data['tr_edit_mode'] == 'ascii' && $this->htmledit) 158 { 159 // non html items edited by html (add nl2br) 160 $tr_editor_mode = 'ascii'; 161 } 162 if ($this->data['tr_edit_mode'] == 'html' && !$this->htmledit) 163 { 164 // html items edited in ascii mode (prevent changing to html) 165 $tr_editor_mode = 'html'; 166 } 167 //echo "<p>data[tr_edit_mode]={$this->data['tr_edit_mode']}, this->htmledit=".array2string($this->htmledit)."</p>\n"; 168 // Ascii Replies are converted to html, if htmledit is disabled (default), we allways convert, as this detection is weak 169 // Conversion must be based on ticket setting, since it persists after the config setting is changed 170 foreach ($this->data['replies'] as &$reply) 171 { 172 if (!($this->data['tr_edit_mode'] == 'html')|| (strlen($reply['reply_message'])==strlen(strip_tags($reply['reply_message'])))) //(stripos($reply['reply_message'], '<br') === false && stripos($reply['reply_message'], '<p>') === false)) 173 { 174 $reply['reply_message'] = Api\Html::htmlspecialchars($reply['reply_message']); 175 } 176 } 177 //// Make sure add comment file directory is empty, in case someone closed 178 // it without saving after selecting or uploading a file 179 if($this->file_access($tr_id, Acl::DELETE)) 180 { 181 $this->remove_comment_dir($tr_id); 182 } 183 } 184 $needInit = false; 185 } 186 else // new item 187 { 188 $needInit = true; 189 $regardInInit = array(); 190 } 191 // for new items we use the session-state or $_GET['tracker'] 192 if (!$this->data['tr_id']) 193 { 194 $regardInInit = array( 195 'tr_tracker' => $this->data['tr_tracker'] 196 ); 197 if (($state = Api\Cache::getSession('tracker','index'. 198 (isset($this->trackers[(int)$_GET['only_tracker']]) ? '-'.$_GET['only_tracker'] : '')))) 199 { 200 $this->data['tr_tracker'] = $regardInInit['tr_tracker'] = $state['col_filter']['tr_tracker'] ? $state['col_filter']['tr_tracker'] : $this->data['tr_tracker']; 201 $this->data['cat_id'] = $regardInInit['cat_id'] = $state['cat_id'] ? $state['cat_id'] : false; 202 $this->data['tr_version'] = $regardInInit['tr_version'] = $state['filter2'] ? $state['filter2'] : $GLOBALS['egw_info']['user']['preferences']['tracker']['default_version']; 203 } 204 if (isset($this->trackers[(int)$_GET['tracker']])) 205 { 206 $this->data['tr_tracker'] = $regardInInit['tr_tracker'] = (int)$_GET['tracker']; 207 } 208 // State can have more than one tracker selected, edit has only 1 209 if(is_array($this->data['tr_tracker'])) 210 { 211 $this->data['tr_tracker'] = $regardInInit['tr_tracker'] = (int) array_pop($this->data['tr_tracker']); 212 } 213 } 214 215 216 // Copy 217 if($_GET['tr_id'] && $_GET['makecp']) 218 { 219 $this->copy($this->data); 220 } 221 // initialize and try to merge what we already have 222 if ($needInit) 223 { 224 $this->init($regardInInit); 225 } 226 if ($_GET['no_popup'] || $_GET['nopopup']) $popup = false; 227 228 // check if user has rights to create new entries and fail if not 229 if (!$this->data['tr_id'] && !$this->check_rights($this->field_acl['add'],null,null,null,'add')) 230 { 231 $msg = lang('Permission denied !!!'); 232 if ($popup) 233 { 234 $GLOBALS['egw']->framework->render('<h1 style="color: red;">'.$msg."</h1>\n",null,true); 235 exit(); 236 } 237 else 238 { 239 unset($_GET['tr_id']); // in case it's still set 240 return $this->index(null,$this->data['tr_tracker'],$msg); 241 } 242 } 243 // on resticted trackers, check if the user has read access, OvE, 20071012 244 $restrict = false; 245 if($this->data['tr_id']) 246 { 247 if (!$this->is_staff($this->data['tr_tracker']) && // user has to be staff or 248 !array_intersect($this->data['tr_assigned'], // he or a group he is a member of is assigned 249 array_merge((array)$this->user,$GLOBALS['egw']->accounts->memberships($this->user,true)))) 250 { 251 // if we have group OR creator restrictions 252 if ($this->restrictions[$this->data['tr_tracker']]['creator'] || 253 $this->restrictions[$this->data['tr_tracker']]['group']) 254 { 255 // we need to be creator OR group member 256 if (!($this->restrictions[$this->data['tr_tracker']]['creator'] && 257 $this->data['tr_creator'] == $this->user || 258 $this->restrictions[$this->data['tr_tracker']]['group'] && 259 in_array($this->data['tr_group'], $GLOBALS['egw']->accounts->memberships($this->user,true)))) 260 { 261 $restrict = true; // if not --> no access 262 } 263 } 264 // Check queue access if enabled and that no has access to queue 0 (All) 265 if ($this->enabled_queue_acl_access && !$this->trackers[$this->data['tr_tracker']] && !$this->is_user(0,$this->user)) 266 { 267 $restrict = true; 268 } 269 // Check for specific access 270 if($GLOBALS['egw']->acl->check('A'.$this->data['tr_id'], Acl::READ, 'tracker')) 271 { 272 $restrict = false; 273 } 274 } 275 } 276 if ($restrict) 277 { 278 $msg = lang('Permission denied !!!'); 279 if ($popup) 280 { 281 $GLOBALS['egw']->framework->render('<h1 style="color: red;">'.$msg."</h1>\n",null,false); 282 exit(); 283 } 284 else 285 { 286 unset($_GET['tr_id']); // in case it's still set 287 return $this->index(null,$this->data['tr_tracker'],$msg); 288 } 289 } 290 } 291 else // submitted form 292 { 293 //_debug_array($content); 294 $button = @key($content['button']); unset($content['button']); 295 if ($content['bounties']['bounty']) $button = 'bounty'; unset($content['bounties']['bounty']); 296 $popup = $content['popup']; unset($content['popup']); 297 $own_referer = $content['own_referer']; unset($content['own_referer']); 298 299 $this->data = $content; 300 unset($this->data['bounties']['new']); 301 switch($button) 302 { 303 case 'save': 304 case 'apply': 305 if (is_array($this->data['tr_cc'])) 306 { 307 foreach($this->data['tr_cc'] as $i => $value) 308 { 309 //imap_rfc822 should not be used, but it works reliable here, until we have some regex solution or use horde stuff 310 $addresses = imap_rfc822_parse_adrlist($value, ''); 311 //error_log(__METHOD__.__LINE__.$value.'->'.array2string($addresses[0])); 312 $this->data['tr_cc'][$i]=$addresses[0]->host ? $addresses[0]->mailbox.'@'.$addresses[0]->host : $addresses[0]->mailbox; 313 } 314 $this->data['tr_cc'] = implode(',',$this->data['tr_cc']); 315 } 316 if (!$this->data['tr_id'] && !$this->check_rights($this->field_acl['add'],null,null,null,'add')) 317 { 318 $msg = lang('Permission denied !!!'); 319 break; 320 } 321 322 $readonlys = $this->readonlys_from_acl(); 323 324 // Save Current edition mode preventing mixed types 325 if ($this->data['tr_edit_mode'] == 'html' && !$this->htmledit) 326 { 327 $this->data['tr_edit_mode'] = 'html'; 328 } 329 elseif ($this->data['tr_edit_mode'] == 'ascii' && $this->htmledit) 330 { 331 $this->data['tr_edit_mode'] = 'ascii'; 332 } 333 else 334 { 335 $this->htmledit ? $this->data['tr_edit_mode'] = 'html' : $this->data['tr_edit_mode'] = 'ascii'; 336 } 337 338 if ($this->htmledit && $this->data['tr_id'] && is_array($content['link_to']['to_id'])) 339 { 340 mail_integration::fix_inline_images('tracker', $this->data['tr_id'], $content['link_to']['to_id'], $content['reply_message']); 341 $this->data['reply_message'] = $content['reply_message']; 342 } 343 344 $ret = $this->save(); 345 346 $this->comment_files($this->data['tr_id'], 347 $this->data['replies'][0]['reply_id'], 348 $this->data 349 ); 350 351 if ($ret === false) 352 { 353 $msg = lang('Nothing to save.'); 354 $state = Api\Cache::getSession('tracker', 'index'); 355 Framework::refresh_opener($msg,'tracker',$this->data['tr_id'],'edit'); 356 357 // only change to current tracker, if not all trackers displayed 358 ($state['col_filter']['tr_tracker'] ? '&tracker='.$this->data['tr_tracker'] : '')."';"; 359 } 360 elseif ($ret === 'tr_modifier' || $ret === 'tr_modified') 361 { 362 $msg .= ($msg ? ', ' : '') .lang('Error: the entry has been updated since you opened it for editing!').'<br />'. 363 lang('Copy your changes to the clipboard, %1reload the entry%2 and merge them.','<a href="'. 364 htmlspecialchars(Egw::link('/index.php',array( 365 'menuaction' => 'tracker.tracker_ui.edit', 366 'tr_id' => $this->data['tr_id'], 367 //'referer' => $referer, 368 ))).'">','</a>'); 369 break; 370 } 371 elseif ($ret == 0 && !is_string($ret)) 372 { 373 $msg = lang('Entry saved'); 374 //apply defaultlinks 375 usort($this->all_cats,function($a, $b) 376 { 377 return strcasecmp($a['name'], $b['name']); 378 }); 379 foreach($this->all_cats as $cat) 380 { 381 if (!is_array($data = $cat['data'])) $data = array('type' => $data); 382 //echo "<p>".$this->data['tr_tracker'].": $cat[name] ($cat[id]/$cat[parent]/$cat[main]): ".print_r($data,true)."</p>\n"; 383 384 if ($cat['parent'] == $this->data['tr_tracker'] && $data['type'] != 'tracker' && $data['type']=='project') 385 { 386 if (!Link::get_link('tracker',$this->data['tr_id'],'projectmanager',$data['projectlist'])) 387 { 388 Link::link('tracker',$this->data['tr_id'],'projectmanager',$data['projectlist']); 389 } 390 } 391 } 392 if (is_array($content['link_to']['to_id']) && count($content['link_to']['to_id'])) 393 { 394 Link::link('tracker',$this->data['tr_id'],$content['link_to']['to_id']); 395 396 // Check if we have inline images from mail 397 if($this->htmledit && mail_integration::fix_inline_images('tracker', $this->data['tr_id'], 398 $content['link_to']['to_id'], $content['tr_description'])) 399 { 400 $this->update(array( 401 'tr_description' => $content['tr_description'], 402 )); 403 } 404 405 // check if we have dragged in images and fix their image urls 406 if (Etemplate\Widget\Vfs::fix_html_dragins('tracker', $this->data['tr_id'], 407 $content['link_to']['to_id'], $content['tr_description'])) 408 { 409 $this->update(array( 410 'tr_description' => $content['tr_description'], 411 )); 412 } 413 } 414 $state = Api\Cache::getSession('tracker', 'index'); 415 Framework::refresh_opener($msg, 'tracker',$this->data['tr_id'],'edit'); 416 } 417 else 418 { 419 $msg = lang('Error saving the entry!!!') . "\n" . lang($ret); 420 break; 421 } 422 if ($button == 'apply') 423 { 424 $_GET['tr_id'] = $this->data['tr_id']; 425 return $this->edit($_GET['tr_id'], $msg, $popup); 426 } 427 // fall-through for save 428 case 'cancel': 429 if ($popup) 430 { 431 Framework::window_close(); 432 exit(); 433 } 434 unset($_GET['tr_id']); // in case it's still set 435 if($own_referer && strpos($own_referer,'cd=yes') === false && 436 strpos($own_referer,'tr_id='.$this->data['tr_id']) === FALSE) 437 { 438 // Go back to where you came from 439 Egw::redirect_link($own_referer); 440 } 441 if (Api\Json\Response::isJSONResponse()) 442 { 443 Api\Json\Response::get()->call('egw.open_link','tracker.tracker_ui.index&ajax=true','_self',false,'tracker'); 444 return; 445 } 446 return $this->index(null,$this->data['tr_tracker'],$msg); 447 448 case 'vote': 449 if ($this->cast_vote()) 450 { 451 $msg = lang('Thank you for voting.'); 452 if ($popup) 453 { 454 Framework::refresh_opener($msg, 'tracker',$this->data['tr_id'], 'edit'); 455 } 456 } 457 break; 458 459 case 'bounty': 460 if (!$this->allow_bounties) break; 461 $bounty = $content['bounties']['new']; 462 if (!$this->is_anonymous()) 463 { 464 if (!$bounty['bounty_name']) $bounty['bounty_name'] = $GLOBALS['egw_info']['user']['account_fullname']; 465 if (!$bounty['bounty_email']) $bounty['bounty_email'] = $GLOBALS['egw_info']['user']['account_email']; 466 } 467 if (!$bounty['bounty_amount'] || !$bounty['bounty_name'] || !$bounty['bounty_email']) 468 { 469 $msg = lang('You need to specify amount, donators name AND email address!'); 470 } 471 elseif ($this->save_bounty($bounty)) 472 { 473 $msg = lang('Thank you for setting this bounty.'). 474 ' '.lang('The bounty will NOT be shown, until the money is received.'); 475 array_unshift($this->data['bounties'],$bounty); 476 unset($content['bounties']['new']); 477 } 478 break; 479 480 default: 481 if (!$this->allow_bounties) break; 482 // check delete bounty 483 $id = @key($this->data['bounties']['delete']); 484 if ($id) 485 { 486 unset($this->data['bounties']['delete']); 487 if ($this->delete_bounty($id)) 488 { 489 $msg = lang('Bounty deleted'); 490 foreach($this->data['bounties'] as $n => $bounty) 491 { 492 if ($bounty['bounty_id'] == $id) 493 { 494 unset($this->data['bounties'][$n]); 495 break; 496 } 497 } 498 } 499 else 500 { 501 $msg = lang('Permission denied !!!'); 502 } 503 } 504 else 505 { 506 // check confirm bounty 507 $id = @key($this->data['bounties']['confirm']); 508 if ($id) 509 { 510 unset($this->data['bounties']['confirm']); 511 foreach($this->data['bounties'] as $n => $bounty) 512 { 513 if ($bounty['bounty_id'] == $id) 514 { 515 if ($this->save_bounty($this->data['bounties'][$n])) 516 { 517 $msg = lang('Bounty confirmed'); 518 Framework::refresh_opener($msg, 'tracker',$this->data['tr_id'], 'edit'); 519 } 520 else 521 { 522 $msg = lang('Permission denied !!!'); 523 } 524 break; 525 } 526 } 527 } 528 } 529 break; 530 } 531 } 532 $tr_id = $this->data['tr_id']; 533 if (!($tracker = $this->data['tr_tracker'])) 534 { 535 reset($this->trackers); 536 $tracker = @key($this->trackers); 537 } 538 if (!$readonlys) $readonlys = $this->readonlys_from_acl(); 539 540 $preserv = $content = $this->data; 541 $content['id'] = $tr_id; 542 if ($content['tr_edit_mode'] == 'ascii' && $content['tr_description'] && $readonlys['tr_description']) 543 { 544 // non html view in a readonly htmlarea (div) needs nl2br 545 $content['tr_description'] = htmlspecialchars($content['tr_description']); 546 $tr_editor_mode = 'ascii'; 547 } 548 549 if ($this->allow_bounties) 550 { 551 if (is_array($content['bounties'])) 552 { 553 $total = 0; 554 foreach($content['bounties'] as $bounty) 555 { 556 $total += $bounty['bounty_amount']; 557 // confirmed bounties cant be deleted and need no confirm button 558 $readonlys['delete['.$bounty['bounty_id'].']'] = 559 $readonlys['confirm['.$bounty['bounty_id'].']'] = !$this->is_admin($tracker) || $bounty['bounty_confirmed']; 560 } 561 $content['bounties']['num_bounties'] = count($content['bounties']); 562 array_unshift($content['bounties'],false); // we need the array index to start with 2! 563 array_unshift($content['bounties'],false); 564 $content['bounties']['total'] = $total ? sprintf('%4.2lf',$total) : ''; 565 } 566 $content['bounties']['currency'] = $this->currency; 567 $content['bounties']['is_admin'] = $this->is_admin($tracker); 568 } 569 $statis = $this->get_tracker_stati($tracker); 570 $content += array( 571 'msg' => $msg, 572 'tr_description_mode' => $readonlys['tr_description'], 573 'on_cancel' => $popup ? 'egw(window).close();' : 'egw.open_link("tracker.tracker_ui.index&ajax=true","_self",false,"tracker")', 574 'no_vote' => '', 575 'show_dates' => $this->show_dates, 576 'link_to' => array( 577 'to_id' => $tr_id, 578 'to_app' => 'tracker', 579 ), 580 'status_help' => !$this->pending_close_days ? lang('Pending items never get close automatic.') : 581 lang('Pending items will be closed automatic after %1 days without response.',$this->pending_close_days), 582 'history' => array( 583 'id' => $tr_id, 584 'app' => 'tracker', 585 'status-widgets' => array( 586 'Co' => 'select-percent', 587 'St' => &$statis, 588 'Ca' => 'select-cat', 589 'Tr' => 'select-cat', 590 'Ve' => 'select-cat', 591 'As' => 'select-account', 592 'Cr' => 'select-account', 593 'pr' => array('Public','Private'), 594 'Cl' => 'date-time', 595 'tr_startdate' => 'date-time', 596 'tr_duedate' => 'date-time', 597 'Re' => self::$resolutions + $this->get_tracker_labels('resolution',$tracker), 598 'Gr' => 'select-account', 599 'comment' => array('label','date-time','diff'), 600 ), 601 ), 602 ); 603 if ($this->allow_bounties && !$this->is_anonymous()) 604 { 605 $content['bounties']['user_name'] = $GLOBALS['egw_info']['user']['account_fullname']; 606 $content['bounties']['user_email'] = $GLOBALS['egw_info']['user']['account_email']; 607 } 608 $preserv['popup'] = $popup; 609 $preserv['own_referer'] = $own_referer; 610 611 if (!$tr_id && isset($_REQUEST['link_app']) && isset($_REQUEST['link_id']) && !is_array($content['link_to']['to_id'])) 612 { 613 $link_ids = is_array($_REQUEST['link_id']) ? $_REQUEST['link_id'] : array($_REQUEST['link_id']); 614 foreach(is_array($_REQUEST['link_app']) ? $_REQUEST['link_app'] : array($_REQUEST['link_app']) as $n => $link_app) 615 { 616 $link_id = $link_ids[$n]; 617 if (preg_match('/^[a-z_0-9-]+:[:a-z_0-9-]+$/i',$link_app.':'.$link_id)) // gard against XSS 618 { 619 switch($link_app) 620 { 621 case 'infolog': 622 static $infolog_bo=null; 623 if(!$infolog_bo) $infolog_bo = new infolog_bo(); 624 $infolog = $app_entry = $infolog_bo->read($link_id); 625 $content = array_merge($content, array( 626 'tr_owner' => $infolog['info_owner'], 627 'tr_private' => $infolog['info_access'] == 'private', 628 'tr_summary' => $infolog['info_subject'], 629 'tr_description' => $infolog['info_des'], 630 'tr_cc' => $infolog['info_cc'], 631 'tr_created' => $infolog['info_startdate'] 632 )); 633 634 // Categories are different, no globals. Match by name. 635 $match = array( 636 $infolog_bo->enums['type'][$infolog['info_type']] => array( 637 'field' => 'tr_tracker', 638 'source'=> $this->trackers 639 ), 640 Api\Categories::id2name($infolog['info_cat']) => array( 641 'field' => 'cat_id', 642 'source'=> $this->get_tracker_labels('cat',$tracker) 643 ) 644 ); 645 foreach($match as $info_field => $info) 646 { 647 $content[$info['field']] = array_search($info_field,$info['source']); 648 } 649 650 // Try to match priorities 651 foreach($this->get_tracker_priorities($content['tr_tracker'], $content['cat_id']) as $p => $label) 652 { 653 if(stripos($label, $infolog_bo->enums['priority'][$infolog['info_priority']]) !== false) 654 { 655 $content['tr_priority'] = $p; 656 break; 657 } 658 } 659 660 // Add responsible as participant - filtered later 661 foreach($infolog['info_responsible'] as $responsible) { 662 $content['tr_assigned'][] = $responsible; 663 } 664 665 // Copy infolog's links 666 foreach(Link::get_links('infolog',$link_id) as $copy_link) 667 { 668 Link::link('tracker', $content['link_to']['to_id'], $copy_link['app'], $copy_link['id'],$copy_link['remark']); 669 } 670 break; 671 672 } 673 // Copy same custom fields 674 $_cfs = Api\Storage\Customfields::get('tracker'); 675 $link_app_cfs = Api\Storage\Customfields::get($link_app); 676 foreach($_cfs as $name => $settings) 677 { 678 unset($settings); 679 if($link_app_cfs[$name]) $content['#'.$name] = $app_entry['#'.$name]; 680 } 681 Link::link('tracker',$content['link_to']['to_id'],$link_app,$link_id); 682 } 683 } 684 } 685 // options for creator selectbox (allways add current selected user!) 686 if ($readonlys['tr_creator']) 687 { 688 $creators = array(); 689 } 690 else 691 { 692 $creators = $this->get_staff($tracker,0,'usersANDtechnicians'); 693 } 694 if ($content['tr_creator'] && !isset($creators[$content['tr_creator']])) 695 { 696 $creators[$content['tr_creator']] = Api\Accounts::username($content['tr_creator']); 697 } 698 699 700 $account_select_pref = $GLOBALS['egw_info']['user']['preferences']['common']['account_selection']; 701 $sel_options = array( 702 'tr_tracker' => &$this->trackers, 703 'cat_id' => $this->get_tracker_labels('cat',is_array($tracker) && count($tracker) == 1?$tracker[0]:$tracker, $default_category), 704 'tr_version' => $this->get_tracker_labels('version',$tracker), 705 'tr_priority' => $this->get_tracker_priorities($tracker,$content['cat_id'], true, $default_priority), 706 'tr_status' => &$statis, 707 'tr_resolution' => $this->get_tracker_labels('resolution',$tracker), 708 'tr_assigned' => $account_select_pref == 'none' ? array() : $this->get_staff($tracker,$this->allow_assign_groups,$this->allow_assign_users?'usersANDtechnicians':'technicians'), 709 'tr_creator' => $creators, 710 // New items default to primary group is no right to change the group 711 'tr_group' => $account_select_pref == 'none' ? array() : $this->get_groups(!$this->check_rights($this->field_acl['tr_group'],$tracker,null,null,'tr_group') && !$this->data['tr_id']), 712 'canned_response' => $this->get_tracker_labels('response'), 713 ); 714 715 // Keep updating category & priority to default until it's saved 716 if(!$tr_id) 717 { 718 $content['cat_id'] = $regardInInit['cat_id'] ? $regardInInit['cat_id'] : ($default_category ? (int)$default_category : $this->data['cat_id']); 719 $content['tr_priority'] = $default_priority ? (int)$this->data['tr_priority'] : $this->data['tr_priority']; 720 } 721 722 foreach($this->field2history as $field => $status) 723 { 724 $sel_options['status'][$status] = $this->field2label[$field]; 725 } 726 $sel_options['status']['xb'] = 'Bounty deleted'; 727 $sel_options['status']['bo'] = 'Bounty set'; 728 $sel_options['status']['Bo'] = 'Bounty confirmed'; 729 $sel_options['status']['comment'] = 'Comment'; 730 731 $readonlys['tabs'] = array( 732 'comments' => !$tr_id || !$content['num_replies'], 733 'add_comment' => !$tr_id || $readonlys['reply_message'], 734 'history' => !$tr_id, 735 'bounties' => !$this->allow_bounties, 736 'custom' => !Api\Storage\Customfields::get('tracker', false, $content['tr_tracker']), 737 ); 738 // Make link_to readonly if the user has no EDIT access 739 $readonlys['link_to'] = !$this->file_access($tr_id, Acl::EDIT); 740 741 if ($tr_id && $readonlys['reply_message']) 742 { 743 $readonlys['button[save]'] = true; 744 } 745 if (!$tr_id && $readonlys['add']) 746 { 747 $msg = lang('Permission denied !!!'); 748 $readonlys['button[save]'] = true; 749 } 750 // Assigned & group are not select-account widgets, so we need to apply 751 // none preference (no value, no options) here. 752 if($account_select_pref == 'none') 753 { 754 $readonlys['tr_assigned'] = true; 755 $readonlys['tr_group'] = true; 756 } 757 if (!$this->allow_voting || !$tr_id || $readonlys['vote'] || ($voted = $this->check_vote())) 758 { 759 $readonlys['button[vote]'] = true; 760 if ($tr_id && $this->allow_voting) 761 { 762 $content['no_vote'] = is_int($voted) ? lang('You voted %1.', 763 date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat']. 764 ($GLOBALS['egw_info']['user']['preferences']['common']['timeformat']==12?' h:i a':' H:i'),$voted)) : 765 lang('You need to login to vote!'); 766 } 767 } 768 if ($readonlys['canned_response']) 769 { 770 $content['no_canned'] = true; 771 } 772 $content['no_links'] = $readonlys['link_to']; 773 $content['bounties']['no_set_bounties'] = $readonlys['bounty']; 774 //error_log(__METHOD__.__LINE__.':'.is_array($tracker)?$tracker[0]:$tracker); 775 $what = ($tracker && isset($this->trackers[(is_array($tracker)?$tracker[0]:$tracker)]) ? $this->trackers[(is_array($tracker)?$tracker[0]:$tracker)] : lang('Tracker')); 776 $GLOBALS['egw_info']['flags']['app_header'] = $tr_id ? lang('Edit %1',$what) : lang('New %1',$what); 777 778 $tpl = $this->template ? $this->template : new Etemplate(); 779 $tpl->read('tracker.edit'); 780 // use a type-specific template (tracker.edit.xyz), if one exists, otherwise fall back to the generic one 781 if (!$tpl->read('tracker.edit'.(isset($this->trackers[(is_array($tracker)?$tracker[0]:$tracker)])?'.'.trim($this->trackers[(is_array($tracker)?$tracker[0]:$tracker)]):''))) 782 { 783 $tpl->read('tracker.edit'); 784 } 785 786 if ($this->tracker_has_cat_specific_priorities($tracker)) 787 { 788 $tpl->set_cell_attribute('cat_id','onchange','widget.getInstanceManager().submit(null,false,true); return false;'); 789 } 790 // No notifications needs label hidden too 791 if($readonlys['no_notifications']) 792 { 793 $tpl->set_cell_attribute('no_notifications', 'disabled', true); 794 } 795 796 if ($content['tr_assigned'] && !is_array($content['tr_assigned'])) 797 { 798 $content['tr_assigned'] = explode(',',$content['tr_assigned']); 799 } 800 if (is_array($content['tr_assigned']) && count($content['tr_assigned']) > 1) 801 { 802 $tpl->set_cell_attribute('tr_assigned','size','3+'); 803 } 804 $tpl->set_cell_attribute('tr_description', 'mode', $tr_editor_mode); 805 $tpl->set_cell_attribute('reply_message', 'mode',$tr_editor_mode); 806 807 $this->setup_comments($tpl, $content, $preserv); 808 809 if (!empty($content['tr_cc'])&&!is_array($content['tr_cc']))$content['tr_cc'] = explode(',',$content['tr_cc']); 810 return $tpl->exec('tracker.tracker_ui.edit',$content,$sel_options,$readonlys,$preserv,$popup ? 2 : 0); 811 } 812 813 /** 814 * Set up the template / content for editable comments 815 * 816 * Editable widgets, context menu actions 817 */ 818 protected function setup_comments(Etemplate &$tpl, Array &$content, Array &$preserve) 819 { 820 // Comment visibility 821 if (is_array($content['replies'])) 822 { 823 foreach($content['replies'] as $key => &$reply) 824 { 825 if(!$reply) 826 { 827 unset($content['replies'][$key]); 828 continue; 829 } 830 if (isset($content['replies'][$key]['reply_visible'])) { 831 $reply['reply_visible_class'] = 'reply_visible_'.$reply['reply_visible']; 832 if($this->check_rights($this->field_acl['edit_reply'], null, null, null, 'edit_reply') || 833 $reply['reply_creator'] == $GLOBALS['egw_info']['user']['account_id'] && $this->check_rights($this->field_acl['edit_own_reply'], null, null, null, 'edit_own_reply')) 834 { 835 $reply['class'] = 'editable'; 836 } 837 } 838 } 839 } 840 if ($content['num_replies'] && (!array_key_exists(0,$content['replies']) || $content['replies'][0])) 841 { 842 array_unshift($content['replies'],false); 843 array_unshift($preserve['replies'],false); 844 } // need array index starting with 1! 845 $content['no_comment_visibility'] = !$this->check_rights(TRACKER_ADMIN|TRACKER_TECHNICIAN|TRACKER_ITEM_ASSIGNEE,null,null,null,'no_comment_visibility') || 846 !$this->allow_restricted_comments; 847 848 // Toggle editable comments 849 $content['editable_comments'] = $this->check_rights($this->field_acl['edit_reply'], null, null, null, 'edit_reply') || 850 $this->check_rights($this->field_acl['edit_own_reply'], null, null, null, 'edit_own_reply') 851 ? 'editable' : ''; 852 853 // Context menu 854 $tpl->set_cell_attribute('replies', 'actions', array( 855 'replies_edit' => array( 856 'icon' => 'edit', 857 'caption' => 'Edit', 858 'allowOnMultiple' => false, 859 'onExecute' => 'javaScript:app.tracker.reply_edit', 860 'enableClass' => 'editable', 861 'hideOnDisabled' => true 862 ), 863 'replies_files' => array( 864 'icon' => 'filemanager/navbar', 865 'caption' => 'Files', 866 'allowOnMultiple' => false, 867 'onExecute' => 'javaScript:app.tracker.reply_files', 868 'enableClass' => 'editable', 869 'hideOnDisabled' => true 870 ), 871 )); 872 } 873 874 /** 875 * query rows for the nextmatch widget 876 * 877 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter' 878 * For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class. 879 * @param array &$rows returned rows/competitions 880 * @param array &$readonlys eg. to disable buttons based on Acl 881 * @return int total number of rows 882 */ 883 function get_rows(&$query_in,&$rows,&$readonlys) 884 { 885 if (!$this->allow_voting && $query_in['order'] == 'votes' || // in case the tracker-config changed in that session 886 !$this->allow_bounties && $query_in['order'] == 'bounties') $query_in['order'] = 'tr_id'; 887 888 $query = $query_in; 889 $old_query = Api\Cache::getSession('tracker',$query['session_for'] ? $query['session_for'] : 'index'.($query_in['only_tracker'] ? '-'.$query_in['only_tracker'] : '')); 890 if (!$query['csv_export']) // do not store query for csv-export in session 891 { 892 Api\Cache::setSession('tracker',$query['session_for'] ? $query['session_for'] : 'index'.($query_in['only_tracker'] ? '-'.$query_in['only_tracker'] : ''), 893 array_diff_key ($query, array_flip(array('rows','actions','action_links','placeholder_actions')))); 894 } 895 // save the state of the index page (filters) in the user prefs 896 // need to save state, before resolving diverse col-filters, eg. to all group-members or sub-cats 897 $state = serialize(array( 898 'cat_id' => $query['cat_id'], // cat 899 'filter' => $query['filter'], // dates 900 'filter2' => $query['filter2'], // version 901 'order' => $query['order'], 902 'sort' => $query['sort'], 903 'num_rows' => $query['num_rows'], 904 'col_filter' => array( 905 'tr_tracker' => $query['col_filter']['tr_tracker'], 906 'tr_creator' => $query['col_filter']['tr_creator'], 907 'tr_assigned' => $query['col_filter']['tr_assigned'], 908 'tr_status' => $query['col_filter']['tr_status'], 909 ), 910 )); 911 if (!$query['csv_export'] && !$query['action'] && $GLOBALS['egw']->session->session_flags != 'A' && // store the current state of non-anonymous users in the prefs 912 $state != $GLOBALS['egw_info']['user']['preferences']['tracker']['index_state']) 913 { 914 //$msg .= "save the index state <br>"; 915 $GLOBALS['egw']->preferences->add('tracker','index_state',$state); 916 // save prefs, but do NOT invalid the cache (unnecessary) 917 $GLOBALS['egw']->preferences->save_repository(false,'user',false); 918 } 919 920 $GLOBALS['egw']->session->commit_session(); 921 $tracker = $query['col_filter']['tr_tracker']; 922 923 // Re-do actions on tracker or category change 924 if($old_query['col_filter']['tr_tracker'] != $tracker || 925 $old_query['cat_id'] != $query['cat_id']) 926 { 927 $query_in['actions'] = $this->get_actions( 928 is_array($tracker) ? $tracker[0] : $tracker, 929 is_array($query['cat_id']) ? $query['cat_id'][0] : $query['cat_id'] 930 ); 931 } 932 933 // handle action and linked filter (show only entries linked to a certain other entry) 934 $link_filters = array(); 935 $links = array(); 936 if ($query['col_filter']['linked']) 937 { 938 $link_filters['linked'] = $query['col_filter']['linked']; 939 $links['linked'] = array(); 940 unset($query['col_filter']['linked']); 941 } 942 if($query['action'] && in_array($query['action'], array_keys($GLOBALS['egw_info']['apps'])) && $query['action_id']) 943 { 944 $link_filters['action'] = array('app'=>$query['action'], 'id' => $query['action_id']); 945 $links['action'] = array(); 946 } 947 foreach($link_filters as $key => $link) 948 { 949 if(!is_array($link)) 950 { 951 // Legacy string style 952 list($app,$id) = explode(':',$link); 953 } 954 else 955 { 956 // Full info 957 $app = $link['app']; 958 $id = $link['id']; 959 } 960 if(!is_array($id)) $id = explode(',',$id); 961 if (!($linked = Link::get_links_multiple($app,$id,true,'tracker'))) 962 { 963 $rows = array(); // no entries linked to selected link --> no rows to return 964 $this->get_rows_options($rows, $tracker); 965 return 0; 966 } 967 968 969 foreach($linked as $infos) 970 { 971 $links[$key] = array_merge($links[$key],$infos); 972 } 973 $links[$key] = array_unique($links[$key]); 974 if($key == 'linked') 975 { 976 $linked = array('app' => $app, 'id' => $id, 'title' => (count($id) == 1 ? Link::title($app, $id) : lang('multiple'))); 977 } 978 } 979 if(count($links)) 980 { 981 $query['col_filter']['tr_id'] = count($links) > 1 ? call_user_func_array('array_intersect', $links) : $links[$key]; 982 } 983 984 // Explode multiples into array 985 if(!is_array($tracker) && strpos($tracker,',') !== false) 986 { 987 $tracker = $query['col_filter']['tr_tracker'] = explode(',',$query['col_filter']['tr_tracker']); 988 } 989 if (!($query['col_filter']['cat_id'] = $query['cat_id'])) unset($query['col_filter']['cat_id']); 990 if (!($query['col_filter']['tr_version'] = $query['filter2'])) unset($query['col_filter']['tr_version']); 991 992 if (!($query['col_filter']['tr_creator'])) unset($query['col_filter']['tr_creator']); 993 994 if ($query['col_filter']['tr_assigned'] < 0) // resolve groups with it's members 995 { 996 $query['col_filter']['tr_assigned'] = $GLOBALS['egw']->accounts->members($query['col_filter']['tr_assigned'],true); 997 $query['col_filter']['tr_assigned'][] = $query_in['col_filter']['tr_assigned']; 998 } 999 elseif($query['col_filter']['tr_assigned'] === 'not') 1000 { 1001 $query['col_filter']['tr_assigned'] = null; 1002 } 1003 elseif(!$query['col_filter']['tr_assigned']) 1004 { 1005 unset($query['col_filter']['tr_assigned']); 1006 } 1007 1008 if (empty($query['col_filter']['tr_tracker'])) 1009 { 1010 $tracker = $query['col_filter']['tr_tracker'] = array_keys($this->trackers); 1011 } 1012 1013 // Get list of currently displayed trackers, so we can get all valid statuses 1014 if ($tracker) 1015 { 1016 $trackers = is_array($tracker) ? $tracker : array($tracker); 1017 } 1018 else 1019 { 1020 $trackers = array(); 1021 } 1022 1023 //echo "<p align=right>uitracker::get_rows() order='$query[order]', sort='$query[sort]', search='$query[search]', start=$query[start], num_rows=$query[num_rows], col_filter=".print_r($query['col_filter'],true)."</p>\n"; 1024 $total = parent::get_rows($query,$rows,$readonlys,$this->allow_voting||$this->allow_bounties); // true = count votes and/or bounties 1025 $prio_labels = $prio_tracker = $prio_cat = null; 1026 foreach($rows as $n => $row) 1027 { 1028 // Check if this is a new (unseen) ticket for the current user 1029 if (self::seen($row, false)) 1030 { 1031 $rows[$n]['seen_class'] = 'tracker_seen'; 1032 } 1033 else 1034 { 1035 $rows[$n]['seen_class'] = 'tracker_unseen'; 1036 } 1037 1038 // Check rights for changing group via context menu, action looks for the CSS class 1039 if($this->check_rights($this->field_acl['tr_group'], null, $row)) 1040 { 1041 $rows[$n]['class'] .= 'group_action'; 1042 } 1043 switch ($this->enabled_color_code_for) 1044 { 1045 case 'tracker': 1046 $rows[$n]['enabled_color_code'] = $row['tr_tracker']; 1047 break; 1048 case 'cat': 1049 $rows[$n]['enabled_color_code'] = $row['cat_id']; 1050 break; 1051 case 'version': 1052 $rows[$n]['enabled_color_code'] = $row['tr_version']; 1053 break; 1054 default: 1055 } 1056 $trackers[] = $row['tr_tracker']; 1057 1058 // show the right tracker and/or cat specific priority label 1059 if ($row['tr_priority']) 1060 { 1061 if (is_null($prio_labels) || $this->priorities && ($row['tr_tracker'] != $prio_tracker || $row['cat_id'] != $prio_cat)) 1062 { 1063 $prio_labels = $this->get_tracker_priorities($prio_tracker=$row['tr_tracker'],$prio_cat = $row['cat_id']); 1064 if ($prio_labels === self::$stock_priorities) // show only the numbers for the stock priorities 1065 { 1066 $prio_labels = array_combine(array_keys(self::$stock_priorities),array_keys(self::$stock_priorities)); 1067 } 1068 } 1069 $rows[$n]['prio_label'] = $prio_labels[$row['tr_priority']]; 1070 } 1071 if (isset($rows[$n]['tr_description'])) 1072 { 1073 if($rows[$n]['tr_edit_mode'] == 'ascii') 1074 { 1075 $rows[$n]['tr_description'] = htmlspecialchars($rows[$n]['tr_description']); 1076 } 1077 $rows[$n]['tr_description'] = nl2br(trim($rows[$n]['tr_description'])); 1078 } 1079 if ($row['overdue'] && !$row['tr_closed']) $rows[$n]['overdue_class'] = 'tracker_overdue'; 1080 if ($row['bounties']) $rows[$n]['currency'] = $this->currency; 1081 1082 if (isset($GLOBALS['egw_info']['user']['apps']['timesheet'])) 1083 { 1084 unset($links); 1085 if (($links = Link::get_links('tracker',$row['tr_id'])) && 1086 isset($GLOBALS['egw_info']['user']['apps']['timesheet'])) 1087 { 1088 // loop through all links of the entries 1089 $timesheets = array(); 1090 foreach ($links as $link) 1091 { 1092 if ($link['app'] == 'projectmanager') 1093 { 1094 //$info['pm_id'] = $link['id']; 1095 } 1096 if ($link['app'] == 'timesheet') $timesheets[] = $link['id']; 1097 } 1098 if (isset($GLOBALS['egw_info']['user']['apps']['timesheet'])) 1099 { 1100 $sum = ExecMethod('timesheet.timesheet_bo.sum',$timesheets); 1101 $rows[$n]['tr_sum_timesheets'] = $sum['duration']; 1102 } 1103 } 1104 } 1105 // do NOT display public tickets with "No", just display "Yes" for private ticktes 1106 if ((string)$row['tr_private'] === '0') $rows[$n]['tr_private'] = ''; 1107 1108 //_debug_array($rows[$n]); 1109 //echo "<p>".$this->trackers[$row['tr_tracker']]."</p>"; 1110 $id=$row['tr_id']; 1111 } 1112 1113 $this->get_rows_options($rows,$tracker,$trackers); 1114 1115 // disable start date / due date column, if disabled in config 1116 if(!$this->show_dates) 1117 { 1118 $rows['no_tr_startdate_tr_duedate'] = true; 1119 } 1120 1121 return $total; 1122 } 1123 1124 /** 1125 * Selectbox options vary depending on the selected tracker. 1126 * 1127 * @param Array $rows List of rows, we'll add the sel_options in 1128 * @param String[] $tracker List of tracker IDs 1129 */ 1130 protected function get_rows_options(&$rows, $selected_trackers, $visible_trackers=array()) 1131 { 1132 if(!is_array($selected_trackers) && strpos($selected_trackers,',') !== false) 1133 { 1134 $tracker = explode(',',$selected_trackers); 1135 } 1136 else 1137 { 1138 $tracker = (Array)$selected_trackers; 1139 } 1140 $rows['sel_options']['tr_assigned'] = array('not' => lang('Not assigned')); 1141 1142 // Add allowed staff 1143 foreach((array)$tracker as $tr_id) 1144 { 1145 $rows['sel_options']['tr_assigned'] += $this->get_staff($tr_id,2,$this->allow_assign_users?'usersANDtechnicians':'technicians'); 1146 } 1147 $rows['sel_options']['assigned'] = $rows['sel_options']['tr_assigned']; // For context menu popup 1148 unset($rows['sel_options']['assigned']['not']); 1149 1150 $cats =array('' => lang('All categories')); 1151 $versions = $resolutions = $statis = array(); 1152 foreach((array)$tracker as $tr_id) 1153 { 1154 $versions += $this->get_tracker_labels('version',$tr_id); 1155 $cats += $this->get_tracker_labels('cat',$tr_id); 1156 $resolutions += $this->get_tracker_labels('resolution',$tr_id); 1157 $statis += $this->get_tracker_stati($tr_id); 1158 } 1159 1160 $trackers = array_unique($visible_trackers); 1161 if($trackers) 1162 { 1163 foreach($trackers as $tracker_id) 1164 { 1165 $statis += $this->get_tracker_stati($tracker_id); 1166 $resolutions += $this->get_tracker_labels('resolution',$tracker_id); 1167 } 1168 } 1169 1170 $rows['sel_options']['tr_status'] = $this->filters+$statis; 1171 $rows['sel_options']['cat_id'] = $cats; 1172 $rows['sel_options']['filter2'] = array(lang('All versions'))+$versions; 1173 $rows['sel_options']['tr_version'] =& $versions; 1174 $rows['sel_options']['tr_resolution'] =& $resolutions; 1175 1176 $rows['is_admin'] = $this->is_admin($tracker); 1177 if ($this->is_admin($tracker)) 1178 { 1179 $rows['sel_options']['canned_response'] = $this->get_tracker_labels('response',$tracker); 1180 $rows['sel_options']['tr_status_admin'] =& $statis; 1181 } 1182 $rows['no_votes'] = !$this->allow_voting; 1183 if (!$this->allow_voting) 1184 { 1185 $query_in['options-selectcols']['votes'] = false; 1186 } 1187 $rows['no_bounties'] = !$this->allow_bounties; 1188 if (!$this->allow_bounties) 1189 { 1190 $query_in['options-selectcols']['bounties'] = false; 1191 } 1192 1193 $rows['no_cat_id'] = !!$rows['col_filter']['cat_id']; 1194 1195 // enable tracker column if all trackers are shown 1196 $rows['no_tr_tracker'] = ($tracker && count($tracker) == 1); 1197 } 1198 1199 /** 1200 * Hook for timesheet to set some extra data and links 1201 * 1202 * @param array $data 1203 * @param int $data[id] tracker_id 1204 * @return array with key => value pairs to set in new timesheet and link_app/link_id arrays 1205 */ 1206 function timesheet_set($data) 1207 { 1208 $set = array(); 1209 if ((int)$data['id'] && ($ticket = $this->read($data['id']))) 1210 { 1211 // Timesheet and files are always excluded 1212 $excluded_apps = array('timesheet',Link::VFS_APPNAME) + $this->exclude_app_on_timesheetcreation; 1213 1214 //error_log(__METHOD__.__LINE__.$this->exclude_app_on_timesheetcreation); 1215 foreach(Link::get_links('tracker',$ticket['tr_id'],'','link_lastmod DESC',true) as $link) 1216 { 1217 if (!in_array($link['app'], $excluded_apps)) 1218 { 1219 $set['link_app'][] = $link['app']; 1220 $set['link_id'][] = $link['id']; 1221 } 1222 } 1223 } 1224 return $set; 1225 } 1226 1227 /** 1228 * Hook for InfoLog to set some extra data and links 1229 * 1230 * @param array $data 1231 * @param int $data[id] tracker_id 1232 * @return array with key => value pairs to set in new infolog and link_app/link_id arrays 1233 */ 1234 function infolog_set($data) 1235 { 1236 if (!($tracker = $this->read($data['id']))) 1237 { 1238 return array(); 1239 } 1240 $set = array( 1241 'info_subject' => $tracker['tr_summary'], 1242 'info_des' => $tracker['tr_description'], 1243 'info_contact' => 'tracker:'.$tracker['tr_id'], 1244 ); 1245 // copy links 1246 foreach(Link::get_links('tracker',$tracker['tr_id'],'','link_lastmod DESC',true) as $link) 1247 { 1248 $set['link_app'][] = $link['app']; 1249 $set['link_id'][] = $link['id']; 1250 1251 // prefer addressbook or projectmanager link as primary contact over default of this ticket 1252 if (in_array($link['app'], array('addressbook','projectmanager')) && 1253 strpos($set['info_contact'], 'addressbook:') !== 0) 1254 { 1255 $set['info_contact'] = $link['app'].':'.$link['id']; 1256 } 1257 } 1258 // copy same named customfields 1259 foreach(Api\Storage\Customfields::get('infolog') as $name => $nul) 1260 { 1261 unset($nul); 1262 if(array_key_exists('#'.$name, $tracker)) 1263 { 1264 $set['#'.$name] = $tracker['#'.$name]; 1265 } 1266 } 1267 return $set; 1268 } 1269 /** 1270 * Check if a ticket has already been seen 1271 * 1272 * @param array $data =null Ticket data 1273 * @param boolean $update =false Set ticket as seen when true 1274 * @param boolean $been_seen =true Mark the ticket as seen/unseen by current user 1275 * @return boolean true=seen before false=new ticket 1276 */ 1277 function seen(&$data, $update=false, $been_seen = true) 1278 { 1279 $seen = array(); 1280 if ($data['tr_seen']) $seen = unserialize($data['tr_seen']); 1281 if ($update === false) 1282 { 1283 return in_array($this->user, $seen); 1284 } 1285 if($been_seen) 1286 { 1287 $seen[] = $this->user; 1288 } 1289 else 1290 { 1291 $key = array_search($this->user,$seen); 1292 if($key !== false) 1293 { 1294 unset($seen[$key]); 1295 } 1296 } 1297 $this->db->update('egw_tracker', array('tr_seen' => serialize(array_unique($seen))), 1298 array('tr_id' => $data['tr_id']),__LINE__,__FILE__,'tracker'); 1299 return false; // This time still false... 1300 } 1301 1302 /** 1303 * Show a tracker 1304 * 1305 * @param array $content =null eTemplate content 1306 * @param int $tracker =null id of tracker 1307 * @param string $msg ='' 1308 * @param int $only_tracker =null show only the given tracker and not tracker-selection 1309 * @param boolean $return_html =false if set to true, html content returned 1310 * @return string html-content, if sitemgr otherwise null 1311 */ 1312 function index($content=null,$tracker=null,$msg='',$only_tracker=null, $return_html=false) 1313 { 1314 //_debug_array($this->trackers); 1315 if (!is_array($content)) 1316 { 1317 if ($_GET['tr_id']) 1318 { 1319 if (!$this->read($_GET['tr_id'])) 1320 { 1321 $msg = lang('Tracker item not found !!!'); 1322 } 1323 else 1324 { 1325 return $this->edit(null,'',false); // false = use no popup 1326 } 1327 } 1328 if (!$msg && $_GET['msg']) $msg = $_GET['msg']; 1329 if ($only_tracker && isset($this->trackers[$only_tracker])) 1330 { 1331 $tracker = $only_tracker; 1332 } 1333 else 1334 { 1335 $only_tracker = null; 1336 } 1337 // if there is no tracker specified, try the tracker submitted 1338 if (!$tracker && (int)$_GET['tracker']) $tracker = $_GET['tracker']; 1339 // if there is still no tracker, use the last tracker that was applied and saved to/with the view with the appsession 1340 if (!$tracker && ($state= Api\Cache::getSession('tracker','index'.($only_tracker ? '-'.$only_tracker : '')))) 1341 { 1342 $tracker= is_array($state['col_filter']['tr_tracker']) ? 1343 $state['col_filter']['tr_tracker'][0] : $state['col_filter']['tr_tracker']; 1344 } 1345 } 1346 else 1347 { 1348 $only_tracker = $content['only_tracker']; unset($content['only_tracker']); 1349 $tracker = $content['nm']['col_filter']['tr_tracker']; 1350 $this->called_by = $content['called_by']; unset($content['called_by']); 1351 1352 if (is_array($content) && isset($content['nm']['rows']['document'])) // handle insert in default document button like an action 1353 { 1354 $id = @key($content['nm']['rows']['document']); 1355 $content['nm']['action'] = 'document'; 1356 $content['nm']['selected'] = array($id); 1357 } 1358 if ($content['admin_popup'] && $content['nm']['action'] == 'admin') 1359 { 1360 $content['nm']['action'] = $content['admin_popup']; 1361 } 1362 // Clear multiple action popup 1363 unset($content['admin']); 1364 1365 if($content['nm']['action']) 1366 { 1367 if (!count($content['nm']['selected']) && !$content['nm']['select_all']) 1368 { 1369 $msg = lang('You need to select some entries first'); 1370 } 1371 else 1372 { 1373 // Some processing to add values in for links and cats 1374 $multi_action = $content['nm']['action']; 1375 // Action has an additional action - add / delete, etc. Buttons named <multi-action>_action[action_name] 1376 if(in_array($multi_action, array('link', 'assigned','group'))) 1377 { 1378 $action = $content[$multi_action.'_popup']; 1379 $content['nm']['action'] .= '_' . key($action[$multi_action . '_action']); 1380 1381 // Action handling function wants a single string value, so mush it together 1382 if(is_array($action[$multi_action])) 1383 { 1384 if($multi_action == 'link') 1385 { 1386 $action[$multi_action] = $action[$multi_action]['app'] . ':' . $action[$multi_action]['id']; 1387 } 1388 else 1389 { 1390 $action[$multi_action] = implode(',',$action[$multi_action]); 1391 } 1392 } 1393 $content['nm']['action'] .= '_' . $action[$multi_action]; 1394 unset($content[$multi_action]); 1395 unset($content[$multi_action.'_popup']); 1396 } 1397 $success = $failed = $action_msg = null; 1398 if ($this->action($content['nm']['action'],$content['nm']['selected'],$content['nm']['select_all'], 1399 $success,$failed,$action_msg,'index',$msg,$content['nm']['checkboxes']['no_notifications'])) 1400 { 1401 $msg .= lang('%1 entries %2',$success,$action_msg); 1402 } 1403 else 1404 { 1405 if(is_null($msg) || $msg == '') 1406 { 1407 $msg = lang('%1 entries %2, %3 failed because of insufficent rights !!!',$success,$action_msg,$failed); 1408 } 1409 } 1410 } 1411 } 1412 } 1413 1414 if (!$tracker) $tracker = $content['nm']['col_filter']['tr_tracker']; 1415 $sel_options = array( 1416 'tr_tracker' => $this->trackers, 1417 'tr_status' => $this->filters + $this->get_tracker_stati($tracker), 1418 'tr_priority' => $this->get_tracker_priorities($tracker,$content['cat_id']), 1419 'tr_resolution' => $this->get_tracker_labels('resolution',$tracker), 1420 // Still need to provide options for the column filter 1421 'tr_private' => array('No', 'Yes'), 1422 ); 1423 if (($escalations = ExecMethod2('tracker.tracker_escalations.query_list','esc_title','esc_id'))) 1424 { 1425 $sel_options['esc_id']['already escalated'] = $escalations; 1426 foreach($escalations as $esc_id => $label) 1427 { 1428 $sel_options['esc_id']['matching filter']['-'.$esc_id] = $label; 1429 } 1430 } 1431 // Merge print 1432 if ($GLOBALS['egw_info']['user']['preferences']['tracker']['document_dir']) 1433 { 1434 $documents = tracker_merge::get_documents($GLOBALS['egw_info']['user']['preferences']['tracker']['document_dir']); 1435 if($documents) 1436 { 1437 $sel_options['action'][lang('Insert in document').':'] = $documents; 1438 } 1439 } 1440 1441 if (!is_array($content)) $content = array(); 1442 $content['nm'] = Api\Cache::getSession('tracker', $this->called_by ? $this->called_by : 'index'.($only_tracker ? '-'.$only_tracker : '')); 1443 $content['msg'] = $msg; 1444 $content['status_help'] = !$this->pending_close_days ? lang('Pending items never get close automatic.') : 1445 lang('Pending items will be closed automatic after %1 days without response.',$this->pending_close_days); 1446 1447 if (!is_array($content['nm']) || !$content['nm']['get_rows']) 1448 { 1449 $date_filters = array(lang('Date filter')); 1450 foreach(array_keys($this->date_filters) as $name) 1451 { 1452 $date_filters[$name] = lang($name); 1453 } 1454 $date_filters['custom'] = lang('custom'); 1455 $content['nm'] = array( 1456 'get_rows' => 'tracker.tracker_ui.get_rows', 1457 'cat_is_select' => 'no_lang', 1458 'filter' => 0, // all 1459 'options-filter' => $date_filters, 1460 'filter_onchange' => "app.tracker.filter_change();", 1461 //'filter_label' => lang('Date filter'), 1462 'filter_no_lang'=> true, 1463 'filter2' => 0, // all 1464 'filter2_tags' => true, 1465 //'filter2_label' => lang('Version'), 1466 'filter2_no_lang'=> true, 1467 'order' => $this->allow_bounties ? 'bounties' : ($this->allow_voting ? 'votes' : 'tr_id'),// IO name of the column to sort after (optional for the sortheaders) 1468 'sort' => 'DESC',// IO direction of the sort: 'ASC' or 'DESC' 1469 'options-tr_assigned' => array('not' => lang('Noone')), 1470 'col_filter' => array( 1471 'tr_status' => 'not-closed', // default filter: not closed 1472 ), 1473 'only_tracker' => $only_tracker, 1474 'default_cols' => '!esc_id,legacy_actions,tr_summary_tr_description,tr_resolution,tr_completion,tr_sum_timesheets,votes,bounties', 1475 'row_id' => 'tr_id', 1476 'row_modified' => 'tr_modified', 1477 'placeholder_actions' => array('add') 1478 ); 1479 switch($this->enabled_color_code_for) 1480 { 1481 case 'cat': 1482 $content['nm']['cat_id_class'] = 'cat_'; 1483 break; 1484 case 'version': 1485 $content['nm']['filter2_class'] = 'cat_'; 1486 break; 1487 default: 1488 } 1489 // use the state of the last session stored in the user prefs 1490 if (!$this->called_by && ($state = @unserialize($GLOBALS['egw_info']['user']['preferences']['tracker']['index_state']))) 1491 { 1492 unset($state['header_left']); unset($state['header_right']); 1493 $content['nm'] = array_merge($content['nm'],$state); 1494 $tracker = $content['nm']['col_filter']['tr_tracker']; 1495 } 1496 elseif (!$this->called_by && !$tracker) 1497 { 1498 reset($this->trackers); 1499 $tracker = @key($this->trackers); 1500 } 1501 // disable times column, if no timesheet rights 1502 if (!isset($GLOBALS['egw_info']['user']['apps']['timesheet'])) 1503 { 1504 $content['nm']['options-selectcols']['tr_sum_timesheets'] = false; 1505 } 1506 // disable start date / due date column, if disabled in config 1507 if(!$this->show_dates) 1508 { 1509 // Need to set each field so parser takes the whole column 1510 $content['nm']['options-selectcols']['tr_startdate'] = false; 1511 $content['nm']['options-selectcols']['tr_duedate'] = false; 1512 } 1513 $content['nm']['no_votes'] = !$this->allow_voting; 1514 $content['nm']['no_bounties'] = !$this->allow_bounties; 1515 $content['nm']['no_tr_sum_timesheets'] = false; 1516 } 1517 if (!$content['nm']['session_for'] && $this->called_by) $content['nm']['session_for'] = $this->called_by; 1518 if($_GET['search']) 1519 { 1520 $content['nm']['search'] = $_GET['search']; 1521 } 1522 // if there is only one tracker, use that one and do NOT show the selectbox 1523 if (count($this->trackers) == 1) 1524 { 1525 reset($this->trackers); 1526 $tracker = @key($this->trackers); 1527 $readonlys['nm']['col_filter[tr_tracker]'] = true; 1528 } 1529 if (!$tracker) 1530 { 1531 $tracker = $content['nm']['col_filter']['tr_tracker'] = ''; 1532 } 1533 else 1534 { 1535 $content['nm']['col_filter']['tr_tracker'] = $tracker; 1536 } 1537 1538 // 1539 // disable favories dropdown button, if not running as infolog 1540 if ($this->called_as || $content['nm']['session_for']) 1541 { 1542 $content['nm']['favorites'] = false; 1543 } 1544 else 1545 { 1546 $content['nm']['favorites'] = true; // Enable favorites 1547 } 1548 $content['duration_format'] = ','.$this->duration_format; 1549 1550 $content['is_admin'] = $this->is_admin($tracker); 1551 //_debug_array($content); 1552 $readonlys['add'] = $readonlys['nm']['add'] = !$this->check_rights($this->field_acl['add'],$tracker,null,null,'add'); 1553 $tpl = new Etemplate(); 1554 if (!$tpl->sitemgr || !$tpl->read('tracker.index.sitemgr')) 1555 { 1556 $tpl->read('tracker.index'); 1557 } 1558 1559 // Apply link / avoid DOM conflicts 1560 if($this->called_by) 1561 { 1562 $content['nm'] = array_merge($content['nm'], Api\Cache::getSession('tracker', $this->called_by)); 1563 } 1564 1565 $content['nm']['actions'] = $this->get_actions($tracker, $content['cat_id']); 1566 1567 // disable filemanager icon, if user has no access to it 1568 $readonlys['filemanager/navbar'] = !isset($GLOBALS['egw_info']['user']['apps']['filemanager']); 1569 1570 // Disable actions if there are none 1571 if(count($sel_options['action']) == 0) 1572 { 1573 $tpl->disable_cells('action', true); 1574 $tpl->disable_cells('use_all', true); 1575 } 1576 1577 // Show only own groups in group popup if queue Acl 1578 if($this->enabled_queue_acl_access) 1579 { 1580 $group = explode(',',$tpl->get_cell_attribute('group', 'size')); 1581 $group[1] = 'owngroups'; 1582 $tpl->set_cell_attribute('group', 'size', implode(',',$group)); 1583 } 1584 Framework::includeJS('.','app','tracker'); 1585 // add scrollbar to long description, if user choose so in his prefs 1586 /* @kl: why is an if used, if it is effectily commented by a semicolon? 1587 if ($this->prefs['limit_des_lines'] > 0 || (string)$this->prefs['limit_des_lines'] == ''); 1588 */ 1589 { 1590 $content['css'] .= '<style type="text/css">@media screen { .trackerDes { '. 1591 ($this->prefs['limit_des_width']?'max-width:'.$this->prefs['limit_des_width'].'em;':'').' max-height: '. 1592 (($this->prefs['limit_des_lines'] ? $this->prefs['limit_des_lines'] : 5) * 1.35). // dono why em is not real lines 1593 'em; overflow: auto; }} 1594@media screen { .colfullWidth { 1595width:100%; 1596}</style>'; 1597 } 1598 1599 $preserve = array( 1600 'only_tracker' => $only_tracker, 1601 'called_by' => $this->called_by 1602 ); 1603 if ($this->enabled_color_code_for == 'tracker') $tpl->setElementAttribute ('nm[col_filter][tr_tracker]', 'value_class', 'cat_'); 1604 return $tpl->exec('tracker.tracker_ui.index',$content,$sel_options,$readonlys,$preserve,$return_html); 1605 } 1606 1607 /** 1608 * Get actions / context menu items 1609 * 1610 * @param int $tracker =null 1611 * @param int $cat_id =null 1612 * @return array see nextmatch_widget::get_actions() 1613 */ 1614 public function get_actions($tracker=null, $cat_id=null) 1615 { 1616 for($i = 0; $i <= 100; $i += 10) 1617 { 1618 $percent[$i] = $i.'%'; 1619 } 1620 // Find the ID for 'Fixed' resolution, used below 1621 $resolution_fixed = key(array_filter($this->get_tracker_labels('resolution'), function($a) { 1622 return $a == 'Fixed'; 1623 })); 1624 $actions = array( 1625 'open' => array( 1626 'caption' => 'Open', 1627 'default' => true, 1628 'allowOnMultiple' => false, 1629 'url' => 'menuaction=tracker.tracker_ui.edit&tr_id=$id', 1630 'popup' => Link::get_registry('tracker', 'add_popup'), 1631 'group' => $group=1, 1632 'onExecute' => Api\Header\UserAgent::mobile()?'javaScript:app.tracker.viewEntry':'', 1633 'mobileViewTemplate' => 'view?'.filemtime(Api\Etemplate\Widget\Template::rel2path('/tracker/templates/mobile/view.xet')) 1634 ), 1635 'print' => array( 1636 'caption' => 'Print', 1637 'allowOnMultiple' => false, 1638 'onExecute' => 'javaScript:app.tracker.tprint', 1639 'group' => $group, 1640 'hideOnMobile' => true 1641 ), 1642 'add' => array( 1643 'caption' => 'Add', 1644 'group' => $group, 1645 'children' => array( 1646 'new' => array( 1647 'caption' => 'New', 1648 'url' => 'menuaction=tracker.tracker_ui.edit', 1649 'popup' => Link::get_registry('tracker', 'add_popup'), 1650 'icon' => 'new', 1651 ), 1652 'copy' => array( 1653 'caption' => 'Copy', 1654 'url' => 'menuaction=tracker.tracker_ui.edit&makecp=1&tr_id=$id', 1655 'popup' => Link::get_registry('tracker', 'add_popup'), 1656 'allowOnMultiple' => false, 1657 'icon' => 'copy', 1658 ), 1659 ), 1660 'hideOnMobile' => true 1661 ), 1662 'no_notifications' => array( 1663 'caption' => 'Do not notify', 1664 'checkbox' => true, 1665 'hint' => 'Do not notify of these changes', 1666 'confirm_mass_selection' => "You are going to change %1 entries: Are you sure you want to send notifications about this change?", 1667 'group' => $group, 1668 ), 1669 // modifying content of one or multiple infolog(s) 1670 'change' => array( 1671 'caption' => 'Change', 1672 'group' => ++$group, 1673 'icon' => 'edit', 1674 'disableClass' => 'rowNoEdit', 1675 'confirm_mass_selection' => true, 1676 'children' => array( 1677 'seen' => array( 1678 'caption' => 'Mark as read', 1679 'group' => 1, 1680 ), 1681 'unseen' => array( 1682 'caption' => 'Mark as unread', 1683 'group' => 1, 1684 ), 1685 'tracker' => array( 1686 'caption' => 'Tracker Queue', 1687 'prefix' => 'tracker_', 1688 'children' => $this->trackers, 1689 'enabled' => count($this->trackers) >= 1, 1690 'hideOnDisabled' => true, 1691 'icon' => 'tracker/navbar', 1692 ), 1693 'cat' => array( 1694 'caption' => 'Category', 1695 'prefix' => 'cat_', 1696 'children' => $items=$this->get_tracker_labels('cat',$tracker), 1697 'enabled' => count($items) >= 1, 1698 'hideOnDisabled' => true, 1699 ), 1700 'version' => array( 1701 'caption' => 'Version', 1702 'prefix' => 'version_', 1703 'children' => $items=$this->get_tracker_labels('version',$tracker), 1704 'enabled' => count($items) >= 1, 1705 'hideOnDisabled' => true, 1706 ), 1707 'assigned' => array( 1708 'caption' => 'Assigned to', 1709 'icon' => 'users', 1710 'nm_action' => 'open_popup', 1711 'onExecute' => 'javaScript:app.tracker.change_assigned' 1712 ), 1713 'priority' => array( 1714 'caption' => 'Priority', 1715 'prefix' => 'priority_', 1716 'children' => $items=$this->get_tracker_priorities($tracker,$cat_id), 1717 'enabled' => count($items) >= 1, 1718 'hideOnDisabled' => true, 1719 ), 1720 'status' => array( 1721 'caption' => 'Status', 1722 'prefix' => 'status_', 1723 'children' => $items=$this->get_tracker_stati($tracker), 1724 'enabled' => count($items) >= 1, 1725 'hideOnDisabled' => true, 1726 'icon' => 'check', 1727 ), 1728 'resolution' => array( 1729 'caption' => 'Resolution', 1730 'prefix' => 'resolution_', 1731 'children' => $items=$this->get_tracker_labels('resolution',$tracker), // ToDo: get tracker specific solutions as well, have them available only when applicable 1732 'enabled' => count($items) >= 1, 1733 'hideOnDisabled' => true, 1734 ), 1735 'completion' => array( 1736 'caption' => 'Completed', 1737 'prefix' => 'completion_', 1738 'children' => $percent, 1739 'icon' => 'completed', 1740 ), 1741 'group' => array( 1742 'caption' => 'Group', 1743 'nm_action' => 'open_popup', 1744 'enableClass' => 'group_action', 1745 ), 1746 'link' => array( 1747 'caption' => 'Links', 1748 'nm_action' => 'open_popup', 1749 ), 1750 ), 1751 'hideOnMobile' => true 1752 ), 1753 'close' => array( 1754 'caption' => 'Close', 1755 'icon' => 'check', 1756 'group' => $group, 1757 'disableClass' => 'rowNoClose', 1758 'confirm_mass_selection' => true, 1759 ), 1760 'close_100_'.$resolution_fixed => array( 1761 'caption' => lang('Close') . ' - 100% ' . lang('fixed'), 1762 'icon' => 'check', 1763 'group' => $group, 1764 'disableClass' => 'rowNoClose', 1765 'confirm_mass_selection' => true, 1766 ), 1767 1768 'admin' => array( 1769 'caption' => 'Multiple changes', 1770 'group' => $group, 1771 'enabled' => $this->is_admin($tracker), 1772 'hideOnDisabled' => true, 1773 'nm_action' => 'open_popup', 1774 'icon' => 'user', 1775 ), 1776 ); 1777 ++$group; // integration with other apps 1778 if ($GLOBALS['egw_info']['user']['apps']['filemanager']) 1779 { 1780 $actions['filemanager'] = array( 1781 'icon' => 'filemanager/navbar', 1782 'caption' => 'Filemanager', 1783 'url' => 'menuaction=filemanager.filemanager_ui.index&path=/apps/tracker/$id&ajax=true', 1784 'allowOnMultiple' => false, 1785 'group' => $group, 1786 ); 1787 } 1788 if ($GLOBALS['egw_info']['user']['apps']['timesheet']) 1789 { 1790 $actions['timesheet'] = array( // interactive add for a single event 1791 'icon' => 'timesheet/navbar', 1792 'caption' => 'Timesheet', 1793 'url' => 'menuaction=timesheet.timesheet_ui.edit&link_app[]=tracker&link_id[]=$id', 1794 'group' => $group, 1795 'allowOnMultiple' => false, 1796 'popup' => Link::get_registry('timesheet', 'add_popup'), 1797 ); 1798 } 1799 if ($GLOBALS['egw_info']['user']['apps']['infolog'] && $this->allow_infolog) 1800 { 1801 $actions['infolog'] = array( 1802 'icon' => 'infolog/navbar', 1803 'caption' => 'InfoLog', 1804 'url' => 'menuaction=infolog.infolog_ui.edit&action=tracker&action_id=$id', 1805 'group' => $group, 1806 'allowOnMultiple' => false, 1807 'popup' => Link::get_registry('infolog', 'add_popup'), 1808 ); 1809 } 1810 1811 $actions += EGroupware\Api\Link\Sharing::get_actions('tracker', $group); 1812 // ACL blocks most access right now TODO: allow access 1813 unset($actions['share']['children']['shareWritable']); 1814 unset($actions['share']['children']['shareFiles']); 1815 // Give a readonly & writable filemanager directory actions 1816 $actions['share']['children']['shareFilemanager']['caption'] = 'Readonly filemanager directory'; 1817 $actions['share']['children']['shareWritableFilemanager'] = array_merge( 1818 $actions['share']['children']['shareFilemanager'], 1819 array('caption' => 'Writable filemanager directory', 1820 'hint' => 'Share the filemanager directory, allowing editing') 1821 ); 1822 1823 1824 $actions['documents'] = tracker_merge::document_action( 1825 $this->prefs['document_dir'], ++$group, 'Insert in document', 'document_', 1826 $this->prefs['default_document'] 1827 ); 1828 1829 //echo "<p>".__METHOD__."($do_email, $tid_filter, $org_view)</p>\n"; _debug_array($actions); 1830 return $actions; 1831 } 1832 1833 /** 1834 * imports a mail as Tracker 1835 * 1836 * @param array $mailContent = null mail content 1837 * @return array 1838 */ 1839 function mail_import(array $mailContent=null) 1840 { 1841 // It would get called from compose as a popup with egw_data 1842 if (!is_array($mailContent) && ($_GET['egw_data'])) 1843 { 1844 // get the mail raw data 1845 Link::get_data ($_GET['egw_data']); 1846 return false; 1847 } 1848 if($this->htmledit && $mailContent['html_message']) 1849 { 1850 $message = $mailContent['html_message']; 1851 } 1852 else 1853 { 1854 // Wrap a pre tag if we are using html editor 1855 $message = $this->htmledit? "<pre>".$mailContent['message']."</pre>": $mailContent['message']; 1856 } 1857 1858 $this->edit($this->prepare_import_mail($mailContent['addresses'], 1859 $mailContent['subject'], 1860 $message, 1861 $mailContent['attachments'], 1862 $mailContent['entry_id'])); 1863 } 1864 1865 /** 1866 * apply an action to multiple tracker entries 1867 * 1868 * @param string|int $action 'status_to',set status of entries 1869 * @param array $checked tracker id's to use if !$use_all 1870 * @param boolean $use_all if true use all entries of the current selection (in the session) 1871 * @param int &$success number of succeded actions 1872 * @param int &$failed number of failed actions (not enought permissions) 1873 * @param string &$action_msg translated verb for the actions, to be used in a message like %1 entries 'deleted' 1874 * @param string|array $session_name 'index' or 'email', or array with session-data depending if we are in the main list or the popup 1875 * @param string &$msg 1876 * @param boolean $no_notification 1877 * @return boolean true if all actions succeded, false otherwise 1878 */ 1879 function action($action,$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg,$no_notification) 1880 { 1881 //echo '<p>'.__METHOD__."('$action',".array2string($checked).','.(int)$use_all.",...)</p>\n"; 1882 $success = $failed = 0; 1883 if ($use_all) 1884 { 1885 // get the whole selection 1886 $query = is_array($session_name) ? $session_name : Api\Cache::getSession('tracker', $session_name); 1887 1888 if ($use_all) 1889 { 1890 @set_time_limit(0); // switch off the execution time limit, as it's for big selections to small 1891 $query['num_rows'] = -1; // all 1892 $readonlys = null; 1893 $this->get_rows($query,$checked,$readonlys); 1894 // $this->get_rows gives some extra data. 1895 foreach($checked as $row => $data) 1896 { 1897 unset($data); 1898 if(!is_numeric($row)) 1899 { 1900 unset($checked[$row]); 1901 } 1902 } 1903 } 1904 } 1905 1906 if (is_array($action) && $action['update']) 1907 { 1908 unset($action['update']); 1909 // remove all 'No change' 1910 foreach($action as $name => $value) 1911 { 1912 if ($value === '') unset($action[$name]); 1913 } 1914 if (!count($checked) || !count($action)) 1915 { 1916 $msg = lang('You need to select something to change AND some tracker items!'); 1917 $failed = true; 1918 } 1919 else 1920 { 1921 foreach($checked as $tr_id) 1922 { 1923 if (!$this->read($tr_id)) continue; 1924 foreach($action as $name => $value) 1925 { 1926 if ($name == 'tr_status_admin') $name = 'tr_status'; 1927 $this->data[$name] = $name == 'tr_assigned' && $value === 'not' ? NULL : $value; 1928 } 1929 if($no_notification) $this->data['no_notifications'] = true; 1930 if (!$this->save()) 1931 { 1932 $success++; 1933 } 1934 else 1935 { 1936 $failed++; 1937 } 1938 } 1939 $action_msg = lang('updated'); 1940 } 1941 } 1942 else 1943 { 1944 // Dialogs to get options 1945 list($action, $settings) = explode('_', $action, 2); 1946 1947 switch($action) 1948 { 1949 case 'close': 1950 $action_msg = lang('closed'); 1951 if(is_string($settings)) // ex: closed-100-fixed 1952 { 1953 $settings = explode('_', $settings); 1954 } 1955 foreach($checked as $tr_id) 1956 { 1957 if (!$this->read($tr_id)) continue; 1958 $this->data['tr_status'] = tracker_bo::STATUS_CLOSED; 1959 if($no_notification) $this->data['no_notifications'] = true; 1960 1961 if($settings[0]) 1962 { 1963 $this->data['tr_completion'] = $settings[0]; 1964 } 1965 if($settings[1]) 1966 { 1967 $this->data['tr_resolution'] = $settings[1]; 1968 } 1969 if (!$this->save()) 1970 { 1971 $success++; 1972 } 1973 else 1974 { 1975 $failed++; 1976 } 1977 } 1978 break; 1979 case 'seen': 1980 case 'unseen': 1981 $action_msg = lang($action); 1982 foreach($checked as $tr_id) 1983 { 1984 if (!$this->read($tr_id)) continue; 1985 self::seen($this->data, true, $action == 'seen'); 1986 $success++; 1987 } 1988 break; 1989 case 'group': 1990 // Popup adds an extra param (add/delete) that group doesn't need 1991 list(,$settings) = explode('_',$settings); 1992 case 'tracker': 1993 case 'cat': 1994 case 'version': 1995 case 'priority': 1996 case 'status': 1997 case 'resolution': 1998 case 'completion': 1999 $action_msg = lang('updated'); 2000 foreach($checked as $tr_id) 2001 { 2002 if (!$this->read($tr_id)) continue; 2003 $this->data[($action == 'cat' ? 'cat_id' : 'tr_'.$action)] = $settings; 2004 if($no_notification) $this->data['no_notifications'] = true; 2005 if (!$this->save()) 2006 { 2007 $success++; 2008 } 2009 else 2010 { 2011 $failed++; 2012 } 2013 } 2014 break; 2015 case 'assigned': 2016 $action_msg = lang('updated'); 2017 foreach($checked as $tr_id) 2018 { 2019 if (!$this->read($tr_id)) continue; 2020 list($add_remove, $idstr) = explode('_', $settings, 2); 2021 $ids = explode(',',$idstr); 2022 if($add_remove == 'ok') 2023 { 2024 $this->data['tr_assigned'] = $ids; 2025 } 2026 else 2027 { 2028 $this->data['tr_assigned'] = $add_remove == 'add' ? 2029 array_merge($this->data['tr_assigned'],$ids) : 2030 array_diff($this->data['tr_assigned'],$ids); 2031 } 2032 // No 0 allowed 2033 $this->data['tr_assigned'] = array_unique(array_diff($this->data['tr_assigned'], array(0))); 2034 if($no_notification) $this->data['no_notifications'] = true; 2035 if (!$this->save()) 2036 { 2037 $success++; 2038 } 2039 else 2040 { 2041 $failed++; 2042 } 2043 } 2044 break; 2045 2046 case 'link': 2047 list($add_remove, $link) = explode('_', $settings, 2); 2048 list($app, $link_id) = explode(':', $link); 2049 if(!$link_id) 2050 { 2051 $msg = lang('You need to select an entry for linking.'); 2052 break; 2053 } 2054 error_log("APp: $app ID: $link_id"); 2055 $title = Link::title($app, $link_id); 2056 foreach($checked as $id) 2057 { 2058 if (!$this->read($id)) 2059 { 2060 $failed++; 2061 continue; 2062 } 2063 if($add_remove == 'add') 2064 { 2065 $action_msg = lang('linked to %1', $title); 2066 if(Link::link('tracker', $id, $app, $link_id)) 2067 { 2068 $success++; 2069 } 2070 else 2071 { 2072 $failed++; 2073 } 2074 } 2075 else 2076 { 2077 $action_msg = lang('unlinked from %1', $title); 2078 $count = Link::unlink(0, 'tracker', $id, '', $app, $link_id); 2079 $success += $count; 2080 } 2081 } 2082 return $failed == 0; 2083 2084 case 'document': 2085 if (!$settings) $settings = $GLOBALS['egw_info']['user']['preferences']['tracker']['default_document']; 2086 $document_merge = new tracker_merge(); 2087 $msg = $document_merge->download($settings, $checked, '', $GLOBALS['egw_info']['user']['preferences']['tracker']['document_dir']); 2088 $failed = count($checked); 2089 return false; 2090 } 2091 } 2092 return !$failed; 2093 } 2094 2095 /** 2096 * Fill in canned comment 2097 * 2098 * @param id Canned comment ID 2099 */ 2100 public function ajax_canned_comment($id, $htmlarea=true) 2101 { 2102 $response = Api\Json\Response::get(); 2103 2104 if($htmlarea) 2105 { 2106 $response->call('app.tracker.canned_comment_response',nl2br($this->get_canned_response($id))); 2107 } 2108 else 2109 { 2110 $response->call('app.tracker.canned_comment_response', $this->get_canned_response($id)); 2111 } 2112 } 2113 2114 /** 2115 * Edit a comment 2116 * 2117 * @param value 2118 * @param tr_id 2119 * @param comment_id 2120 */ 2121 public function ajax_update_reply($value, $tr_id, $comment_id) 2122 { 2123 if(!$this->check_rights($this->field_acl['edit_reply'], null, (int)$tr_id) && !$this->check_rights($this->field_acl['edit_own_reply'], null, (int)$tr_id)) 2124 { 2125 // No rights for any edit 2126 return false; 2127 } 2128 2129 if(!$this->check_rights($this->field_acl['edit_reply'], null, (int)$tr_id)) 2130 { 2131 // Need to read ticket so we can get comment owner & verify 2132 $verified = false; 2133 $this->read((int)$tr_id); 2134 foreach($this->data['replies'] as $key => &$reply) 2135 { 2136 if($reply['reply_id'] == $comment_id && 2137 $reply['reply_creator'] == $GLOBALS['egw_info']['user']['account_id'] && 2138 $this->check_rights($this->field_acl['edit_own_reply'], null, null, null, 'edit_own_reply')) 2139 { 2140 $verified = true; 2141 break; 2142 } 2143 } 2144 if(!$verified) 2145 { 2146 return false; 2147 } 2148 } 2149 2150 // Update the comment 2151 $this->save_comment(array( 2152 'reply_id' => (int)$comment_id, 2153 'reply_message' => $value 2154 )); 2155 } 2156 2157 /** 2158 * shows tracker in other applications 2159 * 2160 * @param $args['location'] location of hooks: {addressbook|projects|calendar}_view 2161 * @param $args['view'] menuaction to view, if location == 'infolog' 2162 * @param $args['app'] app-name, if location == 'infolog' 2163 * @param $args['view_id'] name of the id-var for location == 'infolog' 2164 * @param $args[$args['view_id']] id of the entry 2165 * this function can be called for any app, which should include infolog: \ 2166 * Api\Hooks::process(array( \ 2167 * * 'location' => 'infolog', \ 2168 * * 'app' => <your app>, \ 2169 * * 'view_id' => <id name>, \ 2170 * * <id name> => <id value>, \ 2171 * * 'view' => <menuaction to view an entry in your app> \ 2172 * )); 2173 */ 2174 public function hook_view($args) 2175 { 2176 // Load JS for tracker actions 2177 Framework::includeJS('.','app','tracker'); 2178 2179 switch ($args['location']) 2180 { 2181 case 'addressbook_view': 2182 $app = 'addressbook'; 2183 $view_id = 'ab_id'; 2184 // Just set the filter 2185 $state['action'] = $app; 2186 $state['action_id'] = $args[$view_id]; 2187 Api\Cache::setSession('tracker', $app, $state); 2188 break; 2189 } 2190 if (!isset($app) || !isset($args[$view_id])) 2191 { 2192 return False; 2193 } 2194 $this->called_by = $app; // for read/save_sessiondata, to have different sessions for the hooks 2195 2196 // Set to calling app, so actions wind up in the correct place client side 2197 $GLOBALS['egw_info']['flags']['currentapp'] = $app; 2198 2199 Api\Translation::add_app('tracker'); 2200 2201 $this->index(null); 2202 } 2203 2204 /** 2205 * Copy a given ticket (not storing it!) 2206 * 2207 * Taken care only configured fields get copied and certain fields never to copy (uid etc.). 2208 * 2209 * @param array& $content 2210 */ 2211 function copy(array &$content) 2212 { 2213 $id = $content['tr_id']; 2214 2215 // If original is closed, copy should be open 2216 if($content['tr_closed'] && $content['tr_completion'] == '100') 2217 { 2218 $content['tr_status'] = self::STATUS_OPEN; 2219 $content['tr_completion'] = 0; 2220 // Get default resolution 2221 $this->get_tracker_labels('resolution', $content['tr_tracker'], $content['tr_resolution']); 2222 } 2223 2224 $exclude_fields = array('tr_id', 'tr_closed', 'tr_seen', 2225 'tr_created', 'tr_modified', 'tr_modifier' 2226 ); 2227 foreach ($exclude_fields as $field) 2228 { 2229 unset($content[$field]); 2230 } 2231 // startdate in the past --> set startdate 2232 if ($content['tr_startdate'] && $content['tr_startdate'] < Api\DateTime::to('now')) 2233 { 2234 $content['tr_startdate'] = Api\DateTime::to('now'); 2235 } 2236 // duedate in the past --> unset it 2237 if (isset($content['tr_duedate']) && $content['tr_duedate'] < Api\DateTime::to('now')) 2238 { 2239 unset($content['tr_duedate']); 2240 } 2241 2242 if(!is_array($content['link_to'])) $content['link_to'] = array(); 2243 $content['link_to']['to_app'] = 'tracker'; 2244 $content['link_to']['to_id'] = 0; 2245 // Get links to be copied, if not excluded 2246 if (!in_array('link_to',$exclude_fields) || !in_array('attachments',$exclude_fields)) 2247 { 2248 foreach(Link::get_links($content['link_to']['to_app'], $id) as $link) 2249 { 2250 if ($link['app'] != Link::VFS_APPNAME && !in_array('link_to', $exclude_fields)) 2251 { 2252 Link::link('tracker', $content['link_to']['to_id'], $link['app'], $link['id'], $link['remark']); 2253 } 2254 elseif ($link['app'] == Link::VFS_APPNAME && !in_array('attachments', $exclude_fields)) 2255 { 2256 Link::link('tracker', $content['link_to']['to_id'], Link::VFS_APPNAME, array( 2257 'tmp_name' => Link::vfs_path($link['app2'], $link['id2']).'/'.$link['id'], 2258 'name' => $link['id'], 2259 ), $link['remark']); 2260 } 2261 } 2262 } 2263 $content['links'] = $content['link_to']; 2264 2265 $content['tr_owner'] = !(int)$content['owner'] || !$this->bo->check_perms(Acl::ADD,0,$content['owner']) ? $this->user : $this->owner; 2266 2267 // If current user has no permissions for creator, use them as creator 2268 $readonlys = $this->readonlys_from_acl(); 2269 $content['tr_creator'] = $readonlys['tr_creator'] ? $this->user : $content['tr_creator']; 2270 2271 if (!empty($content['tr_summary'])) 2272 { 2273 $content['tr_summary'] = lang('Copy of:').' '.$content['tr_summary']; 2274 } 2275 2276 $content['msg'] .= ($content['msg']?"\n":'').lang('%1 copied - the copy can now be edited', lang(Link::get_registry('tracker','entry'))); 2277 } 2278 2279 /** 2280 * Modify history to hide changes on restricted comments if the current user 2281 * is not allowed to see them. 2282 * 2283 * @param array $data values for keys "data" (data) and "args": 2284 * values for keys "value", "rows" (reference) and "total" (reference) 2285 */ 2286 public function modify_history(array &$data) 2287 { 2288 // Is current user restricted? 2289 $this->read($data['value']['record_id']); 2290 $user = $GLOBALS['egw_info']['user']['account_id']; 2291 $is_admin = $this->is_admin($this->data['tr_tracker'], $user); 2292 $is_technician = $this->is_technician($this->data['tr_tracker'], $user); 2293 2294 $read_restricted = $is_admin || $is_technician || in_array($user, $this->data['tr_assigned']) || 2295 // if assigned to a group, we need to check memberships of $user 2296 $GLOBALS['egw']->accounts->get_type($this->data['tr_assigned']) == 'g' && 2297 in_array($this->data['tr_assigned'], $GLOBALS['egw']->accounts->memberships($user, true)); 2298 2299 // Can read the hidden comments, no changes needed 2300 if($read_restricted) 2301 { 2302 return; 2303 } 2304 2305 // Hide restricted comments 2306 $remove_indexes = Array(); 2307 foreach($data['rows'] as $index => $row) 2308 { 2309 if($row['status'] !== 'comment') 2310 { 2311 continue; 2312 } 2313 list(,$comment_id) = explode(': ',$row['new_value'][0]); 2314 $comment_index = array_search($comment_id, array_column($this->data['replies'],'reply_id')); 2315 $comment = $this->data['replies'][$comment_index]; 2316 2317 if(!$comment || $comment_index === FALSE || $comment && $comment['reply_visible']) 2318 { 2319 $remove_indexes[] = $index; 2320 } 2321 } 2322 $data['rows'] = array_diff_key($data['rows'], array_flip($remove_indexes)); 2323 $data['total'] -= count($remove_indexes); 2324 } 2325} 2326