---- eventum-new/htdocs/ajax/order.php 2008-10-15 02:02:25.000000000 +0300
-+++ eventum-new/htdocs/ajax/order.php 2008-10-15 02:02:25.000000000 +0300
+--- eventum-2.2/htdocs/ajax/order.php 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/htdocs/ajax/order.php 2009-10-12 22:10:36.429185594 +0300
@@ -0,0 +1,72 @@
+<?
+require_once(dirname(__FILE__) . '/../init.php');
+ }
+ }
+}
---- eventum/htdocs/ajax/update.php 2008-10-15 01:46:20.000000000 +0300
-+++ eventum-new/htdocs/ajax/update.php 2008-10-15 02:02:25.000000000 +0300
-@@ -0,0 +1,30 @@
+--- eventum-2.2/htdocs/ajax/update.php 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/htdocs/ajax/update.php 2009-10-12 22:10:36.439185157 +0300
+@@ -0,0 +1,38 @@
+<?
+require_once(dirname(__FILE__) . '/../init.php');
+require_once(APP_INC_PATH . "class.auth.php");
+ case 'expected_resolution_date':
+ $day = (int)$_POST['day'];
+ $month = (int)$_POST['month'];
-+ $year = (int)$_POST['year'];
-+ if (Issue::updateField($_POST['issueID'], $_POST['fieldName'], sprintf('%04d-%02d-%02d', $year, $month, $day)) !== -1) {
-+ echo Date_Helper::getSimpleDate(sprintf('%04d-%02d-%02d', $year, $month, $day), false);
++ $year = (int)$_POST['year'];
++ if ($day == 0 && $month == 1 && $year == 0) {
++ // clear button
++ $date = null;
++ } else {
++ $date = sprintf('%04d-%02d-%02d', $year, $month, $day);
++ }
++ if (Issue::updateField($_POST['issueID'], $_POST['fieldName'], $date) !== -1) {
++ if (!is_null($date)) {
++ echo Date_Helper::getSimpleDate(sprintf('%04d-%02d-%02d', $year, $month, $day), false);
++ }
+ } else {
+ echo 'update failed';
+ }
+ die('object type not supported');
+ break;
+}
---- eventum/lib/eventum/class.display_column.php 2008-10-15 01:46:20.000000000 +0300
-+++ eventum-new/lib/eventum/class.display_column.php 2008-10-15 02:02:25.000000000 +0300
-@@ -229,7 +229,10 @@
- ),
- "iss_expected_resolution_date" => array(
- "title" => ev_gettext("Expected Resolution Date")
-- )
-+ ),
-+ "isu_order" => array(
-+ "title" => ev_gettext("Order")
-+ ),
- )
- );
- return $columns[$page];
---- eventum-2.2/lib/eventum/class.issue.php~ 2009-07-23 14:02:20.000000000 +0300
-+++ eventum-2.2/lib/eventum/class.issue.php 2009-07-23 14:02:38.085536073 +0300
-@@ -1356,6 +1356,7 @@
- Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
- return -1;
- } else {
-+ self::moveOrderForAllUsers($issue_id, 1000);
- $prj_id = self::getProjectID($issue_id);
-
- // record the change
-@@ -1659,6 +1660,176 @@
- }
- }
-
-+ /**
-+ * Method used to update the a single detail field of a specific issue.
-+ *
-+ * @param integer $issue_id
-+ * @param string $field_name
-+ * @param string $field_value
-+ * @param string $field_type string or integer (for escape)
-+ * @return integer 1 on success, -1 otherwise
-+ */
-+ function updateField($issue_id, $field_name, $filed_value) {
+--- eventum-2.2/htdocs/css/style.css 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/htdocs/css/style.css 2009-10-12 22:10:36.439185157 +0300
+@@ -172,4 +172,22 @@
+ font-size: 70%;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
+ padding: 10px;
+-}
+\ No newline at end of file
++}
++.tDnD_whileDrag td {
++ background-color: #ffffdd;
++}
++.tDnD_whileDrag td {
++ border: 1px solid red;
++}
++.inline_date_pick {
++ cursor: pointer;
++}
++.custom_field {
++ cursor: pointer;
++}
++.showDragHandle {
++ cursor: move;
++ background-image: url(../images/updown2.gif);
++ background-repeat: no-repeat;
++ background-position: center center;
++}
+--- eventum-2.2/htdocs/js/global.js 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/htdocs/js/global.js 2009-10-12 22:10:36.439185157 +0300
+@@ -798,4 +798,44 @@
+ firstDay: user_prefs.week_firstday
+ });
+ });
+
-+ $issue_id = Misc::escapeInteger($issue_id);
++$(document).ready(function() {
++ // dialog type calender isn't working in Konqueror beacuse it's not a supported browser for either jQuery or jQuery UI
++ // http://groups.google.com/group/jquery-ui/browse_thread/thread/ea61238c34cb5f33/046837b02fb90b5c
++ if (navigator.appName != 'Konqueror') {
++ $(".inline_date_pick").click(function() {
++ var masterObj = this;
++ var masterObjPos = $(masterObj).offset();
++ // offset gives uses top and left but datepicker needs pageX and pageY
++ var masterObjPos = {pageX: masterObjPos.left, pageY: masterObjPos.top};
++ $(this).datepicker(
++ // we use dialog type calender so we won't haveto have a hidden element on the page
++ 'dialog',
++ // selected date
++ masterObj.innerHTML,
++ // onclick handler
++ function (date, dteObj) {
++ fieldName = masterObj.id.substr(0,masterObj.id.indexOf('|'));
++ issueID = masterObj.id.substr(masterObj.id.indexOf('|')+1);
++ if (date == '') {
++ // clear button
++ dteObj.selectedDay = 0;
++ dteObj.selectedMonth = 0;
++ dteObj.selectedYear = 0;
++ }
++ //alertProperties(date);
++ $.post("/ajax/update.php", {fieldName: fieldName, issueID: issueID, day: dteObj.selectedDay, month: (dteObj.selectedMonth+1), year: dteObj.selectedYear}, function(data) {
++ masterObj.innerHTML = data;
++ }, "text");
++ },
++ // config
++ {dateFormat: 'dd M yy', duration: ""},
++ // position of the datepicker calender - taken from div's offset
++ masterObjPos
++ );
++ return false;
++ });
++ }
++});
+
-+ $usr_id = Auth::getUserID();
-+ $prj_id = self::getProjectID($issue_id);
+ //-->
+--- eventum-2.2/htdocs/js/global.js.~1~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/htdocs/js/global.js.~1~ 2009-09-14 18:07:55.000000000 +0300
+@@ -0,0 +1,801 @@
++<!--
++// @(#) $Id$
++var today = new Date();
++var expires = new Date(today.getTime() + (56 * 86400000));
+
-+ // get all of the 'current' information of this issue
-+ $current = self::getDetails($issue_id);
++function addFileRow(element_name, field_name)
++{
++ if (document.all) {
++ var fileTable = document.all[element_name];
++ } else if (!document.all && document.getElementById) {
++ var fileTable = document.getElementById(element_name);
++ }
++ if (!fileTable) {
++ return;
++ }
++ rows = fileTable.rows.length;
+
-+ $stmt = "UPDATE
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
-+ SET
-+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
-+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
-+ iss_last_public_action_type='updated'";
++ // check if last box is empty and if it is, don't add another
++ if (document.all) {
++ var last_field = document.all[field_name + '_' + rows];
++ } else if (!document.all && document.getElementById) {
++ var last_field = document.getElementById(field_name + '_' + rows);
++ }
++ if (last_field && last_field.value == '') {
++ return;
++ }
+
-+ switch ($field_name) {
-+ case 'category':
-+ $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'release':
-+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'expected_resolution_date':
-+ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
-+ break;
-+ case 'release':
-+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'priority':
-+ $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'status':
-+ $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'resolution':
-+ $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'summary':
-+ $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
-+ break;
-+ case 'description':
-+ $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
-+ break;
-+ case 'estimated_dev_time':
-+ $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
-+ break;
-+ case 'percent_complete':
-+ $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
-+ break;
-+ case 'trigger_reminders':
-+ $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'group':
-+ $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
-+ break;
-+ case 'private':
-+ $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
-+ break;
-+ default:
-+ Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
-+ return -1;
-+ break;
++ newRow = fileTable.insertRow(rows);
++ cell = newRow.insertCell(0);
++ if (document.all) {
++ cell.innerHTML = '<input id="' + field_name + '_' + (rows+1) + '" class="shortcut" size="40" type="file" name="' + field_name + '" onChange="javascript:addFileRow(\'' + element_name + '\', \'' + field_name + '\');">';
++ } else {
++ var input = document.createElement('INPUT');
++ input.setAttribute('type', 'file');
++ input.name = field_name;
++ input.className = 'shortcut';
++ input.size = 40;
++ input.onchange = new Function('addFileRow(\'' + element_name + '\', \'' + field_name + '\');');
++ input.id = field_name + '_' + (rows+1);
++ cell.appendChild(input);
++ }
++}
++
++function inArray(value, stack)
++{
++ for (var i = 0; i < stack.length; i++) {
++ if (stack[i] == value) {
++ return true;
+ }
++ }
++ return false;
++}
+
-+ $stmt .= "
-+ WHERE
-+ iss_id=$issue_id";
++function getEmailFromAddress(str)
++{
++ var first_pos = str.lastIndexOf('<');
++ var second_pos = str.lastIndexOf('>');
++ if ((first_pos != -1) && (second_pos != -1)) {
++ return str.substring(first_pos+1, second_pos);
++ } else {
++ return str;
++ }
++}
+
-+ $res = DB_Helper::getInstance()->query($stmt);
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return -1;
-+ } else {
-+ $new = array(
-+ 'category' => $current['iss_prc_id'],
-+ 'release' => $current['iss_pre_id'],
-+ 'expected_resolution_date' => $current['iss_expected_resolution_date'],
-+ 'release' => $current['iss_pre_id'],
-+ 'priority' => $current['iss_pri_id'],
-+ 'status' => $current['iss_sta_id'],
-+ 'resolution' => $current['iss_res_id'],
-+ 'summary' => $current['iss_summary'],
-+ 'description' => $current['iss_description'],
-+ 'estimated_dev_time' => $current['iss_dev_time'],
-+ 'percent_complete' => $current['iss_percent_complete'],
-+ 'trigger_reminders' => $current['iss_trigger_reminders'],
-+ 'group' => $current['iss_grp_id'],
-+ 'iss_private' => $current['private']
-+ );
-+ $new[$field_name] = $filed_value;
++function closeAndRefresh()
++{
++ opener.location.href = opener.location;
++ window.close();
++}
+
-+ // add change to the history (only for changes on specific fields?)
-+ $updated_fields = array();
-+ if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
-+ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
-+ }
-+ if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
-+ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
-+ }
-+ if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
-+ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
-+ }
-+ if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
-+ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
-+ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
-+ }
-+ if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
-+ // clear out the last-triggered-reminder flag when changing the status of an issue
-+ Reminder_Action::clearLastTriggered($issue_id);
++function str_replace(s, srch, rplc)
++{
++ var tmp = s;
++ var tmp_before = new String();
++ var tmp_after = new String();
++ var tmp_output = new String();
++ var int_before = 0;
++ var int_after = 0;
+
-+ // if old status was closed and new status is not, clear closed data from issue.
-+ $old_status_details = Status::getDetails($current['iss_sta_id']);
-+ if ($old_status_details['sta_is_closed'] == 1) {
-+ $new_status_details = Status::getDetails($filed_value);
-+ if ($new_status_details['sta_is_closed'] != 1) {
-+ self::clearClosed($issue_id);
-+ }
-+ }
-+ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
-+ }
-+ if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
-+ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
-+ }
-+ if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
-+ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
-+ }
-+ if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
-+ $updated_fields["Summary"] = '';
-+ }
-+ if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
-+ $updated_fields["Description"] = '';
-+ }
-+ if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
-+ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
-+ }
-+ if (count($updated_fields) > 0) {
-+ // log the changes
-+ $changes = '';
-+ $i = 0;
-+ foreach ($updated_fields as $key => $value) {
-+ if ($i > 0) {
-+ $changes .= "; ";
-+ }
-+ if (($key != "Summary") && ($key != "Description")) {
-+ $changes .= "$key: $value";
-+ } else {
-+ $changes .= "$key";
-+ }
-+ $i++;
-+ }
++ while (tmp.toUpperCase().indexOf(srch.toUpperCase()) > -1) {
++ int_before = tmp.toUpperCase().indexOf(srch.toUpperCase());
++ tmp_before = tmp.substring(0, int_before);
++ tmp_output = tmp_output + tmp_before;
++ tmp_output = tmp_output + rplc;
++ int_after = tmp.length - srch.length + 1;
++ tmp = tmp.substring(int_before + srch.length);
++ }
+
-+ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
-+ // send notifications for the issue being updated
-+ Notification::notifyIssueUpdated($issue_id, $current, $new);
-+ }
++ return tmp_output + tmp;
++}
++
++function displayFixedWidth(element_name)
++{
++ var el = getPageElement(element_name);
++ // only do this for mozilla
++ if (is_nav6up) {
++ var content = el.innerHTML;
++ el.innerHTML = '<pre>' + str_replace(content, "<br>", '') + '</pre>';
++ el.className = '';
++ }
++ el.style.fontFamily = 'Mono, Monaco, Courier New, Courier';
++ el.style.whiteSpace = 'pre';
++}
++
++function showSelections(form_name, field_name)
++{
++ var f = getForm(form_name);
++ var field = getFormElement(f, field_name);
++ var selections = getSelectedItems(field);
++ var selected_names = new Array();
++ for (var i = 0; i < selections.length; i++) {
++ selected_names.push(selections[i].text);
++ }
++ var display_div = getPageElement('selection_' + field_name);
++ display_div.innerHTML = 'Current Selections: ' + selected_names.join(', ');
++}
++
++function replaceWords(str, original, replacement)
++{
++ var lines = str.split("\n");
++ for (var i = 0; i < lines.length; i++) {
++ lines[i] = replaceWordsOnLine(lines[i], original, replacement);
++ }
++ return lines.join("\n");
++}
++
++function replaceWordsOnLine(str, original, replacement)
++{
++ var words = str.split(' ');
++ for (var i = 0; i < words.length; i++) {
++ words[i] = words[i].replace(/^\s*/, '').replace(/\s*$/, '');
++ if (words[i] == original) {
++ words[i] = replacement;
+ }
-+ return 1;
+ }
++ return words.join(' ');
++}
+
-
- /**
- * Move the issue to a new project
-@@ -1820,16 +1991,33 @@
- {
- $issue_id = Misc::escapeInteger($issue_id);
- $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
-+ $order = 1;
-+ // move all orders down to free "order space" for this new association
-+ $stmt = "UPDATE
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
-+ SET
-+ isu_order = isu_order + 1
-+ WHERE
-+ isu_usr_id = $assignee_usr_id AND
-+ isu_order >= $order";
-+ $res = DB_Helper::getInstance()->query($stmt);
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return -1;
++function checkSpelling(form_name, field_name)
++{
++ var features = 'width=420,height=400,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
++ var popupWin = window.open('spell_check.php?form_name=' + form_name + '&field_name=' + field_name, '_spellChecking', features);
++ popupWin.focus();
++}
++
++function updateTimeFields(form_name, year_field, month_field, day_field, hour_field, minute_field, date)
++{
++ var f = getForm(form_name);
++ if (typeof date == 'undefined') {
++ date = new Date();
++ }
++ selectOption(f, month_field, padDateValue(date.getMonth()+1));
++ selectOption(f, day_field, padDateValue(date.getDate()));
++ selectOption(f, year_field, date.getFullYear());
++ selectOption(f, hour_field, padDateValue(date.getHours()));
++ // minutes need special case due the 5 minute granularity
++ var minutes = Math.floor(date.getMinutes() / 5) * 5;
++ selectOption(f, minute_field, padDateValue(minutes));
++}
++
++function padDateValue(str)
++{
++ if (str.length == 1) {
++ str = '0' + str;
++ }
++ return str;
++}
++
++function resizeTextarea(page_name, form_name, field_name, change)
++{
++ var f = getForm(form_name);
++ var field = getFormElement(f, field_name);
++ field.cols = field.cols + change;
++ var cookie_name = 'textarea_' + page_name + '_' + field_name;
++ setCookie(cookie_name, field.cols, expires);
++}
++
++function removeOptionByValue(f, field_name, value)
++{
++ var field = getFormElement(f, field_name);
++ for (var i = 0; i < field.options.length; i++) {
++ if (field.options[i].value == value) {
++ field.options[i] = null;
+ }
-+ // insert the new association
- $stmt = "INSERT INTO
- " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
- (
- isu_iss_id,
- isu_usr_id,
-- isu_assigned_date
-+ isu_assigned_date,
-+ isu_order
- ) VALUES (
- $issue_id,
- $assignee_usr_id,
-- '" . Date_Helper::getCurrentDateGMT() . "'
-+ '" . Date_Helper::getCurrentDateGMT() . "',
-+ $order
- )";
- $res = DB_Helper::getInstance()->query($stmt);
- if (PEAR::isError($res)) {
-@@ -1844,6 +2032,78 @@
- }
- }
-
-+ /**
-+ * Method used to get the order list to be rearranged
-+ *
-+ * @access private
-+ * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
-+ * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
-+ * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
-+ */
-+ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
-+ {
-+ // find all affected associantion orders
-+ $stmt = "SELECT isu_usr_id, isu_order FROM
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
-+ WHERE
-+ isu_iss_id IN ($issue_id)";
-+ if ($usr_id !== FALSE) {
-+ $stmt.= " AND isu_usr_id IN ($usr_id)";
++ }
++}
++
++function getTotalCheckboxes(f, field_name)
++{
++ var total = 0;
++ for (var i = 0; i < f.elements.length; i++) {
++ if (f.elements[i].name == field_name) {
++ total++;
+ }
-+ $stmt.= "ORDER BY isu_order";
-+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return -1;
-+ } else {
-+ $deleted_orders = array();
-+ foreach ($res as $row) {
-+ if (empty($deleted_orders[$row['isu_usr_id']])) {
-+ $deleted_orders[$row['isu_usr_id']] = array();
++ }
++ return total;
++}
++
++function getTotalCheckboxesChecked(f, field_name)
++{
++ var total = 0;
++ for (var i = 0; i < f.elements.length; i++) {
++ if ((f.elements[i].name == field_name) && (f.elements[i].checked)) {
++ total++;
++ }
++ }
++ return total;
++}
++
++function hideComboBoxes(except_field)
++{
++ for (var i = 0; i < document.forms.length; i++) {
++ for (var y = 0; y < document.forms[i].elements.length; y++) {
++ if (((document.forms[i].elements[y].type == 'select-one') ||
++ (document.forms[i].elements[y].type == 'select-multiple')) &&
++ (document.forms[i].elements[y].name != except_field) &&
++ (document.forms[i].elements[y].name != 'lookup') &&
++ (document.forms[i].elements[y].name != 'lookup[]')) {
++ document.forms[i].elements[y].style.visibility = 'hidden';
++ }
++ }
++ }
++}
++
++function showComboBoxes()
++{
++ for (var i = 0; i < document.forms.length; i++) {
++ for (var y = 0; y < document.forms[i].elements.length; y++) {
++ if (((document.forms[i].elements[y].type == 'select-one') ||
++ (document.forms[i].elements[y].type == 'select-multiple')) &&
++ (document.forms[i].elements[y].name != 'lookup') &&
++ (document.forms[i].elements[y].name != 'lookup[]')) {
++ document.forms[i].elements[y].style.visibility = 'visible';
++ }
++ }
++ }
++}
++
++function getOverlibContents(options, target_form, target_field, is_multiple)
++{
++ hideComboBoxes(target_field);
++ var html = '<form name="overlib_form" onSubmit="javascript:return lookupOption(this, \'' + target_form + '\', \'' + target_field + '\');">' + options + '<br /><input name="search" class="lookup_field_overlib" type="text" size="24" value="paste or start typing here" onBlur="javascript:this.value=\'paste or start typing here\';" onFocus="javascript:this.value=\'\';" onKeyUp="javascript:lookupField(this.form, this, \'lookup';
++ if ((is_multiple != null) && (is_multiple == true)) {
++ html += '[]';
++ }
++ html += '\');"><input class="button_overlib" type="submit" value="Lookup"><br />'
++ + '<input type="text" name="id_number" size="24" class="lookup_field_overlib" value="id #" onFocus="javascript:this.value=\'\';">'
++ + '<input type="button" class="button_overlib" value="Add By ID" onClick="lookupByID(document.forms[\'overlib_form\'].id_number, \'' + target_form + '\', \'' + target_field + '\')"></form>';
++ return html;
++}
++
++function getFillInput(options, target_form, target_field)
++{
++ hideComboBoxes(target_field);
++ return '<form onSubmit="javascript:return fillInput(this, \'' + target_form + '\', \'' + target_field + '\');">' + options + '<input class="button_overlib" type="submit" value="Lookup"><br><input name="search" class="lookup_field_overlib" type="text" size="24" value="paste or start typing here" onBlur="javascript:this.value=\'paste or start typing here\';" onFocus="javascript:this.value=\'\';" onKeyUp="javascript:lookupField(this.form, this, \'lookup\');"></form>';
++}
++
++function lookupOption(f, target_form, target_field)
++{
++ var w = document;
++ for (var i = 0; i < w.forms.length; i++) {
++ if (w.forms[i].name == target_form) {
++ var test = getFormElement(f, 'lookup');
++ if (!test) {
++ var field = getFormElement(f, 'lookup[]');
++ var target = getFormElement(getForm(target_form), target_field);
++ clearSelectedOptions(target);
++ selectOptions(w.forms[i], target_field, getSelectedItems(field));
++ } else {
++ options = getSelectedOption(f, 'lookup');
++ if (options == -1) {
++ return false;
+ }
-+ $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
++ selectOption(w.forms[i], target_field, options);
+ }
-+ return $deleted_orders;
++ nd();
++ showComboBoxes();
++ break;
+ }
+ }
++ return false;
++}
+
-+ /**
-+ *
-+ * Method used to rearrange order list in the db according to known deleted records
-+ *
-+ * @access private
-+ * @param mixed deleteorder list
-+ * @return void
-+ */
-+ function rearrangeDeleteUserAssociationOrderList($delete_order_list)
-+ {
-+ if (empty($delete_order_list) || (!is_array($delete_order_list))) {
-+ return -1;
++function lookupByID(field, target_form, target_field)
++{
++ if (!isNumberOnly(field.value)) {
++ alert('Please enter numbers only');
++ } else {
++ // try to find value in targer field.
++ target_obj = document.forms[target_form].elements[target_field];
++ found = false;
++ for (i = 0;i<target_obj.options.length; i++) {
++ if (target_obj.options[i].value == field.value) {
++ found = true;
++ target_obj.options[i].selected = true;
++ }
+ }
-+ foreach ($delete_order_list as $isu_usr_id => $orders) {
-+ for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
-+ // move the orders after them up to take the "order space" of the deleted records
-+ $stmt = "UPDATE
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
-+ SET
-+ isu_order = isu_order - " . ($i+1) . "
-+ WHERE
-+ isu_usr_id = $isu_usr_id AND
-+ isu_order > " . $orders[$i];
-+ if ($i < count($orders) - 1) {
-+ $stmt.= " AND
-+ isu_order < " . $orders[$i+1];
++ if (found == false) {
++ alert('ID #' + field.value + ' was not found');
++ } else {
++ field.value = '';
++ // check if we should call "showSelection"
++ if (document.getElementById('selection_' + target_field) != null) {
++ showSelections(target_form, target_field)
++ }
++ }
++ }
++ return false;
++}
++
++function fillInput(f, target_form, target_field)
++{
++ var exists = getFormElement(f, 'lookup');
++ var target_f = getForm(target_form);
++ if (!exists) {
++ var field = getFormElement(f, 'lookup[]');
++ var target_field = getFormElement(target_f, target_field);
++ target_field.value = '';
++ var values = getValues(getSelectedItems(field));
++ target_field.value = values.join('; ');
++ errorDetails(target_f, target_field, false);
++ } else {
++ var field_value = getSelectedOption(f, 'lookup');
++ var field = getFormElement(target_f, target_field);
++ field.value = field_value;
++ errorDetails(target_f, target_field, false);
++ }
++ nd();
++ showComboBoxes();
++ return false;
++}
++
++function lookupField(f, search_field, field_name, callbacks)
++{
++ var search = search_field.value;
++ if (isWhitespace(search)) {
++ return false;
++ }
++ var target_field = getFormElement(f, field_name);
++ if (!target_field) {
++ target_field = getFormElement(f, field_name + '[]');
++ }
++ for (var i = 0; i < target_field.options.length; i++) {
++ var value = target_field.options[i].text.toUpperCase();
++ if (target_field.type == 'select-multiple') {
++ // if we are targetting a multiple select box, then unselect everything
++ // before selecting the matched option
++ if (startsWith(value, search.toUpperCase())) {
++ clearSelectedOptions(target_field);
++ target_field.options[i].selected = true;
++ // handle calling any callbacks
++ if (callbacks != null) {
++ for (var y = 0; y < callbacks.length; y++) {
++ eval(callbacks[y] + ';');
++ }
+ }
-+ $res = DB_Helper::getInstance()->query($stmt);
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return -1;
++ return true;
++ }
++ } else {
++ // normal drop-down boxes will search across the option value, and
++ // not just the beginning of it (e.g. '*hello*' instead of 'hello*')
++ if (value.indexOf(search.toUpperCase()) != -1) {
++ target_field.options[i].selected = true;
++ // handle calling any callbacks
++ if (callbacks != null) {
++ for (var y = 0; y < callbacks.length; y++) {
++ eval(callbacks[y] + ';');
++ }
+ }
++ return true;
+ }
+ }
-+ return 1;
+ }
++ target_field.selectedIndex = 0;
++}
+
-
- /**
- * Method used to delete all user assignments for a specific issue.
-@@ -1859,6 +2119,7 @@
- if (is_array($issue_id)) {
- $issue_id = implode(", ", $issue_id);
- }
-+ $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
- $stmt = "DELETE FROM
- " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
- WHERE
-@@ -1871,6 +2132,7 @@
- if ($usr_id) {
- History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
- }
-+ self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
- return 1;
- }
- }
-@@ -1889,6 +2151,7 @@
- {
- $issue_id = Misc::escapeInteger($issue_id);
- $usr_id = Misc::escapeInteger($usr_id);
-+ $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
- $stmt = "DELETE FROM
- " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
- WHERE
-@@ -1903,6 +2166,7 @@
- History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
- User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
- }
-+ self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
- return 1;
- }
- }
-@@ -2379,6 +2643,11 @@
- {
- $sort_by = self::getParam('sort_by');
- $sort_order = self::getParam('sort_order');
-+ $users = self::getParam('users');
-+ if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
-+ unset($sort_by);
-+ unset($sort_order);
++function clearSelectedOptions(field)
++{
++ for (var i = 0; i < field.options.length; i++) {
++ field.options[i].selected = false;
++ }
++}
++
++function selectAllOptions(f, field_name)
++{
++ var field = getFormElement(f, field_name);
++ for (var y = 0; y < field.options.length; y++) {
++ field.options[y].selected = true;
++ }
++}
++
++function selectOptions(f, field_name, values)
++{
++ var field = getFormElement(f, field_name);
++ for (var i = 0; i < values.length; i++) {
++ for (var y = 0; y < field.options.length; y++) {
++ if (field.options[y].value == values[i].value) {
++ field.options[y].selected = true;
++ }
+ }
- $rows = self::getParam('rows');
- $hide_closed = self::getParam('hide_closed');
- if ($hide_closed === '') {
-@@ -2720,6 +2720,7 @@
- "iss_expected_resolution_date" => "desc",
- "pre_title" => "asc",
- "assigned" => "asc",
-+ "isu_order" => "desc",
- );
-
- foreach ($custom_fields as $fld_id => $fld_name) {
-@@ -3275,6 +3545,8 @@
- $ids = implode(", ", $ids);
- $stmt = "SELECT
- isu_iss_id,
-+ isu_order,
-+ isu_usr_id,
- usr_full_name
- FROM
- " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
-@@ -3286,6 +3558,7 @@
- if (PEAR::isError($res)) {
- Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
- } else {
-+ // gather names of the users assigned to each issue
- $t = array();
- for ($i = 0; $i < count($res); $i++) {
- if (!empty($t[$res[$i]['isu_iss_id']])) {
-@@ -3294,9 +3567,18 @@
- $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
- }
- }
-+ // gather orders
-+ $o = array();
-+ for ($i = 0; $i < count($res); $i++) {
-+ if (empty($o[$res[$i]['isu_iss_id']])) {
-+ $o[$res[$i]['isu_iss_id']] = array();
-+ }
-+ $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
-+ }
- // now populate the $result variable again
- for ($i = 0; $i < count($result); $i++) {
- @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
-+ @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
- }
- }
- }
-@@ -4264,6 +4546,7 @@
- Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
- return -1;
- }
-+ self::moveOrderForAllUsers($issue_id, 1);
- }
-
-
-@@ -4322,5 +4605,121 @@
- }
- return $returns[$msg_id];
- }
++ }
++}
+
-+ /**
-+ * Reorders user's issues as requested by user
-+ * @access public
-+ * @param $usr_id User to be reordered
-+ * @param $issue_id Issue or array of issues to be moved
-+ * @param $neworder The new order of the issues
-+ * @return void
-+ */
-+ function reorderUserIssues($usr_id, $issue_id, $neworder)
-+ {
-+ if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
-+ return false;
++function selectOption(f, field_name, value)
++{
++ field = getFormElement(f, field_name);
++ for (var i = 0; i < field.options.length; i++) {
++ if (field.options[i].value == value) {
++ field.options[i].selected = true;
++ return true;
+ }
-+ if (!is_numeric($usr_id) || !is_numeric($neworder)) {
-+ return false;
++ }
++}
++
++function setHiddenFieldValue(f, field_name, value)
++{
++ var field = getFormElement(f, field_name);
++ field.value = value;
++}
++
++function getForm(form_name)
++{
++ for (var i = 0; i < document.forms.length; i++) {
++ if (document.forms[i].name == form_name) {
++ return document.forms[i];
+ }
-+ $usr_id = Misc::escapeInteger($usr_id);
-+ $issue_id = Misc::escapeInteger($issue_id);
-+ $neworder = Misc::escapeInteger($neworder);
-+ if (is_array($issue_id)) {
-+ $issue_count = count($issue_id);
-+ $issue_id_str = implode(", ", $issue_id);
++ }
++}
++
++function getPageElement(id)
++{
++ if (document.getElementById) {
++ return document.getElementById(id);
++ } else if (document.all) {
++ return document.all[id];
++ }
++}
++
++function getOpenerPageElement(name)
++{
++ if (window.opener.document.getElementById) {
++ return window.opener.document.getElementById(name);
++ } else if (window.opener.document.all) {
++ return window.opener.document.all[name];
++ }
++}
++
++function getFormElement(f, field_name, num)
++{
++ var elements = document.getElementsByName(field_name);
++ var y = 0;
++ for (var i = 0; i < elements.length; i++) {
++ if (f != elements[i].form) {
++ continue;
++ }
++ if (num != null) {
++ if (y == num) {
++ return elements[i];
++ }
++ y++;
+ } else {
-+ $issue_count = 1;
-+ $issue_id_str = $issue_id;
-+ $issue_id = array($issue_id);
++ return elements[i];
+ }
-+ // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
-+ $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
-+ self::rearrangeDeleteUserAssociationOrderList($orderlist);
-+ // move down the orders to free the "order space" needed
-+ $stmt = "UPDATE
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
-+ SET
-+ isu_order = isu_order + $issue_count
-+ WHERE
-+ isu_usr_id = $usr_id AND
-+ isu_order >= $neworder";
-+ $res = DB_Helper::getInstance()->query($stmt);
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return -1;
++ }
++ return false;
++}
++
++function getSelectedItems(field)
++{
++ var selected = new Array();
++ for (var i = 0; i < field.options.length; i++) {
++ if (field.options[i].selected) {
++ selected[selected.length] = field.options[i];
+ }
-+ //update the order for the issues being moved
-+ $i = 0;
-+ foreach ($issue_id as $iss_id) {
-+ $stmt = "UPDATE
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
-+ SET
-+ isu_order = " . ($neworder + $i) . "
-+ WHERE
-+ isu_usr_id = $usr_id AND
-+ isu_iss_id = $iss_id";
-+ $res = DB_Helper::getInstance()->query($stmt);
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return -1;
-+ }
-+ $i++;
++ }
++ return selected;
++}
++
++function getSelectedOptionValues(f, field_name)
++{
++ var field = getFormElement(f, field_name);
++ var selected = new Array();
++ for (var i = 0; i < field.options.length; i++) {
++ if (field.options[i].selected) {
++ selected[selected.length] = field.options[i].value;
+ }
+ }
++ return selected;
++}
+
++function removeAllOptions(f, field_name)
++{
++ var field = getFormElement(f, field_name);
++ if (field.options.length > 0) {
++ field.options[0] = null;
++ removeAllOptions(f, field_name);
++ }
++}
+
-+ /**
-+ * Get users issue order list
-+ * @access public
-+ * @param $user_id User
-+ * @param $order_list Order of the issues
-+ * @return void
-+ */
-+ function getIssueOrderByUser($usr_id) {
++function getValues(list)
++{
++ var values = new Array();
++ for (var i = 0; i < list.length; i++) {
++ values[values.length] = list[i].value;
++ }
++ return values;
++}
+
-+ if (!is_numeric($usr_id)) {
-+ return false;
++function optionExists(field, option)
++{
++ for (var i = 0; i < field.options.length; i++) {
++ if (field.options[i].text == option.text) {
++ return true;
+ }
++ }
++ return false;
++}
+
-+ $stmt = "SELECT
-+ isu_iss_id, isu_order
-+ FROM
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
-+ WHERE
-+ isu_usr_id = " . $usr_id ;
++function addOptions(f, field_name, options)
++{
++ var field = getFormElement(f, field_name);
++ for (var i = 0; i < options.length; i++) {
++ if (!optionExists(field, options[i])) {
++ field.options.length = field.options.length + 1;
++ field.options[field.options.length-1].text = options[i].text;
++ field.options[field.options.length-1].value = options[i].value;
++ }
++ }
++}
+
-+ $order_list = array();
++function replaceParam(str, param, new_value)
++{
++ if (str.indexOf("?") == -1) {
++ return param + "=" + new_value;
++ } else {
++ var pieces = str.split("?");
++ var params = pieces[1].split("&");
++ var new_params = new Array();
++ for (var i = 0; i < params.length; i++) {
++ if (params[i].indexOf(param + "=") == 0) {
++ params[i] = param + "=" + new_value;
++ }
++ new_params[i] = params[i];
++ }
++ // check if the parameter doesn't exist on the URL
++ if ((str.indexOf("?" + param + "=") == -1) && (str.indexOf("&" + param + "=") == -1)) {
++ new_params[new_params.length] = param + "=" + new_value;
++ }
++ return new_params.join("&");
++ }
++}
+
-+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++function checkRadio(form_name, field_name, num)
++{
++ var f = getForm(form_name);
++ var field = getFormElement(f, field_name, num);
++ if (!field.disabled) {
++ field.checked = true;
++ }
++}
+
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return array();
-+ } else {
-+ foreach ($res as $row) {
-+ $order_list[$row["isu_iss_id"]] = $row["isu_order"];
++function toggleCheckbox(form_name, field_name, num)
++{
++ var f = getForm(form_name);
++ var checkbox = getFormElement(f, field_name, num);
++ if (checkbox.disabled) {
++ return false;
++ }
++ if (checkbox.checked) {
++ checkbox.checked = false;
++ } else {
++ checkbox.checked = true;
++ }
++}
++
++var toggle = 'off';
++function toggleSelectAll(f, field_name)
++{
++ for (var i = 0; i < f.elements.length; i++) {
++ if (f.elements[i].disabled) {
++ continue;
++ }
++ if (f.elements[i].name == field_name) {
++ if (toggle == 'off') {
++ f.elements[i].checked = true;
++ } else {
++ f.elements[i].checked = false;
+ }
+ }
-+ return $order_list;
+ }
++ if (toggle == 'off') {
++ toggle = 'on';
++ } else {
++ toggle = 'off';
++ }
++}
+
-+ function moveOrderForAllUsers($issue_id, $neworder)
-+ {
-+ // Move the issue to the top priority for the ppl it's assigned to
-+ $stmt = "SELECT isu_usr_id FROM
-+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
-+ WHERE
-+ isu_iss_id = " . Misc::escapeInteger($issue_id);
-+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
-+ if (PEAR::isError($res)) {
-+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
-+ return -1;
-+ }
-+ foreach ($res as $row) {
-+ self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
++function getCookies()
++{
++ var t = new Array();
++ var pieces = new Array();
++ var cookies = new Object();
++ if (document.cookie) {
++ t = document.cookie.split(';');
++ for (var i = 0; i < t.length; i++) {
++ pieces = t[i].split('=');
++ eval('cookies.' + pieces[0].replace('[', '_').replace(']', '_') + ' = "' + pieces[1] + '";');
+ }
++ return cookies;
+ }
-+
- }
-
---- eventum-r3749/htdocs/js/global.js~ 2008-10-15 02:03:48.000000000 +0300
-+++ eventum-r3749/htdocs/js/global.js 2008-10-15 02:06:00.000000000 +0300
-@@ -799,4 +799,39 @@
- });
- });
- });
++}
+
-+$(document).ready(function() {
-+ // dialog type calender isn't working in Konqueror beacuse it's not a supported browser for either jQuery or jQuery UI
-+ // http://groups.google.com/group/jquery-ui/browse_thread/thread/ea61238c34cb5f33/046837b02fb90b5c
-+ if (navigator.appName != 'Konqueror') {
-+ $(".inline_date_pick").click(function() {
-+ var masterObj = this;
-+ var masterObjPos = $(masterObj).offset();
-+ // offset gives uses top and left but datepicker needs pageX and pageY
-+ var masterObjPos = {pageX: masterObjPos.left, pageY: masterObjPos.top};
-+ $(this).datepicker(
-+ // we use dialog type calender so we won't haveto have a hidden element on the page
-+ 'dialog',
-+ // selected date
-+ masterObj.innerHTML,
-+ // onclick handler
-+ function (date, dteObj) {
-+ fieldName = masterObj.id.substr(0,masterObj.id.indexOf('|'));
-+ issueID = masterObj.id.substr(masterObj.id.indexOf('|')+1);
-+ $.post("/ajax/update.php", {fieldName: fieldName, issueID: issueID, day: dteObj.selectedDay, month: (dteObj.selectedMonth+1), year: dteObj.selectedYear}, function(data) {
-+ if (data.length > 0) {
-+ masterObj.innerHTML = data;
-+ }
-+ }, "text");
-+ },
-+ // config
-+ {dateFormat: 'dd M yy', duration: ""},
-+ // position of the datepicker calender - taken from div's offset
-+ masterObjPos
-+ );
++function isElementVisible(element)
++{
++ if ((!element.style.display) || (element.style.display == getDisplayStyle())) {
++ return true;
++ } else {
+ return false;
-+ });
+ }
-+});
++}
+
- //-->
---- eventum/htdocs/list.php 2008-10-15 01:46:20.000000000 +0300
-+++ eventum-new/htdocs/list.php 2008-10-15 02:02:25.000000000 +0300
-@@ -67,6 +67,11 @@
- $profile['sort_by'] . "&sort_order=" . $profile['sort_order']);
- }
-
-+@$reorder_usr_id = $_REQUEST["reorder_user"];
-+@$reorder_issue_id = $_REQUEST["reorder_source"];
-+@$reorder_neworder = $_REQUEST["reorder_neworder"];
-+Issue::reorderUserIssues($reorder_usr_id, $reorder_issue_id, $reorder_neworder);
++function toggleVisibility(title, create_cookie, use_inline)
++{
++ var element = getPageElement(title + '1');
++ if (isElementVisible(element)) {
++ var new_style = 'none';
++ } else {
++ var new_style = getDisplayStyle(use_inline);
++ }
++ var i = 1;
++ while (1) {
++ element = getPageElement(title + i);
++ if (!element) {
++ break;
++ }
++ element.style.display = new_style;
++ i++;
++ }
++ // if any elements were found, then...
++ if (i > 1) {
++ var link_element = getPageElement(title + '_link');
++ if (link_element) {
++ if (new_style == 'none') {
++ link_element.innerHTML = 'show';
++ link_element.title = 'show details about this section';
++ } else {
++ link_element.innerHTML = 'hide';
++ link_element.title = 'hide details about this section';
++ }
++ }
++ }
++ if (((create_cookie == null) || (create_cookie == false)) && (create_cookie != undefined)) {
++ return false;
++ } else {
++ setCookie('visibility_' + title, new_style, expires);
++ }
++}
+
- $options = Issue::saveSearchParams();
- $tpl->assign("options", $options);
- $tpl->assign("sorting", Issue::getSortingInfo($options));
-@@ -90,6 +95,21 @@
- }
- $assign_options += $users;
-
-+// get the isu_order (assignated users) ordering user
-+if (!empty($options["users"])) {
-+ if ($options["users"] == -2) {
-+ $isu_order_user = $usr_id;
-+ } else
-+ if ($options["users"] > 0) {
-+ $isu_order_user = $options["users"];
++function changeVisibility(title, visibility, use_inline)
++{
++ var element = getPageElement(title);
++ if (visibility) {
++ var new_style = getDisplayStyle(use_inline);
+ } else {
-+ unset($isu_order_user);
++ var new_style = 'none';
+ }
-+} else {
-+ unset($isu_order_user);
++ element.style.display = new_style;
+}
-+$tpl->assign("isu_order_user", $isu_order_user);
+
- $list = Issue::getListing($prj_id, $options, $pagerRow, $rows);
- $tpl->assign("list", $list["list"]);
- $tpl->assign("list_info", $list["info"]);
---- eventum/templates/list.tpl.html 2008-10-15 01:46:20.000000000 +0300
-+++ eventum-new/templates/list.tpl.html 2008-10-15 02:02:25.000000000 +0300
-@@ -89,6 +89,28 @@
- f.target = '_popup';
- f.submit();
- }
-+function reorderBulk(order_user, neworder)
++function getDisplayStyle(use_inline)
+{
-+ url = page_url + "?";
-+ url += "reorder_user=" + order_user;
++ // kind of hackish, but it works perfectly with IE6 and Mozilla 1.1
++ if (is_ie5up) {
++ if (use_inline == true) {
++ return 'inline';
++ } else {
++ return 'block';
++ }
++ } else {
++ return '';
++ }
++}
+
-+ items = document.getElementsByName("item[]");
-+ checkedcount = 0;
-+ for (var i = 0; i < items.length; i++) {
-+ if (items[i].checked) {
-+ url += "&reorder_source[" + checkedcount + "]=" + items[i].value;
-+ checkedcount++;
-+ }
++function getCookie(name)
++{
++ var start = document.cookie.indexOf(name+"=");
++ var len = start+name.length+1;
++ if ((!start) && (name != document.cookie.substring(0,name.length))) return null;
++ if (start == -1) return null;
++ var end = document.cookie.indexOf(";",len);
++ if (end == -1) end = document.cookie.length;
++ return unescape(document.cookie.substring(len,end));
++}
++
++function setCookie(name, value, expires, path, domain, secure)
++{
++ document.cookie = name + "=" +escape(value) +
++ ( (expires) ? ";expires=" + expires.toGMTString() : "") +
++ ( (path) ? ";path=" + path : "") +
++ ( (domain) ? ";domain=" + domain : "") +
++ ( (secure) ? ";secure" : "");
++}
++
++function openHelp(rel_url, topic)
++{
++ var width = 500;
++ var height = 450;
++ var w_offset = 30;
++ var h_offset = 30;
++ var location = 'top=' + h_offset + ',left=' + w_offset + ',';
++ if (screen.width) {
++ location = 'top=' + h_offset + ',left=' + (screen.width - (width + w_offset)) + ',';
+ }
-+ if (checkedcount == 0) {
-+ alert('{/literal}{t escape=js}Please choose which issues to move to the new place.{/t}{literal}');
-+ return false;
++ var features = 'width=' + width + ',height=' + height + ',' + location + 'resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
++ var helpWin = window.open(rel_url + 'help.php?topic=' + topic, '_help', features);
++ helpWin.focus();
++}
++
++function selectOnlyValidOption(selectObj)
++{
++ if (selectObj.selectedIndex < 1) {
++ if (selectObj.length == 1) {
++ selectObj.selectedIndex = 0;
++ return;
++ }
++ if (selectObj.length <= 2 && selectObj.options[0].value == -1) {
++ selectObj.selectedIndex = 1;
++ return;
++ }
+ }
++}
+
-+ url += "&reorder_neworder=" + neworder;
-+
-+ window.location.href = url;
++// this method will confirm that you want the window to close
++var checkClose = false;
++var closeConfirmMessage = 'Do you want to close this window?';
++function handleClose()
++{
++ if (checkClose == true) {
++ return closeConfirmMessage;
++ } else {
++ return;
++ }
+}
- function hideClosed(f)
- {
- if (f.hide_closed.checked) {
-@@ -150,6 +172,13 @@
- f.go.disabled = true;
- }
- }
-+function updateCustomFields(issue_id)
++
++function checkWindowClose(msg)
+{
-+ var features = 'width=560,height=460,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
-+ var customWin = window.open('custom_fields.php?issue_id=' + issue_id, '_custom_fields', features);
-+ customWin.focus();
-+ return false;
++ if (msg == false) {
++ checkClose = false;
++ } else {
++ checkClose = true;
++ closeConfirmMessage = msg;
++ }
+}
- //-->
- </script>
- {/literal}
-@@ -166,11 +195,11 @@
- <input type="hidden" name="cat" value="bulk_update">
- <tr>
- <td>
-- <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0">
-- <tr>
-+ <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0" id="issue_list_table">
-+ <tr class="nodrag">
- <td colspan="{$col_count}" class="default">
- <table width="100%" cellspacing="0" cellpadding="0" border="0">
-- <tr>
-+ <tr class="nodrag">
- <td class="default">
- <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
- {include file="help_link.tpl.html" topic="list"}
-@@ -190,7 +219,7 @@
- </table>
- </td>
- </tr>
-- <tr bgcolor="{$cell_color}">
-+ <tr bgcolor="{$cell_color}" class="nodrag">
- {if $current_role > $roles.developer}
- <td width="1%">
- <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
-@@ -205,7 +234,7 @@
- {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
- </td>
- {/foreach}
-- {else}
-+ {elseif $field_name != 'isu_order' || $isu_order_user}
- <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
- {if $field_name == 'iss_summary'}
- <table cellspacing="0" cellpadding="1" width="100%">
-@@ -221,6 +250,9 @@
- </table>
- {elseif $sorting.links[$field_name] != ''}
- <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
-+ {if $field_name == 'isu_order'}
-+ <br>{$users[$isu_order_user]}
-+ {/if}
- {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
- {else}
- {$column.title}
-@@ -229,19 +261,20 @@
- {/if}
- {/foreach}
- </tr>
-+ <tbody>
- {section name="i" loop=$list}
-- <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}>
-+ <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if} id="{$list[i].iss_id}" {if !$list[i].assigned_users_order[$current_user_id]}class="nodrag"{/if}>
- {if $current_role > $roles.developer}
- <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
- {/if}
- {foreach from=$columns item=column key=field_name}
- {if $field_name == 'custom_fields'}
- {foreach from=$list[i].custom_field key=fld_id item=fld_value}
-- <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
-- {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
-+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default custom_field" onclick="return updateCustomFields({$list[i].iss_id});">
-+ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
- </td>
- {/foreach}
-- {else}
-+ {elseif $field_name != 'isu_order' || $isu_order_user}
- <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
- {if $field_name == 'iss_id'}
- <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
-@@ -276,7 +309,7 @@
- {elseif $field_name == 'iss_percent_complete'}
- {$list[i].iss_percent_complete|escape:"html"}%
- {elseif $field_name == 'iss_expected_resolution_date'}
-- {$list[i].iss_expected_resolution_date|escape:"html"}
-+ <div class="inline_date_pick" id="expected_resolution_date|{$list[i].iss_id}">{$list[i].iss_expected_resolution_date|escape:"html"} </div>
- {elseif $field_name == 'iss_summary'}
- <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
- {if $list[i].redeemed}
-@@ -285,6 +318,10 @@
- {if $list[i].iss_private == 1}
- <b>[Private]</b>
- {/if}
-+ {elseif $field_name == 'isu_order'}
-+ {if $list[i].assigned_users_order[$current_user_id]}
-+ <img src="{$rel_url}images/updown.gif" alt="move">
-+ {/if}
- {/if}
- </td>
- {/if}
-@@ -297,10 +334,11 @@
- </td>
- </tr>
- {/section}
-- <tr bgcolor="{$cell_color}">
-+ </tbody>
-+ <tr bgcolor="{$cell_color}" class="nodrag">
- <td colspan="{$col_count}">
- <table width="100%" cellspacing="0" cellpadding="0">
-- <tr>
-+ <tr class="nodrag">
- <td width="30%" nowrap>
- {if $current_role > $roles.developer}
- <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
-@@ -352,6 +390,29 @@
- </form>
- </table>
- <br />
--
-+<script type="text/javascript">
-+{*
-+ * Order issues by drag and drop:
-+ * only if sorted by order and viewing your own issues
-+ *}
-+{if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}
-+{literal}
-+var before = ''; // make it global variable
-+$('#issue_list_table').tableDnD({
-+ onDragClass: "tDnD_whileDrag",
-+ onDragStart: function(table, row) {
-+ before = $.tableDnD.serialize('id');
-+ },
-+ onDrop: function(table, row) {
-+ $.post("/ajax/order.php", {before: before, after: $.tableDnD.serialize('id')}, function(data) {
-+ if (data.length > 0) {
-+ alert(data);
-+ }
-+ }, "text");
++
++// Replace special characters MS uses for quotes with normal versions
++function replaceSpecialCharacters(e)
++{
++ var s = new String(e.value);
++ var newString = '';
++ var thisChar;
++ var charCode;
++ for (i = 0; i < s.length; i++) {
++ thisChar = s.charAt(i);
++ charCode = s.charCodeAt(i);
++ if ((charCode == 8220) || (charCode == 8221)) {
++ thisChar = '"';
++ } else if (charCode == 8217) {
++ thisChar = "'";
++ } else if (charCode == 8230) {
++ thisChar = "...";
++ } else if (charCode == 8226) {
++ thisChar = "*";
++ } else if (charCode == 8211) {
++ thisChar = "-";
++ }
++ newString = newString + thisChar;
+ }
++ e.value = newString;
++}
++
++
++function getEventTarget(e)
++{
++ var targ;
++ if (!e) var e = window.event;
++ if (e.target) targ = e.target;
++ else if (e.srcElement) targ = e.srcElement;
++ if (targ.nodeType == 3) // defeat Safari bug
++ targ = targ.parentNode;
++
++ return targ;
++}
++
++// call when document ready
++$(document).ready(function() {
++ $('.date_picker').datepicker({
++ dateFormat: 'yy-mm-dd',
++ firstDay: user_prefs.week_firstday
++ });
+});
-+{/literal}
-+{/if}
-+</script>
- {include file="app_info.tpl.html"}
--{include file="footer.tpl.html"}
-+{include file="footer.tpl.html"}
-\ No newline at end of file
---- ./htdocs/js/jquery/jquery.tablednd.js (revision 0)
-+++ ./htdocs/js/jquery/jquery.tablednd.js (revision 0)
++//-->
+--- eventum-2.2/htdocs/js/jquery/jquery.tablednd.js 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/htdocs/js/jquery/jquery.tablednd.js 2009-10-12 22:10:36.435851675 +0300
@@ -0,0 +1,382 @@
+/**
+ * TableDnD plug-in for JQuery, allows you to drag and drop table rows
+ return result;
+ },
+
-+ serializeTables: function() {
-+ var result = "";
-+ this.each(function() {
-+ // this is now bound to each matching table
-+ result += jQuery.tableDnD.serializeTable(this);
-+ });
-+ return result;
++ serializeTables: function() {
++ var result = "";
++ this.each(function() {
++ // this is now bound to each matching table
++ result += jQuery.tableDnD.serializeTable(this);
++ });
++ return result;
++ }
++
++}
++
++jQuery.fn.extend(
++ {
++ tableDnD : jQuery.tableDnD.build,
++ tableDnDUpdate : jQuery.tableDnD.updateTables,
++ tableDnDSerialize: jQuery.tableDnD.serializeTables
++ }
++);
+\ No newline at end of file
+--- eventum-2.2/htdocs/list.php 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/htdocs/list.php 2009-10-12 22:10:36.435851675 +0300
+@@ -53,6 +53,11 @@
+ $profile['sort_by'] . "&sort_order=" . $profile['sort_order']);
+ }
+
++@$reorder_usr_id = $_REQUEST["reorder_user"];
++@$reorder_issue_id = $_REQUEST["reorder_source"];
++@$reorder_neworder = $_REQUEST["reorder_neworder"];
++Issue::reorderUserIssues($reorder_usr_id, $reorder_issue_id, $reorder_neworder);
++
+ $options = Issue::saveSearchParams();
+ $tpl->assign("options", $options);
+ $tpl->assign("sorting", Issue::getSortingInfo($options));
+@@ -78,6 +83,21 @@
+ }
+ $assign_options += $users;
+
++// get the isu_order (assignated users) ordering user
++if (!empty($options["users"])) {
++ if ($options["users"] == -2) {
++ $isu_order_user = $usr_id;
++ } else
++ if ($options["users"] > 0) {
++ $isu_order_user = $options["users"];
++ } else {
++ unset($isu_order_user);
++ }
++} else {
++ unset($isu_order_user);
++}
++$tpl->assign("isu_order_user", $isu_order_user);
++
+ $list = Issue::getListing($prj_id, $options, $pagerRow, $rows);
+ $tpl->assign("list", $list["list"]);
+ $tpl->assign("list_info", $list["info"]);
+--- eventum-2.2/htdocs/list.php.~1~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/htdocs/list.php.~1~ 2009-09-14 18:07:55.000000000 +0300
+@@ -0,0 +1,116 @@
++<?php
++/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
++// +----------------------------------------------------------------------+
++// | Eventum - Issue Tracking System |
++// +----------------------------------------------------------------------+
++// | Copyright (c) 2003 - 2008 MySQL AB |
++// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
++// | |
++// | This program is free software; you can redistribute it and/or modify |
++// | it under the terms of the GNU General Public License as published by |
++// | the Free Software Foundation; either version 2 of the License, or |
++// | (at your option) any later version. |
++// | |
++// | This program is distributed in the hope that it will be useful, |
++// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
++// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
++// | GNU General Public License for more details. |
++// | |
++// | You should have received a copy of the GNU General Public License |
++// | along with this program; if not, write to: |
++// | |
++// | Free Software Foundation, Inc. |
++// | 59 Temple Place - Suite 330 |
++// | Boston, MA 02111-1307, USA. |
++// +----------------------------------------------------------------------+
++// | Authors: João Prado Maia <jpm@mysql.com> |
++// +----------------------------------------------------------------------+
++//
++// @(#) $Id$
++
++require_once dirname(__FILE__) . '/../init.php';
++
++$tpl = new Template_Helper();
++$tpl->setTemplate("list.tpl.html");
++
++Auth::checkAuthentication(APP_COOKIE);
++$usr_id = Auth::getUserID();
++$prj_id = Auth::getCurrentProject();
++
++$pagerRow = Issue::getParam('pagerRow');
++if (empty($pagerRow)) {
++ $pagerRow = 0;
++}
++$rows = Issue::getParam('rows');
++if (empty($rows)) {
++ $rows = APP_DEFAULT_PAGER_SIZE;
++}
++
++if (@$_REQUEST['view'] == 'my_assignments') {
++ $profile = Search_Profile::getProfile($usr_id, $prj_id, 'issue');
++ Search_Profile::remove($usr_id, $prj_id, 'issue');
++ Auth::redirect("list.php?users=$usr_id&hide_closed=1&rows=$rows&sort_by=" .
++ $profile['sort_by'] . "&sort_order=" . $profile['sort_order']);
++}
++
++$options = Issue::saveSearchParams();
++$tpl->assign("options", $options);
++$tpl->assign("sorting", Issue::getSortingInfo($options));
++
++// generate options for assign list. If there are groups and user is above a customer, include groups
++$groups = Group::getAssocList($prj_id);
++$users = Project::getUserAssocList($prj_id, 'active', User::getRoleID('Customer'));
++$assign_options = array(
++ "" => ev_gettext("Any"),
++ "-1" => ev_gettext("un-assigned"),
++ "-2" => ev_gettext("myself and un-assigned")
++);
++if (Auth::isAnonUser())
++ unset($assign_options["-2"]);
++else if (User::getGroupID($usr_id) != '') {
++ $assign_options['-3'] = ev_gettext('myself and my group');
++ $assign_options['-4'] = ev_gettext('myself, un-assigned and my group');
++}
++if ((count($groups) > 0) && (Auth::getCurrentRole() > User::getRoleID("Customer"))) {
++ foreach ($groups as $grp_id => $grp_name) {
++ $assign_options["grp:$grp_id"] = ev_gettext("Group") . ": " . $grp_name;
++ }
++}
++$assign_options += $users;
++
++$list = Issue::getListing($prj_id, $options, $pagerRow, $rows);
++$tpl->assign("list", $list["list"]);
++$tpl->assign("list_info", $list["info"]);
++$tpl->assign("csv_data", base64_encode(@$list["csv"]));
++
++$tpl->assign("columns", Display_Column::getColumnsToDisplay($prj_id, 'list_issues'));
++$tpl->assign("priorities", Priority::getAssocList($prj_id));
++$tpl->assign("status", Status::getAssocStatusList($prj_id));
++$tpl->assign("assign_options", $assign_options);
++$tpl->assign("custom", Filter::getAssocList($prj_id));
++$tpl->assign("csts", Filter::getListing(true));
++$tpl->assign("active_filters", Filter::getActiveFilters($options));
++$tpl->assign("categories", Category::getAssocList($prj_id));
++$tpl->assign("releases", Release::getAssocList($prj_id, true));
++$tpl->assign("reporters", Project::getReporters($prj_id));
++
++$prefs = Prefs::get($usr_id);
++$tpl->assign("refresh_rate", $prefs['list_refresh_rate'] * 60);
++$tpl->assign("refresh_page", "list.php");
++
++// items needed for bulk update tool
++if (Auth::getCurrentRole() > User::getRoleID("Developer")) {
++ $tpl->assign("users", $users);
++
++ if (Workflow::hasWorkflowIntegration($prj_id)) {
++ $open_statuses = Workflow::getAllowedStatuses($prj_id);
++ } else {
++ $open_statuses = Status::getAssocStatusList($prj_id, false);
++ }
++
++ $tpl->assign("open_status", $open_statuses);
++ $tpl->assign("closed_status", Status::getClosedAssocList($prj_id));
++ $tpl->assign("available_releases", Release::getAssocList($prj_id));
++}
++
++$tpl->displayTemplate();
+--- eventum-2.2/lib/eventum/class.display_column.php 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/lib/eventum/class.display_column.php 2009-10-12 22:10:36.429185594 +0300
+@@ -230,7 +230,10 @@
+ ),
+ "iss_expected_resolution_date" => array(
+ "title" => ev_gettext("Expected Resolution Date")
+- )
++ ),
++ "isu_order" => array(
++ "title" => ev_gettext("Order")
++ ),
+ )
+ );
+ return $columns[$page];
+--- eventum-2.2/lib/eventum/class.display_column.php.~1~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/lib/eventum/class.display_column.php.~1~ 2009-09-14 18:07:55.000000000 +0300
+@@ -0,0 +1,318 @@
++<?php
++/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
++// +----------------------------------------------------------------------+
++// | Eventum - Issue Tracking System |
++// +----------------------------------------------------------------------+
++// | Copyright (c) 2003 - 2008 MySQL AB |
++// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
++// | |
++// | This program is free software; you can redistribute it and/or modify |
++// | it under the terms of the GNU General Public License as published by |
++// | the Free Software Foundation; either version 2 of the License, or |
++// | (at your option) any later version. |
++// | |
++// | This program is distributed in the hope that it will be useful, |
++// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
++// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
++// | GNU General Public License for more details. |
++// | |
++// | You should have received a copy of the GNU General Public License |
++// | along with this program; if not, write to: |
++// | |
++// | Free Software Foundation, Inc. |
++// | 59 Temple Place - Suite 330 |
++// | Boston, MA 02111-1307, USA. |
++// +----------------------------------------------------------------------+
++// | Authors: Bryan Alsdorf <bryan@mysql.com> |
++// +----------------------------------------------------------------------+
++//
++//
++
++
++/**
++ * Class to handle determining which columns should be displayed and in what order
++ * on a page (e.g. Issue Listing page).
++ *
++ * @author Bryan Alsdorf <bryan@mysql.com>
++ * @version 1.0
++ */
++
++class Display_Column
++{
++ /**
++ * Returns the columns that should be displayed for the specified page.
++ * This method will remove columns that should not be displayed, due to
++ * lack of customer integration or insufficient role.
++ *
++ * @access public
++ * @param integer $prj_id The ID of the project.
++ * @param string $page The page to return columns for.
++ * @return array An array of columns that should be displayed.
++ */
++ function getColumnsToDisplay($prj_id, $page)
++ {
++ static $returns;
++
++ // poor man's caching system
++ if (!empty($returns[$prj_id][$page])) {
++ return $returns[$prj_id][$page];
++ }
++
++ $current_role = Auth::getCurrentRole();
++ $data = self::getSelectedColumns($prj_id, $page);
++ $has_customer_integration = Customer::hasCustomerIntegration($prj_id);
++ $only_with_customers = array('iss_customer_id', 'support_level');
++
++ // remove groups if there are no groups in the system.
++ if (count(Group::getAssocList($prj_id)) < 1) {
++ unset($data['iss_grp_id']);
++ }
++ // remove category column if there are no categories in the system
++ if (count(Category::getAssocList($prj_id)) < 1) {
++ unset($data['prc_title']);
++ }
++ // remove custom fields column if there are no custom fields
++ if (count(Custom_Field::getFieldsToBeListed($prj_id)) < 1) {
++ unset($data['custom_fields']);
++ }
++ // remove customer field if user has a role of customer
++ if ($current_role == User::getRoleID("Customer")) {
++ unset($data['iss_customer_id']);
++ }
++
++ foreach ($data as $field => $info) {
++ // remove fields based on role
++ if ($info['min_role'] > $current_role) {
++ unset($data[$field]);
++ continue;
++ }
++ // remove fields based on customer integration
++ if (!$has_customer_integration && (in_array($field, $only_with_customers))) {
++ unset($data[$field]);
++ continue;
++ }
++ // get title
++ $data[$field] = self::getColumnInfo($page, $field);
++ }
++ $returns[$prj_id][$page] = $data;
++ return $data;
++ }
++
++
++ /**
++ * Returns the columns that have been selected to be displayed on the specified page. This list
++ * contains all selected columns, even if they won't actually be displayed.
++ *
++ * @access public
++ * @param integer $prj_id The ID of the project.
++ * @param string $page The page to return columns for.
++ * @return array An array of columns that should be displayed.
++ */
++ function getSelectedColumns($prj_id, $page)
++ {
++ static $returns;
++
++ // poor man's caching system
++ if (!empty($returns[$prj_id][$page])) {
++ return $returns[$prj_id][$page];
++ }
++
++ $stmt = "SELECT
++ ctd_field,
++ ctd_min_role,
++ ctd_rank
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
++ WHERE
++ ctd_prj_id = $prj_id AND
++ ctd_page = '$page'
++ ORDER BY
++ ctd_rank";
++ $res = DB_Helper::getInstance()->getAssoc($stmt, false, array(), DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ $returns[$prj_id][$page] = array();
++ foreach ($res as $field_name => $row) {
++ $returns[$prj_id][$page][$field_name] = self::getColumnInfo($page, $field_name);
++ $returns[$prj_id][$page][$field_name]['min_role'] = $row['ctd_min_role'];
++ $returns[$prj_id][$page][$field_name]['rank'] = $row['ctd_rank'];
++ }
++ return $returns[$prj_id][$page];
++ }
++ }
++
++
++ /**
++ * Returns the info of the column
++ *
++ * @access public
++ * @param string $page The name of the page.
++ * @param string $column The name of the column
++ * @return string Info on the column
++ */
++ function getColumnInfo($page, $column)
++ {
++ $columns = self::getAllColumns($page);
++ return $columns[$column];
++ }
++
++
++ /**
++ * Returns all columns available for a page
++ *
++ * @access public
++ * @param string $page The name of the page
++ * @return array An array of columns
++ */
++ function getAllColumns($page)
++ {
++ $columns = array(
++ "list_issues" => array(
++ "pri_rank" => array(
++ "title" => ev_gettext("Priority")
++ ),
++ "iss_id" => array(
++ "title" => ev_gettext("Issue ID")
++ ),
++ "usr_full_name" => array(
++ "title" => ev_gettext("Reporter")
++ ),
++ "iss_created_date" => array(
++ "title" => ev_gettext("Created Date")
++ ),
++ "iss_grp_id" => array(
++ "title" => ev_gettext("Group")
++ ),
++ "assigned" => array(
++ "title" => ev_gettext("Assigned")
++ ),
++ "time_spent" => array(
++ "title" => ev_gettext("Time Spent")
++ ),
++ "iss_percent_complete" => array(
++ "title" => ev_gettext("% Complete"),
++ "default_role" => 9
++ ),
++ "iss_dev_time" => array(
++ "title" => ev_gettext("Est Dev Time"),
++ "default_role" => 9
++ ),
++ "prc_title" => array(
++ "title" => ev_gettext("Category")
++ ),
++ "pre_title" => array(
++ "title" => ev_gettext("Release")
++ ),
++ "iss_customer_id" => array(
++ "title" => ev_gettext("Customer")
++ ),
++ "support_level" => array(
++ "title" => ev_gettext("Support Level")
++ ),
++ "sta_rank" => array(
++ "title" => ev_gettext("Status")
++ ),
++ "sta_change_date" => array(
++ "title" => ev_gettext("Status Change Date")
++ ),
++ "last_action_date" => array(
++ "title" => ev_gettext("Last Action Date")
++ ),
++ "custom_fields" => array(
++ "title" => ev_gettext("Custom Fields")
++ ),
++ "iss_summary" => array(
++ "title" => ev_gettext("Summary"),
++ "align" => "left",
++ "width" => '30%'
++ ),
++ "iss_expected_resolution_date" => array(
++ "title" => ev_gettext("Expected Resolution Date")
++ )
++ )
++ );
++ return $columns[$page];
++ }
++
++
++ /**
++ * Saves settings on which columns should be displayed.
++ *
++ * @access public
++ * @return integer 1 if settings were saved successfully, -1 if there was an error.
++ */
++ function save()
++ {
++ $page = Misc::escapeString($_REQUEST['page']);
++ $prj_id = Misc::escapeInteger($_REQUEST['prj_id']);
++
++ $ranks = $_REQUEST['rank'];
++ asort($ranks);
++
++ // delete current entries
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
++ WHERE
++ ctd_prj_id = $prj_id AND
++ ctd_page = '$page'";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $rank = 1;
++ foreach ($ranks as $field_name => $requested_rank) {
++ $sql = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
++ SET
++ ctd_prj_id = $prj_id,
++ ctd_page = '$page',
++ ctd_field = '$field_name',
++ ctd_min_role = " . $_REQUEST['min_role'][$field_name] . ",
++ ctd_rank = $rank";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $rank++;
++ }
++ return 1;
++ }
++
++
++ /**
++ * Adds records in database for new project.
++ *
++ * @param integer $prj_id The ID of the project.
++ */
++ function setupNewProject($prj_id)
++ {
++ $page = 'list_issues';
++ $columns = self::getAllColumns($page);
++ $rank = 1;
++ foreach ($columns as $field_name => $column) {
++ if (!empty($column['default_role'])) {
++ $min_role = $column['default_role'];
++ } else {
++ $min_role = 1;
++ }
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
++ SET
++ ctd_prj_id = $prj_id,
++ ctd_page = '$page',
++ ctd_field = '$field_name',
++ ctd_min_role = $min_role,
++ ctd_rank = $rank";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $rank++;
++ }
++ }
++}
+--- eventum-2.2/lib/eventum/class.issue.php 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/lib/eventum/class.issue.php 2009-10-12 22:10:36.445851670 +0300
+@@ -1333,6 +1333,7 @@
+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
+ return -1;
+ } else {
++ self::moveOrderForAllUsers($issue_id, 1000);
+ $prj_id = self::getProjectID($issue_id);
+
+ // record the change
+@@ -1636,6 +1637,180 @@
+ }
+ }
+
++ /**
++ * Method used to update the a single detail field of a specific issue.
++ *
++ * @param integer $issue_id
++ * @param string $field_name
++ * @param string $field_value
++ * @param string $field_type string or integer (for escape)
++ * @return integer 1 on success, -1 otherwise
++ */
++ function updateField($issue_id, $field_name, $filed_value) {
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $usr_id = Auth::getUserID();
++ $prj_id = self::getProjectID($issue_id);
++
++ // get all of the 'current' information of this issue
++ $current = self::getDetails($issue_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='updated'";
++
++ switch ($field_name) {
++ case 'category':
++ $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'release':
++ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'expected_resolution_date':
++ if (is_null($filed_value)) {
++ $stmt .= ", iss_expected_resolution_date = null";
++ } else {
++ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
++ }
++ break;
++ case 'release':
++ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'priority':
++ $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'status':
++ $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'resolution':
++ $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'summary':
++ $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'description':
++ $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'estimated_dev_time':
++ $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'percent_complete':
++ $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'trigger_reminders':
++ $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'group':
++ $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'private':
++ $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
++ break;
++ default:
++ Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
++ return -1;
++ break;
++ }
++
++ $stmt .= "
++ WHERE
++ iss_id=$issue_id";
++
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $new = array(
++ 'category' => $current['iss_prc_id'],
++ 'release' => $current['iss_pre_id'],
++ 'expected_resolution_date' => $current['iss_expected_resolution_date'],
++ 'release' => $current['iss_pre_id'],
++ 'priority' => $current['iss_pri_id'],
++ 'status' => $current['iss_sta_id'],
++ 'resolution' => $current['iss_res_id'],
++ 'summary' => $current['iss_summary'],
++ 'description' => $current['iss_description'],
++ 'estimated_dev_time' => $current['iss_dev_time'],
++ 'percent_complete' => $current['iss_percent_complete'],
++ 'trigger_reminders' => $current['iss_trigger_reminders'],
++ 'group' => $current['iss_grp_id'],
++ 'iss_private' => $current['private']
++ );
++ $new[$field_name] = $filed_value;
++
++ // add change to the history (only for changes on specific fields?)
++ $updated_fields = array();
++ if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
++ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
++ }
++ if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
++ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
++ }
++ if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
++ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
++ }
++ if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
++ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
++ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
++ }
++ if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ $old_status_details = Status::getDetails($current['iss_sta_id']);
++ if ($old_status_details['sta_is_closed'] == 1) {
++ $new_status_details = Status::getDetails($filed_value);
++ if ($new_status_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
++ }
++ if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
++ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
++ }
++ if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
++ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
++ }
++ if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
++ $updated_fields["Summary"] = '';
++ }
++ if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
++ $updated_fields["Description"] = '';
++ }
++ if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
++ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
++ }
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $i = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($i > 0) {
++ $changes .= "; ";
++ }
++ if (($key != "Summary") && ($key != "Description")) {
++ $changes .= "$key: $value";
++ } else {
++ $changes .= "$key";
++ }
++ $i++;
++ }
++
++ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
++ // send notifications for the issue being updated
++ Notification::notifyIssueUpdated($issue_id, $current, $new);
++ }
++ }
++ return 1;
++ }
++
+
+ /**
+ * Move the issue to a new project
+@@ -1800,16 +1975,33 @@
+ {
+ $issue_id = Misc::escapeInteger($issue_id);
+ $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
++ $order = 1;
++ // move all orders down to free "order space" for this new association
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order + 1
++ WHERE
++ isu_usr_id = $assignee_usr_id AND
++ isu_order >= $order";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ // insert the new association
+ $stmt = "INSERT INTO
+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
+ (
+ isu_iss_id,
+ isu_usr_id,
+- isu_assigned_date
++ isu_assigned_date,
++ isu_order
+ ) VALUES (
+ $issue_id,
+ $assignee_usr_id,
+- '" . Date_Helper::getCurrentDateGMT() . "'
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ $order
+ )";
+ $res = DB_Helper::getInstance()->query($stmt);
+ if (PEAR::isError($res)) {
+@@ -1824,6 +2016,78 @@
+ }
+ }
+
++ /**
++ * Method used to get the order list to be rearranged
++ *
++ * @access private
++ * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
++ * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
++ * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
++ */
++ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
++ {
++ // find all affected associantion orders
++ $stmt = "SELECT isu_usr_id, isu_order FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id IN ($issue_id)";
++ if (!empty($usr_id)) {
++ $stmt.= " AND isu_usr_id IN ($usr_id)";
++ }
++ $stmt.= "ORDER BY isu_order";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $deleted_orders = array();
++ foreach ($res as $row) {
++ if (empty($deleted_orders[$row['isu_usr_id']])) {
++ $deleted_orders[$row['isu_usr_id']] = array();
++ }
++ $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
++ }
++ return $deleted_orders;
++ }
++ }
++
++ /**
++ *
++ * Method used to rearrange order list in the db according to known deleted records
++ *
++ * @access private
++ * @param mixed deleteorder list
++ * @return void
++ */
++ function rearrangeDeleteUserAssociationOrderList($delete_order_list)
++ {
++ if (empty($delete_order_list) || (!is_array($delete_order_list))) {
++ return -1;
++ }
++ foreach ($delete_order_list as $isu_usr_id => $orders) {
++ for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
++ // move the orders after them up to take the "order space" of the deleted records
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order - " . ($i+1) . "
++ WHERE
++ isu_usr_id = $isu_usr_id AND
++ isu_order > " . $orders[$i];
++ if ($i < count($orders) - 1) {
++ $stmt.= " AND
++ isu_order < " . $orders[$i+1];
++ }
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ }
++ }
++ return 1;
++ }
++
+
+ /**
+ * Method used to delete all user assignments for a specific issue.
+@@ -1839,6 +2103,7 @@
+ if (is_array($issue_id)) {
+ $issue_id = implode(", ", $issue_id);
+ }
++ $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
+ $stmt = "DELETE FROM
+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
+ WHERE
+@@ -1869,6 +2134,7 @@
+ {
+ $issue_id = Misc::escapeInteger($issue_id);
+ $usr_id = Misc::escapeInteger($usr_id);
++ $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
+ $stmt = "DELETE FROM
+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
+ WHERE
+@@ -1883,6 +2149,7 @@
+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
+ User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
+ }
++ self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
+ return 1;
+ }
+ }
+@@ -2342,6 +2609,11 @@
+ {
+ $sort_by = self::getParam('sort_by');
+ $sort_order = self::getParam('sort_order');
++ $users = self::getParam('users');
++ if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
++ unset($sort_by);
++ unset($sort_order);
++ }
+ $rows = self::getParam('rows');
+ $hide_closed = self::getParam('hide_closed');
+ if ($hide_closed === '') {
+@@ -2448,6 +2720,7 @@
+ "iss_expected_resolution_date" => "desc",
+ "pre_title" => "asc",
+ "assigned" => "asc",
++ "isu_order" => "desc",
+ );
+
+ foreach ($custom_fields as $fld_id => $fld_name) {
+@@ -3253,6 +3526,8 @@
+ $ids = implode(", ", $ids);
+ $stmt = "SELECT
+ isu_iss_id,
++ isu_order,
++ isu_usr_id,
+ usr_full_name
+ FROM
+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
+@@ -3264,6 +3539,7 @@
+ if (PEAR::isError($res)) {
+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
+ } else {
++ // gather names of the users assigned to each issue
+ $t = array();
+ for ($i = 0; $i < count($res); $i++) {
+ if (!empty($t[$res[$i]['isu_iss_id']])) {
+@@ -3272,9 +3548,18 @@
+ $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
+ }
+ }
++ // gather orders
++ $o = array();
++ for ($i = 0; $i < count($res); $i++) {
++ if (empty($o[$res[$i]['isu_iss_id']])) {
++ $o[$res[$i]['isu_iss_id']] = array();
++ }
++ $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
++ }
+ // now populate the $result variable again
+ for ($i = 0; $i < count($result); $i++) {
+ @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
++ @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
+ }
+ }
+ }
+@@ -4247,6 +4532,7 @@
+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
+ return -1;
+ }
++ self::moveOrderForAllUsers($issue_id, 1);
+ }
+
+
+@@ -4346,4 +4632,120 @@
+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
+ "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
+ }
++
++ /**
++ * Reorders user's issues as requested by user
++ * @access public
++ * @param $usr_id User to be reordered
++ * @param $issue_id Issue or array of issues to be moved
++ * @param $neworder The new order of the issues
++ * @return void
++ */
++ function reorderUserIssues($usr_id, $issue_id, $neworder)
++ {
++ if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
++ return false;
++ }
++ if (!is_numeric($usr_id) || !is_numeric($neworder)) {
++ return false;
++ }
++ $usr_id = Misc::escapeInteger($usr_id);
++ $issue_id = Misc::escapeInteger($issue_id);
++ $neworder = Misc::escapeInteger($neworder);
++ if (is_array($issue_id)) {
++ $issue_count = count($issue_id);
++ $issue_id_str = implode(", ", $issue_id);
++ } else {
++ $issue_count = 1;
++ $issue_id_str = $issue_id;
++ $issue_id = array($issue_id);
++ }
++ // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
++ $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
++ self::rearrangeDeleteUserAssociationOrderList($orderlist);
++ // move down the orders to free the "order space" needed
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order + $issue_count
++ WHERE
++ isu_usr_id = $usr_id AND
++ isu_order >= $neworder";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ //update the order for the issues being moved
++ $i = 0;
++ foreach ($issue_id as $iss_id) {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = " . ($neworder + $i) . "
++ WHERE
++ isu_usr_id = $usr_id AND
++ isu_iss_id = $iss_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $i++;
++ }
++ }
++
++
++ /**
++ * Get users issue order list
++ * @access public
++ * @param $user_id User
++ * @param $order_list Order of the issues
++ * @return void
++ */
++ function getIssueOrderByUser($usr_id) {
++
++ if (!is_numeric($usr_id)) {
++ return false;
++ }
++
++ $stmt = "SELECT
++ isu_iss_id, isu_order
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_usr_id = " . $usr_id ;
++
++ $order_list = array();
++
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ foreach ($res as $row) {
++ $order_list[$row["isu_iss_id"]] = $row["isu_order"];
++ }
++ }
++ return $order_list;
++ }
++
++ function moveOrderForAllUsers($issue_id, $neworder)
++ {
++ // Move the issue to the top priority for the ppl it's assigned to
++ $stmt = "SELECT isu_usr_id FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ foreach ($res as $row) {
++ self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
++ }
++ }
++
+ }
+--- eventum-2.2/lib/eventum/class.issue.php.~1~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/lib/eventum/class.issue.php.~1~ 2009-09-14 18:07:55.000000000 +0300
+@@ -0,0 +1,4349 @@
++<?php
++/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
++// +----------------------------------------------------------------------+
++// | Eventum - Issue Tracking System |
++// +----------------------------------------------------------------------+
++// | Copyright (c) 2003 - 2008 MySQL AB |
++// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
++// | |
++// | This program is free software; you can redistribute it and/or modify |
++// | it under the terms of the GNU General Public License as published by |
++// | the Free Software Foundation; either version 2 of the License, or |
++// | (at your option) any later version. |
++// | |
++// | This program is distributed in the hope that it will be useful, |
++// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
++// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
++// | GNU General Public License for more details. |
++// | |
++// | You should have received a copy of the GNU General Public License |
++// | along with this program; if not, write to: |
++// | |
++// | Free Software Foundation, Inc. |
++// | 59 Temple Place - Suite 330 |
++// | Boston, MA 02111-1307, USA. |
++// +----------------------------------------------------------------------+
++// | Authors: João Prado Maia <jpm@mysql.com> |
++// +----------------------------------------------------------------------+
++//
++
++
++/**
++ * Class designed to handle all business logic related to the issues in the
++ * system, such as adding or updating them or listing them in the grid mode.
++ *
++ * @author João Prado Maia <jpm@mysql.com>
++ * @version $Revision$
++ */
++
++class Issue
++{
++ /**
++ * Method used to check whether a given issue ID exists or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $check_project If we should check that this issue is in the current project
++ * @return boolean
++ */
++ function exists($issue_id, $check_project = true)
++ {
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ if ($check_project) {
++ $stmt .= " AND
++ iss_prj_id = " . Auth::getCurrentProject();
++ }
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the list of column heading titles for the
++ * CSV export functionality of the issue listing screen.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @return array The list of column heading titles
++ */
++ function getColumnHeadings($prj_id)
++ {
++ $headings = array(
++ 'Priority',
++ 'Issue ID',
++ 'Reporter',
++ );
++ // hide the group column from the output if no
++ // groups are available in the database
++ $groups = Group::getAssocList($prj_id);
++ if (count($groups) > 0) {
++ $headings[] = 'Group';
++ }
++ $headings[] = 'Assigned';
++ $headings[] = 'Time Spent';
++ // hide the category column from the output if no
++ // categories are available in the database
++ $categories = Category::getAssocList($prj_id);
++ if (count($categories) > 0) {
++ $headings[] = 'Category';
++ }
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $headings[] = 'Customer';
++ }
++ $headings[] = 'Status';
++ $headings[] = 'Status Change Date';
++ $headings[] = 'Last Action Date';
++ $headings[] = 'Est. Dev. TIme';
++ $headings[] = 'Summary';
++ $headings[] = 'Expected Resolution Date';
++ return $headings;
++ }
++
++
++ /**
++ * Method used to get the full list of date fields available to issues, to
++ * be used when customizing the issue listing screen in the 'last status
++ * change date' column.
++ *
++ * @access public
++ * @param boolean $display_customer_fields Whether to include any customer related fields or not
++ * @return array The list of available date fields
++ */
++ function getDateFieldsAssocList($display_customer_fields = FALSE)
++ {
++ $fields = array(
++ 'iss_created_date' => 'Created Date',
++ 'iss_updated_date' => 'Last Updated Date',
++ 'iss_last_response_date' => 'Last Response Date',
++ 'iss_closed_date' => 'Closed Date'
++ );
++ if ($display_customer_fields) {
++ $fields['iss_last_customer_action_date'] = 'Customer Action Date';
++ }
++ asort($fields);
++ return $fields;
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs and their respective
++ * titles associated to a given project.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @return array The list of issues
++ */
++ function getAssocListByProject($prj_id)
++ {
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Misc::escapeInteger($prj_id) . "
++ ORDER BY
++ iss_id ASC";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The status ID
++ */
++ function getStatusID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_sta_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Records the last customer action date for a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function recordLastCustomerAction($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='customer action'
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++
++
++ /**
++ * Returns the customer ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getCustomerID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the contract ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getContractID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_contract_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Sets the contract ID for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer The contract ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setContractID($issue_id, $contract_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $old_contract_id = self::getContractID($issue_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // log this
++ History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ /**
++ * Returns the customer ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getContactID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_contact_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the project associated to a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $force_refresh If the cache should not be used.
++ * @return integer The project ID
++ */
++ function getProjectID($issue_id, $force_refresh = false)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_prj_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to remotely assign a given issue to an user.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing the change
++ * @param boolean $assignee The user ID of the assignee
++ * @return integer The status ID
++ */
++ function remoteAssign($issue_id, $usr_id, $assignee)
++ {
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
++ // clear up the assignments for this issue, and then assign it to the current user
++ self::deleteUserAssociations($issue_id, $usr_id);
++ $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
++ if ($res != -1) {
++ // save a history entry about this...
++ History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
++ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
++ if ($assignee != $usr_id) {
++ Notification::notifyNewAssignment(array($assignee), $issue_id);
++ }
++ }
++ return $res;
++ }
++
++
++ /**
++ * Method used to set the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $status_id The new status ID
++ * @param boolean $notify If a notification should be sent about this change.
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setStatus($issue_id, $status_id, $notify = false)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $status_id = Misc::escapeInteger($status_id);
++
++ $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
++ if ($workflow !== true) {
++ return $workflow;
++ }
++
++ // check if the status is already set to the 'new' one
++ if (self::getStatusID($issue_id) == $status_id) {
++ return -1;
++ }
++
++ $old_status = self::getStatusID($issue_id);
++ $old_details = Status::getDetails($old_status);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_sta_id=$status_id,
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='update'
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ if (@$old_details['sta_is_closed'] == 1) {
++ $new_details = Status::getDetails($status_id);
++ if ($new_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++
++ if ($notify) {
++ Notification::notifyStatusChange($issue_id, $old_status, $status_id);
++ }
++
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to remotely set the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing this change
++ * @param integer $new_status The new status ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setRemoteStatus($issue_id, $usr_id, $new_status)
++ {
++ $sta_id = Status::getStatusID($new_status);
++
++ $res = self::setStatus($issue_id, $sta_id);
++ if ($res == 1) {
++ // record history entry
++ History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
++ }
++ return $res;
++ }
++
++
++ /**
++ * Method used to set the release of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $pre_id The ID of the release to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setRelease($issue_id, $pre_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $pre_id = Misc::escapeInteger($pre_id);
++
++ if ($pre_id != self::getRelease($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_pre_id = $pre_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current release of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The release
++ */
++ function getRelease($issue_id)
++ {
++ $sql = "SELECT
++ iss_pre_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to set the priority of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $pri_id The ID of the priority to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setPriority($issue_id, $pri_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $pri_id = Misc::escapeInteger($pri_id);
++
++ if ($pri_id != self::getPriority($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_pri_id = $pri_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current issue priority
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The priority
++ */
++ function getPriority($issue_id)
++ {
++ $sql = "SELECT
++ iss_pri_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to set the category of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $prc_id The ID of the category to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setCategory($issue_id, $prc_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $prc_id = Misc::escapeInteger($prc_id);
++
++ if ($prc_id != self::getPriority($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prc_id = $prc_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current issue category
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The category
++ */
++ function getCategory($issue_id)
++ {
++ $sql = "SELECT
++ iss_prc_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get all issues associated with a status that doesn't have
++ * the 'closed' context.
++ *
++ * @access public
++ * @param integer $prj_id The project ID to list issues from
++ * @param integer $usr_id The user ID of the user requesting this information
++ * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
++ * @param integer $status_id The status ID to be used to restrict results
++ * @return array The list of open issues
++ */
++ function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
++ {
++ $prj_id = Misc::escapeInteger($prj_id);
++ $status_id = Misc::escapeInteger($status_id);
++ $projects = Project::getRemoteAssocListByUser($usr_id);
++ if (@count($projects) == 0) {
++ return '';
++ }
++
++ $stmt = "SELECT
++ iss_id,
++ iss_summary,
++ sta_title
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ )
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id
++ WHERE ";
++ if (!empty($status_id)) {
++ $stmt .= " sta_id=$status_id AND ";
++ }
++ $stmt .= "
++ iss_prj_id=$prj_id AND
++ sta_id=iss_sta_id AND
++ sta_is_closed=0";
++ if ($show_all_issues == false) {
++ $stmt .= " AND
++ isu_usr_id=$usr_id";
++ }
++ $stmt .= "\nGROUP BY
++ iss_id";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ if (count($res) > 0) {
++ self::getAssignedUsersByIssues($res);
++ }
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to build the required parameters to simulate an email reply
++ * to the user who reported the issue, using the issue details like summary
++ * and description as email fields.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The email parameters
++ */
++ function getReplyDetails($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $stmt = "SELECT
++ iss_created_date,
++ usr_full_name AS reporter,
++ usr_email AS reporter_email,
++ iss_description AS description,
++ iss_summary AS sup_subject
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ iss_usr_id=usr_id AND
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
++ $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to record the last updated timestamp for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param string $type The type of update that was made (optional)
++ * @return boolean
++ */
++ function markAsUpdated($issue_id, $type = false)
++ {
++ $public = array("staff response", "customer action", "file uploaded", "user response");
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
++ if ($type != false) {
++ if (in_array($type, $public)) {
++ $field = "iss_last_public_action_";
++ } else {
++ $field = "iss_last_internal_action_";
++ }
++ $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
++ $field . "type ='" . Misc::escapeString($type) . "'\n";
++ }
++ $stmt .= "WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ // update last response dates if this is a staff response
++ if ($type == "staff response") {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ DB_Helper::getInstance()->query($stmt);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
++ WHERE
++ iss_first_response_date IS NULL AND
++ iss_id = " . Misc::escapeInteger($issue_id);
++ DB_Helper::getInstance()->query($stmt);
++ }
++
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to check whether a given issue has duplicates
++ * or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return boolean
++ */
++ function hasDuplicates($issue_id)
++ {
++ $stmt = "SELECT
++ COUNT(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to update the duplicated issues for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function updateDuplicates($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $ids = self::getDuplicateList($issue_id);
++ if ($ids == '') {
++ return -1;
++ }
++ $ids = @array_keys($ids);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
++ if (@$_POST["keep"] == "no") {
++ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
++ }
++ $stmt .= "
++ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
++ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
++ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
++ WHERE
++ iss_id IN (" . implode(", ", $ids) . ")";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // record the change
++ for ($i = 0; $i < count($ids); $i++) {
++ History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
++ "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
++ }
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to get a list of the duplicate issues for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of duplicates
++ */
++ function getDuplicateList($issue_id)
++ {
++ $res = self::getDuplicateDetailsList($issue_id);
++ if (@count($res) == 0) {
++ return '';
++ } else {
++ $list = array();
++ for ($i = 0; $i < count($res); $i++) {
++ $list[$res[$i]['issue_id']] = $res[$i]['title'];
++ }
++ return $list;
++ }
++ }
++
++
++ /**
++ * Method used to get a list of the duplicate issues (and their details)
++ * for a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of duplicates
++ */
++ function getDuplicateDetailsList($issue_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_id issue_id,
++ iss_summary title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_sta_id=sta_id AND
++ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to clear the duplicate status of an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function clearDuplicateStatus($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_duplicated_iss_id=NULL
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // record the change
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to mark an issue as a duplicate of an existing one.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function markAsDuplicate($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (!self::exists($issue_id)) {
++ return -1;
++ }
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if (!empty($_POST["comments"])) {
++ // add note with the comments of marking an issue as a duplicate of another one
++ $_POST['title'] = 'Issue duplication comments';
++ $_POST["note"] = $_POST["comments"];
++ Note::insert(Auth::getUserID(), $issue_id);
++ }
++ // record the change
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
++ "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ function isDuplicate($issue_id)
++ {
++ $sql = "SELECT
++ count(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id) . " AND
++ iss_duplicated_iss_id IS NULL";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ }
++ if ($res > 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to get an associative array of user ID => user
++ * status associated with a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of users
++ */
++ function getAssignedUsersStatus($issue_id)
++ {
++ $stmt = "SELECT
++ usr_id,
++ usr_status
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the summary associated with a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return string The issue summary
++ */
++ function getTitle($issue_id)
++ {
++ $stmt = "SELECT
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the issue ID associated with a specific summary.
++ *
++ * @access public
++ * @param string $summary The summary to look for
++ * @return integer The issue ID
++ */
++ function getIssueID($summary)
++ {
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_summary='" . Misc::escapeString($summary) . "'";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ if (empty($res)) {
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to add a new anonymous based issue in the system.
++ *
++ * @access public
++ * @return integer The new issue ID
++ */
++ function addAnonymousReport()
++ {
++ $options = Project::getAnonymousPostOptions($_POST["project"]);
++ $initial_status = Project::getInitialStatus($_POST["project"]);
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ (
++ iss_prj_id,
++ iss_prc_id,
++ iss_pre_id,
++ iss_pri_id,
++ iss_usr_id,";
++ if (!empty($initial_status)) {
++ $stmt .= "iss_sta_id,";
++ }
++ $stmt .= "
++ iss_created_date,
++ iss_last_public_action_date,
++ iss_last_public_action_type,
++ iss_summary,
++ iss_description,
++ iss_root_message_id
++ ) VALUES (
++ " . Misc::escapeInteger($_POST["project"]) . ",
++ " . $options["category"] . ",
++ 0,
++ " . $options["priority"] . ",
++ " . $options["reporter"] . ",";
++ if (!empty($initial_status)) {
++ $stmt .= "$initial_status,";
++ }
++ $stmt .= "
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ 'created',
++ '" . Misc::escapeString($_POST["summary"]) . "',
++ '" . Misc::escapeString($_POST["description"]) . "',
++ '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
++ )";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return $res;
++ } else {
++ $new_issue_id = DB_Helper::get_last_insert_id();
++ // log the creation of the issue
++ History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
++
++ // now process any files being uploaded
++ $found = 0;
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ if (!@empty($_FILES["file"]["name"][$i])) {
++ $found = 1;
++ break;
++ }
++ }
++ if ($found) {
++ $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ $filename = @$_FILES["file"]["name"][$i];
++ if (empty($filename)) {
++ continue;
++ }
++ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
++ if (!empty($blob)) {
++ Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
++ }
++ }
++ }
++ // need to process any custom fields ?
++ if (@count($_POST["custom_fields"]) > 0) {
++ foreach ($_POST["custom_fields"] as $fld_id => $value) {
++ Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
++ }
++ }
++
++ // now add the user/issue association
++ $assign = array();
++ $users = @$options["users"];
++ $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
++ for ($i = 0; $i < count($users); $i++) {
++ Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
++ $assign[] = $users[$i];
++ }
++
++ Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($_POST['project'], $new_issue_id);
++
++ return $new_issue_id;
++ }
++ }
++
++
++ /**
++ * Method used to remove all issues associated with a specific list of
++ * projects.
++ *
++ * @access public
++ * @param array $ids The list of projects to look for
++ * @return boolean
++ */
++ function removeByProjects($ids)
++ {
++ $items = @implode(", ", Misc::escapeInteger($ids));
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id IN ($items)";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if (count($res) > 0) {
++ self::deleteAssociations($res);
++ Attachment::removeByIssues($res);
++ SCM::removeByIssues($res);
++ Impact_Analysis::removeByIssues($res);
++ self::deleteUserAssociations($res);
++ Note::removeByIssues($res);
++ Time_Tracking::removeByIssues($res);
++ Notification::removeByIssues($res);
++ Custom_Field::removeByIssues($res);
++ Phone_Support::removeByIssues($res);
++ History::removeByIssues($res);
++ // now really delete the issues
++ $items = implode(", ", $res);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id IN ($items)";
++ DB_Helper::getInstance()->query($stmt);
++ }
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to close off an issue.
++ *
++ * @access public
++ * @param integer $usr_id The user ID
++ * @param integer $issue_id The issue ID
++ * @param bool $send_notification Whether to send a notification about this action or not
++ * @param integer $resolution_id The resolution ID
++ * @param integer $status_id The status ID
++ * @param string $reason The reason for closing this issue
++ * @param string $send_notification_to Who this notification should be sent too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
++ {
++ $usr_id = Misc::escapeInteger($usr_id);
++ $issue_id = Misc::escapeInteger($issue_id);
++ $resolution_id = Misc::escapeInteger($resolution_id);
++ $status_id = Misc::escapeInteger($status_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='closed',
++ iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
++ if (!empty($resolution_id)) {
++ $stmt .= "iss_res_id=$resolution_id,\n";
++ }
++ $stmt .= "iss_sta_id=$status_id
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $prj_id = self::getProjectID($issue_id);
++
++ // record the change
++ History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
++
++ if ($send_notification_to == 'all') {
++
++ $from = User::getFromHeader($usr_id);
++ $message_id = User::getFromHeader($usr_id);
++ $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
++ '', '', 'Issue closed comments', $reason, '');
++
++ $structure = Mime_Helper::decode($full_email, true, false);
++
++ $email = array(
++ 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
++ 'issue_id' => $issue_id,
++ 'message_id' => $message_id,
++ 'date' => Date_Helper::getCurrentDateGMT(),
++ 'subject' => 'Issue closed comments',
++ 'from' => $from,
++ 'has_attachment'=> 0,
++ 'body' => $reason,
++ 'full_email' => $full_email,
++ 'headers' => $structure->headers
++ );
++ Support::insertEmail($email, $structure, $sup_id, true);
++ $ids = $sup_id;
++ } else {
++ // add note with the reason to close the issue
++ $_POST['title'] = 'Issue closed comments';
++ $_POST["note"] = $reason;
++ Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
++ $ids = false;
++ }
++
++ if ($send_notification) {
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ // send a special confirmation email when customer issues are closed
++ $stmt = "SELECT
++ iss_customer_contact_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
++ if (!empty($customer_contact_id)) {
++ Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
++ }
++ }
++ // send notifications for the issue being closed
++ Notification::notify($issue_id, 'closed', $ids);
++ }
++ Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to update the details of a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 or -2 otherwise
++ */
++ function update($issue_id)
++ {
++ global $errors;
++ $errors = array();
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $usr_id = Auth::getUserID();
++ $prj_id = self::getProjectID($issue_id);
++
++ $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
++ if ($workflow !== true) {
++ return $workflow;
++ }
++
++ // get all of the 'current' information of this issue
++ $current = self::getDetails($issue_id);
++ // update the issue associations
++ if (empty($_POST['associated_issues'])) {
++ $associated_issues = array();
++ } else {
++ $associated_issues = explode(',', @$_POST['associated_issues']);
++ // make sure all associated issues are valid (and in this project)
++ for ($i = 0; $i < count($associated_issues); $i++) {
++ if (!self::exists(trim($associated_issues[$i]), false)) {
++ $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
++ unset($associated_issues[$i]);
++ }
++ }
++ }
++ $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
++ if (count($association_diff) > 0) {
++ // go through the new assocations, if association already exists, skip it
++ $associations_to_remove = $current['associated_issues'];
++ if (count($associated_issues) > 0) {
++ foreach ($associated_issues as $index => $associated_id) {
++ if (!in_array($associated_id, $current['associated_issues'])) {
++ self::addAssociation($issue_id, $associated_id, $usr_id);
++ } else {
++ // already assigned, remove this user from list of users to remove
++ unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
++ }
++ }
++ }
++ if (count($associations_to_remove) > 0) {
++ foreach ($associations_to_remove as $associated_id) {
++ self::deleteAssociation($issue_id, $associated_id);
++ }
++ }
++ }
++ $assignments_changed = false;
++ if (@$_POST["keep_assignments"] == "no") {
++ // only change the issue-user associations if there really were any changes
++ $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
++ if (!empty($_POST['assignments'])) {
++ $new_assignees = @$_POST['assignments'];
++ } else {
++ $new_assignees = array();
++ }
++ $assignment_notifications = array();
++
++ // remove people from the assignment list, if appropriate
++ foreach ($old_assignees as $assignee) {
++ if (!in_array($assignee, $new_assignees)) {
++ self::deleteUserAssociation($issue_id, $assignee);
++ $assignments_changed = true;
++ }
++ }
++ // add people to the assignment list, if appropriate
++ foreach ($new_assignees as $assignee) {
++ if (!in_array($assignee, $old_assignees)) {
++ self::addUserAssociation($usr_id, $issue_id, $assignee);
++ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
++ $assignment_notifications[] = $assignee;
++ $assignments_changed = true;
++ }
++ }
++ if (count($assignment_notifications) > 0) {
++ Notification::notifyNewAssignment($assignment_notifications, $issue_id);
++ }
++ }
++ if (empty($_POST["estimated_dev_time"])) {
++ $_POST["estimated_dev_time"] = 0;
++ }
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='updated',";
++ if (!empty($_POST["category"])) {
++ $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
++ }
++ if (@$_POST["keep"] == "no") {
++ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
++ }
++ if (!empty($_POST['expected_resolution_date'])) {
++ $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
++ } else {
++ $stmt .= "iss_expected_resolution_date=null,";
++ }
++ $stmt .= "
++ iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
++ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
++ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
++ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
++ iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
++ iss_description='" . Misc::escapeString($_POST["description"]) . "',
++ iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
++ iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
++ iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
++ iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
++ if (isset($_POST['private'])) {
++ $stmt .= ",
++ iss_private = " . Misc::escapeInteger($_POST['private']);
++ }
++ $stmt .= "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add change to the history (only for changes on specific fields?)
++ $updated_fields = array();
++ if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
++ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
++ }
++ if ($current["iss_prc_id"] != $_POST["category"]) {
++ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
++ }
++ if ($current["iss_pre_id"] != $_POST["release"]) {
++ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
++ }
++ if ($current["iss_pri_id"] != $_POST["priority"]) {
++ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
++ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
++ }
++ if ($current["iss_sta_id"] != $_POST["status"]) {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ $old_status_details = Status::getDetails($current['iss_sta_id']);
++ if ($old_status_details['sta_is_closed'] == 1) {
++ $new_status_details = Status::getDetails($_POST["status"]);
++ if ($new_status_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
++ }
++ if ($current["iss_res_id"] != $_POST["resolution"]) {
++ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
++ }
++ if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
++ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
++ }
++ if ($current["iss_summary"] != $_POST["summary"]) {
++ $updated_fields["Summary"] = '';
++ }
++ if ($current["iss_description"] != $_POST["description"]) {
++ $updated_fields["Description"] = '';
++ }
++ if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
++ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
++ }
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $i = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($i > 0) {
++ $changes .= "; ";
++ }
++ if (($key != "Summary") && ($key != "Description")) {
++ $changes .= "$key: $value";
++ } else {
++ $changes .= "$key";
++ }
++ $i++;
++ }
++ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
++ // send notifications for the issue being updated
++ Notification::notifyIssueUpdated($issue_id, $current, $_POST);
++ }
++
++ // record group change as a seperate change
++ if ($current["iss_grp_id"] != (int)$_POST["group"]) {
++ History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
++ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
++ }
++
++ // now update any duplicates, if any
++ $update_dupe = array(
++ 'Category',
++ 'Release',
++ 'Priority',
++ 'Release',
++ 'Resolution'
++ );
++ // COMPAT: the following line requires PHP > 4.0.4
++ $intersect = array_intersect($update_dupe, array_keys($updated_fields));
++ if (($current["duplicates"] != '') && (count($intersect) > 0)) {
++ self::updateDuplicates($issue_id);
++ }
++
++ // if there is customer integration, mark last customer action
++ if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
++ self::recordLastCustomerAction($issue_id);
++ }
++
++ if ($assignments_changed) {
++ // XXX: we may want to also send the email notification for those "new" assignees
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
++ }
++
++ Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
++ // Move issue to another project
++ if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
++ $new_prj_id = (int)@$_POST['new_prj'];
++ if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
++ if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
++ $res = self::moveIssue($issue_id, $new_prj_id);
++ if ($res == -1) {
++ return $res;
++ }
++ } else {
++ return -1;
++ }
++ }
++ }
++ return 1;
++ }
++ }
++
++
++ /**
++ * Move the issue to a new project
++ *
++ * @param integer $issue_id
++ * @param integer $new_prj_id
++ * @return integer 1 on success, -1 otherwise
++ */
++ function moveIssue($issue_id, $new_prj_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $currentDetails = self::getDetails($issue_id);
++
++ // set new category
++ $new_iss_prc_list = Category::getAssocList($new_prj_id);
++ $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
++ $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
++ if ($new_prc_id === false) {
++ // use the first category listed in the new project
++ $new_prc_id = key($new_iss_prc_list);
++ }
++
++ // set new priority
++ $new_iss_pri_list = Priority::getAssocList($new_prj_id);
++ $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
++ $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
++ if ($new_pri_id === false) {
++ // use the first category listed in the new project
++ $new_pri_id = key($new_iss_pri_list);
++ }
++
++ // XXX: Set status if needed when moving issue
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
++ iss_pri_id=" . $new_pri_id . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ }
++
++ // clear project cache
++ self::getProjectID($issue_id, true);
++
++ Notification::notifyNewIssue($new_prj_id, $issue_id);
++ }
++ }
++
++
++ /**
++ * Method used to associate an existing issue with another one.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $issue_id The other issue ID
++ * @return void
++ */
++ function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $associated_id = Misc::escapeInteger($associated_id);
++
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ (
++ isa_issue_id,
++ isa_associated_id
++ ) VALUES (
++ $issue_id,
++ $associated_id
++ )";
++ DB_Helper::getInstance()->query($stmt);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
++ // link the associated issue back to this one
++ if ($link_issues) {
++ self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
++ }
++ }
++
++
++ /**
++ * Method used to remove the issue associations related to a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return void
++ */
++ function deleteAssociations($issue_id, $usr_id = FALSE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (is_array($issue_id)) {
++ $issue_id = implode(", ", $issue_id);
++ }
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ WHERE
++ isa_issue_id IN ($issue_id) OR
++ isa_associated_id IN ($issue_id)";
++ DB_Helper::getInstance()->query($stmt);
++ if ($usr_id) {
++ History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
++ }
++ }
++
++
++ /**
++ * Method used to remove a issue association from an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $associated_id The associated issue ID to remove.
++ * @return void
++ */
++ function deleteAssociation($issue_id, $associated_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $associated_id = Misc::escapeInteger($associated_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ WHERE
++ (
++ isa_issue_id = $issue_id AND
++ isa_associated_id = $associated_id
++ ) OR
++ (
++ isa_issue_id = $associated_id AND
++ isa_associated_id = $issue_id
++ )";
++ DB_Helper::getInstance()->query($stmt);
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
++ "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
++ History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
++ "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
++ }
++
++
++ /**
++ * Method used to assign an issue with an user.
++ *
++ * @access public
++ * @param integer $usr_id The user ID of the person performing this change
++ * @param integer $issue_id The issue ID
++ * @param integer $assignee_usr_id The user ID of the assignee
++ * @param boolean $add_history Whether to add a history entry about this or not
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ (
++ isu_iss_id,
++ isu_usr_id,
++ isu_assigned_date
++ ) VALUES (
++ $issue_id,
++ $assignee_usr_id,
++ '" . Date_Helper::getCurrentDateGMT() . "'
++ )";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($add_history) {
++ History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
++ 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
++ }
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to delete all user assignments for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing the change
++ * @return void
++ */
++ function deleteUserAssociations($issue_id, $usr_id = FALSE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (is_array($issue_id)) {
++ $issue_id = implode(", ", $issue_id);
++ }
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id IN ($issue_id)";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($usr_id) {
++ History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
++ }
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to delete a single user assignments for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user to remove.
++ * @param boolean $add_history Whether to add a history entry about this or not
++ * @return void
++ */
++ function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $usr_id = Misc::escapeInteger($usr_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id = $issue_id AND
++ isu_usr_id = $usr_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($add_history) {
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
++ User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
++ }
++ return 1;
++ }
++ }
++
++
++ /**
++ * Creates an issue with the given email information.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @param integer $usr_id The user responsible for this action
++ * @param string $sender The original sender of this email
++ * @param string $summary The issue summary
++ * @param string $description The issue description
++ * @param integer $category The category ID
++ * @param integer $priority The priority ID
++ * @param array $assignment The list of users to assign this issue to
++ * @param string $date The date the email was originally sent.
++ * @param string $msg_id The message ID of the email we are creating this issue from.
++ * @return void
++ */
++ function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
++ {
++ $data = array();
++ $exclude_list = array();
++
++ $sender_email = Mail_Helper::getEmailAddress($sender);
++ $sender_usr_id = User::getUserIDByEmail($sender_email, true);
++ if (!empty($sender_usr_id)) {
++ $reporter = $sender_usr_id;
++ $exclude_list[] = $sender_usr_id;
++ }
++
++ $data = array(
++ 'category' => $category,
++ 'priority' => $priority,
++ 'description' => $description,
++ 'summary' => $summary,
++ 'msg_id' => $msg_id,
++ );
++
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
++ if (!empty($customer_id)) {
++ $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
++ // overwrite the reporter with the customer contact
++ $reporter = User::getUserIDByContactID($customer_contact_id);
++ $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
++
++ $data['customer'] = $customer_id;
++ $data['contact'] = $customer_contact_id;
++# $data['contract'] = // XXX missing
++ $data['contact_person_lname'] = $contact['last_name'];
++ $data['contact_person_fname'] = $contact['first_name'];
++ $data['contact_email'] = $sender_email;
++ $data['contact_phone'] = $contact['phone'];
++ $data['contact_timezone'] = $contact_timezone;
++ }
++ } else {
++ $customer_id = FALSE;
++ }
++ if (empty($reporter)) {
++ $reporter = APP_SYSTEM_USER_ID;
++ }
++
++ $data['reporter'] = $reporter;
++
++ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
++ if ($issue_id == -1) {
++ return -1;
++ }
++
++ $has_TAM = false;
++ $has_RR = false;
++ // log the creation of the issue
++ History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
++
++ $emails = array();
++ $manager_usr_ids = array();
++ if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
++ // if there are any technical account managers associated with this customer, add these users to the notification list
++ $managers = Customer::getAccountManagers($prj_id, $customer_id);
++ $manager_usr_ids = array_keys($managers);
++ $manager_emails = array_values($managers);
++ $emails = array_merge($emails, $manager_emails);
++ }
++ // add the reporter to the notification list
++ $emails[] = $sender;
++ $emails = array_unique($emails);
++ $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
++ foreach ($emails as $address) {
++ Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
++ }
++
++ // only assign the issue to an user if the associated customer has any technical account managers
++ $users = array();
++ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
++ foreach ($manager_usr_ids as $manager_usr_id) {
++ $users[] = $manager_usr_id;
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
++ }
++ $has_TAM = true;
++ }
++ // now add the user/issue association
++ if (@count($assignment) > 0) {
++ for ($i = 0; $i < count($assignment); $i++) {
++ Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
++ if ($assignment[$i] != $usr_id) {
++ $users[] = $assignment[$i];
++ }
++ }
++ } else {
++ // only use the round-robin feature if this new issue was not
++ // already assigned to a customer account manager
++ if (@count($manager_usr_ids) < 1) {
++ $assignee = Round_Robin::getNextAssignee($prj_id);
++ // assign the issue to the round robin person
++ if (!empty($assignee)) {
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
++ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
++ $users[] = $assignee;
++ $has_RR = true;
++ }
++ }
++ }
++ if (count($users) > 0) {
++ $has_assignee = true;
++ }
++
++ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
++
++ // send special 'an issue was auto-created for you' notification back to the sender
++ Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
++
++ return $issue_id;
++ }
++
++
++ /**
++ * Return errors that happened when creating new issue from POST method.
++ *
++ * @return array
++ */
++ private static $insert_errors = array();
++ static function getInsertErrors() {
++ return self::$insert_errors;
++ }
++
++ /**
++ * Method used to add a new issue using the normal report form.
++ *
++ * @access public
++ * @return integer The new issue ID
++ */
++ function createFromPost()
++ {
++ $keys = array(
++ 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
++ 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
++ 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
++ );
++ $data = array();
++ foreach ($keys as $key) {
++ if (isset($_POST[$key])) {
++ $data[$key] = $_POST[$key];
++ }
++ }
++
++ $prj_id = Auth::getCurrentProject();
++ $usr_id = Auth::getUserID();
++
++ // if we are creating an issue for a customer, put the
++ // main customer contact as the reporter for it
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $contact_usr_id = User::getUserIDByContactID($data['contact']);
++ if (empty($contact_usr_id)) {
++ $contact_usr_id = $usr_id;
++ }
++ $data['reporter'] = $contact_usr_id;
++ } else {
++ $data['reporter'] = $usr_id;
++ }
++
++ $data['msg_id'] = Mail_Helper::generateMessageID();
++
++ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
++ if ($issue_id == -1) {
++ return -1;
++ }
++
++ $has_TAM = false;
++ $has_RR = false;
++ $info = User::getNameEmail($usr_id);
++ // log the creation of the issue
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
++
++ $emails = array();
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
++ $emails = $data['contact_extra_emails'];
++ }
++ // add the primary contact to the notification list
++ if ($data['add_primary_contact'] == 'yes') {
++ $contact_email = User::getEmailByContactID($data['contact']);
++ if (!empty($contact_email)) {
++ $emails[] = $contact_email;
++ }
++ }
++ // if there are any technical account managers associated with this customer, add these users to the notification list
++ $managers = Customer::getAccountManagers($prj_id, $data['customer']);
++ $manager_usr_ids = array_keys($managers);
++ $manager_emails = array_values($managers);
++ $emails = array_merge($emails, $manager_emails);
++ }
++ // add the reporter to the notification list
++ $emails[] = $info['usr_email'];
++ $emails = array_unique($emails);
++ foreach ($emails as $address) {
++ Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
++ }
++
++ // only assign the issue to an user if the associated customer has any technical account managers
++ $users = array();
++ $has_TAM = false;
++ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
++ foreach ($manager_usr_ids as $manager_usr_id) {
++ $users[] = $manager_usr_id;
++ self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
++ }
++ $has_TAM = true;
++ }
++ // now add the user/issue association (aka assignments)
++ if (!empty($data['users']) && count($data['users']) > 0) {
++ for ($i = 0; $i < count($data['users']); $i++) {
++ Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
++ Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
++ self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
++ if ($data['users'][$i] != $usr_id) {
++ $users[] = $data['users'][$i];
++ }
++ }
++ } else {
++ // only use the round-robin feature if this new issue was not
++ // already assigned to a customer account manager
++ if (@count($manager_usr_ids) < 1) {
++ $assignee = Round_Robin::getNextAssignee($prj_id);
++ // assign the issue to the round robin person
++ if (!empty($assignee)) {
++ $users[] = $assignee;
++ self::addUserAssociation($usr_id, $issue_id, $assignee, false);
++ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
++ $has_RR = true;
++ }
++ }
++ }
++
++ // now process any files being uploaded
++ $found = 0;
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ if (!@empty($_FILES["file"]["name"][$i])) {
++ $found = 1;
++ break;
++ }
++ }
++ if ($found) {
++ $files = array();
++ for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
++ $filename = @$_FILES["file"]["name"][$i];
++ if (empty($filename)) {
++ continue;
++ }
++ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
++ if (empty($blob)) {
++ // error reading a file
++ self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
++ continue;
++ }
++ $files[] = array(
++ "filename" => $filename,
++ "type" => $_FILES['file']['type'][$i],
++ "blob" => $blob
++ );
++ }
++ if (count($files) > 0) {
++ $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
++ foreach ($files as $file) {
++ Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
++ }
++ }
++ }
++ // need to associate any emails ?
++ if (!empty($data['attached_emails'])) {
++ $items = explode(",", $data['attached_emails']);
++ Support::associate($usr_id, $issue_id, $items);
++ }
++ // need to notify any emails being converted into issues ?
++ if (@count($data['notify_senders']) > 0) {
++ $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
++ } else {
++ $recipients = array();
++ }
++ // need to process any custom fields ?
++ if (@count($data['custom_fields']) > 0) {
++ foreach ($data['custom_fields'] as $fld_id => $value) {
++ Custom_Field::associateIssue($issue_id, $fld_id, $value);
++ }
++ }
++ // also send a special confirmation email to the customer contact
++ if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
++ // also need to pass the list of sender emails already notified,
++ // so we can avoid notifying the same person again
++ $contact_email = User::getEmailByContactID($data['contact']);
++ if (@!in_array($contact_email, $recipients)) {
++ Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
++ }
++ }
++
++ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($prj_id, $issue_id);
++
++ return $issue_id;
++ }
++
++ /**
++ * Insert issue to database.
++ *
++ * @param integer $prj_id The project ID
++ * @param integer $usr_id The user responsible for this action
++ * @param array $data of issue to be inserted
++ * @return integer The new issue ID
++ */
++ private function insertIssue($prj_id, $usr_id, $data)
++ {
++
++ // XXX missing_fields never used
++ $missing_fields = array();
++ if ($data['category'] == -1) {
++ $missing_fields[] = 'Category';
++ }
++ if ($data['priority'] == -1) {
++ $missing_fields[] = 'Priority';
++ }
++
++ // if there is no reporter set, use the system user
++ if (empty($data['reporter'])) {
++ $data['reporter'] = APP_SYSTEM_USER_ID;
++ }
++
++ if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
++ $data['estimated_dev_time'] = 0;
++ }
++
++ // add new issue
++ $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
++ "SET ".
++ "iss_prj_id=" . $prj_id . ",";
++ if (!empty($data['group'])) {
++ $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
++ }
++ if (!empty($data['category'])) {
++ $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
++ }
++ if (!empty($data['release'])) {
++ $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
++ }
++ if (!empty($data['priority'])) {
++ $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
++ }
++
++ $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
++
++ $initial_status = Project::getInitialStatus($prj_id);
++ if (!empty($initial_status)) {
++ $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
++ }
++
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $stmt .= "
++ iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
++ if (!empty($data['contact'])) {
++ $stmt .= "
++ iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
++ }
++ $stmt .= "
++ iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
++ iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
++ iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
++ iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
++ iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
++ iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
++ }
++
++ $stmt .= "
++ iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='created',
++ iss_summary='" . Misc::escapeString($data['summary']) . "',
++ iss_description='" . Misc::escapeString($data['description']) . "',
++ iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
++ if (!empty($data['contact'])) {
++ $stmt .= "
++ iss_private=" . Misc::escapeInteger($data['private']) . " ,";
++ }
++ $stmt .= "
++ iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
++ ";
++
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++
++ $issue_id = DB_Helper::get_last_insert_id();
++ return $issue_id;
++ }
++
++
++ /**
++ * Method used to get a specific parameter in the issue listing cookie.
++ *
++ * @access public
++ * @param string $name The name of the parameter
++ * @return mixed The value of the specified parameter
++ */
++ function getParam($name)
++ {
++ $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
++
++ if (isset($_GET[$name])) {
++ return $_GET[$name];
++ } elseif (isset($_POST[$name])) {
++ return $_POST[$name];
++ } elseif (isset($profile[$name])) {
++ return $profile[$name];
++ } else {
++ return "";
++ }
++ }
++
++
++ /**
++ * Method used to save the current search parameters in a cookie.
++ *
++ * @access public
++ * @return array The search parameters
++ */
++ function saveSearchParams()
++ {
++ $sort_by = self::getParam('sort_by');
++ $sort_order = self::getParam('sort_order');
++ $rows = self::getParam('rows');
++ $hide_closed = self::getParam('hide_closed');
++ if ($hide_closed === '') {
++ $hide_closed = 1;
++ }
++ $search_type = self::getParam('search_type');
++ if (empty($search_type)) {
++ $search_type = 'all_text';
++ }
++ $custom_field = self::getParam('custom_field');
++ if (is_string($custom_field)) {
++ $custom_field = unserialize(urldecode($custom_field));
++ }
++ $cookie = array(
++ 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
++ 'pagerRow' => self::getParam('pagerRow'),
++ 'hide_closed' => $hide_closed,
++ "sort_by" => $sort_by ? $sort_by : "pri_rank",
++ "sort_order" => $sort_order ? $sort_order : "ASC",
++ // quick filter form
++ 'keywords' => self::getParam('keywords'),
++ 'search_type' => $search_type,
++ 'users' => self::getParam('users'),
++ 'status' => self::getParam('status'),
++ 'priority' => self::getParam('priority'),
++ 'category' => self::getParam('category'),
++ 'customer_email' => self::getParam('customer_email'),
++ // advanced search form
++ 'show_authorized_issues' => self::getParam('show_authorized_issues'),
++ 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
++ 'reporter' => self::getParam('reporter'),
++ // other fields
++ 'release' => self::getParam('release'),
++ // custom fields
++ 'custom_field' => $custom_field
++ );
++ // now do some magic to properly format the date fields
++ $date_fields = array(
++ 'created_date',
++ 'updated_date',
++ 'last_response_date',
++ 'first_response_date',
++ 'closed_date'
++ );
++ foreach ($date_fields as $field_name) {
++ $field = self::getParam($field_name);
++ if (empty($field)) {
++ continue;
++ }
++ if (@$field['filter_type'] == 'in_past') {
++ @$cookie[$field_name] = array(
++ 'filter_type' => 'in_past',
++ 'time_period' => $field['time_period']
++ );
++ } else {
++ $end_field_name = $field_name . '_end';
++ $end_field = self::getParam($end_field_name);
++ @$cookie[$field_name] = array(
++ 'past_hour' => $field['past_hour'],
++ 'Year' => $field['Year'],
++ 'Month' => $field['Month'],
++ 'Day' => $field['Day'],
++ 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
++ 'filter_type' => $field['filter_type'],
++ 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
++ );
++ @$cookie[$end_field_name] = array(
++ 'Year' => $end_field['Year'],
++ 'Month' => $end_field['Month'],
++ 'Day' => $end_field['Day']
++ );
++ }
++ }
++ Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
++ return $cookie;
++ }
++
++
++ /**
++ * Method used to get the current sorting options used in the grid layout
++ * of the issue listing page.
++ *
++ * @access public
++ * @param array $options The current search parameters
++ * @return array The sorting options
++ */
++ function getSortingInfo($options)
++ {
++
++ $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
++
++ // default order for last action date, priority should be descending
++ // for textual fields, like summary, ascending is reasonable
++ $fields = array(
++ "pri_rank" => "desc",
++ "iss_id" => "desc",
++ "iss_customer_id" => "desc",
++ "prc_title" => "asc",
++ "sta_rank" => "asc",
++ "iss_created_date" => "desc",
++ "iss_summary" => "asc",
++ "last_action_date" => "desc",
++ "usr_full_name" => "asc",
++ "iss_expected_resolution_date" => "desc",
++ "pre_title" => "asc",
++ "assigned" => "asc",
++ );
++
++ foreach ($custom_fields as $fld_id => $fld_name) {
++ $fields['custom_field_' . $fld_id] = "desc";
++ }
++
++ $sortfields = array_combine(array_keys($fields), array_keys($fields));
++ $sortfields["pre_title"] = "pre_scheduled_date";
++ $sortfields["assigned"] = "isu_usr_id";
++
++ $items = array(
++ "links" => array(),
++ "images" => array()
++ );
++ foreach ($sortfields as $field => $sortfield) {
++ $sort_order = $fields[$field];
++ if ($options["sort_by"] == $sortfield) {
++ $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
++ if (strtolower($options["sort_order"]) == "asc") {
++ $sort_order = "desc";
++ } else {
++ $sort_order = "asc";
++ }
++ }
++ $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
++ }
++ return $items;
++ }
++
++
++ /**
++ * Returns the list of action date fields appropriate for the
++ * current user ID.
++ *
++ * @access public
++ * @return array The list of action date fields
++ */
++ function getLastActionFields()
++ {
++ $last_action_fields = array(
++ "iss_last_public_action_date"
++ );
++ if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
++ $last_action_fields[] = "iss_last_internal_action_date";
++ }
++ if (count($last_action_fields) > 1) {
++ return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
++ } else {
++ return $last_action_fields[0] . " AS last_action_date";
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues to be displayed in the grid layout.
++ *
++ * @access public
++ * @param integer $prj_id The current project ID
++ * @param array $options The search parameters
++ * @param integer $current_row The current page number
++ * @param integer $max The maximum number of rows per page
++ * @return array The list of issues to be displayed
++ */
++ function getListing($prj_id, $options, $current_row = 0, $max = 5)
++ {
++ if (strtoupper($max) == "ALL") {
++ $max = 9999999;
++ }
++ $start = $current_row * $max;
++ // get the current user's role
++ $usr_id = Auth::getUserID();
++ $role_id = User::getRoleByUser($usr_id, $prj_id);
++
++ // get any custom fields that should be displayed
++ $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
++
++ $stmt = "SELECT
++ iss_id,
++ iss_grp_id,
++ iss_prj_id,
++ iss_sta_id,
++ iss_customer_id,
++ iss_customer_contract_id,
++ iss_created_date,
++ iss_updated_date,
++ iss_last_response_date,
++ iss_closed_date,
++ iss_last_customer_action_date,
++ iss_usr_id,
++ iss_summary,
++ pri_title,
++ prc_title,
++ sta_title,
++ sta_color status_color,
++ sta_id,
++ iqu_status,
++ grp_name `group`,
++ pre_title,
++ iss_last_public_action_date,
++ iss_last_public_action_type,
++ iss_last_internal_action_date,
++ iss_last_internal_action_type,
++ " . self::getLastActionFields() . ",
++ IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
++ iss_private,
++ usr_full_name,
++ iss_percent_complete,
++ iss_dev_time,
++ iss_expected_resolution_date
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
++ // join custom fields if we are searching by custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
++ }
++ } else {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
++ }
++ }
++ }
++ $stmt .= ")";
++ // check for the custom fields we want to sort by
++ if (strstr($options['sort_by'], 'custom_field') !== false) {
++ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
++ $stmt .= "\n LEFT JOIN \n" .
++ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
++ ON
++ (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
++ }
++ if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id";
++ }
++ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
++ ON
++ iur_iss_id=iss_id";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
++ ON
++ sub_iss_id=iss_id";
++ }
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
++ ON
++ iss_grp_id=grp_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id=prc_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id = pre_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ ON
++ iss_id=iqu_iss_id AND
++ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
++ WHERE
++ iss_prj_id= " . Misc::escapeInteger($prj_id);
++ $stmt .= self::buildWhereClause($options);
++
++ if (strstr($options["sort_by"], 'custom_field') !== false) {
++ $fld_details = Custom_Field::getDetails($fld_id);
++ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
++ } else {
++ $sort_by = Misc::escapeString($options["sort_by"]);
++ }
++
++ $stmt .= "
++ GROUP BY
++ iss_id
++ ORDER BY
++ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
++ iss_id DESC";
++ $total_rows = Pager::getTotalRows($stmt);
++ $stmt .= "
++ LIMIT
++ " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array(
++ "list" => "",
++ "info" => ""
++ );
++ } else {
++ if (count($res) > 0) {
++ self::getAssignedUsersByIssues($res);
++ Time_Tracking::getTimeSpentByIssues($res);
++ // need to get the customer titles for all of these issues...
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ Customer::getCustomerTitlesByIssues($prj_id, $res);
++ Customer::getSupportLevelsByIssues($prj_id, $res);
++ }
++ self::formatLastActionDates($res);
++ self::getLastStatusChangeDates($prj_id, $res);
++ } elseif ($current_row > 0) {
++ // if there are no results, and the page is not the first page reset page to one and reload results
++ Auth::redirect("list.php?pagerRow=0&rows=$max");
++ }
++ $groups = Group::getAssocList($prj_id);
++ $categories = Category::getAssocList($prj_id);
++ $column_headings = self::getColumnHeadings($prj_id);
++ if (count($custom_fields) > 0) {
++ $column_headings = array_merge($column_headings,$custom_fields);
++ }
++ $csv[] = @implode("\t", $column_headings);
++ for ($i = 0; $i < count($res); $i++) {
++ $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
++ $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
++ $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
++ $fields = array(
++ $res[$i]['pri_title'],
++ $res[$i]['iss_id'],
++ $res[$i]['usr_full_name'],
++ );
++ // hide the group column from the output if no
++ // groups are available in the database
++ if (count($groups) > 0) {
++ $fields[] = $res[$i]['group'];
++ }
++ $fields[] = $res[$i]['assigned_users'];
++ $fields[] = $res[$i]['time_spent'];
++ // hide the category column from the output if no
++ // categories are available in the database
++ if (count($categories) > 0) {
++ $fields[] = $res[$i]['prc_title'];
++ }
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $fields[] = @$res[$i]['customer_title'];
++ // check if current user is acustomer and has a per incident contract.
++ // if so, check if issue is redeemed.
++ if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
++ if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
++ (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
++ $res[$i]['redeemed'] = true;
++ }
++ }
++ }
++ $fields[] = $res[$i]['sta_title'];
++ $fields[] = $res[$i]["status_change_date"];
++ $fields[] = $res[$i]["last_action_date"];
++ $fields[] = $res[$i]['iss_dev_time'];
++ $fields[] = $res[$i]['iss_summary'];
++ $fields[] = $res[$i]['iss_expected_resolution_date'];
++
++ if (count($custom_fields) > 0) {
++ $res[$i]['custom_field'] = array();
++ $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
++ foreach ($custom_field_values as $this_field) {
++ if (!empty($custom_fields[$this_field['fld_id']])) {
++ $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
++ $fields[] = $this_field['value'];
++ }
++ }
++ }
++
++ $csv[] = @implode("\t", $fields);
++ }
++ $total_pages = ceil($total_rows / $max);
++ $last_page = $total_pages - 1;
++ return array(
++ "list" => $res,
++ "info" => array(
++ "current_page" => $current_row,
++ "start_offset" => $start,
++ "end_offset" => $start + count($res),
++ "total_rows" => $total_rows,
++ "total_pages" => $total_pages,
++ "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
++ "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
++ "last_page" => $last_page,
++ "custom_fields" => $custom_fields
++ ),
++ "csv" => @implode("\n", $csv)
++ );
++ }
++ }
++
++
++ /**
++ * Processes a result set to format the "Last Action Date" column.
++ *
++ * @access public
++ * @param array $result The result set
++ */
++ function formatLastActionDates(&$result)
++ {
++ for ($i = 0; $i < count($result); $i++) {
++ if (($result[$i]['action_type'] == "internal") &&
++ (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
++ $label = $result[$i]["iss_last_internal_action_type"];
++ $last_date = $result[$i]["iss_last_internal_action_date"];
++ } else {
++ $label = $result[$i]["iss_last_public_action_type"];
++ $last_date = $result[$i]["iss_last_public_action_date"];
++ }
++ $date = new Date($last_date);
++ $current = new Date(Date_Helper::getCurrentDateGMT());
++ $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
++ Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
++ }
++ }
++
++
++ /**
++ * Retrieves the last status change date for the given issue.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @param array $result The associative array of data
++ * @see self::getListing()
++ */
++ function getLastStatusChangeDates($prj_id, &$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_sta_id"];
++ }
++ if (count($ids) == 0) {
++ return false;
++ }
++ $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
++ for ($i = 0; $i < count($result); $i++) {
++ if (empty($result[$i]['iss_sta_id'])) {
++ $result[$i]['status_change_date'] = '';
++ } else {
++ list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
++ if ((empty($label)) || (empty($date_field_name))) {
++ $result[$i]['status_change_date'] = '';
++ continue;
++ }
++ $current = new Date(Date_Helper::getCurrentDateGMT());
++ $desc = "$label: %s ago";
++ $target_date = $result[$i][$date_field_name];
++ if (empty($target_date)) {
++ $result[$i]['status_change_date'] = '';
++ continue;
++ }
++ $date = new Date($target_date);
++ $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues to be displayed in the grid layout.
++ *
++ * @access public
++ * @param array $options The search parameters
++ * @return string The where clause
++ */
++ function buildWhereClause($options)
++ {
++ $usr_id = Auth::getUserID();
++ $prj_id = Auth::getCurrentProject();
++ $role_id = User::getRoleByUser($usr_id, $prj_id);
++
++ $stmt = ' AND iss_usr_id = usr_id';
++ if ($role_id == User::getRoleID('Customer')) {
++ $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
++ } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
++ $stmt .= " AND (
++ iss_usr_id = $usr_id OR
++ iur_usr_id = $usr_id
++ )";
++ }
++
++ if (!empty($options["users"])) {
++ $stmt .= " AND (\n";
++ if (stristr($options["users"], "grp") !== false) {
++ $chunks = explode(":", $options["users"]);
++ $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
++ } else {
++ if ($options['users'] == '-1') {
++ $stmt .= 'isu_usr_id IS NULL';
++ } elseif ($options['users'] == '-2') {
++ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
++ } elseif ($options['users'] == '-3') {
++ $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
++ } elseif ($options['users'] == '-4') {
++ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
++ } else {
++ $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
++ }
++ }
++ $stmt .= ')';
++ }
++ if (!empty($options["reporter"])) {
++ $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
++ }
++ if (!empty($options["show_authorized_issues"])) {
++ $stmt .= " AND (iur_usr_id=$usr_id)";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= " AND (sub_usr_id=$usr_id)";
++ }
++ if (!empty($options["keywords"])) {
++ $stmt .= " AND (\n";
++ if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
++ $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
++ } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
++ // check if the user is trying to search by customer email
++ $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
++ if (count($customer_ids) > 0) {
++ $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
++ } else {
++ // no results, kill query
++ $stmt .= " iss_customer_id = -1";
++ }
++ } else {
++ $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
++ $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
++ }
++ $stmt .= "\n) ";
++ }
++ if (!empty($options["priority"])) {
++ $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
++ }
++ if (!empty($options["status"])) {
++ $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
++ }
++ if (!empty($options["category"])) {
++ if (!is_array($options['category'])) {
++ $options['category'] = array($options['category']);
++ }
++ $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
++ }
++ if (!empty($options["hide_closed"])) {
++ $stmt .= " AND sta_is_closed=0";
++ }
++ if (!empty($options['release'])) {
++ $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
++ }
++ // now for the date fields
++ $date_fields = array(
++ 'created_date',
++ 'updated_date',
++ 'last_response_date',
++ 'first_response_date',
++ 'closed_date'
++ );
++ foreach ($date_fields as $field_name) {
++ if (!empty($options[$field_name])) {
++ switch ($options[$field_name]['filter_type']) {
++ case 'greater':
++ $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
++ break;
++ case 'less':
++ $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
++ break;
++ case 'between':
++ $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
++ break;
++ case 'null':
++ $stmt .= " AND iss_$field_name IS NULL";
++ break;
++ case 'in_past':
++ if (strlen($options[$field_name]['time_period']) == 0) {
++ $options[$field_name]['time_period'] = 0;
++ }
++ $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
++ Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
++ break;
++ }
++ }
++ }
++ // custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
++ if (($field['fld_type'] == 'date') &&
++ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
++ }
++ } elseif ($field['fld_type'] == 'date') {
++ if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
++ continue;
++ }
++ $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
++ cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
++ } else if ($field['fld_type'] == 'integer') {
++ $value = $search_value['value'];
++ switch ($search_value['filter_type']) {
++ case 'ge': $cmp = '>='; break;
++ case 'le': $cmp = '<='; break;
++ case 'gt': $cmp = '>'; break;
++ case 'lt': $cmp = '<'; break;
++ default: $cmp = '='; break;
++ }
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
++ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
++ } else {
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
++ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
++ if ($field['fld_type'] == 'combo') {
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
++ } else {
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
++ }
++ $stmt .= ')';
++ }
++ }
++ }
++ // clear cached full-text values if we are not searching fulltext anymore
++ if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
++ Session::set('fulltext_string', '');
++ Session::set('fulltext_issues', '');
++ }
++ return $stmt;
++ }
++
++
++ /**
++ * Method used to get the previous and next issues that are available
++ * according to the current search parameters.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param array $options The search parameters
++ * @return array The list of issues
++ */
++ function getSides($issue_id, $options)
++ {
++ $usr_id = Auth::getUserID();
++ $role_id = Auth::getCurrentRole();
++
++ $stmt = "SELECT
++ iss_id,
++ " . self::getLastActionFields() . "
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
++ // join custom fields if we are searching by custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ if (($field['fld_type'] == 'date') &&
++ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
++ }
++ } else {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
++ }
++ }
++ }
++ $stmt .= ")";
++ // check for the custom fields we want to sort by
++ if (strstr($options['sort_by'], 'custom_field') !== false) {
++ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
++ $stmt .= "\n LEFT JOIN \n" .
++ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
++ ON
++ (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
++ }
++ if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id";
++ }
++ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
++ ON
++ iur_iss_id=iss_id";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
++ ON
++ sub_iss_id=iss_id";
++ }
++ if (@$options["sort_by"] == "pre_scheduled_date") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id = pre_id";
++ }
++ if (@$options['sort_by'] == 'prc_title') {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id = prc_id";
++ }
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ $stmt .= self::buildWhereClause($options);
++ if (strstr($options["sort_by"], 'custom_field') !== false) {
++ $fld_details = Custom_Field::getDetails($fld_id);
++ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
++ } else {
++ $sort_by = Misc::escapeString($options["sort_by"]);
++ }
++ $stmt .= "
++ GROUP BY
++ iss_id
++ ORDER BY
++ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
++ iss_id DESC";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ // COMPAT: the next line requires PHP >= 4.0.5
++ $index = array_search($issue_id, $res);
++ if (!empty($res[$index+1])) {
++ $next = $res[$index+1];
++ }
++ if (!empty($res[$index-1])) {
++ $previous = $res[$index-1];
++ }
++ return array(
++ "next" => @$next,
++ "previous" => @$previous
++ );
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of user IDs assigned to a specific
++ * issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of user IDs
++ */
++ function getAssignedUserIDs($issue_id)
++ {
++ $stmt = "SELECT
++ usr_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to see if a user is assigned to an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id An integer containg the ID of the user.
++ * @return boolean true if the user(s) are assigned to the issue.
++ */
++ function isAssignedToUser($issue_id, $usr_id)
++ {
++ $assigned_users = self::getAssignedUserIDs($issue_id);
++ if (in_array($usr_id, $assigned_users)) {
++ return true;
++ } else {
++ return false;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of reporters associated with a given
++ * list of issues.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getReportersByIssues(&$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ $ids = implode(", ", $ids);
++ $stmt = "SELECT
++ iss_id,
++ CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ iss_usr_id=usr_id AND
++ iss_id IN ($ids)";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ // now populate the $result variable again
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of assigned users by a list
++ * of issues. This was originally created to optimize the issue
++ * listing page.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getAssignedUsersByIssues(&$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ if (count($ids) < 1) {
++ return;
++ }
++ $ids = implode(", ", $ids);
++ $stmt = "SELECT
++ isu_iss_id,
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_usr_id=usr_id AND
++ isu_iss_id IN ($ids)";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ $t = array();
++ for ($i = 0; $i < count($res); $i++) {
++ if (!empty($t[$res[$i]['isu_iss_id']])) {
++ $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
++ } else {
++ $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
++ }
++ }
++ // now populate the $result variable again
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to add the issue description to a list of issues.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getDescriptionByIssues(&$result)
++ {
++ if (count($result) == 0) {
++ return;
++ }
++
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ $ids = implode(", ", $ids);
++
++ $stmt = "SELECT
++ iss_id,
++ iss_description
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id in ($ids)";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of users (the full names) assigned to a
++ * specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of users
++ */
++ function getAssignedUsers($issue_id)
++ {
++ $stmt = "SELECT
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the details for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $force_refresh If the cache should not be used.
++ * @return array The details for the specified issue
++ */
++ function getDetails($issue_id, $force_refresh = false)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (empty($issue_id)) {
++ return '';
++ }
++
++ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
++ prj_title,
++ prc_title,
++ pre_title,
++ pri_title,
++ sta_title,
++ sta_abbreviation,
++ sta_color status_color,
++ sta_is_closed
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
++ )
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id=prc_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id=pre_id
++ WHERE
++ iss_id=$issue_id AND
++ iss_prj_id=prj_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ if (empty($res)) {
++ return "";
++ } else {
++ $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
++ // get customer information, if any
++ if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
++ $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
++ $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
++ $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
++ $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
++ $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
++ $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
++ if (empty($res['iss_first_response_date'])) {
++ $first_response_deadline = $created_date_ts + $max_first_response_time;
++ if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
++ $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
++ } else {
++ $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
++ }
++ }
++ }
++ $res['iss_original_description'] = $res["iss_description"];
++ if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
++ $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
++ $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
++ }
++ $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
++ $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
++ $res['iss_created_date_ts'] = $created_date_ts;
++ $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
++ list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
++ $temp = self::getAssignedUsersStatus($res["iss_id"]);
++ $res["has_inactive_users"] = 0;
++ $res["assigned_users"] = array();
++ $res["assigned_inactive_users"] = array();
++ foreach ($temp as $usr_id => $usr_status) {
++ if (!User::isActiveStatus($usr_status)) {
++ $res["assigned_inactive_users"][] = $usr_id;
++ $res["has_inactive_users"] = 1;
++ } else {
++ $res["assigned_users"][] = $usr_id;
++ }
++ }
++ if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
++ $res["is_current_user_assigned"] = 1;
++ } else {
++ $res["is_current_user_assigned"] = 0;
++ }
++ $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
++ $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
++ $res["reporter"] = User::getFullName($res["iss_usr_id"]);
++ if (empty($res["iss_updated_date"])) {
++ $res["iss_updated_date"] = 'not updated yet';
++ } else {
++ $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
++ }
++ $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
++ if (Release::isAssignable($res["iss_pre_id"])) {
++ $release = Release::getDetails($res["iss_pre_id"]);
++ $res["pre_title"] = $release["pre_title"];
++ $res["pre_status"] = $release["pre_status"];
++ }
++ // need to return the list of issues that are duplicates of this one
++ $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
++ $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
++ // also get the issue title of the duplicated issue
++ if (!empty($res['iss_duplicated_iss_id'])) {
++ $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
++ }
++
++ // get group information
++ if (!empty($res["iss_grp_id"])) {
++ $res["group"] = Group::getDetails($res["iss_grp_id"]);
++ }
++
++ // get quarantine issue
++ $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
++
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get some simple details about the given duplicated issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The duplicated issue details
++ */
++ function getDuplicatedDetails($issue_id)
++ {
++ $stmt = "SELECT
++ iss_summary title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_sta_id=sta_id AND
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to bulk update a list of issues
++ *
++ * @access public
++ * @return boolean
++ */
++ function bulkUpdate()
++ {
++ // check if user performing this chance has the proper role
++ if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
++ return -1;
++ }
++
++ $items = Misc::escapeInteger($_POST['item']);
++ $new_status_id = Misc::escapeInteger($_POST['status']);
++ $new_release_id = Misc::escapeInteger(@$_POST['release']);
++ $new_priority_id = Misc::escapeInteger($_POST['priority']);
++ $new_category_id = Misc::escapeInteger($_POST['category']);
++
++ for ($i = 0; $i < count($items); $i++) {
++ if (!self::canAccess($items[$i], Auth::getUserID())) {
++ continue;
++ } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
++ // make sure issue is not in another project
++ continue;
++ }
++
++ $updated_fields = array();
++
++ // update assignment
++ if (count(@$_POST['users']) > 0) {
++ $users = Misc::escapeInteger($_POST['users']);
++ // get who this issue is currently assigned too
++ $stmt = "SELECT
++ isu_usr_id,
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_usr_id = usr_id AND
++ isu_iss_id = " . $items[$i];
++ $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($current_assignees)) {
++ Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ foreach ($current_assignees as $usr_id => $usr_name) {
++ if (!in_array($usr_id, $users)) {
++ self::deleteUserAssociation($items[$i], $usr_id, false);
++ }
++ }
++ $new_user_names = array();
++ $new_assignees = array();
++ foreach ($users as $usr_id) {
++ $new_user_names[$usr_id] = User::getFullName($usr_id);
++
++ // check if the issue is already assigned to this person
++ $stmt = "SELECT
++ COUNT(*) AS total
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id=" . $items[$i] . " AND
++ isu_usr_id=" . $usr_id;
++ $total = DB_Helper::getInstance()->getOne($stmt);
++ if ($total > 0) {
++ continue;
++ } else {
++ $new_assignees[] = $usr_id;
++ // add the assignment
++ self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
++ Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
++ Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
++ }
++ }
++ Notification::notifyNewAssignment($new_assignees, $items[$i]);
++ $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
++ }
++
++ // update status
++ if (!empty($new_status_id)) {
++ $old_status_id = self::getStatusID($items[$i]);
++ $res = self::setStatus($items[$i], $new_status_id, false);
++ if ($res == 1) {
++ $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
++ }
++ }
++
++ // update release
++ if (!empty($new_release_id)) {
++ $old_release_id = self::getRelease($items[$i]);
++ $res = self::setRelease($items[$i], $new_release_id);
++ if ($res == 1) {
++ $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
++ }
++ }
++
++ // update priority
++ if (!empty($new_priority_id)) {
++ $old_priority_id = self::getPriority($items[$i]);
++ $res = self::setPriority($items[$i], $new_priority_id);
++ if ($res == 1) {
++ $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
++ }
++ }
++
++ // update category
++ if (!empty($new_category_id)) {
++ $old_category_id = self::getCategory($items[$i]);
++ $res = self::setCategory($items[$i], $new_category_id);
++ if ($res == 1) {
++ $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
++ }
++ }
++
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $k = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($k > 0) {
++ $changes .= "; ";
++ }
++ $changes .= "$key: $value";
++ $k++;
++ }
++ History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
++ }
++
++ // close if request
++ if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
++ self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
++ }
++ }
++ return true;
++ }
++
++
++ /**
++ * Method used to set the initial impact analysis for a specific issue
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setImpactAnalysis($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='update',
++ iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
++ iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add the impact analysis to the history of the issue
++ $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs that area available in the
++ * system.
++ *
++ * @access public
++ * @param string $extra_condition An extra condition in the WHERE clause
++ * @return array The list of issue IDs
++ */
++ function getColList($extra_condition = NULL)
++ {
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ if (!empty($extra_condition)) {
++ $stmt .= " AND $extra_condition ";
++ }
++ $stmt .= "
++ ORDER BY
++ iss_id DESC";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs and their respective
++ * titles.
++ *
++ * @access public
++ * @param string $extra_condition An extra condition in the WHERE clause
++ * @return array The list of issues
++ */
++ function getAssocList($extra_condition = NULL)
++ {
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ if (!empty($extra_condition)) {
++ $stmt .= " AND $extra_condition ";
++ }
++ $stmt .= "
++ ORDER BY
++ iss_id ASC";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues associated to a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of associated issues
++ */
++ function getAssociatedIssues($issue_id)
++ {
++ $issues = self::getAssociatedIssuesDetails($issue_id);
++ $associated = array();
++ for ($i = 0; $i < count($issues); $i++) {
++ $associated[] = $issues[$i]['associated_issue'];
++ }
++ return $associated;
++ }
++
++
++ /**
++ * Method used to get the list of issues associated details to a
++ * specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of associated issues
++ */
++ function getAssociatedIssuesDetails($issue_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ isa_associated_id associated_issue,
++ iss_summary associated_title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ isa_associated_id=iss_id AND
++ iss_sta_id=sta_id AND
++ isa_issue_id=$issue_id";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to check whether an issue was already closed or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return boolean
++ */
++ function isClosed($issue_id)
++ {
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ iss_sta_id=sta_id AND
++ sta_is_closed=1";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Returns a simple list of issues that are currently set to some
++ * form of quarantine. This is mainly used by the IRC interface.
++ *
++ * @access public
++ * @return array List of quarantined issues
++ */
++ function getQuarantinedIssueList()
++ {
++ // XXX: would be nice to restrict the result list to only one project
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id=iss_id AND
++ iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
++ iqu_expiration IS NOT NULL";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ self::getAssignedUsersByIssues($res);
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the status of a quarantine.
++ *
++ * @param integer $issue_id The issue ID
++ * @return integer Indicates what the current state of quarantine is.
++ */
++ function getQuarantineInfo($issue_id)
++ {
++ $stmt = "SELECT
++ iqu_status,
++ iqu_expiration
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
++ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
++ iqu_expiration IS NULL)";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ if (!empty($res["iqu_expiration"])) {
++ $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
++ $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
++ }
++ return $res;
++ }
++ }
++
++
++ /**
++ * Sets the quarantine status. Optionally an expiration date can be set
++ * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $status The quarantine status
++ * @param string $expiration The expiration date of quarantine (default empty)
++ */
++ function setQuarantine($issue_id, $status, $expiration = '')
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $status = Misc::escapeInteger($status);
++
++ // see if there is an existing record
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ if ($res > 0) {
++ // update
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ SET
++ iqu_status = $status";
++ if (!empty($expiration)) {
++ $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
++ }
++ $stmt .= "\nWHERE
++ iqu_iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add history entry about this change taking place
++ if ($status == 0) {
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
++ "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
++ }
++ }
++ } else {
++ // insert
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ (
++ iqu_iss_id,
++ iqu_status";
++ if (!empty($expiration)) {
++ $stmt .= ",\niqu_expiration\n";
++ }
++ $stmt .= ") VALUES (
++ $issue_id,
++ $status";
++ if (!empty($expiration)) {
++ $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
++ }
++ $stmt .= ")";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ }
++ return 1;
++ }
++
++
++ /**
++ * Sets the group of the issue.
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $group_id The ID of the group
++ * @return integer 1 if successful, -1 or -2 otherwise
++ */
++ function setGroup($issue_id, $group_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $group_id = Misc::escapeInteger($group_id);
++
++ $current = self::getDetails($issue_id);
++ if ($current["iss_grp_id"] == $group_id) {
++ return -2;
++ }
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_grp_id = $group_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $current_user = Auth::getUserID();
++ if (empty($current_user)) {
++ $current_user = APP_SYSTEM_USER_ID;
++ }
++ History::add($issue_id, $current_user, History::getTypeID('group_changed'),
++ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
++ return 1;
++ }
++
++
++ /**
++ * Returns the group ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The associated group ID
++ */
++ function getGroupID($issue_id)
++ {
++ $stmt = "SELECT
++ iss_grp_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns an array of issues based on full text search results.
++ *
++ * @param array $options An array of search options
++ * @return array An array of issue IDS
++ */
++ function getFullTextIssues($options)
++ {
++ // check if a list of issues for this full text search is already cached
++ $fulltext_string = Session::get('fulltext_string');
++ if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
++ return Session::get('fulltext_issues');
++ }
++
++ // no pre-existing list, generate them
++ $stmt = "(SELECT
++ DISTINCT(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(not_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
++ WHERE
++ MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(ttr_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
++ WHERE
++ MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(phs_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
++ WHERE
++ MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(sup_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
++ WHERE
++ sup_id = seb_sup_id AND
++ MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ )";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array(-1);
++ } else {
++ $stmt = "SELECT
++ DISTINCT(icf_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
++ WHERE
++ MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
++ $custom_res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($custom_res)) {
++ Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
++ return array(-1);
++ }
++ $issues = array_merge($res, $custom_res);
++ // we kill the query results on purpose to flag that no
++ // issues could be found with fulltext search
++ if (count($issues) < 1) {
++ $issues = array(-1);
++ }
++ Session::set('fulltext_string', $options['keywords']);
++ Session::set('fulltext_issues', $issues);
++ return $issues;
++ }
++ }
++
++
++ /**
++ * Method to determine if user can access a particular issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue.
++ * @param integer $usr_id The ID of the user
++ * @return boolean If the user can access the issue
++ */
++ function canAccess($issue_id, $usr_id)
++ {
++ static $access;
++
++ if (empty($issue_id)) {
++ return true;
++ }
++
++ if (isset($access[$issue_id . "-" . $usr_id])) {
++ return $access[$issue_id . "-" . $usr_id];
++ }
++
++ $details = self::getDetails($issue_id);
++ if (empty($details)) {
++ return true;
++ }
++ $usr_details = User::getDetails($usr_id);
++ $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
++ $prj_id = self::getProjectID($issue_id);
++
++
++ if (empty($usr_role)) {
++ // check if they are even allowed to access the project
++ $return = false;
++ } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
++ ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
++ // check customer permissions
++ $return = false;
++ } elseif ($details['iss_private'] == 1) {
++ // check if the issue is even private
++
++ // check role, reporter, assigment and group
++ if ($usr_role > User::getRoleID("Developer")) {
++ $return = true;
++ } elseif ($details['iss_usr_id'] == $usr_id) {
++ $return = true;
++ } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
++ $return = true;
++ } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
++ ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
++ $return = true;
++ } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
++ $return = true;
++ } else {
++ $return = false;
++ }
++ } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
++ ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
++ return false;
++ } else {
++ $return = true;
++ }
++
++ $access[$issue_id . "-" . $usr_id] = $return;
++ return $return;
++ }
++
++
++ /**
++ * Returns true if the specified issue is private, false otherwise
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return boolean If the issue is private or not
++ */
++ function isPrivate($issue_id)
++ {
++ static $returns;
++
++ if (!isset($returns[$issue_id])) {
++ $sql = "SELECT
++ iss_private
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return true;
++ } else {
++ if ($res == 1) {
++ $returns[$issue_id] = true;
++ } else {
++ $returns[$issue_id] = false;
++ }
++ }
++ }
++ return $returns[$issue_id];
++ }
++
++
++ /**
++ * Clears closed information from an issues.
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ */
++ function clearClosed($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_closed_date = null,
++ iss_res_id = null
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ }
++
++
++ /**
++ * Returns the message ID that should be used as the parent ID for all messages
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ */
++ function getRootMessageID($issue_id)
++ {
++ $sql = "SELECT
++ iss_root_message_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the issue ID of the issue with the specified root message ID, or false
++ * @access public
++ * @param string $msg_id The Message ID
++ * @return integer The ID of the issue
++ */
++ function getIssueByRootMessageID($msg_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$msg_id])) {
++ return $returns[$msg_id];
++ }
++ $sql = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ }
++ if (empty($res)) {
++ $returns[$msg_id] = false;
++ } else {
++ $returns[$msg_id] = $res;
++ }
++ return $returns[$msg_id];
++ }
++
++
++ /**
++ * Sets the assignees for the issue
++ *
++ * @param integer $issue_id
++ * @param array $assignees
++ */
++ function setAssignees($issue_id, $assignees)
++ {
++ if (!is_array($assignees)) {
++ $assignees = array();
++ }
++
++ // see if there is anything to change
++ $old_assignees = self::getAssignedUserIDs($issue_id);
++ if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
++ return;
++ }
++
++ $old_assignee_names = self::getAssignedUsers($issue_id);
++
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
++ // clear up the assignments for this issue, and then assign it to the current user
++ self::deleteUserAssociations($issue_id);
++ $assignee_names = array();
++ foreach ($assignees as $assignee) {
++ $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
++ if ($res == -1) {
++ return false;
++ }
++ $assignee_names[] = User::getFullName($assignee);
++ Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
++ }
++
++ Notification::notifyNewAssignment($assignees, $issue_id);
++
++ // save a history entry about this...
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
++ "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
++ }
++}
+--- eventum-2.2/lib/eventum/class.issue.php.~2~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/lib/eventum/class.issue.php.~2~ 2009-10-12 22:10:36.432519927 +0300
+@@ -0,0 +1,4748 @@
++<?php
++/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
++// +----------------------------------------------------------------------+
++// | Eventum - Issue Tracking System |
++// +----------------------------------------------------------------------+
++// | Copyright (c) 2003 - 2008 MySQL AB |
++// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
++// | |
++// | This program is free software; you can redistribute it and/or modify |
++// | it under the terms of the GNU General Public License as published by |
++// | the Free Software Foundation; either version 2 of the License, or |
++// | (at your option) any later version. |
++// | |
++// | This program is distributed in the hope that it will be useful, |
++// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
++// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
++// | GNU General Public License for more details. |
++// | |
++// | You should have received a copy of the GNU General Public License |
++// | along with this program; if not, write to: |
++// | |
++// | Free Software Foundation, Inc. |
++// | 59 Temple Place - Suite 330 |
++// | Boston, MA 02111-1307, USA. |
++// +----------------------------------------------------------------------+
++// | Authors: João Prado Maia <jpm@mysql.com> |
++// +----------------------------------------------------------------------+
++//
++
++
++/**
++ * Class designed to handle all business logic related to the issues in the
++ * system, such as adding or updating them or listing them in the grid mode.
++ *
++ * @author João Prado Maia <jpm@mysql.com>
++ * @version $Revision$
++ */
++
++class Issue
++{
++ /**
++ * Method used to check whether a given issue ID exists or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $check_project If we should check that this issue is in the current project
++ * @return boolean
++ */
++ function exists($issue_id, $check_project = true)
++ {
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ if ($check_project) {
++ $stmt .= " AND
++ iss_prj_id = " . Auth::getCurrentProject();
++ }
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the list of column heading titles for the
++ * CSV export functionality of the issue listing screen.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @return array The list of column heading titles
++ */
++ function getColumnHeadings($prj_id)
++ {
++ $headings = array(
++ 'Priority',
++ 'Issue ID',
++ 'Reporter',
++ );
++ // hide the group column from the output if no
++ // groups are available in the database
++ $groups = Group::getAssocList($prj_id);
++ if (count($groups) > 0) {
++ $headings[] = 'Group';
++ }
++ $headings[] = 'Assigned';
++ $headings[] = 'Time Spent';
++ // hide the category column from the output if no
++ // categories are available in the database
++ $categories = Category::getAssocList($prj_id);
++ if (count($categories) > 0) {
++ $headings[] = 'Category';
++ }
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $headings[] = 'Customer';
++ }
++ $headings[] = 'Status';
++ $headings[] = 'Status Change Date';
++ $headings[] = 'Last Action Date';
++ $headings[] = 'Est. Dev. TIme';
++ $headings[] = 'Summary';
++ $headings[] = 'Expected Resolution Date';
++ return $headings;
++ }
++
++
++ /**
++ * Method used to get the full list of date fields available to issues, to
++ * be used when customizing the issue listing screen in the 'last status
++ * change date' column.
++ *
++ * @access public
++ * @param boolean $display_customer_fields Whether to include any customer related fields or not
++ * @return array The list of available date fields
++ */
++ function getDateFieldsAssocList($display_customer_fields = FALSE)
++ {
++ $fields = array(
++ 'iss_created_date' => 'Created Date',
++ 'iss_updated_date' => 'Last Updated Date',
++ 'iss_last_response_date' => 'Last Response Date',
++ 'iss_closed_date' => 'Closed Date'
++ );
++ if ($display_customer_fields) {
++ $fields['iss_last_customer_action_date'] = 'Customer Action Date';
++ }
++ asort($fields);
++ return $fields;
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs and their respective
++ * titles associated to a given project.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @return array The list of issues
++ */
++ function getAssocListByProject($prj_id)
++ {
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Misc::escapeInteger($prj_id) . "
++ ORDER BY
++ iss_id ASC";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The status ID
++ */
++ function getStatusID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_sta_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Records the last customer action date for a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function recordLastCustomerAction($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='customer action'
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++
++
++ /**
++ * Returns the customer ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getCustomerID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the contract ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getContractID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_contract_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Sets the contract ID for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer The contract ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setContractID($issue_id, $contract_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $old_contract_id = self::getContractID($issue_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // log this
++ History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ /**
++ * Returns the customer ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getContactID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_contact_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the project associated to a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $force_refresh If the cache should not be used.
++ * @return integer The project ID
++ */
++ function getProjectID($issue_id, $force_refresh = false)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_prj_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to remotely assign a given issue to an user.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing the change
++ * @param boolean $assignee The user ID of the assignee
++ * @return integer The status ID
++ */
++ function remoteAssign($issue_id, $usr_id, $assignee)
++ {
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
++ // clear up the assignments for this issue, and then assign it to the current user
++ self::deleteUserAssociations($issue_id, $usr_id);
++ $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
++ if ($res != -1) {
++ // save a history entry about this...
++ History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
++ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
++ if ($assignee != $usr_id) {
++ Notification::notifyNewAssignment(array($assignee), $issue_id);
++ }
++ }
++ return $res;
++ }
++
++
++ /**
++ * Method used to set the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $status_id The new status ID
++ * @param boolean $notify If a notification should be sent about this change.
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setStatus($issue_id, $status_id, $notify = false)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $status_id = Misc::escapeInteger($status_id);
++
++ $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
++ if ($workflow !== true) {
++ return $workflow;
++ }
++
++ // check if the status is already set to the 'new' one
++ if (self::getStatusID($issue_id) == $status_id) {
++ return -1;
++ }
++
++ $old_status = self::getStatusID($issue_id);
++ $old_details = Status::getDetails($old_status);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_sta_id=$status_id,
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='update'
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ if (@$old_details['sta_is_closed'] == 1) {
++ $new_details = Status::getDetails($status_id);
++ if ($new_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++
++ if ($notify) {
++ Notification::notifyStatusChange($issue_id, $old_status, $status_id);
++ }
++
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to remotely set the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing this change
++ * @param integer $new_status The new status ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setRemoteStatus($issue_id, $usr_id, $new_status)
++ {
++ $sta_id = Status::getStatusID($new_status);
++
++ $res = self::setStatus($issue_id, $sta_id);
++ if ($res == 1) {
++ // record history entry
++ History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
++ }
++ return $res;
++ }
++
++
++ /**
++ * Method used to set the release of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $pre_id The ID of the release to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setRelease($issue_id, $pre_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $pre_id = Misc::escapeInteger($pre_id);
++
++ if ($pre_id != self::getRelease($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_pre_id = $pre_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current release of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The release
++ */
++ function getRelease($issue_id)
++ {
++ $sql = "SELECT
++ iss_pre_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to set the priority of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $pri_id The ID of the priority to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setPriority($issue_id, $pri_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $pri_id = Misc::escapeInteger($pri_id);
++
++ if ($pri_id != self::getPriority($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_pri_id = $pri_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current issue priority
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The priority
++ */
++ function getPriority($issue_id)
++ {
++ $sql = "SELECT
++ iss_pri_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to set the category of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $prc_id The ID of the category to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setCategory($issue_id, $prc_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $prc_id = Misc::escapeInteger($prc_id);
++
++ if ($prc_id != self::getPriority($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prc_id = $prc_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current issue category
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The category
++ */
++ function getCategory($issue_id)
++ {
++ $sql = "SELECT
++ iss_prc_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get all issues associated with a status that doesn't have
++ * the 'closed' context.
++ *
++ * @access public
++ * @param integer $prj_id The project ID to list issues from
++ * @param integer $usr_id The user ID of the user requesting this information
++ * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
++ * @param integer $status_id The status ID to be used to restrict results
++ * @return array The list of open issues
++ */
++ function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
++ {
++ $prj_id = Misc::escapeInteger($prj_id);
++ $status_id = Misc::escapeInteger($status_id);
++ $projects = Project::getRemoteAssocListByUser($usr_id);
++ if (@count($projects) == 0) {
++ return '';
++ }
++
++ $stmt = "SELECT
++ iss_id,
++ iss_summary,
++ sta_title
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ )
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id
++ WHERE ";
++ if (!empty($status_id)) {
++ $stmt .= " sta_id=$status_id AND ";
++ }
++ $stmt .= "
++ iss_prj_id=$prj_id AND
++ sta_id=iss_sta_id AND
++ sta_is_closed=0";
++ if ($show_all_issues == false) {
++ $stmt .= " AND
++ isu_usr_id=$usr_id";
++ }
++ $stmt .= "\nGROUP BY
++ iss_id";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ if (count($res) > 0) {
++ self::getAssignedUsersByIssues($res);
++ }
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to build the required parameters to simulate an email reply
++ * to the user who reported the issue, using the issue details like summary
++ * and description as email fields.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The email parameters
++ */
++ function getReplyDetails($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $stmt = "SELECT
++ iss_created_date,
++ usr_full_name AS reporter,
++ usr_email AS reporter_email,
++ iss_description AS description,
++ iss_summary AS sup_subject
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ iss_usr_id=usr_id AND
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
++ $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to record the last updated timestamp for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param string $type The type of update that was made (optional)
++ * @return boolean
++ */
++ function markAsUpdated($issue_id, $type = false)
++ {
++ $public = array("staff response", "customer action", "file uploaded", "user response");
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
++ if ($type != false) {
++ if (in_array($type, $public)) {
++ $field = "iss_last_public_action_";
++ } else {
++ $field = "iss_last_internal_action_";
++ }
++ $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
++ $field . "type ='" . Misc::escapeString($type) . "'\n";
++ }
++ $stmt .= "WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ // update last response dates if this is a staff response
++ if ($type == "staff response") {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ DB_Helper::getInstance()->query($stmt);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
++ WHERE
++ iss_first_response_date IS NULL AND
++ iss_id = " . Misc::escapeInteger($issue_id);
++ DB_Helper::getInstance()->query($stmt);
++ }
++
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to check whether a given issue has duplicates
++ * or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return boolean
++ */
++ function hasDuplicates($issue_id)
++ {
++ $stmt = "SELECT
++ COUNT(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to update the duplicated issues for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function updateDuplicates($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $ids = self::getDuplicateList($issue_id);
++ if ($ids == '') {
++ return -1;
++ }
++ $ids = @array_keys($ids);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
++ if (@$_POST["keep"] == "no") {
++ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
++ }
++ $stmt .= "
++ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
++ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
++ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
++ WHERE
++ iss_id IN (" . implode(", ", $ids) . ")";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // record the change
++ for ($i = 0; $i < count($ids); $i++) {
++ History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
++ "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
++ }
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to get a list of the duplicate issues for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of duplicates
++ */
++ function getDuplicateList($issue_id)
++ {
++ $res = self::getDuplicateDetailsList($issue_id);
++ if (@count($res) == 0) {
++ return '';
++ } else {
++ $list = array();
++ for ($i = 0; $i < count($res); $i++) {
++ $list[$res[$i]['issue_id']] = $res[$i]['title'];
++ }
++ return $list;
++ }
++ }
++
++
++ /**
++ * Method used to get a list of the duplicate issues (and their details)
++ * for a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of duplicates
++ */
++ function getDuplicateDetailsList($issue_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_id issue_id,
++ iss_summary title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_sta_id=sta_id AND
++ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to clear the duplicate status of an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function clearDuplicateStatus($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_duplicated_iss_id=NULL
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // record the change
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to mark an issue as a duplicate of an existing one.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function markAsDuplicate($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (!self::exists($issue_id)) {
++ return -1;
++ }
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if (!empty($_POST["comments"])) {
++ // add note with the comments of marking an issue as a duplicate of another one
++ $_POST['title'] = 'Issue duplication comments';
++ $_POST["note"] = $_POST["comments"];
++ Note::insert(Auth::getUserID(), $issue_id);
++ }
++ // record the change
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
++ "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ function isDuplicate($issue_id)
++ {
++ $sql = "SELECT
++ count(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id) . " AND
++ iss_duplicated_iss_id IS NULL";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ }
++ if ($res > 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to get an associative array of user ID => user
++ * status associated with a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of users
++ */
++ function getAssignedUsersStatus($issue_id)
++ {
++ $stmt = "SELECT
++ usr_id,
++ usr_status
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the summary associated with a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return string The issue summary
++ */
++ function getTitle($issue_id)
++ {
++ $stmt = "SELECT
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the issue ID associated with a specific summary.
++ *
++ * @access public
++ * @param string $summary The summary to look for
++ * @return integer The issue ID
++ */
++ function getIssueID($summary)
++ {
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_summary='" . Misc::escapeString($summary) . "'";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ if (empty($res)) {
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to add a new anonymous based issue in the system.
++ *
++ * @access public
++ * @return integer The new issue ID
++ */
++ function addAnonymousReport()
++ {
++ $options = Project::getAnonymousPostOptions($_POST["project"]);
++ $initial_status = Project::getInitialStatus($_POST["project"]);
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ (
++ iss_prj_id,
++ iss_prc_id,
++ iss_pre_id,
++ iss_pri_id,
++ iss_usr_id,";
++ if (!empty($initial_status)) {
++ $stmt .= "iss_sta_id,";
++ }
++ $stmt .= "
++ iss_created_date,
++ iss_last_public_action_date,
++ iss_last_public_action_type,
++ iss_summary,
++ iss_description,
++ iss_root_message_id
++ ) VALUES (
++ " . Misc::escapeInteger($_POST["project"]) . ",
++ " . $options["category"] . ",
++ 0,
++ " . $options["priority"] . ",
++ " . $options["reporter"] . ",";
++ if (!empty($initial_status)) {
++ $stmt .= "$initial_status,";
++ }
++ $stmt .= "
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ 'created',
++ '" . Misc::escapeString($_POST["summary"]) . "',
++ '" . Misc::escapeString($_POST["description"]) . "',
++ '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
++ )";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return $res;
++ } else {
++ $new_issue_id = DB_Helper::get_last_insert_id();
++ // log the creation of the issue
++ History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
++
++ // now process any files being uploaded
++ $found = 0;
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ if (!@empty($_FILES["file"]["name"][$i])) {
++ $found = 1;
++ break;
++ }
++ }
++ if ($found) {
++ $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ $filename = @$_FILES["file"]["name"][$i];
++ if (empty($filename)) {
++ continue;
++ }
++ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
++ if (!empty($blob)) {
++ Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
++ }
++ }
++ }
++ // need to process any custom fields ?
++ if (@count($_POST["custom_fields"]) > 0) {
++ foreach ($_POST["custom_fields"] as $fld_id => $value) {
++ Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
++ }
++ }
++
++ // now add the user/issue association
++ $assign = array();
++ $users = @$options["users"];
++ $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
++ for ($i = 0; $i < count($users); $i++) {
++ Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
++ $assign[] = $users[$i];
++ }
++
++ Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($_POST['project'], $new_issue_id);
++
++ return $new_issue_id;
++ }
++ }
++
++
++ /**
++ * Method used to remove all issues associated with a specific list of
++ * projects.
++ *
++ * @access public
++ * @param array $ids The list of projects to look for
++ * @return boolean
++ */
++ function removeByProjects($ids)
++ {
++ $items = @implode(", ", Misc::escapeInteger($ids));
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id IN ($items)";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if (count($res) > 0) {
++ self::deleteAssociations($res);
++ Attachment::removeByIssues($res);
++ SCM::removeByIssues($res);
++ Impact_Analysis::removeByIssues($res);
++ self::deleteUserAssociations($res);
++ Note::removeByIssues($res);
++ Time_Tracking::removeByIssues($res);
++ Notification::removeByIssues($res);
++ Custom_Field::removeByIssues($res);
++ Phone_Support::removeByIssues($res);
++ History::removeByIssues($res);
++ // now really delete the issues
++ $items = implode(", ", $res);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id IN ($items)";
++ DB_Helper::getInstance()->query($stmt);
++ }
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to close off an issue.
++ *
++ * @access public
++ * @param integer $usr_id The user ID
++ * @param integer $issue_id The issue ID
++ * @param bool $send_notification Whether to send a notification about this action or not
++ * @param integer $resolution_id The resolution ID
++ * @param integer $status_id The status ID
++ * @param string $reason The reason for closing this issue
++ * @param string $send_notification_to Who this notification should be sent too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
++ {
++ $usr_id = Misc::escapeInteger($usr_id);
++ $issue_id = Misc::escapeInteger($issue_id);
++ $resolution_id = Misc::escapeInteger($resolution_id);
++ $status_id = Misc::escapeInteger($status_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='closed',
++ iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
++ if (!empty($resolution_id)) {
++ $stmt .= "iss_res_id=$resolution_id,\n";
++ }
++ $stmt .= "iss_sta_id=$status_id
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ self::moveOrderForAllUsers($issue_id, 1000);
++ $prj_id = self::getProjectID($issue_id);
++
++ // record the change
++ History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
++
++ if ($send_notification_to == 'all') {
++
++ $from = User::getFromHeader($usr_id);
++ $message_id = User::getFromHeader($usr_id);
++ $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
++ '', '', 'Issue closed comments', $reason, '');
++
++ $structure = Mime_Helper::decode($full_email, true, false);
++
++ $email = array(
++ 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
++ 'issue_id' => $issue_id,
++ 'message_id' => $message_id,
++ 'date' => Date_Helper::getCurrentDateGMT(),
++ 'subject' => 'Issue closed comments',
++ 'from' => $from,
++ 'has_attachment'=> 0,
++ 'body' => $reason,
++ 'full_email' => $full_email,
++ 'headers' => $structure->headers
++ );
++ Support::insertEmail($email, $structure, $sup_id, true);
++ $ids = $sup_id;
++ } else {
++ // add note with the reason to close the issue
++ $_POST['title'] = 'Issue closed comments';
++ $_POST["note"] = $reason;
++ Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
++ $ids = false;
++ }
++
++ if ($send_notification) {
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ // send a special confirmation email when customer issues are closed
++ $stmt = "SELECT
++ iss_customer_contact_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
++ if (!empty($customer_contact_id)) {
++ Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
++ }
++ }
++ // send notifications for the issue being closed
++ Notification::notify($issue_id, 'closed', $ids);
++ }
++ Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to update the details of a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 or -2 otherwise
++ */
++ function update($issue_id)
++ {
++ global $errors;
++ $errors = array();
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $usr_id = Auth::getUserID();
++ $prj_id = self::getProjectID($issue_id);
++
++ $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
++ if ($workflow !== true) {
++ return $workflow;
++ }
++
++ // get all of the 'current' information of this issue
++ $current = self::getDetails($issue_id);
++ // update the issue associations
++ if (empty($_POST['associated_issues'])) {
++ $associated_issues = array();
++ } else {
++ $associated_issues = explode(',', @$_POST['associated_issues']);
++ // make sure all associated issues are valid (and in this project)
++ for ($i = 0; $i < count($associated_issues); $i++) {
++ if (!self::exists(trim($associated_issues[$i]), false)) {
++ $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
++ unset($associated_issues[$i]);
++ }
++ }
++ }
++ $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
++ if (count($association_diff) > 0) {
++ // go through the new assocations, if association already exists, skip it
++ $associations_to_remove = $current['associated_issues'];
++ if (count($associated_issues) > 0) {
++ foreach ($associated_issues as $index => $associated_id) {
++ if (!in_array($associated_id, $current['associated_issues'])) {
++ self::addAssociation($issue_id, $associated_id, $usr_id);
++ } else {
++ // already assigned, remove this user from list of users to remove
++ unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
++ }
++ }
++ }
++ if (count($associations_to_remove) > 0) {
++ foreach ($associations_to_remove as $associated_id) {
++ self::deleteAssociation($issue_id, $associated_id);
++ }
++ }
++ }
++ $assignments_changed = false;
++ if (@$_POST["keep_assignments"] == "no") {
++ // only change the issue-user associations if there really were any changes
++ $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
++ if (!empty($_POST['assignments'])) {
++ $new_assignees = @$_POST['assignments'];
++ } else {
++ $new_assignees = array();
++ }
++ $assignment_notifications = array();
++
++ // remove people from the assignment list, if appropriate
++ foreach ($old_assignees as $assignee) {
++ if (!in_array($assignee, $new_assignees)) {
++ self::deleteUserAssociation($issue_id, $assignee);
++ $assignments_changed = true;
++ }
++ }
++ // add people to the assignment list, if appropriate
++ foreach ($new_assignees as $assignee) {
++ if (!in_array($assignee, $old_assignees)) {
++ self::addUserAssociation($usr_id, $issue_id, $assignee);
++ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
++ $assignment_notifications[] = $assignee;
++ $assignments_changed = true;
++ }
++ }
++ if (count($assignment_notifications) > 0) {
++ Notification::notifyNewAssignment($assignment_notifications, $issue_id);
++ }
++ }
++ if (empty($_POST["estimated_dev_time"])) {
++ $_POST["estimated_dev_time"] = 0;
++ }
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='updated',";
++ if (!empty($_POST["category"])) {
++ $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
++ }
++ if (@$_POST["keep"] == "no") {
++ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
++ }
++ if (!empty($_POST['expected_resolution_date'])) {
++ $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
++ } else {
++ $stmt .= "iss_expected_resolution_date=null,";
++ }
++ $stmt .= "
++ iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
++ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
++ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
++ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
++ iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
++ iss_description='" . Misc::escapeString($_POST["description"]) . "',
++ iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
++ iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
++ iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
++ iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
++ if (isset($_POST['private'])) {
++ $stmt .= ",
++ iss_private = " . Misc::escapeInteger($_POST['private']);
++ }
++ $stmt .= "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add change to the history (only for changes on specific fields?)
++ $updated_fields = array();
++ if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
++ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
++ }
++ if ($current["iss_prc_id"] != $_POST["category"]) {
++ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
++ }
++ if ($current["iss_pre_id"] != $_POST["release"]) {
++ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
++ }
++ if ($current["iss_pri_id"] != $_POST["priority"]) {
++ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
++ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
++ }
++ if ($current["iss_sta_id"] != $_POST["status"]) {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ $old_status_details = Status::getDetails($current['iss_sta_id']);
++ if ($old_status_details['sta_is_closed'] == 1) {
++ $new_status_details = Status::getDetails($_POST["status"]);
++ if ($new_status_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
++ }
++ if ($current["iss_res_id"] != $_POST["resolution"]) {
++ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
++ }
++ if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
++ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
++ }
++ if ($current["iss_summary"] != $_POST["summary"]) {
++ $updated_fields["Summary"] = '';
++ }
++ if ($current["iss_description"] != $_POST["description"]) {
++ $updated_fields["Description"] = '';
++ }
++ if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
++ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
++ }
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $i = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($i > 0) {
++ $changes .= "; ";
++ }
++ if (($key != "Summary") && ($key != "Description")) {
++ $changes .= "$key: $value";
++ } else {
++ $changes .= "$key";
++ }
++ $i++;
++ }
++ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
++ // send notifications for the issue being updated
++ Notification::notifyIssueUpdated($issue_id, $current, $_POST);
++ }
++
++ // record group change as a seperate change
++ if ($current["iss_grp_id"] != (int)$_POST["group"]) {
++ History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
++ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
++ }
++
++ // now update any duplicates, if any
++ $update_dupe = array(
++ 'Category',
++ 'Release',
++ 'Priority',
++ 'Release',
++ 'Resolution'
++ );
++ // COMPAT: the following line requires PHP > 4.0.4
++ $intersect = array_intersect($update_dupe, array_keys($updated_fields));
++ if (($current["duplicates"] != '') && (count($intersect) > 0)) {
++ self::updateDuplicates($issue_id);
++ }
++
++ // if there is customer integration, mark last customer action
++ if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
++ self::recordLastCustomerAction($issue_id);
++ }
++
++ if ($assignments_changed) {
++ // XXX: we may want to also send the email notification for those "new" assignees
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
++ }
++
++ Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
++ // Move issue to another project
++ if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
++ $new_prj_id = (int)@$_POST['new_prj'];
++ if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
++ if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
++ $res = self::moveIssue($issue_id, $new_prj_id);
++ if ($res == -1) {
++ return $res;
++ }
++ } else {
++ return -1;
++ }
++ }
++ }
++ return 1;
++ }
++ }
++
++ /**
++ * Method used to update the a single detail field of a specific issue.
++ *
++ * @param integer $issue_id
++ * @param string $field_name
++ * @param string $field_value
++ * @param string $field_type string or integer (for escape)
++ * @return integer 1 on success, -1 otherwise
++ */
++ function updateField($issue_id, $field_name, $filed_value) {
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $usr_id = Auth::getUserID();
++ $prj_id = self::getProjectID($issue_id);
++
++ // get all of the 'current' information of this issue
++ $current = self::getDetails($issue_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='updated'";
++
++ switch ($field_name) {
++ case 'category':
++ $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'release':
++ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'expected_resolution_date':
++ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'release':
++ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'priority':
++ $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'status':
++ $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'resolution':
++ $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'summary':
++ $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'description':
++ $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'estimated_dev_time':
++ $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'percent_complete':
++ $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'trigger_reminders':
++ $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'group':
++ $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'private':
++ $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
++ break;
++ default:
++ Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
++ return -1;
++ break;
++ }
++
++ $stmt .= "
++ WHERE
++ iss_id=$issue_id";
++
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $new = array(
++ 'category' => $current['iss_prc_id'],
++ 'release' => $current['iss_pre_id'],
++ 'expected_resolution_date' => $current['iss_expected_resolution_date'],
++ 'release' => $current['iss_pre_id'],
++ 'priority' => $current['iss_pri_id'],
++ 'status' => $current['iss_sta_id'],
++ 'resolution' => $current['iss_res_id'],
++ 'summary' => $current['iss_summary'],
++ 'description' => $current['iss_description'],
++ 'estimated_dev_time' => $current['iss_dev_time'],
++ 'percent_complete' => $current['iss_percent_complete'],
++ 'trigger_reminders' => $current['iss_trigger_reminders'],
++ 'group' => $current['iss_grp_id'],
++ 'iss_private' => $current['private']
++ );
++ $new[$field_name] = $filed_value;
++
++ // add change to the history (only for changes on specific fields?)
++ $updated_fields = array();
++ if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
++ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
++ }
++ if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
++ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
++ }
++ if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
++ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
++ }
++ if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
++ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
++ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
++ }
++ if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ $old_status_details = Status::getDetails($current['iss_sta_id']);
++ if ($old_status_details['sta_is_closed'] == 1) {
++ $new_status_details = Status::getDetails($filed_value);
++ if ($new_status_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
++ }
++ if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
++ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
++ }
++ if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
++ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
++ }
++ if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
++ $updated_fields["Summary"] = '';
++ }
++ if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
++ $updated_fields["Description"] = '';
++ }
++ if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
++ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
++ }
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $i = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($i > 0) {
++ $changes .= "; ";
++ }
++ if (($key != "Summary") && ($key != "Description")) {
++ $changes .= "$key: $value";
++ } else {
++ $changes .= "$key";
++ }
++ $i++;
++ }
++
++ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
++ // send notifications for the issue being updated
++ Notification::notifyIssueUpdated($issue_id, $current, $new);
++ }
++ }
++ return 1;
++ }
++
++
++ /**
++ * Move the issue to a new project
++ *
++ * @param integer $issue_id
++ * @param integer $new_prj_id
++ * @return integer 1 on success, -1 otherwise
++ */
++ function moveIssue($issue_id, $new_prj_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $currentDetails = self::getDetails($issue_id);
++
++ // set new category
++ $new_iss_prc_list = Category::getAssocList($new_prj_id);
++ $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
++ $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
++ if ($new_prc_id === false) {
++ // use the first category listed in the new project
++ $new_prc_id = key($new_iss_prc_list);
++ }
++
++ // set new priority
++ $new_iss_pri_list = Priority::getAssocList($new_prj_id);
++ $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
++ $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
++ if ($new_pri_id === false) {
++ // use the first category listed in the new project
++ $new_pri_id = key($new_iss_pri_list);
++ }
++
++ // XXX: Set status if needed when moving issue
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
++ iss_pri_id=" . $new_pri_id . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ }
++
++ // clear project cache
++ self::getProjectID($issue_id, true);
++
++ Notification::notifyNewIssue($new_prj_id, $issue_id);
++ }
++ }
++
++
++ /**
++ * Method used to associate an existing issue with another one.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $issue_id The other issue ID
++ * @return void
++ */
++ function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $associated_id = Misc::escapeInteger($associated_id);
++
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ (
++ isa_issue_id,
++ isa_associated_id
++ ) VALUES (
++ $issue_id,
++ $associated_id
++ )";
++ DB_Helper::getInstance()->query($stmt);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
++ // link the associated issue back to this one
++ if ($link_issues) {
++ self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
++ }
++ }
++
++
++ /**
++ * Method used to remove the issue associations related to a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return void
++ */
++ function deleteAssociations($issue_id, $usr_id = FALSE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (is_array($issue_id)) {
++ $issue_id = implode(", ", $issue_id);
++ }
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ WHERE
++ isa_issue_id IN ($issue_id) OR
++ isa_associated_id IN ($issue_id)";
++ DB_Helper::getInstance()->query($stmt);
++ if ($usr_id) {
++ History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
++ }
++ }
++
++
++ /**
++ * Method used to remove a issue association from an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $associated_id The associated issue ID to remove.
++ * @return void
++ */
++ function deleteAssociation($issue_id, $associated_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $associated_id = Misc::escapeInteger($associated_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ WHERE
++ (
++ isa_issue_id = $issue_id AND
++ isa_associated_id = $associated_id
++ ) OR
++ (
++ isa_issue_id = $associated_id AND
++ isa_associated_id = $issue_id
++ )";
++ DB_Helper::getInstance()->query($stmt);
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
++ "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
++ History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
++ "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
++ }
++
++
++ /**
++ * Method used to assign an issue with an user.
++ *
++ * @access public
++ * @param integer $usr_id The user ID of the person performing this change
++ * @param integer $issue_id The issue ID
++ * @param integer $assignee_usr_id The user ID of the assignee
++ * @param boolean $add_history Whether to add a history entry about this or not
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
++ $order = 1;
++ // move all orders down to free "order space" for this new association
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order + 1
++ WHERE
++ isu_usr_id = $assignee_usr_id AND
++ isu_order >= $order";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ // insert the new association
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ (
++ isu_iss_id,
++ isu_usr_id,
++ isu_assigned_date,
++ isu_order
++ ) VALUES (
++ $issue_id,
++ $assignee_usr_id,
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ $order
++ )";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($add_history) {
++ History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
++ 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
++ }
++ return 1;
++ }
++ }
++
++ /**
++ * Method used to get the order list to be rearranged
++ *
++ * @access private
++ * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
++ * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
++ * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
++ */
++ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
++ {
++ // find all affected associantion orders
++ $stmt = "SELECT isu_usr_id, isu_order FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id IN ($issue_id)";
++ if ($usr_id !== FALSE) {
++ $stmt.= " AND isu_usr_id IN ($usr_id)";
++ }
++ $stmt.= "ORDER BY isu_order";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $deleted_orders = array();
++ foreach ($res as $row) {
++ if (empty($deleted_orders[$row['isu_usr_id']])) {
++ $deleted_orders[$row['isu_usr_id']] = array();
++ }
++ $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
++ }
++ return $deleted_orders;
++ }
++ }
++
++ /**
++ *
++ * Method used to rearrange order list in the db according to known deleted records
++ *
++ * @access private
++ * @param mixed deleteorder list
++ * @return void
++ */
++ function rearrangeDeleteUserAssociationOrderList($delete_order_list)
++ {
++ if (empty($delete_order_list) || (!is_array($delete_order_list))) {
++ return -1;
++ }
++ foreach ($delete_order_list as $isu_usr_id => $orders) {
++ for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
++ // move the orders after them up to take the "order space" of the deleted records
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order - " . ($i+1) . "
++ WHERE
++ isu_usr_id = $isu_usr_id AND
++ isu_order > " . $orders[$i];
++ if ($i < count($orders) - 1) {
++ $stmt.= " AND
++ isu_order < " . $orders[$i+1];
++ }
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ }
++ }
++ return 1;
++ }
++
++
++ /**
++ * Method used to delete all user assignments for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing the change
++ * @return void
++ */
++ function deleteUserAssociations($issue_id, $usr_id = FALSE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (is_array($issue_id)) {
++ $issue_id = implode(", ", $issue_id);
++ }
++ $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id IN ($issue_id)";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($usr_id) {
++ History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
++ }
++ self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to delete a single user assignments for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user to remove.
++ * @param boolean $add_history Whether to add a history entry about this or not
++ * @return void
++ */
++ function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $usr_id = Misc::escapeInteger($usr_id);
++ $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id = $issue_id AND
++ isu_usr_id = $usr_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($add_history) {
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
++ User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
++ }
++ self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Creates an issue with the given email information.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @param integer $usr_id The user responsible for this action
++ * @param string $sender The original sender of this email
++ * @param string $summary The issue summary
++ * @param string $description The issue description
++ * @param integer $category The category ID
++ * @param integer $priority The priority ID
++ * @param array $assignment The list of users to assign this issue to
++ * @param string $date The date the email was originally sent.
++ * @param string $msg_id The message ID of the email we are creating this issue from.
++ * @return void
++ */
++ function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
++ {
++ $data = array();
++ $exclude_list = array();
++
++ $sender_email = Mail_Helper::getEmailAddress($sender);
++ $sender_usr_id = User::getUserIDByEmail($sender_email, true);
++ if (!empty($sender_usr_id)) {
++ $reporter = $sender_usr_id;
++ $exclude_list[] = $sender_usr_id;
++ }
++
++ $data = array(
++ 'category' => $category,
++ 'priority' => $priority,
++ 'description' => $description,
++ 'summary' => $summary,
++ 'msg_id' => $msg_id,
++ );
++
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
++ if (!empty($customer_id)) {
++ $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
++ // overwrite the reporter with the customer contact
++ $reporter = User::getUserIDByContactID($customer_contact_id);
++ $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
++
++ $data['customer'] = $customer_id;
++ $data['contact'] = $customer_contact_id;
++# $data['contract'] = // XXX missing
++ $data['contact_person_lname'] = $contact['last_name'];
++ $data['contact_person_fname'] = $contact['first_name'];
++ $data['contact_email'] = $sender_email;
++ $data['contact_phone'] = $contact['phone'];
++ $data['contact_timezone'] = $contact_timezone;
++ }
++ } else {
++ $customer_id = FALSE;
++ }
++ if (empty($reporter)) {
++ $reporter = APP_SYSTEM_USER_ID;
++ }
++
++ $data['reporter'] = $reporter;
++
++ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
++ if ($issue_id == -1) {
++ return -1;
++ }
++
++ $has_TAM = false;
++ $has_RR = false;
++ // log the creation of the issue
++ History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
++
++ $emails = array();
++ $manager_usr_ids = array();
++ if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
++ // if there are any technical account managers associated with this customer, add these users to the notification list
++ $managers = Customer::getAccountManagers($prj_id, $customer_id);
++ $manager_usr_ids = array_keys($managers);
++ $manager_emails = array_values($managers);
++ $emails = array_merge($emails, $manager_emails);
++ }
++ // add the reporter to the notification list
++ $emails[] = $sender;
++ $emails = array_unique($emails);
++ $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
++ foreach ($emails as $address) {
++ Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
++ }
++
++ // only assign the issue to an user if the associated customer has any technical account managers
++ $users = array();
++ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
++ foreach ($manager_usr_ids as $manager_usr_id) {
++ $users[] = $manager_usr_id;
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
++ }
++ $has_TAM = true;
++ }
++ // now add the user/issue association
++ if (@count($assignment) > 0) {
++ for ($i = 0; $i < count($assignment); $i++) {
++ Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
++ if ($assignment[$i] != $usr_id) {
++ $users[] = $assignment[$i];
++ }
++ }
++ } else {
++ // only use the round-robin feature if this new issue was not
++ // already assigned to a customer account manager
++ if (@count($manager_usr_ids) < 1) {
++ $assignee = Round_Robin::getNextAssignee($prj_id);
++ // assign the issue to the round robin person
++ if (!empty($assignee)) {
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
++ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
++ $users[] = $assignee;
++ $has_RR = true;
++ }
++ }
++ }
++ if (count($users) > 0) {
++ $has_assignee = true;
++ }
++
++ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
++
++ // send special 'an issue was auto-created for you' notification back to the sender
++ Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
++
++ return $issue_id;
++ }
++
++
++ /**
++ * Return errors that happened when creating new issue from POST method.
++ *
++ * @return array
++ */
++ private static $insert_errors = array();
++ static function getInsertErrors() {
++ return self::$insert_errors;
++ }
++
++ /**
++ * Method used to add a new issue using the normal report form.
++ *
++ * @access public
++ * @return integer The new issue ID
++ */
++ function createFromPost()
++ {
++ $keys = array(
++ 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
++ 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
++ 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
++ );
++ $data = array();
++ foreach ($keys as $key) {
++ if (isset($_POST[$key])) {
++ $data[$key] = $_POST[$key];
++ }
++ }
++
++ $prj_id = Auth::getCurrentProject();
++ $usr_id = Auth::getUserID();
++
++ // if we are creating an issue for a customer, put the
++ // main customer contact as the reporter for it
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $contact_usr_id = User::getUserIDByContactID($data['contact']);
++ if (empty($contact_usr_id)) {
++ $contact_usr_id = $usr_id;
++ }
++ $data['reporter'] = $contact_usr_id;
++ } else {
++ $data['reporter'] = $usr_id;
++ }
++
++ $data['msg_id'] = Mail_Helper::generateMessageID();
++
++ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
++ if ($issue_id == -1) {
++ return -1;
++ }
++
++ $has_TAM = false;
++ $has_RR = false;
++ $info = User::getNameEmail($usr_id);
++ // log the creation of the issue
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
++
++ $emails = array();
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
++ $emails = $data['contact_extra_emails'];
++ }
++ // add the primary contact to the notification list
++ if ($data['add_primary_contact'] == 'yes') {
++ $contact_email = User::getEmailByContactID($data['contact']);
++ if (!empty($contact_email)) {
++ $emails[] = $contact_email;
++ }
++ }
++ // if there are any technical account managers associated with this customer, add these users to the notification list
++ $managers = Customer::getAccountManagers($prj_id, $data['customer']);
++ $manager_usr_ids = array_keys($managers);
++ $manager_emails = array_values($managers);
++ $emails = array_merge($emails, $manager_emails);
++ }
++ // add the reporter to the notification list
++ $emails[] = $info['usr_email'];
++ $emails = array_unique($emails);
++ foreach ($emails as $address) {
++ Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
++ }
++
++ // only assign the issue to an user if the associated customer has any technical account managers
++ $users = array();
++ $has_TAM = false;
++ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
++ foreach ($manager_usr_ids as $manager_usr_id) {
++ $users[] = $manager_usr_id;
++ self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
++ }
++ $has_TAM = true;
++ }
++ // now add the user/issue association (aka assignments)
++ if (!empty($data['users']) && count($data['users']) > 0) {
++ for ($i = 0; $i < count($data['users']); $i++) {
++ Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
++ Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
++ self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
++ if ($data['users'][$i] != $usr_id) {
++ $users[] = $data['users'][$i];
++ }
++ }
++ } else {
++ // only use the round-robin feature if this new issue was not
++ // already assigned to a customer account manager
++ if (@count($manager_usr_ids) < 1) {
++ $assignee = Round_Robin::getNextAssignee($prj_id);
++ // assign the issue to the round robin person
++ if (!empty($assignee)) {
++ $users[] = $assignee;
++ self::addUserAssociation($usr_id, $issue_id, $assignee, false);
++ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
++ $has_RR = true;
++ }
++ }
++ }
++
++ // now process any files being uploaded
++ $found = 0;
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ if (!@empty($_FILES["file"]["name"][$i])) {
++ $found = 1;
++ break;
++ }
++ }
++ if ($found) {
++ $files = array();
++ for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
++ $filename = @$_FILES["file"]["name"][$i];
++ if (empty($filename)) {
++ continue;
++ }
++ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
++ if (empty($blob)) {
++ // error reading a file
++ self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
++ continue;
++ }
++ $files[] = array(
++ "filename" => $filename,
++ "type" => $_FILES['file']['type'][$i],
++ "blob" => $blob
++ );
++ }
++ if (count($files) > 0) {
++ $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
++ foreach ($files as $file) {
++ Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
++ }
++ }
++ }
++ // need to associate any emails ?
++ if (!empty($data['attached_emails'])) {
++ $items = explode(",", $data['attached_emails']);
++ Support::associate($usr_id, $issue_id, $items);
++ }
++ // need to notify any emails being converted into issues ?
++ if (@count($data['notify_senders']) > 0) {
++ $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
++ } else {
++ $recipients = array();
++ }
++ // need to process any custom fields ?
++ if (@count($data['custom_fields']) > 0) {
++ foreach ($data['custom_fields'] as $fld_id => $value) {
++ Custom_Field::associateIssue($issue_id, $fld_id, $value);
++ }
++ }
++ // also send a special confirmation email to the customer contact
++ if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
++ // also need to pass the list of sender emails already notified,
++ // so we can avoid notifying the same person again
++ $contact_email = User::getEmailByContactID($data['contact']);
++ if (@!in_array($contact_email, $recipients)) {
++ Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
++ }
++ }
++
++ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($prj_id, $issue_id);
++
++ return $issue_id;
++ }
++
++ /**
++ * Insert issue to database.
++ *
++ * @param integer $prj_id The project ID
++ * @param integer $usr_id The user responsible for this action
++ * @param array $data of issue to be inserted
++ * @return integer The new issue ID
++ */
++ private function insertIssue($prj_id, $usr_id, $data)
++ {
++
++ // XXX missing_fields never used
++ $missing_fields = array();
++ if ($data['category'] == -1) {
++ $missing_fields[] = 'Category';
++ }
++ if ($data['priority'] == -1) {
++ $missing_fields[] = 'Priority';
++ }
++
++ // if there is no reporter set, use the system user
++ if (empty($data['reporter'])) {
++ $data['reporter'] = APP_SYSTEM_USER_ID;
++ }
++
++ if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
++ $data['estimated_dev_time'] = 0;
++ }
++
++ // add new issue
++ $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
++ "SET ".
++ "iss_prj_id=" . $prj_id . ",";
++ if (!empty($data['group'])) {
++ $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
++ }
++ if (!empty($data['category'])) {
++ $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
++ }
++ if (!empty($data['release'])) {
++ $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
++ }
++ if (!empty($data['priority'])) {
++ $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
++ }
++
++ $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
++
++ $initial_status = Project::getInitialStatus($prj_id);
++ if (!empty($initial_status)) {
++ $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
++ }
++
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $stmt .= "
++ iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
++ if (!empty($data['contact'])) {
++ $stmt .= "
++ iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
++ }
++ $stmt .= "
++ iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
++ iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
++ iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
++ iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
++ iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
++ iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
++ }
++
++ $stmt .= "
++ iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='created',
++ iss_summary='" . Misc::escapeString($data['summary']) . "',
++ iss_description='" . Misc::escapeString($data['description']) . "',
++ iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
++ if (!empty($data['contact'])) {
++ $stmt .= "
++ iss_private=" . Misc::escapeInteger($data['private']) . " ,";
++ }
++ $stmt .= "
++ iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
++ ";
++
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++
++ $issue_id = DB_Helper::get_last_insert_id();
++ return $issue_id;
++ }
++
++
++ /**
++ * Method used to get a specific parameter in the issue listing cookie.
++ *
++ * @access public
++ * @param string $name The name of the parameter
++ * @return mixed The value of the specified parameter
++ */
++ function getParam($name)
++ {
++ $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
++
++ if (isset($_GET[$name])) {
++ return $_GET[$name];
++ } elseif (isset($_POST[$name])) {
++ return $_POST[$name];
++ } elseif (isset($profile[$name])) {
++ return $profile[$name];
++ } else {
++ return "";
++ }
++ }
++
++
++ /**
++ * Method used to save the current search parameters in a cookie.
++ *
++ * @access public
++ * @return array The search parameters
++ */
++ function saveSearchParams()
++ {
++ $sort_by = self::getParam('sort_by');
++ $sort_order = self::getParam('sort_order');
++ $users = self::getParam('users');
++ if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
++ unset($sort_by);
++ unset($sort_order);
++ }
++ $rows = self::getParam('rows');
++ $hide_closed = self::getParam('hide_closed');
++ if ($hide_closed === '') {
++ $hide_closed = 1;
++ }
++ $search_type = self::getParam('search_type');
++ if (empty($search_type)) {
++ $search_type = 'all_text';
++ }
++ $custom_field = self::getParam('custom_field');
++ if (is_string($custom_field)) {
++ $custom_field = unserialize(urldecode($custom_field));
++ }
++ $cookie = array(
++ 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
++ 'pagerRow' => self::getParam('pagerRow'),
++ 'hide_closed' => $hide_closed,
++ "sort_by" => $sort_by ? $sort_by : "pri_rank",
++ "sort_order" => $sort_order ? $sort_order : "ASC",
++ // quick filter form
++ 'keywords' => self::getParam('keywords'),
++ 'search_type' => $search_type,
++ 'users' => self::getParam('users'),
++ 'status' => self::getParam('status'),
++ 'priority' => self::getParam('priority'),
++ 'category' => self::getParam('category'),
++ 'customer_email' => self::getParam('customer_email'),
++ // advanced search form
++ 'show_authorized_issues' => self::getParam('show_authorized_issues'),
++ 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
++ 'reporter' => self::getParam('reporter'),
++ // other fields
++ 'release' => self::getParam('release'),
++ // custom fields
++ 'custom_field' => $custom_field
++ );
++ // now do some magic to properly format the date fields
++ $date_fields = array(
++ 'created_date',
++ 'updated_date',
++ 'last_response_date',
++ 'first_response_date',
++ 'closed_date'
++ );
++ foreach ($date_fields as $field_name) {
++ $field = self::getParam($field_name);
++ if (empty($field)) {
++ continue;
++ }
++ if (@$field['filter_type'] == 'in_past') {
++ @$cookie[$field_name] = array(
++ 'filter_type' => 'in_past',
++ 'time_period' => $field['time_period']
++ );
++ } else {
++ $end_field_name = $field_name . '_end';
++ $end_field = self::getParam($end_field_name);
++ @$cookie[$field_name] = array(
++ 'past_hour' => $field['past_hour'],
++ 'Year' => $field['Year'],
++ 'Month' => $field['Month'],
++ 'Day' => $field['Day'],
++ 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
++ 'filter_type' => $field['filter_type'],
++ 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
++ );
++ @$cookie[$end_field_name] = array(
++ 'Year' => $end_field['Year'],
++ 'Month' => $end_field['Month'],
++ 'Day' => $end_field['Day']
++ );
++ }
++ }
++ Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
++ return $cookie;
++ }
++
++
++ /**
++ * Method used to get the current sorting options used in the grid layout
++ * of the issue listing page.
++ *
++ * @access public
++ * @param array $options The current search parameters
++ * @return array The sorting options
++ */
++ function getSortingInfo($options)
++ {
++
++ $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
++
++ // default order for last action date, priority should be descending
++ // for textual fields, like summary, ascending is reasonable
++ $fields = array(
++ "pri_rank" => "desc",
++ "iss_id" => "desc",
++ "iss_customer_id" => "desc",
++ "prc_title" => "asc",
++ "sta_rank" => "asc",
++ "iss_created_date" => "desc",
++ "iss_summary" => "asc",
++ "last_action_date" => "desc",
++ "usr_full_name" => "asc",
++ "iss_expected_resolution_date" => "desc",
++ "pre_title" => "asc",
++ "assigned" => "asc",
++ "isu_order" => "desc",
++ );
++
++ foreach ($custom_fields as $fld_id => $fld_name) {
++ $fields['custom_field_' . $fld_id] = "desc";
++ }
++
++ $sortfields = array_combine(array_keys($fields), array_keys($fields));
++ $sortfields["pre_title"] = "pre_scheduled_date";
++ $sortfields["assigned"] = "isu_usr_id";
++
++ $items = array(
++ "links" => array(),
++ "images" => array()
++ );
++ foreach ($sortfields as $field => $sortfield) {
++ $sort_order = $fields[$field];
++ if ($options["sort_by"] == $sortfield) {
++ $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
++ if (strtolower($options["sort_order"]) == "asc") {
++ $sort_order = "desc";
++ } else {
++ $sort_order = "asc";
++ }
++ }
++ $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
++ }
++ return $items;
++ }
++
++
++ /**
++ * Returns the list of action date fields appropriate for the
++ * current user ID.
++ *
++ * @access public
++ * @return array The list of action date fields
++ */
++ function getLastActionFields()
++ {
++ $last_action_fields = array(
++ "iss_last_public_action_date"
++ );
++ if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
++ $last_action_fields[] = "iss_last_internal_action_date";
++ }
++ if (count($last_action_fields) > 1) {
++ return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
++ } else {
++ return $last_action_fields[0] . " AS last_action_date";
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues to be displayed in the grid layout.
++ *
++ * @access public
++ * @param integer $prj_id The current project ID
++ * @param array $options The search parameters
++ * @param integer $current_row The current page number
++ * @param integer $max The maximum number of rows per page
++ * @return array The list of issues to be displayed
++ */
++ function getListing($prj_id, $options, $current_row = 0, $max = 5)
++ {
++ if (strtoupper($max) == "ALL") {
++ $max = 9999999;
++ }
++ $start = $current_row * $max;
++ // get the current user's role
++ $usr_id = Auth::getUserID();
++ $role_id = User::getRoleByUser($usr_id, $prj_id);
++
++ // get any custom fields that should be displayed
++ $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
++
++ $stmt = "SELECT
++ iss_id,
++ iss_grp_id,
++ iss_prj_id,
++ iss_sta_id,
++ iss_customer_id,
++ iss_customer_contract_id,
++ iss_created_date,
++ iss_updated_date,
++ iss_last_response_date,
++ iss_closed_date,
++ iss_last_customer_action_date,
++ iss_usr_id,
++ iss_summary,
++ pri_title,
++ prc_title,
++ sta_title,
++ sta_color status_color,
++ sta_id,
++ iqu_status,
++ grp_name `group`,
++ pre_title,
++ iss_last_public_action_date,
++ iss_last_public_action_type,
++ iss_last_internal_action_date,
++ iss_last_internal_action_type,
++ " . self::getLastActionFields() . ",
++ IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
++ iss_private,
++ usr_full_name,
++ iss_percent_complete,
++ iss_dev_time,
++ iss_expected_resolution_date
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
++ // join custom fields if we are searching by custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
++ }
++ } else {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
++ }
++ }
++ }
++ $stmt .= ")";
++ // check for the custom fields we want to sort by
++ if (strstr($options['sort_by'], 'custom_field') !== false) {
++ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
++ $stmt .= "\n LEFT JOIN \n" .
++ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
++ ON
++ (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
++ }
++ if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id";
++ }
++ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
++ ON
++ iur_iss_id=iss_id";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
++ ON
++ sub_iss_id=iss_id";
++ }
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
++ ON
++ iss_grp_id=grp_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id=prc_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id = pre_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ ON
++ iss_id=iqu_iss_id AND
++ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
++ WHERE
++ iss_prj_id= " . Misc::escapeInteger($prj_id);
++ $stmt .= self::buildWhereClause($options);
++
++ if (strstr($options["sort_by"], 'custom_field') !== false) {
++ $fld_details = Custom_Field::getDetails($fld_id);
++ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
++ } else {
++ $sort_by = Misc::escapeString($options["sort_by"]);
++ }
++
++ $stmt .= "
++ GROUP BY
++ iss_id
++ ORDER BY
++ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
++ iss_id DESC";
++ $total_rows = Pager::getTotalRows($stmt);
++ $stmt .= "
++ LIMIT
++ " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array(
++ "list" => "",
++ "info" => ""
++ );
++ } else {
++ if (count($res) > 0) {
++ self::getAssignedUsersByIssues($res);
++ Time_Tracking::getTimeSpentByIssues($res);
++ // need to get the customer titles for all of these issues...
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ Customer::getCustomerTitlesByIssues($prj_id, $res);
++ Customer::getSupportLevelsByIssues($prj_id, $res);
++ }
++ self::formatLastActionDates($res);
++ self::getLastStatusChangeDates($prj_id, $res);
++ } elseif ($current_row > 0) {
++ // if there are no results, and the page is not the first page reset page to one and reload results
++ Auth::redirect("list.php?pagerRow=0&rows=$max");
++ }
++ $groups = Group::getAssocList($prj_id);
++ $categories = Category::getAssocList($prj_id);
++ $column_headings = self::getColumnHeadings($prj_id);
++ if (count($custom_fields) > 0) {
++ $column_headings = array_merge($column_headings,$custom_fields);
++ }
++ $csv[] = @implode("\t", $column_headings);
++ for ($i = 0; $i < count($res); $i++) {
++ $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
++ $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
++ $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
++ $fields = array(
++ $res[$i]['pri_title'],
++ $res[$i]['iss_id'],
++ $res[$i]['usr_full_name'],
++ );
++ // hide the group column from the output if no
++ // groups are available in the database
++ if (count($groups) > 0) {
++ $fields[] = $res[$i]['group'];
++ }
++ $fields[] = $res[$i]['assigned_users'];
++ $fields[] = $res[$i]['time_spent'];
++ // hide the category column from the output if no
++ // categories are available in the database
++ if (count($categories) > 0) {
++ $fields[] = $res[$i]['prc_title'];
++ }
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $fields[] = @$res[$i]['customer_title'];
++ // check if current user is acustomer and has a per incident contract.
++ // if so, check if issue is redeemed.
++ if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
++ if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
++ (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
++ $res[$i]['redeemed'] = true;
++ }
++ }
++ }
++ $fields[] = $res[$i]['sta_title'];
++ $fields[] = $res[$i]["status_change_date"];
++ $fields[] = $res[$i]["last_action_date"];
++ $fields[] = $res[$i]['iss_dev_time'];
++ $fields[] = $res[$i]['iss_summary'];
++ $fields[] = $res[$i]['iss_expected_resolution_date'];
++
++ if (count($custom_fields) > 0) {
++ $res[$i]['custom_field'] = array();
++ $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
++ foreach ($custom_field_values as $this_field) {
++ if (!empty($custom_fields[$this_field['fld_id']])) {
++ $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
++ $fields[] = $this_field['value'];
++ }
++ }
++ }
++
++ $csv[] = @implode("\t", $fields);
++ }
++ $total_pages = ceil($total_rows / $max);
++ $last_page = $total_pages - 1;
++ return array(
++ "list" => $res,
++ "info" => array(
++ "current_page" => $current_row,
++ "start_offset" => $start,
++ "end_offset" => $start + count($res),
++ "total_rows" => $total_rows,
++ "total_pages" => $total_pages,
++ "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
++ "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
++ "last_page" => $last_page,
++ "custom_fields" => $custom_fields
++ ),
++ "csv" => @implode("\n", $csv)
++ );
++ }
++ }
++
++
++ /**
++ * Processes a result set to format the "Last Action Date" column.
++ *
++ * @access public
++ * @param array $result The result set
++ */
++ function formatLastActionDates(&$result)
++ {
++ for ($i = 0; $i < count($result); $i++) {
++ if (($result[$i]['action_type'] == "internal") &&
++ (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
++ $label = $result[$i]["iss_last_internal_action_type"];
++ $last_date = $result[$i]["iss_last_internal_action_date"];
++ } else {
++ $label = $result[$i]["iss_last_public_action_type"];
++ $last_date = $result[$i]["iss_last_public_action_date"];
++ }
++ $date = new Date($last_date);
++ $current = new Date(Date_Helper::getCurrentDateGMT());
++ $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
++ Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
++ }
++ }
++
++
++ /**
++ * Retrieves the last status change date for the given issue.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @param array $result The associative array of data
++ * @see self::getListing()
++ */
++ function getLastStatusChangeDates($prj_id, &$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_sta_id"];
++ }
++ if (count($ids) == 0) {
++ return false;
++ }
++ $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
++ for ($i = 0; $i < count($result); $i++) {
++ if (empty($result[$i]['iss_sta_id'])) {
++ $result[$i]['status_change_date'] = '';
++ } else {
++ list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
++ if ((empty($label)) || (empty($date_field_name))) {
++ $result[$i]['status_change_date'] = '';
++ continue;
++ }
++ $current = new Date(Date_Helper::getCurrentDateGMT());
++ $desc = "$label: %s ago";
++ $target_date = $result[$i][$date_field_name];
++ if (empty($target_date)) {
++ $result[$i]['status_change_date'] = '';
++ continue;
++ }
++ $date = new Date($target_date);
++ $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues to be displayed in the grid layout.
++ *
++ * @access public
++ * @param array $options The search parameters
++ * @return string The where clause
++ */
++ function buildWhereClause($options)
++ {
++ $usr_id = Auth::getUserID();
++ $prj_id = Auth::getCurrentProject();
++ $role_id = User::getRoleByUser($usr_id, $prj_id);
++
++ $stmt = ' AND iss_usr_id = usr_id';
++ if ($role_id == User::getRoleID('Customer')) {
++ $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
++ } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
++ $stmt .= " AND (
++ iss_usr_id = $usr_id OR
++ iur_usr_id = $usr_id
++ )";
++ }
++
++ if (!empty($options["users"])) {
++ $stmt .= " AND (\n";
++ if (stristr($options["users"], "grp") !== false) {
++ $chunks = explode(":", $options["users"]);
++ $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
++ } else {
++ if ($options['users'] == '-1') {
++ $stmt .= 'isu_usr_id IS NULL';
++ } elseif ($options['users'] == '-2') {
++ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
++ } elseif ($options['users'] == '-3') {
++ $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
++ } elseif ($options['users'] == '-4') {
++ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
++ } else {
++ $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
++ }
++ }
++ $stmt .= ')';
++ }
++ if (!empty($options["reporter"])) {
++ $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
++ }
++ if (!empty($options["show_authorized_issues"])) {
++ $stmt .= " AND (iur_usr_id=$usr_id)";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= " AND (sub_usr_id=$usr_id)";
++ }
++ if (!empty($options["keywords"])) {
++ $stmt .= " AND (\n";
++ if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
++ $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
++ } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
++ // check if the user is trying to search by customer email
++ $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
++ if (count($customer_ids) > 0) {
++ $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
++ } else {
++ // no results, kill query
++ $stmt .= " iss_customer_id = -1";
++ }
++ } else {
++ $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
++ $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
++ }
++ $stmt .= "\n) ";
++ }
++ if (!empty($options["priority"])) {
++ $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
++ }
++ if (!empty($options["status"])) {
++ $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
++ }
++ if (!empty($options["category"])) {
++ if (!is_array($options['category'])) {
++ $options['category'] = array($options['category']);
++ }
++ $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
++ }
++ if (!empty($options["hide_closed"])) {
++ $stmt .= " AND sta_is_closed=0";
++ }
++ if (!empty($options['release'])) {
++ $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
++ }
++ // now for the date fields
++ $date_fields = array(
++ 'created_date',
++ 'updated_date',
++ 'last_response_date',
++ 'first_response_date',
++ 'closed_date'
++ );
++ foreach ($date_fields as $field_name) {
++ if (!empty($options[$field_name])) {
++ switch ($options[$field_name]['filter_type']) {
++ case 'greater':
++ $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
++ break;
++ case 'less':
++ $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
++ break;
++ case 'between':
++ $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
++ break;
++ case 'null':
++ $stmt .= " AND iss_$field_name IS NULL";
++ break;
++ case 'in_past':
++ if (strlen($options[$field_name]['time_period']) == 0) {
++ $options[$field_name]['time_period'] = 0;
++ }
++ $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
++ Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
++ break;
++ }
++ }
++ }
++ // custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
++ if (($field['fld_type'] == 'date') &&
++ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
++ }
++ } elseif ($field['fld_type'] == 'date') {
++ if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
++ continue;
++ }
++ $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
++ cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
++ } else if ($field['fld_type'] == 'integer') {
++ $value = $search_value['value'];
++ switch ($search_value['filter_type']) {
++ case 'ge': $cmp = '>='; break;
++ case 'le': $cmp = '<='; break;
++ case 'gt': $cmp = '>'; break;
++ case 'lt': $cmp = '<'; break;
++ default: $cmp = '='; break;
++ }
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
++ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
++ } else {
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
++ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
++ if ($field['fld_type'] == 'combo') {
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
++ } else {
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
++ }
++ $stmt .= ')';
++ }
++ }
++ }
++ // clear cached full-text values if we are not searching fulltext anymore
++ if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
++ Session::set('fulltext_string', '');
++ Session::set('fulltext_issues', '');
++ }
++ return $stmt;
++ }
++
++
++ /**
++ * Method used to get the previous and next issues that are available
++ * according to the current search parameters.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param array $options The search parameters
++ * @return array The list of issues
++ */
++ function getSides($issue_id, $options)
++ {
++ $usr_id = Auth::getUserID();
++ $role_id = Auth::getCurrentRole();
++
++ $stmt = "SELECT
++ iss_id,
++ " . self::getLastActionFields() . "
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
++ // join custom fields if we are searching by custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ if (($field['fld_type'] == 'date') &&
++ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
++ }
++ } else {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
++ }
++ }
++ }
++ $stmt .= ")";
++ // check for the custom fields we want to sort by
++ if (strstr($options['sort_by'], 'custom_field') !== false) {
++ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
++ $stmt .= "\n LEFT JOIN \n" .
++ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
++ ON
++ (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
++ }
++ if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id";
++ }
++ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
++ ON
++ iur_iss_id=iss_id";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
++ ON
++ sub_iss_id=iss_id";
++ }
++ if (@$options["sort_by"] == "pre_scheduled_date") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id = pre_id";
++ }
++ if (@$options['sort_by'] == 'prc_title') {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id = prc_id";
++ }
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ $stmt .= self::buildWhereClause($options);
++ if (strstr($options["sort_by"], 'custom_field') !== false) {
++ $fld_details = Custom_Field::getDetails($fld_id);
++ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
++ } else {
++ $sort_by = Misc::escapeString($options["sort_by"]);
++ }
++ $stmt .= "
++ GROUP BY
++ iss_id
++ ORDER BY
++ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
++ iss_id DESC";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ // COMPAT: the next line requires PHP >= 4.0.5
++ $index = array_search($issue_id, $res);
++ if (!empty($res[$index+1])) {
++ $next = $res[$index+1];
++ }
++ if (!empty($res[$index-1])) {
++ $previous = $res[$index-1];
++ }
++ return array(
++ "next" => @$next,
++ "previous" => @$previous
++ );
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of user IDs assigned to a specific
++ * issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of user IDs
++ */
++ function getAssignedUserIDs($issue_id)
++ {
++ $stmt = "SELECT
++ usr_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to see if a user is assigned to an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id An integer containg the ID of the user.
++ * @return boolean true if the user(s) are assigned to the issue.
++ */
++ function isAssignedToUser($issue_id, $usr_id)
++ {
++ $assigned_users = self::getAssignedUserIDs($issue_id);
++ if (in_array($usr_id, $assigned_users)) {
++ return true;
++ } else {
++ return false;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of reporters associated with a given
++ * list of issues.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getReportersByIssues(&$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ $ids = implode(", ", $ids);
++ $stmt = "SELECT
++ iss_id,
++ CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ iss_usr_id=usr_id AND
++ iss_id IN ($ids)";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ // now populate the $result variable again
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of assigned users by a list
++ * of issues. This was originally created to optimize the issue
++ * listing page.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getAssignedUsersByIssues(&$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ if (count($ids) < 1) {
++ return;
++ }
++ $ids = implode(", ", $ids);
++ $stmt = "SELECT
++ isu_iss_id,
++ isu_order,
++ isu_usr_id,
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_usr_id=usr_id AND
++ isu_iss_id IN ($ids)";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ // gather names of the users assigned to each issue
++ $t = array();
++ for ($i = 0; $i < count($res); $i++) {
++ if (!empty($t[$res[$i]['isu_iss_id']])) {
++ $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
++ } else {
++ $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
++ }
++ }
++ // gather orders
++ $o = array();
++ for ($i = 0; $i < count($res); $i++) {
++ if (empty($o[$res[$i]['isu_iss_id']])) {
++ $o[$res[$i]['isu_iss_id']] = array();
++ }
++ $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
++ }
++ // now populate the $result variable again
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
++ @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to add the issue description to a list of issues.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getDescriptionByIssues(&$result)
++ {
++ if (count($result) == 0) {
++ return;
++ }
++
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ $ids = implode(", ", $ids);
++
++ $stmt = "SELECT
++ iss_id,
++ iss_description
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id in ($ids)";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of users (the full names) assigned to a
++ * specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of users
++ */
++ function getAssignedUsers($issue_id)
++ {
++ $stmt = "SELECT
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the details for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $force_refresh If the cache should not be used.
++ * @return array The details for the specified issue
++ */
++ function getDetails($issue_id, $force_refresh = false)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (empty($issue_id)) {
++ return '';
++ }
++
++ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
++ prj_title,
++ prc_title,
++ pre_title,
++ pri_title,
++ sta_title,
++ sta_abbreviation,
++ sta_color status_color,
++ sta_is_closed
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
++ )
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id=prc_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id=pre_id
++ WHERE
++ iss_id=$issue_id AND
++ iss_prj_id=prj_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ if (empty($res)) {
++ return "";
++ } else {
++ $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
++ // get customer information, if any
++ if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
++ $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
++ $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
++ $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
++ $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
++ $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
++ $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
++ if (empty($res['iss_first_response_date'])) {
++ $first_response_deadline = $created_date_ts + $max_first_response_time;
++ if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
++ $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
++ } else {
++ $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
++ }
++ }
++ }
++ $res['iss_original_description'] = $res["iss_description"];
++ if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
++ $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
++ $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
++ }
++ $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
++ $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
++ $res['iss_created_date_ts'] = $created_date_ts;
++ $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
++ list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
++ $temp = self::getAssignedUsersStatus($res["iss_id"]);
++ $res["has_inactive_users"] = 0;
++ $res["assigned_users"] = array();
++ $res["assigned_inactive_users"] = array();
++ foreach ($temp as $usr_id => $usr_status) {
++ if (!User::isActiveStatus($usr_status)) {
++ $res["assigned_inactive_users"][] = $usr_id;
++ $res["has_inactive_users"] = 1;
++ } else {
++ $res["assigned_users"][] = $usr_id;
++ }
++ }
++ if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
++ $res["is_current_user_assigned"] = 1;
++ } else {
++ $res["is_current_user_assigned"] = 0;
++ }
++ $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
++ $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
++ $res["reporter"] = User::getFullName($res["iss_usr_id"]);
++ if (empty($res["iss_updated_date"])) {
++ $res["iss_updated_date"] = 'not updated yet';
++ } else {
++ $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
++ }
++ $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
++ if (Release::isAssignable($res["iss_pre_id"])) {
++ $release = Release::getDetails($res["iss_pre_id"]);
++ $res["pre_title"] = $release["pre_title"];
++ $res["pre_status"] = $release["pre_status"];
++ }
++ // need to return the list of issues that are duplicates of this one
++ $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
++ $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
++ // also get the issue title of the duplicated issue
++ if (!empty($res['iss_duplicated_iss_id'])) {
++ $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
++ }
++
++ // get group information
++ if (!empty($res["iss_grp_id"])) {
++ $res["group"] = Group::getDetails($res["iss_grp_id"]);
++ }
++
++ // get quarantine issue
++ $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
++
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get some simple details about the given duplicated issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The duplicated issue details
++ */
++ function getDuplicatedDetails($issue_id)
++ {
++ $stmt = "SELECT
++ iss_summary title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_sta_id=sta_id AND
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to bulk update a list of issues
++ *
++ * @access public
++ * @return boolean
++ */
++ function bulkUpdate()
++ {
++ // check if user performing this chance has the proper role
++ if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
++ return -1;
++ }
++
++ $items = Misc::escapeInteger($_POST['item']);
++ $new_status_id = Misc::escapeInteger($_POST['status']);
++ $new_release_id = Misc::escapeInteger(@$_POST['release']);
++ $new_priority_id = Misc::escapeInteger($_POST['priority']);
++ $new_category_id = Misc::escapeInteger($_POST['category']);
++
++ for ($i = 0; $i < count($items); $i++) {
++ if (!self::canAccess($items[$i], Auth::getUserID())) {
++ continue;
++ } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
++ // make sure issue is not in another project
++ continue;
++ }
++
++ $updated_fields = array();
++
++ // update assignment
++ if (count(@$_POST['users']) > 0) {
++ $users = Misc::escapeInteger($_POST['users']);
++ // get who this issue is currently assigned too
++ $stmt = "SELECT
++ isu_usr_id,
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_usr_id = usr_id AND
++ isu_iss_id = " . $items[$i];
++ $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($current_assignees)) {
++ Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ foreach ($current_assignees as $usr_id => $usr_name) {
++ if (!in_array($usr_id, $users)) {
++ self::deleteUserAssociation($items[$i], $usr_id, false);
++ }
++ }
++ $new_user_names = array();
++ $new_assignees = array();
++ foreach ($users as $usr_id) {
++ $new_user_names[$usr_id] = User::getFullName($usr_id);
++
++ // check if the issue is already assigned to this person
++ $stmt = "SELECT
++ COUNT(*) AS total
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id=" . $items[$i] . " AND
++ isu_usr_id=" . $usr_id;
++ $total = DB_Helper::getInstance()->getOne($stmt);
++ if ($total > 0) {
++ continue;
++ } else {
++ $new_assignees[] = $usr_id;
++ // add the assignment
++ self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
++ Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
++ Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
++ }
++ }
++ Notification::notifyNewAssignment($new_assignees, $items[$i]);
++ $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
++ }
++
++ // update status
++ if (!empty($new_status_id)) {
++ $old_status_id = self::getStatusID($items[$i]);
++ $res = self::setStatus($items[$i], $new_status_id, false);
++ if ($res == 1) {
++ $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
++ }
++ }
++
++ // update release
++ if (!empty($new_release_id)) {
++ $old_release_id = self::getRelease($items[$i]);
++ $res = self::setRelease($items[$i], $new_release_id);
++ if ($res == 1) {
++ $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
++ }
++ }
++
++ // update priority
++ if (!empty($new_priority_id)) {
++ $old_priority_id = self::getPriority($items[$i]);
++ $res = self::setPriority($items[$i], $new_priority_id);
++ if ($res == 1) {
++ $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
++ }
++ }
++
++ // update category
++ if (!empty($new_category_id)) {
++ $old_category_id = self::getCategory($items[$i]);
++ $res = self::setCategory($items[$i], $new_category_id);
++ if ($res == 1) {
++ $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
++ }
++ }
++
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $k = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($k > 0) {
++ $changes .= "; ";
++ }
++ $changes .= "$key: $value";
++ $k++;
++ }
++ History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
++ }
++
++ // close if request
++ if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
++ self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
++ }
++ }
++ return true;
++ }
++
++
++ /**
++ * Method used to set the initial impact analysis for a specific issue
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setImpactAnalysis($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='update',
++ iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
++ iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add the impact analysis to the history of the issue
++ $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs that area available in the
++ * system.
++ *
++ * @access public
++ * @param string $extra_condition An extra condition in the WHERE clause
++ * @return array The list of issue IDs
++ */
++ function getColList($extra_condition = NULL)
++ {
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ if (!empty($extra_condition)) {
++ $stmt .= " AND $extra_condition ";
++ }
++ $stmt .= "
++ ORDER BY
++ iss_id DESC";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs and their respective
++ * titles.
++ *
++ * @access public
++ * @param string $extra_condition An extra condition in the WHERE clause
++ * @return array The list of issues
++ */
++ function getAssocList($extra_condition = NULL)
++ {
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ if (!empty($extra_condition)) {
++ $stmt .= " AND $extra_condition ";
++ }
++ $stmt .= "
++ ORDER BY
++ iss_id ASC";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues associated to a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of associated issues
++ */
++ function getAssociatedIssues($issue_id)
++ {
++ $issues = self::getAssociatedIssuesDetails($issue_id);
++ $associated = array();
++ for ($i = 0; $i < count($issues); $i++) {
++ $associated[] = $issues[$i]['associated_issue'];
++ }
++ return $associated;
++ }
++
++
++ /**
++ * Method used to get the list of issues associated details to a
++ * specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of associated issues
++ */
++ function getAssociatedIssuesDetails($issue_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ isa_associated_id associated_issue,
++ iss_summary associated_title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ isa_associated_id=iss_id AND
++ iss_sta_id=sta_id AND
++ isa_issue_id=$issue_id";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to check whether an issue was already closed or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return boolean
++ */
++ function isClosed($issue_id)
++ {
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ iss_sta_id=sta_id AND
++ sta_is_closed=1";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Returns a simple list of issues that are currently set to some
++ * form of quarantine. This is mainly used by the IRC interface.
++ *
++ * @access public
++ * @return array List of quarantined issues
++ */
++ function getQuarantinedIssueList()
++ {
++ // XXX: would be nice to restrict the result list to only one project
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id=iss_id AND
++ iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
++ iqu_expiration IS NOT NULL";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ self::getAssignedUsersByIssues($res);
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the status of a quarantine.
++ *
++ * @param integer $issue_id The issue ID
++ * @return integer Indicates what the current state of quarantine is.
++ */
++ function getQuarantineInfo($issue_id)
++ {
++ $stmt = "SELECT
++ iqu_status,
++ iqu_expiration
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
++ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
++ iqu_expiration IS NULL)";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ if (!empty($res["iqu_expiration"])) {
++ $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
++ $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
++ }
++ return $res;
++ }
++ }
++
++
++ /**
++ * Sets the quarantine status. Optionally an expiration date can be set
++ * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $status The quarantine status
++ * @param string $expiration The expiration date of quarantine (default empty)
++ */
++ function setQuarantine($issue_id, $status, $expiration = '')
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $status = Misc::escapeInteger($status);
++
++ // see if there is an existing record
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ if ($res > 0) {
++ // update
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ SET
++ iqu_status = $status";
++ if (!empty($expiration)) {
++ $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
++ }
++ $stmt .= "\nWHERE
++ iqu_iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add history entry about this change taking place
++ if ($status == 0) {
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
++ "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
++ }
++ }
++ } else {
++ // insert
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ (
++ iqu_iss_id,
++ iqu_status";
++ if (!empty($expiration)) {
++ $stmt .= ",\niqu_expiration\n";
++ }
++ $stmt .= ") VALUES (
++ $issue_id,
++ $status";
++ if (!empty($expiration)) {
++ $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
++ }
++ $stmt .= ")";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ }
++ return 1;
++ }
++
++
++ /**
++ * Sets the group of the issue.
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $group_id The ID of the group
++ * @return integer 1 if successful, -1 or -2 otherwise
++ */
++ function setGroup($issue_id, $group_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $group_id = Misc::escapeInteger($group_id);
++
++ $current = self::getDetails($issue_id);
++ if ($current["iss_grp_id"] == $group_id) {
++ return -2;
++ }
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_grp_id = $group_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $current_user = Auth::getUserID();
++ if (empty($current_user)) {
++ $current_user = APP_SYSTEM_USER_ID;
++ }
++ History::add($issue_id, $current_user, History::getTypeID('group_changed'),
++ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
++ return 1;
++ }
++
++
++ /**
++ * Returns the group ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The associated group ID
++ */
++ function getGroupID($issue_id)
++ {
++ $stmt = "SELECT
++ iss_grp_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns an array of issues based on full text search results.
++ *
++ * @param array $options An array of search options
++ * @return array An array of issue IDS
++ */
++ function getFullTextIssues($options)
++ {
++ // check if a list of issues for this full text search is already cached
++ $fulltext_string = Session::get('fulltext_string');
++ if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
++ return Session::get('fulltext_issues');
++ }
++
++ // no pre-existing list, generate them
++ $stmt = "(SELECT
++ DISTINCT(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(not_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
++ WHERE
++ MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(ttr_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
++ WHERE
++ MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(phs_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
++ WHERE
++ MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(sup_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
++ WHERE
++ sup_id = seb_sup_id AND
++ MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ )";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array(-1);
++ } else {
++ $stmt = "SELECT
++ DISTINCT(icf_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
++ WHERE
++ MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
++ $custom_res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($custom_res)) {
++ Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
++ return array(-1);
++ }
++ $issues = array_merge($res, $custom_res);
++ // we kill the query results on purpose to flag that no
++ // issues could be found with fulltext search
++ if (count($issues) < 1) {
++ $issues = array(-1);
++ }
++ Session::set('fulltext_string', $options['keywords']);
++ Session::set('fulltext_issues', $issues);
++ return $issues;
++ }
++ }
++
++
++ /**
++ * Method to determine if user can access a particular issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue.
++ * @param integer $usr_id The ID of the user
++ * @return boolean If the user can access the issue
++ */
++ function canAccess($issue_id, $usr_id)
++ {
++ static $access;
++
++ if (empty($issue_id)) {
++ return true;
++ }
++
++ if (isset($access[$issue_id . "-" . $usr_id])) {
++ return $access[$issue_id . "-" . $usr_id];
++ }
++
++ $details = self::getDetails($issue_id);
++ if (empty($details)) {
++ return true;
++ }
++ $usr_details = User::getDetails($usr_id);
++ $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
++ $prj_id = self::getProjectID($issue_id);
++
++
++ if (empty($usr_role)) {
++ // check if they are even allowed to access the project
++ $return = false;
++ } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
++ ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
++ // check customer permissions
++ $return = false;
++ } elseif ($details['iss_private'] == 1) {
++ // check if the issue is even private
++
++ // check role, reporter, assigment and group
++ if ($usr_role > User::getRoleID("Developer")) {
++ $return = true;
++ } elseif ($details['iss_usr_id'] == $usr_id) {
++ $return = true;
++ } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
++ $return = true;
++ } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
++ ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
++ $return = true;
++ } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
++ $return = true;
++ } else {
++ $return = false;
++ }
++ } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
++ ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
++ return false;
++ } else {
++ $return = true;
++ }
++
++ $access[$issue_id . "-" . $usr_id] = $return;
++ return $return;
++ }
++
++
++ /**
++ * Returns true if the specified issue is private, false otherwise
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return boolean If the issue is private or not
++ */
++ function isPrivate($issue_id)
++ {
++ static $returns;
++
++ if (!isset($returns[$issue_id])) {
++ $sql = "SELECT
++ iss_private
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return true;
++ } else {
++ if ($res == 1) {
++ $returns[$issue_id] = true;
++ } else {
++ $returns[$issue_id] = false;
++ }
++ }
++ }
++ return $returns[$issue_id];
++ }
++
++
++ /**
++ * Clears closed information from an issues.
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ */
++ function clearClosed($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_closed_date = null,
++ iss_res_id = null
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ self::moveOrderForAllUsers($issue_id, 1);
++ }
++
++
++ /**
++ * Returns the message ID that should be used as the parent ID for all messages
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ */
++ function getRootMessageID($issue_id)
++ {
++ $sql = "SELECT
++ iss_root_message_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the issue ID of the issue with the specified root message ID, or false
++ * @access public
++ * @param string $msg_id The Message ID
++ * @return integer The ID of the issue
++ */
++ function getIssueByRootMessageID($msg_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$msg_id])) {
++ return $returns[$msg_id];
++ }
++ $sql = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ }
++ if (empty($res)) {
++ $returns[$msg_id] = false;
++ } else {
++ $returns[$msg_id] = $res;
++ }
++ return $returns[$msg_id];
++ }
++
++
++ /**
++ * Sets the assignees for the issue
++ *
++ * @param integer $issue_id
++ * @param array $assignees
++ */
++ function setAssignees($issue_id, $assignees)
++ {
++ if (!is_array($assignees)) {
++ $assignees = array();
++ }
++
++ // see if there is anything to change
++ $old_assignees = self::getAssignedUserIDs($issue_id);
++ if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
++ return;
++ }
++
++ $old_assignee_names = self::getAssignedUsers($issue_id);
++
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
++ // clear up the assignments for this issue, and then assign it to the current user
++ self::deleteUserAssociations($issue_id);
++ $assignee_names = array();
++ foreach ($assignees as $assignee) {
++ $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
++ if ($res == -1) {
++ return false;
++ }
++ $assignee_names[] = User::getFullName($assignee);
++ Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
++ }
++
++ Notification::notifyNewAssignment($assignees, $issue_id);
++
++ // save a history entry about this...
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
++ "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
++ }
++
++ /**
++ * Reorders user's issues as requested by user
++ * @access public
++ * @param $usr_id User to be reordered
++ * @param $issue_id Issue or array of issues to be moved
++ * @param $neworder The new order of the issues
++ * @return void
++ */
++ function reorderUserIssues($usr_id, $issue_id, $neworder)
++ {
++ if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
++ return false;
++ }
++ if (!is_numeric($usr_id) || !is_numeric($neworder)) {
++ return false;
++ }
++ $usr_id = Misc::escapeInteger($usr_id);
++ $issue_id = Misc::escapeInteger($issue_id);
++ $neworder = Misc::escapeInteger($neworder);
++ if (is_array($issue_id)) {
++ $issue_count = count($issue_id);
++ $issue_id_str = implode(", ", $issue_id);
++ } else {
++ $issue_count = 1;
++ $issue_id_str = $issue_id;
++ $issue_id = array($issue_id);
++ }
++ // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
++ $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
++ self::rearrangeDeleteUserAssociationOrderList($orderlist);
++ // move down the orders to free the "order space" needed
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order + $issue_count
++ WHERE
++ isu_usr_id = $usr_id AND
++ isu_order >= $neworder";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ //update the order for the issues being moved
++ $i = 0;
++ foreach ($issue_id as $iss_id) {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = " . ($neworder + $i) . "
++ WHERE
++ isu_usr_id = $usr_id AND
++ isu_iss_id = $iss_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $i++;
++ }
++ }
++
++
++ /**
++ * Get users issue order list
++ * @access public
++ * @param $user_id User
++ * @param $order_list Order of the issues
++ * @return void
++ */
++ function getIssueOrderByUser($usr_id) {
++
++ if (!is_numeric($usr_id)) {
++ return false;
++ }
++
++ $stmt = "SELECT
++ isu_iss_id, isu_order
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_usr_id = " . $usr_id ;
++
++ $order_list = array();
++
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ foreach ($res as $row) {
++ $order_list[$row["isu_iss_id"]] = $row["isu_order"];
++ }
++ }
++ return $order_list;
++ }
++
++ function moveOrderForAllUsers($issue_id, $neworder)
++ {
++ // Move the issue to the top priority for the ppl it's assigned to
++ $stmt = "SELECT isu_usr_id FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ foreach ($res as $row) {
++ self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
++ }
++ }
++
++}
+--- eventum-2.2/lib/eventum/class.issue.php.~3~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/lib/eventum/class.issue.php.~3~ 2009-10-12 22:10:36.442518536 +0300
+@@ -0,0 +1,4752 @@
++<?php
++/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
++// +----------------------------------------------------------------------+
++// | Eventum - Issue Tracking System |
++// +----------------------------------------------------------------------+
++// | Copyright (c) 2003 - 2008 MySQL AB |
++// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
++// | |
++// | This program is free software; you can redistribute it and/or modify |
++// | it under the terms of the GNU General Public License as published by |
++// | the Free Software Foundation; either version 2 of the License, or |
++// | (at your option) any later version. |
++// | |
++// | This program is distributed in the hope that it will be useful, |
++// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
++// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
++// | GNU General Public License for more details. |
++// | |
++// | You should have received a copy of the GNU General Public License |
++// | along with this program; if not, write to: |
++// | |
++// | Free Software Foundation, Inc. |
++// | 59 Temple Place - Suite 330 |
++// | Boston, MA 02111-1307, USA. |
++// +----------------------------------------------------------------------+
++// | Authors: João Prado Maia <jpm@mysql.com> |
++// +----------------------------------------------------------------------+
++//
++
++
++/**
++ * Class designed to handle all business logic related to the issues in the
++ * system, such as adding or updating them or listing them in the grid mode.
++ *
++ * @author João Prado Maia <jpm@mysql.com>
++ * @version $Revision$
++ */
++
++class Issue
++{
++ /**
++ * Method used to check whether a given issue ID exists or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $check_project If we should check that this issue is in the current project
++ * @return boolean
++ */
++ function exists($issue_id, $check_project = true)
++ {
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ if ($check_project) {
++ $stmt .= " AND
++ iss_prj_id = " . Auth::getCurrentProject();
++ }
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the list of column heading titles for the
++ * CSV export functionality of the issue listing screen.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @return array The list of column heading titles
++ */
++ function getColumnHeadings($prj_id)
++ {
++ $headings = array(
++ 'Priority',
++ 'Issue ID',
++ 'Reporter',
++ );
++ // hide the group column from the output if no
++ // groups are available in the database
++ $groups = Group::getAssocList($prj_id);
++ if (count($groups) > 0) {
++ $headings[] = 'Group';
++ }
++ $headings[] = 'Assigned';
++ $headings[] = 'Time Spent';
++ // hide the category column from the output if no
++ // categories are available in the database
++ $categories = Category::getAssocList($prj_id);
++ if (count($categories) > 0) {
++ $headings[] = 'Category';
++ }
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $headings[] = 'Customer';
++ }
++ $headings[] = 'Status';
++ $headings[] = 'Status Change Date';
++ $headings[] = 'Last Action Date';
++ $headings[] = 'Est. Dev. TIme';
++ $headings[] = 'Summary';
++ $headings[] = 'Expected Resolution Date';
++ return $headings;
++ }
++
++
++ /**
++ * Method used to get the full list of date fields available to issues, to
++ * be used when customizing the issue listing screen in the 'last status
++ * change date' column.
++ *
++ * @access public
++ * @param boolean $display_customer_fields Whether to include any customer related fields or not
++ * @return array The list of available date fields
++ */
++ function getDateFieldsAssocList($display_customer_fields = FALSE)
++ {
++ $fields = array(
++ 'iss_created_date' => 'Created Date',
++ 'iss_updated_date' => 'Last Updated Date',
++ 'iss_last_response_date' => 'Last Response Date',
++ 'iss_closed_date' => 'Closed Date'
++ );
++ if ($display_customer_fields) {
++ $fields['iss_last_customer_action_date'] = 'Customer Action Date';
++ }
++ asort($fields);
++ return $fields;
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs and their respective
++ * titles associated to a given project.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @return array The list of issues
++ */
++ function getAssocListByProject($prj_id)
++ {
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Misc::escapeInteger($prj_id) . "
++ ORDER BY
++ iss_id ASC";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The status ID
++ */
++ function getStatusID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_sta_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Records the last customer action date for a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function recordLastCustomerAction($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='customer action'
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++
++
++ /**
++ * Returns the customer ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getCustomerID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the contract ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getContractID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_contract_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Sets the contract ID for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer The contract ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setContractID($issue_id, $contract_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $old_contract_id = self::getContractID($issue_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // log this
++ History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ /**
++ * Returns the customer ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The customer ID associated with the issue
++ */
++ function getContactID($issue_id)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_customer_contact_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the project associated to a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $force_refresh If the cache should not be used.
++ * @return integer The project ID
++ */
++ function getProjectID($issue_id, $force_refresh = false)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_prj_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to remotely assign a given issue to an user.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing the change
++ * @param boolean $assignee The user ID of the assignee
++ * @return integer The status ID
++ */
++ function remoteAssign($issue_id, $usr_id, $assignee)
++ {
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
++ // clear up the assignments for this issue, and then assign it to the current user
++ self::deleteUserAssociations($issue_id, $usr_id);
++ $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
++ if ($res != -1) {
++ // save a history entry about this...
++ History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
++ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
++ if ($assignee != $usr_id) {
++ Notification::notifyNewAssignment(array($assignee), $issue_id);
++ }
++ }
++ return $res;
++ }
++
++
++ /**
++ * Method used to set the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $status_id The new status ID
++ * @param boolean $notify If a notification should be sent about this change.
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setStatus($issue_id, $status_id, $notify = false)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $status_id = Misc::escapeInteger($status_id);
++
++ $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
++ if ($workflow !== true) {
++ return $workflow;
++ }
++
++ // check if the status is already set to the 'new' one
++ if (self::getStatusID($issue_id) == $status_id) {
++ return -1;
++ }
++
++ $old_status = self::getStatusID($issue_id);
++ $old_details = Status::getDetails($old_status);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_sta_id=$status_id,
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='update'
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ if (@$old_details['sta_is_closed'] == 1) {
++ $new_details = Status::getDetails($status_id);
++ if ($new_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++
++ if ($notify) {
++ Notification::notifyStatusChange($issue_id, $old_status, $status_id);
++ }
++
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to remotely set the status of a given issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing this change
++ * @param integer $new_status The new status ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setRemoteStatus($issue_id, $usr_id, $new_status)
++ {
++ $sta_id = Status::getStatusID($new_status);
++
++ $res = self::setStatus($issue_id, $sta_id);
++ if ($res == 1) {
++ // record history entry
++ History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
++ }
++ return $res;
++ }
++
++
++ /**
++ * Method used to set the release of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $pre_id The ID of the release to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setRelease($issue_id, $pre_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $pre_id = Misc::escapeInteger($pre_id);
++
++ if ($pre_id != self::getRelease($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_pre_id = $pre_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current release of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The release
++ */
++ function getRelease($issue_id)
++ {
++ $sql = "SELECT
++ iss_pre_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to set the priority of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $pri_id The ID of the priority to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setPriority($issue_id, $pri_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $pri_id = Misc::escapeInteger($pri_id);
++
++ if ($pri_id != self::getPriority($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_pri_id = $pri_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current issue priority
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The priority
++ */
++ function getPriority($issue_id)
++ {
++ $sql = "SELECT
++ iss_pri_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to set the category of an issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $prc_id The ID of the category to set this issue too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setCategory($issue_id, $prc_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $prc_id = Misc::escapeInteger($prc_id);
++
++ if ($prc_id != self::getPriority($issue_id)) {
++ $sql = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prc_id = $prc_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ return 1;
++ }
++ }
++ }
++
++
++ /**
++ * Returns the current issue category
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return integer The category
++ */
++ function getCategory($issue_id)
++ {
++ $sql = "SELECT
++ iss_prc_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get all issues associated with a status that doesn't have
++ * the 'closed' context.
++ *
++ * @access public
++ * @param integer $prj_id The project ID to list issues from
++ * @param integer $usr_id The user ID of the user requesting this information
++ * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
++ * @param integer $status_id The status ID to be used to restrict results
++ * @return array The list of open issues
++ */
++ function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
++ {
++ $prj_id = Misc::escapeInteger($prj_id);
++ $status_id = Misc::escapeInteger($status_id);
++ $projects = Project::getRemoteAssocListByUser($usr_id);
++ if (@count($projects) == 0) {
++ return '';
++ }
++
++ $stmt = "SELECT
++ iss_id,
++ iss_summary,
++ sta_title
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ )
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id
++ WHERE ";
++ if (!empty($status_id)) {
++ $stmt .= " sta_id=$status_id AND ";
++ }
++ $stmt .= "
++ iss_prj_id=$prj_id AND
++ sta_id=iss_sta_id AND
++ sta_is_closed=0";
++ if ($show_all_issues == false) {
++ $stmt .= " AND
++ isu_usr_id=$usr_id";
++ }
++ $stmt .= "\nGROUP BY
++ iss_id";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ if (count($res) > 0) {
++ self::getAssignedUsersByIssues($res);
++ }
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to build the required parameters to simulate an email reply
++ * to the user who reported the issue, using the issue details like summary
++ * and description as email fields.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The email parameters
++ */
++ function getReplyDetails($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $stmt = "SELECT
++ iss_created_date,
++ usr_full_name AS reporter,
++ usr_email AS reporter_email,
++ iss_description AS description,
++ iss_summary AS sup_subject
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ iss_usr_id=usr_id AND
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return '';
++ } else {
++ $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
++ $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to record the last updated timestamp for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param string $type The type of update that was made (optional)
++ * @return boolean
++ */
++ function markAsUpdated($issue_id, $type = false)
++ {
++ $public = array("staff response", "customer action", "file uploaded", "user response");
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
++ if ($type != false) {
++ if (in_array($type, $public)) {
++ $field = "iss_last_public_action_";
++ } else {
++ $field = "iss_last_internal_action_";
++ }
++ $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
++ $field . "type ='" . Misc::escapeString($type) . "'\n";
++ }
++ $stmt .= "WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ // update last response dates if this is a staff response
++ if ($type == "staff response") {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ DB_Helper::getInstance()->query($stmt);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
++ WHERE
++ iss_first_response_date IS NULL AND
++ iss_id = " . Misc::escapeInteger($issue_id);
++ DB_Helper::getInstance()->query($stmt);
++ }
++
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to check whether a given issue has duplicates
++ * or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return boolean
++ */
++ function hasDuplicates($issue_id)
++ {
++ $stmt = "SELECT
++ COUNT(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to update the duplicated issues for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function updateDuplicates($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $ids = self::getDuplicateList($issue_id);
++ if ($ids == '') {
++ return -1;
++ }
++ $ids = @array_keys($ids);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
++ if (@$_POST["keep"] == "no") {
++ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
++ }
++ $stmt .= "
++ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
++ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
++ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
++ WHERE
++ iss_id IN (" . implode(", ", $ids) . ")";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // record the change
++ for ($i = 0; $i < count($ids); $i++) {
++ History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
++ "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
++ }
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to get a list of the duplicate issues for a given
++ * issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of duplicates
++ */
++ function getDuplicateList($issue_id)
++ {
++ $res = self::getDuplicateDetailsList($issue_id);
++ if (@count($res) == 0) {
++ return '';
++ } else {
++ $list = array();
++ for ($i = 0; $i < count($res); $i++) {
++ $list[$res[$i]['issue_id']] = $res[$i]['title'];
++ }
++ return $list;
++ }
++ }
++
++
++ /**
++ * Method used to get a list of the duplicate issues (and their details)
++ * for a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of duplicates
++ */
++ function getDuplicateDetailsList($issue_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ iss_id issue_id,
++ iss_summary title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_sta_id=sta_id AND
++ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to clear the duplicate status of an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function clearDuplicateStatus($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_duplicated_iss_id=NULL
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // record the change
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to mark an issue as a duplicate of an existing one.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function markAsDuplicate($issue_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (!self::exists($issue_id)) {
++ return -1;
++ }
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='updated',
++ iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if (!empty($_POST["comments"])) {
++ // add note with the comments of marking an issue as a duplicate of another one
++ $_POST['title'] = 'Issue duplication comments';
++ $_POST["note"] = $_POST["comments"];
++ Note::insert(Auth::getUserID(), $issue_id);
++ }
++ // record the change
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
++ "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
++ return 1;
++ }
++ }
++
++
++ function isDuplicate($issue_id)
++ {
++ $sql = "SELECT
++ count(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id) . " AND
++ iss_duplicated_iss_id IS NULL";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ }
++ if ($res > 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to get an associative array of user ID => user
++ * status associated with a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of users
++ */
++ function getAssignedUsersStatus($issue_id)
++ {
++ $stmt = "SELECT
++ usr_id,
++ usr_status
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the summary associated with a given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return string The issue summary
++ */
++ function getTitle($issue_id)
++ {
++ $stmt = "SELECT
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the issue ID associated with a specific summary.
++ *
++ * @access public
++ * @param string $summary The summary to look for
++ * @return integer The issue ID
++ */
++ function getIssueID($summary)
++ {
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_summary='" . Misc::escapeString($summary) . "'";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ if (empty($res)) {
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to add a new anonymous based issue in the system.
++ *
++ * @access public
++ * @return integer The new issue ID
++ */
++ function addAnonymousReport()
++ {
++ $options = Project::getAnonymousPostOptions($_POST["project"]);
++ $initial_status = Project::getInitialStatus($_POST["project"]);
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ (
++ iss_prj_id,
++ iss_prc_id,
++ iss_pre_id,
++ iss_pri_id,
++ iss_usr_id,";
++ if (!empty($initial_status)) {
++ $stmt .= "iss_sta_id,";
++ }
++ $stmt .= "
++ iss_created_date,
++ iss_last_public_action_date,
++ iss_last_public_action_type,
++ iss_summary,
++ iss_description,
++ iss_root_message_id
++ ) VALUES (
++ " . Misc::escapeInteger($_POST["project"]) . ",
++ " . $options["category"] . ",
++ 0,
++ " . $options["priority"] . ",
++ " . $options["reporter"] . ",";
++ if (!empty($initial_status)) {
++ $stmt .= "$initial_status,";
++ }
++ $stmt .= "
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ 'created',
++ '" . Misc::escapeString($_POST["summary"]) . "',
++ '" . Misc::escapeString($_POST["description"]) . "',
++ '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
++ )";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return $res;
++ } else {
++ $new_issue_id = DB_Helper::get_last_insert_id();
++ // log the creation of the issue
++ History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
++
++ // now process any files being uploaded
++ $found = 0;
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ if (!@empty($_FILES["file"]["name"][$i])) {
++ $found = 1;
++ break;
++ }
++ }
++ if ($found) {
++ $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ $filename = @$_FILES["file"]["name"][$i];
++ if (empty($filename)) {
++ continue;
++ }
++ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
++ if (!empty($blob)) {
++ Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
++ }
++ }
++ }
++ // need to process any custom fields ?
++ if (@count($_POST["custom_fields"]) > 0) {
++ foreach ($_POST["custom_fields"] as $fld_id => $value) {
++ Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
++ }
++ }
++
++ // now add the user/issue association
++ $assign = array();
++ $users = @$options["users"];
++ $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
++ for ($i = 0; $i < count($users); $i++) {
++ Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
++ $assign[] = $users[$i];
++ }
++
++ Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($_POST['project'], $new_issue_id);
++
++ return $new_issue_id;
++ }
++ }
++
++
++ /**
++ * Method used to remove all issues associated with a specific list of
++ * projects.
++ *
++ * @access public
++ * @param array $ids The list of projects to look for
++ * @return boolean
++ */
++ function removeByProjects($ids)
++ {
++ $items = @implode(", ", Misc::escapeInteger($ids));
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id IN ($items)";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if (count($res) > 0) {
++ self::deleteAssociations($res);
++ Attachment::removeByIssues($res);
++ SCM::removeByIssues($res);
++ Impact_Analysis::removeByIssues($res);
++ self::deleteUserAssociations($res);
++ Note::removeByIssues($res);
++ Time_Tracking::removeByIssues($res);
++ Notification::removeByIssues($res);
++ Custom_Field::removeByIssues($res);
++ Phone_Support::removeByIssues($res);
++ History::removeByIssues($res);
++ // now really delete the issues
++ $items = implode(", ", $res);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id IN ($items)";
++ DB_Helper::getInstance()->query($stmt);
++ }
++ return true;
++ }
++ }
++
++
++ /**
++ * Method used to close off an issue.
++ *
++ * @access public
++ * @param integer $usr_id The user ID
++ * @param integer $issue_id The issue ID
++ * @param bool $send_notification Whether to send a notification about this action or not
++ * @param integer $resolution_id The resolution ID
++ * @param integer $status_id The status ID
++ * @param string $reason The reason for closing this issue
++ * @param string $send_notification_to Who this notification should be sent too
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
++ {
++ $usr_id = Misc::escapeInteger($usr_id);
++ $issue_id = Misc::escapeInteger($issue_id);
++ $resolution_id = Misc::escapeInteger($resolution_id);
++ $status_id = Misc::escapeInteger($status_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='closed',
++ iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
++ if (!empty($resolution_id)) {
++ $stmt .= "iss_res_id=$resolution_id,\n";
++ }
++ $stmt .= "iss_sta_id=$status_id
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ self::moveOrderForAllUsers($issue_id, 1000);
++ $prj_id = self::getProjectID($issue_id);
++
++ // record the change
++ History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
++
++ if ($send_notification_to == 'all') {
++
++ $from = User::getFromHeader($usr_id);
++ $message_id = User::getFromHeader($usr_id);
++ $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
++ '', '', 'Issue closed comments', $reason, '');
++
++ $structure = Mime_Helper::decode($full_email, true, false);
++
++ $email = array(
++ 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
++ 'issue_id' => $issue_id,
++ 'message_id' => $message_id,
++ 'date' => Date_Helper::getCurrentDateGMT(),
++ 'subject' => 'Issue closed comments',
++ 'from' => $from,
++ 'has_attachment'=> 0,
++ 'body' => $reason,
++ 'full_email' => $full_email,
++ 'headers' => $structure->headers
++ );
++ Support::insertEmail($email, $structure, $sup_id, true);
++ $ids = $sup_id;
++ } else {
++ // add note with the reason to close the issue
++ $_POST['title'] = 'Issue closed comments';
++ $_POST["note"] = $reason;
++ Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
++ $ids = false;
++ }
++
++ if ($send_notification) {
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ // send a special confirmation email when customer issues are closed
++ $stmt = "SELECT
++ iss_customer_contact_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
++ if (!empty($customer_contact_id)) {
++ Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
++ }
++ }
++ // send notifications for the issue being closed
++ Notification::notify($issue_id, 'closed', $ids);
++ }
++ Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to update the details of a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 or -2 otherwise
++ */
++ function update($issue_id)
++ {
++ global $errors;
++ $errors = array();
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $usr_id = Auth::getUserID();
++ $prj_id = self::getProjectID($issue_id);
++
++ $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
++ if ($workflow !== true) {
++ return $workflow;
++ }
++
++ // get all of the 'current' information of this issue
++ $current = self::getDetails($issue_id);
++ // update the issue associations
++ if (empty($_POST['associated_issues'])) {
++ $associated_issues = array();
++ } else {
++ $associated_issues = explode(',', @$_POST['associated_issues']);
++ // make sure all associated issues are valid (and in this project)
++ for ($i = 0; $i < count($associated_issues); $i++) {
++ if (!self::exists(trim($associated_issues[$i]), false)) {
++ $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
++ unset($associated_issues[$i]);
++ }
++ }
++ }
++ $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
++ if (count($association_diff) > 0) {
++ // go through the new assocations, if association already exists, skip it
++ $associations_to_remove = $current['associated_issues'];
++ if (count($associated_issues) > 0) {
++ foreach ($associated_issues as $index => $associated_id) {
++ if (!in_array($associated_id, $current['associated_issues'])) {
++ self::addAssociation($issue_id, $associated_id, $usr_id);
++ } else {
++ // already assigned, remove this user from list of users to remove
++ unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
++ }
++ }
++ }
++ if (count($associations_to_remove) > 0) {
++ foreach ($associations_to_remove as $associated_id) {
++ self::deleteAssociation($issue_id, $associated_id);
++ }
++ }
++ }
++ $assignments_changed = false;
++ if (@$_POST["keep_assignments"] == "no") {
++ // only change the issue-user associations if there really were any changes
++ $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
++ if (!empty($_POST['assignments'])) {
++ $new_assignees = @$_POST['assignments'];
++ } else {
++ $new_assignees = array();
++ }
++ $assignment_notifications = array();
++
++ // remove people from the assignment list, if appropriate
++ foreach ($old_assignees as $assignee) {
++ if (!in_array($assignee, $new_assignees)) {
++ self::deleteUserAssociation($issue_id, $assignee);
++ $assignments_changed = true;
++ }
++ }
++ // add people to the assignment list, if appropriate
++ foreach ($new_assignees as $assignee) {
++ if (!in_array($assignee, $old_assignees)) {
++ self::addUserAssociation($usr_id, $issue_id, $assignee);
++ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
++ $assignment_notifications[] = $assignee;
++ $assignments_changed = true;
++ }
++ }
++ if (count($assignment_notifications) > 0) {
++ Notification::notifyNewAssignment($assignment_notifications, $issue_id);
++ }
++ }
++ if (empty($_POST["estimated_dev_time"])) {
++ $_POST["estimated_dev_time"] = 0;
++ }
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='updated',";
++ if (!empty($_POST["category"])) {
++ $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
++ }
++ if (@$_POST["keep"] == "no") {
++ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
++ }
++ if (!empty($_POST['expected_resolution_date'])) {
++ $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
++ } else {
++ $stmt .= "iss_expected_resolution_date=null,";
++ }
++ $stmt .= "
++ iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
++ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
++ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
++ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
++ iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
++ iss_description='" . Misc::escapeString($_POST["description"]) . "',
++ iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
++ iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
++ iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
++ iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
++ if (isset($_POST['private'])) {
++ $stmt .= ",
++ iss_private = " . Misc::escapeInteger($_POST['private']);
++ }
++ $stmt .= "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add change to the history (only for changes on specific fields?)
++ $updated_fields = array();
++ if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
++ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
++ }
++ if ($current["iss_prc_id"] != $_POST["category"]) {
++ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
++ }
++ if ($current["iss_pre_id"] != $_POST["release"]) {
++ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
++ }
++ if ($current["iss_pri_id"] != $_POST["priority"]) {
++ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
++ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
++ }
++ if ($current["iss_sta_id"] != $_POST["status"]) {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ $old_status_details = Status::getDetails($current['iss_sta_id']);
++ if ($old_status_details['sta_is_closed'] == 1) {
++ $new_status_details = Status::getDetails($_POST["status"]);
++ if ($new_status_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
++ }
++ if ($current["iss_res_id"] != $_POST["resolution"]) {
++ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
++ }
++ if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
++ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
++ }
++ if ($current["iss_summary"] != $_POST["summary"]) {
++ $updated_fields["Summary"] = '';
++ }
++ if ($current["iss_description"] != $_POST["description"]) {
++ $updated_fields["Description"] = '';
++ }
++ if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
++ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
++ }
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $i = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($i > 0) {
++ $changes .= "; ";
++ }
++ if (($key != "Summary") && ($key != "Description")) {
++ $changes .= "$key: $value";
++ } else {
++ $changes .= "$key";
++ }
++ $i++;
++ }
++ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
++ // send notifications for the issue being updated
++ Notification::notifyIssueUpdated($issue_id, $current, $_POST);
++ }
++
++ // record group change as a seperate change
++ if ($current["iss_grp_id"] != (int)$_POST["group"]) {
++ History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
++ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
++ }
++
++ // now update any duplicates, if any
++ $update_dupe = array(
++ 'Category',
++ 'Release',
++ 'Priority',
++ 'Release',
++ 'Resolution'
++ );
++ // COMPAT: the following line requires PHP > 4.0.4
++ $intersect = array_intersect($update_dupe, array_keys($updated_fields));
++ if (($current["duplicates"] != '') && (count($intersect) > 0)) {
++ self::updateDuplicates($issue_id);
++ }
++
++ // if there is customer integration, mark last customer action
++ if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
++ self::recordLastCustomerAction($issue_id);
++ }
++
++ if ($assignments_changed) {
++ // XXX: we may want to also send the email notification for those "new" assignees
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
++ }
++
++ Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
++ // Move issue to another project
++ if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
++ $new_prj_id = (int)@$_POST['new_prj'];
++ if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
++ if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
++ $res = self::moveIssue($issue_id, $new_prj_id);
++ if ($res == -1) {
++ return $res;
++ }
++ } else {
++ return -1;
++ }
++ }
++ }
++ return 1;
++ }
++ }
++
++ /**
++ * Method used to update the a single detail field of a specific issue.
++ *
++ * @param integer $issue_id
++ * @param string $field_name
++ * @param string $field_value
++ * @param string $field_type string or integer (for escape)
++ * @return integer 1 on success, -1 otherwise
++ */
++ function updateField($issue_id, $field_name, $filed_value) {
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ $usr_id = Auth::getUserID();
++ $prj_id = self::getProjectID($issue_id);
++
++ // get all of the 'current' information of this issue
++ $current = self::getDetails($issue_id);
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='updated'";
++
++ switch ($field_name) {
++ case 'category':
++ $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'release':
++ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'expected_resolution_date':
++ if (is_null($filed_value)) {
++ $stmt .= ", iss_expected_resolution_date = null";
++ } else {
++ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
++ }
++ break;
++ case 'release':
++ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'priority':
++ $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'status':
++ $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'resolution':
++ $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'summary':
++ $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'description':
++ $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'estimated_dev_time':
++ $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'percent_complete':
++ $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
++ break;
++ case 'trigger_reminders':
++ $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'group':
++ $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
++ break;
++ case 'private':
++ $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
++ break;
++ default:
++ Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
++ return -1;
++ break;
++ }
++
++ $stmt .= "
++ WHERE
++ iss_id=$issue_id";
++
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $new = array(
++ 'category' => $current['iss_prc_id'],
++ 'release' => $current['iss_pre_id'],
++ 'expected_resolution_date' => $current['iss_expected_resolution_date'],
++ 'release' => $current['iss_pre_id'],
++ 'priority' => $current['iss_pri_id'],
++ 'status' => $current['iss_sta_id'],
++ 'resolution' => $current['iss_res_id'],
++ 'summary' => $current['iss_summary'],
++ 'description' => $current['iss_description'],
++ 'estimated_dev_time' => $current['iss_dev_time'],
++ 'percent_complete' => $current['iss_percent_complete'],
++ 'trigger_reminders' => $current['iss_trigger_reminders'],
++ 'group' => $current['iss_grp_id'],
++ 'iss_private' => $current['private']
++ );
++ $new[$field_name] = $filed_value;
++
++ // add change to the history (only for changes on specific fields?)
++ $updated_fields = array();
++ if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
++ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
++ }
++ if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
++ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
++ }
++ if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
++ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
++ }
++ if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
++ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
++ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
++ }
++ if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
++ // clear out the last-triggered-reminder flag when changing the status of an issue
++ Reminder_Action::clearLastTriggered($issue_id);
++
++ // if old status was closed and new status is not, clear closed data from issue.
++ $old_status_details = Status::getDetails($current['iss_sta_id']);
++ if ($old_status_details['sta_is_closed'] == 1) {
++ $new_status_details = Status::getDetails($filed_value);
++ if ($new_status_details['sta_is_closed'] != 1) {
++ self::clearClosed($issue_id);
++ }
++ }
++ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
++ }
++ if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
++ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
++ }
++ if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
++ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
++ }
++ if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
++ $updated_fields["Summary"] = '';
++ }
++ if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
++ $updated_fields["Description"] = '';
++ }
++ if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
++ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
++ }
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $i = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($i > 0) {
++ $changes .= "; ";
++ }
++ if (($key != "Summary") && ($key != "Description")) {
++ $changes .= "$key: $value";
++ } else {
++ $changes .= "$key";
++ }
++ $i++;
++ }
++
++ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
++ // send notifications for the issue being updated
++ Notification::notifyIssueUpdated($issue_id, $current, $new);
++ }
++ }
++ return 1;
++ }
++
++
++ /**
++ * Move the issue to a new project
++ *
++ * @param integer $issue_id
++ * @param integer $new_prj_id
++ * @return integer 1 on success, -1 otherwise
++ */
++ function moveIssue($issue_id, $new_prj_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
++ WHERE
++ iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $currentDetails = self::getDetails($issue_id);
++
++ // set new category
++ $new_iss_prc_list = Category::getAssocList($new_prj_id);
++ $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
++ $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
++ if ($new_prc_id === false) {
++ // use the first category listed in the new project
++ $new_prc_id = key($new_iss_prc_list);
++ }
++
++ // set new priority
++ $new_iss_pri_list = Priority::getAssocList($new_prj_id);
++ $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
++ $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
++ if ($new_pri_id === false) {
++ // use the first category listed in the new project
++ $new_pri_id = key($new_iss_pri_list);
++ }
++
++ // XXX: Set status if needed when moving issue
++
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
++ iss_pri_id=" . $new_pri_id . "
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ }
++
++ // clear project cache
++ self::getProjectID($issue_id, true);
++
++ Notification::notifyNewIssue($new_prj_id, $issue_id);
++ }
++ }
++
++
++ /**
++ * Method used to associate an existing issue with another one.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $issue_id The other issue ID
++ * @return void
++ */
++ function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $associated_id = Misc::escapeInteger($associated_id);
++
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ (
++ isa_issue_id,
++ isa_associated_id
++ ) VALUES (
++ $issue_id,
++ $associated_id
++ )";
++ DB_Helper::getInstance()->query($stmt);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
++ // link the associated issue back to this one
++ if ($link_issues) {
++ self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
++ }
++ }
++
++
++ /**
++ * Method used to remove the issue associations related to a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return void
++ */
++ function deleteAssociations($issue_id, $usr_id = FALSE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (is_array($issue_id)) {
++ $issue_id = implode(", ", $issue_id);
++ }
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ WHERE
++ isa_issue_id IN ($issue_id) OR
++ isa_associated_id IN ($issue_id)";
++ DB_Helper::getInstance()->query($stmt);
++ if ($usr_id) {
++ History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
++ }
++ }
++
++
++ /**
++ * Method used to remove a issue association from an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $associated_id The associated issue ID to remove.
++ * @return void
++ */
++ function deleteAssociation($issue_id, $associated_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $associated_id = Misc::escapeInteger($associated_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
++ WHERE
++ (
++ isa_issue_id = $issue_id AND
++ isa_associated_id = $associated_id
++ ) OR
++ (
++ isa_issue_id = $associated_id AND
++ isa_associated_id = $issue_id
++ )";
++ DB_Helper::getInstance()->query($stmt);
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
++ "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
++ History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
++ "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
++ }
++
++
++ /**
++ * Method used to assign an issue with an user.
++ *
++ * @access public
++ * @param integer $usr_id The user ID of the person performing this change
++ * @param integer $issue_id The issue ID
++ * @param integer $assignee_usr_id The user ID of the assignee
++ * @param boolean $add_history Whether to add a history entry about this or not
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
++ $order = 1;
++ // move all orders down to free "order space" for this new association
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order + 1
++ WHERE
++ isu_usr_id = $assignee_usr_id AND
++ isu_order >= $order";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ // insert the new association
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ (
++ isu_iss_id,
++ isu_usr_id,
++ isu_assigned_date,
++ isu_order
++ ) VALUES (
++ $issue_id,
++ $assignee_usr_id,
++ '" . Date_Helper::getCurrentDateGMT() . "',
++ $order
++ )";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($add_history) {
++ History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
++ 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
++ }
++ return 1;
++ }
++ }
++
++ /**
++ * Method used to get the order list to be rearranged
++ *
++ * @access private
++ * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
++ * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
++ * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
++ */
++ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
++ {
++ // find all affected associantion orders
++ $stmt = "SELECT isu_usr_id, isu_order FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id IN ($issue_id)";
++ if ($usr_id !== FALSE) {
++ $stmt.= " AND isu_usr_id IN ($usr_id)";
++ }
++ $stmt.= "ORDER BY isu_order";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ $deleted_orders = array();
++ foreach ($res as $row) {
++ if (empty($deleted_orders[$row['isu_usr_id']])) {
++ $deleted_orders[$row['isu_usr_id']] = array();
++ }
++ $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
++ }
++ return $deleted_orders;
++ }
++ }
++
++ /**
++ *
++ * Method used to rearrange order list in the db according to known deleted records
++ *
++ * @access private
++ * @param mixed deleteorder list
++ * @return void
++ */
++ function rearrangeDeleteUserAssociationOrderList($delete_order_list)
++ {
++ if (empty($delete_order_list) || (!is_array($delete_order_list))) {
++ return -1;
++ }
++ foreach ($delete_order_list as $isu_usr_id => $orders) {
++ for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
++ // move the orders after them up to take the "order space" of the deleted records
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order - " . ($i+1) . "
++ WHERE
++ isu_usr_id = $isu_usr_id AND
++ isu_order > " . $orders[$i];
++ if ($i < count($orders) - 1) {
++ $stmt.= " AND
++ isu_order < " . $orders[$i+1];
++ }
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ }
++ }
++ return 1;
++ }
++
++
++ /**
++ * Method used to delete all user assignments for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user ID of the person performing the change
++ * @return void
++ */
++ function deleteUserAssociations($issue_id, $usr_id = FALSE)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ if (is_array($issue_id)) {
++ $issue_id = implode(", ", $issue_id);
++ }
++ $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id IN ($issue_id)";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($usr_id) {
++ History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
++ }
++ self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to delete a single user assignments for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id The user to remove.
++ * @param boolean $add_history Whether to add a history entry about this or not
++ * @return void
++ */
++ function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $usr_id = Misc::escapeInteger($usr_id);
++ $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
++ $stmt = "DELETE FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id = $issue_id AND
++ isu_usr_id = $usr_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ if ($add_history) {
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
++ User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
++ }
++ self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Creates an issue with the given email information.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @param integer $usr_id The user responsible for this action
++ * @param string $sender The original sender of this email
++ * @param string $summary The issue summary
++ * @param string $description The issue description
++ * @param integer $category The category ID
++ * @param integer $priority The priority ID
++ * @param array $assignment The list of users to assign this issue to
++ * @param string $date The date the email was originally sent.
++ * @param string $msg_id The message ID of the email we are creating this issue from.
++ * @return void
++ */
++ function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
++ {
++ $data = array();
++ $exclude_list = array();
++
++ $sender_email = Mail_Helper::getEmailAddress($sender);
++ $sender_usr_id = User::getUserIDByEmail($sender_email, true);
++ if (!empty($sender_usr_id)) {
++ $reporter = $sender_usr_id;
++ $exclude_list[] = $sender_usr_id;
++ }
++
++ $data = array(
++ 'category' => $category,
++ 'priority' => $priority,
++ 'description' => $description,
++ 'summary' => $summary,
++ 'msg_id' => $msg_id,
++ );
++
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
++ if (!empty($customer_id)) {
++ $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
++ // overwrite the reporter with the customer contact
++ $reporter = User::getUserIDByContactID($customer_contact_id);
++ $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
++
++ $data['customer'] = $customer_id;
++ $data['contact'] = $customer_contact_id;
++# $data['contract'] = // XXX missing
++ $data['contact_person_lname'] = $contact['last_name'];
++ $data['contact_person_fname'] = $contact['first_name'];
++ $data['contact_email'] = $sender_email;
++ $data['contact_phone'] = $contact['phone'];
++ $data['contact_timezone'] = $contact_timezone;
++ }
++ } else {
++ $customer_id = FALSE;
++ }
++ if (empty($reporter)) {
++ $reporter = APP_SYSTEM_USER_ID;
++ }
++
++ $data['reporter'] = $reporter;
++
++ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
++ if ($issue_id == -1) {
++ return -1;
++ }
++
++ $has_TAM = false;
++ $has_RR = false;
++ // log the creation of the issue
++ History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
++
++ $emails = array();
++ $manager_usr_ids = array();
++ if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
++ // if there are any technical account managers associated with this customer, add these users to the notification list
++ $managers = Customer::getAccountManagers($prj_id, $customer_id);
++ $manager_usr_ids = array_keys($managers);
++ $manager_emails = array_values($managers);
++ $emails = array_merge($emails, $manager_emails);
++ }
++ // add the reporter to the notification list
++ $emails[] = $sender;
++ $emails = array_unique($emails);
++ $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
++ foreach ($emails as $address) {
++ Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
++ }
++
++ // only assign the issue to an user if the associated customer has any technical account managers
++ $users = array();
++ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
++ foreach ($manager_usr_ids as $manager_usr_id) {
++ $users[] = $manager_usr_id;
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
++ }
++ $has_TAM = true;
++ }
++ // now add the user/issue association
++ if (@count($assignment) > 0) {
++ for ($i = 0; $i < count($assignment); $i++) {
++ Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
++ if ($assignment[$i] != $usr_id) {
++ $users[] = $assignment[$i];
++ }
++ }
++ } else {
++ // only use the round-robin feature if this new issue was not
++ // already assigned to a customer account manager
++ if (@count($manager_usr_ids) < 1) {
++ $assignee = Round_Robin::getNextAssignee($prj_id);
++ // assign the issue to the round robin person
++ if (!empty($assignee)) {
++ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
++ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
++ $users[] = $assignee;
++ $has_RR = true;
++ }
++ }
++ }
++ if (count($users) > 0) {
++ $has_assignee = true;
++ }
++
++ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
++
++ // send special 'an issue was auto-created for you' notification back to the sender
++ Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
++
++ return $issue_id;
++ }
++
++
++ /**
++ * Return errors that happened when creating new issue from POST method.
++ *
++ * @return array
++ */
++ private static $insert_errors = array();
++ static function getInsertErrors() {
++ return self::$insert_errors;
++ }
++
++ /**
++ * Method used to add a new issue using the normal report form.
++ *
++ * @access public
++ * @return integer The new issue ID
++ */
++ function createFromPost()
++ {
++ $keys = array(
++ 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
++ 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
++ 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
++ );
++ $data = array();
++ foreach ($keys as $key) {
++ if (isset($_POST[$key])) {
++ $data[$key] = $_POST[$key];
++ }
++ }
++
++ $prj_id = Auth::getCurrentProject();
++ $usr_id = Auth::getUserID();
++
++ // if we are creating an issue for a customer, put the
++ // main customer contact as the reporter for it
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $contact_usr_id = User::getUserIDByContactID($data['contact']);
++ if (empty($contact_usr_id)) {
++ $contact_usr_id = $usr_id;
++ }
++ $data['reporter'] = $contact_usr_id;
++ } else {
++ $data['reporter'] = $usr_id;
++ }
++
++ $data['msg_id'] = Mail_Helper::generateMessageID();
++
++ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
++ if ($issue_id == -1) {
++ return -1;
++ }
++
++ $has_TAM = false;
++ $has_RR = false;
++ $info = User::getNameEmail($usr_id);
++ // log the creation of the issue
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
++
++ $emails = array();
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
++ $emails = $data['contact_extra_emails'];
++ }
++ // add the primary contact to the notification list
++ if ($data['add_primary_contact'] == 'yes') {
++ $contact_email = User::getEmailByContactID($data['contact']);
++ if (!empty($contact_email)) {
++ $emails[] = $contact_email;
++ }
++ }
++ // if there are any technical account managers associated with this customer, add these users to the notification list
++ $managers = Customer::getAccountManagers($prj_id, $data['customer']);
++ $manager_usr_ids = array_keys($managers);
++ $manager_emails = array_values($managers);
++ $emails = array_merge($emails, $manager_emails);
++ }
++ // add the reporter to the notification list
++ $emails[] = $info['usr_email'];
++ $emails = array_unique($emails);
++ foreach ($emails as $address) {
++ Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
++ }
++
++ // only assign the issue to an user if the associated customer has any technical account managers
++ $users = array();
++ $has_TAM = false;
++ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
++ foreach ($manager_usr_ids as $manager_usr_id) {
++ $users[] = $manager_usr_id;
++ self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
++ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
++ }
++ $has_TAM = true;
++ }
++ // now add the user/issue association (aka assignments)
++ if (!empty($data['users']) && count($data['users']) > 0) {
++ for ($i = 0; $i < count($data['users']); $i++) {
++ Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
++ Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
++ self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
++ if ($data['users'][$i] != $usr_id) {
++ $users[] = $data['users'][$i];
++ }
++ }
++ } else {
++ // only use the round-robin feature if this new issue was not
++ // already assigned to a customer account manager
++ if (@count($manager_usr_ids) < 1) {
++ $assignee = Round_Robin::getNextAssignee($prj_id);
++ // assign the issue to the round robin person
++ if (!empty($assignee)) {
++ $users[] = $assignee;
++ self::addUserAssociation($usr_id, $issue_id, $assignee, false);
++ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
++ $has_RR = true;
++ }
++ }
++ }
++
++ // now process any files being uploaded
++ $found = 0;
++ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
++ if (!@empty($_FILES["file"]["name"][$i])) {
++ $found = 1;
++ break;
++ }
++ }
++ if ($found) {
++ $files = array();
++ for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
++ $filename = @$_FILES["file"]["name"][$i];
++ if (empty($filename)) {
++ continue;
++ }
++ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
++ if (empty($blob)) {
++ // error reading a file
++ self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
++ continue;
++ }
++ $files[] = array(
++ "filename" => $filename,
++ "type" => $_FILES['file']['type'][$i],
++ "blob" => $blob
++ );
++ }
++ if (count($files) > 0) {
++ $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
++ foreach ($files as $file) {
++ Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
++ }
++ }
++ }
++ // need to associate any emails ?
++ if (!empty($data['attached_emails'])) {
++ $items = explode(",", $data['attached_emails']);
++ Support::associate($usr_id, $issue_id, $items);
++ }
++ // need to notify any emails being converted into issues ?
++ if (@count($data['notify_senders']) > 0) {
++ $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
++ } else {
++ $recipients = array();
++ }
++ // need to process any custom fields ?
++ if (@count($data['custom_fields']) > 0) {
++ foreach ($data['custom_fields'] as $fld_id => $value) {
++ Custom_Field::associateIssue($issue_id, $fld_id, $value);
++ }
++ }
++ // also send a special confirmation email to the customer contact
++ if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
++ // also need to pass the list of sender emails already notified,
++ // so we can avoid notifying the same person again
++ $contact_email = User::getEmailByContactID($data['contact']);
++ if (@!in_array($contact_email, $recipients)) {
++ Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
++ }
++ }
++
++ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
++
++ // also notify any users that want to receive emails anytime a new issue is created
++ Notification::notifyNewIssue($prj_id, $issue_id);
++
++ return $issue_id;
++ }
++
++ /**
++ * Insert issue to database.
++ *
++ * @param integer $prj_id The project ID
++ * @param integer $usr_id The user responsible for this action
++ * @param array $data of issue to be inserted
++ * @return integer The new issue ID
++ */
++ private function insertIssue($prj_id, $usr_id, $data)
++ {
++
++ // XXX missing_fields never used
++ $missing_fields = array();
++ if ($data['category'] == -1) {
++ $missing_fields[] = 'Category';
++ }
++ if ($data['priority'] == -1) {
++ $missing_fields[] = 'Priority';
++ }
++
++ // if there is no reporter set, use the system user
++ if (empty($data['reporter'])) {
++ $data['reporter'] = APP_SYSTEM_USER_ID;
++ }
++
++ if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
++ $data['estimated_dev_time'] = 0;
++ }
++
++ // add new issue
++ $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
++ "SET ".
++ "iss_prj_id=" . $prj_id . ",";
++ if (!empty($data['group'])) {
++ $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
++ }
++ if (!empty($data['category'])) {
++ $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
++ }
++ if (!empty($data['release'])) {
++ $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
++ }
++ if (!empty($data['priority'])) {
++ $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
++ }
++
++ $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
++
++ $initial_status = Project::getInitialStatus($prj_id);
++ if (!empty($initial_status)) {
++ $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
++ }
++
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $stmt .= "
++ iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
++ if (!empty($data['contact'])) {
++ $stmt .= "
++ iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
++ }
++ $stmt .= "
++ iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
++ iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
++ iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
++ iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
++ iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
++ iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
++ }
++
++ $stmt .= "
++ iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_public_action_type='created',
++ iss_summary='" . Misc::escapeString($data['summary']) . "',
++ iss_description='" . Misc::escapeString($data['description']) . "',
++ iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
++ if (!empty($data['contact'])) {
++ $stmt .= "
++ iss_private=" . Misc::escapeInteger($data['private']) . " ,";
++ }
++ $stmt .= "
++ iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
++ ";
++
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++
++ $issue_id = DB_Helper::get_last_insert_id();
++ return $issue_id;
++ }
++
++
++ /**
++ * Method used to get a specific parameter in the issue listing cookie.
++ *
++ * @access public
++ * @param string $name The name of the parameter
++ * @return mixed The value of the specified parameter
++ */
++ function getParam($name)
++ {
++ $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
++
++ if (isset($_GET[$name])) {
++ return $_GET[$name];
++ } elseif (isset($_POST[$name])) {
++ return $_POST[$name];
++ } elseif (isset($profile[$name])) {
++ return $profile[$name];
++ } else {
++ return "";
++ }
++ }
++
++
++ /**
++ * Method used to save the current search parameters in a cookie.
++ *
++ * @access public
++ * @return array The search parameters
++ */
++ function saveSearchParams()
++ {
++ $sort_by = self::getParam('sort_by');
++ $sort_order = self::getParam('sort_order');
++ $users = self::getParam('users');
++ if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
++ unset($sort_by);
++ unset($sort_order);
++ }
++ $rows = self::getParam('rows');
++ $hide_closed = self::getParam('hide_closed');
++ if ($hide_closed === '') {
++ $hide_closed = 1;
++ }
++ $search_type = self::getParam('search_type');
++ if (empty($search_type)) {
++ $search_type = 'all_text';
++ }
++ $custom_field = self::getParam('custom_field');
++ if (is_string($custom_field)) {
++ $custom_field = unserialize(urldecode($custom_field));
++ }
++ $cookie = array(
++ 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
++ 'pagerRow' => self::getParam('pagerRow'),
++ 'hide_closed' => $hide_closed,
++ "sort_by" => $sort_by ? $sort_by : "pri_rank",
++ "sort_order" => $sort_order ? $sort_order : "ASC",
++ // quick filter form
++ 'keywords' => self::getParam('keywords'),
++ 'search_type' => $search_type,
++ 'users' => self::getParam('users'),
++ 'status' => self::getParam('status'),
++ 'priority' => self::getParam('priority'),
++ 'category' => self::getParam('category'),
++ 'customer_email' => self::getParam('customer_email'),
++ // advanced search form
++ 'show_authorized_issues' => self::getParam('show_authorized_issues'),
++ 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
++ 'reporter' => self::getParam('reporter'),
++ // other fields
++ 'release' => self::getParam('release'),
++ // custom fields
++ 'custom_field' => $custom_field
++ );
++ // now do some magic to properly format the date fields
++ $date_fields = array(
++ 'created_date',
++ 'updated_date',
++ 'last_response_date',
++ 'first_response_date',
++ 'closed_date'
++ );
++ foreach ($date_fields as $field_name) {
++ $field = self::getParam($field_name);
++ if (empty($field)) {
++ continue;
++ }
++ if (@$field['filter_type'] == 'in_past') {
++ @$cookie[$field_name] = array(
++ 'filter_type' => 'in_past',
++ 'time_period' => $field['time_period']
++ );
++ } else {
++ $end_field_name = $field_name . '_end';
++ $end_field = self::getParam($end_field_name);
++ @$cookie[$field_name] = array(
++ 'past_hour' => $field['past_hour'],
++ 'Year' => $field['Year'],
++ 'Month' => $field['Month'],
++ 'Day' => $field['Day'],
++ 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
++ 'filter_type' => $field['filter_type'],
++ 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
++ );
++ @$cookie[$end_field_name] = array(
++ 'Year' => $end_field['Year'],
++ 'Month' => $end_field['Month'],
++ 'Day' => $end_field['Day']
++ );
++ }
++ }
++ Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
++ return $cookie;
++ }
++
++
++ /**
++ * Method used to get the current sorting options used in the grid layout
++ * of the issue listing page.
++ *
++ * @access public
++ * @param array $options The current search parameters
++ * @return array The sorting options
++ */
++ function getSortingInfo($options)
++ {
++
++ $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
++
++ // default order for last action date, priority should be descending
++ // for textual fields, like summary, ascending is reasonable
++ $fields = array(
++ "pri_rank" => "desc",
++ "iss_id" => "desc",
++ "iss_customer_id" => "desc",
++ "prc_title" => "asc",
++ "sta_rank" => "asc",
++ "iss_created_date" => "desc",
++ "iss_summary" => "asc",
++ "last_action_date" => "desc",
++ "usr_full_name" => "asc",
++ "iss_expected_resolution_date" => "desc",
++ "pre_title" => "asc",
++ "assigned" => "asc",
++ "isu_order" => "desc",
++ );
++
++ foreach ($custom_fields as $fld_id => $fld_name) {
++ $fields['custom_field_' . $fld_id] = "desc";
++ }
++
++ $sortfields = array_combine(array_keys($fields), array_keys($fields));
++ $sortfields["pre_title"] = "pre_scheduled_date";
++ $sortfields["assigned"] = "isu_usr_id";
++
++ $items = array(
++ "links" => array(),
++ "images" => array()
++ );
++ foreach ($sortfields as $field => $sortfield) {
++ $sort_order = $fields[$field];
++ if ($options["sort_by"] == $sortfield) {
++ $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
++ if (strtolower($options["sort_order"]) == "asc") {
++ $sort_order = "desc";
++ } else {
++ $sort_order = "asc";
++ }
++ }
++ $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
++ }
++ return $items;
++ }
++
++
++ /**
++ * Returns the list of action date fields appropriate for the
++ * current user ID.
++ *
++ * @access public
++ * @return array The list of action date fields
++ */
++ function getLastActionFields()
++ {
++ $last_action_fields = array(
++ "iss_last_public_action_date"
++ );
++ if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
++ $last_action_fields[] = "iss_last_internal_action_date";
++ }
++ if (count($last_action_fields) > 1) {
++ return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
++ } else {
++ return $last_action_fields[0] . " AS last_action_date";
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues to be displayed in the grid layout.
++ *
++ * @access public
++ * @param integer $prj_id The current project ID
++ * @param array $options The search parameters
++ * @param integer $current_row The current page number
++ * @param integer $max The maximum number of rows per page
++ * @return array The list of issues to be displayed
++ */
++ function getListing($prj_id, $options, $current_row = 0, $max = 5)
++ {
++ if (strtoupper($max) == "ALL") {
++ $max = 9999999;
++ }
++ $start = $current_row * $max;
++ // get the current user's role
++ $usr_id = Auth::getUserID();
++ $role_id = User::getRoleByUser($usr_id, $prj_id);
++
++ // get any custom fields that should be displayed
++ $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
++
++ $stmt = "SELECT
++ iss_id,
++ iss_grp_id,
++ iss_prj_id,
++ iss_sta_id,
++ iss_customer_id,
++ iss_customer_contract_id,
++ iss_created_date,
++ iss_updated_date,
++ iss_last_response_date,
++ iss_closed_date,
++ iss_last_customer_action_date,
++ iss_usr_id,
++ iss_summary,
++ pri_title,
++ prc_title,
++ sta_title,
++ sta_color status_color,
++ sta_id,
++ iqu_status,
++ grp_name `group`,
++ pre_title,
++ iss_last_public_action_date,
++ iss_last_public_action_type,
++ iss_last_internal_action_date,
++ iss_last_internal_action_type,
++ " . self::getLastActionFields() . ",
++ IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
++ iss_private,
++ usr_full_name,
++ iss_percent_complete,
++ iss_dev_time,
++ iss_expected_resolution_date
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
++ // join custom fields if we are searching by custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
++ }
++ } else {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
++ }
++ }
++ }
++ $stmt .= ")";
++ // check for the custom fields we want to sort by
++ if (strstr($options['sort_by'], 'custom_field') !== false) {
++ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
++ $stmt .= "\n LEFT JOIN \n" .
++ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
++ ON
++ (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
++ }
++ if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id";
++ }
++ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
++ ON
++ iur_iss_id=iss_id";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
++ ON
++ sub_iss_id=iss_id";
++ }
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
++ ON
++ iss_grp_id=grp_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id=prc_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id = pre_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ ON
++ iss_id=iqu_iss_id AND
++ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
++ WHERE
++ iss_prj_id= " . Misc::escapeInteger($prj_id);
++ $stmt .= self::buildWhereClause($options);
++
++ if (strstr($options["sort_by"], 'custom_field') !== false) {
++ $fld_details = Custom_Field::getDetails($fld_id);
++ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
++ } else {
++ $sort_by = Misc::escapeString($options["sort_by"]);
++ }
++
++ $stmt .= "
++ GROUP BY
++ iss_id
++ ORDER BY
++ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
++ iss_id DESC";
++ $total_rows = Pager::getTotalRows($stmt);
++ $stmt .= "
++ LIMIT
++ " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array(
++ "list" => "",
++ "info" => ""
++ );
++ } else {
++ if (count($res) > 0) {
++ self::getAssignedUsersByIssues($res);
++ Time_Tracking::getTimeSpentByIssues($res);
++ // need to get the customer titles for all of these issues...
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ Customer::getCustomerTitlesByIssues($prj_id, $res);
++ Customer::getSupportLevelsByIssues($prj_id, $res);
++ }
++ self::formatLastActionDates($res);
++ self::getLastStatusChangeDates($prj_id, $res);
++ } elseif ($current_row > 0) {
++ // if there are no results, and the page is not the first page reset page to one and reload results
++ Auth::redirect("list.php?pagerRow=0&rows=$max");
++ }
++ $groups = Group::getAssocList($prj_id);
++ $categories = Category::getAssocList($prj_id);
++ $column_headings = self::getColumnHeadings($prj_id);
++ if (count($custom_fields) > 0) {
++ $column_headings = array_merge($column_headings,$custom_fields);
++ }
++ $csv[] = @implode("\t", $column_headings);
++ for ($i = 0; $i < count($res); $i++) {
++ $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
++ $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
++ $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
++ $fields = array(
++ $res[$i]['pri_title'],
++ $res[$i]['iss_id'],
++ $res[$i]['usr_full_name'],
++ );
++ // hide the group column from the output if no
++ // groups are available in the database
++ if (count($groups) > 0) {
++ $fields[] = $res[$i]['group'];
++ }
++ $fields[] = $res[$i]['assigned_users'];
++ $fields[] = $res[$i]['time_spent'];
++ // hide the category column from the output if no
++ // categories are available in the database
++ if (count($categories) > 0) {
++ $fields[] = $res[$i]['prc_title'];
++ }
++ if (Customer::hasCustomerIntegration($prj_id)) {
++ $fields[] = @$res[$i]['customer_title'];
++ // check if current user is acustomer and has a per incident contract.
++ // if so, check if issue is redeemed.
++ if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
++ if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
++ (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
++ $res[$i]['redeemed'] = true;
++ }
++ }
++ }
++ $fields[] = $res[$i]['sta_title'];
++ $fields[] = $res[$i]["status_change_date"];
++ $fields[] = $res[$i]["last_action_date"];
++ $fields[] = $res[$i]['iss_dev_time'];
++ $fields[] = $res[$i]['iss_summary'];
++ $fields[] = $res[$i]['iss_expected_resolution_date'];
++
++ if (count($custom_fields) > 0) {
++ $res[$i]['custom_field'] = array();
++ $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
++ foreach ($custom_field_values as $this_field) {
++ if (!empty($custom_fields[$this_field['fld_id']])) {
++ $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
++ $fields[] = $this_field['value'];
++ }
++ }
++ }
++
++ $csv[] = @implode("\t", $fields);
++ }
++ $total_pages = ceil($total_rows / $max);
++ $last_page = $total_pages - 1;
++ return array(
++ "list" => $res,
++ "info" => array(
++ "current_page" => $current_row,
++ "start_offset" => $start,
++ "end_offset" => $start + count($res),
++ "total_rows" => $total_rows,
++ "total_pages" => $total_pages,
++ "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
++ "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
++ "last_page" => $last_page,
++ "custom_fields" => $custom_fields
++ ),
++ "csv" => @implode("\n", $csv)
++ );
++ }
++ }
++
++
++ /**
++ * Processes a result set to format the "Last Action Date" column.
++ *
++ * @access public
++ * @param array $result The result set
++ */
++ function formatLastActionDates(&$result)
++ {
++ for ($i = 0; $i < count($result); $i++) {
++ if (($result[$i]['action_type'] == "internal") &&
++ (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
++ $label = $result[$i]["iss_last_internal_action_type"];
++ $last_date = $result[$i]["iss_last_internal_action_date"];
++ } else {
++ $label = $result[$i]["iss_last_public_action_type"];
++ $last_date = $result[$i]["iss_last_public_action_date"];
++ }
++ $date = new Date($last_date);
++ $current = new Date(Date_Helper::getCurrentDateGMT());
++ $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
++ Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
++ }
++ }
++
++
++ /**
++ * Retrieves the last status change date for the given issue.
++ *
++ * @access public
++ * @param integer $prj_id The project ID
++ * @param array $result The associative array of data
++ * @see self::getListing()
++ */
++ function getLastStatusChangeDates($prj_id, &$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_sta_id"];
++ }
++ if (count($ids) == 0) {
++ return false;
++ }
++ $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
++ for ($i = 0; $i < count($result); $i++) {
++ if (empty($result[$i]['iss_sta_id'])) {
++ $result[$i]['status_change_date'] = '';
++ } else {
++ list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
++ if ((empty($label)) || (empty($date_field_name))) {
++ $result[$i]['status_change_date'] = '';
++ continue;
++ }
++ $current = new Date(Date_Helper::getCurrentDateGMT());
++ $desc = "$label: %s ago";
++ $target_date = $result[$i][$date_field_name];
++ if (empty($target_date)) {
++ $result[$i]['status_change_date'] = '';
++ continue;
++ }
++ $date = new Date($target_date);
++ $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues to be displayed in the grid layout.
++ *
++ * @access public
++ * @param array $options The search parameters
++ * @return string The where clause
++ */
++ function buildWhereClause($options)
++ {
++ $usr_id = Auth::getUserID();
++ $prj_id = Auth::getCurrentProject();
++ $role_id = User::getRoleByUser($usr_id, $prj_id);
++
++ $stmt = ' AND iss_usr_id = usr_id';
++ if ($role_id == User::getRoleID('Customer')) {
++ $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
++ } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
++ $stmt .= " AND (
++ iss_usr_id = $usr_id OR
++ iur_usr_id = $usr_id
++ )";
++ }
++
++ if (!empty($options["users"])) {
++ $stmt .= " AND (\n";
++ if (stristr($options["users"], "grp") !== false) {
++ $chunks = explode(":", $options["users"]);
++ $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
++ } else {
++ if ($options['users'] == '-1') {
++ $stmt .= 'isu_usr_id IS NULL';
++ } elseif ($options['users'] == '-2') {
++ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
++ } elseif ($options['users'] == '-3') {
++ $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
++ } elseif ($options['users'] == '-4') {
++ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
++ } else {
++ $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
++ }
++ }
++ $stmt .= ')';
++ }
++ if (!empty($options["reporter"])) {
++ $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
++ }
++ if (!empty($options["show_authorized_issues"])) {
++ $stmt .= " AND (iur_usr_id=$usr_id)";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= " AND (sub_usr_id=$usr_id)";
++ }
++ if (!empty($options["keywords"])) {
++ $stmt .= " AND (\n";
++ if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
++ $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
++ } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
++ // check if the user is trying to search by customer email
++ $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
++ if (count($customer_ids) > 0) {
++ $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
++ } else {
++ // no results, kill query
++ $stmt .= " iss_customer_id = -1";
++ }
++ } else {
++ $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
++ $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
++ }
++ $stmt .= "\n) ";
++ }
++ if (!empty($options["priority"])) {
++ $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
++ }
++ if (!empty($options["status"])) {
++ $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
++ }
++ if (!empty($options["category"])) {
++ if (!is_array($options['category'])) {
++ $options['category'] = array($options['category']);
++ }
++ $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
++ }
++ if (!empty($options["hide_closed"])) {
++ $stmt .= " AND sta_is_closed=0";
++ }
++ if (!empty($options['release'])) {
++ $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
++ }
++ // now for the date fields
++ $date_fields = array(
++ 'created_date',
++ 'updated_date',
++ 'last_response_date',
++ 'first_response_date',
++ 'closed_date'
++ );
++ foreach ($date_fields as $field_name) {
++ if (!empty($options[$field_name])) {
++ switch ($options[$field_name]['filter_type']) {
++ case 'greater':
++ $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
++ break;
++ case 'less':
++ $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
++ break;
++ case 'between':
++ $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
++ break;
++ case 'null':
++ $stmt .= " AND iss_$field_name IS NULL";
++ break;
++ case 'in_past':
++ if (strlen($options[$field_name]['time_period']) == 0) {
++ $options[$field_name]['time_period'] = 0;
++ }
++ $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
++ Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
++ break;
++ }
++ }
++ }
++ // custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
++ if (($field['fld_type'] == 'date') &&
++ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
++ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
++ }
++ } elseif ($field['fld_type'] == 'date') {
++ if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
++ continue;
++ }
++ $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
++ cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
++ } else if ($field['fld_type'] == 'integer') {
++ $value = $search_value['value'];
++ switch ($search_value['filter_type']) {
++ case 'ge': $cmp = '>='; break;
++ case 'le': $cmp = '<='; break;
++ case 'gt': $cmp = '>'; break;
++ case 'lt': $cmp = '<'; break;
++ default: $cmp = '='; break;
++ }
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
++ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
++ } else {
++ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
++ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
++ if ($field['fld_type'] == 'combo') {
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
++ } else {
++ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
++ }
++ $stmt .= ')';
++ }
++ }
++ }
++ // clear cached full-text values if we are not searching fulltext anymore
++ if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
++ Session::set('fulltext_string', '');
++ Session::set('fulltext_issues', '');
++ }
++ return $stmt;
++ }
++
++
++ /**
++ * Method used to get the previous and next issues that are available
++ * according to the current search parameters.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param array $options The search parameters
++ * @return array The list of issues
++ */
++ function getSides($issue_id, $options)
++ {
++ $usr_id = Auth::getUserID();
++ $role_id = Auth::getCurrentRole();
++
++ $stmt = "SELECT
++ iss_id,
++ " . self::getLastActionFields() . "
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
++ // join custom fields if we are searching by custom fields
++ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
++ foreach ($options['custom_field'] as $fld_id => $search_value) {
++ if (empty($search_value)) {
++ continue;
++ }
++ $field = Custom_Field::getDetails($fld_id);
++ if (($field['fld_type'] == 'date') &&
++ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
++ continue;
++ }
++ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
++ continue;
++ }
++
++ if ($field['fld_type'] == 'multiple') {
++ $search_value = Misc::escapeInteger($search_value);
++ foreach ($search_value as $cfo_id) {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
++ }
++ } else {
++ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
++ }
++ }
++ }
++ $stmt .= ")";
++ // check for the custom fields we want to sort by
++ if (strstr($options['sort_by'], 'custom_field') !== false) {
++ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
++ $stmt .= "\n LEFT JOIN \n" .
++ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
++ ON
++ (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
++ }
++ if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ ON
++ isu_iss_id=iss_id";
++ }
++ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
++ ON
++ iur_iss_id=iss_id";
++ }
++ if (!empty($options["show_notification_list_issues"])) {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
++ ON
++ sub_iss_id=iss_id";
++ }
++ if (@$options["sort_by"] == "pre_scheduled_date") {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id = pre_id";
++ }
++ if (@$options['sort_by'] == 'prc_title') {
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id = prc_id";
++ }
++ $stmt .= "
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ $stmt .= self::buildWhereClause($options);
++ if (strstr($options["sort_by"], 'custom_field') !== false) {
++ $fld_details = Custom_Field::getDetails($fld_id);
++ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
++ } else {
++ $sort_by = Misc::escapeString($options["sort_by"]);
++ }
++ $stmt .= "
++ GROUP BY
++ iss_id
++ ORDER BY
++ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
++ iss_id DESC";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ // COMPAT: the next line requires PHP >= 4.0.5
++ $index = array_search($issue_id, $res);
++ if (!empty($res[$index+1])) {
++ $next = $res[$index+1];
++ }
++ if (!empty($res[$index-1])) {
++ $previous = $res[$index-1];
++ }
++ return array(
++ "next" => @$next,
++ "previous" => @$previous
++ );
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of user IDs assigned to a specific
++ * issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of user IDs
++ */
++ function getAssignedUserIDs($issue_id)
++ {
++ $stmt = "SELECT
++ usr_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to see if a user is assigned to an issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $usr_id An integer containg the ID of the user.
++ * @return boolean true if the user(s) are assigned to the issue.
++ */
++ function isAssignedToUser($issue_id, $usr_id)
++ {
++ $assigned_users = self::getAssignedUserIDs($issue_id);
++ if (in_array($usr_id, $assigned_users)) {
++ return true;
++ } else {
++ return false;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of reporters associated with a given
++ * list of issues.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getReportersByIssues(&$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ $ids = implode(", ", $ids);
++ $stmt = "SELECT
++ iss_id,
++ CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ iss_usr_id=usr_id AND
++ iss_id IN ($ids)";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ // now populate the $result variable again
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of assigned users by a list
++ * of issues. This was originally created to optimize the issue
++ * listing page.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getAssignedUsersByIssues(&$result)
++ {
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ if (count($ids) < 1) {
++ return;
++ }
++ $ids = implode(", ", $ids);
++ $stmt = "SELECT
++ isu_iss_id,
++ isu_order,
++ isu_usr_id,
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_usr_id=usr_id AND
++ isu_iss_id IN ($ids)";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ // gather names of the users assigned to each issue
++ $t = array();
++ for ($i = 0; $i < count($res); $i++) {
++ if (!empty($t[$res[$i]['isu_iss_id']])) {
++ $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
++ } else {
++ $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
++ }
++ }
++ // gather orders
++ $o = array();
++ for ($i = 0; $i < count($res); $i++) {
++ if (empty($o[$res[$i]['isu_iss_id']])) {
++ $o[$res[$i]['isu_iss_id']] = array();
++ }
++ $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
++ }
++ // now populate the $result variable again
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
++ @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to add the issue description to a list of issues.
++ *
++ * @access public
++ * @param array $result The result set
++ * @return void
++ */
++ function getDescriptionByIssues(&$result)
++ {
++ if (count($result) == 0) {
++ return;
++ }
++
++ $ids = array();
++ for ($i = 0; $i < count($result); $i++) {
++ $ids[] = $result[$i]["iss_id"];
++ }
++ $ids = implode(", ", $ids);
++
++ $stmt = "SELECT
++ iss_id,
++ iss_description
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id in ($ids)";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ } else {
++ for ($i = 0; $i < count($result); $i++) {
++ @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of users (the full names) assigned to a
++ * specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of users
++ */
++ function getAssignedUsers($issue_id)
++ {
++ $stmt = "SELECT
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ isu_usr_id=usr_id";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the details for a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param boolean $force_refresh If the cache should not be used.
++ * @return array The details for the specified issue
++ */
++ function getDetails($issue_id, $force_refresh = false)
++ {
++ static $returns;
++
++ $issue_id = Misc::escapeInteger($issue_id);
++
++ if (empty($issue_id)) {
++ return '';
++ }
++
++ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
++ prj_title,
++ prc_title,
++ pre_title,
++ pri_title,
++ sta_title,
++ sta_abbreviation,
++ sta_color status_color,
++ sta_is_closed
++ FROM
++ (
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
++ )
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
++ ON
++ iss_pri_id=pri_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ ON
++ iss_sta_id=sta_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
++ ON
++ iss_prc_id=prc_id
++ LEFT JOIN
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
++ ON
++ iss_pre_id=pre_id
++ WHERE
++ iss_id=$issue_id AND
++ iss_prj_id=prj_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ if (empty($res)) {
++ return "";
++ } else {
++ $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
++ // get customer information, if any
++ if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
++ $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
++ $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
++ $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
++ $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
++ $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
++ $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
++ if (empty($res['iss_first_response_date'])) {
++ $first_response_deadline = $created_date_ts + $max_first_response_time;
++ if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
++ $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
++ } else {
++ $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
++ }
++ }
++ }
++ $res['iss_original_description'] = $res["iss_description"];
++ if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
++ $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
++ $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
++ }
++ $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
++ $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
++ $res['iss_created_date_ts'] = $created_date_ts;
++ $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
++ list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
++ $temp = self::getAssignedUsersStatus($res["iss_id"]);
++ $res["has_inactive_users"] = 0;
++ $res["assigned_users"] = array();
++ $res["assigned_inactive_users"] = array();
++ foreach ($temp as $usr_id => $usr_status) {
++ if (!User::isActiveStatus($usr_status)) {
++ $res["assigned_inactive_users"][] = $usr_id;
++ $res["has_inactive_users"] = 1;
++ } else {
++ $res["assigned_users"][] = $usr_id;
++ }
++ }
++ if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
++ $res["is_current_user_assigned"] = 1;
++ } else {
++ $res["is_current_user_assigned"] = 0;
++ }
++ $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
++ $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
++ $res["reporter"] = User::getFullName($res["iss_usr_id"]);
++ if (empty($res["iss_updated_date"])) {
++ $res["iss_updated_date"] = 'not updated yet';
++ } else {
++ $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
++ }
++ $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
++ if (Release::isAssignable($res["iss_pre_id"])) {
++ $release = Release::getDetails($res["iss_pre_id"]);
++ $res["pre_title"] = $release["pre_title"];
++ $res["pre_status"] = $release["pre_status"];
++ }
++ // need to return the list of issues that are duplicates of this one
++ $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
++ $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
++ // also get the issue title of the duplicated issue
++ if (!empty($res['iss_duplicated_iss_id'])) {
++ $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
++ }
++
++ // get group information
++ if (!empty($res["iss_grp_id"])) {
++ $res["group"] = Group::getDetails($res["iss_grp_id"]);
++ }
++
++ // get quarantine issue
++ $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
++
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++ }
++
++
++ /**
++ * Method used to get some simple details about the given duplicated issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The duplicated issue details
++ */
++ function getDuplicatedDetails($issue_id)
++ {
++ $stmt = "SELECT
++ iss_summary title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_sta_id=sta_id AND
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to bulk update a list of issues
++ *
++ * @access public
++ * @return boolean
++ */
++ function bulkUpdate()
++ {
++ // check if user performing this chance has the proper role
++ if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
++ return -1;
++ }
++
++ $items = Misc::escapeInteger($_POST['item']);
++ $new_status_id = Misc::escapeInteger($_POST['status']);
++ $new_release_id = Misc::escapeInteger(@$_POST['release']);
++ $new_priority_id = Misc::escapeInteger($_POST['priority']);
++ $new_category_id = Misc::escapeInteger($_POST['category']);
++
++ for ($i = 0; $i < count($items); $i++) {
++ if (!self::canAccess($items[$i], Auth::getUserID())) {
++ continue;
++ } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
++ // make sure issue is not in another project
++ continue;
++ }
++
++ $updated_fields = array();
++
++ // update assignment
++ if (count(@$_POST['users']) > 0) {
++ $users = Misc::escapeInteger($_POST['users']);
++ // get who this issue is currently assigned too
++ $stmt = "SELECT
++ isu_usr_id,
++ usr_full_name
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
++ WHERE
++ isu_usr_id = usr_id AND
++ isu_iss_id = " . $items[$i];
++ $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($current_assignees)) {
++ Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ foreach ($current_assignees as $usr_id => $usr_name) {
++ if (!in_array($usr_id, $users)) {
++ self::deleteUserAssociation($items[$i], $usr_id, false);
++ }
++ }
++ $new_user_names = array();
++ $new_assignees = array();
++ foreach ($users as $usr_id) {
++ $new_user_names[$usr_id] = User::getFullName($usr_id);
++
++ // check if the issue is already assigned to this person
++ $stmt = "SELECT
++ COUNT(*) AS total
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id=" . $items[$i] . " AND
++ isu_usr_id=" . $usr_id;
++ $total = DB_Helper::getInstance()->getOne($stmt);
++ if ($total > 0) {
++ continue;
++ } else {
++ $new_assignees[] = $usr_id;
++ // add the assignment
++ self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
++ Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
++ Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
++ }
++ }
++ Notification::notifyNewAssignment($new_assignees, $items[$i]);
++ $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
++ }
++
++ // update status
++ if (!empty($new_status_id)) {
++ $old_status_id = self::getStatusID($items[$i]);
++ $res = self::setStatus($items[$i], $new_status_id, false);
++ if ($res == 1) {
++ $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
++ }
++ }
++
++ // update release
++ if (!empty($new_release_id)) {
++ $old_release_id = self::getRelease($items[$i]);
++ $res = self::setRelease($items[$i], $new_release_id);
++ if ($res == 1) {
++ $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
++ }
++ }
++
++ // update priority
++ if (!empty($new_priority_id)) {
++ $old_priority_id = self::getPriority($items[$i]);
++ $res = self::setPriority($items[$i], $new_priority_id);
++ if ($res == 1) {
++ $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
++ }
++ }
++
++ // update category
++ if (!empty($new_category_id)) {
++ $old_category_id = self::getCategory($items[$i]);
++ $res = self::setCategory($items[$i], $new_category_id);
++ if ($res == 1) {
++ $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
++ }
++ }
++
++ if (count($updated_fields) > 0) {
++ // log the changes
++ $changes = '';
++ $k = 0;
++ foreach ($updated_fields as $key => $value) {
++ if ($k > 0) {
++ $changes .= "; ";
++ }
++ $changes .= "$key: $value";
++ $k++;
++ }
++ History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
++ }
++
++ // close if request
++ if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
++ self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
++ }
++ }
++ return true;
++ }
++
++
++ /**
++ * Method used to set the initial impact analysis for a specific issue
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer 1 if the update worked, -1 otherwise
++ */
++ function setImpactAnalysis($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
++ iss_last_internal_action_type='update',
++ iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
++ iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add the impact analysis to the history of the issue
++ $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
++ return 1;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs that area available in the
++ * system.
++ *
++ * @access public
++ * @param string $extra_condition An extra condition in the WHERE clause
++ * @return array The list of issue IDs
++ */
++ function getColList($extra_condition = NULL)
++ {
++ $stmt = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ if (!empty($extra_condition)) {
++ $stmt .= " AND $extra_condition ";
++ }
++ $stmt .= "
++ ORDER BY
++ iss_id DESC";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the full list of issue IDs and their respective
++ * titles.
++ *
++ * @access public
++ * @param string $extra_condition An extra condition in the WHERE clause
++ * @return array The list of issues
++ */
++ function getAssocList($extra_condition = NULL)
++ {
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_prj_id=" . Auth::getCurrentProject();
++ if (!empty($extra_condition)) {
++ $stmt .= " AND $extra_condition ";
++ }
++ $stmt .= "
++ ORDER BY
++ iss_id ASC";
++ $res = DB_Helper::getInstance()->getAssoc($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return "";
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to get the list of issues associated to a specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of associated issues
++ */
++ function getAssociatedIssues($issue_id)
++ {
++ $issues = self::getAssociatedIssuesDetails($issue_id);
++ $associated = array();
++ for ($i = 0; $i < count($issues); $i++) {
++ $associated[] = $issues[$i]['associated_issue'];
++ }
++ return $associated;
++ }
++
++
++ /**
++ * Method used to get the list of issues associated details to a
++ * specific issue.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return array The list of associated issues
++ */
++ function getAssociatedIssuesDetails($issue_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$issue_id])) {
++ return $returns[$issue_id];
++ }
++
++ $stmt = "SELECT
++ isa_associated_id associated_issue,
++ iss_summary associated_title,
++ sta_title current_status,
++ sta_is_closed is_closed
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ isa_associated_id=iss_id AND
++ iss_sta_id=sta_id AND
++ isa_issue_id=$issue_id";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ $returns[$issue_id] = $res;
++ return $res;
++ }
++ }
++
++
++ /**
++ * Method used to check whether an issue was already closed or not.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return boolean
++ */
++ function isClosed($issue_id)
++ {
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id) . " AND
++ iss_sta_id=sta_id AND
++ sta_is_closed=1";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ if ($res == 0) {
++ return false;
++ } else {
++ return true;
++ }
++ }
++ }
++
++
++ /**
++ * Returns a simple list of issues that are currently set to some
++ * form of quarantine. This is mainly used by the IRC interface.
++ *
++ * @access public
++ * @return array List of quarantined issues
++ */
++ function getQuarantinedIssueList()
++ {
++ // XXX: would be nice to restrict the result list to only one project
++ $stmt = "SELECT
++ iss_id,
++ iss_summary
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id=iss_id AND
++ iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
++ iqu_expiration IS NOT NULL";
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ self::getAssignedUsersByIssues($res);
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the status of a quarantine.
++ *
++ * @param integer $issue_id The issue ID
++ * @return integer Indicates what the current state of quarantine is.
++ */
++ function getQuarantineInfo($issue_id)
++ {
++ $stmt = "SELECT
++ iqu_status,
++ iqu_expiration
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
++ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
++ iqu_expiration IS NULL)";
++ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ if (!empty($res["iqu_expiration"])) {
++ $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
++ $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
++ }
++ return $res;
++ }
++ }
++
++
++ /**
++ * Sets the quarantine status. Optionally an expiration date can be set
++ * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @param integer $status The quarantine status
++ * @param string $expiration The expiration date of quarantine (default empty)
++ */
++ function setQuarantine($issue_id, $status, $expiration = '')
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $status = Misc::escapeInteger($status);
++
++ // see if there is an existing record
++ $stmt = "SELECT
++ COUNT(*)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ WHERE
++ iqu_iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ if ($res > 0) {
++ // update
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ SET
++ iqu_status = $status";
++ if (!empty($expiration)) {
++ $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
++ }
++ $stmt .= "\nWHERE
++ iqu_iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ } else {
++ // add history entry about this change taking place
++ if ($status == 0) {
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
++ "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
++ }
++ }
++ } else {
++ // insert
++ $stmt = "INSERT INTO
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
++ (
++ iqu_iss_id,
++ iqu_status";
++ if (!empty($expiration)) {
++ $stmt .= ",\niqu_expiration\n";
++ }
++ $stmt .= ") VALUES (
++ $issue_id,
++ $status";
++ if (!empty($expiration)) {
++ $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
++ }
++ $stmt .= ")";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ }
++ return 1;
++ }
++
++
++ /**
++ * Sets the group of the issue.
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @param integer $group_id The ID of the group
++ * @return integer 1 if successful, -1 or -2 otherwise
++ */
++ function setGroup($issue_id, $group_id)
++ {
++ $issue_id = Misc::escapeInteger($issue_id);
++ $group_id = Misc::escapeInteger($group_id);
++
++ $current = self::getDetails($issue_id);
++ if ($current["iss_grp_id"] == $group_id) {
++ return -2;
++ }
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_grp_id = $group_id
++ WHERE
++ iss_id = $issue_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $current_user = Auth::getUserID();
++ if (empty($current_user)) {
++ $current_user = APP_SYSTEM_USER_ID;
++ }
++ History::add($issue_id, $current_user, History::getTypeID('group_changed'),
++ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
++ return 1;
++ }
++
++
++ /**
++ * Returns the group ID associated with the given issue ID.
++ *
++ * @access public
++ * @param integer $issue_id The issue ID
++ * @return integer The associated group ID
++ */
++ function getGroupID($issue_id)
++ {
++ $stmt = "SELECT
++ iss_grp_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return 0;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns an array of issues based on full text search results.
++ *
++ * @param array $options An array of search options
++ * @return array An array of issue IDS
++ */
++ function getFullTextIssues($options)
++ {
++ // check if a list of issues for this full text search is already cached
++ $fulltext_string = Session::get('fulltext_string');
++ if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
++ return Session::get('fulltext_issues');
++ }
++
++ // no pre-existing list, generate them
++ $stmt = "(SELECT
++ DISTINCT(iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(not_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
++ WHERE
++ MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(ttr_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
++ WHERE
++ MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(phs_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
++ WHERE
++ MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ ) UNION (
++ SELECT
++ DISTINCT(sup_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
++ WHERE
++ sup_id = seb_sup_id AND
++ MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
++ )";
++ $res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array(-1);
++ } else {
++ $stmt = "SELECT
++ DISTINCT(icf_iss_id)
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
++ WHERE
++ MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
++ $custom_res = DB_Helper::getInstance()->getCol($stmt);
++ if (PEAR::isError($custom_res)) {
++ Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
++ return array(-1);
++ }
++ $issues = array_merge($res, $custom_res);
++ // we kill the query results on purpose to flag that no
++ // issues could be found with fulltext search
++ if (count($issues) < 1) {
++ $issues = array(-1);
++ }
++ Session::set('fulltext_string', $options['keywords']);
++ Session::set('fulltext_issues', $issues);
++ return $issues;
++ }
++ }
++
++
++ /**
++ * Method to determine if user can access a particular issue
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue.
++ * @param integer $usr_id The ID of the user
++ * @return boolean If the user can access the issue
++ */
++ function canAccess($issue_id, $usr_id)
++ {
++ static $access;
++
++ if (empty($issue_id)) {
++ return true;
++ }
++
++ if (isset($access[$issue_id . "-" . $usr_id])) {
++ return $access[$issue_id . "-" . $usr_id];
++ }
++
++ $details = self::getDetails($issue_id);
++ if (empty($details)) {
++ return true;
++ }
++ $usr_details = User::getDetails($usr_id);
++ $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
++ $prj_id = self::getProjectID($issue_id);
++
++
++ if (empty($usr_role)) {
++ // check if they are even allowed to access the project
++ $return = false;
++ } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
++ ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
++ // check customer permissions
++ $return = false;
++ } elseif ($details['iss_private'] == 1) {
++ // check if the issue is even private
++
++ // check role, reporter, assigment and group
++ if ($usr_role > User::getRoleID("Developer")) {
++ $return = true;
++ } elseif ($details['iss_usr_id'] == $usr_id) {
++ $return = true;
++ } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
++ $return = true;
++ } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
++ ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
++ $return = true;
++ } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
++ $return = true;
++ } else {
++ $return = false;
++ }
++ } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
++ ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
++ return false;
++ } else {
++ $return = true;
++ }
++
++ $access[$issue_id . "-" . $usr_id] = $return;
++ return $return;
++ }
++
++
++ /**
++ * Returns true if the specified issue is private, false otherwise
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ * @return boolean If the issue is private or not
++ */
++ function isPrivate($issue_id)
++ {
++ static $returns;
++
++ if (!isset($returns[$issue_id])) {
++ $sql = "SELECT
++ iss_private
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=$issue_id";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return true;
++ } else {
++ if ($res == 1) {
++ $returns[$issue_id] = true;
++ } else {
++ $returns[$issue_id] = false;
++ }
++ }
++ }
++ return $returns[$issue_id];
++ }
++
++
++ /**
++ * Clears closed information from an issues.
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ */
++ function clearClosed($issue_id)
++ {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ SET
++ iss_closed_date = null,
++ iss_res_id = null
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ self::moveOrderForAllUsers($issue_id, 1);
++ }
++
++
++ /**
++ * Returns the message ID that should be used as the parent ID for all messages
++ *
++ * @access public
++ * @param integer $issue_id The ID of the issue
++ */
++ function getRootMessageID($issue_id)
++ {
++ $sql = "SELECT
++ iss_root_message_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_id=" . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ } else {
++ return $res;
++ }
++ }
++
++
++ /**
++ * Returns the issue ID of the issue with the specified root message ID, or false
++ * @access public
++ * @param string $msg_id The Message ID
++ * @return integer The ID of the issue
++ */
++ function getIssueByRootMessageID($msg_id)
++ {
++ static $returns;
++
++ if (!empty($returns[$msg_id])) {
++ return $returns[$msg_id];
++ }
++ $sql = "SELECT
++ iss_id
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
++ WHERE
++ iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
++ $res = DB_Helper::getInstance()->getOne($sql);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return false;
++ }
++ if (empty($res)) {
++ $returns[$msg_id] = false;
++ } else {
++ $returns[$msg_id] = $res;
++ }
++ return $returns[$msg_id];
++ }
++
++
++ /**
++ * Sets the assignees for the issue
++ *
++ * @param integer $issue_id
++ * @param array $assignees
++ */
++ function setAssignees($issue_id, $assignees)
++ {
++ if (!is_array($assignees)) {
++ $assignees = array();
++ }
++
++ // see if there is anything to change
++ $old_assignees = self::getAssignedUserIDs($issue_id);
++ if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
++ return;
++ }
++
++ $old_assignee_names = self::getAssignedUsers($issue_id);
++
++ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
++ // clear up the assignments for this issue, and then assign it to the current user
++ self::deleteUserAssociations($issue_id);
++ $assignee_names = array();
++ foreach ($assignees as $assignee) {
++ $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
++ if ($res == -1) {
++ return false;
++ }
++ $assignee_names[] = User::getFullName($assignee);
++ Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
++ }
++
++ Notification::notifyNewAssignment($assignees, $issue_id);
++
++ // save a history entry about this...
++ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
++ "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
++ }
++
++ /**
++ * Reorders user's issues as requested by user
++ * @access public
++ * @param $usr_id User to be reordered
++ * @param $issue_id Issue or array of issues to be moved
++ * @param $neworder The new order of the issues
++ * @return void
++ */
++ function reorderUserIssues($usr_id, $issue_id, $neworder)
++ {
++ if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
++ return false;
++ }
++ if (!is_numeric($usr_id) || !is_numeric($neworder)) {
++ return false;
++ }
++ $usr_id = Misc::escapeInteger($usr_id);
++ $issue_id = Misc::escapeInteger($issue_id);
++ $neworder = Misc::escapeInteger($neworder);
++ if (is_array($issue_id)) {
++ $issue_count = count($issue_id);
++ $issue_id_str = implode(", ", $issue_id);
++ } else {
++ $issue_count = 1;
++ $issue_id_str = $issue_id;
++ $issue_id = array($issue_id);
++ }
++ // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
++ $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
++ self::rearrangeDeleteUserAssociationOrderList($orderlist);
++ // move down the orders to free the "order space" needed
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = isu_order + $issue_count
++ WHERE
++ isu_usr_id = $usr_id AND
++ isu_order >= $neworder";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ //update the order for the issues being moved
++ $i = 0;
++ foreach ($issue_id as $iss_id) {
++ $stmt = "UPDATE
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ SET
++ isu_order = " . ($neworder + $i) . "
++ WHERE
++ isu_usr_id = $usr_id AND
++ isu_iss_id = $iss_id";
++ $res = DB_Helper::getInstance()->query($stmt);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ $i++;
++ }
++ }
++
++
++ /**
++ * Get users issue order list
++ * @access public
++ * @param $user_id User
++ * @param $order_list Order of the issues
++ * @return void
++ */
++ function getIssueOrderByUser($usr_id) {
++
++ if (!is_numeric($usr_id)) {
++ return false;
++ }
++
++ $stmt = "SELECT
++ isu_iss_id, isu_order
++ FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_usr_id = " . $usr_id ;
++
++ $order_list = array();
++
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return array();
++ } else {
++ foreach ($res as $row) {
++ $order_list[$row["isu_iss_id"]] = $row["isu_order"];
++ }
++ }
++ return $order_list;
++ }
++
++ function moveOrderForAllUsers($issue_id, $neworder)
++ {
++ // Move the issue to the top priority for the ppl it's assigned to
++ $stmt = "SELECT isu_usr_id FROM
++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
++ WHERE
++ isu_iss_id = " . Misc::escapeInteger($issue_id);
++ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
++ return -1;
++ }
++ foreach ($res as $row) {
++ self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
++ }
++ }
++
++}
+--- eventum-2.2/templates/header.tpl.html 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/templates/header.tpl.html 2009-10-12 22:10:36.435851675 +0300
+@@ -8,6 +8,7 @@
+ <script type="text/javascript" src="{$rel_url}js/jquery/form.js?c=9984"></script>
+ <script type="text/javascript" src="{$rel_url}js/jquery/blockui.js?c=eb13"></script>
+ <script type="text/javascript" src="{$rel_url}js/jquery/ui.datepicker.js?c=a911"></script>
++<script type="text/javascript" src="{$rel_url}js/jquery/jquery.tablednd.js"></script>
+ <link rel="stylesheet" href="{$rel_url}js/jquery/ui.datepicker.css?c=5096">
+ <script type="text/javascript">
+ <!--
+--- eventum-2.2/templates/list.tpl.html 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/templates/list.tpl.html 2009-10-12 22:10:36.439185157 +0300
+@@ -92,6 +92,28 @@
+ f.target = '_popup';
+ f.submit();
+ }
++function reorderBulk(order_user, neworder)
++{
++ url = page_url + "?";
++ url += "reorder_user=" + order_user;
++
++ items = document.getElementsByName("item[]");
++ checkedcount = 0;
++ for (var i = 0; i < items.length; i++) {
++ if (items[i].checked) {
++ url += "&reorder_source[" + checkedcount + "]=" + items[i].value;
++ checkedcount++;
++ }
++ }
++ if (checkedcount == 0) {
++ alert('{/literal}{t escape=js}Please choose which issues to move to the new place.{/t}{literal}');
++ return false;
++ }
++
++ url += "&reorder_neworder=" + neworder;
++
++ window.location.href = url;
++}
+ function hideClosed(f)
+ {
+ if (f.hide_closed.checked) {
+@@ -153,6 +175,13 @@
+ f.go.disabled = true;
+ }
+ }
++function updateCustomFields(issue_id)
++{
++ var features = 'width=560,height=460,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
++ var customWin = window.open('custom_fields.php?issue_id=' + issue_id, '_custom_fields', features);
++ customWin.focus();
++ return false;
++}
+ //-->
+ </script>
+ {/literal}
+@@ -169,11 +198,11 @@
+ <input type="hidden" name="cat" value="bulk_update">
+ <tr>
+ <td>
+- <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0">
+- <tr>
++ <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0" id="issue_list_table">
++ <tr class="nodrag">
+ <td colspan="{$col_count}" class="default">
+ <table width="100%" cellspacing="0" cellpadding="0" border="0">
+- <tr>
++ <tr class="nodrag">
+ <td class="default">
+ <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
+ {include file="help_link.tpl.html" topic="list"}
+@@ -193,7 +222,7 @@
+ </table>
+ </td>
+ </tr>
+- <tr bgcolor="{$cell_color}">
++ <tr bgcolor="{$cell_color}" class="nodrag">
+ {if $current_role > $roles.developer}
+ <td width="1%">
+ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
+@@ -208,7 +237,7 @@
+ {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
+ </td>
+ {/foreach}
+- {else}
++ {elseif $field_name != 'isu_order' || $isu_order_user}
+ <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
+ {if $field_name == 'iss_summary'}
+ <table cellspacing="0" cellpadding="1" width="100%">
+@@ -232,20 +261,21 @@
+ {/if}
+ {/foreach}
+ </tr>
++ <tbody>
+ {section name="i" loop=$list}
+- <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}>
++ <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if} id="{$list[i].iss_id}" {if !$list[i].assigned_users_order[$current_user_id]}class="nodrag"{/if}>
+ {if $current_role > $roles.developer}
+ <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
+ {/if}
+ {foreach from=$columns item=column key=field_name}
+ {if $field_name == 'custom_fields'}
+ {foreach from=$list[i].custom_field key=fld_id item=fld_value}
+- <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
+- {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
++ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default custom_field" onclick="return updateCustomFields({$list[i].iss_id});">
++ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
+ </td>
+ {/foreach}
+- {else}
+- <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
++ {elseif $field_name != 'isu_order' || $isu_order_user}
++ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default{if $field_name == 'isu_order'} dragHandle{/if}">
+ {if $field_name == 'iss_id'}
+ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
+ {elseif $field_name == 'pri_rank'}
+@@ -279,7 +309,7 @@
+ {elseif $field_name == 'iss_percent_complete'}
+ {$list[i].iss_percent_complete|escape:"html"}%
+ {elseif $field_name == 'iss_expected_resolution_date'}
+- {$list[i].iss_expected_resolution_date|escape:"html"}
++ <div class="inline_date_pick" id="expected_resolution_date|{$list[i].iss_id}">{$list[i].iss_expected_resolution_date|escape:"html"} </div>
+ {elseif $field_name == 'iss_summary'}
+ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
+ {if $list[i].redeemed}
+@@ -288,6 +318,8 @@
+ {if $list[i].iss_private == 1}
+ <b>[Private]</b>
+ {/if}
++ {elseif $field_name == 'isu_order'}
++ {if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user} {/if}
+ {/if}
+ </td>
+ {/if}
+@@ -300,10 +332,11 @@
+ </td>
+ </tr>
+ {/section}
+- <tr bgcolor="{$cell_color}">
++ </tbody>
++ <tr bgcolor="{$cell_color}" class="nodrag">
+ <td colspan="{$col_count}">
+ <table width="100%" cellspacing="0" cellpadding="0">
+- <tr>
++ <tr class="nodrag">
+ <td width="30%" nowrap>
+ {if $current_role > $roles.developer}
+ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
+@@ -355,6 +388,35 @@
+ </form>
+ </table>
+ <br />
+-
++<script type="text/javascript">
++{*
++ * Order issues by drag and drop:
++ * only if sorted by order and viewing your own issues
++ *}
++{if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}
++{literal}
++var before = ''; // make it global variable
++$('#issue_list_table').tableDnD({
++ onDragClass: "tDnD_whileDrag",
++ onDragStart: function(table, row) {
++ before = $.tableDnD.serialize('id');
++ },
++ onDrop: function(table, row) {
++ $.post("/ajax/order.php", {before: before, after: $.tableDnD.serialize('id')}, function(data) {
++ if (data.length > 0) {
++ alert(data);
++ }
++ }, "text");
++ },
++ dragHandle: "dragHandle"
++});
++$("#issue_list_table tr").hover(function() {
++ $('#' + this.id + ' .dragHandle').addClass('showDragHandle');
++}, function() {
++ $('#' + this.id + ' .dragHandle').removeClass('showDragHandle');
++});
++{/literal}
++{/if}
++</script>
+ {include file="app_info.tpl.html"}
+ {include file="footer.tpl.html"}
+--- eventum-2.2/templates/list.tpl.html.~1~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/templates/list.tpl.html.~1~ 2009-09-14 18:07:55.000000000 +0300
+@@ -0,0 +1,360 @@
++{include file="header.tpl.html" extra_title="List of Issues"}
++{include file="navigation.tpl.html"}
++
++{if $current_role != $roles.customer}
++{include file="quick_filter_form.tpl.html"}
++{include file="current_filters.tpl.html"}
++{/if}
++<script type="text/javascript">
++<!--
++var page_url = '{$smarty.server.PHP_SELF}';
++var current_page = {if $list_info.current_page == ''}0{else}{$list_info.current_page}{/if};
++var last_page = {if $list_info.last_page == ''}0{else}{$list_info.last_page}{/if};
++{literal}
++// this function will display the bulk update form if any issues are selected.
++// if no issues are selected it will be hidden.
++function toggleBulkUpdate()
++{
++ var items = document.getElementsByName('item[]');
++
++ // loop through looking to see which are checked
++ var show = false;
++ for (var i = 0; i < items.length; i++) {
++ if (items[i].checked) {
++ show = true;
++ break;
++ }
++ }
++ if (show) {
++ changeVisibility('bulk_update1', show);
++ }
++}
++function resetBulkUpdate()
++{
++ var f = getForm('list_form');
++ clearSelectedOptions(getFormElement(f, 'users[]'));
++ clearSelectedOptions(getFormElement(f, 'status'));
++ if (getFormElement(f, 'release')) {
++ clearSelectedOptions(getFormElement(f, 'release'));
++ }
++}
++function bulkUpdate()
++{
++ var f = getForm('list_form');
++ if (!hasOneChecked(f, 'item[]')) {
++ alert('{/literal}{t escape=js}Please choose which issues to update.{/t}{literal}');
++ return false;
++ }
++
++ // figure out what is changing
++ var changed = new Array();
++ if (hasOneSelected(f, 'users[]')) {
++ changed[changed.length] = 'Assignment';
++ }
++ if (f.elements.status.selectedIndex != 0) {
++ changed[changed.length] = 'Status';
++ }
++ if ((f.elements.release) && (f.elements.release.selectedIndex != 0)) {
++ changed[changed.length] = 'Release';
++ }
++ if ((f.elements.priority) && (f.elements.priority.selectedIndex != 0)) {
++ changed[changed.length] = 'Priority';
++ }
++ if ((f.elements.category) && (f.elements.category.selectedIndex != 0)) {
++ changed[changed.length] = 'Category';
++ }
++ if ((f.elements.closed_status) && (f.elements.closed_status.selectedIndex != 0)) {
++ changed[changed.length] = 'Closed Status';
++ }
++ if (changed.length < 1) {
++ alert('{/literal}{t escape=js}Please choose new values for the selected issues{/t}{literal}');
++ return false;
++ }
++ var msg = '{/literal}{t escape=js}Warning: If you continue, you will change the {/t}{literal}';
++ for (var i = 0; i < changed.length; i++) {
++ msg += changed[i];
++ if ((changed.length > 1) && (i == (changed.length-2))) {
++ msg += ' and ';
++ } else {
++ if (i != (changed.length-1)) {
++ msg += ', ';
++ }
++ }
++ }
++ msg += ' {/literal}{t escape=js}for all selected issues. Are you sure you want to continue?{/t}{literal}';
++ if (!confirm(msg)) {
++ return false;
++ }
++ var features = 'width=420,height=200,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
++ var popupWin = window.open('', '_popup', features);
++ popupWin.focus();
++ f.action = 'popup.php';
++ f.target = '_popup';
++ f.submit();
++}
++function hideClosed(f)
++{
++ if (f.hide_closed.checked) {
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '1');
++ } else {
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '0');
++ }
++}
++function resizePager(f)
++{
++ var pagesize = f.page_size.options[f.page_size.selectedIndex].value;
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'rows', pagesize);
++}
++function checkPageField(ev)
++{
++ // check if the user is trying to submit the form by hitting <enter>
++ if (((window.event) && (window.event.keyCode == 13)) ||
++ ((ev) && (ev.which == 13))) {
++ return false;
++ }
++}
++function goPage(f, new_page)
++{
++ if ((new_page > last_page+1) || (new_page <= 0) ||
++ (new_page == current_page+1) || (!isNumberOnly(new_page))) {
++ f.page.value = current_page+1;
++ return false;
++ }
++ setPage(new_page-1);
++}
++function setPage(new_page)
++{
++ if ((new_page > last_page) || (new_page < 0) ||
++ (new_page == current_page)) {
++ return false;
++ }
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'pagerRow', new_page);
++}
++function downloadCSV()
++{
++ var f = this.document.csv_form;
++ f.submit();
++ return false;
++}
++window.onload = disableFields;
++function disableFields()
++{
++ var f = document.list_form;
++ if (current_page == 0) {
++ f.first.disabled = true;
++ f.previous.disabled = true;
++ }
++ if ((current_page == last_page) || (last_page <= 0)) {
++ f.next.disabled = true;
++ f.last.disabled = true;
++ }
++ if ((current_page == 0) && (last_page <= 0)) {
++ f.page.disabled = true;
++ f.go.disabled = true;
++ }
++}
++//-->
++</script>
++{/literal}
++{* Figure out how many columns there are *}
++{assign var="col_count" value=$columns|@count}
++{if $list_info.custom_fields|@count > 1}
++{math assign="col_count" equation="x+y-1" x=$col_count y=$list_info.custom_fields|@count}
++{/if}
++{if $current_role > $roles.developer}
++ {math assign="col_count" equation="x+1" x=$col_count}
++{/if}
++<table width="100%" bgcolor="{$cell_color}" border="0" cellspacing="0" cellpadding="1" align="center">
++ <form name="list_form" method="post" action="{$smarty.server.PHP_SELF}">
++ <input type="hidden" name="cat" value="bulk_update">
++ <tr>
++ <td>
++ <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0">
++ <tr>
++ <td colspan="{$col_count}" class="default">
++ <table width="100%" cellspacing="0" cellpadding="0" border="0">
++ <tr>
++ <td class="default">
++ <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
++ {include file="help_link.tpl.html" topic="list"}
++ </td>
++ {if $current_role != $roles.customer}
++ <td align="right" class="default" nowrap>
++ {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up}
++ {t}hide/show{/t}:
++ [ <a class="link" title="{t}hide / show the quick search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('filter_form');">{t}quick search{/t}</a> ]
++ {if $current_role > $roles.reporter}[ <a class="link" title="{t}hide / show the advanced search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('custom_filter_form');">{t}advanced search{/t}</a> ]{/if}
++ {if $current_role > $roles.reporter}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('current_filters');"> {t}current filters{/t}</a> ]{/if}
++ {if $current_role > $roles.developer}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('bulk_update');"> {t}bulk update tool{/t}</a> ]{/if}
++ {/if}
++ </td>
++ {/if}
++ </tr>
++ </table>
++ </td>
++ </tr>
++ <tr bgcolor="{$cell_color}">
++ {if $current_role > $roles.developer}
++ <td width="1%">
++ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
++ </td>
++ {/if}
++ {foreach from=$columns item=column key=field_name}
++ {if $field_name == 'custom_fields'}
++ {foreach from=$list_info.custom_fields key=fld_id item=fld_title}
++ {assign var="fld_name_id" value="custom_field_`$fld_id`"}
++ <td align="{$column.align|default:'center'}" class="default_white" nowrap>
++ <a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link">{$fld_title|escape:"html"}</a>
++ {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
++ </td>
++ {/foreach}
++ {else}
++ <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
++ {if $field_name == 'iss_summary'}
++ <table cellspacing="0" cellpadding="1" width="100%">
++ <tr>
++ <td class="default_white">
++ <a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link">{t}Summary{/t}</a>
++ {if $sorting.images.iss_summary != ""}<a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link"><img border="0" src="{$sorting.images.iss_summary}"></a>{/if}
++ </td>
++ <td align="right">
++ <span class="default_white">{t}Export Data:{/t}</span> <input alt="{t}generate excel-friendly report{/t}" type="image" src="{$rel_url}images/excel.jpg" class="shortcut" value="{t}Export to Excel{/t}" onClick="javascript:return downloadCSV();">
++ </td>
++ </tr>
++ </table>
++ {elseif $sorting.links[$field_name] != ''}
++ <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
++ {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
++ {else}
++ {$column.title}
++ {/if}
++ </td>
++ {/if}
++ {/foreach}
++ </tr>
++ {section name="i" loop=$list}
++ <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}>
++ {if $current_role > $roles.developer}
++ <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
++ {/if}
++ {foreach from=$columns item=column key=field_name}
++ {if $field_name == 'custom_fields'}
++ {foreach from=$list[i].custom_field key=fld_id item=fld_value}
++ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
++ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
++ </td>
++ {/foreach}
++ {else}
++ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
++ {if $field_name == 'iss_id'}
++ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
++ {elseif $field_name == 'pri_rank'}
++ {$list[i].pri_title|escape:"html"}
++ {elseif $field_name == 'iss_grp_id'}
++ {$list[i].group}
++ {elseif $field_name == 'assigned'}
++ {$list[i].assigned_users}
++ {elseif $field_name == 'time_spent'}
++ {$list[i].time_spent}
++ {elseif $field_name == 'prc_title'}
++ {$list[i].prc_title}
++ {elseif $field_name == 'pre_title'}
++ {$list[i].pre_title|escape:"html"}
++ {elseif $field_name == 'iss_customer_id'}
++ {$list[i].customer_title|escape:"html"}
++ {elseif $field_name == 'support_level'}
++ {$list[i].support_level|escape:"html"}
++ {elseif $field_name == 'sta_rank'}
++ {$list[i].sta_title|escape:"html"}
++ {elseif $field_name == 'iss_created_date'}
++ {$list[i].iss_created_date|escape:"html"}
++ {elseif $field_name == 'iss_dev_time'}
++ {$list[i].iss_dev_time|escape:"html"}
++ {elseif $field_name == 'sta_change_date'}
++ {$list[i].status_change_date|escape:"html"}
++ {elseif $field_name == 'last_action_date'}
++ {$list[i].last_action_date|escape:"html"}
++ {elseif $field_name == 'usr_full_name'}
++ {$list[i].usr_full_name|escape:"html"}
++ {elseif $field_name == 'iss_percent_complete'}
++ {$list[i].iss_percent_complete|escape:"html"}%
++ {elseif $field_name == 'iss_expected_resolution_date'}
++ {$list[i].iss_expected_resolution_date|escape:"html"}
++ {elseif $field_name == 'iss_summary'}
++ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
++ {if $list[i].redeemed}
++ [Redeemed]
++ {/if}
++ {if $list[i].iss_private == 1}
++ <b>[Private]</b>
++ {/if}
++ {/if}
++ </td>
++ {/if}
++ {/foreach}
++ </tr>
++ {sectionelse}
++ <tr bgcolor="gray">
++ <td colspan="{$col_count}" class="default_white" align="center">
++ <i>{t}No issues could be found.{/t}</i>
++ </td>
++ </tr>
++ {/section}
++ <tr bgcolor="{$cell_color}">
++ <td colspan="{$col_count}">
++ <table width="100%" cellspacing="0" cellpadding="0">
++ <tr>
++ <td width="30%" nowrap>
++ {if $current_role > $roles.developer}
++ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
++ {/if}
++ </td>
++ <td width="40%" align="center" nowrap>
++ <nobr>
++ <input name="first" type="button" value="|<" class="shortcut" onClick="javascript:setPage(0);">
++ <input name="previous" type="button" value="<<" class="shortcut" onClick="javascript:setPage({$list_info.previous_page});">
++ <input type="text" name="page" size="3" maxlength="3" value="{if $list_info.current_page == ''}1{else}{math equation="x + 1" x=$list_info.current_page}{/if}" style="background: {$cell_color};" class="paging_input" onKeyPress="javascript:return checkPageField(event);">
++ <input name="go" type="button" value="{t}Go{/t}" class="shortcut" onClick="javascript:goPage(this.form, this.form.page.value);">
++ <input name="next" type="button" value=">>" class="shortcut" onClick="javascript:setPage({$list_info.next_page});">
++ <input name="last" type="button" value=">|" class="shortcut" onClick="javascript:setPage({$list_info.last_page});">
++ </nobr>
++ </td>
++ <td nowrap>
++ <span class="default_white">{t}Rows per Page:{/t}</span>
++ <select name="page_size" class="default" onChange="javascript:resizePager(this.form);">
++ <option value="5" {if $options.rows == 5}selected{/if}>5</option>
++ <option value="10" {if $options.rows == 10}selected{/if}>10</option>
++ <option value="25" {if $options.rows == 25}selected{/if}>25</option>
++ <option value="50" {if $options.rows == 50}selected{/if}>50</option>
++ <option value="100" {if $options.rows == 100}selected{/if}>100</option>
++ <option value="ALL" {if $options.rows == "ALL"}selected{/if}>{t}ALL{/t}</option>
++ </select>
++ <input type="button" value="{t}Set{/t}" class="shortcut" onClick="javascript:resizePager(this.form);">
++ </td>
++ <td width="30%" class="default_white" align="right">
++ <input type="checkbox" id="hide_closed" name="hide_closed" {if $options.hide_closed}checked{/if} onClick="javascript:hideClosed(this.form);"> <label for="hide_closed">{t}Hide Closed Issues{/t}</label>
++ </td>
++ </tr>
++ </table>
++ </td>
++ </tr>
++ </table>
++ </td>
++ </tr>
++ {if $current_role > $roles.developer}
++ <tr>
++ <td bgcolor="#FFFFFF">
++ <br />
++ {include file="bulk_update.tpl.html"}
++ </td>
++ </tr>
++ {/if}
++ </form>
++ <form target="_csvWindow" method="post" action="csv.php" name="csv_form">
++ <input type="hidden" name="csv_data" value="{$csv_data}">
++ </form>
++</table>
++<br />
++
++{include file="app_info.tpl.html"}
++{include file="footer.tpl.html"}
+--- eventum-2.2/templates/list.tpl.html.~2~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/templates/list.tpl.html.~2~ 2009-10-12 22:10:36.435851675 +0300
+@@ -0,0 +1,421 @@
++{include file="header.tpl.html" extra_title="List of Issues"}
++{include file="navigation.tpl.html"}
++
++{if $current_role != $roles.customer}
++{include file="quick_filter_form.tpl.html"}
++{include file="current_filters.tpl.html"}
++{/if}
++<script type="text/javascript">
++<!--
++var page_url = '{$smarty.server.PHP_SELF}';
++var current_page = {if $list_info.current_page == ''}0{else}{$list_info.current_page}{/if};
++var last_page = {if $list_info.last_page == ''}0{else}{$list_info.last_page}{/if};
++{literal}
++// this function will display the bulk update form if any issues are selected.
++// if no issues are selected it will be hidden.
++function toggleBulkUpdate()
++{
++ var items = document.getElementsByName('item[]');
++
++ // loop through looking to see which are checked
++ var show = false;
++ for (var i = 0; i < items.length; i++) {
++ if (items[i].checked) {
++ show = true;
++ break;
++ }
++ }
++ if (show) {
++ changeVisibility('bulk_update1', show);
++ }
++}
++function resetBulkUpdate()
++{
++ var f = getForm('list_form');
++ clearSelectedOptions(getFormElement(f, 'users[]'));
++ clearSelectedOptions(getFormElement(f, 'status'));
++ if (getFormElement(f, 'release')) {
++ clearSelectedOptions(getFormElement(f, 'release'));
++ }
++}
++function bulkUpdate()
++{
++ var f = getForm('list_form');
++ if (!hasOneChecked(f, 'item[]')) {
++ alert('{/literal}{t escape=js}Please choose which issues to update.{/t}{literal}');
++ return false;
++ }
++
++ // figure out what is changing
++ var changed = new Array();
++ if (hasOneSelected(f, 'users[]')) {
++ changed[changed.length] = 'Assignment';
++ }
++ if (f.elements.status.selectedIndex != 0) {
++ changed[changed.length] = 'Status';
++ }
++ if ((f.elements.release) && (f.elements.release.selectedIndex != 0)) {
++ changed[changed.length] = 'Release';
++ }
++ if ((f.elements.priority) && (f.elements.priority.selectedIndex != 0)) {
++ changed[changed.length] = 'Priority';
++ }
++ if ((f.elements.category) && (f.elements.category.selectedIndex != 0)) {
++ changed[changed.length] = 'Category';
++ }
++ if ((f.elements.closed_status) && (f.elements.closed_status.selectedIndex != 0)) {
++ changed[changed.length] = 'Closed Status';
++ }
++ if (changed.length < 1) {
++ alert('{/literal}{t escape=js}Please choose new values for the selected issues{/t}{literal}');
++ return false;
++ }
++ var msg = '{/literal}{t escape=js}Warning: If you continue, you will change the {/t}{literal}';
++ for (var i = 0; i < changed.length; i++) {
++ msg += changed[i];
++ if ((changed.length > 1) && (i == (changed.length-2))) {
++ msg += ' and ';
++ } else {
++ if (i != (changed.length-1)) {
++ msg += ', ';
++ }
++ }
++ }
++ msg += ' {/literal}{t escape=js}for all selected issues. Are you sure you want to continue?{/t}{literal}';
++ if (!confirm(msg)) {
++ return false;
++ }
++ var features = 'width=420,height=200,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
++ var popupWin = window.open('', '_popup', features);
++ popupWin.focus();
++ f.action = 'popup.php';
++ f.target = '_popup';
++ f.submit();
++}
++function reorderBulk(order_user, neworder)
++{
++ url = page_url + "?";
++ url += "reorder_user=" + order_user;
++
++ items = document.getElementsByName("item[]");
++ checkedcount = 0;
++ for (var i = 0; i < items.length; i++) {
++ if (items[i].checked) {
++ url += "&reorder_source[" + checkedcount + "]=" + items[i].value;
++ checkedcount++;
++ }
++ }
++ if (checkedcount == 0) {
++ alert('{/literal}{t escape=js}Please choose which issues to move to the new place.{/t}{literal}');
++ return false;
+ }
+
++ url += "&reorder_neworder=" + neworder;
++
++ window.location.href = url;
+}
-+
-+jQuery.fn.extend(
-+ {
-+ tableDnD : jQuery.tableDnD.build,
-+ tableDnDUpdate : jQuery.tableDnD.updateTables,
-+ tableDnDSerialize: jQuery.tableDnD.serializeTables
-+ }
-+);
-\ No newline at end of file
---- eventum-2.2/templates/header.tpl.html~ 2009-06-30 02:07:28.000000000 +0300
-+++ eventum-2.2/templates/header.tpl.html 2009-06-30 02:07:49.981934267 +0300
-@@ -8,6 +8,7 @@
- <script type="text/javascript" src="{$rel_url}js/jquery/form.js?c=9984"></script>
- <script type="text/javascript" src="{$rel_url}js/jquery/blockui.js?c=eb13"></script>
- <script type="text/javascript" src="{$rel_url}js/jquery/ui.datepicker.js?c=a911"></script>
-+<script type="text/javascript" src="{$rel_url}js/jquery/jquery.tablednd.js"></script>
- <link rel="stylesheet" href="{$rel_url}js/jquery/ui.datepicker.css?c=5096">
- <script type="text/javascript">
- <!--
---- eventum-r3765/htdocs/css/style.css~ 2008-06-19 08:30:31.000000000 +0300
-+++ eventum-r3765/htdocs/css/style.css 2008-10-29 17:28:49.393768970 +0200
-@@ -172,4 +172,9 @@
- font-size: 70%;
- font-family: Verdana, Arial, Helvetica, sans-serif;
- padding: 10px;
--}
-\ No newline at end of file
++function hideClosed(f)
++{
++ if (f.hide_closed.checked) {
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '1');
++ } else {
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '0');
++ }
+}
-+
-+.tDnD_whileDrag td {
-+ background-color: #ffffdd;
-+ border: 1px solid red;
++function resizePager(f)
++{
++ var pagesize = f.page_size.options[f.page_size.selectedIndex].value;
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'rows', pagesize);
+}
---- eventum~/htdocs/css/style.css 2008-11-06 16:54:30.000000000 +0200
-+++ eventum/htdocs/css/style.css 2008-11-06 16:20:50.038350957 +0200
-@@ -173,8 +173,21 @@
- font-family: Verdana, Arial, Helvetica, sans-serif;
- padding: 10px;
- }
--
- .tDnD_whileDrag td {
-- background-color: #ffffdd;
-- border: 1px solid red;
-+ background-color: #ffffdd;
++function checkPageField(ev)
++{
++ // check if the user is trying to submit the form by hitting <enter>
++ if (((window.event) && (window.event.keyCode == 13)) ||
++ ((ev) && (ev.which == 13))) {
++ return false;
++ }
+}
-+.tDnD_whileDrag td {
-+ border: 1px solid red;
++function goPage(f, new_page)
++{
++ if ((new_page > last_page+1) || (new_page <= 0) ||
++ (new_page == current_page+1) || (!isNumberOnly(new_page))) {
++ f.page.value = current_page+1;
++ return false;
++ }
++ setPage(new_page-1);
+}
-+.inline_date_pick {
-+ cursor: pointer;
++function setPage(new_page)
++{
++ if ((new_page > last_page) || (new_page < 0) ||
++ (new_page == current_page)) {
++ return false;
++ }
++ window.location.href = page_url + "?" + replaceParam(window.location.href, 'pagerRow', new_page);
+}
-+.custom_field {
-+ cursor: pointer;
++function downloadCSV()
++{
++ var f = this.document.csv_form;
++ f.submit();
++ return false;
+}
-+.showDragHandle {
-+ cursor: move;
-+ background-image: url(../images/updown2.gif);
-+ background-repeat: no-repeat;
-+ background-position: center center;
- }
---- eventum~/templates/list.tpl.html 2008-11-06 16:54:30.000000000 +0200
-+++ eventum/templates/list.tpl.html 2008-11-11 11:22:36.206950437 +0200
-@@ -250,9 +250,6 @@
- </table>
- {elseif $sorting.links[$field_name] != ''}
- <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
-- {if $field_name == 'isu_order'}
-- <br>{$users[$isu_order_user]}
-- {/if}
- {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
- {else}
- {$column.title}
-@@ -275,7 +272,7 @@
- </td>
- {/foreach}
- {elseif $field_name != 'isu_order' || $isu_order_user}
-- <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
-+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default{if $field_name == 'isu_order'} dragHandle{/if}">
- {if $field_name == 'iss_id'}
- <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
- {elseif $field_name == 'pri_rank'}
-@@ -319,9 +316,7 @@
- <b>[Private]</b>
- {/if}
- {elseif $field_name == 'isu_order'}
-- {if $list[i].assigned_users_order[$current_user_id]}
-- <img src="{$rel_url}images/updown.gif" alt="move">
-- {/if}
-+ {if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user} {/if}
- {/if}
- </td>
- {/if}
-@@ -334,7 +329,7 @@
- </td>
- </tr>
- {/section}
-- </tbody>
-+ </tbody>
- <tr bgcolor="{$cell_color}" class="nodrag">
- <td colspan="{$col_count}">
- <table width="100%" cellspacing="0" cellpadding="0">
-@@ -409,10 +404,16 @@
- alert(data);
- }
- }, "text");
-- }
++window.onload = disableFields;
++function disableFields()
++{
++ var f = document.list_form;
++ if (current_page == 0) {
++ f.first.disabled = true;
++ f.previous.disabled = true;
++ }
++ if ((current_page == last_page) || (last_page <= 0)) {
++ f.next.disabled = true;
++ f.last.disabled = true;
++ }
++ if ((current_page == 0) && (last_page <= 0)) {
++ f.page.disabled = true;
++ f.go.disabled = true;
++ }
++}
++function updateCustomFields(issue_id)
++{
++ var features = 'width=560,height=460,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
++ var customWin = window.open('custom_fields.php?issue_id=' + issue_id, '_custom_fields', features);
++ customWin.focus();
++ return false;
++}
++//-->
++</script>
++{/literal}
++{* Figure out how many columns there are *}
++{assign var="col_count" value=$columns|@count}
++{if $list_info.custom_fields|@count > 1}
++{math assign="col_count" equation="x+y-1" x=$col_count y=$list_info.custom_fields|@count}
++{/if}
++{if $current_role > $roles.developer}
++ {math assign="col_count" equation="x+1" x=$col_count}
++{/if}
++<table width="100%" bgcolor="{$cell_color}" border="0" cellspacing="0" cellpadding="1" align="center">
++ <form name="list_form" method="post" action="{$smarty.server.PHP_SELF}">
++ <input type="hidden" name="cat" value="bulk_update">
++ <tr>
++ <td>
++ <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0" id="issue_list_table">
++ <tr class="nodrag">
++ <td colspan="{$col_count}" class="default">
++ <table width="100%" cellspacing="0" cellpadding="0" border="0">
++ <tr class="nodrag">
++ <td class="default">
++ <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
++ {include file="help_link.tpl.html" topic="list"}
++ </td>
++ {if $current_role != $roles.customer}
++ <td align="right" class="default" nowrap>
++ {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up}
++ {t}hide/show{/t}:
++ [ <a class="link" title="{t}hide / show the quick search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('filter_form');">{t}quick search{/t}</a> ]
++ {if $current_role > $roles.reporter}[ <a class="link" title="{t}hide / show the advanced search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('custom_filter_form');">{t}advanced search{/t}</a> ]{/if}
++ {if $current_role > $roles.reporter}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('current_filters');"> {t}current filters{/t}</a> ]{/if}
++ {if $current_role > $roles.developer}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('bulk_update');"> {t}bulk update tool{/t}</a> ]{/if}
++ {/if}
++ </td>
++ {/if}
++ </tr>
++ </table>
++ </td>
++ </tr>
++ <tr bgcolor="{$cell_color}" class="nodrag">
++ {if $current_role > $roles.developer}
++ <td width="1%">
++ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
++ </td>
++ {/if}
++ {foreach from=$columns item=column key=field_name}
++ {if $field_name == 'custom_fields'}
++ {foreach from=$list_info.custom_fields key=fld_id item=fld_title}
++ {assign var="fld_name_id" value="custom_field_`$fld_id`"}
++ <td align="{$column.align|default:'center'}" class="default_white" nowrap>
++ <a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link">{$fld_title|escape:"html"}</a>
++ {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
++ </td>
++ {/foreach}
++ {elseif $field_name != 'isu_order' || $isu_order_user}
++ <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
++ {if $field_name == 'iss_summary'}
++ <table cellspacing="0" cellpadding="1" width="100%">
++ <tr>
++ <td class="default_white">
++ <a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link">{t}Summary{/t}</a>
++ {if $sorting.images.iss_summary != ""}<a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link"><img border="0" src="{$sorting.images.iss_summary}"></a>{/if}
++ </td>
++ <td align="right">
++ <span class="default_white">{t}Export Data:{/t}</span> <input alt="{t}generate excel-friendly report{/t}" type="image" src="{$rel_url}images/excel.jpg" class="shortcut" value="{t}Export to Excel{/t}" onClick="javascript:return downloadCSV();">
++ </td>
++ </tr>
++ </table>
++ {elseif $sorting.links[$field_name] != ''}
++ <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
++ {if $field_name == 'isu_order'}
++ <br>{$users[$isu_order_user]}
++ {/if}
++ {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
++ {else}
++ {$column.title}
++ {/if}
++ </td>
++ {/if}
++ {/foreach}
++ </tr>
++ <tbody>
++ {section name="i" loop=$list}
++ <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if} id="{$list[i].iss_id}" {if !$list[i].assigned_users_order[$current_user_id]}class="nodrag"{/if}>
++ {if $current_role > $roles.developer}
++ <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
++ {/if}
++ {foreach from=$columns item=column key=field_name}
++ {if $field_name == 'custom_fields'}
++ {foreach from=$list[i].custom_field key=fld_id item=fld_value}
++ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default custom_field" onclick="return updateCustomFields({$list[i].iss_id});">
++ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
++ </td>
++ {/foreach}
++ {elseif $field_name != 'isu_order' || $isu_order_user}
++ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
++ {if $field_name == 'iss_id'}
++ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
++ {elseif $field_name == 'pri_rank'}
++ {$list[i].pri_title|escape:"html"}
++ {elseif $field_name == 'iss_grp_id'}
++ {$list[i].group}
++ {elseif $field_name == 'assigned'}
++ {$list[i].assigned_users}
++ {elseif $field_name == 'time_spent'}
++ {$list[i].time_spent}
++ {elseif $field_name == 'prc_title'}
++ {$list[i].prc_title}
++ {elseif $field_name == 'pre_title'}
++ {$list[i].pre_title|escape:"html"}
++ {elseif $field_name == 'iss_customer_id'}
++ {$list[i].customer_title|escape:"html"}
++ {elseif $field_name == 'support_level'}
++ {$list[i].support_level|escape:"html"}
++ {elseif $field_name == 'sta_rank'}
++ {$list[i].sta_title|escape:"html"}
++ {elseif $field_name == 'iss_created_date'}
++ {$list[i].iss_created_date|escape:"html"}
++ {elseif $field_name == 'iss_dev_time'}
++ {$list[i].iss_dev_time|escape:"html"}
++ {elseif $field_name == 'sta_change_date'}
++ {$list[i].status_change_date|escape:"html"}
++ {elseif $field_name == 'last_action_date'}
++ {$list[i].last_action_date|escape:"html"}
++ {elseif $field_name == 'usr_full_name'}
++ {$list[i].usr_full_name|escape:"html"}
++ {elseif $field_name == 'iss_percent_complete'}
++ {$list[i].iss_percent_complete|escape:"html"}%
++ {elseif $field_name == 'iss_expected_resolution_date'}
++ <div class="inline_date_pick" id="expected_resolution_date|{$list[i].iss_id}">{$list[i].iss_expected_resolution_date|escape:"html"} </div>
++ {elseif $field_name == 'iss_summary'}
++ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
++ {if $list[i].redeemed}
++ [Redeemed]
++ {/if}
++ {if $list[i].iss_private == 1}
++ <b>[Private]</b>
++ {/if}
++ {elseif $field_name == 'isu_order'}
++ {if $list[i].assigned_users_order[$current_user_id]}
++ <img src="{$rel_url}images/updown.gif" alt="move">
++ {/if}
++ {/if}
++ </td>
++ {/if}
++ {/foreach}
++ </tr>
++ {sectionelse}
++ <tr bgcolor="gray">
++ <td colspan="{$col_count}" class="default_white" align="center">
++ <i>{t}No issues could be found.{/t}</i>
++ </td>
++ </tr>
++ {/section}
++ </tbody>
++ <tr bgcolor="{$cell_color}" class="nodrag">
++ <td colspan="{$col_count}">
++ <table width="100%" cellspacing="0" cellpadding="0">
++ <tr class="nodrag">
++ <td width="30%" nowrap>
++ {if $current_role > $roles.developer}
++ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
++ {/if}
++ </td>
++ <td width="40%" align="center" nowrap>
++ <nobr>
++ <input name="first" type="button" value="|<" class="shortcut" onClick="javascript:setPage(0);">
++ <input name="previous" type="button" value="<<" class="shortcut" onClick="javascript:setPage({$list_info.previous_page});">
++ <input type="text" name="page" size="3" maxlength="3" value="{if $list_info.current_page == ''}1{else}{math equation="x + 1" x=$list_info.current_page}{/if}" style="background: {$cell_color};" class="paging_input" onKeyPress="javascript:return checkPageField(event);">
++ <input name="go" type="button" value="{t}Go{/t}" class="shortcut" onClick="javascript:goPage(this.form, this.form.page.value);">
++ <input name="next" type="button" value=">>" class="shortcut" onClick="javascript:setPage({$list_info.next_page});">
++ <input name="last" type="button" value=">|" class="shortcut" onClick="javascript:setPage({$list_info.last_page});">
++ </nobr>
++ </td>
++ <td nowrap>
++ <span class="default_white">{t}Rows per Page:{/t}</span>
++ <select name="page_size" class="default" onChange="javascript:resizePager(this.form);">
++ <option value="5" {if $options.rows == 5}selected{/if}>5</option>
++ <option value="10" {if $options.rows == 10}selected{/if}>10</option>
++ <option value="25" {if $options.rows == 25}selected{/if}>25</option>
++ <option value="50" {if $options.rows == 50}selected{/if}>50</option>
++ <option value="100" {if $options.rows == 100}selected{/if}>100</option>
++ <option value="ALL" {if $options.rows == "ALL"}selected{/if}>{t}ALL{/t}</option>
++ </select>
++ <input type="button" value="{t}Set{/t}" class="shortcut" onClick="javascript:resizePager(this.form);">
++ </td>
++ <td width="30%" class="default_white" align="right">
++ <input type="checkbox" id="hide_closed" name="hide_closed" {if $options.hide_closed}checked{/if} onClick="javascript:hideClosed(this.form);"> <label for="hide_closed">{t}Hide Closed Issues{/t}</label>
++ </td>
++ </tr>
++ </table>
++ </td>
++ </tr>
++ </table>
++ </td>
++ </tr>
++ {if $current_role > $roles.developer}
++ <tr>
++ <td bgcolor="#FFFFFF">
++ <br />
++ {include file="bulk_update.tpl.html"}
++ </td>
++ </tr>
++ {/if}
++ </form>
++ <form target="_csvWindow" method="post" action="csv.php" name="csv_form">
++ <input type="hidden" name="csv_data" value="{$csv_data}">
++ </form>
++</table>
++<br />
++<script type="text/javascript">
++{*
++ * Order issues by drag and drop:
++ * only if sorted by order and viewing your own issues
++ *}
++{if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}
++{literal}
++var before = ''; // make it global variable
++$('#issue_list_table').tableDnD({
++ onDragClass: "tDnD_whileDrag",
++ onDragStart: function(table, row) {
++ before = $.tableDnD.serialize('id');
+ },
-+ dragHandle: "dragHandle"
++ onDrop: function(table, row) {
++ $.post("/ajax/order.php", {before: before, after: $.tableDnD.serialize('id')}, function(data) {
++ if (data.length > 0) {
++ alert(data);
++ }
++ }, "text");
++ }
+});
-+$("#issue_list_table tr").hover(function() {
-+ $('#' + this.id + ' .dragHandle').addClass('showDragHandle');
-+}, function() {
-+ $('#' + this.id + ' .dragHandle').removeClass('showDragHandle');
- });
- {/literal}
- {/if}
- </script>
- {include file="app_info.tpl.html"}
--{include file="footer.tpl.html"}
-\ No newline at end of file
++{/literal}
++{/if}
++</script>
++{include file="app_info.tpl.html"}
+{include file="footer.tpl.html"}
---- eventum-r3776/upgrade/update-database.php 2008-11-18 01:10:18.806283202 +0200
-+++ eventum/upgrade/update-database.php 2008-11-18 01:15:20.703082655 +0200
-@@ -83,6 +83,7 @@
- 1 => '01_notes.php',
- 2 => '02_usr_alias.php',
- 3 => '03_prj_mail_aliases.php',
-+ 4 => '04_isu_order.php',
- );
-
- // sanity check. check that the version table exists.
---- /dev/null 2008-11-04 20:33:38.146691408 +0200
-+++ eventum/upgrade/patches/04_isu_order.php 2008-11-18 01:14:02.972104347 +0200
+\ No newline at end of file
+--- eventum-2.2/upgrade/patches/04_isu_order.php 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/upgrade/patches/04_isu_order.php 2009-10-12 22:10:36.439185157 +0300
@@ -0,0 +1,15 @@
+<?php
+
+
+ return $stmts;
+}
---- ../eventum-old2/htdocs/ajax/update.php 2008-10-13 11:14:23.673428821 +0300
-+++ ./htdocs/ajax/update.php 2008-11-19 15:05:54.490054961 +0200
-@@ -16,9 +16,17 @@
- case 'expected_resolution_date':
- $day = (int)$_POST['day'];
- $month = (int)$_POST['month'];
-- $year = (int)$_POST['year'];
-- if (Issue::updateField($_POST['issueID'], $_POST['fieldName'], sprintf('%04d-%02d-%02d', $year, $month, $day)) !== -1) {
-- echo Date_Helper::getSimpleDate(sprintf('%04d-%02d-%02d', $year, $month, $day), false);
-+ $year = (int)$_POST['year'];
-+ if ($day == 0 && $month == 1 && $year == 0) {
-+ // clear button
-+ $date = null;
-+ } else {
-+ $date = sprintf('%04d-%02d-%02d', $year, $month, $day);
+--- eventum-2.2/upgrade/update-database.php 2009-09-14 18:07:55.000000000 +0300
++++ eventum-2.2-order/upgrade/update-database.php 2009-10-12 22:10:36.439185157 +0300
+@@ -73,6 +73,7 @@
+ 1 => '01_notes.php',
+ 2 => '02_usr_alias.php',
+ 3 => '03_prj_mail_aliases.php',
++ 4 => '04_isu_order.php',
+ );
+
+ // sanity check. check that the version table exists.
+--- eventum-2.2/upgrade/update-database.php.~1~ 1970-01-01 02:00:00.000000000 +0200
++++ eventum-2.2-order/upgrade/update-database.php.~1~ 2009-09-14 18:07:55.000000000 +0300
+@@ -0,0 +1,133 @@
++#!/usr/bin/php
++<?php
++require_once 'init.php';
++
++// on fresh install config is empty or missing
++if (!defined('APP_SQL_DBNAME')) {
++ fwrite(STDERR, "Eventum not configured. Please run setup.\n");
++ exit(1);
++}
++
++define('EXIT_OK', 0);
++define('EXIT_ERROR', 1);
++
++function db_getAll($query) {
++ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
++ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
++
++ $res = DB_Helper::getInstance()->getAll($query, DB_FETCHMODE_ASSOC);
++ if (PEAR::isError($res)) {
++ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
++ exit(1);
+ }
-+ if (Issue::updateField($_POST['issueID'], $_POST['fieldName'], $date) !== -1) {
-+ if (!is_null($date)) {
-+ echo Date_Helper::getSimpleDate(sprintf('%04d-%02d-%02d', $year, $month, $day), false);
++ return $res;
++}
++
++function db_getOne($query) {
++ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
++ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
++
++ $res = DB_Helper::getInstance()->getOne($query);
++ if (PEAR::isError($res)) {
++ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
++ exit(1);
++ }
++ return $res;
++}
++
++function db_getCol($query) {
++ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
++ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
++
++ $res = DB_Helper::getInstance()->getCol($query);
++ if (PEAR::isError($res)) {
++ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
++ exit(1);
++ }
++ return $res;
++}
++
++function db_query($query) {
++ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
++ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
++
++ $res = DB_Helper::getInstance()->query($query);
++ if (PEAR::isError($res)) {
++ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
++ exit(1);
++ }
++ return $res;
++}
++
++function apply_db_changes($stmts) {
++ foreach ($stmts as $stmt) {
++ db_query($stmt);
++ }
++}
++
++function patch_database() {
++ /*
++ * database versions. each version script can create it's dynamic queries
++ */
++ $versions = array(
++ 1 => '01_notes.php',
++ 2 => '02_usr_alias.php',
++ 3 => '03_prj_mail_aliases.php',
++ );
++
++ // sanity check. check that the version table exists.
++ $version = db_getOne("SELECT ver_version FROM %TABLE_PREFIX%version");
++ if (!isset($version)) {
++ # insert initial value
++ db_query("INSERT INTO %TABLE_PREFIX%version SET ver_version=0");
++ $version = 0;
++ }
++ $target = max(array_keys($versions));
++ echo "Current database version: $version; Versions available: $target\n";
++ if ($target < $version) {
++ echo "ERROR: Your database version is greater ($version) than this upgrade supports ($target)!\n";
++ return EXIT_ERROR;
++ }
++ if ($target == $version) {
++ echo "Database already at version $version. Nothing to upgrade.\n";
++ return EXIT_OK;
++ }
++
++ echo "Upgrading database to version $target\n";
++ for ($i = $version + 1; $i <= $target; $i++) {
++ if (empty($versions[$i])) {
++ echo "ERROR: patch $i is not recorded in upgrade script.\n";
++ return EXIT_ERROR;
+ }
- } else {
- echo 'update failed';
- }
---- eventum_tar/htdocs/js/global.js 2008-11-06 16:54:30.000000000 +0200
-+++ ./htdocs/js/global.js 2008-11-19 15:06:21.820071605 +0200
-@@ -817,10 +817,15 @@
- function (date, dteObj) {
- fieldName = masterObj.id.substr(0,masterObj.id.indexOf('|'));
- issueID = masterObj.id.substr(masterObj.id.indexOf('|')+1);
-+ if (date == '') {
-+ // clear button
-+ dteObj.selectedDay = 0;
-+ dteObj.selectedMonth = 0;
-+ dteObj.selectedYear = 0;
-+ }
-+ //alertProperties(date);
- $.post("/ajax/update.php", {fieldName: fieldName, issueID: issueID, day: dteObj.selectedDay, month: (dteObj.selectedMonth+1), year: dteObj.selectedYear}, function(data) {
-- if (data.length > 0) {
-- masterObj.innerHTML = data;
-- }
-+ masterObj.innerHTML = data;
- }, "text");
- },
- // config
---- eventum-old2/lib/eventum/class.issue.php 2008-10-15 18:30:31.590172372 +0300
-+++ ./lib/eventum/class.issue.php 2008-11-19 15:04:43.710659328 +0200
-@@ -1694,7 +1694,11 @@
- $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
- break;
- case 'expected_resolution_date':
-- $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
-+ if (is_null($filed_value)) {
-+ $stmt .= ", iss_expected_resolution_date = null";
-+ } else {
-+ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
-+ }
- break;
- case 'release':
- $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
---- eventum/lib/eventum/class.issue.php 2008-10-15 01:46:20.000000000 +0300
-+++ eventum-new/lib/eventum/class.issue.php 2008-10-15 02:02:25.000000000 +0300
-@@ -2050,7 +2050,7 @@
- " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
- WHERE
- isu_iss_id IN ($issue_id)";
-- if ($usr_id !== FALSE) {
-+ if (!empty($usr_id)) {
- $stmt.= " AND isu_usr_id IN ($usr_id)";
- }
- $stmt.= "ORDER BY isu_order";
-@@ -2135,7 +2135,6 @@
- if ($usr_id) {
- History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
- }
-- self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
- return 1;
- }
- }
++ $patch = APP_SQL_PATCHES_PATH . '/' . $versions[$i];
++ echo "Checking patch $patch\n";
++ if (!file_exists($patch)) {
++ echo "ERROR: Patch file doesn't exist\n";
++ return EXIT_ERROR;
++ }
++ require $patch;
++ $func = "db_patch_$i";
++ if (!function_exists($func)) {
++ echo "ERROR: Patch did not define '$func' function\n";
++ return EXIT_ERROR;
++ }
++ $patchset = $func();
++ echo "Applying patch ", $i, ": ", count($patchset), " queries\n";
++ apply_db_changes($patchset);
++ db_query("UPDATE %TABLE_PREFIX%version SET ver_version=$i");
++ }
++
++ return EXIT_OK;
++}
++
++if (php_sapi_name() != 'cli') {
++ echo "<pre>\n";
++}
++
++$ret = patch_database();
++
++if (php_sapi_name() != 'cli') {
++ echo "</pre>\n";
++}
++
++exit($ret);