[ Index ]

PHP Cross Reference of Eventum

title

Body

[close]

/include/ -> class.issue.php (source)

   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  }


Generated: Wed Dec 19 21:21:33 2007 Cross-referenced by PHPXref 0.7