--- 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 @@ + $nID) { + if ($nID != $before_filterd[$key]) { + if ($nID) { + $stmt = "UPDATE + " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user + SET + isu_order = " . $order[$before_filterd[$key]] . " + WHERE + isu_iss_id = $nID 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__); + die('update failed'); + } + } + } +} --- 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 @@ + --- 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 @@ + --- 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 + * You can set up various options to control how the system will work + * Copyright (c) Denis Howlett + * Licensed like jQuery, see http://docs.jquery.com/License. + * + * Configuration options: + * + * onDragStyle + * This is the style that is assigned to the row during drag. There are limitations to the styles that can be + * associated with a row (such as you can't assign a border--well you can, but it won't be + * displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as + * a map (as used in the jQuery css(...) function). + * onDropStyle + * This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations + * to what you can do. Also this replaces the original style, so again consider using onDragClass which + * is simply added and then removed on drop. + * onDragClass + * This class is added for the duration of the drag and then removed when the row is dropped. It is more + * flexible than using onDragStyle since it can be inherited by the row cells and other content. The default + * is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your + * stylesheet. + * onDrop + * Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table + * and the row that was dropped. You can work out the new order of the rows by using + * table.rows. + * onDragStart + * Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the + * table and the row which the user has started to drag. + * onAllowDrop + * Pass a function that will be called as a row is over another row. If the function returns true, allow + * dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under + * the cursor. It returns a boolean: true allows the drop, false doesn't allow it. + * scrollAmount + * This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the + * window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2, + * FF3 beta + * dragHandle + * This is the name of a class that you assign to one or more cells in each row that is draggable. If you + * specify this class, then you are responsible for setting cursor: move in the CSS and only these cells + * will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where + * the whole row is draggable. + * + * Other ways to control behaviour: + * + * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows + * that you don't want to be draggable. + * + * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form + * []=&[]= so that you can send this back to the server. The table must have + * an ID as must all the rows. + * + * Other methods: + * + * $("...").tableDnDUpdate() + * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells). + * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again. + * The table maintains the original configuration (so you don't have to specify it again). + * + * $("...").tableDnDSerialize() + * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be + * called from anywhere and isn't dependent on the currentTable being set up correctly before calling + * + * Known problems: + * - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0 + * + * Version 0.2: 2008-02-20 First public version + * Version 0.3: 2008-02-07 Added onDragStart option + * Made the scroll amount configurable (default is 5 as before) + * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes + * Added onAllowDrop to control dropping + * Fixed a bug which meant that you couldn't set the scroll amount in both directions + * Added serialize method + * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row + * draggable + * Improved the serialize method to use a default (and settable) regular expression. + * Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table + */ +jQuery.tableDnD = { + /** Keep hold of the current table being dragged */ + currentTable : null, + /** Keep hold of the current drag object if any */ + dragObject: null, + /** The current mouse offset */ + mouseOffset: null, + /** Remember the old value of Y so that we don't do too much processing */ + oldY: 0, + + /** Actually build the structure */ + build: function(options) { + // Set up the defaults if any + + this.each(function() { + // This is bound to each matching table, set up the defaults and override with user options + this.tableDnDConfig = jQuery.extend({ + onDragStyle: null, + onDropStyle: null, + // Add in the default class for whileDragging + onDragClass: "tDnD_whileDrag", + onDrop: null, + onDragStart: null, + scrollAmount: 5, + serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs + serializeParamName: null, // If you want to specify another parameter name instead of the table ID + dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable + }, options || {}); + // Now make the rows draggable + jQuery.tableDnD.makeDraggable(this); + }); + + // Now we need to capture the mouse up and mouse move event + // We can use bind so that we don't interfere with other event handlers + jQuery(document) + .bind('mousemove', jQuery.tableDnD.mousemove) + .bind('mouseup', jQuery.tableDnD.mouseup); + + // Don't break the chain + return this; + }, + + /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */ + makeDraggable: function(table) { + var config = table.tableDnDConfig; + if (table.tableDnDConfig.dragHandle) { + // We only need to add the event to the specified cells + var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table); + cells.each(function() { + // The cell is bound to "this" + jQuery(this).mousedown(function(ev) { + jQuery.tableDnD.dragObject = this.parentNode; + jQuery.tableDnD.currentTable = table; + jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev); + if (config.onDragStart) { + // Call the onDrop method if there is one + config.onDragStart(table, this); + } + return false; + }); + }) + } else { + // For backwards compatibility, we add the event to the whole row + var rows = jQuery("tr", table); // get all the rows as a wrapped set + rows.each(function() { + // Iterate through each row, the row is bound to "this" + var row = jQuery(this); + if (! row.hasClass("nodrag")) { + row.mousedown(function(ev) { + if (ev.target.tagName == "TD") { + jQuery.tableDnD.dragObject = this; + jQuery.tableDnD.currentTable = table; + jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev); + if (config.onDragStart) { + // Call the onDrop method if there is one + config.onDragStart(table, this); + } + return false; + } + }).css("cursor", "move"); // Store the tableDnD object + } + }); + } + }, + + updateTables: function() { + this.each(function() { + // this is now bound to each matching table + if (this.tableDnDConfig) { + jQuery.tableDnD.makeDraggable(this); + } + }) + }, + + /** Get the mouse coordinates from the event (allowing for browser differences) */ + mouseCoords: function(ev){ + if(ev.pageX || ev.pageY){ + return {x:ev.pageX, y:ev.pageY}; + } + return { + x:ev.clientX + document.body.scrollLeft - document.body.clientLeft, + y:ev.clientY + document.body.scrollTop - document.body.clientTop + }; + }, + + /** Given a target element and a mouse event, get the mouse offset from that element. + To do this we need the element's position and the mouse position */ + getMouseOffset: function(target, ev) { + ev = ev || window.event; + + var docPos = this.getPosition(target); + var mousePos = this.mouseCoords(ev); + return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y}; + }, + + /** Get the position of an element by going up the DOM tree and adding up all the offsets */ + getPosition: function(e){ + var left = 0; + var top = 0; + /** Safari fix -- thanks to Luis Chato for this! */ + if (e.offsetHeight == 0) { + /** Safari 2 doesn't correctly grab the offsetTop of a table row + this is detailed here: + http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/ + the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild. + note that firefox will return a text node as a first child, so designing a more thorough + solution may need to take that into account, for now this seems to work in firefox, safari, ie */ + e = e.firstChild; // a table cell + } + + while (e.offsetParent){ + left += e.offsetLeft; + top += e.offsetTop; + e = e.offsetParent; + } + + left += e.offsetLeft; + top += e.offsetTop; + + return {x:left, y:top}; + }, + + mousemove: function(ev) { + if (jQuery.tableDnD.dragObject == null) { + return; + } + + var dragObj = jQuery(jQuery.tableDnD.dragObject); + var config = jQuery.tableDnD.currentTable.tableDnDConfig; + var mousePos = jQuery.tableDnD.mouseCoords(ev); + var y = mousePos.y - jQuery.tableDnD.mouseOffset.y; + //auto scroll the window + var yOffset = window.pageYOffset; + if (document.all) { + // Windows version + //yOffset=document.body.scrollTop; + if (typeof document.compatMode != 'undefined' && + document.compatMode != 'BackCompat') { + yOffset = document.documentElement.scrollTop; + } + else if (typeof document.body != 'undefined') { + yOffset=document.body.scrollTop; + } + + } + + if (mousePos.y-yOffset < config.scrollAmount) { + window.scrollBy(0, -config.scrollAmount); + } else { + var windowHeight = window.innerHeight ? window.innerHeight + : document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight; + if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) { + window.scrollBy(0, config.scrollAmount); + } + } + + + if (y != jQuery.tableDnD.oldY) { + // work out if we're going up or down... + var movingDown = y > jQuery.tableDnD.oldY; + // update the old value + jQuery.tableDnD.oldY = y; + // update the style to show we're dragging + if (config.onDragClass) { + dragObj.addClass(config.onDragClass); + } else { + dragObj.css(config.onDragStyle); + } + // If we're over a row then move the dragged row to there so that the user sees the + // effect dynamically + var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y); + if (currentRow) { + // TODO worry about what happens when there are multiple TBODIES + if (movingDown && jQuery.tableDnD.dragObject != currentRow) { + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling); + } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) { + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow); + } + } + } + + return false; + }, + + /** We're only worried about the y position really, because we can only move rows up and down */ + findDropTargetRow: function(draggedRow, y) { + var rows = jQuery.tableDnD.currentTable.rows; + for (var i=0; i rowY - rowHeight) && (y < (rowY + rowHeight))) { + // that's the row we're over + // If it's the same as the current row, ignore it + if (row == draggedRow) {return null;} + var config = jQuery.tableDnD.currentTable.tableDnDConfig; + if (config.onAllowDrop) { + if (config.onAllowDrop(draggedRow, row)) { + return row; + } else { + return null; + } + } else { + // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic) + var nodrop = jQuery(row).hasClass("nodrop"); + if (! nodrop) { + return row; + } else { + return null; + } + } + return row; + } + } + return null; + }, + + mouseup: function(e) { + if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) { + var droppedRow = jQuery.tableDnD.dragObject; + var config = jQuery.tableDnD.currentTable.tableDnDConfig; + // If we have a dragObject, then we need to release it, + // The row will already have been moved to the right place so we just reset stuff + if (config.onDragClass) { + jQuery(droppedRow).removeClass(config.onDragClass); + } else { + jQuery(droppedRow).css(config.onDropStyle); + } + jQuery.tableDnD.dragObject = null; + if (config.onDrop) { + // Call the onDrop method if there is one + config.onDrop(jQuery.tableDnD.currentTable, droppedRow); + } + jQuery.tableDnD.currentTable = null; // let go of the table too + } + }, + + serialize: function() { + if (jQuery.tableDnD.currentTable) { + return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable); + } else { + return "Error: No Table id set, you need to set an id on your table and every row"; + } + }, + + serializeTable: function(table) { + var result = ""; + var tableId = table.id; + var rows = table.rows; + for (var i=0; i 0) result += "&"; + var rowId = rows[i].id; + if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) { + rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0]; + } + + result += tableId + '[]=' + rowId; + } + 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 @@ + | +// +----------------------------------------------------------------------+ +// +// @(#) $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 @@ + | +// +----------------------------------------------------------------------+ +// +// + + +/** + * Class to handle determining which columns should be displayed and in what order + * on a page (e.g. Issue Listing page). + * + * @author Bryan Alsdorf + * @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 @@ + | +// +----------------------------------------------------------------------+ +// + + +/** + * 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 + * @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 @@ + | +// +----------------------------------------------------------------------+ +// + + +/** + * 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 + * @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 @@ + | +// +----------------------------------------------------------------------+ +// + + +/** + * 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 + * @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 @@ + {/literal} @@ -169,11 +198,11 @@ - - +
+ - + {if $current_role > $roles.developer} {/foreach} - {else} + {elseif $field_name != 'isu_order' || $isu_order_user}
- +
{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}) {include file="help_link.tpl.html" topic="list"} @@ -193,7 +222,7 @@
@@ -208,7 +237,7 @@ {if $sorting.images[$fld_name_id] != ""}{/if} {if $field_name == 'iss_summary'} @@ -232,20 +261,21 @@ {/if} {/foreach} + {section name="i" loop=$list} - = $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}> + = $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} {/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} - {/foreach} - {else} - {/if} @@ -300,10 +332,11 @@ {/section} - + +
- {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id} + + {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id} + {elseif $field_name != 'isu_order' || $isu_order_user} + {if $field_name == 'iss_id'} {$list[i].iss_id} {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"} +
{$list[i].iss_expected_resolution_date|escape:"html"} 
{elseif $field_name == 'iss_summary'} {$list[i].iss_summary|escape:"html"} {if $list[i].redeemed} @@ -288,6 +318,8 @@ {if $list[i].iss_private == 1} [Private] {/if} + {elseif $field_name == 'isu_order'} + {if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user} {/if} {/if}
- +
{if $current_role > $roles.developer} @@ -355,6 +388,35 @@

- + {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} + +{/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} + + + + + + + {if $current_role > $roles.developer} + + + + {/if} + + + + +
+ + + + + + {if $current_role > $roles.developer} + + {/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`"} + + {/foreach} + {else} + + {/if} + {/foreach} + + {section name="i" loop=$list} + = $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}> + {if $current_role > $roles.developer} + + {/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} + + {/foreach} + {else} + + {/if} + {/foreach} + + {sectionelse} + + + + {/section} + + + +
+ + + + {if $current_role != $roles.customer} + + {/if} + +
+ {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}) + {include file="help_link.tpl.html" topic="list"} + + {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up} + {t}hide/show{/t}: + [ {t}quick search{/t} ] + {if $current_role > $roles.reporter}[ {t}advanced search{/t} ]{/if} + {if $current_role > $roles.reporter}[ {t}current filters{/t} ]{/if} + {if $current_role > $roles.developer}[ {t}bulk update tool{/t} ]{/if} + {/if} +
+
+ + + {$fld_title|escape:"html"} + {if $sorting.images[$fld_name_id] != ""}{/if} + + {if $field_name == 'iss_summary'} + + + + + +
+ {t}Summary{/t} + {if $sorting.images.iss_summary != ""}{/if} + + {t}Export Data:{/t} +
+ {elseif $sorting.links[$field_name] != ''} + {$column.title} + {if $sorting.images[$field_name] != ""}{/if} + {else} + {$column.title} + {/if} +
+ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id} + + {if $field_name == 'iss_id'} + {$list[i].iss_id} + {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'} + {$list[i].iss_summary|escape:"html"} + {if $list[i].redeemed} + [Redeemed] + {/if} + {if $list[i].iss_private == 1} + [Private] + {/if} + {/if} +
+ {t}No issues could be found.{/t} +
+ + + + + + + +
+ {if $current_role > $roles.developer} + + {/if} + + + + + + + + + + + {t}Rows per Page:{/t} + + + +   +
+
+
+
+ {include file="bulk_update.tpl.html"} +
+
+ +{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} + +{/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} + + + + + + + {if $current_role > $roles.developer} + + + + {/if} + + + + +
+ + + + + + {if $current_role > $roles.developer} + + {/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`"} + + {/foreach} + {elseif $field_name != 'isu_order' || $isu_order_user} + + {/if} + {/foreach} + + + {section name="i" loop=$list} + = $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} + + {/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} + + {/foreach} + {elseif $field_name != 'isu_order' || $isu_order_user} + + {/if} + {/foreach} + + {sectionelse} + + + + {/section} + + + + +
+ + + + {if $current_role != $roles.customer} + + {/if} + +
+ {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}) + {include file="help_link.tpl.html" topic="list"} + + {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up} + {t}hide/show{/t}: + [ {t}quick search{/t} ] + {if $current_role > $roles.reporter}[ {t}advanced search{/t} ]{/if} + {if $current_role > $roles.reporter}[ {t}current filters{/t} ]{/if} + {if $current_role > $roles.developer}[ {t}bulk update tool{/t} ]{/if} + {/if} +
+
+ + + {$fld_title|escape:"html"} + {if $sorting.images[$fld_name_id] != ""}{/if} + + {if $field_name == 'iss_summary'} + + + + + +
+ {t}Summary{/t} + {if $sorting.images.iss_summary != ""}{/if} + + {t}Export Data:{/t} +
+ {elseif $sorting.links[$field_name] != ''} + {$column.title} + {if $field_name == 'isu_order'} +
{$users[$isu_order_user]} + {/if} + {if $sorting.images[$field_name] != ""}{/if} + {else} + {$column.title} + {/if} +
+ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id} + + {if $field_name == 'iss_id'} + {$list[i].iss_id} + {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'} + {$list[i].iss_summary|escape:"html"} + {if $list[i].redeemed} + [Redeemed] + {/if} + {if $list[i].iss_private == 1} + [Private] + {/if} + {elseif $field_name == 'isu_order'} + {if $list[i].assigned_users_order[$current_user_id]} + move + {/if} + {/if} +
+ {t}No issues could be found.{/t} +
+ + + + + + + +
+ {if $current_role > $roles.developer} + + {/if} + + + + + + + + + + + {t}Rows per Page:{/t} + + + +   +
+
+
+
+ {include file="bulk_update.tpl.html"} +
+
+ +{include file="app_info.tpl.html"} +{include file="footer.tpl.html"} \ 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 @@ + '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 +getAll($query, DB_FETCHMODE_ASSOC); + if (PEAR::isError($res)) { + echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n"; + exit(1); + } + 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; + } + $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 "
\n";
+}
+
+$ret = patch_database();
+
+if (php_sapi_name() != 'cli') {
+	echo "
\n"; +} + +exit($ret);