X-Git-Url: http://git.pld-linux.org/?a=blobdiff_plain;f=eventum-order.patch;h=5764514ba18bbecb6b4ef71b611d190aebb24fb3;hb=371b276ab631c8d0f88f9387e6f31dffa7ad386a;hp=0594dabf28342fb7295a4b01cca7007ae5839641;hpb=f4782eee27174c93cd24097a0c572e82a0ae6717;p=packages%2Feventum.git diff --git a/eventum-order.patch b/eventum-order.patch index 0594dab..5764514 100644 --- a/eventum-order.patch +++ b/eventum-order.patch @@ -1,10 +1,8 @@ --- 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 @@ +@@ -0,0 +1,69 @@ + ++ * 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, + -+$(document).ready(function() { -+ // dialog type calender isn't working in Konqueror beacuse it's not a supported browser for either jQuery or jQuery UI -+ // http://groups.google.com/group/jquery-ui/browse_thread/thread/ea61238c34cb5f33/046837b02fb90b5c -+ if (navigator.appName != 'Konqueror') { -+ $(".inline_date_pick").click(function() { -+ var masterObj = this; -+ var masterObjPos = $(masterObj).offset(); -+ // offset gives uses top and left but datepicker needs pageX and pageY -+ var masterObjPos = {pageX: masterObjPos.left, pageY: masterObjPos.top}; -+ $(this).datepicker( -+ // we use dialog type calender so we won't haveto have a hidden element on the page -+ 'dialog', -+ // selected date -+ masterObj.innerHTML, -+ // onclick handler -+ function (date, dteObj) { -+ fieldName = masterObj.id.substr(0,masterObj.id.indexOf('|')); -+ issueID = masterObj.id.substr(masterObj.id.indexOf('|')+1); -+ if (date == '') { -+ // clear button -+ dteObj.selectedDay = 0; -+ dteObj.selectedMonth = 0; -+ dteObj.selectedYear = 0; -+ } -+ //alertProperties(date); -+ $.post("/ajax/update.php", {fieldName: fieldName, issueID: issueID, day: dteObj.selectedDay, month: (dteObj.selectedMonth+1), year: dteObj.selectedYear}, function(data) { -+ masterObj.innerHTML = data; -+ }, "text"); -+ }, -+ // config -+ {dateFormat: 'dd M yy', duration: ""}, -+ // position of the datepicker calender - taken from div's offset -+ masterObjPos -+ ); -+ return false; ++ /** 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); + }); -+ } -+}); + - //--> ---- 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; ++ result += tableId + '[]=' + rowId; + } ++ return result; ++ }, + -+ $access[$issue_id . "-" . $usr_id] = $return; -+ return $return; ++ serializeTables: function() { ++ var result = ""; ++ this.each(function() { ++ // this is now bound to each matching table ++ result += jQuery.tableDnD.serializeTable(this); ++ }); ++ return result; + } + ++} + -+ /** -+ * 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; ++jQuery.fn.extend( ++ { ++ tableDnD : jQuery.tableDnD.build, ++ tableDnDUpdate : jQuery.tableDnD.updateTables, ++ tableDnDSerialize: jQuery.tableDnD.serializeTables ++ } ++); +\ No newline at end of file +--- eventum-2.3.2/htdocs/list.php~ 2012-03-09 18:19:56.000000000 +0200 ++++ eventum-2.3.2/htdocs/list.php 2012-03-09 18:32:43.998284397 +0200 +@@ -67,6 +67,11 @@ + } + } + ++@$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); + -+ 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]; + if (!empty($_REQUEST['nosave'])) { + $options = Search::saveSearchParams(false); + } else { +@@ -92,6 +97,24 @@ + } + $assign_options += $users; + ++// get the isu_order (assigned 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); ++} + ++if (isset($isu_order_user)) { ++ $tpl->assign("isu_order_user", $isu_order_user); ++} + -+ /** -+ * Clears closed information from an issues. -+ * -+ * @access public -+ * @param integer $issue_id The ID of the issue -+ */ -+ function clearClosed($issue_id) -+ { + $list = Search::getListing($prj_id, $options, $pagerRow, $rows); + $tpl->assign("list", $list["list"]); + $tpl->assign("list_info", $list["info"]); +--- 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.3.1/lib/eventum/class.issue.php~ 2011-09-15 09:36:55.000000000 +0300 ++++ eventum-2.3.1/lib/eventum/class.issue.php 2011-09-15 09:42:02.844032474 +0300 +@@ -1374,6 +1374,7 @@ + return -1; + } + ++ self::moveOrderForAllUsers($issue_id, 1000); + $prj_id = self::getProjectID($issue_id); + + // record the change +@@ -1800,16 +1801,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 ++ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user + SET -+ iss_closed_date = null, -+ iss_res_id = null ++ isu_order = isu_order + 1 + WHERE -+ iss_id=" . Misc::escapeInteger($issue_id); ++ 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; + } -+ 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; -+ } -+ } -+ -+ ++ // 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 +1842,78 @@ + } + } + + /** -+ * 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 ++ * 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 getIssueByRootMessageID($msg_id) ++ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "") + { -+ static $returns; -+ -+ if (!empty($returns[$msg_id])) { -+ return $returns[$msg_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)"; + } -+ $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); ++ $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 false; -+ } -+ if (empty($res)) { -+ $returns[$msg_id] = false; ++ return -1; + } else { -+ $returns[$msg_id] = $res; ++ $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; + } -+ return $returns[$msg_id]; + } + -+ + /** -+ * Sets the assignees for the issue + * -+ * @param integer $issue_id -+ * @param array $assignees ++ * Method used to rearrange order list in the db according to known deleted records ++ * ++ * @access private ++ * @param mixed deleteorder list ++ * @return void + */ -+ function setAssignees($issue_id, $assignees) ++ function rearrangeDeleteUserAssociationOrderList($delete_order_list) + { -+ 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; ++ if (empty($delete_order_list) || (!is_array($delete_order_list))) { ++ return -1; + } -+ -+ $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; ++ 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; ++ } + } -+ $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())); ++ return 1; + } ++ + + /** + * Method used to delete all user assignments for a specific issue. +@@ -1839,6 +1929,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 +1960,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 +@@ -2020,6 +2021,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; + } + +@@ -3253,6 +3352,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 +3365,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 +3374,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 +4358,7 @@ + Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__); + return -1; + } ++ self::moveOrderForAllUsers($issue_id, 1); + } + + +@@ -4346,4 +4458,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 @@ -16238,19 +854,42 @@ + 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 @@ ++ } ++ + } +--- eventum-2.3.1/lib/eventum/class.search.php~ 2011-04-20 17:22:15.000000000 +0300 ++++ eventum-2.3.1/lib/eventum/class.search.php 2011-04-20 17:44:34.498519260 +0300 +@@ -63,6 +63,12 @@ + { + $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 === '') { +@@ -174,6 +174,7 @@ + "iss_expected_resolution_date" => "desc", + "pre_title" => "asc", + "assigned" => "asc", ++ "isu_order" => "desc", + ); + + foreach ($custom_fields as $fld_id => $fld_name) { +--- eventum-2.3.1/templates/header.tpl.html~ 2011-09-15 09:36:55.000000000 +0300 ++++ eventum-2.3.1/templates/header.tpl.html 2011-09-15 09:43:49.318473817 +0300 +@@ -18,6 +18,7 @@ + - - {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);