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\Acl; 16use EGroupware\Api\Vfs; 17 18/** 19 * Some constants for the check_rights function 20 */ 21define('TRACKER_ADMIN',1); 22define('TRACKER_TECHNICIAN',2); 23define('TRACKER_USER',4); // non-anonymous user with tracker-rights 24define('TRACKER_EVERYBODY',8); // everyone incl. anonymous user 25define('TRACKER_ITEM_CREATOR',16); 26define('TRACKER_ITEM_ASSIGNEE',32); 27define('TRACKER_ITEM_NEW',64); 28define('TRACKER_ITEM_GROUP',128); 29 30/** 31 * Business Object of the tracker 32 */ 33class tracker_bo extends tracker_so 34{ 35 /** 36 * Timestamps which need to be converted to user-time and back 37 * 38 * @var array 39 */ 40 var $timestamps = array('tr_created','tr_modified','tr_closed','reply_created'); 41 /** 42 * Current user 43 * 44 * @var int; 45 */ 46 var $user; 47 48 /** 49 * Existing trackers (stored as app-global cats with cat_data='tracker') 50 * 51 * @var array 52 */ 53 var $trackers; 54 /** 55 * Existing priorities 56 * 57 * @var array 58 */ 59 static $stock_priorities = array( 60 1 => '1 - lowest', 61 2 => '2', 62 3 => '3', 63 4 => '4', 64 5 => '5 - medium', 65 6 => '6', 66 7 => '7', 67 8 => '8', 68 9 => '9 - highest', 69 ); 70 /** 71 * Priorities by tracker or key=0 for all trackers 72 * 73 * Not set trackers use the key=0 or if that's not set the stock priorities 74 * 75 * @var array 76 */ 77 protected $priorities; 78 79 /** 80 * Stati used by all trackers 81 * 82 * @var array 83 */ 84 static $stati = array( 85 self::STATUS_OPEN => 'Open(status)', 86 self::STATUS_CLOSED => 'Closed', 87 self::STATUS_DELETED => 'Deleted', 88 self::STATUS_PENDING => 'Pending', 89 ); 90 /** 91 * Resolutions used by all trackers historically 92 * 93 * Kept around for history display, but no longer used 94 * @var array 95 */ 96 static $resolutions = array( 97 'n' => 'None', 98 'a' => 'Accepted', 99 'd' => 'Duplicate', 100 'f' => 'Fixed', 101 'i' => 'Invalid', 102 'I' => 'Info only', 103 'l' => 'Later', 104 'o' => 'Out of date', 105 'p' => 'Postponed', 106 'O' => 'Outsourced', 107 'r' => 'Rejected', 108 'R' => 'Remind', 109 'w' => 'Wont fix', 110 'W' => 'Works for me', 111 ); 112 /** 113 * Technicians by tracker or key=0 for all trackers 114 * 115 * @var array 116 */ 117 var $technicians; 118 /** 119 * Admins by tracker or key=0 for all trackers 120 * 121 * @var array 122 */ 123 var $admins; 124 /** 125 * Users by tracker or key=0 for all trackers 126 * 127 * @var array 128 */ 129 var $users; 130 /** 131 * ACL for the fields of the tracker 132 * 133 * field-name is the key with values or'ed together from the TRACKER_ constants 134 * 135 * @var array 136 */ 137 var $field_acl; 138 /** 139 * Restricions settings (tracker specific, keys: group, creator) 140 * 141 * @var array 142 */ 143 var $restrictions; 144 /** 145 * Enabled the Acl queue access? 146 * 147 * @var boolean 148 */ 149 var $enabled_queue_acl_access = false; 150 /** 151 * Mailhandler settings (tracker unspecific) 152 * Keys: 153 * interval 154 * address 155 * server 156 * servertype 157 * serverport 158 * folder 159 * username 160 * password 161 * delete_from_server (true/false) 162 * default_tracker (<empty>=reject new tickets|TrackerID) 163 * unrecognized_mails (ignore/delete/forward/default) 164 * unrec_reply (0=Creator/1=Nobody) 165 * unrec_mail (<empty>=ignore|UID) 166 * forward_to 167 * auto_reply (0=Never/1=New/2=Always) 168 * reply_unknown (1=Yes/0=No) 169 * reply_text (text message) 170 * bounces (ignore/delete/forward) 171 * autoreplies (ignore/delete/forward/process) 172 * 173 * @var array 174 */ 175 var $mailhandling = array(); 176 /** 177 * Supported server types for mail handling as an array of arrays with spec => descr 178 * 179 * @var array 180 */ 181 var $mailservertypes = array( 182 0 => array('imap/notls', 'Standard IMAP'), 183 1 => array('imap/tls', 'IMAP, TLS secured'), 184 2 => array('imap/ssl', 'IMAP, SSL secured'), 185 3 => array('pop3', 'POP3'), 186 ); 187 188 /** 189 * how to handle mailheaderinfo, provided as an array of arrays with spec => descr 190 * 191 * @var array 192 */ 193 var $mailheaderhandling = array( 194 0 => array('noinfo', 'no, no additional Mailheader to description and comments'), 195 1 => array('infotodesc', 'yes, add Mailheader to description'), 196 2 => array('infotocomment', 'yes, add Mailheader to comments'), 197 3 => array('infotoboth', 'yes, add Mailheader to both (description and comments)'), 198 ); 199 200 /** 201 * Translates field / acl-names to labels 202 * 203 * @var array 204 */ 205 var $field2label = array( 206 'tr_summary' => 'Summary', 207 'tr_tracker' => 'Tracker', 208 'cat_id' => 'Category', 209 'tr_version' => 'Version', 210 'tr_status' => 'Status', 211 'tr_description' => 'Description', 212 'tr_assigned' => 'Assigned to', 213 'tr_private' => 'Private', 214// 'tr_budget' => 'Budget', 215 'tr_resolution' => 'Resolution', 216 'tr_completion' => 'Completed', 217 'tr_priority' => 'Priority', 218 'tr_startdate' => 'Start date', 219 'tr_duedate' => 'Due date', 220 'tr_closed' => 'Closed', 221 'tr_creator' => 'Created by', 222 'tr_created' => 'Created on', 223 'tr_group' => 'Group', 224 'tr_cc' => 'CC', 225 // pseudo fields used in edit 226 'link_to' => 'Attachments & Links', 227 'canned_response' => 'Canned response', 228 'reply_message' => 'Add comment', 229 'edit_own_reply' => 'Edit own comments', 230 'edit_reply' => 'Edit others comments', 231 'add' => 'Add', 232 'vote' => 'Vote for it!', 233 'no_notifications' => 'No notifications', 234 'bounty' => 'Set bounty', 235 'num_replies' => 'Number of replies', 236 'customfields' => 'Custom fields', 237 ); 238 /** 239 * Translate field-name to 2-char history status 240 * 241 * @var array 242 */ 243 var $field2history = array( 244 'tr_summary' => 'Su', 245 'tr_tracker' => 'Tr', 246 'cat_id' => 'Ca', 247 'tr_version' => 'Ve', 248 'tr_status' => 'St', 249 'tr_description' => 'De', 250 'tr_creator' => 'Cr', 251 'tr_assigned' => 'As', 252 'tr_private' => 'pr', 253// 'tr_budget' => 'Bu', 254 'tr_completion' => 'Co', 255 'tr_priority' => 'Pr', 256 'tr_startdate' => 'tr_startdate', 257 'tr_duedate' => 'tr_duedate', 258 'tr_closed' => 'Cl', 259 'tr_resolution' => 'Re', 260 'tr_cc' => 'Cc', 261 'tr_group' => 'Gr', 262 // no need to track number of replies, as replies are versioned 263 //'num_replies' => 'Nr', 264/* the following bounty-stati are only for reference 265 'bounty-set' => 'bo', 266 'bounty-deleted' => 'xb', 267 'bounty-confirmed'=> 'Bo', 268*/ 269 // all custom fields together 270 'customfields' => '#c', 271 ); 272 /** 273 * Allow to assign tracker items to groups: 0=no; 1=yes, display groups+users; 2=yes, display users+groups 274 * 275 * @var int 276 */ 277 var $allow_assign_groups=1; 278 /** 279 * Allow to vote on tracker items 280 * 281 * @var boolean 282 */ 283 var $allow_voting=true; 284 /** 285 * How many days to mark a not responded item overdue 286 * 287 * @var int 288 */ 289 var $overdue_days=14; 290 /** 291 * How many days to mark a pending item closed 292 * 293 * @var int 294 */ 295 var $pending_close_days=7; 296 /** 297 * Permit html editing on details and comments 298 */ 299 var $htmledit = false; 300 var $all_cats; 301 var $historylog; 302 303 /** 304 * config var for color code 305 * @var type 306 */ 307 var $enabled_color_code_for = ''; 308 309 /** 310 * Instance of the tracker_tracking object 311 * 312 * @var tracker_tracking 313 */ 314 var $tracking; 315 /** 316 * Names of all config vars 317 * 318 * @var array 319 */ 320 var $config_names = array( 321 'technicians','admins','users','notification','projects','priorities','default_priority','restrictions', 'user_category_preference', // tracker specific 322 'field_acl','allow_assign_groups','allow_voting','overdue_days','pending_close_days','htmledit','create_new_as_private','allow_assign_users','allow_infolog','allow_restricted_comments','mailhandling','enabled_color_code_for', // tracker unspecific 323 'allow_bounties','currency','enabled_queue_acl_access','exclude_app_on_timesheetcreation','show_dates', 'comment_reopens' 324 ); 325 /** 326 * Notification settings (tracker specific, keys: sender, link, copy, lang) 327 * 328 * @var array 329 */ 330 var $notification; 331 /** 332 * Allow bounties to be set on tracker items 333 * 334 * @var string 335 */ 336 var $allow_bounties = false; 337 /** 338 * Currency used by the bounties 339 * 340 * @var string 341 */ 342 var $currency = 'Euro'; 343 /** 344 * Filters to manage advanced logical statis 345 */ 346 var $filters = array( 347 'closed' => '♦ Closed', 348 'not-closed' => '♦ Not closed', 349 'own-not-closed' => '♦ Own not closed', 350 'ownorassigned-not-closed' => '♦ Own or assigned not closed', 351 'without-reply-not-closed' => '♦ Without reply not closed', 352 'own-without-reply-not-closed' => '♦ Own without reply not closed', 353 'without-30-days-reply-not-closed' => '♦ Without 30 days reply not closed', 354 ); 355 356 /** 357 * Filter for search limiting the date-range 358 * 359 * @var array 360 */ 361 var $date_filters = array( // Start: year,month,day,week, End: year,month,day,week 362 'Overdue' => false, 363 'Today' => array(0,0,0,0, 0,0,1,0), 364 'Yesterday' => array(0,0,-1,0, 0,0,0,0), 365 'This week' => array(0,0,0,0, 0,0,0,1), 366 'Last week' => array(0,0,0,-1, 0,0,0,0), 367 'This month' => array(0,0,0,0, 0,1,0,0), 368 'Last month' => array(0,-1,0,0, 0,0,0,0), 369 'Last 3 months' => array(0,-3,0,0, 0,0,0,0), 370 'This quarter'=> array(0,0,0,0, 0,0,0,0), // Just a marker, needs special handling 371 'Last quarter'=> array(0,-4,0,0, 0,-4,0,0), // Just a marker 372 'This year' => array(0,0,0,0, 1,0,0,0), 373 'Last year' => array(-1,0,0,0, 0,0,0,0), 374 '2 years ago' => array(-2,0,0,0, -1,0,0,0), 375 '3 years ago' => array(-3,0,0,0, -2,0,0,0), 376 ); 377 378 379 380 /** 381 * Constructor 382 * 383 * @return tracker_bo 384 */ 385 function __construct() 386 { 387 parent::__construct(); 388 389 $this->user = $GLOBALS['egw_info']['user']['account_id']; 390 $this->today = mktime(0,0,0,date('m',$this->now),date('d',$this->now),date('Y',$this->now)); 391 392 // read the tracker-configuration 393 $this->load_config(); 394 395 $this->trackers = $this->get_tracker_labels(); 396 } 397 398 /** 399 * initializes data with the content of key 400 * 401 * Reimplemented to set some defaults 402 * 403 * @param array $keys = array() array with keys in form internalName => value 404 * @return array internal data after init 405 */ 406 function init($keys=array()) 407 { 408 parent::init(); 409 if (isset($keys['tr_tracker'])&&!empty($keys['tr_tracker'])) $this->data['tr_tracker']=$keys['tr_tracker']; 410 if (is_array($this->trackers)&&(!isset($this->data['tr_tracker'])||empty($this->data['tr_tracker']))) // init is called from Api\Storage\Base::__construct(), where $this->trackers is NOT set 411 { 412 $this->data['tr_tracker'] = key($this->trackers); // Need some tracker so creator rights are correct 413 } 414 $this->data['tr_creator'] = $GLOBALS['egw_info']['user']['account_id']; 415 $this->data['tr_private'] = $this->create_new_as_private; 416 $this->data['tr_group'] = $GLOBALS['egw_info']['user']['account_primary_group']; 417 // set default resolution 418 $this->get_tracker_labels('resolution', $this->data['tr_tracker'], $this->data['tr_resolution']); 419 420 // set default priority 421 $default_priority = null; 422 $this->get_tracker_priorities($this->data['tr_tracker'],$this->data['cat_id'], true, $default_priority); 423 $this->data['tr_priority'] = $default_priority; 424 425 // Set default category 426 if(!$this->data['cat_id']) 427 { 428 $default_category = null; 429 $this->get_tracker_labels('cat', $this->data['tr_tracker'], $default_category); 430 $this->data['cat_id'] = $default_category; 431 } 432 433 $this->data_merge($keys); 434 435 return $this->data; 436 } 437 438 /** 439 * Changes the data from the db-format to your work-format 440 * 441 * @param array $data if given works on that array and returns result, else works on internal data-array 442 * @return array with changed data 443 */ 444 function db2data($data=null) 445 { 446 if (($intern = !is_array($data))) 447 { 448 $data =& $this->data; 449 } 450 if (is_array($data['replies'])) 451 { 452 foreach($data['replies'] as &$reply) 453 { 454 $reply['reply_servertime'] = $reply['reply_created']; 455 $reply['reply_created'] = Api\DateTime::server2user($reply['reply_created'],$this->timestamp_type); 456 } 457 } 458 // check if item is overdue 459 if ($this->overdue_days > 0) 460 { 461 $modified = $data['tr_modified'] ? $data['tr_modified'] : $data['tr_created']; 462 $limit = $this->now - $this->overdue_days * 24*60*60; 463 $data['overdue'] = !in_array($data['tr_status'],$this->get_tracker_stati(null,true)) && // only open items can be overdue 464 (!$data['tr_modified'] || $data['tr_modifier'] == $data['tr_creator']) && $modified < $limit; 465 466 } 467 468 // Consider due date independent of overdue days 469 $data['overdue'] |= ($data['tr_duedate'] && $this->now > $data['tr_duedate'] && !in_array($data['tr_status'], $this->get_tracker_stati(null,true))); 470 471 // Keep a copy of the timestamps in server time, so notifications can change them for each user 472 foreach($this->timestamps as $field) 473 { 474 $data[$field . '_servertime'] = $data[$field]; 475 } 476 477 // will run all regular timestamps ($this->timestamps) trough Api\DateTime::server2user() 478 return parent::db2data($intern ? null : $data); // important to use null, if $intern! 479 } 480 481 /** 482 * Changes the data from your work-format to the db-format 483 * 484 * @param array $data if given works on that array and returns result, else works on internal data-array 485 * @return array with changed data 486 */ 487 function data2db($data=null) 488 { 489 if (($intern = !is_array($data))) 490 { 491 $data = &$this->data; 492 } 493 if (substr($data['tr_completion'],-1) == '%') $data['tr_completion'] = (int) round(substr($data['tr_completion'],0,-1)); 494 495 // will run all regular timestamps ($this->timestamps) through Api\DateTime::user2server() 496 return parent::data2db($intern ? null : $data); // important to use null, if $intern! 497 } 498 499 /** 500 * Read a tracker item 501 * 502 * Reimplemented to store the old status 503 * 504 * @param array $keys array with keys in form internalName => value, may be a scalar value if only one key 505 * @param string|array $extra_cols string or array of strings to be added to the SELECT, eg. "count(*) as num" 506 * @param string $join sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or 507 * @param int $user = null for which user to check, default current user 508 * @return array|boolean data if row could be retrived else False 509 */ 510 function read($keys,$extra_cols='',$join='',$user=null) 511 { 512 if (($ret = parent::read($keys, $extra_cols, $join))) 513 { 514 // read_extras need to know if $user is admin and/or technician of queue of ticket 515 $ret = $this->read_extra($this->is_admin($this->data['tr_tracker'], $user), 516 $this->is_technician($this->data['tr_tracker'], $user), $user); 517 518 $this->data['old_status'] = $this->data['tr_status']; 519 520 if ($this->deny_private()) $ret = $this->data = false; 521 } 522 return $ret; 523 } 524 525 /** 526 * saves the content of data to the db 527 * 528 * @param array $keys if given $keys are copied to data before saveing => allows a save as 529 * @param array $autoreply when called from the mailhandler, contains data for the autoreply 530 * (only for forwarding to tracling) 531 * @return int 0 on success and errno != 0 else 532 */ 533 function save($keys=null, $autoreply=null) 534 { 535 if ($keys) $this->data_merge($keys); 536 537 if (!$this->data['tr_id']) // new entry 538 { 539 $this->data['tr_created'] = (isset($this->data['tr_created'])&&!empty($this->data['tr_created'])?$this->data['tr_created']:$this->now); 540 $this->data['tr_creator'] = $this->data['tr_creator'] ? $this->data['tr_creator'] : $this->user; 541 $this->data['tr_version'] = $this->data['tr_version'] ? $this->data['tr_version'] : $GLOBALS['egw_info']['user']['preferences']['tracker']['default_version']; 542 $this->data['tr_status'] = $this->data['tr_status'] ? $this->data['tr_status'] : self::STATUS_OPEN; 543 544 if (!$this->data['tr_resolution']) // if no resolution set, ask labels for resolution default 545 { 546 $this->get_tracker_labels('resolution', $this->data['tr_tracker'], $this->data['tr_resolution']); 547 } 548 $this->data['tr_seen'] = serialize(array($this->user)); 549 550 if (!$this->data['tr_group']) 551 { 552 $this->data['tr_group'] = $GLOBALS['egw']->accounts->data['account_primary_group']; 553 } 554 555 if ($this->data['cat_id'] && !$this->data['tr_assigned']) 556 { 557 $this->autoassign(); 558 } 559 } 560 else 561 { 562 // check if we have a real modification 563 // read the old record 564 $new =& $this->data; 565 unset($this->data); 566 $this->read($new['tr_id']); 567 $old =& $this->data; 568 unset($this->data); 569 $this->data =& $new; 570 571 if (!is_object($this->tracking)) $this->tracking = new tracker_tracking($this); 572 $changed = $this->tracking->changed_fields($new, $old); 573 // Avoid saving the entry when the same entry has been opened and modified by someone else 574 if (is_array($changed) && $new['tr_modified'] != $old['tr_modified']) return 'tr_modified'; 575 //error_log(__METHOD__.__LINE__.' ReplyMessage:'.$this->data['reply_message'].' Mode:'.$this->data['tr_edit_mode'].' Config:'.$this->htmledit); 576 $testReply = $this->data['reply_message']; 577 if ($this->htmledit && isset($this->data['reply_message']) && !empty($this->data['reply_message'])) 578 { 579 $testReply = trim(Api\Mail\Html::convertHTMLToText(Api\Html::purify($this->data['reply_message']), false, true, true)); 580 } 581 //error_log(__METHOD__.__LINE__.' TestReplyMessage:'.$testReply); 582 if (!$changed && !((isset($this->data['reply_message']) && !empty($this->data['reply_message']) && !empty($testReply)) || 583 (isset($this->data['canned_response']) && !empty($this->data['canned_response'])))) 584 { 585 //error_log(__METHOD__.__LINE__." no change --> no save needed"); 586 return false; 587 } 588 // Check for modifying field without access 589 $readonlys = $this->readonlys_from_acl(); 590 foreach($changed as $field) 591 { 592 if ($readonlys[$field]) 593 { 594 //error_log(__METHOD__.__LINE__.' Field:'.$field.'->'.array2string($readonlys).function_backtrace()); 595 return $field; 596 } 597 } 598 599 // Auto-assign if category changed & noone assigned 600 if ($this->data['cat_id'] && $this->data['cat_id'] != $old['cat_id'] && !$this->data['tr_assigned']) 601 { 602 $this->autoassign(); 603 } 604 605 // Changes mark the ticket unseen for everbody but the current 606 // user if the ticket wasn't closed at the same time 607 if (!in_array($this->data['tr_status'],$this->get_tracker_stati(null, true))) 608 { 609 $seen = array(); 610 $this->data['tr_seen'] = unserialize($this->data['tr_seen']); 611 612 // This only matters if no other changes have been made 613 if($this->data['reply_visible'] && empty($changed)) 614 { 615 // Keep those that can't see the comment 616 $seen = array_intersect($this->data['tr_seen'], array_keys(array_diff( 617 $this->get_staff($this->data['tracker_id'], 2, 'users'), 618 $this->get_staff($this->data['tracker_id'], 2, 'technicians') 619 ))); 620 } 621 $seen[] = $this->user; 622 $this->data['tr_seen'] = serialize($seen); 623 } 624 $this->data['tr_modified'] = $this->now; 625 $this->data['tr_modifier'] = $this->user; 626 $changed[] = 'tr_modified'; 627 628 // set close-date if status is closed and not yet set 629 if (in_array($this->data['tr_status'],array_keys($this->get_tracker_stati(null, true))) && 630 is_null($this->data['tr_closed'])) 631 { 632 $this->data['tr_closed'] = $this->now; 633 $changed[] = 'tr_closed'; 634 } 635 // unset closed date, if item is re-opend 636 if (!in_array($this->data['tr_status'],array_keys($this->get_tracker_stati(null, true))) && 637 !is_null($this->data['tr_closed'])) 638 { 639 $this->data['tr_closed'] = null; 640 $changed[] = 'tr_closed'; 641 } 642 if (($this->data['reply_message'] && !empty($testReply)) || $this->data['canned_response']) 643 { 644 if ($this->data['canned_response']) 645 { 646 $this->data['reply_message'] = $this->get_canned_response($this->data['canned_response']). 647 ($this->data['reply_message'] ? "\n\n".$this->data['reply_message'] : ''); 648 } 649 $this->data['reply_created'] = (isset($this->data['reply_created'])&&!empty($this->data['reply_created'])?$this->data['reply_created']:$this->now); 650 $this->data['reply_creator'] = $this->user; 651 652 // replies set status pending back to open 653 if (($this->data['old_status'] == self::STATUS_PENDING && $this->data['old_status'] == $this->data['tr_status']) || 654 (($this->comment_reopens || !property_exists($this, 'comment_reopens')) && $this->is_closed_status($this->data['old_status']) && $this->data['old_status'] == $this->data['tr_status'])) 655 { 656 $this->data['tr_status'] = self::STATUS_OPEN; 657 } 658 } 659 else 660 { 661 if (isset($this->data['reply_message'])) unset($this->data['reply_message']); 662 if (isset($this->data['canned_response'])) unset($this->data['canned_response']); 663 } 664 665 // Reset escalation flags on variable fields (comment, modified, etc.) 666 $esc = new tracker_escalations(); 667 $esc->reset($this->data, $changed); 668 } 669 if (!($err = parent::save())) 670 { 671 // try to resolve inline images which are not already resolved by mail_integration, 672 // like images from mailhandling or comments. 673 $replaced = false; 674 foreach((array)$this->data['link_to']['to_id'] as $link) 675 { 676 if (is_array($link) && !empty($link['id']['cid'])) 677 { 678 $link_callback = function($cid) use($link) { 679 if ($link['id']['cid'] == $cid) 680 { 681 return Api\Egw::link(Api\Vfs::download_url(Api\Link::vfs_path('tracker', $this->data['tr_id'], Api\Vfs::basename($link['id']['name'])))); 682 } 683 else 684 { 685 return "cid:".$cid; 686 } 687 }; 688 foreach(array('src','url','background') as $type) 689 { 690 $this->data['tr_description'] = mail_ui::resolve_inline_image_byType($this->data['tr_description'], null, null, null, $type, $link_callback); 691 $this->data['reply_message'] = mail_ui::resolve_inline_image_byType($this->data['reply_message'], null, null, null, $type, $link_callback); 692 } 693 $replaced = true; 694 } 695 } 696 if ($replaced) 697 { 698 $this->update (array( 699 'tr_description' => $this->data['tr_description'], 700 'reply_message' => $this->data['reply_message'] 701 )); 702 } 703 704 // create (and remove) links in custom fields 705 Api\Storage\Customfields::update_links('tracker',$this->data,$old,'tr_id'); 706 707 // so other apps can update eg. their titles and the cached title gets unset 708 Link::notify_update('tracker',$this->data['tr_id'],$this->data); 709 710 if (!is_object($this->tracking)) 711 { 712 $this->tracking = new tracker_tracking($this); 713 } 714 if($this->prefs['notify_own_modification']) 715 { 716 $this->tracking->notify_current_user = true; 717 } 718 $this->tracking->html_content_allow = true; 719 $notification_copy = $this->notification[$this->data['tr_tracker']]['copy'] ?: $this->notification[0]['copy']; 720 $no_notification = $autoreply['reply_text'] && !$notification_copy ? !($old) : $this->data['no_notifications']; 721 if (!$this->tracking->track($this->data,$old,$this->user,null,null,$no_notification)) 722 { 723 return $err == 0 && empty($this->tracking->errors) || !is_array($this->tracking->errors)? 724 0:implode(', ',$this->tracking->errors); 725 } 726 if ($autoreply) 727 { 728 $this->tracking->autoreply($this->data,$autoreply,$old); 729 } 730 } 731 return $err; 732 } 733 734 /** 735 * Get a list of all groups 736 * 737 * @param boolean $primary = false, when not ACL to change the group, return primary group only on new tickets 738 * @return array with gid => group-name pairs 739 */ 740 function &get_groups($primary=false) 741 { 742 static $groups = null; 743 static $primary_group = null; 744 745 if($primary) 746 { 747 if (isset($primary_group)) 748 { 749 return $primary_group; 750 } 751 } 752 else 753 { 754 if(isset($groups)) 755 { 756 return $groups; 757 } 758 } 759 760 $group_list = $GLOBALS['egw']->accounts->search(array('type' => 'groups', 'order' => 'account_lid', 'sort' => 'ASC')); 761 foreach($group_list as $gid) 762 { 763 $groups[$gid['account_id']] = $gid['account_lid']; 764 } 765 $primary_group[$GLOBALS['egw']->accounts->data['account_primary_group']] = $groups[$GLOBALS['egw']->accounts->data['account_primary_group']]; 766 767 return ($primary ? $primary_group : $groups); 768 } 769 770 /** 771 * Get the staff (technicians or admins) of a tracker 772 * 773 * @param int $tracker tracker-id or 0, 0 = staff of all trackers! 774 * @param int $return_groups = 2 0=users, 1=groups+users, 2=users+groups 775 * @param string $what = 'technicians' technicians=technicians (incl. admins), admins=only admins, users=only users 776 * @return array with uid => user-name pairs 777 */ 778 function &get_staff($tracker,$return_groups=2,$what='technicians') 779 { 780 static $staff_cache = null; 781 782 //echo "botracker::get_staff($tracker,$return_groups,$what)".function_backtrace()."<br>"; 783 //error_log(__METHOD__.__LINE__.array2string($tracker)); 784 // some caching 785 $r = 0; 786 $rv = array(); 787 foreach ((array)$tracker as $track) 788 { 789 if (!empty($tracker) && isset($staff_cache[$track]) && isset($staff_cache[$track][(int)$return_groups]) && 790 isset($staff_cache[$track][(int)$return_groups][$what])) 791 { 792 $r++; 793 //echo "from cache"; _debug_array($staff_cache[$tracker][$return_groups][$what]); 794 $rv = $rv+$staff_cache[$track][(int)$return_groups][$what]; 795 } 796 } 797 if (!empty($rv) && $r==count((array)$tracker)) return $rv; 798 799 $staff = array(); 800 if (is_array($tracker)) 801 { 802 $_tracker = $tracker; 803 array_unshift($_tracker,0); 804 } 805 else 806 { 807 $_tracker = array(0,$tracker); 808 } 809 810 switch($what) 811 { 812 case 'users': 813 case 'usersANDtechnicians': 814 if (is_null($this->users) || $this->users==='NULL') $this->users = array(); 815 foreach($tracker ? $_tracker : array_keys($this->users) as $t) 816 { 817 if (is_array($this->users[$t])) $staff = array_merge($staff,$this->users[$t]); 818 } 819 if ($what == 'users') break; 820 case 'technicians': 821 if (is_null($this->technicians) || $this->technicians==='NULL') $this->technicians = array(); 822 foreach($tracker ? $_tracker : array_keys($this->technicians) as $t) 823 { 824 if (is_array($this->technicians[$t])) $staff = array_merge($staff,$this->technicians[$t]); 825 } 826 // fall through, as technicians include admins 827 case 'admins': 828 if (is_null($this->admins) || $this->admins==='NULL') $this->admins = array(); 829 foreach($tracker ? $_tracker : array_keys($this->admins) as $t) 830 { 831 if (is_array($this->admins[$t])) $staff = array_merge($staff,$this->admins[$t]); 832 } 833 break; 834 } 835 836 // split users and groups and resolve the groups into there users 837 $users = $groups = array(); 838 foreach(array_unique($staff) as $uid) 839 { 840 if ($GLOBALS['egw']->accounts->get_type($uid) == 'g') 841 { 842 if ($return_groups) $groups[(string)$uid] = Api\Accounts::username($uid); 843 foreach((array)$GLOBALS['egw']->accounts->members($uid,true) as $u) 844 { 845 if (!isset($users[$u])) $users[$u] = Api\Accounts::username($u); 846 } 847 } 848 else // users 849 { 850 if (!isset($users[$uid])) $users[$uid] = Api\Accounts::username($uid); 851 } 852 } 853 // sort alphabetic 854 natcasesort($users); 855 natcasesort($groups); 856 857 // groups or users first 858 $staff_sorted = $this->allow_assign_groups == 1 ? $groups : $users; 859 860 if ($this->allow_assign_groups) // do we need a second one 861 { 862 foreach($this->allow_assign_groups == 1 ? $users : $groups as $uid => $label) 863 { 864 $staff_sorted[$uid] = $label; 865 } 866 } 867 //_debug_array($staff); 868 if (!is_array($tracker)) $staff_cache[$tracker][(int)$return_groups][$what] = $staff_sorted; 869 870 return $staff_sorted; 871 } 872 873 /** 874 * Check if a user (default current user) is an admin for the given tracker 875 * 876 * @param int $tracker ID of tracker 877 * @param int $user = null ID of user, default current user $this->user 878 * @param boolean $checkGivenUser = false flag to force the check If the given User is admin, no matter if $this->user=0 879 * @return boolean 880 */ 881 function is_admin($tracker,$user=null,$checkGivenUser=false) 882 { 883 if (is_null($user)) $user = $this->user; 884 885 $admins =& $this->get_staff($tracker,0,'admins'); 886 // evaluate $checkGivenUser flag to force the check If the given User is admin, no matter if $this->user=0 887 // this is used and needed to control (email)notification on close-pending 888 if ($checkGivenUser) 889 { 890 return isset($admins[$user]); 891 } 892 return $this->user===0 || isset($admins[$user]); // this->user is set to 0 by close_pending 893 } 894 895 /** 896 * Check if a user (default current user) is an technichan for the given tracker 897 * 898 * @param int $tracker ID of tracker 899 * @param int $user=null ID of user, default current user $this->user 900 * @return boolean 901 */ 902 function is_technician($tracker,$user=null,$checkgroups=false) 903 { 904 if (is_null($user)) $user = $this->user; 905 906 $technicians =& $this->get_staff($tracker,$checkgroups ? 2 : 0,'technicians'); 907 908 return isset($technicians[$user]); 909 } 910 911 /** 912 * Check if a user (default current user) is an user for the given tracker 913 * 914 * If queue ACL access is NOT enabled, we return is_tracker_user() (user is non-anonymous and has tracker run-rights) 915 * 916 * @param int $tracker ID of tracker 917 * @param int $user = null ID of user, default current user $this->user 918 * @return boolean 919 */ 920 function is_user($tracker,$user=null) 921 { 922 if (is_null($user)) $user = $this->user; 923 924 $users =& $this->get_staff($tracker,0,'users'); 925 926 return isset($users[$user]); 927 } 928 929 /** 930 * Check if a user (default current user) is staff member for the given tracker 931 * 932 * @param int $tracker ID of tracker 933 * @param int $user = null ID of user, default current user $this->user 934 * @return boolean 935 */ 936 function is_staff($tracker,$user=null) 937 { 938 if (is_null($user)) $user = $this->user; 939 940 return ($this->is_technician($tracker,$user) || $this->is_admin($tracker,$user)); 941 } 942 943 /** 944 * Check if a user (default current user) is anonymous 945 * 946 * @param int $user = null ID of user, default current user $this->user 947 * @return boolean 948 */ 949 function is_anonymous($user=null) 950 { 951 static $cache = array(); // some caching to not read Acl multiple times from the database ($user != $this->user) 952 953 if (!$user) $user = $this->user; 954 955 $anonymous =& $cache[$user]; 956 957 if (!isset($anonymous)) 958 { 959 if ($user == $this->user) 960 { 961 $anonymous = $GLOBALS['egw']->acl->check('anonymous',1,'phpgwapi'); 962 } 963 else 964 { 965 $rights = $GLOBALS['egw']->acl->get_all_location_rights($user,'phpgwapi',$use_memberships=false); 966 $anonymous = (boolean)$rights['anonymous']; 967 } 968 } 969 return $anonymous; 970 } 971 972 /** 973 * Check if a user (default current user) is a non-anoymous user with run-rights for tracker 974 * 975 * @param int $user = null ID of user, default current user $this->user 976 * @return boolean 977 */ 978 function is_tracker_user($user=null) 979 { 980 static $cache = array(); // some caching to not read Acl multiple times from the database ($user != $this->user) 981 982 if (is_null($user)) $user = $this->user; 983 984 $is_user =& $cache[$user]; 985 986 if (!isset($is_user)) 987 { 988 if ($this->is_anonymous($user)) 989 { 990 $reason = 'anonymous'; 991 $is_user = false; 992 } 993 elseif ($user == $this->user) 994 { 995 $is_user = isset($GLOBALS['egw_info']['user']['apps']['tracker']); 996 $reason = 'egw_info[user][apps][tracker] is '.(!$is_user ? 'NOT ' : '').'set'; 997 } 998 else 999 { 1000 $rights = $GLOBALS['egw']->acl->get_all_location_rights($user,'tracker',$use_memberships=true); 1001 $is_user = (boolean)$rights['run']; 1002 $reason = 'has '.(!$is_user ? 'NO' : '').'run rights'; 1003 } 1004 } 1005 //error_log(__METHOD__."($user) this->user=$this->user returning ($reason) ".array2string($is_user)); 1006 return $is_user; 1007 } 1008 1009 /** 1010 * Check if given or current ticket is private and user is not creator, assignee or admin 1011 * 1012 * @param array $data = null array with ticket or null for $this->data 1013 * @param int $user = null account_id or null for current user 1014 * @return boolean true = deny access to private ticket, false grant access (ticket not private or access allowed) 1015 */ 1016 function deny_private(array $data=null,$user=null) 1017 { 1018 if (!$user) $user = $this->user; 1019 if (!$data) $data = $this->data; 1020 $memberships = $GLOBALS['egw']->accounts->memberships($user, true); 1021 $memberships[] = $user; 1022 1023 return $data['tr_private'] && !($user == $data['tr_creator'] || $this->is_admin($data['tr_tracker'],$user) || 1024 $data['tr_assigned'] && array_intersect($memberships, $data['tr_assigned'])); 1025 } 1026 1027 /** 1028 * Check what rights the current user has on a given or the current tracker item ($this->data) or a given tracker 1029 * 1030 * @param int $needed or'ed together: TRACKER_ADMIN|TRACKER_TECHNICIAN|TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE 1031 * @param int $check_only_tracker = null should only the given tracker be checked and NO $this->data specific checks be performed, default no 1032 * @param int|array $data = null array with tracker item, integer tr_id or default null for loaded tracker item ($this->tracker) 1033 * @param int $user = null for which user to check, default current user 1034 * @param string $name = null something to put in error_log 1035 * @return boolean true if user has the $needed rights, false otherwise 1036 */ 1037 function check_rights($needed,$check_only_tracker=null,$data=null,$user=null,$name=null) 1038 { 1039 if (!$user) $user = $this->user; 1040 1041 if (!$data) 1042 { 1043 $data = $this->data; 1044 } 1045 elseif(!is_array($data)) 1046 { 1047 $backup = $this->data; 1048 if (!($data = $this->read(array('tr_id' => $data)))) 1049 { 1050 $access = false; 1051 $line = __LINE__; 1052 } 1053 $this->data = $backup; 1054 } 1055 $tracker = $check_only_tracker ? $check_only_tracker : $data['tr_tracker']; 1056 1057 if (isset($access)) 1058 { 1059 // nothing to do, already set 1060 } 1061 elseif (!$needed) 1062 { 1063 $access = false; 1064 $line = __LINE__; 1065 } 1066 // private tickets are only visible to creator, assignee and admins 1067 elseif(!$check_only_tracker && $this->deny_private($data,$user)) 1068 { 1069 $access = false; 1070 $line = __LINE__.' (private)'; 1071 } 1072 elseif ($needed & TRACKER_EVERYBODY) 1073 { 1074 $access = true; 1075 $line = __LINE__; 1076 } 1077 // item creator 1078 elseif (!$check_only_tracker && $needed & TRACKER_ITEM_CREATOR && $user == $data['tr_creator']) 1079 { 1080 $access = true; 1081 $line = __LINE__; 1082 } 1083 // item group 1084 elseif (!$check_only_tracker && $needed & TRACKER_ITEM_GROUP && 1085 ($memberships = $GLOBALS['egw']->accounts->memberships($user,true)) && in_array($data['tr_group'],$memberships)) 1086 { 1087 $access = true; 1088 $line = __LINE__; 1089 } 1090 // tracker user 1091 elseif ($needed & TRACKER_USER && $this->is_tracker_user($user)) 1092 { 1093 $access = true; 1094 $line = __LINE__; 1095 } 1096 // tracker admins and technicians 1097 elseif ($tracker) 1098 { 1099 if ($needed & TRACKER_ADMIN && $this->is_admin($tracker,$user)) 1100 { 1101 $access = true; 1102 $line = __LINE__; 1103 } 1104 elseif ($needed & TRACKER_TECHNICIAN && $this->is_technician($tracker,$user)) 1105 { 1106 $access = true; 1107 $line = __LINE__; 1108 } 1109 } 1110 if (isset($access)) 1111 { 1112 // nothing to do, already set 1113 } 1114 // new items: everyone is the owner of new items 1115 elseif (!$check_only_tracker && !$data['tr_id']) 1116 { 1117 $access = !!($needed & (TRACKER_ITEM_CREATOR|TRACKER_ITEM_NEW)); 1118 $line = __LINE__; 1119 } 1120 // assignee 1121 elseif (!$check_only_tracker && ($needed & TRACKER_ITEM_ASSIGNEE) && $data['tr_assigned']) 1122 { 1123 foreach((array)$data['tr_assigned'] as $assignee) 1124 { 1125 if ($user == $assignee) 1126 { 1127 $access = true; 1128 $line = __LINE__; 1129 break; 1130 } 1131 // group assinged 1132 if ($this->allow_assign_groups && $assignee < 0) 1133 { 1134 if (($members = $GLOBALS['egw']->accounts->members($assignee,true)) && in_array($user,$members)) 1135 { 1136 $access = true; 1137 $line = __LINE__; 1138 break; 1139 } 1140 } 1141 } 1142 } 1143 if (!isset($access)) 1144 { 1145 $access = false; 1146 $line = __LINE__; 1147 } 1148 //error_log(__METHOD__."($needed, $check_only_tracker, tr_id=$data[tr_id], user=$user) '$name' returning in $line ".array2string($access).(!$needed ? ': '.function_backtrace() : '')); 1149 unset($name); 1150 return $access; 1151 } 1152 1153 /** 1154 * Check access to the file store 1155 * 1156 * We need to map Tracker ACL to read or write access of the filestore: 1157 * - read access: a non-anonymous Tracker user, plus beeing able to read the tracker item 1158 * - write access: user is allowed to upload files or link with other entries 1159 * 1160 * @param int|array $id id of entry or entry array 1161 * @param int $check Acl::READ for read and Acl::EDIT for write or delete access 1162 * @param string $rel_path = null currently not used in Tracker 1163 * @param int $user = null for which user to check, default current user 1164 * @return boolean true if access is granted or false otherwise 1165 */ 1166 function file_access($id,$check,$rel_path=null,$user=null) 1167 { 1168 unset($rel_path); // unused, but required by function signature 1169 static $cache = array(); // as tracker does NOT cache read items, we run a cache here to not query items multiple times 1170 1171 if (!$user) $user = $this->user; 1172 1173 if (!is_array($id)) 1174 { 1175 $access =& $cache[$user][(int)$id][$check]; 1176 } 1177 if (!isset($access)) 1178 { 1179 $needed = $check == Acl::READ ? TRACKER_USER : $this->field_acl['link_to']; 1180 $name = 'file_access '.($check == Acl::READ ? 'read' : 'write'); 1181 1182 $access = $this->check_rights($needed,null,$id,$user,$name); 1183 } 1184 //error_log(__METHOD__."($id,$check,'$rel_path',$user) returning ".array2string($access)); 1185 return $access; 1186 } 1187 1188 /** 1189 * Check if users is allowed to vote and has not already voted 1190 * 1191 * @param int $tr_id = null tracker-id, default current tracker-item ($this->data) 1192 * @return int|boolean true for no rights, timestamp voted or null 1193 */ 1194 function check_vote($tr_id=null) 1195 { 1196 if (is_null($tr_id)) $tr_id = $this->data['tr_id']; 1197 1198 if (!$tr_id || !$this->check_rights($this->field_acl['vote'],null,null,null,'vote')) return true; 1199 1200 if ($this->is_anonymous()) 1201 { 1202 $ip = $_SERVER['REMOTE_ADDR'].(isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? ':'.$_SERVER['HTTP_X_FORWARDED_FOR'] : ''); 1203 } 1204 if (($time = parent::check_vote($tr_id,$this->user,$ip))) 1205 { 1206 $time += $this->tz_offset_s; 1207 } 1208 return $time; 1209 } 1210 1211 /** 1212 * Cast vote for given tracker-item 1213 * 1214 * @param int $tr_id = null tracker-id, default current tracker-item ($this->data) 1215 * @return boolean true = vote casted, false=already voted before 1216 */ 1217 function cast_vote($tr_id=null) 1218 { 1219 if (is_null($tr_id)) $tr_id = $this->data['tr_id']; 1220 1221 if ($this->check_vote($tr_id)) return false; 1222 1223 $ip = $_SERVER['REMOTE_ADDR'].(isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? ':'.$_SERVER['HTTP_X_FORWARDED_FOR'] : ''); 1224 1225 return parent::cast_vote($tr_id,$this->user,$ip); 1226 } 1227 1228 /** 1229 * Get tracker specific labels: tracker, version, categorie 1230 * 1231 * The labels are saved as categories and can be tracker specific (sub-cat of the tracker) or for all trackers. 1232 * The "cat_data" column stores if a tracker-cat is a "tracker", "version", "cat" or empty. 1233 * Labels need to be either tracker specific or global and NOT in denyglobal. 1234 * 1235 * @param string $type = 'tracker' 'tracker', 'version', 'cat', 'resolution' 1236 * @param int $tracker = null tracker to use or null to use $this->data['tr_tracker'] 1237 * @param int &$default = null on return default, if it is set 1238 */ 1239 function get_tracker_labels($type='tracker', $tracker=null, &$default=null) 1240 { 1241 if (is_null($this->all_cats)) 1242 { 1243 if (!isset($GLOBALS['egw']->categories)) 1244 { 1245 $GLOBALS['egw']->categories = new Api\Categories($this->user,'tracker'); 1246 } 1247 if (isset($GLOBALS['egw']->categories) && $GLOBALS['egw']->categories->app_name == 'tracker') 1248 { 1249 $cats = $GLOBALS['egw']->categories; 1250 } 1251 else 1252 { 1253 $cats = new Api\Categories($this->user,'tracker'); 1254 } 1255 $this->all_cats = $cats->return_array('all',0,false); 1256 if (!is_array($this->all_cats)) $this->all_cats = array(); 1257 //_debug_array($this->all_cats); 1258 } 1259 if (!$tracker) $tracker = $this->data['tr_tracker']; 1260 1261 $labels = array(); 1262 $default = $none_id = null; 1263 foreach($this->all_cats as $cat) 1264 { 1265 $cat_data =& $cat['data']; 1266 $cat_type = isset($cat_data['type']) ? $cat_data['type'] : 'cat'; 1267 if ($cat_type == $type && // cats need to be either tracker specific or global and tracker NOT in denyglobal 1268 (!$cat['parent'] && !($tracker && in_array($tracker, (array)$cat_data['denyglobal'])) || 1269 $cat['main'] == $tracker && $cat['id'] != $tracker)) 1270 { 1271 $labels[$cat['id']] = $cat['name']; 1272 // set default with precedence to tracker specific one 1273 if (is_array($cat_data) && isset($cat_data['isdefault']) && $cat_data['isdefault'] && (!isset($default) || $cat['main'] == $tracker)) 1274 { 1275 $default = $cat['id']; 1276 } 1277 if ($cat['name'] == 'None' && (!isset($none_id) || $cat['main'] == $tracker)) 1278 { 1279 $none_id = $cat['id']; 1280 } 1281 } 1282 } 1283 // if no default specified, fall back to id of cat with name "None" 1284 if (!isset($default) && isset($none_id)) 1285 { 1286 $default = $none_id; 1287 } 1288 1289 if ($type == 'tracker' && !$GLOBALS['egw_info']['user']['apps']['admin'] && $this->enabled_queue_acl_access) 1290 { 1291 foreach (array_keys($labels) as $tracker_id) 1292 { 1293 if (!$this->is_user($tracker_id,$this->user) && !$this->is_technician($tracker_id,$this->user) && !$this->is_admin($tracker_id,$this->user)) 1294 { 1295 unset($labels[$tracker_id]); 1296 } 1297 } 1298 } 1299 1300 $user_cat_default = $GLOBALS['egw_info']['user']['preferences']['tracker'][$tracker.'_cat_default']; 1301 if ($type == 'cat' && $user_cat_default && $labels[$user_cat_default]) 1302 { 1303 $default = $user_cat_default; 1304 } 1305 1306 natcasesort($labels); 1307 1308 //echo "botracker::get_tracker_labels('$type',$tracker)"; _debug_array($labels); 1309 return $labels; 1310 } 1311 1312 /** 1313 * Get tracker specific stati 1314 * 1315 * There's a bunch of pre-defined stati, plus statis stored as labels, which can be per tracker 1316 * 1317 * @param int $tracker = null tracker to use of null to use $this->data['tr_tracker'] 1318 * @param boolean $closed True to get 'closed' stati, false to get open stati, null for all 1319 */ 1320 function get_tracker_stati($tracker=null, $closed = null) 1321 { 1322 $stati = self::$stati + $this->get_tracker_labels('stati',$tracker); 1323 if($closed === null) return $stati; 1324 1325 $filtered = (!$closed ? array(self::STATUS_OPEN => 'Open(status)') : array(self::STATUS_CLOSED => 'Closed')); 1326 1327 foreach($stati as $id => $name) 1328 { 1329 if($id > 0 && $data = $GLOBALS['egw']->categories->id2name($id,'data')) 1330 { 1331 if($closed == $data['closed']) $filtered[$id] = $name; 1332 } 1333 } 1334 return $filtered; 1335 } 1336 1337 /** 1338 * Check if the given status is a closed status 1339 * 1340 * @param int $status ID of a status 1341 * @param int [$tracker] Optional tracker queue ID. If not provided, current tracker 1342 * will be used 1343 */ 1344 public function is_closed_status($status, $tracker = null) 1345 { 1346 $stati = $this->get_tracker_stati($tracker, true); 1347 return (array_key_exists($status, $stati)); 1348 } 1349 /** 1350 * Get tracker and category specific priorities 1351 * 1352 * Currently priorities are a fixed list with numeric values from 1 to 9 as keys and customizable labels 1353 * 1354 * @param int $tracker = null tracker to use or null to use tracker unspecific priorities 1355 * @param int $cat_id = null category to use or null to use categorie unspecific priorities 1356 * @param boolean $remove_empty = true should empty labels be displayed, default no 1357 * @param int& $default =null on return default, if it is set 1358 * @return array 1359 */ 1360 function get_tracker_priorities($tracker=null,$cat_id=null,$remove_empty=true, &$default = null) 1361 { 1362 if(!$tracker) 1363 { 1364 $tracker = 0; 1365 } 1366 if (isset($this->priorities[$tracker.'-'.$cat_id])) 1367 { 1368 $prios = $this->priorities[$tracker.'-'.$cat_id]; 1369 } 1370 elseif (isset($this->priorities[$tracker])) 1371 { 1372 $prios = $this->priorities[$tracker]; 1373 } 1374 elseif (isset($this->priorities['0-'.$cat_id])) 1375 { 1376 $prios = $this->priorities['0-'.$cat_id]; 1377 } 1378 elseif(isset($this->priorities[0])) 1379 { 1380 $prios = $this->priorities[0]; 1381 } 1382 else 1383 { 1384 $prios = self::$stock_priorities; 1385 } 1386 $default = $prios['default'] ? $prios['default'] : 5; 1387 unset($prios['default']); 1388 1389 if ($remove_empty) 1390 { 1391 foreach($prios as $key => $val) 1392 { 1393 if ($val === '') unset($prios[$key]); 1394 } 1395 } 1396 //echo "<p>".__METHOD__."(tracker=$tracker,$remove_empty) prios=".array2string($prios)."</p>\n"; 1397 return $prios; 1398 } 1399 1400 /** 1401 * Set the default category for the given tracker 1402 * 1403 * @param int $tracker 1404 * @param int $category 1405 * @param string $type 1406 */ 1407 public function set_default_category($tracker = null, $category = false, $type = 'cat') 1408 { 1409 foreach($this->all_cats as $cat) 1410 { 1411 $cat_data =& $cat['data']; 1412 $cat_type = isset($cat_data['type']) ? $cat_data['type'] : 'cat'; 1413 if ($cat_type == $type && // cats need to be tracker specific 1414 ($cat['main'] == $tracker && $cat['id'] != $tracker || 1415 !$tracker && !$cat['parent'] // or global 1416 )) 1417 { 1418 if($cat['id'] == $category) 1419 { 1420 $cat_data['isdefault'] = true; 1421 } 1422 else 1423 { 1424 unset($cat_data['isdefault']); 1425 } 1426 $GLOBALS['egw']->categories->edit($cat); 1427 } 1428 } 1429 // Need to reset before they're available 1430 $this->all_cats = null; 1431 $this->load_config(); 1432 $this->init(); 1433 } 1434 1435 /** 1436 * Check if the given tracker uses category specific priorities and eg. need to reload of user changes the cat 1437 * 1438 * @param int $tracker 1439 * @return boolean 1440 */ 1441 function tracker_has_cat_specific_priorities($tracker) 1442 { 1443 if (!$this->priorities) return false; 1444 1445 $prefix = (int)$tracker.'-'; 1446 $len = strlen($prefix); 1447 foreach(array_keys($this->priorities) as $key) 1448 { 1449 if (substr($key,0,$len) == $prefix || substr($key,0,2) == '0-') return true; 1450 } 1451 return false; 1452 } 1453 1454 /** 1455 * Reload the labels (tracker, cats, versions, projects) 1456 * 1457 */ 1458 function reload_labels() 1459 { 1460 unset($this->all_cats); 1461 $this->trackers = $this->get_tracker_labels(); 1462 } 1463 1464 /** 1465 * Get the canned response via it's id 1466 * 1467 * Canned responses are now saved in the the data array, as the description is limited to 255 chars, which is to small. 1468 * 1469 * @param int $id 1470 * @return string|boolean string with the response or false if id not found 1471 */ 1472 function get_canned_response($id) 1473 { 1474 foreach($this->all_cats as $cat) 1475 { 1476 if ($cat['data']['type'] == 'response' && $cat['id'] == $id) 1477 { 1478 return $cat['data']['response'] ? $cat['data']['response'] : $cat['description']; 1479 } 1480 } 1481 return false; 1482 } 1483 1484 /** 1485 * Try to autoassign to a new tracker item 1486 * 1487 * @return int|boolean account_id or false 1488 */ 1489 function autoassign() 1490 { 1491 foreach($this->all_cats as $cat) 1492 { 1493 if ($cat['id'] == $this->data['cat_id']) 1494 { 1495 $user = $cat['data']['autoassign']; 1496 1497 if ($user && $this->is_technician($this->data['tr_tracker'],$user,true)) 1498 { 1499 return $this->data['tr_assigned'] = $user; 1500 } 1501 } 1502 } 1503 return false; 1504 } 1505 1506 /** 1507 * get title for an tracker item identified by $entry 1508 * 1509 * Is called as hook to participate in the linking 1510 * 1511 * @param int|array $entry int ts_id or array with tracker item 1512 * @return string|boolean string with title, null if tracker item not found, false if no perms to view it 1513 */ 1514 function link_title( $entry ) 1515 { 1516 if (!is_array($entry)) 1517 { 1518 $entry = $this->read( $entry ); 1519 } 1520 if (!$entry) 1521 { 1522 return $entry; 1523 } 1524 return $this->trackers[$entry['tr_tracker']].' #'.$entry['tr_id'].': '.$entry['tr_summary']; 1525 } 1526 1527 /** 1528 * get titles for multiple tracker items 1529 * 1530 * Is called as hook to participate in the linking 1531 * 1532 * @param array $ids array with tracker id's 1533 * @return array with titles, see link_title 1534 */ 1535 function link_titles( $ids ) 1536 { 1537 $titles = array(); 1538 if (($tickets = $this->search(array('tr_id' => $ids),'tr_id,tr_tracker,tr_summary'))) 1539 { 1540 foreach($tickets as $ticket) 1541 { 1542 $titles[$ticket['tr_id']] = $this->link_title($ticket); 1543 } 1544 } 1545 // we assume all not returned tickets are not readable by the user, as we notify Link about each deleted ticket 1546 foreach($ids as $id) 1547 { 1548 if (!isset($titles[$id])) $titles[$id] = false; 1549 } 1550 return $titles; 1551 } 1552 1553 /** 1554 * query tracker for entries matching $pattern, we search only open entries 1555 * 1556 * Is called as hook to participate in the linking 1557 * 1558 * @param string $pattern pattern to search 1559 * @param array $options Array of options for the search 1560 * @return array with ts_id - title pairs of the matching entries 1561 */ 1562 function link_query( $pattern, Array &$options = array() ) 1563 { 1564 $limit = false; 1565 $result = array(); 1566 if($options['start'] || $options['num_rows']) { 1567 $limit = array($options['start'], $options['num_rows']); 1568 } 1569 $filter[]=array('tr_status != '. self::STATUS_DELETED); 1570 $filter['tr_tracker']=array_keys($this->trackers); 1571 foreach((array) $this->search($pattern,false,'tr_modified DESC','','%',false,'OR',$limit,$filter) as $item ) 1572 { 1573 if ($item) $result[$item['tr_id']] = $this->link_title($item); 1574 } 1575 $options['total'] = $this->total; 1576 return $result; 1577 } 1578 1579 /** 1580 * query rows for the nextmatch widget 1581 * 1582 * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter' 1583 * For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class. 1584 * @param array &$rows returned rows/competitions 1585 * @param array &$readonlys eg. to disable buttons based on Acl, not use here, maybe in a derived class 1586 * @param string $join = '' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or 1587 * "LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join! 1588 * @param boolean $need_full_no_count = false If true an unlimited query is run to determine the total number of rows, default false 1589 * @return int total number of rows 1590 */ 1591 function get_rows(&$query,&$rows,&$readonlys,$join=true,$need_full_no_count=false,$only_keys=false,$extra_cols=array()) 1592 { 1593 if($query['filter']) 1594 { 1595 $query['col_filter'][] = $this->date_filter($query['filter'],$query['startdate'],$query['enddate'],$query['order']); 1596 } 1597 return parent::get_rows($query,$rows,$readonlys,$join,$need_full_no_count,$only_keys,$extra_cols); 1598 } 1599 1600 /** 1601 * Add a new tracker-queue 1602 * 1603 * @param string $name 1604 * @param string $color 1605 * @return int|boolean integer tracker-id on success or false otherwise 1606 */ 1607 function add_tracker($name, $color = '') 1608 { 1609 $cats = new Api\Categories(Api\Categories::GLOBAL_ACCOUNT,'tracker'); // global cat! 1610 if ($name && ($id = $cats->add(array( 1611 'name' => $name, 1612 'descr' => 'tracker', 1613 'data' => serialize(array('type' => 'tracker', 'color' => $color)), 1614 'access' => 'public', 1615 )))) 1616 { 1617 $this->trackers[$id] = $name; 1618 1619 1620 return $id; 1621 } 1622 return false; 1623 } 1624 1625 /** 1626 * Change color for a tracker-queue 1627 * 1628 * @param type $tracker 1629 * @param type $color 1630 * @return boolean 1631 */ 1632 function change_color_tracker($tracker, $color) 1633 { 1634 $cats = new Api\Categories(Api\Categories::GLOBAL_ACCOUNT,'tracker'); 1635 if ($tracker > 0 && ($data = $cats->read($tracker))) 1636 { 1637 $data['data']['color'] = $color; 1638 $cats->edit($data); 1639 return true; 1640 } 1641 return false; 1642 } 1643 1644 /** 1645 * Rename a tracker-queue 1646 * 1647 * @param int $tracker 1648 * @param string $name 1649 * @return boolean true on success or false otherwise 1650 */ 1651 function rename_tracker($tracker,$name) 1652 { 1653 $cats = new Api\Categories(Api\Categories::GLOBAL_ACCOUNT,'tracker'); 1654 if ($tracker > 0 && !empty($name) && ($data = $cats->read($tracker))) 1655 { 1656 if ($data['name'] != $name) 1657 { 1658 $data['name'] = $this->trackers[$tracker] = $name; 1659 $cats->edit($data); 1660 } 1661 return true; 1662 } 1663 return false; 1664 } 1665 1666 /** 1667 * Delete a tracker include all items, categories, staff, ... 1668 * 1669 * @param int $tracker 1670 * @return boolean true on success, false otherwise 1671 */ 1672 function delete_tracker($tracker) 1673 { 1674 if (!$tracker) return false; 1675 1676 if (!is_object($this->historylog)) 1677 { 1678 $this->historylog = new Api\Storage\History('tracker'); 1679 } 1680 $ids = $this->query_list($this->table_name.'.tr_id','',array('tr_tracker' => $tracker)); 1681 if ($ids) $this->historylog->delete($ids); 1682 1683 $GLOBALS['egw']->categories->delete($tracker,true); 1684 1685 $this->reload_labels(); 1686 unset($this->admins[$tracker]); 1687 unset($this->technicians[$tracker]); 1688 unset($this->users[$tracker]); 1689 $this->mailhandling[$tracker]['interval'] = 0; // Cancel async job 1690 $this->delete(array('tr_tracker' => $tracker)); 1691 $this->save_config(); 1692 1693 return true; 1694 } 1695 1696 /** 1697 * Save the tracker configuration stored in various class-vars 1698 */ 1699 function save_config() 1700 { 1701 foreach($this->config_names as $name) 1702 { 1703 #echo "<p>calling Api\Config::save_value('$name','{$this->$name}','tracker')</p>\n"; 1704 Api\Config::save_value($name,$this->$name,'tracker'); 1705 } 1706 self::set_async_job($this->pending_close_days > 0); 1707 1708 $mailhandler = new tracker_mailhandler(); 1709 foreach((array)$this->mailhandling as $queue_id => $handling) { 1710 $mailhandler->set_async_job($queue_id, $handling['interval']); 1711 } 1712 } 1713 1714 /** 1715 * Load the tracker config into various class-vars 1716 * 1717 */ 1718 function load_config() 1719 { 1720 $migrate_config = false; // update old config-values, can be removed soon 1721 foreach((array)Api\Config::read('tracker') as $name => $value) 1722 { 1723 if (substr($name,0,13) == 'notification_') // update old config-values, can be removed soon 1724 { 1725 $this->notification[0][substr($name,13)] = $value; 1726 Api\Config::save_value($name,null,'tracker'); 1727 $migrate_config = true; 1728 continue; 1729 } 1730 $this->$name = $value; 1731 } 1732 if ($migrate_config) // update old config-values, can be removed soon 1733 { 1734 foreach($this->notification as $name => $value) 1735 { 1736 Api\Config::save_value($name,$value,'tracker'); 1737 } 1738 } 1739 1740 if (is_array($this->notification) && !$this->notification[0]['lang']) 1741 { 1742 $this->notification[0]['lang'] = $GLOBALS['egw']->preferences->default_prefs('common', 'lang'); 1743 } 1744 foreach(array( 1745 'tr_summary' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1746 'tr_tracker' => TRACKER_ITEM_NEW|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1747 'cat_id' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1748 'tr_version' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1749 'tr_status' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1750 'tr_description' => TRACKER_ITEM_NEW, 1751 'tr_creator' => TRACKER_ADMIN, 1752 'tr_assigned' => TRACKER_ITEM_CREATOR|TRACKER_ADMIN, 1753 'tr_private' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1754 'tr_budget' => TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1755 'tr_resolution' => TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1756 'tr_completion' => TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1757 'tr_priority' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1758 'tr_startdate' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1759 'tr_duedate' => TRACKER_ITEM_CREATOR|TRACKER_ADMIN, 1760 'tr_cc' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1761 'tr_group' => TRACKER_TECHNICIAN|TRACKER_ADMIN, 1762 'customfields' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1763 // set automatic by botracker::save() 1764 'tr_id' => 0, 1765 'tr_created' => 0, 1766 'tr_modifier' => 0, 1767 'tr_modified' => 0, 1768 'tr_closed' => 0, 1769 // pseudo fields used in edit 1770 'link_to' => TRACKER_ITEM_CREATOR|TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1771 'canned_response' => TRACKER_ITEM_ASSIGNEE|TRACKER_ADMIN, 1772 'reply_message' => TRACKER_USER, 1773 'edit_own_reply' => 0, 1774 'edit_reply' => TRACKER_ADMIN|TRACKER_TECHNICIAN, 1775 'add' => TRACKER_USER, 1776 'vote' => TRACKER_EVERYBODY, // TRACKER_USER for NO anon user 1777 'bounty' => TRACKER_EVERYBODY, 1778 'no_notifications' => TRACKER_ITEM_ASSIGNEE|TRACKER_TECHNICIAN|TRACKER_ADMIN, 1779 ) as $name => $value) 1780 { 1781 if (!isset($this->field_acl[$name])) $this->field_acl[$name] = $value; 1782 } 1783 1784 // Add date filters if using start/due dates 1785 if($this->show_dates) 1786 { 1787 $this->date_filters = array( 1788 'started' => false, 1789 'upcoming' => false 1790 ) + $this->date_filters; 1791 } 1792 } 1793 1794 /** 1795 * Check if exist and if not start or stop an async job to close pending items 1796 * 1797 * @param boolean $start = true true=start, false=stop 1798 */ 1799 static function set_async_job($start=true) 1800 { 1801 //echo '<p>'.__METHOD__.'('.($start?'true':'false').")</p>\n"; 1802 1803 $async = new Api\Asyncservice(); 1804 1805 if ($start === !$async->read('tracker-close-pending')) 1806 { 1807 if ($start) 1808 { 1809 $async->set_timer(array('hour' => '*'),'tracker-close-pending','tracker.tracker_bo.close_pending',null); 1810 } 1811 else 1812 { 1813 $async->cancel_timer('tracker-close-pending'); 1814 } 1815 } 1816 } 1817 1818 /** 1819 * Close pending tracker items, which are not answered withing $this->pending_close_days days 1820 */ 1821 function close_pending() 1822 { 1823 $this->user = 0; // we dont want to run under the id of the current or the user created the async job 1824 1825 if (($ids = $this->query_list('tr_id','tr_id',array( 1826 'tr_status' => self::STATUS_PENDING, 1827 'tr_modified < '.(time()-$this->pending_close_days*24*60*60), 1828 )))) 1829 { 1830 if (($default_lang = $GLOBALS['egw']->preferences->default_prefs('common','lang')) && // load the system default language 1831 Api\Translation::$userlang != $default_lang) 1832 { 1833 $save_lang = $GLOBALS['egw_info']['user']['preferences']['common']['lang']; 1834 $GLOBALS['egw_info']['user']['preferences']['common']['lang'] = $default_lang; 1835 Api\Translation::init(); 1836 } 1837 Api\Translation::add_app('tracker'); 1838 1839 foreach($ids as $tr_id) 1840 { 1841 if ($this->read($tr_id)) 1842 { 1843 $this->data['tr_status'] = self::STATUS_CLOSED; 1844 $this->data['reply_message'] = lang('This Tracker item was closed automatically by the system. It was previously set to a Pending status, and the original submitter did not respond within %1 days.',$this->pending_close_days); 1845 $this->save(); 1846 } 1847 } 1848 if ($save_lang) 1849 { 1850 $GLOBALS['egw_info']['user']['preferences']['common']['lang'] = $save_lang; 1851 Api\Translation::init(); 1852 } 1853 } 1854 } 1855 1856 /** 1857 * Read bounties specified by the given keys 1858 * 1859 * Reimplement to convert to user-time 1860 * 1861 * @param array|int $keys array with key(s) or integer bounty-id 1862 * @return array with bounties 1863 */ 1864 function read_bounties($keys) 1865 { 1866 if (!$this->allow_bounties) return array(); 1867 1868 if (($bounties = parent::read_bounties($keys))) 1869 { 1870 foreach($bounties as $n => $bounty) 1871 { 1872 foreach(array('bounty_created','bounty_confirmed') as $name) 1873 { 1874 if ($bounty[$name]) $bounties[$n][$name] += $this->tz_offset_s; 1875 } 1876 } 1877 } 1878 return $bounties; 1879 } 1880 1881 /** 1882 * Save or update a bounty 1883 * 1884 * @param array &$data 1885 * @return int|boolean integer bounty_id or false on error 1886 */ 1887 function save_bounty(&$data) 1888 { 1889 if (!$this->allow_bounties) return false; 1890 1891 if (($new = !$data['bounty_id'])) // new bounty 1892 { 1893 if (!$data['bounty_amount'] || !$data['bounty_name'] || !$data['bounty_email']) return false; 1894 1895 $data['bounty_creator'] = $this->user; 1896 $data['bounty_created'] = $this->now; 1897 if (!$data['tr_id']) $data['tr_id'] = $this->data['tr_id']; 1898 } 1899 else 1900 { 1901 if (!$this->is_admin($this->data['tr_tracker']) || 1902 !($bounties = $this->read_bounties(array('bounty_id' => $data['bounty_id'])))) 1903 { 1904 return false; 1905 } 1906 $old = $bounties[0]; 1907 1908 $data['bounty_confirmer'] = $this->user; 1909 $data['bounty_confirmed'] = $this->now; 1910 } 1911 // convert to server-time 1912 foreach(array('bounty_created','bounty_confirmed') as $name) 1913 { 1914 if ($data[$name]) $data[$name] -= $this->tz_offset_s; 1915 } 1916 if (($data['bounty_id'] = parent::save_bounty($data))) 1917 { 1918 $this->_bounty2history($data,$old); 1919 } 1920 // convert back to user-time 1921 foreach(array('bounty_created','bounty_confirmed') as $name) 1922 { 1923 if ($data[$name]) $data[$name] += $this->tz_offset_s; 1924 } 1925 return $data['bounty_id']; 1926 } 1927 1928 /** 1929 * Delete a bounty, the bounty must not be confirmed and you must be an tracker-admin! 1930 * 1931 * @param int $id 1932 * @return boolean true on success or false otherwise 1933 */ 1934 function delete_bounty($id) 1935 { 1936 //echo "<p>botracker::delete_bounty($id)</p>\n"; 1937 if (!($bounties = $this->read_bounties(array('bounty_id' => $id))) || 1938 $bounties[0]['bounty_confirmed'] || !$this->is_admin($this->data['tr_tracker'])) 1939 { 1940 return false; 1941 } 1942 if (parent::delete_bounty($id)) 1943 { 1944 $this->_bounty2history(null,$bounties[0]); 1945 1946 return true; 1947 } 1948 return false; 1949 } 1950 1951 /** 1952 * Historylog a bounty 1953 * 1954 * @internal 1955 * @param array $new new value 1956 * @param array $old = null old value 1957 */ 1958 function _bounty2history($new,$old=null) 1959 { 1960 if (!is_object($this->historylog)) 1961 { 1962 $this->historylog = new Api\Storage\History('tracker'); 1963 } 1964 if (is_null($new) && $old) 1965 { 1966 $status = 'xb'; // bounty deleted 1967 } 1968 elseif ($new['bounty_confirmed']) 1969 { 1970 $status = 'Bo'; // bounty confirmed 1971 } 1972 else 1973 { 1974 $status = 'bo'; // bounty set 1975 } 1976 $this->historylog->add($status,$this->data['tr_id'],$this->_serialize_bounty($new),$this->_serialize_bounty($old)); 1977 } 1978 1979 /** 1980 * Serialize the bounty for the historylog 1981 * 1982 * @internal 1983 * @param array $bounty 1984 * @return string 1985 */ 1986 function _serialize_bounty($bounty) 1987 { 1988 return !is_array($bounty) ? $bounty : '#'.$bounty['bounty_id'].', '.$bounty['bounty_name'].' <'.$bounty['bounty_email']. 1989 '> ('.$GLOBALS['egw']->accounts->id2name($bounty['bounty_creator']).') '. 1990 $bounty['bounty_amount'].' '.$this->currency.($bounty['bounty_confirmed'] ? ' Ok' : ''); 1991 } 1992 1993 /** 1994 * Provide response data of get_ticketId to client-side 1995 * JSON response to client with data = (int)ticket_id 1996 * or 0 if there was no ticket registered for the given subject 1997 * 1998 * @param type $_subject 1999 */ 2000 function ajax_getTicketId($_subject='') 2001 { 2002 $response = Api\Json\Response::get(); 2003 $response->data($this->get_ticketId($_subject)); 2004 } 2005 2006 /** 2007 * Try to extract a ticket number from a subject line 2008 * 2009 * @param string the subjectline from the incoming message, may be modified when we find some id, but not matching available trackers 2010 * @return int ticket ID, or 0 of no ticket ID was recognized 2011 */ 2012 function get_ticketId(&$subj='') 2013 { 2014 if (empty($subj)) 2015 { 2016 return 0; // Don't bother... 2017 } 2018 2019 // The subject line is expected to be in the format: 2020 // [Re: |Fwd: |etc ]<Tracker name> #<id>: <Summary> 2021 // allow colon or dash to separate Id from summary, as our notifications use a dash (' - ') and not a colon (': ') 2022 $tr_data = null; 2023 if (!preg_match_all("/(.*)( #[0-9]+:? ?-? )(.*)$/",$subj, $tr_data) && !$tr_data[2]) 2024 { 2025 return 0; // 2026 } 2027 if (strpos($tr_data[1][0],'#') !== false) // there is more than one part of the subject, that could be a tracker ID 2028 { 2029 // try once more, and modify the tr_data as we go for comparsion with tracker subject 2030 $buff = $tr_data; 2031 unset($tr_data); 2032 preg_match_all("/(.*)( #[0-9]+:? ?-? )(.*)$/",$buff[1][0], $tr_data); 2033 $tr_data[0][0] = $buff[0][0]; 2034 $tr_data[3][0] = $tr_data[3][0].$buff[2][0].$buff[3][0]; 2035 } 2036 $tr_id = null; 2037 $tracker_id = preg_match_all("/[0-9]+/",$tr_data[2][0], $tr_id) ? $tr_id[0][0] : null; 2038 if (!is_numeric($tracker_id)) return 0; // nothing found that looks like an ID 2039 //error_log(__METHOD__.array2string(array(0=>$tracker_id,1=>$subj))); 2040 $trackerData = $this->search(array('tr_id' => $tracker_id),'tr_summary'); 2041 if (is_numeric($tracker_id) && empty($trackerData)) // we have a numeric ID, but we could not find it in our database, is it external? 2042 { 2043 // we modify the subject as external tracker ids mess up our recognition of tracker ids 2044 if ($tracker_id > 0) $subj = $tr_data[1][0].str_replace('#','ID:',$tr_data[2][0]).$tr_data[3][0]; 2045 return 0; 2046 } 2047 // Use strncmp() here, since a Fwd might add a sqr bracket. 2048 if (strncmp(trim($trackerData[0]['tr_summary']), trim($tr_data[3][0]), strlen(trim($trackerData[0]['tr_summary']))) && 2049 // Some mail apps might truncate long subjects, 72 seems to be the smallest 2050 // Those are OK if what remains matches 2051 strlen($tr_data[3][0]) <= 70 && 2052 strpos(trim($tr_data[3][0]), trim($trackerData[0]['tr_summary'])) !== 0 2053 ) 2054 { 2055 //_debug_array($trackerData); 2056 return 0; // Summary doesn't match. Should this be ok? 2057 } 2058 return $tracker_id; 2059 } 2060 2061 /** 2062 * prepares the content of an email to be imported as tracker 2063 * 2064 * @author Klaus Leithoff <kl@stylite.de> 2065 * @param array $_addresses array of addresses 2066 * - array (email,name) 2067 * @param string $_subject 2068 * @param string $_message 2069 * @param array $_attachments 2070 * @param string $_ticket_id ticket id 2071 * @param int $_queue optional param to pass queue 2072 * @return array $content array for tracker_ui 2073 */ 2074 function prepare_import_mail($_addresses, $_subject, $_message, $_attachments, $_ticket_id, $_queue = 0) 2075 { 2076 foreach((array)$_addresses as $address) 2077 { 2078 if (is_array($address) && isset($address['email'])) 2079 { 2080 $emails[] =$address['email']; 2081 } 2082 else 2083 { 2084 $parsedAddresses = Api\Mail::parseAddressList($address); 2085 foreach($parsedAddresses as $i => $adr) 2086 { 2087 $emails[] = $adr->mailbox.'@'.$adr->host; 2088 } 2089 } 2090 } 2091 2092 $ticketId = $_ticket_id? $_ticket_id: $this->get_ticketId($_subject); 2093 //_debug_array('TickedId found:'.$ticketId); 2094 // we have to check if we know this ticket before proceeding 2095 if ($ticketId == 0) 2096 { 2097 $trackerentry = array( 2098 'tr_id' => 0, 2099 'tr_cc' => implode(', ',$emails), 2100 'tr_summary' => $_subject, 2101 'tr_description' => $_message, 2102 'referer' => false, 2103 'popup' => true, 2104 'link_to' => array( 2105 'to_app' => 'tracker', 2106 'to_id' => 0, 2107 ), 2108 ); 2109 // find the addressbookentry to link with 2110 $addressbook = new Api\Contacts(); 2111 $contacts = array(); 2112 $filter = array(); 2113 foreach ($emails as $mailadr) 2114 { 2115 // for LDAP, AD or UCS, check if the email belongs to an account first 2116 if ($GLOBALS['egw']->accounts->name2id($mailadr, 'account_email')) 2117 { 2118 $filter['owner'] = 0; 2119 } 2120 else 2121 { 2122 unset($filter['owner']); 2123 } 2124 $contacts = array_merge($contacts,(array)$addressbook->search( 2125 array( 2126 'email' => $mailadr, 2127 'email_home' => $mailadr 2128 ),'contact_id,contact_email,contact_email_home,egw_addressbook.account_id as account_id','','','',false,'OR',false,$filter,'',false)); 2129 } 2130 if (!$contacts || !is_array($contacts) || !is_array($contacts[0])) 2131 { 2132 $trackerentry['msg'] = lang('Attention: No Contact with address %1 found.',implode(', ',$emails)); 2133 $trackerentry['tr_creator'] = $this->user; // use current user as creator instead 2134 } 2135 else 2136 { 2137 // create as "ordinary" links and try to find/set the creator according to the sender (if it is a valid user to the all queues (tracker=0)) 2138 foreach ($contacts as $contact) 2139 { 2140 Link::link('tracker',$trackerentry['link_to']['to_id'],'addressbook',(isset($contact['contact_id'])?$contact['contact_id']:$contact['id'])); 2141 //error_log(__METHOD__.__LINE__.'linking ->'.array2string($trackerentry['link_to']['to_id']).' Status:'.$gg.': for'.(isset($contact['contact_id'])?$contact['contact_id']:$contact['id'])); 2142 $staff = $this->get_staff($tracker=0,0,'usersANDtechnicians'); 2143 if (empty($trackerentry['tr_creator'])&& $contact['account_id']>0) 2144 { 2145 $buff = explode(',',strtolower($trackerentry['tr_cc'])) ; 2146 unset($trackerentry['tr_cc']); 2147 foreach (array('email','email_home') as $k => $n) 2148 { 2149 if (!empty($contact[$n]) && !empty($buff)) 2150 { 2151 $break = false; 2152 $cnt = count($buff); 2153 $i = 0; 2154 while ( $break == false ) 2155 { 2156 $key = array_search(strtolower($contact[$n]),$buff); 2157 //_debug_array('found:'.$n.'->'.$key); 2158 if ($key !== false && isset($staff[$contact['account_id']])) 2159 { 2160 unset($buff[$key]); 2161 if (empty($trackerentry['tr_creator'])) $trackerentry['tr_creator'] = $contact['account_id']; 2162 } 2163 $i++; 2164 if ($key==false || $i>=$cnt) $break=true; 2165 } 2166 } 2167 } 2168 $trackerentry['tr_cc'] = implode(',',$buff); 2169 } 2170 } 2171 if (empty($trackerentry['tr_creator'])) 2172 { 2173 $trackerentry['msg'] = lang('Attention: No Contact with address %1 found.',implode(', ',$emails)); 2174 $trackerentry['tr_creator']=$this->user; 2175 } 2176 } 2177 } 2178 else 2179 { 2180 // find the addressbookentry to idetify the reply creator 2181 $addressbook = new Api\Contacts(); 2182 $contacts = array(); 2183 $filter = array(); 2184 foreach ($emails as $mailadr) 2185 { 2186 $contacts = array_merge($contacts,(array)$addressbook->search( 2187 array( 2188 'email' => $mailadr, 2189 'email_home' => $mailadr 2190 ),'contact_id,contact_email,contact_email_home,egw_addressbook.account_id as account_id','','','',false,'OR',false,$filter,'',false)); 2191 } 2192 $found= false; 2193 if (!$contacts || !is_array($contacts) || !is_array($contacts[0])) 2194 { 2195 $msg['reply_creator'] = $this->user; // use current user as creator instead 2196 } 2197 else 2198 { 2199 $msg['reply_creator'] = $this->user; 2200 // try to find/set the creator according to the sender (if it is a valid user to the all queues (tracker=0)) 2201 //error_log(__METHOD__.__LINE__.' Number of Contacts Found:'.count($contacts)); 2202 foreach ($contacts as $contact) 2203 { 2204 if (empty($contact['account_id'])) continue; 2205 //error_log(__METHOD__.__LINE__.' Contact Found:'.array2string($contact)); 2206 $staff = $this->get_staff($tracker=0,0,'usersANDtechnicians'); 2207 //error_log(__METHOD__.__LINE__.array2string($staff)); 2208 if ($found==false && $contact['account_id']>0) 2209 { 2210 foreach (array('email','email_home') as $k => $n) 2211 { 2212 if (!empty($contact[$n])) 2213 { 2214 // we found someone as staff, so we set it as current user 2215 if (isset($staff[$contact['account_id']])) 2216 { 2217 //error_log(__METHOD__.__LINE__.' ->'.$n.':'.array2string($contact)); 2218 $msg['reply_creator'] = $contact['account_id']; 2219 $found = true; 2220 } 2221 } 2222 } 2223 } 2224 } 2225 } 2226 if($found===false) $msg['msg'] = lang('Attention: No Contact with address %1 found.',implode(', ',$emails)); 2227 $this->read($ticketId); 2228 //echo "<p>data[tr_edit_mode]={$this->data['tr_edit_mode']}, this->htmledit=".array2string($this->htmledit)."</p>\n"; 2229 // Ascii Replies are converted to html, if htmledit is disabled (default), we allways convert, as this detection is weak 2230 if (is_array($this->data['replies'])) 2231 { 2232 foreach ($this->data['replies'] as &$reply) 2233 { 2234 if (!$this->htmledit || stripos($reply['reply_message'], '<br') === false && stripos($reply['reply_message'], '<p>') === false) 2235 { 2236 $reply['reply_message'] = nl2br(Api\Html::htmlspecialchars($reply['reply_message'])); 2237 } 2238 } 2239 } 2240 $trackerentry = $this->data; 2241 $trackerentry['reply_message'] = $_message; 2242 $trackerentry['popup'] = true; 2243 $trackerentry['link_to'] = array( 2244 'to_app' => 'tracker', 2245 'to_id' => $ticketId 2246 ); 2247 if (isset($msg['msg'])) $trackerentry['msg'] = $msg['msg']; 2248 if (isset($msg['reply_creator'])) $trackerentry['reply_creator'] = $msg['reply_creator']; 2249 } 2250 $queue = $_queue; // all; we use this, as we do not have a queue, when preparing a new ticket 2251 if (isset($trackerentry['tr_tracker']) && !empty($trackerentry['tr_tracker'])) $queue = $trackerentry['tr_tracker']; 2252 // since we only add replies for existing tickets, we do not mess with tr_cc in that case 2253 if ($ticketId==0 && (!isset($this->mailhandling[$queue]['auto_cc']) || empty($this->mailhandling[$queue]['auto_cc']))) unset($trackerentry['tr_cc']); 2254 if (is_array($_attachments)) 2255 { 2256 foreach ($_attachments as $attachment) 2257 { 2258 if($ticketId) 2259 { 2260 // Put it where it will be tied to the comment 2261 $attachment['name'] = 'comments/.new/'.$attachment['name']; 2262 } 2263 if($ticketId && $attachment['egw_data']) 2264 { 2265 Link::link('tracker',$trackerentry['link_to']['to_id'],array(array('app' => Link::DATA_APPNAME,'id'=>$attachment))); 2266 } 2267 else if($attachment['egw_data']) 2268 { 2269 Link::link('tracker',$trackerentry['link_to']['to_id'],Link::DATA_APPNAME,$attachment); 2270 } 2271 else if(is_readable($attachment['tmp_name']) || 2272 (Vfs::is_readable($attachment['tmp_name']) && parse_url($attachment['tmp_name'], PHP_URL_SCHEME) === 'vfs')) 2273 { 2274 Link::link('tracker',$trackerentry['link_to']['to_id'],'file',$attachment); 2275 } 2276 } 2277 } 2278 return $trackerentry; 2279 } 2280 2281 /** 2282 * return SQL implementing filtering by date 2283 * 2284 * If the currently sorted column is a date, we filter by that date, otherwise 2285 * we sort on tr_created 2286 * 2287 * @param string $name 2288 * @param int &$start 2289 * @param int &$end 2290 * @param string &$column 2291 * @return string 2292 */ 2293 function date_filter($name,&$start,&$end, $column = 'tr_created') 2294 { 2295 if(!$column || 2296 // Just these columns 2297 !in_array($column, array('tr_created','tr_startdate','tr_duedate','tr_closed')) 2298 // Any date column 2299 //!in_array($column, tracker_egw_record::$types['date-time'])) 2300 ) 2301 { 2302 $column = 'tr_created'; 2303 } 2304 switch(strtolower($name)) 2305 { 2306 case 'overdue': 2307 $limit = $this->now - $this->overdue_days * 24*60*60; 2308 2309 return "(tr_duedate IS NOT NULL and tr_duedate < {$this->now} 2310OR tr_duedate IS NULL AND 2311 CASE 2312 WHEN tr_modified IS NULL 2313 THEN 2314 tr_created < $limit 2315 ELSE 2316 tr_modified < $limit 2317 END 2318) "; 2319 2320 case 'started': 2321 return "(tr_startdate IS NULL OR tr_startdate < {$this->now} )" ; 2322 2323 case 'upcoming': 2324 return "(tr_startdate IS NOT NULL and tr_startdate > {$this->now} )"; 2325 } 2326 return Api\DateTime::sql_filter($name, $start, $end, $column, $this->date_filters); 2327 } 2328 2329 /** 2330 * set fields readonly, depending on the rights the current user has on the actual tracker item 2331 * 2332 * @return array 2333 */ 2334 function readonlys_from_acl() 2335 { 2336 //echo "<p>uitracker::get_readonlys() is_admin(tracker={$this->data['tr_tracker']})=".$this->is_admin($this->data['tr_tracker']).", id={$this->data['tr_id']}, creator={$this->data['tr_creator']}, assigned={$this->data['tr_assigned']}, user=$this->user</p>\n"; 2337 $readonlys = array(); 2338 foreach((array)$this->field_acl as $name => $rigths) 2339 { 2340 $readonlys[$name] = !$rigths || !$this->check_rights($rigths, null, null, null, $name); 2341 } 2342 if ($this->customfields && $readonlys['customfields']) 2343 { 2344 foreach(array_keys($this->customfields) as $name) 2345 { 2346 $readonlys['#'.$name] = $readonlys['customfields']; 2347 } 2348 } 2349 return $readonlys; 2350 } 2351 2352 /** 2353 * Get a list of users with open tickets, either created or assigned. 2354 * 2355 * Limits the amount of checking to do for notifications by only getting users with 2356 * tickets where the start date, due date or created + limit is within 4 days 2357 * 2358 * @return array of user IDs 2359 */ 2360 public function users_with_open_entries() 2361 { 2362 2363 $users = array(); 2364 2365 $config_limit = $this->now - $this->overdue_days * 24*60*60; 2366 $four_days = 4 * 24*60*60; 2367 2368 $where = array( 2369 'tr_status' => array_keys($this->get_tracker_stati(null, false)), 2370 "(tr_duedate IS NOT NULL and ABS(tr_duedate - {$this->now}) < {$four_days} 2371OR tr_startdate IS NOT NULL AND ABS(tr_startdate - {$this->now}) < $four_days 2372OR tr_duedate IS NULL AND 2373 CASE 2374 WHEN tr_modified IS NULL 2375 THEN 2376 ABS(tr_created - $config_limit) < $four_days 2377 ELSE 2378 ABS(tr_modified - $config_limit) < $four_days 2379 END 2380 ) " 2381 ); 2382 2383 // Creator 2384 foreach($this->db->select(self::TRACKER_TABLE, array('DISTINCT tr_creator'),$where,__LINE__,__FILE__) as $user) 2385 { 2386 $users[] = $user['tr_creator']; 2387 } 2388 2389 // Assigned 2390 foreach($this->db->select( 2391 self::ASSIGNEE_TABLE, array('DISTINCT tr_assigned'),$where,__LINE__,__FILE__, 2392 false, '',false,-1, 2393 'JOIN '.self::TRACKER_TABLE.' ON '.self::TRACKER_TABLE.'.tr_id = '.self::ASSIGNEE_TABLE.'.tr_id' 2394 ) as $user) 2395 { 2396 $user = $user['tr_assigned']; 2397 if($user < 0) $user = $GLOBALS['egw']->accounts->members($user,true); 2398 $users[] = $user; 2399 } 2400 2401 return array_unique($users); 2402 } 2403 2404 2405 /** 2406 * Deal with files from Add comment tab 2407 * 2408 * @param Arra $content 2409 */ 2410 protected function comment_files($tr_id, $reply_id, &$content = array()) 2411 { 2412 $path = "/apps/tracker/{$tr_id}/comments/"; 2413 2414 // Get files. Established tickets (should) let files go to VFS, we'll move them. 2415 $files = Api\Vfs::find( 2416 "{$path}.new/", 2417 array('type' => 'f', 'maxdepth' => 1) 2418 ); 2419 if(count($files) && !$content['reply_message']) 2420 { 2421 // No comment makes no sense, add something then get that reply ID 2422 $content['reply_message'] = lang('File(s) added'); 2423 $this->save(); 2424 return $this->comment_files($tr_id, $this->data['replies'][0]['reply_id'], $this->data); 2425 } 2426 $comment = Api\Accounts::username($GLOBALS['egw_info']['user']['account_id']) . ' ' . 2427 Api\DateTime::to(); 2428 foreach($files as $key => $file) 2429 { 2430 $file_name = is_array($file) && $file['name'] ? $file['name'] : Api\Vfs::basename($file); 2431 $file_path = is_array($file) ? ($file['tmp_name'] ? $file['tmp_name'] : $file['path']) : $file; 2432 $target = "$path{$reply_id}/{$file_name}"; 2433 2434 // Move to final destination 2435 Api\Vfs::rename($file_path, $target); 2436 2437 // Comment with user and date 2438 $result = Api\Vfs::proppatch($target, array(array('name' => 'comment', 'val' => $comment))); 2439 } 2440 $this->remove_comment_dir($tr_id); 2441 } 2442 2443 /** 2444 * Empty and remove the 'Add comment' temporary directory 2445 * 2446 * @param int $tr_id 2447 */ 2448 protected function remove_comment_dir($tr_id) 2449 { 2450 $path = "/apps/tracker/{$tr_id}/comments/.new"; 2451 if(!Api\Vfs::is_dir($path)) 2452 { 2453 return; 2454 } 2455 $files = array_diff(Api\Vfs::scandir($path), array('.','..')); 2456 foreach ($files as $file) { 2457 Api\Vfs::unlink("$path/$file"); 2458 } 2459 Api\Vfs::rmdir($path); 2460 } 2461} 2462