[ Index ] |
PHP Cross Reference of Eventum |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */ 3 // +----------------------------------------------------------------------+ 4 // | Eventum - Issue Tracking System | 5 // +----------------------------------------------------------------------+ 6 // | Copyright (c) 2003, 2004, 2005, 2006, 2007 MySQL AB | 7 // | | 8 // | This program is free software; you can redistribute it and/or modify | 9 // | it under the terms of the GNU General Public License as published by | 10 // | the Free Software Foundation; either version 2 of the License, or | 11 // | (at your option) any later version. | 12 // | | 13 // | This program is distributed in the hope that it will be useful, | 14 // | but WITHOUT ANY WARRANTY; without even the implied warranty of | 15 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 16 // | GNU General Public License for more details. | 17 // | | 18 // | You should have received a copy of the GNU General Public License | 19 // | along with this program; if not, write to: | 20 // | | 21 // | Free Software Foundation, Inc. | 22 // | 59 Temple Place - Suite 330 | 23 // | Boston, MA 02111-1307, USA. | 24 // +----------------------------------------------------------------------+ 25 // | Authors: João Prado Maia <jpm@mysql.com> | 26 // +----------------------------------------------------------------------+ 27 // 28 29 require_once (APP_INC_PATH . "class.validation.php"); 30 require_once (APP_INC_PATH . "class.time_tracking.php"); 31 require_once (APP_INC_PATH . "class.misc.php"); 32 require_once (APP_INC_PATH . "class.attachment.php"); 33 require_once (APP_INC_PATH . "class.auth.php"); 34 require_once (APP_INC_PATH . "class.user.php"); 35 require_once (APP_INC_PATH . "class.note.php"); 36 require_once (APP_INC_PATH . "class.history.php"); 37 require_once (APP_INC_PATH . "class.notification.php"); 38 require_once (APP_INC_PATH . "class.pager.php"); 39 require_once (APP_INC_PATH . "class.date.php"); 40 require_once (APP_INC_PATH . "class.category.php"); 41 require_once (APP_INC_PATH . "class.release.php"); 42 require_once (APP_INC_PATH . "class.resolution.php"); 43 require_once (APP_INC_PATH . "class.support.php"); 44 require_once (APP_INC_PATH . "class.scm.php"); 45 require_once (APP_INC_PATH . "class.impact_analysis.php"); 46 require_once (APP_INC_PATH . "class.custom_field.php"); 47 require_once (APP_INC_PATH . "class.phone_support.php"); 48 require_once (APP_INC_PATH . "class.status.php"); 49 require_once (APP_INC_PATH . "class.round_robin.php"); 50 require_once (APP_INC_PATH . "class.authorized_replier.php"); 51 require_once (APP_INC_PATH . "class.workflow.php"); 52 require_once (APP_INC_PATH . "class.priority.php"); 53 require_once (APP_INC_PATH . "class.reminder_action.php"); 54 require_once (APP_INC_PATH . "class.search_profile.php"); 55 require_once (APP_INC_PATH . "class.session.php"); 56 57 /** 58 * Class designed to handle all business logic related to the issues in the 59 * system, such as adding or updating them or listing them in the grid mode. 60 * 61 * @author João Prado Maia <jpm@mysql.com> 62 * @version $Revision: 1.114 $ 63 */ 64 65 class Issue 66 { 67 /** 68 * Method used to check whether a given issue ID exists or not. 69 * 70 * @access public 71 * @param integer $issue_id The issue ID 72 * @param boolean $check_project If we should check that this issue is in the current project 73 * @return boolean 74 */ 75 function exists($issue_id, $check_project = true) 76 { 77 $stmt = "SELECT 78 COUNT(*) 79 FROM 80 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 81 WHERE 82 iss_id=" . Misc::escapeInteger($issue_id); 83 if ($check_project) { 84 $stmt .= " AND 85 iss_prj_id = " . Auth::getCurrentProject(); 86 } 87 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 88 if (PEAR::isError($res)) { 89 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 90 return false; 91 } else { 92 if ($res == 0) { 93 return false; 94 } else { 95 return true; 96 } 97 } 98 } 99 100 101 /** 102 * Method used to get the list of column heading titles for the 103 * CSV export functionality of the issue listing screen. 104 * 105 * @access public 106 * @param integer $prj_id The project ID 107 * @return array The list of column heading titles 108 */ 109 function getColumnHeadings($prj_id) 110 { 111 $headings = array( 112 'Priority', 113 'Issue ID', 114 'Reporter', 115 ); 116 // hide the group column from the output if no 117 // groups are available in the database 118 $groups = Group::getAssocList($prj_id); 119 if (count($groups) > 0) { 120 $headings[] = 'Group'; 121 } 122 $headings[] = 'Assigned'; 123 $headings[] = 'Time Spent'; 124 // hide the category column from the output if no 125 // categories are available in the database 126 $categories = Category::getAssocList($prj_id); 127 if (count($categories) > 0) { 128 $headings[] = 'Category'; 129 } 130 if (Customer::hasCustomerIntegration($prj_id)) { 131 $headings[] = 'Customer'; 132 } 133 $headings[] = 'Status'; 134 $headings[] = 'Status Change Date'; 135 $headings[] = 'Last Action Date'; 136 $headings[] = 'Est. Dev. TIme'; 137 $headings[] = 'Summary'; 138 $headings[] = 'Expected Resolution Date'; 139 return $headings; 140 } 141 142 143 /** 144 * Method used to get the full list of date fields available to issues, to 145 * be used when customizing the issue listing screen in the 'last status 146 * change date' column. 147 * 148 * @access public 149 * @param boolean $display_customer_fields Whether to include any customer related fields or not 150 * @return array The list of available date fields 151 */ 152 function getDateFieldsAssocList($display_customer_fields = FALSE) 153 { 154 $fields = array( 155 'iss_created_date' => 'Created Date', 156 'iss_updated_date' => 'Last Updated Date', 157 'iss_last_response_date' => 'Last Response Date', 158 'iss_closed_date' => 'Closed Date' 159 ); 160 if ($display_customer_fields) { 161 $fields['iss_last_customer_action_date'] = 'Customer Action Date'; 162 } 163 asort($fields); 164 return $fields; 165 } 166 167 168 /** 169 * Method used to get the full list of issue IDs and their respective 170 * titles associated to a given project. 171 * 172 * @access public 173 * @param integer $prj_id The project ID 174 * @return array The list of issues 175 */ 176 function getAssocListByProject($prj_id) 177 { 178 $stmt = "SELECT 179 iss_id, 180 iss_summary 181 FROM 182 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 183 WHERE 184 iss_prj_id=" . Misc::escapeInteger($prj_id) . " 185 ORDER BY 186 iss_id ASC"; 187 $res = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 188 if (PEAR::isError($res)) { 189 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 190 return ""; 191 } else { 192 return $res; 193 } 194 } 195 196 197 /** 198 * Method used to get the status of a given issue. 199 * 200 * @access public 201 * @param integer $issue_id The issue ID 202 * @return integer The status ID 203 */ 204 function getStatusID($issue_id) 205 { 206 static $returns; 207 208 $issue_id = Misc::escapeInteger($issue_id); 209 210 if (!empty($returns[$issue_id])) { 211 return $returns[$issue_id]; 212 } 213 214 $stmt = "SELECT 215 iss_sta_id 216 FROM 217 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 218 WHERE 219 iss_id=$issue_id"; 220 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 221 if (PEAR::isError($res)) { 222 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 223 return ''; 224 } else { 225 $returns[$issue_id] = $res; 226 return $res; 227 } 228 } 229 230 231 /** 232 * Records the last customer action date for a given issue ID. 233 * 234 * @access public 235 * @param integer $issue_id The issue ID 236 * @return integer 1 if the update worked, -1 otherwise 237 */ 238 function recordLastCustomerAction($issue_id) 239 { 240 $stmt = "UPDATE 241 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 242 SET 243 iss_last_customer_action_date='" . Date_API::getCurrentDateGMT() . "', 244 iss_last_public_action_date='" . Date_API::getCurrentDateGMT() . "', 245 iss_last_public_action_type='customer action' 246 WHERE 247 iss_id=" . Misc::escapeInteger($issue_id); 248 $res = $GLOBALS["db_api"]->dbh->query($stmt); 249 if (PEAR::isError($res)) { 250 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 251 return -1; 252 } else { 253 return 1; 254 } 255 } 256 257 258 /** 259 * Returns the customer ID associated with the given issue ID. 260 * 261 * @access public 262 * @param integer $issue_id The issue ID 263 * @return integer The customer ID associated with the issue 264 */ 265 function getCustomerID($issue_id) 266 { 267 static $returns; 268 269 $issue_id = Misc::escapeInteger($issue_id); 270 271 if (!empty($returns[$issue_id])) { 272 return $returns[$issue_id]; 273 } 274 275 $stmt = "SELECT 276 iss_customer_id 277 FROM 278 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 279 WHERE 280 iss_id=$issue_id"; 281 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 282 if (PEAR::isError($res)) { 283 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 284 return ''; 285 } else { 286 $returns[$issue_id] = $res; 287 return $res; 288 } 289 } 290 291 292 /** 293 * Returns the contract ID associated with the given issue ID. 294 * 295 * @access public 296 * @param integer $issue_id The issue ID 297 * @return integer The customer ID associated with the issue 298 */ 299 function getContractID($issue_id) 300 { 301 static $returns; 302 303 $issue_id = Misc::escapeInteger($issue_id); 304 305 if (!empty($returns[$issue_id])) { 306 return $returns[$issue_id]; 307 } 308 309 $stmt = "SELECT 310 iss_customer_contract_id 311 FROM 312 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 313 WHERE 314 iss_id=$issue_id"; 315 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 316 if (PEAR::isError($res)) { 317 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 318 return ''; 319 } else { 320 $returns[$issue_id] = $res; 321 return $res; 322 } 323 } 324 325 326 /** 327 * Sets the contract ID for a specific issue. 328 * 329 * @access public 330 * @param integer $issue_id The issue ID 331 * @param integer The contract ID 332 * @return integer 1 if the update worked, -1 otherwise 333 */ 334 function setContractID($issue_id, $contract_id) 335 { 336 $issue_id = Misc::escapeInteger($issue_id); 337 338 $old_contract_id = Issue::getContractID($issue_id); 339 340 $stmt = "UPDATE 341 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 342 SET 343 iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . " 344 WHERE 345 iss_id=$issue_id"; 346 $res = $GLOBALS["db_api"]->dbh->query($stmt); 347 if (PEAR::isError($res)) { 348 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 349 return -1; 350 } else { 351 // log this 352 History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID())); 353 return 1; 354 } 355 } 356 357 358 /** 359 * Returns the customer ID associated with the given issue ID. 360 * 361 * @access public 362 * @param integer $issue_id The issue ID 363 * @return integer The customer ID associated with the issue 364 */ 365 function getContactID($issue_id) 366 { 367 static $returns; 368 369 $issue_id = Misc::escapeInteger($issue_id); 370 371 if (!empty($returns[$issue_id])) { 372 return $returns[$issue_id]; 373 } 374 375 $stmt = "SELECT 376 iss_customer_contact_id 377 FROM 378 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 379 WHERE 380 iss_id=$issue_id"; 381 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 382 if (PEAR::isError($res)) { 383 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 384 return ''; 385 } else { 386 $returns[$issue_id] = $res; 387 return $res; 388 } 389 } 390 391 392 /** 393 * Method used to get the project associated to a given issue. 394 * 395 * @access public 396 * @param integer $issue_id The issue ID 397 * @return integer The project ID 398 */ 399 function getProjectID($issue_id) 400 { 401 static $returns; 402 403 $issue_id = Misc::escapeInteger($issue_id); 404 405 if (!empty($returns[$issue_id])) { 406 return $returns[$issue_id]; 407 } 408 409 $stmt = "SELECT 410 iss_prj_id 411 FROM 412 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 413 WHERE 414 iss_id=$issue_id"; 415 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 416 if (PEAR::isError($res)) { 417 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 418 return ''; 419 } else { 420 $returns[$issue_id] = $res; 421 return $res; 422 } 423 } 424 425 426 /** 427 * Method used to remotely assign a given issue to an user. 428 * 429 * @access public 430 * @param integer $issue_id The issue ID 431 * @param integer $usr_id The user ID of the person performing the change 432 * @param boolean $assignee The user ID of the assignee 433 * @return integer The status ID 434 */ 435 function remoteAssign($issue_id, $usr_id, $assignee) 436 { 437 Workflow::handleAssignmentChange(Issue::getProjectID($issue_id), $issue_id, $usr_id, Issue::getDetails($issue_id), array($assignee), true); 438 // clear up the assignments for this issue, and then assign it to the current user 439 Issue::deleteUserAssociations($issue_id, $usr_id); 440 $res = Issue::addUserAssociation($usr_id, $issue_id, $assignee, false); 441 if ($res != -1) { 442 // save a history entry about this... 443 History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id)); 444 Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions(), false); 445 if ($assignee != $usr_id) { 446 Notification::notifyNewAssignment(array($assignee), $issue_id); 447 } 448 } 449 return $res; 450 } 451 452 453 /** 454 * Method used to set the status of a given issue. 455 * 456 * @access public 457 * @param integer $issue_id The issue ID 458 * @param integer $status_id The new status ID 459 * @param boolean $notify If a notification should be sent about this change. 460 * @return integer 1 if the update worked, -1 otherwise 461 */ 462 function setStatus($issue_id, $status_id, $notify = false) 463 { 464 $issue_id = Misc::escapeInteger($issue_id); 465 $status_id = Misc::escapeInteger($status_id); 466 467 // check if the status is already set to the 'new' one 468 if (Issue::getStatusID($issue_id) == $status_id) { 469 return -1; 470 } 471 472 $old_status = Issue::getStatusID($issue_id); 473 $old_details = Status::getDetails($old_status); 474 475 $stmt = "UPDATE 476 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 477 SET 478 iss_sta_id=$status_id, 479 iss_updated_date='" . Date_API::getCurrentDateGMT() . "', 480 iss_last_public_action_date='" . Date_API::getCurrentDateGMT() . "', 481 iss_last_public_action_type='update' 482 WHERE 483 iss_id=$issue_id"; 484 $res = $GLOBALS["db_api"]->dbh->query($stmt); 485 if (PEAR::isError($res)) { 486 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 487 return -1; 488 } else { 489 // clear out the last-triggered-reminder flag when changing the status of an issue 490 Reminder_Action::clearLastTriggered($issue_id); 491 492 // if old status was closed and new status is not, clear closed data from issue. 493 if (@$old_details['sta_is_closed'] == 1) { 494 $new_details = Status::getDetails($status_id); 495 if ($new_details['sta_is_closed'] != 1) { 496 Issue::clearClosed($issue_id); 497 } 498 } 499 500 if ($notify) { 501 Notification::notifyStatusChange($issue_id, $old_status, $status_id); 502 } 503 504 return 1; 505 } 506 } 507 508 509 /** 510 * Method used to remotely set the status of a given issue. 511 * 512 * @access public 513 * @param integer $issue_id The issue ID 514 * @param integer $usr_id The user ID of the person performing this change 515 * @param integer $new_status The new status ID 516 * @return integer 1 if the update worked, -1 otherwise 517 */ 518 function setRemoteStatus($issue_id, $usr_id, $new_status) 519 { 520 $sta_id = Status::getStatusID($new_status); 521 522 $res = Issue::setStatus($issue_id, $sta_id); 523 if ($res == 1) { 524 // record history entry 525 History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id)); 526 } 527 return $res; 528 } 529 530 531 /** 532 * Method used to set the release of an issue 533 * 534 * @access public 535 * @param integer $issue_id The ID of the issue 536 * @param integer $pre_id The ID of the release to set this issue too 537 * @return integer 1 if the update worked, -1 otherwise 538 */ 539 function setRelease($issue_id, $pre_id) 540 { 541 $issue_id = Misc::escapeInteger($issue_id); 542 $pre_id = Misc::escapeInteger($pre_id); 543 544 if ($pre_id != Issue::getRelease($issue_id)) { 545 $sql = "UPDATE 546 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 547 SET 548 iss_pre_id = $pre_id 549 WHERE 550 iss_id = $issue_id"; 551 $res = $GLOBALS["db_api"]->dbh->query($sql); 552 if (PEAR::isError($res)) { 553 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 554 return -1; 555 } else { 556 return 1; 557 } 558 } 559 } 560 561 562 /** 563 * Returns the current release of an issue 564 * 565 * @access public 566 * @param integer $issue_id The ID of the issue 567 * @return integer The release 568 */ 569 function getRelease($issue_id) 570 { 571 $sql = "SELECT 572 iss_pre_id 573 FROM 574 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 575 WHERE 576 iss_id = " . Misc::escapeInteger($issue_id); 577 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 578 if (PEAR::isError($res)) { 579 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 580 return 0; 581 } else { 582 return $res; 583 } 584 } 585 586 587 /** 588 * Method used to set the priority of an issue 589 * 590 * @access public 591 * @param integer $issue_id The ID of the issue 592 * @param integer $pri_id The ID of the priority to set this issue too 593 * @return integer 1 if the update worked, -1 otherwise 594 */ 595 function setPriority($issue_id, $pri_id) 596 { 597 $issue_id = Misc::escapeInteger($issue_id); 598 $pri_id = Misc::escapeInteger($pri_id); 599 600 if ($pri_id != Issue::getPriority($issue_id)) { 601 $sql = "UPDATE 602 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 603 SET 604 iss_pri_id = $pri_id 605 WHERE 606 iss_id = $issue_id"; 607 $res = $GLOBALS["db_api"]->dbh->query($sql); 608 if (PEAR::isError($res)) { 609 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 610 return -1; 611 } else { 612 return 1; 613 } 614 } 615 } 616 617 618 /** 619 * Returns the current issue priority 620 * 621 * @access public 622 * @param integer $issue_id The ID of the issue 623 * @return integer The priority 624 */ 625 function getPriority($issue_id) 626 { 627 $sql = "SELECT 628 iss_pri_id 629 FROM 630 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 631 WHERE 632 iss_id = " . Misc::escapeInteger($issue_id); 633 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 634 if (PEAR::isError($res)) { 635 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 636 return 0; 637 } else { 638 return $res; 639 } 640 } 641 642 643 /** 644 * Method used to set the category of an issue 645 * 646 * @access public 647 * @param integer $issue_id The ID of the issue 648 * @param integer $prc_id The ID of the category to set this issue too 649 * @return integer 1 if the update worked, -1 otherwise 650 */ 651 function setCategory($issue_id, $prc_id) 652 { 653 $issue_id = Misc::escapeInteger($issue_id); 654 $prc_id = Misc::escapeInteger($prc_id); 655 656 if ($prc_id != Issue::getPriority($issue_id)) { 657 $sql = "UPDATE 658 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 659 SET 660 iss_prc_id = $prc_id 661 WHERE 662 iss_id = $issue_id"; 663 $res = $GLOBALS["db_api"]->dbh->query($sql); 664 if (PEAR::isError($res)) { 665 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 666 return -1; 667 } else { 668 return 1; 669 } 670 } 671 } 672 673 674 /** 675 * Returns the current issue category 676 * 677 * @access public 678 * @param integer $issue_id The ID of the issue 679 * @return integer The category 680 */ 681 function getCategory($issue_id) 682 { 683 $sql = "SELECT 684 iss_prc_id 685 FROM 686 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 687 WHERE 688 iss_id = " . Misc::escapeInteger($issue_id); 689 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 690 if (PEAR::isError($res)) { 691 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 692 return 0; 693 } else { 694 return $res; 695 } 696 } 697 698 699 /** 700 * Method used to get all issues associated with a status that doesn't have 701 * the 'closed' context. 702 * 703 * @access public 704 * @param integer $prj_id The project ID to list issues from 705 * @param integer $usr_id The user ID of the user requesting this information 706 * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address 707 * @param integer $status_id The status ID to be used to restrict results 708 * @return array The list of open issues 709 */ 710 function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id) 711 { 712 $prj_id = Misc::escapeInteger($prj_id); 713 $status_id = Misc::escapeInteger($status_id); 714 $projects = Project::getRemoteAssocListByUser($usr_id); 715 if (@count($projects) == 0) { 716 return ''; 717 } 718 719 $stmt = "SELECT 720 iss_id, 721 iss_summary, 722 sta_title 723 FROM 724 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 725 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 726 LEFT JOIN 727 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user 728 ON 729 isu_iss_id=iss_id 730 WHERE "; 731 if (!empty($status_id)) { 732 $stmt .= " sta_id=$status_id AND "; 733 } 734 $stmt .= " 735 iss_prj_id=$prj_id AND 736 sta_id=iss_sta_id AND 737 sta_is_closed=0"; 738 if ($show_all_issues == false) { 739 $stmt .= " AND 740 isu_usr_id=$usr_id"; 741 } 742 $stmt .= "\nGROUP BY 743 iss_id"; 744 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 745 if (PEAR::isError($res)) { 746 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 747 return ''; 748 } else { 749 if (count($res) > 0) { 750 Issue::getAssignedUsersByIssues($res); 751 } 752 return $res; 753 } 754 } 755 756 757 /** 758 * Method used to build the required parameters to simulate an email reply 759 * to the user who reported the issue, using the issue details like summary 760 * and description as email fields. 761 * 762 * @access public 763 * @param integer $issue_id The issue ID 764 * @return array The email parameters 765 */ 766 function getReplyDetails($issue_id) 767 { 768 $issue_id = Misc::escapeInteger($issue_id); 769 770 $stmt = "SELECT 771 iss_created_date, 772 usr_full_name AS reporter, 773 usr_email AS reporter_email, 774 iss_description AS description, 775 iss_summary AS sup_subject 776 FROM 777 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 778 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 779 WHERE 780 iss_usr_id=usr_id AND 781 iss_id=$issue_id"; 782 $res = $GLOBALS["db_api"]->dbh->getRow($stmt, DB_FETCHMODE_ASSOC); 783 if (PEAR::isError($res)) { 784 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 785 return ''; 786 } else { 787 $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"]; 788 $res['created_date_ts'] = Date_API::getUnixTimestamp($res['iss_created_date'], 'GMT'); 789 return $res; 790 } 791 } 792 793 794 /** 795 * Method used to record the last updated timestamp for a given 796 * issue ID. 797 * 798 * @access public 799 * @param integer $issue_id The issue ID 800 * @param string $type The type of update that was made (optional) 801 * @return boolean 802 */ 803 function markAsUpdated($issue_id, $type = false) 804 { 805 $public = array("staff response", "customer action", "file uploaded", "user response"); 806 $stmt = "UPDATE 807 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 808 SET 809 iss_updated_date='" . Date_API::getCurrentDateGMT() . "'\n"; 810 if ($type != false) { 811 if (in_array($type, $public)) { 812 $field = "iss_last_public_action_"; 813 } else { 814 $field = "iss_last_internal_action_"; 815 } 816 $stmt .= ",\n " . $field . "date = '" . Date_API::getCurrentDateGMT() . "',\n" . 817 $field . "type ='" . Misc::escapeString($type) . "'\n"; 818 } 819 $stmt .= "WHERE 820 iss_id=" . Misc::escapeInteger($issue_id); 821 $res = $GLOBALS["db_api"]->dbh->query($stmt); 822 if (PEAR::isError($res)) { 823 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 824 return false; 825 } else { 826 // update last response dates if this is a staff response 827 if ($type == "staff response") { 828 $stmt = "UPDATE 829 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 830 SET 831 iss_last_response_date='" . Date_API::getCurrentDateGMT() . "' 832 WHERE 833 iss_id = " . Misc::escapeInteger($issue_id); 834 $GLOBALS["db_api"]->dbh->query($stmt); 835 $stmt = "UPDATE 836 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 837 SET 838 iss_first_response_date='" . Date_API::getCurrentDateGMT() . "' 839 WHERE 840 iss_first_response_date IS NULL AND 841 iss_id = " . Misc::escapeInteger($issue_id); 842 $GLOBALS["db_api"]->dbh->query($stmt); 843 } 844 845 return true; 846 } 847 } 848 849 850 /** 851 * Method used to check whether a given issue has duplicates 852 * or not. 853 * 854 * @access public 855 * @param integer $issue_id The issue ID 856 * @return boolean 857 */ 858 function hasDuplicates($issue_id) 859 { 860 $stmt = "SELECT 861 COUNT(iss_id) 862 FROM 863 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 864 WHERE 865 iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id); 866 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 867 if (PEAR::isError($res)) { 868 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 869 return false; 870 } else { 871 if ($res == 0) { 872 return false; 873 } else { 874 return true; 875 } 876 } 877 } 878 879 880 /** 881 * Method used to update the duplicated issues for a given 882 * issue ID. 883 * 884 * @access public 885 * @param integer $issue_id The issue ID 886 * @return integer 1 if the update worked, -1 otherwise 887 */ 888 function updateDuplicates($issue_id) 889 { 890 $issue_id = Misc::escapeInteger($issue_id); 891 892 $ids = Issue::getDuplicateList($issue_id); 893 if ($ids == '') { 894 return -1; 895 } 896 $ids = @array_keys($ids); 897 $stmt = "UPDATE 898 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 899 SET 900 iss_updated_date='" . Date_API::getCurrentDateGMT() . "', 901 iss_last_internal_action_date='" . Date_API::getCurrentDateGMT() . "', 902 iss_last_internal_action_type='updated', 903 iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ","; 904 if (@$_POST["keep"] == "no") { 905 $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ","; 906 } 907 $stmt .= " 908 iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ", 909 iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ", 910 iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . " 911 WHERE 912 iss_id IN (" . implode(", ", $ids) . ")"; 913 $res = $GLOBALS["db_api"]->dbh->query($stmt); 914 if (PEAR::isError($res)) { 915 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 916 return -1; 917 } else { 918 // record the change 919 for ($i = 0; $i < count($ids); $i++) { 920 History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'), 921 "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues."); 922 } 923 return 1; 924 } 925 } 926 927 928 /** 929 * Method used to get a list of the duplicate issues for a given 930 * issue ID. 931 * 932 * @access public 933 * @param integer $issue_id The issue ID 934 * @return array The list of duplicates 935 */ 936 function getDuplicateList($issue_id) 937 { 938 $res = Issue::getDuplicateDetailsList($issue_id); 939 if (@count($res) == 0) { 940 return ''; 941 } else { 942 $list = array(); 943 for ($i = 0; $i < count($res); $i++) { 944 $list[$res[$i]['issue_id']] = $res[$i]['title']; 945 } 946 return $list; 947 } 948 } 949 950 951 /** 952 * Method used to get a list of the duplicate issues (and their details) 953 * for a given issue ID. 954 * 955 * @access public 956 * @param integer $issue_id The issue ID 957 * @return array The list of duplicates 958 */ 959 function getDuplicateDetailsList($issue_id) 960 { 961 static $returns; 962 963 if (!empty($returns[$issue_id])) { 964 return $returns[$issue_id]; 965 } 966 967 $stmt = "SELECT 968 iss_id issue_id, 969 iss_summary title, 970 sta_title current_status, 971 sta_is_closed is_closed 972 FROM 973 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 974 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 975 WHERE 976 iss_sta_id=sta_id AND 977 iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id); 978 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 979 if (PEAR::isError($res)) { 980 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 981 return array(); 982 } else { 983 $returns[$issue_id] = $res; 984 return $res; 985 } 986 } 987 988 989 /** 990 * Method used to clear the duplicate status of an issue. 991 * 992 * @access public 993 * @param integer $issue_id The issue ID 994 * @return integer 1 if the update worked, -1 otherwise 995 */ 996 function clearDuplicateStatus($issue_id) 997 { 998 $issue_id = Misc::escapeInteger($issue_id); 999 $stmt = "UPDATE 1000 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1001 SET 1002 iss_updated_date='" . Date_API::getCurrentDateGMT() . "', 1003 iss_last_internal_action_date='" . Date_API::getCurrentDateGMT() . "', 1004 iss_last_internal_action_type='updated', 1005 iss_duplicated_iss_id=NULL 1006 WHERE 1007 iss_id=$issue_id"; 1008 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1009 if (PEAR::isError($res)) { 1010 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1011 return -1; 1012 } else { 1013 // record the change 1014 History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID())); 1015 return 1; 1016 } 1017 } 1018 1019 1020 /** 1021 * Method used to mark an issue as a duplicate of an existing one. 1022 * 1023 * @access public 1024 * @param integer $issue_id The issue ID 1025 * @return integer 1 if the update worked, -1 otherwise 1026 */ 1027 function markAsDuplicate($issue_id) 1028 { 1029 $issue_id = Misc::escapeInteger($issue_id); 1030 1031 $stmt = "UPDATE 1032 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1033 SET 1034 iss_updated_date='" . Date_API::getCurrentDateGMT() . "', 1035 iss_last_internal_action_date='" . Date_API::getCurrentDateGMT() . "', 1036 iss_last_internal_action_type='updated', 1037 iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . " 1038 WHERE 1039 iss_id=$issue_id"; 1040 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1041 if (PEAR::isError($res)) { 1042 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1043 return -1; 1044 } else { 1045 if (!empty($_POST["comments"])) { 1046 // add note with the comments of marking an issue as a duplicate of another one 1047 $_POST['title'] = 'Issue duplication comments'; 1048 $_POST["note"] = $_POST["comments"]; 1049 Note::insert(Auth::getUserID(), $issue_id); 1050 } 1051 // record the change 1052 History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'), 1053 "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID())); 1054 return 1; 1055 } 1056 } 1057 1058 1059 /** 1060 * Method used to get an associative array of user ID => user 1061 * status associated with a given issue ID. 1062 * 1063 * @access public 1064 * @param integer $issue_id The issue ID 1065 * @return array The list of users 1066 */ 1067 function getAssignedUsersStatus($issue_id) 1068 { 1069 $stmt = "SELECT 1070 usr_id, 1071 usr_status 1072 FROM 1073 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user, 1074 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 1075 WHERE 1076 isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND 1077 isu_usr_id=usr_id"; 1078 $res = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 1079 if (PEAR::isError($res)) { 1080 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1081 return array(); 1082 } else { 1083 return $res; 1084 } 1085 } 1086 1087 1088 /** 1089 * Method used to get the summary associated with a given issue ID. 1090 * 1091 * @access public 1092 * @param integer $issue_id The issue ID 1093 * @return string The issue summary 1094 */ 1095 function getTitle($issue_id) 1096 { 1097 $stmt = "SELECT 1098 iss_summary 1099 FROM 1100 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1101 WHERE 1102 iss_id=" . Misc::escapeInteger($issue_id); 1103 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 1104 if (PEAR::isError($res)) { 1105 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1106 return ""; 1107 } else { 1108 return $res; 1109 } 1110 } 1111 1112 1113 /** 1114 * Method used to get the issue ID associated with a specific summary. 1115 * 1116 * @access public 1117 * @param string $summary The summary to look for 1118 * @return integer The issue ID 1119 */ 1120 function getIssueID($summary) 1121 { 1122 $stmt = "SELECT 1123 iss_id 1124 FROM 1125 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1126 WHERE 1127 iss_summary='" . Misc::escapeString($summary) . "'"; 1128 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 1129 if (PEAR::isError($res)) { 1130 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1131 return 0; 1132 } else { 1133 if (empty($res)) { 1134 return 0; 1135 } else { 1136 return $res; 1137 } 1138 } 1139 } 1140 1141 1142 /** 1143 * Method used to add a new anonymous based issue in the system. 1144 * 1145 * @access public 1146 * @return integer The new issue ID 1147 */ 1148 function addAnonymousReport() 1149 { 1150 $options = Project::getAnonymousPostOptions($_POST["project"]); 1151 $initial_status = Project::getInitialStatus($_POST["project"]); 1152 $stmt = "INSERT INTO 1153 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1154 ( 1155 iss_prj_id, 1156 iss_prc_id, 1157 iss_pre_id, 1158 iss_pri_id, 1159 iss_usr_id,"; 1160 if (!empty($initial_status)) { 1161 $stmt .= "iss_sta_id,"; 1162 } 1163 $stmt .= " 1164 iss_created_date, 1165 iss_last_public_action_date, 1166 iss_last_public_action_type, 1167 iss_summary, 1168 iss_description, 1169 iss_root_message_id 1170 ) VALUES ( 1171 " . Misc::escapeInteger($_POST["project"]) . ", 1172 " . $options["category"] . ", 1173 0, 1174 " . $options["priority"] . ", 1175 " . $options["reporter"] . ","; 1176 if (!empty($initial_status)) { 1177 $stmt .= "$initial_status,"; 1178 } 1179 $stmt .= " 1180 '" . Date_API::getCurrentDateGMT() . "', 1181 '" . Date_API::getCurrentDateGMT() . "', 1182 'created', 1183 '" . Misc::escapeString($_POST["summary"]) . "', 1184 '" . Misc::escapeString($_POST["description"]) . "', 1185 '" . Misc::escapeString(Mail_API::generateMessageID()) . "' 1186 )"; 1187 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1188 if (PEAR::isError($res)) { 1189 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1190 return $res; 1191 } else { 1192 $new_issue_id = $GLOBALS["db_api"]->get_last_insert_id(); 1193 // log the creation of the issue 1194 History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously'); 1195 1196 // now process any files being uploaded 1197 $found = 0; 1198 for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) { 1199 if (!@empty($_FILES["file"]["name"][$i])) { 1200 $found = 1; 1201 break; 1202 } 1203 } 1204 if ($found) { 1205 $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously'); 1206 for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) { 1207 $filename = @$_FILES["file"]["name"][$i]; 1208 if (empty($filename)) { 1209 continue; 1210 } 1211 $blob = Misc::getFileContents($_FILES["file"]["tmp_name"][$i]); 1212 if (!empty($blob)) { 1213 Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob); 1214 } 1215 } 1216 } 1217 // need to process any custom fields ? 1218 if (@count($_POST["custom_fields"]) > 0) { 1219 foreach ($_POST["custom_fields"] as $fld_id => $value) { 1220 Custom_Field::associateIssue($new_issue_id, $fld_id, $value); 1221 } 1222 } 1223 1224 // now add the user/issue association 1225 $assign = array(); 1226 $users = @$options["users"]; 1227 $actions = Notification::getDefaultActions(); 1228 for ($i = 0; $i < count($users); $i++) { 1229 Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions); 1230 Issue::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]); 1231 $assign[] = $users[$i]; 1232 } 1233 1234 // also notify any users that want to receive emails anytime a new issue is created 1235 Notification::notifyNewIssue($_POST['project'], $new_issue_id); 1236 1237 Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false); 1238 1239 return $new_issue_id; 1240 } 1241 } 1242 1243 1244 /** 1245 * Method used to remove all issues associated with a specific list of 1246 * projects. 1247 * 1248 * @access public 1249 * @param array $ids The list of projects to look for 1250 * @return boolean 1251 */ 1252 function removeByProjects($ids) 1253 { 1254 $items = @implode(", ", Misc::escapeInteger($ids)); 1255 $stmt = "SELECT 1256 iss_id 1257 FROM 1258 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1259 WHERE 1260 iss_prj_id IN ($items)"; 1261 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 1262 if (PEAR::isError($res)) { 1263 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1264 return false; 1265 } else { 1266 if (count($res) > 0) { 1267 Issue::deleteAssociations($res); 1268 Attachment::removeByIssues($res); 1269 SCM::removeByIssues($res); 1270 Impact_Analysis::removeByIssues($res); 1271 Issue::deleteUserAssociations($res); 1272 Note::removeByIssues($res); 1273 Time_Tracking::removeByIssues($res); 1274 Notification::removeByIssues($res); 1275 Custom_Field::removeByIssues($res); 1276 Phone_Support::removeByIssues($res); 1277 History::removeByIssues($res); 1278 // now really delete the issues 1279 $items = implode(", ", $res); 1280 $stmt = "DELETE FROM 1281 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1282 WHERE 1283 iss_id IN ($items)"; 1284 $GLOBALS["db_api"]->dbh->query($stmt); 1285 } 1286 return true; 1287 } 1288 } 1289 1290 1291 /** 1292 * Method used to close off an issue. 1293 * 1294 * @access public 1295 * @param integer $usr_id The user ID 1296 * @param integer $issue_id The issue ID 1297 * @param bool $send_notification Whether to send a notification about this action or not 1298 * @param integer $resolution_id The resolution ID 1299 * @param integer $status_id The status ID 1300 * @param string $reason The reason for closing this issue 1301 * @param string $send_notification_to Who this notification should be sent too 1302 * @return integer 1 if the update worked, -1 otherwise 1303 */ 1304 function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal') 1305 { 1306 $usr_id = Misc::escapeInteger($usr_id); 1307 $issue_id = Misc::escapeInteger($issue_id); 1308 $resolution_id = Misc::escapeInteger($resolution_id); 1309 $status_id = Misc::escapeInteger($status_id); 1310 1311 $stmt = "UPDATE 1312 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1313 SET 1314 iss_updated_date='" . Date_API::getCurrentDateGMT() . "', 1315 iss_last_public_action_date='" . Date_API::getCurrentDateGMT() . "', 1316 iss_last_public_action_type='closed', 1317 iss_closed_date='" . Date_API::getCurrentDateGMT() . "',\n"; 1318 if (!empty($resolution_id)) { 1319 $stmt .= "iss_res_id=$resolution_id,\n"; 1320 } 1321 $stmt .= "iss_sta_id=$status_id 1322 WHERE 1323 iss_id=$issue_id"; 1324 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1325 if (PEAR::isError($res)) { 1326 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1327 return -1; 1328 } else { 1329 $prj_id = Issue::getProjectID($issue_id); 1330 1331 // record the change 1332 History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id)); 1333 1334 if ($send_notification_to == 'all') { 1335 1336 $from = User::getFromHeader($usr_id); 1337 $message_id = User::getFromHeader($usr_id); 1338 $full_email = Support::buildFullHeaders($issue_id, $message_id, $from, 1339 '', '', 'Issue closed comments', $reason, ''); 1340 1341 $structure = Mime_Helper::decode($full_email, true, false); 1342 1343 $email = array( 1344 'ema_id' => Email_Account::getEmailAccount(Issue::getProjectID($issue_id)), 1345 'issue_id' => $issue_id, 1346 'message_id' => $message_id, 1347 'date' => Date_API::getCurrentDateGMT(), 1348 'subject' => 'Issue closed comments', 1349 'from' => $from, 1350 'has_attachment'=> 0, 1351 'body' => $reason, 1352 'full_email' => $full_email, 1353 'headers' => $structure->headers 1354 ); 1355 Support::insertEmail($email, $structure, $sup_id, true); 1356 $ids = $sup_id; 1357 } else { 1358 // add note with the reason to close the issue 1359 $_POST['title'] = 'Issue closed comments'; 1360 $_POST["note"] = $reason; 1361 Note::insert($usr_id, $issue_id, false, true, true, $send_notification); 1362 $ids = false; 1363 } 1364 1365 if ($send_notification) { 1366 if (Customer::hasCustomerIntegration($prj_id)) { 1367 // send a special confirmation email when customer issues are closed 1368 $stmt = "SELECT 1369 iss_customer_contact_id 1370 FROM 1371 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1372 WHERE 1373 iss_id=$issue_id"; 1374 $customer_contact_id = $GLOBALS["db_api"]->dbh->getOne($stmt); 1375 if (!empty($customer_contact_id)) { 1376 Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason); 1377 } 1378 } 1379 // send notifications for the issue being closed 1380 Notification::notify($issue_id, 'closed', $ids); 1381 } 1382 Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason); 1383 return 1; 1384 } 1385 } 1386 1387 1388 /** 1389 * Method used to update the details of a specific issue. 1390 * 1391 * @access public 1392 * @param integer $issue_id The issue ID 1393 * @return integer 1 if the update worked, -1 or -2 otherwise 1394 */ 1395 function update($issue_id) 1396 { 1397 1398 global $errors; 1399 $errors = array(); 1400 1401 $issue_id = Misc::escapeInteger($issue_id); 1402 1403 $usr_id = Auth::getUserID(); 1404 $prj_id = Issue::getProjectID($issue_id); 1405 // get all of the 'current' information of this issue 1406 $current = Issue::getDetails($issue_id); 1407 // update the issue associations 1408 if (empty($_POST['associated_issues'])) { 1409 $associated_issues = array(); 1410 } else { 1411 $associated_issues = explode(',', @$_POST['associated_issues']); 1412 // make sure all associated issues are valid (and in this project) 1413 for ($i = 0; $i < count($associated_issues); $i++) { 1414 if (!Issue::exists(trim($associated_issues[$i]), false)) { 1415 $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.'; 1416 unset($associated_issues[$i]); 1417 } 1418 } 1419 } 1420 $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues); 1421 if (count($association_diff) > 0) { 1422 // go through the new assocations, if association already exists, skip it 1423 $associations_to_remove = $current['associated_issues']; 1424 if (count($associated_issues) > 0) { 1425 foreach ($associated_issues as $index => $associated_id) { 1426 if (!in_array($associated_id, $current['associated_issues'])) { 1427 Issue::addAssociation($issue_id, $associated_id, $usr_id); 1428 } else { 1429 // already assigned, remove this user from list of users to remove 1430 unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]); 1431 } 1432 } 1433 } 1434 if (count($associations_to_remove) > 0) { 1435 foreach ($associations_to_remove as $associated_id) { 1436 Issue::deleteAssociation($issue_id, $associated_id); 1437 } 1438 } 1439 } 1440 if ((!empty($_POST['expected_resolution_date']['Year'])) && 1441 (!empty($_POST['expected_resolution_date']['Month'])) && 1442 (!empty($_POST['expected_resolution_date']['Day']))) { 1443 $_POST['expected_resolution_date'] = sprintf('%s-%s-%s', $_POST['expected_resolution_date']['Year'], 1444 $_POST['expected_resolution_date']['Month'], 1445 $_POST['expected_resolution_date']['Day']); 1446 } else { 1447 $_POST['expected_resolution_date'] = ''; 1448 } 1449 $assignments_changed = false; 1450 if (@$_POST["keep_assignments"] == "no") { 1451 // only change the issue-user associations if there really were any changes 1452 $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']); 1453 if (!empty($_POST['assignments'])) { 1454 $new_assignees = @$_POST['assignments']; 1455 } else { 1456 $new_assignees = array(); 1457 } 1458 $assignment_notifications = array(); 1459 1460 // remove people from the assignment list, if appropriate 1461 foreach ($old_assignees as $assignee) { 1462 if (!in_array($assignee, $new_assignees)) { 1463 Issue::deleteUserAssociation($issue_id, $assignee); 1464 $assignments_changed = true; 1465 } 1466 } 1467 // add people to the assignment list, if appropriate 1468 foreach ($new_assignees as $assignee) { 1469 if (!in_array($assignee, $old_assignees)) { 1470 Issue::addUserAssociation($usr_id, $issue_id, $assignee); 1471 Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions(), TRUE); 1472 $assignment_notifications[] = $assignee; 1473 $assignments_changed = true; 1474 } 1475 } 1476 if (count($assignment_notifications) > 0) { 1477 Notification::notifyNewAssignment($assignment_notifications, $issue_id); 1478 } 1479 } 1480 if (empty($_POST["estimated_dev_time"])) { 1481 $_POST["estimated_dev_time"] = 0; 1482 } 1483 $stmt = "UPDATE 1484 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1485 SET 1486 iss_updated_date='" . Date_API::getCurrentDateGMT() . "', 1487 iss_last_public_action_date='" . Date_API::getCurrentDateGMT() . "', 1488 iss_last_public_action_type='updated',"; 1489 if (!empty($_POST["category"])) { 1490 $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ","; 1491 } 1492 if (@$_POST["keep"] == "no") { 1493 $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ","; 1494 } 1495 if (!empty($_POST['expected_resolution_date'])) { 1496 $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',"; 1497 } else { 1498 $stmt .= "iss_expected_resolution_date=null,"; 1499 } 1500 $stmt .= " 1501 iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ", 1502 iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ", 1503 iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ", 1504 iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ", 1505 iss_summary='" . Misc::escapeString($_POST["summary"]) . "', 1506 iss_description='" . Misc::escapeString($_POST["description"]) . "', 1507 iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "', 1508 iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "', 1509 iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ", 1510 iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'"; 1511 if (isset($_POST['private'])) { 1512 $stmt .= ", 1513 iss_private = " . Misc::escapeInteger($_POST['private']); 1514 } 1515 $stmt .= " 1516 WHERE 1517 iss_id=$issue_id"; 1518 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1519 if (PEAR::isError($res)) { 1520 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1521 return -1; 1522 } else { 1523 // add change to the history (only for changes on specific fields?) 1524 $updated_fields = array(); 1525 if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) { 1526 $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']); 1527 } 1528 if ($current["iss_prc_id"] != $_POST["category"]) { 1529 $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"])); 1530 } 1531 if ($current["iss_pre_id"] != $_POST["release"]) { 1532 $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"])); 1533 } 1534 if ($current["iss_pri_id"] != $_POST["priority"]) { 1535 $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"])); 1536 Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST); 1537 } 1538 if ($current["iss_sta_id"] != $_POST["status"]) { 1539 // clear out the last-triggered-reminder flag when changing the status of an issue 1540 Reminder_Action::clearLastTriggered($issue_id); 1541 1542 // if old status was closed and new status is not, clear closed data from issue. 1543 $old_status_details = Status::getDetails($current['iss_sta_id']); 1544 if ($old_status_details['sta_is_closed'] == 1) { 1545 $new_status_details = Status::getDetails($_POST["status"]); 1546 if ($new_status_details['sta_is_closed'] != 1) { 1547 Issue::clearClosed($issue_id); 1548 } 1549 } 1550 $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"])); 1551 } 1552 if ($current["iss_res_id"] != $_POST["resolution"]) { 1553 $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"])); 1554 } 1555 if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) { 1556 $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60))); 1557 } 1558 if ($current["iss_summary"] != $_POST["summary"]) { 1559 $updated_fields["Summary"] = ''; 1560 } 1561 if ($current["iss_description"] != $_POST["description"]) { 1562 $updated_fields["Description"] = ''; 1563 } 1564 if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) { 1565 $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private'])); 1566 } 1567 if (count($updated_fields) > 0) { 1568 // log the changes 1569 $changes = ''; 1570 $i = 0; 1571 foreach ($updated_fields as $key => $value) { 1572 if ($i > 0) { 1573 $changes .= "; "; 1574 } 1575 if (($key != "Summary") && ($key != "Description")) { 1576 $changes .= "$key: $value"; 1577 } else { 1578 $changes .= "$key"; 1579 } 1580 $i++; 1581 } 1582 History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id)); 1583 // send notifications for the issue being updated 1584 Notification::notifyIssueUpdated($issue_id, $current, $_POST); 1585 } 1586 1587 // record group change as a seperate change 1588 if ($current["iss_grp_id"] != (int)$_POST["group"]) { 1589 History::add($issue_id, $usr_id, History::getTypeID('group_changed'), 1590 "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id)); 1591 } 1592 1593 // now update any duplicates, if any 1594 $update_dupe = array( 1595 'Category', 1596 'Release', 1597 'Priority', 1598 'Release', 1599 'Resolution' 1600 ); 1601 // COMPAT: the following line requires PHP > 4.0.4 1602 $intersect = array_intersect($update_dupe, array_keys($updated_fields)); 1603 if (($current["duplicates"] != '') && (count($intersect) > 0)) { 1604 Issue::updateDuplicates($issue_id); 1605 } 1606 1607 // if there is customer integration, mark last customer action 1608 if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) { 1609 Issue::recordLastCustomerAction($issue_id); 1610 } 1611 1612 if ($assignments_changed) { 1613 // XXX: we may want to also send the email notification for those "new" assignees 1614 Workflow::handleAssignmentChange(Issue::getProjectID($issue_id), $issue_id, $usr_id, Issue::getDetails($issue_id), @$_POST['assignments'], false); 1615 } 1616 1617 Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST); 1618 // Move issue to another project 1619 if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) { 1620 $new_prj_id = (int)@$_POST['new_prj']; 1621 if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) { 1622 if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) { 1623 $stmt = "UPDATE 1624 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1625 SET 1626 iss_prj_id=" . Misc::escapeInteger($new_prj_id) . " 1627 WHERE 1628 iss_id=$issue_id"; 1629 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1630 if (PEAR::isError($res)) { 1631 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1632 return -1; 1633 } else { 1634 $currentDetails = Issue::getDetails($issue_id); 1635 1636 // set new category 1637 $new_iss_prc_list = Category::getAssocList($new_prj_id); 1638 $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']); 1639 $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list); 1640 if ($new_prc_id === false) { 1641 // use the first category listed in the new project 1642 $new_prc_id = key($new_iss_prc_list); 1643 } 1644 1645 // set new priority 1646 $new_iss_pri_list = Priority::getAssocList($new_prj_id); 1647 $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']); 1648 $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list); 1649 if ($new_pri_id === false) { 1650 // use the first category listed in the new project 1651 $new_pri_id = key($new_iss_pri_list); 1652 } 1653 1654 // XXX: Set status if needed when moving issue 1655 1656 $stmt = "UPDATE 1657 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1658 SET 1659 iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ", 1660 iss_pri_id=" . $new_pri_id . " 1661 WHERE 1662 iss_id=$issue_id"; 1663 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1664 if (PEAR::isError($res)) { 1665 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1666 } 1667 1668 // XXX: Send notification about issue being moved 1669 } 1670 } else { 1671 return -1; 1672 } 1673 } 1674 } 1675 return 1; 1676 } 1677 } 1678 1679 1680 /** 1681 * Method used to associate an existing issue with another one. 1682 * 1683 * @access public 1684 * @param integer $issue_id The issue ID 1685 * @param integer $issue_id The other issue ID 1686 * @return void 1687 */ 1688 function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE) 1689 { 1690 $issue_id = Misc::escapeInteger($issue_id); 1691 $associated_id = Misc::escapeInteger($associated_id); 1692 1693 $stmt = "INSERT INTO 1694 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association 1695 ( 1696 isa_issue_id, 1697 isa_associated_id 1698 ) VALUES ( 1699 $issue_id, 1700 $associated_id 1701 )"; 1702 $GLOBALS["db_api"]->dbh->query($stmt); 1703 History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id)); 1704 // link the associated issue back to this one 1705 if ($link_issues) { 1706 Issue::addAssociation($associated_id, $issue_id, $usr_id, FALSE); 1707 } 1708 } 1709 1710 1711 /** 1712 * Method used to remove the issue associations related to a specific issue. 1713 * 1714 * @access public 1715 * @param integer $issue_id The issue ID 1716 * @return void 1717 */ 1718 function deleteAssociations($issue_id, $usr_id = FALSE) 1719 { 1720 $issue_id = Misc::escapeInteger($issue_id); 1721 if (is_array($issue_id)) { 1722 $issue_id = implode(", ", $issue_id); 1723 } 1724 $stmt = "DELETE FROM 1725 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association 1726 WHERE 1727 isa_issue_id IN ($issue_id) OR 1728 isa_associated_id IN ($issue_id)"; 1729 $GLOBALS["db_api"]->dbh->query($stmt); 1730 if ($usr_id) { 1731 History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id)); 1732 } 1733 } 1734 1735 1736 /** 1737 * Method used to remove a issue association from an issue. 1738 * 1739 * @access public 1740 * @param integer $issue_id The issue ID 1741 * @param integer $associated_id The associated issue ID to remove. 1742 * @return void 1743 */ 1744 function deleteAssociation($issue_id, $associated_id) 1745 { 1746 $issue_id = Misc::escapeInteger($issue_id); 1747 $associated_id = Misc::escapeInteger($associated_id); 1748 $stmt = "DELETE FROM 1749 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association 1750 WHERE 1751 ( 1752 isa_issue_id = $issue_id AND 1753 isa_associated_id = $associated_id 1754 ) OR 1755 ( 1756 isa_issue_id = $associated_id AND 1757 isa_associated_id = $issue_id 1758 )"; 1759 $GLOBALS["db_api"]->dbh->query($stmt); 1760 History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'), 1761 "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID())); 1762 History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'), 1763 "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID())); 1764 } 1765 1766 1767 /** 1768 * Method used to assign an issue with an user. 1769 * 1770 * @access public 1771 * @param integer $usr_id The user ID of the person performing this change 1772 * @param integer $issue_id The issue ID 1773 * @param integer $assignee_usr_id The user ID of the assignee 1774 * @param boolean $add_history Whether to add a history entry about this or not 1775 * @return integer 1 if the update worked, -1 otherwise 1776 */ 1777 function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE) 1778 { 1779 $issue_id = Misc::escapeInteger($issue_id); 1780 $assignee_usr_id = Misc::escapeInteger($assignee_usr_id); 1781 $stmt = "INSERT INTO 1782 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user 1783 ( 1784 isu_iss_id, 1785 isu_usr_id, 1786 isu_assigned_date 1787 ) VALUES ( 1788 $issue_id, 1789 $assignee_usr_id, 1790 '" . Date_API::getCurrentDateGMT() . "' 1791 )"; 1792 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1793 if (PEAR::isError($res)) { 1794 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1795 return -1; 1796 } else { 1797 if ($add_history) { 1798 History::add($issue_id, $usr_id, History::getTypeID('user_associated'), 1799 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id)); 1800 } 1801 return 1; 1802 } 1803 } 1804 1805 1806 /** 1807 * Method used to delete all user assignments for a specific issue. 1808 * 1809 * @access public 1810 * @param integer $issue_id The issue ID 1811 * @param integer $usr_id The user ID of the person performing the change 1812 * @return void 1813 */ 1814 function deleteUserAssociations($issue_id, $usr_id = FALSE) 1815 { 1816 $issue_id = Misc::escapeInteger($issue_id); 1817 if (is_array($issue_id)) { 1818 $issue_id = implode(", ", $issue_id); 1819 } 1820 $stmt = "DELETE FROM 1821 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user 1822 WHERE 1823 isu_iss_id IN ($issue_id)"; 1824 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1825 if (PEAR::isError($res)) { 1826 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1827 return -1; 1828 } else { 1829 if ($usr_id) { 1830 History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id)); 1831 } 1832 return 1; 1833 } 1834 } 1835 1836 1837 /** 1838 * Method used to delete a single user assignments for a specific issue. 1839 * 1840 * @access public 1841 * @param integer $issue_id The issue ID 1842 * @param integer $usr_id The user to remove. 1843 * @param boolean $add_history Whether to add a history entry about this or not 1844 * @return void 1845 */ 1846 function deleteUserAssociation($issue_id, $usr_id, $add_history = true) 1847 { 1848 $issue_id = Misc::escapeInteger($issue_id); 1849 $usr_id = Misc::escapeInteger($usr_id); 1850 $stmt = "DELETE FROM 1851 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user 1852 WHERE 1853 isu_iss_id = $issue_id AND 1854 isu_usr_id = $usr_id"; 1855 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1856 if (PEAR::isError($res)) { 1857 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1858 return -1; 1859 } else { 1860 if ($add_history) { 1861 History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'), 1862 User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID())); 1863 } 1864 return 1; 1865 } 1866 } 1867 1868 1869 /** 1870 * Creates an issue with the given email information. 1871 * 1872 * @access public 1873 * @param integer $prj_id The project ID 1874 * @param integer $usr_id The user responsible for this action 1875 * @param string $sender The original sender of this email 1876 * @param string $summary The issue summary 1877 * @param string $description The issue description 1878 * @param integer $category The category ID 1879 * @param integer $priority The priority ID 1880 * @param array $assignment The list of users to assign this issue to 1881 * @param string $date The date the email was originally sent. 1882 * @param string $msg_id The message ID of the email we are creating this issue from. 1883 * @return void 1884 */ 1885 function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id) 1886 { 1887 $exclude_list = array(); 1888 $sender_email = Mail_API::getEmailAddress($sender); 1889 $sender_usr_id = User::getUserIDByEmail($sender_email); 1890 if (!empty($sender_usr_id)) { 1891 $reporter = $sender_usr_id; 1892 $exclude_list[] = $sender_usr_id; 1893 } else { 1894 $reporter = APP_SYSTEM_USER_ID; 1895 } 1896 if (Customer::hasCustomerIntegration($prj_id)) { 1897 list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email)); 1898 if (!empty($customer_id)) { 1899 $contact = Customer::getContactDetails($prj_id, $customer_contact_id); 1900 // overwrite the reporter with the customer contact 1901 $reporter = User::getUserIDByContactID($customer_contact_id); 1902 $contact_timezone = Date_API::getPreferredTimezone($reporter); 1903 } 1904 } else { 1905 $customer_id = FALSE; 1906 } 1907 1908 $initial_status = Project::getInitialStatus($prj_id); 1909 // add new issue 1910 $stmt = "INSERT INTO 1911 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 1912 ( 1913 iss_prj_id,\n"; 1914 if (!empty($category)) { 1915 $stmt .= "iss_prc_id,\n"; 1916 } 1917 $stmt .= "iss_pri_id, 1918 iss_usr_id,"; 1919 if (!empty($initial_status)) { 1920 $stmt .= "iss_sta_id,"; 1921 } 1922 if (!empty($customer_id)) { 1923 $stmt .= " 1924 iss_customer_id, 1925 iss_customer_contact_id, 1926 iss_contact_person_lname, 1927 iss_contact_person_fname, 1928 iss_contact_email, 1929 iss_contact_phone, 1930 iss_contact_timezone,"; 1931 } 1932 $stmt .= " 1933 iss_created_date, 1934 iss_last_public_action_date, 1935 iss_last_public_action_type, 1936 iss_summary, 1937 iss_description, 1938 iss_root_message_id 1939 ) VALUES ( 1940 " . $prj_id . ",\n"; 1941 if (!empty($category)) { 1942 $stmt .= Misc::escapeInteger($category) . ",\n"; 1943 } 1944 $stmt .= Misc::escapeInteger($priority) . ", 1945 " . Misc::escapeInteger($reporter) . ","; 1946 if (!empty($initial_status)) { 1947 $stmt .= Misc::escapeInteger($initial_status) . ","; 1948 } 1949 if (!empty($customer_id)) { 1950 $stmt .= " 1951 " . Misc::escapeInteger($customer_id) . ", 1952 " . Misc::escapeInteger($customer_contact_id) . ", 1953 '" . Misc::escapeString($contact['last_name']) . "', 1954 '" . Misc::escapeString($contact['first_name']) . "', 1955 '" . Misc::escapeString($sender_email) . "', 1956 '" . Misc::escapeString($contact['phone']) . "', 1957 '" . Misc::escapeString($contact_timezone) . "',"; 1958 } 1959 $stmt .= " 1960 '" . Date_API::getCurrentDateGMT() . "', 1961 '" . Date_API::getCurrentDateGMT() . "', 1962 'created', 1963 '" . Misc::escapeString($summary) . "', 1964 '" . Misc::escapeString($description) . "', 1965 '" . Misc::escapeString($msg_id) . "' 1966 )"; 1967 $res = $GLOBALS["db_api"]->dbh->query($stmt); 1968 if (PEAR::isError($res)) { 1969 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 1970 return -1; 1971 } else { 1972 $new_issue_id = $GLOBALS["db_api"]->get_last_insert_id(); 1973 $has_TAM = false; 1974 $has_RR = false; 1975 // log the creation of the issue 1976 History::add($new_issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender); 1977 1978 $emails = array(); 1979 $manager_usr_ids = array(); 1980 if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) { 1981 // if there are any technical account managers associated with this customer, add these users to the notification list 1982 $managers = Customer::getAccountManagers($prj_id, $customer_id); 1983 $manager_usr_ids = array_keys($managers); 1984 $manager_emails = array_values($managers); 1985 $emails = array_merge($emails, $manager_emails); 1986 } 1987 // add the reporter to the notification list 1988 $emails[] = $sender; 1989 $emails = array_unique($emails); // COMPAT: version >= 4.0.1 1990 $actions = Notification::getDefaultActions(); 1991 foreach ($emails as $address) { 1992 Notification::subscribeEmail($reporter, $new_issue_id, $address, $actions); 1993 } 1994 1995 // only assign the issue to an user if the associated customer has any technical account managers 1996 $users = array(); 1997 if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) { 1998 foreach ($manager_usr_ids as $manager_usr_id) { 1999 $users[] = $manager_usr_id; 2000 Issue::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $manager_usr_id, false); 2001 History::add($new_issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)'); 2002 } 2003 $has_TAM = true; 2004 } 2005 // now add the user/issue association 2006 if (@count($assignment) > 0) { 2007 for ($i = 0; $i < count($assignment); $i++) { 2008 Notification::subscribeUser($reporter, $new_issue_id, $assignment[$i], $actions); 2009 Issue::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $assignment[$i]); 2010 if ($assignment[$i] != $usr_id) { 2011 $users[] = $assignment[$i]; 2012 } 2013 } 2014 } else { 2015 // only use the round-robin feature if this new issue was not 2016 // already assigned to a customer account manager 2017 if (@count($manager_usr_ids) < 1) { 2018 $assignee = Round_Robin::getNextAssignee($prj_id); 2019 // assign the issue to the round robin person 2020 if (!empty($assignee)) { 2021 Issue::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $assignee, false); 2022 History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)'); 2023 $users[] = $assignee; 2024 $has_RR = true; 2025 } 2026 } 2027 } 2028 if (count($users) > 0) { 2029 $has_assignee = true; 2030 } 2031 2032 // send special 'an issue was auto-created for you' notification back to the sender 2033 Notification::notifyAutoCreatedIssue($prj_id, $new_issue_id, $sender, $date, $summary); 2034 // also notify any users that want to receive emails anytime a new issue is created 2035 Notification::notifyNewIssue($prj_id, $new_issue_id, $exclude_list); 2036 2037 Workflow::handleNewIssue($prj_id, $new_issue_id, $has_TAM, $has_RR); 2038 2039 return $new_issue_id; 2040 } 2041 } 2042 2043 2044 /** 2045 * Method used to add a new issue using the normal report form. 2046 * 2047 * @access public 2048 * @return integer The new issue ID 2049 */ 2050 function insert() 2051 { 2052 global $insert_errors; 2053 2054 $usr_id = Auth::getUserID(); 2055 $prj_id = Auth::getCurrentProject(); 2056 $initial_status = Project::getInitialStatus($prj_id); 2057 2058 $insert_errors = array(); 2059 2060 $missing_fields = array(); 2061 if ($_POST["category"] == '-1') { 2062 $missing_fields[] = "Category"; 2063 } 2064 if ($_POST["priority"] == '-1') { 2065 $missing_fields[] = "Priority"; 2066 } 2067 2068 if ($_POST["estimated_dev_time"] == '') { 2069 $_POST["estimated_dev_time"] = 0; 2070 } 2071 2072 // add new issue 2073 $stmt = "INSERT INTO 2074 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 2075 ( 2076 iss_prj_id,\n"; 2077 if (!empty($_POST["group"])) { 2078 $stmt .= "iss_grp_id,\n"; 2079 } 2080 if (!empty($_POST["category"])) { 2081 $stmt .= "iss_prc_id,\n"; 2082 } 2083 if (!empty($_POST["release"])) { 2084 $stmt .= "iss_pre_id,\n"; 2085 } 2086 if (!empty($_POST["priority"])) { 2087 $stmt .= "iss_pri_id,\n"; 2088 } 2089 $stmt .= "iss_usr_id,"; 2090 if (!empty($initial_status)) { 2091 $stmt .= "iss_sta_id,"; 2092 } 2093 if (Customer::hasCustomerIntegration($prj_id)) { 2094 $stmt .= " 2095 iss_customer_id, 2096 iss_customer_contract_id, 2097 iss_customer_contact_id, 2098 iss_contact_person_lname, 2099 iss_contact_person_fname, 2100 iss_contact_email, 2101 iss_contact_phone, 2102 iss_contact_timezone,"; 2103 } 2104 $stmt .= " 2105 iss_created_date, 2106 iss_last_public_action_date, 2107 iss_last_public_action_type, 2108 iss_summary, 2109 iss_description, 2110 iss_dev_time, 2111 iss_private, 2112 iss_root_message_id 2113 ) VALUES ( 2114 " . $prj_id . ",\n"; 2115 if (!empty($_POST["group"])) { 2116 $stmt .= Misc::escapeInteger($_POST["group"]) . ",\n"; 2117 } 2118 if (!empty($_POST["category"])) { 2119 $stmt .= Misc::escapeInteger($_POST["category"]) . ",\n"; 2120 } 2121 if (!empty($_POST["release"])) { 2122 $stmt .= Misc::escapeInteger($_POST["release"]) . ",\n"; 2123 } 2124 if (!empty($_POST["priority"])) { 2125 $stmt .= Misc::escapeInteger($_POST["priority"]) . ","; 2126 } 2127 // if we are creating an issue for a customer, put the 2128 // main customer contact as the reporter for it 2129 if (Customer::hasCustomerIntegration($prj_id)) { 2130 $contact_usr_id = User::getUserIDByContactID($_POST['contact']); 2131 if (empty($contact_usr_id)) { 2132 $contact_usr_id = $usr_id; 2133 } 2134 $stmt .= Misc::escapeInteger($contact_usr_id) . ","; 2135 } else { 2136 $stmt .= $usr_id . ","; 2137 } 2138 if (!empty($initial_status)) { 2139 $stmt .= Misc::escapeInteger($initial_status) . ","; 2140 } 2141 if (Customer::hasCustomerIntegration($prj_id)) { 2142 $stmt .= " 2143 " . Misc::escapeInteger($_POST['customer']) . ", 2144 '" . Misc::escapeString($_POST['contract']) . "', 2145 " . Misc::escapeInteger($_POST['contact']) . ", 2146 '" . Misc::escapeString($_POST["contact_person_lname"]) . "', 2147 '" . Misc::escapeString($_POST["contact_person_fname"]) . "', 2148 '" . Misc::escapeString($_POST["contact_email"]) . "', 2149 '" . Misc::escapeString($_POST["contact_phone"]) . "', 2150 '" . Misc::escapeString($_POST["contact_timezone"]) . "',"; 2151 } 2152 $stmt .= " 2153 '" . Date_API::getCurrentDateGMT() . "', 2154 '" . Date_API::getCurrentDateGMT() . "', 2155 'created', 2156 '" . Misc::escapeString($_POST["summary"]) . "', 2157 '" . Misc::escapeString($_POST["description"]) . "', 2158 " . Misc::escapeString($_POST["estimated_dev_time"]) . ", 2159 " . Misc::escapeInteger($_POST["private"]) . " , 2160 '" . Misc::escapeString(Mail_API::generateMessageID()) . "' 2161 )"; 2162 $res = $GLOBALS["db_api"]->dbh->query($stmt); 2163 if (PEAR::isError($res)) { 2164 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2165 return -1; 2166 } else { 2167 $new_issue_id = $GLOBALS["db_api"]->get_last_insert_id(); 2168 $has_TAM = false; 2169 $has_RR = false; 2170 $info = User::getNameEmail($usr_id); 2171 // log the creation of the issue 2172 History::add($new_issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID())); 2173 2174 $emails = array(); 2175 if (Customer::hasCustomerIntegration($prj_id)) { 2176 if (@count($_POST['contact_extra_emails']) > 0) { 2177 $emails = $_POST['contact_extra_emails']; 2178 } 2179 // add the primary contact to the notification list 2180 if ($_POST['add_primary_contact'] == 'yes') { 2181 $contact_email = User::getEmailByContactID($_POST['contact']); 2182 if (!empty($contact_email)) { 2183 $emails[] = $contact_email; 2184 } 2185 } 2186 // if there are any technical account managers associated with this customer, add these users to the notification list 2187 $managers = Customer::getAccountManagers($prj_id, $_POST['customer']); 2188 $manager_usr_ids = array_keys($managers); 2189 $manager_emails = array_values($managers); 2190 $emails = array_merge($emails, $manager_emails); 2191 } 2192 // add the reporter to the notification list 2193 $emails[] = $info['usr_email']; 2194 $emails = array_unique($emails); // COMPAT: version >= 4.0.1 2195 $actions = Notification::getDefaultActions(); 2196 foreach ($emails as $address) { 2197 Notification::subscribeEmail($usr_id, $new_issue_id, $address, $actions); 2198 } 2199 2200 // only assign the issue to an user if the associated customer has any technical account managers 2201 $users = array(); 2202 $has_TAM = false; 2203 if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) { 2204 foreach ($manager_usr_ids as $manager_usr_id) { 2205 $users[] = $manager_usr_id; 2206 Issue::addUserAssociation($usr_id, $new_issue_id, $manager_usr_id, false); 2207 History::add($new_issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)'); 2208 } 2209 $has_TAM = true; 2210 } 2211 // now add the user/issue association (aka assignments) 2212 if (@count($_POST["users"]) > 0) { 2213 for ($i = 0; $i < count($_POST["users"]); $i++) { 2214 Notification::subscribeUser($usr_id, $new_issue_id, $_POST["users"][$i], $actions); 2215 Issue::addUserAssociation($usr_id, $new_issue_id, $_POST["users"][$i]); 2216 if ($_POST["users"][$i] != $usr_id) { 2217 $users[] = $_POST["users"][$i]; 2218 } 2219 } 2220 } else { 2221 // only use the round-robin feature if this new issue was not 2222 // already assigned to a customer account manager 2223 if (@count($manager_usr_ids) < 1) { 2224 $assignee = Round_Robin::getNextAssignee($prj_id); 2225 // assign the issue to the round robin person 2226 if (!empty($assignee)) { 2227 $users[] = $assignee; 2228 Issue::addUserAssociation($usr_id, $new_issue_id, $assignee, false); 2229 History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)'); 2230 $has_RR = true; 2231 } 2232 } 2233 } 2234 2235 // now process any files being uploaded 2236 $found = 0; 2237 for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) { 2238 if (!@empty($_FILES["file"]["name"][$i])) { 2239 $found = 1; 2240 break; 2241 } 2242 } 2243 if ($found) { 2244 $files = array(); 2245 for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) { 2246 $filename = @$_FILES["file"]["name"][$i]; 2247 if (empty($filename)) { 2248 continue; 2249 } 2250 $blob = Misc::getFileContents($_FILES["file"]["tmp_name"][$i]); 2251 if (empty($blob)) { 2252 // error reading a file 2253 $insert_errors["file[$i]"] = "There was an error uploading the file '$filename'."; 2254 continue; 2255 } 2256 $files[] = array( 2257 "filename" => $filename, 2258 "type" => $_FILES['file']['type'][$i], 2259 "blob" => $blob 2260 ); 2261 } 2262 if (count($files) > 0) { 2263 $attachment_id = Attachment::add($new_issue_id, $usr_id, 'Files uploaded at issue creation time'); 2264 foreach ($files as $file) { 2265 Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]); 2266 } 2267 } 2268 } 2269 // need to associate any emails ? 2270 if (!empty($_POST["attached_emails"])) { 2271 $items = explode(",", $_POST["attached_emails"]); 2272 Support::associate($usr_id, $new_issue_id, $items); 2273 } 2274 // need to notify any emails being converted into issues ? 2275 if (@count($_POST["notify_senders"]) > 0) { 2276 $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $new_issue_id, $_POST["notify_senders"], @$_POST['customer']); 2277 } else { 2278 $recipients = array(); 2279 } 2280 // need to process any custom fields ? 2281 if (@count($_POST["custom_fields"]) > 0) { 2282 foreach ($_POST["custom_fields"] as $fld_id => $value) { 2283 Custom_Field::associateIssue($new_issue_id, $fld_id, $value); 2284 } 2285 } 2286 // also send a special confirmation email to the customer contact 2287 if ((@$_POST['notify_customer'] == 'yes') && (!empty($_POST['contact']))) { 2288 // also need to pass the list of sender emails already notified, 2289 // so we can avoid notifying the same person again 2290 $contact_email = User::getEmailByContactID($_POST['contact']); 2291 if (@!in_array($contact_email, $recipients)) { 2292 Customer::notifyCustomerIssue($prj_id, $new_issue_id, $_POST['contact']); 2293 } 2294 } 2295 2296 Workflow::handleNewIssue($prj_id, $new_issue_id, $has_TAM, $has_RR); 2297 2298 // also notify any users that want to receive emails anytime a new issue is created 2299 Notification::notifyNewIssue($prj_id, $new_issue_id); 2300 2301 return $new_issue_id; 2302 } 2303 } 2304 2305 2306 /** 2307 * Method used to get a specific parameter in the issue listing cookie. 2308 * 2309 * @access public 2310 * @param string $name The name of the parameter 2311 * @return mixed The value of the specified parameter 2312 */ 2313 function getParam($name) 2314 { 2315 $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue'); 2316 2317 if (isset($_GET[$name])) { 2318 return $_GET[$name]; 2319 } elseif (isset($_POST[$name])) { 2320 return $_POST[$name]; 2321 } elseif (isset($profile[$name])) { 2322 return $profile[$name]; 2323 } else { 2324 return ""; 2325 } 2326 } 2327 2328 2329 /** 2330 * Method used to save the current search parameters in a cookie. 2331 * 2332 * @access public 2333 * @return array The search parameters 2334 */ 2335 function saveSearchParams() 2336 { 2337 $sort_by = Issue::getParam('sort_by'); 2338 $sort_order = Issue::getParam('sort_order'); 2339 $rows = Issue::getParam('rows'); 2340 $hide_closed = Issue::getParam('hide_closed'); 2341 if ($hide_closed === '') { 2342 $hide_closed = 1; 2343 } 2344 $search_type = Issue::getParam('search_type'); 2345 if (empty($search_type)) { 2346 $search_type = 'all_text'; 2347 } 2348 $custom_field = Issue::getParam('custom_field'); 2349 if (is_string($custom_field)) { 2350 $custom_field = unserialize(urldecode($custom_field)); 2351 } 2352 $cookie = array( 2353 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE, 2354 'pagerRow' => Issue::getParam('pagerRow'), 2355 'hide_closed' => $hide_closed, 2356 "sort_by" => $sort_by ? $sort_by : "pri_rank", 2357 "sort_order" => $sort_order ? $sort_order : "ASC", 2358 // quick filter form 2359 'keywords' => Issue::getParam('keywords'), 2360 'search_type' => $search_type, 2361 'users' => Issue::getParam('users'), 2362 'status' => Issue::getParam('status'), 2363 'priority' => Issue::getParam('priority'), 2364 'category' => Issue::getParam('category'), 2365 'customer_email' => Issue::getParam('customer_email'), 2366 // advanced search form 2367 'show_authorized_issues' => Issue::getParam('show_authorized_issues'), 2368 'show_notification_list_issues' => Issue::getParam('show_notification_list_issues'), 2369 'reporter' => Issue::getParam('reporter'), 2370 // other fields 2371 'release' => Issue::getParam('release'), 2372 // custom fields 2373 'custom_field' => $custom_field 2374 ); 2375 // now do some magic to properly format the date fields 2376 $date_fields = array( 2377 'created_date', 2378 'updated_date', 2379 'last_response_date', 2380 'first_response_date', 2381 'closed_date' 2382 ); 2383 foreach ($date_fields as $field_name) { 2384 $field = Issue::getParam($field_name); 2385 if (empty($field)) { 2386 continue; 2387 } 2388 if (@$field['filter_type'] == 'in_past') { 2389 @$cookie[$field_name] = array( 2390 'filter_type' => 'in_past', 2391 'time_period' => $field['time_period'] 2392 ); 2393 } else { 2394 $end_field_name = $field_name . '_end'; 2395 $end_field = Issue::getParam($end_field_name); 2396 @$cookie[$field_name] = array( 2397 'past_hour' => $field['past_hour'], 2398 'Year' => $field['Year'], 2399 'Month' => $field['Month'], 2400 'Day' => $field['Day'], 2401 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'], 2402 'filter_type' => $field['filter_type'], 2403 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day'] 2404 ); 2405 @$cookie[$end_field_name] = array( 2406 'Year' => $end_field['Year'], 2407 'Month' => $end_field['Month'], 2408 'Day' => $end_field['Day'] 2409 ); 2410 } 2411 } 2412 Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie); 2413 return $cookie; 2414 } 2415 2416 2417 /** 2418 * Method used to get the current sorting options used in the grid layout 2419 * of the issue listing page. 2420 * 2421 * @access public 2422 * @param array $options The current search parameters 2423 * @return array The sorting options 2424 */ 2425 function getSortingInfo($options) 2426 { 2427 2428 $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject()); 2429 2430 // default order for last action date, priority should be descending 2431 // for textual fields, like summary, ascending is reasonable 2432 $fields = array( 2433 "pri_rank" => "desc", 2434 "iss_id" => "desc", 2435 "iss_customer_id" => "desc", 2436 "prc_title" => "asc", 2437 "sta_rank" => "asc", 2438 "iss_created_date" => "desc", 2439 "iss_summary" => "asc", 2440 "last_action_date" => "desc", 2441 "usr_full_name" => "asc", 2442 "iss_expected_resolution_date" => "desc", 2443 ); 2444 2445 foreach ($custom_fields as $fld_id => $fld_name) { 2446 $fields['custom_field_' . $fld_id] = "desc"; 2447 } 2448 $items = array( 2449 "links" => array(), 2450 "images" => array() 2451 ); 2452 foreach ($fields as $field => $sort_order) { 2453 if ($options["sort_by"] == $field) { 2454 $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif"; 2455 if (strtolower($options["sort_order"]) == "asc") { 2456 $sort_order = "desc"; 2457 } else { 2458 $sort_order = "asc"; 2459 } 2460 } 2461 $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $field . "&sort_order=" . $sort_order; 2462 } 2463 return $items; 2464 } 2465 2466 2467 /** 2468 * Returns the list of action date fields appropriate for the 2469 * current user ID. 2470 * 2471 * @access public 2472 * @return array The list of action date fields 2473 */ 2474 function getLastActionFields() 2475 { 2476 $last_action_fields = array( 2477 "iss_last_public_action_date" 2478 ); 2479 if (Auth::getCurrentRole() > User::getRoleID('Customer')) { 2480 $last_action_fields[] = "iss_last_internal_action_date"; 2481 } 2482 if (count($last_action_fields) > 1) { 2483 return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date"; 2484 } else { 2485 return $last_action_fields[0] . " AS last_action_date"; 2486 } 2487 } 2488 2489 2490 /** 2491 * Method used to get the list of issues to be displayed in the grid layout. 2492 * 2493 * @access public 2494 * @param integer $prj_id The current project ID 2495 * @param array $options The search parameters 2496 * @param integer $current_row The current page number 2497 * @param integer $max The maximum number of rows per page 2498 * @return array The list of issues to be displayed 2499 */ 2500 function getListing($prj_id, $options, $current_row = 0, $max = 5) 2501 { 2502 if (strtoupper($max) == "ALL") { 2503 $max = 9999999; 2504 } 2505 $start = $current_row * $max; 2506 // get the current user's role 2507 $usr_id = Auth::getUserID(); 2508 $role_id = User::getRoleByUser($usr_id, $prj_id); 2509 2510 // get any custom fields that should be displayed 2511 $custom_fields = Custom_Field::getFieldsToBeListed($prj_id); 2512 2513 $stmt = "SELECT 2514 iss_id, 2515 iss_grp_id, 2516 iss_prj_id, 2517 iss_sta_id, 2518 iss_customer_id, 2519 iss_customer_contract_id, 2520 iss_created_date, 2521 iss_updated_date, 2522 iss_last_response_date, 2523 iss_closed_date, 2524 iss_last_customer_action_date, 2525 iss_usr_id, 2526 iss_summary, 2527 pri_title, 2528 prc_title, 2529 sta_title, 2530 sta_color status_color, 2531 sta_id, 2532 iqu_status, 2533 grp_name `group`, 2534 pre_title, 2535 iss_last_public_action_date, 2536 iss_last_public_action_type, 2537 iss_last_internal_action_date, 2538 iss_last_internal_action_type, 2539 " . Issue::getLastActionFields() . ", 2540 IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type, 2541 iss_private, 2542 usr_full_name, 2543 iss_percent_complete, 2544 iss_dev_time, 2545 iss_expected_resolution_date 2546 FROM 2547 ( 2548 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 2549 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user"; 2550 // join custom fields if we are searching by custom fields 2551 if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) { 2552 foreach ($options['custom_field'] as $fld_id => $search_value) { 2553 if (empty($search_value)) { 2554 continue; 2555 } 2556 $field = Custom_Field::getDetails($fld_id); 2557 if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) { 2558 continue; 2559 } 2560 if ($field['fld_type'] == 'multiple') { 2561 $search_value = Misc::escapeInteger($search_value); 2562 foreach ($search_value as $cfo_id) { 2563 $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n"; 2564 } 2565 } else { 2566 $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n"; 2567 } 2568 } 2569 } 2570 $stmt .= ")"; 2571 // check for the custom fields we want to sort by 2572 if (strstr($options['sort_by'], 'custom_field') !== false) { 2573 $fld_id = str_replace("custom_field_", '', $options['sort_by']); 2574 $stmt .= "\n LEFT JOIN \n" . 2575 APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort 2576 ON 2577 (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n"; 2578 } 2579 if (!empty($options["users"])) { 2580 $stmt .= " 2581 LEFT JOIN 2582 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user 2583 ON 2584 isu_iss_id=iss_id"; 2585 } 2586 if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) { 2587 $stmt .= " 2588 LEFT JOIN 2589 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier 2590 ON 2591 iur_iss_id=iss_id"; 2592 } 2593 if (!empty($options["show_notification_list_issues"])) { 2594 $stmt .= " 2595 LEFT JOIN 2596 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription 2597 ON 2598 sub_iss_id=iss_id"; 2599 } 2600 $stmt .= " 2601 LEFT JOIN 2602 " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group` 2603 ON 2604 iss_grp_id=grp_id 2605 LEFT JOIN 2606 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category 2607 ON 2608 iss_prc_id=prc_id 2609 LEFT JOIN 2610 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release 2611 ON 2612 iss_pre_id = pre_id 2613 LEFT JOIN 2614 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 2615 ON 2616 iss_sta_id=sta_id 2617 LEFT JOIN 2618 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority 2619 ON 2620 iss_pri_id=pri_id 2621 LEFT JOIN 2622 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine 2623 ON 2624 iss_id=iqu_iss_id AND 2625 (iqu_expiration > '" . Date_API::getCurrentDateGMT() . "' OR iqu_expiration IS NULL) 2626 WHERE 2627 iss_prj_id= " . Misc::escapeInteger($prj_id); 2628 $stmt .= Issue::buildWhereClause($options); 2629 2630 if (strstr($options["sort_by"], 'custom_field') !== false) { 2631 $fld_details = Custom_Field::getDetails($fld_id); 2632 $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']); 2633 } else { 2634 $sort_by = Misc::escapeString($options["sort_by"]); 2635 } 2636 2637 $stmt .= " 2638 GROUP BY 2639 iss_id 2640 ORDER BY 2641 " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ", 2642 iss_id DESC"; 2643 $total_rows = Pager::getTotalRows($stmt); 2644 $stmt .= " 2645 LIMIT 2646 " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max); 2647 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 2648 if (PEAR::isError($res)) { 2649 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 2650 return array( 2651 "list" => "", 2652 "info" => "" 2653 ); 2654 } else { 2655 if (count($res) > 0) { 2656 Issue::getAssignedUsersByIssues($res); 2657 Time_Tracking::getTimeSpentByIssues($res); 2658 // need to get the customer titles for all of these issues... 2659 if (Customer::hasCustomerIntegration($prj_id)) { 2660 Customer::getCustomerTitlesByIssues($prj_id, $res); 2661 Customer::getSupportLevelsByIssues($prj_id, $res); 2662 } 2663 Issue::formatLastActionDates($res); 2664 Issue::getLastStatusChangeDates($prj_id, $res); 2665 } elseif ($current_row > 0) { 2666 // if there are no results, and the page is not the first page reset page to one and reload results 2667 Auth::redirect(APP_RELATIVE_URL . "list.php?pagerRow=0&rows=$max"); 2668 } 2669 $groups = Group::getAssocList($prj_id); 2670 $categories = Category::getAssocList($prj_id); 2671 $column_headings = Issue::getColumnHeadings($prj_id); 2672 if (count($custom_fields) > 0) { 2673 $column_headings = array_merge($column_headings,$custom_fields); 2674 } 2675 $csv[] = @implode("\t", $column_headings); 2676 for ($i = 0; $i < count($res); $i++) { 2677 $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]); 2678 $res[$i]["iss_expected_resolution_date"] = Date_API::getSimpleDate($res[$i]["iss_expected_resolution_date"], false); 2679 $fields = array( 2680 $res[$i]['pri_title'], 2681 $res[$i]['iss_id'], 2682 $res[$i]['usr_full_name'], 2683 ); 2684 // hide the group column from the output if no 2685 // groups are available in the database 2686 if (count($groups) > 0) { 2687 $fields[] = $res[$i]['group']; 2688 } 2689 $fields[] = $res[$i]['assigned_users']; 2690 $fields[] = $res[$i]['time_spent']; 2691 // hide the category column from the output if no 2692 // categories are available in the database 2693 if (count($categories) > 0) { 2694 $fields[] = $res[$i]['prc_title']; 2695 } 2696 if (Customer::hasCustomerIntegration($prj_id)) { 2697 $fields[] = @$res[$i]['customer_title']; 2698 // check if current user is acustomer and has a per incident contract. 2699 // if so, check if issue is redeemed. 2700 if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) { 2701 if ((Customer::hasPerIncidentContract($prj_id, Issue::getCustomerID($res[$i]['iss_id'])) && 2702 (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) { 2703 $res[$i]['redeemed'] = true; 2704 } 2705 } 2706 } 2707 $fields[] = $res[$i]['sta_title']; 2708 $fields[] = $res[$i]["status_change_date"]; 2709 $fields[] = $res[$i]["last_action_date"]; 2710 $fields[] = $res[$i]['iss_dev_time']; 2711 $fields[] = $res[$i]['iss_summary']; 2712 $fields[] = $res[$i]['iss_expected_resolution_date']; 2713 2714 if (count($custom_fields) > 0) { 2715 $res[$i]['custom_field'] = array(); 2716 $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']); 2717 foreach ($custom_field_values as $this_field) { 2718 if (!empty($custom_fields[$this_field['fld_id']])) { 2719 $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value']; 2720 $fields[] = $this_field['value']; 2721 } 2722 } 2723 } 2724 2725 $csv[] = @implode("\t", $fields); 2726 } 2727 $total_pages = ceil($total_rows / $max); 2728 $last_page = $total_pages - 1; 2729 return array( 2730 "list" => $res, 2731 "info" => array( 2732 "current_page" => $current_row, 2733 "start_offset" => $start, 2734 "end_offset" => $start + count($res), 2735 "total_rows" => $total_rows, 2736 "total_pages" => $total_pages, 2737 "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1), 2738 "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1), 2739 "last_page" => $last_page, 2740 "custom_fields" => $custom_fields 2741 ), 2742 "csv" => @implode("\n", $csv) 2743 ); 2744 } 2745 } 2746 2747 2748 /** 2749 * Processes a result set to format the "Last Action Date" column. 2750 * 2751 * @access public 2752 * @param array $result The result set 2753 */ 2754 function formatLastActionDates(&$result) 2755 { 2756 for ($i = 0; $i < count($result); $i++) { 2757 if (($result[$i]['action_type'] == "internal") && 2758 (Auth::getCurrentRole() > User::getRoleID('Customer'))) { 2759 $label = $result[$i]["iss_last_internal_action_type"]; 2760 $last_date = $result[$i]["iss_last_internal_action_date"]; 2761 } else { 2762 $label = $result[$i]["iss_last_public_action_type"]; 2763 $last_date = $result[$i]["iss_last_public_action_date"]; 2764 } 2765 $date = new Date($last_date); 2766 $current = new Date(Date_API::getCurrentDateGMT()); 2767 $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label), 2768 Date_API::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME))); 2769 } 2770 } 2771 2772 2773 /** 2774 * Retrieves the last status change date for the given issue. 2775 * 2776 * @access public 2777 * @param integer $prj_id The project ID 2778 * @param array $result The associative array of data 2779 * @see Issue::getListing() 2780 */ 2781 function getLastStatusChangeDates($prj_id, &$result) 2782 { 2783 $ids = array(); 2784 for ($i = 0; $i < count($result); $i++) { 2785 $ids[] = $result[$i]["iss_sta_id"]; 2786 } 2787 if (count($ids) == 0) { 2788 return false; 2789 } 2790 $customizations = Status::getProjectStatusCustomization($prj_id, $ids); 2791 for ($i = 0; $i < count($result); $i++) { 2792 if (empty($result[$i]['iss_sta_id'])) { 2793 $result[$i]['status_change_date'] = ''; 2794 } else { 2795 list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']]; 2796 if ((empty($label)) || (empty($date_field_name))) { 2797 $result[$i]['status_change_date'] = ''; 2798 continue; 2799 } 2800 $current = new Date(Date_API::getCurrentDateGMT()); 2801 $desc = "$label: %s ago"; 2802 $target_date = $result[$i][$date_field_name]; 2803 if (empty($target_date)) { 2804 $result[$i]['status_change_date'] = ''; 2805 continue; 2806 } 2807 $date = new Date($target_date); 2808 $result[$i]['status_change_date'] = sprintf($desc, Date_API::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME))); 2809 } 2810 } 2811 } 2812 2813 2814 /** 2815 * Method used to get the list of issues to be displayed in the grid layout. 2816 * 2817 * @access public 2818 * @param array $options The search parameters 2819 * @return string The where clause 2820 */ 2821 function buildWhereClause($options) 2822 { 2823 $usr_id = Auth::getUserID(); 2824 $prj_id = Auth::getCurrentProject(); 2825 $role_id = User::getRoleByUser($usr_id, $prj_id); 2826 2827 $stmt = ' AND iss_usr_id = usr_id'; 2828 if ($role_id == User::getRoleID('Customer')) { 2829 $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id); 2830 } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) { 2831 $stmt .= " AND ( 2832 iss_usr_id = $usr_id OR 2833 iur_usr_id = $usr_id 2834 )"; 2835 } 2836 2837 if (!empty($options["users"])) { 2838 $stmt .= " AND (\n"; 2839 if (stristr($options["users"], "grp") !== false) { 2840 $chunks = explode(":", $options["users"]); 2841 $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]); 2842 } else { 2843 if ($options['users'] == '-1') { 2844 $stmt .= 'isu_usr_id IS NULL'; 2845 } elseif ($options['users'] == '-2') { 2846 $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id; 2847 } elseif ($options['users'] == '-3') { 2848 $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id); 2849 } elseif ($options['users'] == '-4') { 2850 $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id); 2851 } else { 2852 $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]); 2853 } 2854 } 2855 $stmt .= ')'; 2856 } 2857 if (!empty($options["reporter"])) { 2858 $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]); 2859 } 2860 if (!empty($options["show_authorized_issues"])) { 2861 $stmt .= " AND (iur_usr_id=$usr_id)"; 2862 } 2863 if (!empty($options["show_notification_list_issues"])) { 2864 $stmt .= " AND (sub_usr_id=$usr_id)"; 2865 } 2866 if (!empty($options["keywords"])) { 2867 $stmt .= " AND (\n"; 2868 if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) { 2869 $stmt .= "iss_id IN(" . join(', ', Issue::getFullTextIssues($options)) . ")"; 2870 } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) { 2871 // check if the user is trying to search by customer email 2872 $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']); 2873 if (count($customer_ids) > 0) { 2874 $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")"; 2875 } else { 2876 // no results, kill query 2877 $stmt .= " iss_customer_id = -1"; 2878 } 2879 } else { 2880 $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]); 2881 $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")"; 2882 } 2883 $stmt .= "\n) "; 2884 } 2885 if (!empty($options["priority"])) { 2886 $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]); 2887 } 2888 if (!empty($options["status"])) { 2889 $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]); 2890 } 2891 if (!empty($options["category"])) { 2892 $stmt .= " AND iss_prc_id=" . Misc::escapeInteger($options["category"]); 2893 } 2894 if (!empty($options["hide_closed"])) { 2895 $stmt .= " AND sta_is_closed=0"; 2896 } 2897 if (!empty($options['release'])) { 2898 $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']); 2899 } 2900 // now for the date fields 2901 $date_fields = array( 2902 'created_date', 2903 'updated_date', 2904 'last_response_date', 2905 'first_response_date', 2906 'closed_date' 2907 ); 2908 foreach ($date_fields as $field_name) { 2909 if (!empty($options[$field_name])) { 2910 switch ($options[$field_name]['filter_type']) { 2911 case 'greater': 2912 $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'"; 2913 break; 2914 case 'less': 2915 $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'"; 2916 break; 2917 case 'between': 2918 $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'"; 2919 break; 2920 case 'null': 2921 $stmt .= " AND iss_$field_name IS NULL"; 2922 break; 2923 case 'in_past': 2924 if (strlen($options[$field_name]['time_period']) == 0) { 2925 $options[$field_name]['time_period'] = 0; 2926 } 2927 $stmt .= " AND (UNIX_TIMESTAMP('" . Date_API::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" . 2928 Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)"; 2929 break; 2930 } 2931 } 2932 } 2933 // custom fields 2934 if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) { 2935 foreach ($options['custom_field'] as $fld_id => $search_value) { 2936 if (empty($search_value)) { 2937 continue; 2938 } 2939 $field = Custom_Field::getDetails($fld_id); 2940 $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']); 2941 if (($field['fld_type'] == 'date') && 2942 ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) { 2943 continue; 2944 } 2945 2946 if ($field['fld_type'] == 'multiple') { 2947 $search_value = Misc::escapeInteger($search_value); 2948 foreach ($search_value as $cfo_id) { 2949 $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id"; 2950 $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id"; 2951 $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id"; 2952 } 2953 } elseif ($field['fld_type'] == 'date') { 2954 if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) { 2955 continue; 2956 } 2957 $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day']; 2958 $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND 2959 cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')"; 2960 } else { 2961 $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id"; 2962 $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id"; 2963 if ($field['fld_type'] == 'combo') { 2964 $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")"; 2965 } else { 2966 $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'"; 2967 } 2968 $stmt .= ')'; 2969 } 2970 } 2971 } 2972 // clear cached full-text values if we are not searching fulltext anymore 2973 if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) { 2974 Session::set('fulltext_string', ''); 2975 Session::set('fulltext_issues', ''); 2976 } 2977 return $stmt; 2978 } 2979 2980 2981 /** 2982 * Method used to get the previous and next issues that are available 2983 * according to the current search parameters. 2984 * 2985 * @access public 2986 * @param integer $issue_id The issue ID 2987 * @param array $options The search parameters 2988 * @return array The list of issues 2989 */ 2990 function getSides($issue_id, $options) 2991 { 2992 $usr_id = Auth::getUserID(); 2993 $role_id = Auth::getCurrentRole(); 2994 2995 $stmt = "SELECT 2996 iss_id, 2997 " . Issue::getLastActionFields() . " 2998 FROM 2999 ( 3000 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 3001 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user"; 3002 // join custom fields if we are searching by custom fields 3003 if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) { 3004 foreach ($options['custom_field'] as $fld_id => $search_value) { 3005 if (empty($search_value)) { 3006 continue; 3007 } 3008 $field = Custom_Field::getDetails($fld_id); 3009 if (($field['fld_type'] == 'date') && 3010 ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) { 3011 continue; 3012 } 3013 3014 if ($field['fld_type'] == 'multiple') { 3015 $search_value = Misc::escapeInteger($search_value); 3016 foreach ($search_value as $cfo_id) { 3017 $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n"; 3018 } 3019 } else { 3020 $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n"; 3021 } 3022 } 3023 } 3024 $stmt .= ")"; 3025 // check for the custom fields we want to sort by 3026 if (strstr($options['sort_by'], 'custom_field') !== false) { 3027 $fld_id = str_replace("custom_field_", '', $options['sort_by']); 3028 $stmt .= "\n LEFT JOIN \n" . 3029 APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort 3030 ON 3031 (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n"; 3032 } 3033 if (!empty($options["users"])) { 3034 $stmt .= " 3035 LEFT JOIN 3036 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user 3037 ON 3038 isu_iss_id=iss_id"; 3039 } 3040 if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) { 3041 $stmt .= " 3042 LEFT JOIN 3043 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier 3044 ON 3045 iur_iss_id=iss_id"; 3046 } 3047 if (!empty($options["show_notification_list_issues"])) { 3048 $stmt .= " 3049 LEFT JOIN 3050 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription 3051 ON 3052 sub_iss_id=iss_id"; 3053 } 3054 if (@$options['sort_by'] == 'prc_title') { 3055 $stmt .= " 3056 LEFT JOIN 3057 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category 3058 ON 3059 iss_prc_id = prc_id"; 3060 } 3061 $stmt .= " 3062 LEFT JOIN 3063 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 3064 ON 3065 iss_sta_id=sta_id 3066 LEFT JOIN 3067 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority 3068 ON 3069 iss_pri_id=pri_id 3070 WHERE 3071 iss_prj_id=" . Auth::getCurrentProject(); 3072 $stmt .= Issue::buildWhereClause($options); 3073 if (strstr($options["sort_by"], 'custom_field') !== false) { 3074 $fld_details = Custom_Field::getDetails($fld_id); 3075 $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']); 3076 } else { 3077 $sort_by = Misc::escapeString($options["sort_by"]); 3078 } 3079 $stmt .= " 3080 GROUP BY 3081 iss_id 3082 ORDER BY 3083 " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ", 3084 iss_id DESC"; 3085 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 3086 if (PEAR::isError($res)) { 3087 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3088 return ""; 3089 } else { 3090 // COMPAT: the next line requires PHP >= 4.0.5 3091 $index = array_search($issue_id, $res); 3092 if (!empty($res[$index+1])) { 3093 $next = $res[$index+1]; 3094 } 3095 if (!empty($res[$index-1])) { 3096 $previous = $res[$index-1]; 3097 } 3098 return array( 3099 "next" => @$next, 3100 "previous" => @$previous 3101 ); 3102 } 3103 } 3104 3105 3106 /** 3107 * Method used to get the full list of user IDs assigned to a specific 3108 * issue. 3109 * 3110 * @access public 3111 * @param integer $issue_id The issue ID 3112 * @return array The list of user IDs 3113 */ 3114 function getAssignedUserIDs($issue_id) 3115 { 3116 $stmt = "SELECT 3117 usr_id 3118 FROM 3119 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user, 3120 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 3121 WHERE 3122 isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND 3123 isu_usr_id=usr_id"; 3124 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 3125 if (PEAR::isError($res)) { 3126 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3127 return array(); 3128 } else { 3129 return $res; 3130 } 3131 } 3132 3133 3134 /** 3135 * Method used to see if a user is assigned to an issue. 3136 * 3137 * @access public 3138 * @param integer $issue_id The issue ID 3139 * @param integer $usr_id An integer containg the ID of the user. 3140 * @return boolean true if the user(s) are assigned to the issue. 3141 */ 3142 function isAssignedToUser($issue_id, $usr_id) 3143 { 3144 $assigned_users = Issue::getAssignedUserIDs($issue_id); 3145 if (in_array($usr_id, $assigned_users)) { 3146 return true; 3147 } else { 3148 return false; 3149 } 3150 } 3151 3152 3153 /** 3154 * Method used to get the full list of reporters associated with a given 3155 * list of issues. 3156 * 3157 * @access public 3158 * @param array $result The result set 3159 * @return void 3160 */ 3161 function getReportersByIssues(&$result) 3162 { 3163 $ids = array(); 3164 for ($i = 0; $i < count($result); $i++) { 3165 $ids[] = $result[$i]["iss_id"]; 3166 } 3167 $ids = implode(", ", $ids); 3168 $stmt = "SELECT 3169 iss_id, 3170 CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name 3171 FROM 3172 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 3173 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 3174 WHERE 3175 iss_usr_id=usr_id AND 3176 iss_id IN ($ids)"; 3177 $res = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 3178 if (PEAR::isError($res)) { 3179 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3180 } else { 3181 // now populate the $result variable again 3182 for ($i = 0; $i < count($result); $i++) { 3183 @$result[$i]['reporter'] = $res[$result[$i]['iss_id']]; 3184 } 3185 } 3186 } 3187 3188 3189 /** 3190 * Method used to get the full list of assigned users by a list 3191 * of issues. This was originally created to optimize the issue 3192 * listing page. 3193 * 3194 * @access public 3195 * @param array $result The result set 3196 * @return void 3197 */ 3198 function getAssignedUsersByIssues(&$result) 3199 { 3200 $ids = array(); 3201 for ($i = 0; $i < count($result); $i++) { 3202 $ids[] = $result[$i]["iss_id"]; 3203 } 3204 if (count($ids) < 1) { 3205 return; 3206 } 3207 $ids = implode(", ", $ids); 3208 $stmt = "SELECT 3209 isu_iss_id, 3210 usr_full_name 3211 FROM 3212 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user, 3213 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 3214 WHERE 3215 isu_usr_id=usr_id AND 3216 isu_iss_id IN ($ids)"; 3217 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 3218 if (PEAR::isError($res)) { 3219 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3220 } else { 3221 $t = array(); 3222 for ($i = 0; $i < count($res); $i++) { 3223 if (!empty($t[$res[$i]['isu_iss_id']])) { 3224 $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name']; 3225 } else { 3226 $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name']; 3227 } 3228 } 3229 // now populate the $result variable again 3230 for ($i = 0; $i < count($result); $i++) { 3231 @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']]; 3232 } 3233 } 3234 } 3235 3236 3237 /** 3238 * Method used to add the issue description to a list of issues. 3239 * 3240 * @access public 3241 * @param array $result The result set 3242 * @return void 3243 */ 3244 function getDescriptionByIssues(&$result) 3245 { 3246 if (count($result) == 0) { 3247 return; 3248 } 3249 3250 $ids = array(); 3251 for ($i = 0; $i < count($result); $i++) { 3252 $ids[] = $result[$i]["iss_id"]; 3253 } 3254 $ids = implode(", ", $ids); 3255 3256 $stmt = "SELECT 3257 iss_id, 3258 iss_description 3259 FROM 3260 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 3261 WHERE 3262 iss_id in ($ids)"; 3263 $res = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 3264 if (PEAR::isError($res)) { 3265 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3266 } else { 3267 for ($i = 0; $i < count($result); $i++) { 3268 @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']]; 3269 } 3270 } 3271 } 3272 3273 3274 /** 3275 * Method used to get the full list of users (the full names) assigned to a 3276 * specific issue. 3277 * 3278 * @access public 3279 * @param integer $issue_id The issue ID 3280 * @return array The list of users 3281 */ 3282 function getAssignedUsers($issue_id) 3283 { 3284 $stmt = "SELECT 3285 usr_full_name 3286 FROM 3287 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user, 3288 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 3289 WHERE 3290 isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND 3291 isu_usr_id=usr_id"; 3292 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 3293 if (PEAR::isError($res)) { 3294 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3295 return array(); 3296 } else { 3297 return $res; 3298 } 3299 } 3300 3301 3302 /** 3303 * Method used to get the details for a specific issue. 3304 * 3305 * @access public 3306 * @param integer $issue_id The issue ID 3307 * @param boolean $force_refresh If the cache should not be used. 3308 * @return array The details for the specified issue 3309 */ 3310 function getDetails($issue_id, $force_refresh = false) 3311 { 3312 static $returns; 3313 3314 $issue_id = Misc::escapeInteger($issue_id); 3315 3316 if (empty($issue_id)) { 3317 return ''; 3318 } 3319 3320 if ((!empty($returns[$issue_id])) && ($force_refresh != true)) { 3321 return $returns[$issue_id]; 3322 } 3323 3324 $stmt = "SELECT 3325 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*, 3326 prj_title, 3327 prc_title, 3328 pre_title, 3329 pri_title, 3330 sta_title, 3331 sta_abbreviation, 3332 sta_color status_color, 3333 sta_is_closed 3334 FROM 3335 ( 3336 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 3337 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project 3338 ) 3339 LEFT JOIN 3340 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority 3341 ON 3342 iss_pri_id=pri_id 3343 LEFT JOIN 3344 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 3345 ON 3346 iss_sta_id=sta_id 3347 LEFT JOIN 3348 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category 3349 ON 3350 iss_prc_id=prc_id 3351 LEFT JOIN 3352 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release 3353 ON 3354 iss_pre_id=pre_id 3355 WHERE 3356 iss_id=$issue_id AND 3357 iss_prj_id=prj_id"; 3358 $res = $GLOBALS["db_api"]->dbh->getRow($stmt, DB_FETCHMODE_ASSOC); 3359 if (PEAR::isError($res)) { 3360 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3361 return ""; 3362 } else { 3363 if (empty($res)) { 3364 return ""; 3365 } else { 3366 $created_date_ts = Date_API::getUnixTimestamp($res['iss_created_date'], Date_API::getDefaultTimezone()); 3367 // get customer information, if any 3368 if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) { 3369 $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']); 3370 $res['contact_local_time'] = Date_API::getFormattedDate(Date_API::getCurrentDateGMT(), $res['iss_contact_timezone']); 3371 $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']); 3372 $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']); 3373 $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']); 3374 $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60); 3375 if (empty($res['iss_first_response_date'])) { 3376 $first_response_deadline = $created_date_ts + $max_first_response_time; 3377 if (Date_API::getCurrentUnixTimestampGMT() <= $first_response_deadline) { 3378 $res['max_first_response_time_left'] = Date_API::getFormattedDateDiff($first_response_deadline, Date_API::getCurrentUnixTimestampGMT()); 3379 } else { 3380 $res['overdue_first_response_time'] = Date_API::getFormattedDateDiff(Date_API::getCurrentUnixTimestampGMT(), $first_response_deadline); 3381 } 3382 } 3383 } 3384 $res['iss_original_description'] = $res["iss_description"]; 3385 if (!strstr($_SERVER["PHP_SELF"], 'update.php')) { 3386 $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"])); 3387 $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]); 3388 } 3389 $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"])); 3390 $res["iss_created_date"] = Date_API::getFormattedDate($res["iss_created_date"]); 3391 $res['iss_created_date_ts'] = $created_date_ts; 3392 $res["assignments"] = @implode(", ", array_values(Issue::getAssignedUsers($res["iss_id"]))); 3393 list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]); 3394 $temp = Issue::getAssignedUsersStatus($res["iss_id"]); 3395 $res["has_inactive_users"] = 0; 3396 $res["assigned_users"] = array(); 3397 $res["assigned_inactive_users"] = array(); 3398 foreach ($temp as $usr_id => $usr_status) { 3399 if (!User::isActiveStatus($usr_status)) { 3400 $res["assigned_inactive_users"][] = $usr_id; 3401 $res["has_inactive_users"] = 1; 3402 } else { 3403 $res["assigned_users"][] = $usr_id; 3404 } 3405 } 3406 if (@in_array(Auth::getUserID(), $res["assigned_users"])) { 3407 $res["is_current_user_assigned"] = 1; 3408 } else { 3409 $res["is_current_user_assigned"] = 0; 3410 } 3411 $res["associated_issues_details"] = Issue::getAssociatedIssuesDetails($res["iss_id"]); 3412 $res["associated_issues"] = Issue::getAssociatedIssues($res["iss_id"]); 3413 $res["reporter"] = User::getFullName($res["iss_usr_id"]); 3414 if (empty($res["iss_updated_date"])) { 3415 $res["iss_updated_date"] = 'not updated yet'; 3416 } else { 3417 $res["iss_updated_date"] = Date_API::getFormattedDate($res["iss_updated_date"]); 3418 } 3419 $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]); 3420 if (Release::isAssignable($res["iss_pre_id"])) { 3421 $release = Release::getDetails($res["iss_pre_id"]); 3422 $res["pre_title"] = $release["pre_title"]; 3423 $res["pre_status"] = $release["pre_status"]; 3424 } 3425 // need to return the list of issues that are duplicates of this one 3426 $res["duplicates"] = Issue::getDuplicateList($res["iss_id"]); 3427 $res["duplicates_details"] = Issue::getDuplicateDetailsList($res["iss_id"]); 3428 // also get the issue title of the duplicated issue 3429 if (!empty($res['iss_duplicated_iss_id'])) { 3430 $res['duplicated_issue'] = Issue::getDuplicatedDetails($res['iss_duplicated_iss_id']); 3431 } 3432 3433 // get group information 3434 if (!empty($res["iss_grp_id"])) { 3435 $res["group"] = Group::getDetails($res["iss_grp_id"]); 3436 } 3437 3438 // get quarantine issue 3439 $res["quarantine"] = Issue::getQuarantineInfo($res["iss_id"]); 3440 3441 $returns[$issue_id] = $res; 3442 return $res; 3443 } 3444 } 3445 } 3446 3447 3448 /** 3449 * Method used to get some simple details about the given duplicated issue. 3450 * 3451 * @access public 3452 * @param integer $issue_id The issue ID 3453 * @return array The duplicated issue details 3454 */ 3455 function getDuplicatedDetails($issue_id) 3456 { 3457 $stmt = "SELECT 3458 iss_summary title, 3459 sta_title current_status, 3460 sta_is_closed is_closed 3461 FROM 3462 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 3463 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 3464 WHERE 3465 iss_sta_id=sta_id AND 3466 iss_id=$issue_id"; 3467 $res = $GLOBALS["db_api"]->dbh->getRow($stmt, DB_FETCHMODE_ASSOC); 3468 if (PEAR::isError($res)) { 3469 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3470 return array(); 3471 } else { 3472 return $res; 3473 } 3474 } 3475 3476 3477 /** 3478 * Method used to bulk update a list of issues 3479 * 3480 * @access public 3481 * @return boolean 3482 */ 3483 function bulkUpdate() 3484 { 3485 // check if user performing this chance has the proper role 3486 if (Auth::getCurrentRole() < User::getRoleID('Manager')) { 3487 return -1; 3488 } 3489 3490 $items = Misc::escapeInteger($_POST['item']); 3491 $new_status_id = Misc::escapeInteger($_POST['status']); 3492 $new_release_id = Misc::escapeInteger($_POST['release']); 3493 $new_priority_id = Misc::escapeInteger($_POST['priority']); 3494 $new_category_id = Misc::escapeInteger($_POST['category']); 3495 3496 for ($i = 0; $i < count($items); $i++) { 3497 if (!Issue::canAccess($items[$i], Auth::getUserID())) { 3498 continue; 3499 } elseif (Issue::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) { 3500 // make sure issue is not in another project 3501 continue; 3502 } 3503 3504 $updated_fields = array(); 3505 3506 // update assignment 3507 if (count(@$_POST['users']) > 0) { 3508 $users = Misc::escapeInteger($_POST['users']); 3509 // get who this issue is currently assigned too 3510 $stmt = "SELECT 3511 isu_usr_id, 3512 usr_full_name 3513 FROM 3514 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user, 3515 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user 3516 WHERE 3517 isu_usr_id = usr_id AND 3518 isu_iss_id = " . $items[$i]; 3519 $current_assignees = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 3520 if (PEAR::isError($current_assignees)) { 3521 Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__); 3522 return -1; 3523 } 3524 foreach ($current_assignees as $usr_id => $usr_name) { 3525 if (!in_array($usr_id, $users)) { 3526 Issue::deleteUserAssociation($items[$i], $usr_id, false); 3527 } 3528 } 3529 $new_user_names = array(); 3530 $new_assignees = array(); 3531 foreach ($users as $usr_id) { 3532 $new_user_names[$usr_id] = User::getFullName($usr_id); 3533 3534 // check if the issue is already assigned to this person 3535 $stmt = "SELECT 3536 COUNT(*) AS total 3537 FROM 3538 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user 3539 WHERE 3540 isu_iss_id=" . $items[$i] . " AND 3541 isu_usr_id=" . $usr_id; 3542 $total = $GLOBALS["db_api"]->dbh->getOne($stmt); 3543 if ($total > 0) { 3544 continue; 3545 } else { 3546 $new_assignees[] = $usr_id; 3547 // add the assignment 3548 Issue::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false); 3549 Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions()); 3550 Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID()); 3551 } 3552 } 3553 Notification::notifyNewAssignment($new_assignees, $items[$i]); 3554 $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names)); 3555 } 3556 3557 // update status 3558 if (!empty($new_status_id)) { 3559 $old_status_id = Issue::getStatusID($items[$i]); 3560 $res = Issue::setStatus($items[$i], $new_status_id, false); 3561 if ($res == 1) { 3562 $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id)); 3563 } 3564 } 3565 3566 // update release 3567 if (!empty($new_release_id)) { 3568 $old_release_id = Issue::getRelease($items[$i]); 3569 $res = Issue::setRelease($items[$i], $new_release_id); 3570 if ($res == 1) { 3571 $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id)); 3572 } 3573 } 3574 3575 // update priority 3576 if (!empty($new_priority_id)) { 3577 $old_priority_id = Issue::getPriority($items[$i]); 3578 $res = Issue::setPriority($items[$i], $new_priority_id); 3579 if ($res == 1) { 3580 $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id)); 3581 } 3582 } 3583 3584 // update category 3585 if (!empty($new_category_id)) { 3586 $old_category_id = Issue::getCategory($items[$i]); 3587 $res = Issue::setCategory($items[$i], $new_category_id); 3588 if ($res == 1) { 3589 $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id)); 3590 } 3591 } 3592 3593 if (count($updated_fields) > 0) { 3594 // log the changes 3595 $changes = ''; 3596 $k = 0; 3597 foreach ($updated_fields as $key => $value) { 3598 if ($k > 0) { 3599 $changes .= "; "; 3600 } 3601 $changes .= "$key: $value"; 3602 $k++; 3603 } 3604 History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID())); 3605 } 3606 } 3607 return true; 3608 } 3609 3610 3611 /** 3612 * Method used to set the initial impact analysis for a specific issue 3613 * 3614 * @access public 3615 * @param integer $issue_id The issue ID 3616 * @return integer 1 if the update worked, -1 otherwise 3617 */ 3618 function setImpactAnalysis($issue_id) 3619 { 3620 $stmt = "UPDATE 3621 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 3622 SET 3623 iss_updated_date='" . Date_API::getCurrentDateGMT() . "', 3624 iss_last_internal_action_date='" . Date_API::getCurrentDateGMT() . "', 3625 iss_last_internal_action_type='update', 3626 iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ", 3627 iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "' 3628 WHERE 3629 iss_id=" . Misc::escapeInteger($issue_id); 3630 $res = $GLOBALS["db_api"]->dbh->query($stmt); 3631 if (PEAR::isError($res)) { 3632 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3633 return -1; 3634 } else { 3635 // add the impact analysis to the history of the issue 3636 $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID()); 3637 History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary); 3638 return 1; 3639 } 3640 } 3641 3642 3643 /** 3644 * Method used to get the full list of issue IDs that area available in the 3645 * system. 3646 * 3647 * @access public 3648 * @param string $extra_condition An extra condition in the WHERE clause 3649 * @return array The list of issue IDs 3650 */ 3651 function getColList($extra_condition = NULL) 3652 { 3653 $stmt = "SELECT 3654 iss_id 3655 FROM 3656 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 3657 WHERE 3658 iss_prj_id=" . Auth::getCurrentProject(); 3659 if (!empty($extra_condition)) { 3660 $stmt .= " AND $extra_condition "; 3661 } 3662 $stmt .= " 3663 ORDER BY 3664 iss_id DESC"; 3665 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 3666 if (PEAR::isError($res)) { 3667 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3668 return ""; 3669 } else { 3670 return $res; 3671 } 3672 } 3673 3674 3675 /** 3676 * Method used to get the full list of issue IDs and their respective 3677 * titles. 3678 * 3679 * @access public 3680 * @param string $extra_condition An extra condition in the WHERE clause 3681 * @return array The list of issues 3682 */ 3683 function getAssocList($extra_condition = NULL) 3684 { 3685 $stmt = "SELECT 3686 iss_id, 3687 iss_summary 3688 FROM 3689 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 3690 WHERE 3691 iss_prj_id=" . Auth::getCurrentProject(); 3692 if (!empty($extra_condition)) { 3693 $stmt .= " AND $extra_condition "; 3694 } 3695 $stmt .= " 3696 ORDER BY 3697 iss_id ASC"; 3698 $res = $GLOBALS["db_api"]->dbh->getAssoc($stmt); 3699 if (PEAR::isError($res)) { 3700 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3701 return ""; 3702 } else { 3703 return $res; 3704 } 3705 } 3706 3707 3708 /** 3709 * Method used to get the list of issues associated to a specific issue. 3710 * 3711 * @access public 3712 * @param integer $issue_id The issue ID 3713 * @return array The list of associated issues 3714 */ 3715 function getAssociatedIssues($issue_id) 3716 { 3717 $issues = Issue::getAssociatedIssuesDetails($issue_id); 3718 $associated = array(); 3719 for ($i = 0; $i < count($issues); $i++) { 3720 $associated[] = $issues[$i]['associated_issue']; 3721 } 3722 return $associated; 3723 } 3724 3725 3726 /** 3727 * Method used to get the list of issues associated details to a 3728 * specific issue. 3729 * 3730 * @access public 3731 * @param integer $issue_id The issue ID 3732 * @return array The list of associated issues 3733 */ 3734 function getAssociatedIssuesDetails($issue_id) 3735 { 3736 static $returns; 3737 3738 if (!empty($returns[$issue_id])) { 3739 return $returns[$issue_id]; 3740 } 3741 3742 $stmt = "SELECT 3743 isa_associated_id associated_issue, 3744 iss_summary associated_title, 3745 sta_title current_status, 3746 sta_is_closed is_closed 3747 FROM 3748 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association, 3749 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 3750 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 3751 WHERE 3752 isa_associated_id=iss_id AND 3753 iss_sta_id=sta_id AND 3754 isa_issue_id=$issue_id"; 3755 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 3756 if (PEAR::isError($res)) { 3757 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3758 return array(); 3759 } else { 3760 $returns[$issue_id] = $res; 3761 return $res; 3762 } 3763 } 3764 3765 3766 /** 3767 * Method used to check whether an issue was already closed or not. 3768 * 3769 * @access public 3770 * @param integer $issue_id The issue ID 3771 * @return boolean 3772 */ 3773 function isClosed($issue_id) 3774 { 3775 $stmt = "SELECT 3776 COUNT(*) 3777 FROM 3778 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 3779 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status 3780 WHERE 3781 iss_id=" . Misc::escapeInteger($issue_id) . " AND 3782 iss_sta_id=sta_id AND 3783 sta_is_closed=1"; 3784 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 3785 if (PEAR::isError($res)) { 3786 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3787 return false; 3788 } else { 3789 if ($res == 0) { 3790 return false; 3791 } else { 3792 return true; 3793 } 3794 } 3795 } 3796 3797 3798 /** 3799 * Returns a simple list of issues that are currently set to some 3800 * form of quarantine. This is mainly used by the IRC interface. 3801 * 3802 * @access public 3803 * @return array List of quarantined issues 3804 */ 3805 function getQuarantinedIssueList() 3806 { 3807 // XXX: would be nice to restrict the result list to only one project 3808 $stmt = "SELECT 3809 iss_id, 3810 iss_summary 3811 FROM 3812 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue, 3813 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine 3814 WHERE 3815 iqu_iss_id=iss_id AND 3816 iqu_expiration >= '" . Date_API::getCurrentDateGMT() . "' AND 3817 iqu_expiration IS NOT NULL"; 3818 $res = $GLOBALS["db_api"]->dbh->getAll($stmt, DB_FETCHMODE_ASSOC); 3819 if (PEAR::isError($res)) { 3820 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3821 return array(); 3822 } else { 3823 Issue::getAssignedUsersByIssues($res); 3824 return $res; 3825 } 3826 } 3827 3828 3829 /** 3830 * Returns the status of a quarantine. 3831 * 3832 * @param integer $issue_id The issue ID 3833 * @return integer Indicates what the current state of quarantine is. 3834 */ 3835 function getQuarantineInfo($issue_id) 3836 { 3837 $stmt = "SELECT 3838 iqu_status, 3839 iqu_expiration 3840 FROM 3841 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine 3842 WHERE 3843 iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND 3844 (iqu_expiration > '" . Date_API::getCurrentDateGMT() . "' OR 3845 iqu_expiration IS NULL)"; 3846 $res = $GLOBALS["db_api"]->dbh->getRow($stmt, DB_FETCHMODE_ASSOC); 3847 if (PEAR::isError($res)) { 3848 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3849 return array(); 3850 } else { 3851 if (!empty($res["iqu_expiration"])) { 3852 $expiration_ts = Date_API::getUnixTimestamp($res['iqu_expiration'], Date_API::getDefaultTimezone()); 3853 $res["time_till_expiration"] = Date_API::getFormattedDateDiff($expiration_ts, Date_API::getCurrentUnixTimestampGMT()); 3854 } 3855 return $res; 3856 } 3857 } 3858 3859 3860 /** 3861 * Sets the quarantine status. Optionally an expiration date can be set 3862 * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active. 3863 * 3864 * @access public 3865 * @param integer $issue_id The issue ID 3866 * @param integer $status The quarantine status 3867 * @param string $expiration The expiration date of quarantine (default empty) 3868 */ 3869 function setQuarantine($issue_id, $status, $expiration = '') 3870 { 3871 $issue_id = Misc::escapeInteger($issue_id); 3872 $status = Misc::escapeInteger($status); 3873 3874 // see if there is an existing record 3875 $stmt = "SELECT 3876 COUNT(*) 3877 FROM 3878 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine 3879 WHERE 3880 iqu_iss_id = $issue_id"; 3881 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 3882 if (PEAR::isError($res)) { 3883 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3884 return -1; 3885 } 3886 if ($res > 0) { 3887 // update 3888 $stmt = "UPDATE 3889 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine 3890 SET 3891 iqu_status = $status"; 3892 if (!empty($expiration)) { 3893 $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'"; 3894 } 3895 $stmt .= "\nWHERE 3896 iqu_iss_id = $issue_id"; 3897 $res = $GLOBALS["db_api"]->dbh->query($stmt); 3898 if (PEAR::isError($res)) { 3899 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3900 return -1; 3901 } else { 3902 // add history entry about this change taking place 3903 if ($status == 0) { 3904 History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'), 3905 "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID())); 3906 } 3907 } 3908 } else { 3909 // insert 3910 $stmt = "INSERT INTO 3911 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine 3912 ( 3913 iqu_iss_id, 3914 iqu_status"; 3915 if (!empty($expiration)) { 3916 $stmt .= ",\niqu_expiration\n"; 3917 } 3918 $stmt .= ") VALUES ( 3919 $issue_id, 3920 $status"; 3921 if (!empty($expiration)) { 3922 $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n"; 3923 } 3924 $stmt .= ")"; 3925 $res = $GLOBALS["db_api"]->dbh->query($stmt); 3926 if (PEAR::isError($res)) { 3927 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3928 return -1; 3929 } 3930 } 3931 return 1; 3932 } 3933 3934 3935 /** 3936 * Sets the group of the issue. 3937 * 3938 * @access public 3939 * @param integer $issue_id The ID of the issue 3940 * @param integer $group_id The ID of the group 3941 * @return integer 1 if successful, -1 or -2 otherwise 3942 */ 3943 function setGroup($issue_id, $group_id) 3944 { 3945 $issue_id = Misc::escapeInteger($issue_id); 3946 $group_id = Misc::escapeInteger($group_id); 3947 3948 $current = Issue::getDetails($issue_id); 3949 if ($current["iss_grp_id"] == $group_id) { 3950 return -2; 3951 } 3952 $stmt = "UPDATE 3953 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 3954 SET 3955 iss_grp_id = $group_id 3956 WHERE 3957 iss_id = $issue_id"; 3958 $res = $GLOBALS["db_api"]->dbh->query($stmt); 3959 if (PEAR::isError($res)) { 3960 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3961 return -1; 3962 } 3963 $current_user = Auth::getUserID(); 3964 if (empty($current_user)) { 3965 $current_user = APP_SYSTEM_USER_ID; 3966 } 3967 History::add($issue_id, $current_user, History::getTypeID('group_changed'), 3968 "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user)); 3969 return 1; 3970 } 3971 3972 3973 /** 3974 * Returns the group ID associated with the given issue ID. 3975 * 3976 * @access public 3977 * @param integer $issue_id The issue ID 3978 * @return integer The associated group ID 3979 */ 3980 function getGroupID($issue_id) 3981 { 3982 $stmt = "SELECT 3983 iss_grp_id 3984 FROM 3985 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 3986 WHERE 3987 iss_id=" . Misc::escapeInteger($issue_id); 3988 $res = $GLOBALS["db_api"]->dbh->getOne($stmt); 3989 if (PEAR::isError($res)) { 3990 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 3991 return 0; 3992 } else { 3993 return $res; 3994 } 3995 } 3996 3997 3998 /** 3999 * Returns an array of issues based on full text search results. 4000 * 4001 * @param array $options An array of search options 4002 * @return array An array of issue IDS 4003 */ 4004 function getFullTextIssues($options) 4005 { 4006 // check if a list of issues for this full text search is already cached 4007 $fulltext_string = Session::get('fulltext_string'); 4008 if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) { 4009 return Session::get('fulltext_issues'); 4010 } 4011 4012 // no pre-existing list, generate them 4013 $stmt = "(SELECT 4014 DISTINCT(iss_id) 4015 FROM 4016 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 4017 WHERE 4018 MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE) 4019 ) UNION ( 4020 SELECT 4021 DISTINCT(not_iss_id) 4022 FROM 4023 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note 4024 WHERE 4025 MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE) 4026 ) UNION ( 4027 SELECT 4028 DISTINCT(ttr_iss_id) 4029 FROM 4030 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking 4031 WHERE 4032 MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE) 4033 ) UNION ( 4034 SELECT 4035 DISTINCT(phs_iss_id) 4036 FROM 4037 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support 4038 WHERE 4039 MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE) 4040 ) UNION ( 4041 SELECT 4042 DISTINCT(sup_iss_id) 4043 FROM 4044 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email, 4045 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body 4046 WHERE 4047 sup_id = seb_sup_id AND 4048 MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE) 4049 )"; 4050 $res = $GLOBALS["db_api"]->dbh->getCol($stmt); 4051 if (PEAR::isError($res)) { 4052 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 4053 return array(-1); 4054 } else { 4055 $stmt = "SELECT 4056 DISTINCT(icf_iss_id) 4057 FROM 4058 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field 4059 WHERE 4060 MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)"; 4061 $custom_res = $GLOBALS["db_api"]->dbh->getCol($stmt); 4062 if (PEAR::isError($custom_res)) { 4063 Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__); 4064 return array(-1); 4065 } 4066 $issues = array_merge($res, $custom_res); 4067 // we kill the query results on purpose to flag that no 4068 // issues could be found with fulltext search 4069 if (count($issues) < 1) { 4070 $issues = array(-1); 4071 } 4072 Session::set('fulltext_string', $options['keywords']); 4073 Session::set('fulltext_issues', $issues); 4074 return $issues; 4075 } 4076 } 4077 4078 4079 /** 4080 * Method to determine if user can access a particular issue 4081 * 4082 * @access public 4083 * @param integer $issue_id The ID of the issue. 4084 * @param integer $usr_id The ID of the user 4085 * @return boolean If the user can access the issue 4086 */ 4087 function canAccess($issue_id, $usr_id) 4088 { 4089 static $access; 4090 4091 if (empty($issue_id)) { 4092 return true; 4093 } 4094 4095 if (isset($access[$issue_id . "-" . $usr_id])) { 4096 return $access[$issue_id . "-" . $usr_id]; 4097 } 4098 4099 $details = Issue::getDetails($issue_id); 4100 if (empty($details)) { 4101 return true; 4102 } 4103 $usr_details = User::getDetails($usr_id); 4104 $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']); 4105 $prj_id = Issue::getProjectID($issue_id); 4106 4107 // check customer permissions 4108 if ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) && 4109 ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) { 4110 $return = false; 4111 } elseif ($details['iss_private'] == 1) { 4112 // check if the issue is even private 4113 4114 // check role, reporter, assigment and group 4115 if (User::getRoleByUser($usr_id, $details['iss_prj_id']) > User::getRoleID("Developer")) { 4116 $return = true; 4117 } elseif ($details['iss_usr_id'] == $usr_id) { 4118 $return = true; 4119 } elseif (Issue::isAssignedToUser($issue_id, $usr_id)) { 4120 $return = true; 4121 } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) && 4122 ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) { 4123 $return = true; 4124 } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) { 4125 $return = true; 4126 } else { 4127 $return = false; 4128 } 4129 } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) && 4130 ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) { 4131 return false; 4132 } else { 4133 $return = true; 4134 } 4135 4136 $access[$issue_id . "-" . $usr_id] = $return; 4137 return $return; 4138 } 4139 4140 4141 /** 4142 * Returns true if the specified issue is private, false otherwise 4143 * 4144 * @access public 4145 * @param integer $issue_id The ID of the issue 4146 * @return boolean If the issue is private or not 4147 */ 4148 function isPrivate($issue_id) 4149 { 4150 static $returns; 4151 4152 if (!isset($returns[$issue_id])) { 4153 $sql = "SELECT 4154 iss_private 4155 FROM 4156 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 4157 WHERE 4158 iss_id=$issue_id"; 4159 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 4160 if (PEAR::isError($res)) { 4161 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 4162 return true; 4163 } else { 4164 if ($res == 1) { 4165 $returns[$issue_id] = true; 4166 } else { 4167 $returns[$issue_id] = false; 4168 } 4169 } 4170 } 4171 return $returns[$issue_id]; 4172 } 4173 4174 4175 /** 4176 * Clears closed information from an issues. 4177 * 4178 * @access public 4179 * @param integer $issue_id The ID of the issue 4180 */ 4181 function clearClosed($issue_id) 4182 { 4183 $stmt = "UPDATE 4184 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 4185 SET 4186 iss_closed_date = null, 4187 iss_res_id = null 4188 WHERE 4189 iss_id=" . Misc::escapeInteger($issue_id); 4190 $res = $GLOBALS["db_api"]->dbh->query($stmt); 4191 if (PEAR::isError($res)) { 4192 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 4193 return -1; 4194 } 4195 } 4196 4197 4198 /** 4199 * Returns the message ID that should be used as the parent ID for all messages 4200 * 4201 * @access public 4202 * @param integer $issue_id The ID of the issue 4203 */ 4204 function getRootMessageID($issue_id) 4205 { 4206 $sql = "SELECT 4207 iss_root_message_id 4208 FROM 4209 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 4210 WHERE 4211 iss_id=" . Misc::escapeInteger($issue_id); 4212 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 4213 if (PEAR::isError($res)) { 4214 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 4215 return false; 4216 } else { 4217 return $res; 4218 } 4219 } 4220 4221 4222 /** 4223 * Returns the issue ID of the issue with the specified root message ID, or false 4224 * @access public 4225 * @param string $msg_id The Message ID 4226 * @return integer The ID of the issue 4227 */ 4228 function getIssueByRootMessageID($msg_id) 4229 { 4230 static $returns; 4231 4232 if (!empty($returns[$msg_id])) { 4233 return $returns[$msg_id]; 4234 } 4235 $sql = "SELECT 4236 iss_id 4237 FROM 4238 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue 4239 WHERE 4240 iss_root_message_id = '" . Misc::escapeString($msg_id) . "'"; 4241 $res = $GLOBALS["db_api"]->dbh->getOne($sql); 4242 if (PEAR::isError($res)) { 4243 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); 4244 return false; 4245 } 4246 if (empty($res)) { 4247 $returns[$msg_id] = false; 4248 } else { 4249 $returns[$msg_id] = $res; 4250 } 4251 return $returns[$msg_id]; 4252 } 4253 } 4254 4255 // benchmarking the included file (aka setup time) 4256 if (APP_BENCHMARK) { 4257 $GLOBALS['bench']->setMarker('Included Issue Class'); 4258 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Dec 19 21:21:33 2007 | Cross-referenced by PHPXref 0.7 |