]> git.pld-linux.org Git - packages/eventum.git/blame - eventum-order.patch
- rediff
[packages/eventum.git] / eventum-order.patch
CommitLineData
f4782eee
ER
1--- eventum-2.2/htdocs/ajax/order.php 1970-01-01 02:00:00.000000000 +0200
2+++ eventum-2.2-order/htdocs/ajax/order.php 2009-10-12 22:10:36.429185594 +0300
be762003
ER
3@@ -0,0 +1,72 @@
4+<?
5+require_once(dirname(__FILE__) . '/../init.php');
6+require_once(APP_INC_PATH . "class.auth.php");
7+require_once(APP_INC_PATH . "class.issue.php");
8+
9+// check login
10+if (!Auth::hasValidCookie(APP_COOKIE)) {
11+ exit;
12+}
13+
14+
15+
16+if (!isset($_POST['before']) || !isset($_POST['after'])) {
17+ exit;
18+}
19+
29e18d8b
ER
20+parse_str($_POST['before'], $before);
21+parse_str($_POST['after'], $after);
be762003
ER
22+
23+$before = $before['issue_list_table'];
24+$after = $after['issue_list_table'];
25+
26+
27+$before = array_slice($before, 2, count($before)-3);
28+$after = array_slice($after, 2, count($after)-3);
29+
30+if (count($before) != count($after) or count($before) < 1) {
31+ exit;
32+}
33+
34+$usr_id = Auth::getUserID();
35+
36+$order = Issue::getIssueOrderByUser($usr_id);
37+
38+if (!count($order)) {
39+ // no prev order list
40+ exit;
41+}
42+
43+$after_filterd = array();
44+$before_filterd = array();
45+
46+// remove issues that are not assigned to me
47+foreach ($after as $id) {
48+ if (isset($order[$id])) {
49+ $after_filterd[] = $id;
50+ }
51+}
52+foreach ($before as $id) {
53+ if (isset($order[$id])) {
54+ $before_filterd[] = $id;
55+ }
56+}
57+
58+foreach ($after_filterd as $key => $nID) {
59+ if ($nID != $before_filterd[$key]) {
60+ if ($nID) {
61+ $stmt = "UPDATE
62+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
63+ SET
64+ isu_order = " . $order[$before_filterd[$key]] . "
65+ WHERE
66+ isu_iss_id = $nID AND
67+ isu_usr_id = $usr_id";
c2e7ae63 68+ $res = DB_Helper::getInstance()->query($stmt);
be762003
ER
69+ if (PEAR::isError($res)) {
70+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
71+ die('update failed');
72+ }
73+ }
74+ }
75+}
f4782eee
ER
76--- eventum-2.2/htdocs/ajax/update.php 1970-01-01 02:00:00.000000000 +0200
77+++ eventum-2.2-order/htdocs/ajax/update.php 2009-10-12 22:10:36.439185157 +0300
78@@ -0,0 +1,38 @@
be762003
ER
79+<?
80+require_once(dirname(__FILE__) . '/../init.php');
81+require_once(APP_INC_PATH . "class.auth.php");
82+require_once(APP_INC_PATH . "class.issue.php");
83+
84+// check login
85+if (!Auth::hasValidCookie(APP_COOKIE)) {
86+ exit;
87+}
88+
89+if (!is_numeric($_POST['issueID'])) {
90+ exit;
91+}
92+
93+switch ($_POST['fieldName']) {
94+ case 'expected_resolution_date':
95+ $day = (int)$_POST['day'];
96+ $month = (int)$_POST['month'];
f4782eee
ER
97+ $year = (int)$_POST['year'];
98+ if ($day == 0 && $month == 1 && $year == 0) {
99+ // clear button
100+ $date = null;
101+ } else {
102+ $date = sprintf('%04d-%02d-%02d', $year, $month, $day);
103+ }
104+ if (Issue::updateField($_POST['issueID'], $_POST['fieldName'], $date) !== -1) {
105+ if (!is_null($date)) {
106+ echo Date_Helper::getSimpleDate(sprintf('%04d-%02d-%02d', $year, $month, $day), false);
107+ }
be762003
ER
108+ } else {
109+ echo 'update failed';
110+ }
111+ exit;
112+ break;
113+ default:
114+ die('object type not supported');
115+ break;
116+}
f4782eee
ER
117--- eventum-2.2/htdocs/css/style.css 2009-09-14 18:07:55.000000000 +0300
118+++ eventum-2.2-order/htdocs/css/style.css 2009-10-12 22:10:36.439185157 +0300
119@@ -172,4 +172,22 @@
120 font-size: 70%;
121 font-family: Verdana, Arial, Helvetica, sans-serif;
122 padding: 10px;
123-}
124\ No newline at end of file
125+}
126+.tDnD_whileDrag td {
127+ background-color: #ffffdd;
128+}
129+.tDnD_whileDrag td {
130+ border: 1px solid red;
131+}
132+.inline_date_pick {
133+ cursor: pointer;
134+}
135+.custom_field {
136+ cursor: pointer;
137+}
138+.showDragHandle {
139+ cursor: move;
140+ background-image: url(../images/updown2.gif);
141+ background-repeat: no-repeat;
142+ background-position: center center;
143+}
144--- eventum-2.2/htdocs/js/global.js 2009-09-14 18:07:55.000000000 +0300
145+++ eventum-2.2-order/htdocs/js/global.js 2009-10-12 22:10:36.439185157 +0300
146@@ -798,4 +798,44 @@
147 firstDay: user_prefs.week_firstday
148 });
149 });
be762003 150+
f4782eee
ER
151+$(document).ready(function() {
152+ // dialog type calender isn't working in Konqueror beacuse it's not a supported browser for either jQuery or jQuery UI
153+ // http://groups.google.com/group/jquery-ui/browse_thread/thread/ea61238c34cb5f33/046837b02fb90b5c
154+ if (navigator.appName != 'Konqueror') {
155+ $(".inline_date_pick").click(function() {
156+ var masterObj = this;
157+ var masterObjPos = $(masterObj).offset();
158+ // offset gives uses top and left but datepicker needs pageX and pageY
159+ var masterObjPos = {pageX: masterObjPos.left, pageY: masterObjPos.top};
160+ $(this).datepicker(
161+ // we use dialog type calender so we won't haveto have a hidden element on the page
162+ 'dialog',
163+ // selected date
164+ masterObj.innerHTML,
165+ // onclick handler
166+ function (date, dteObj) {
167+ fieldName = masterObj.id.substr(0,masterObj.id.indexOf('|'));
168+ issueID = masterObj.id.substr(masterObj.id.indexOf('|')+1);
169+ if (date == '') {
170+ // clear button
171+ dteObj.selectedDay = 0;
172+ dteObj.selectedMonth = 0;
173+ dteObj.selectedYear = 0;
174+ }
175+ //alertProperties(date);
176+ $.post("/ajax/update.php", {fieldName: fieldName, issueID: issueID, day: dteObj.selectedDay, month: (dteObj.selectedMonth+1), year: dteObj.selectedYear}, function(data) {
177+ masterObj.innerHTML = data;
178+ }, "text");
179+ },
180+ // config
181+ {dateFormat: 'dd M yy', duration: ""},
182+ // position of the datepicker calender - taken from div's offset
183+ masterObjPos
184+ );
185+ return false;
186+ });
187+ }
188+});
be762003 189+
f4782eee
ER
190 //-->
191--- eventum-2.2/htdocs/js/global.js.~1~ 1970-01-01 02:00:00.000000000 +0200
192+++ eventum-2.2-order/htdocs/js/global.js.~1~ 2009-09-14 18:07:55.000000000 +0300
193@@ -0,0 +1,801 @@
194+<!--
195+// @(#) $Id$
196+var today = new Date();
197+var expires = new Date(today.getTime() + (56 * 86400000));
be762003 198+
f4782eee
ER
199+function addFileRow(element_name, field_name)
200+{
201+ if (document.all) {
202+ var fileTable = document.all[element_name];
203+ } else if (!document.all && document.getElementById) {
204+ var fileTable = document.getElementById(element_name);
205+ }
206+ if (!fileTable) {
207+ return;
208+ }
209+ rows = fileTable.rows.length;
be762003 210+
f4782eee
ER
211+ // check if last box is empty and if it is, don't add another
212+ if (document.all) {
213+ var last_field = document.all[field_name + '_' + rows];
214+ } else if (!document.all && document.getElementById) {
215+ var last_field = document.getElementById(field_name + '_' + rows);
216+ }
217+ if (last_field && last_field.value == '') {
218+ return;
219+ }
be762003 220+
f4782eee
ER
221+ newRow = fileTable.insertRow(rows);
222+ cell = newRow.insertCell(0);
223+ if (document.all) {
224+ cell.innerHTML = '<input id="' + field_name + '_' + (rows+1) + '" class="shortcut" size="40" type="file" name="' + field_name + '" onChange="javascript:addFileRow(\'' + element_name + '\', \'' + field_name + '\');">';
225+ } else {
226+ var input = document.createElement('INPUT');
227+ input.setAttribute('type', 'file');
228+ input.name = field_name;
229+ input.className = 'shortcut';
230+ input.size = 40;
231+ input.onchange = new Function('addFileRow(\'' + element_name + '\', \'' + field_name + '\');');
232+ input.id = field_name + '_' + (rows+1);
233+ cell.appendChild(input);
234+ }
235+}
236+
237+function inArray(value, stack)
238+{
239+ for (var i = 0; i < stack.length; i++) {
240+ if (stack[i] == value) {
241+ return true;
be762003 242+ }
f4782eee
ER
243+ }
244+ return false;
245+}
be762003 246+
f4782eee
ER
247+function getEmailFromAddress(str)
248+{
249+ var first_pos = str.lastIndexOf('<');
250+ var second_pos = str.lastIndexOf('>');
251+ if ((first_pos != -1) && (second_pos != -1)) {
252+ return str.substring(first_pos+1, second_pos);
253+ } else {
254+ return str;
255+ }
256+}
be762003 257+
f4782eee
ER
258+function closeAndRefresh()
259+{
260+ opener.location.href = opener.location;
261+ window.close();
262+}
be762003 263+
f4782eee
ER
264+function str_replace(s, srch, rplc)
265+{
266+ var tmp = s;
267+ var tmp_before = new String();
268+ var tmp_after = new String();
269+ var tmp_output = new String();
270+ var int_before = 0;
271+ var int_after = 0;
be762003 272+
f4782eee
ER
273+ while (tmp.toUpperCase().indexOf(srch.toUpperCase()) > -1) {
274+ int_before = tmp.toUpperCase().indexOf(srch.toUpperCase());
275+ tmp_before = tmp.substring(0, int_before);
276+ tmp_output = tmp_output + tmp_before;
277+ tmp_output = tmp_output + rplc;
278+ int_after = tmp.length - srch.length + 1;
279+ tmp = tmp.substring(int_before + srch.length);
280+ }
be762003 281+
f4782eee
ER
282+ return tmp_output + tmp;
283+}
284+
285+function displayFixedWidth(element_name)
286+{
287+ var el = getPageElement(element_name);
288+ // only do this for mozilla
289+ if (is_nav6up) {
290+ var content = el.innerHTML;
291+ el.innerHTML = '<pre>' + str_replace(content, "<br>", '') + '</pre>';
292+ el.className = '';
293+ }
294+ el.style.fontFamily = 'Mono, Monaco, Courier New, Courier';
295+ el.style.whiteSpace = 'pre';
296+}
297+
298+function showSelections(form_name, field_name)
299+{
300+ var f = getForm(form_name);
301+ var field = getFormElement(f, field_name);
302+ var selections = getSelectedItems(field);
303+ var selected_names = new Array();
304+ for (var i = 0; i < selections.length; i++) {
305+ selected_names.push(selections[i].text);
306+ }
307+ var display_div = getPageElement('selection_' + field_name);
308+ display_div.innerHTML = 'Current Selections: ' + selected_names.join(', ');
309+}
310+
311+function replaceWords(str, original, replacement)
312+{
313+ var lines = str.split("\n");
314+ for (var i = 0; i < lines.length; i++) {
315+ lines[i] = replaceWordsOnLine(lines[i], original, replacement);
316+ }
317+ return lines.join("\n");
318+}
319+
320+function replaceWordsOnLine(str, original, replacement)
321+{
322+ var words = str.split(' ');
323+ for (var i = 0; i < words.length; i++) {
324+ words[i] = words[i].replace(/^\s*/, '').replace(/\s*$/, '');
325+ if (words[i] == original) {
326+ words[i] = replacement;
be762003 327+ }
be762003 328+ }
f4782eee
ER
329+ return words.join(' ');
330+}
be762003 331+
f4782eee
ER
332+function checkSpelling(form_name, field_name)
333+{
334+ var features = 'width=420,height=400,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
335+ var popupWin = window.open('spell_check.php?form_name=' + form_name + '&field_name=' + field_name, '_spellChecking', features);
336+ popupWin.focus();
337+}
338+
339+function updateTimeFields(form_name, year_field, month_field, day_field, hour_field, minute_field, date)
340+{
341+ var f = getForm(form_name);
342+ if (typeof date == 'undefined') {
343+ date = new Date();
344+ }
345+ selectOption(f, month_field, padDateValue(date.getMonth()+1));
346+ selectOption(f, day_field, padDateValue(date.getDate()));
347+ selectOption(f, year_field, date.getFullYear());
348+ selectOption(f, hour_field, padDateValue(date.getHours()));
349+ // minutes need special case due the 5 minute granularity
350+ var minutes = Math.floor(date.getMinutes() / 5) * 5;
351+ selectOption(f, minute_field, padDateValue(minutes));
352+}
353+
354+function padDateValue(str)
355+{
356+ if (str.length == 1) {
357+ str = '0' + str;
358+ }
359+ return str;
360+}
361+
362+function resizeTextarea(page_name, form_name, field_name, change)
363+{
364+ var f = getForm(form_name);
365+ var field = getFormElement(f, field_name);
366+ field.cols = field.cols + change;
367+ var cookie_name = 'textarea_' + page_name + '_' + field_name;
368+ setCookie(cookie_name, field.cols, expires);
369+}
370+
371+function removeOptionByValue(f, field_name, value)
372+{
373+ var field = getFormElement(f, field_name);
374+ for (var i = 0; i < field.options.length; i++) {
375+ if (field.options[i].value == value) {
376+ field.options[i] = null;
9fbd0c65 377+ }
f4782eee
ER
378+ }
379+}
380+
381+function getTotalCheckboxes(f, field_name)
382+{
383+ var total = 0;
384+ for (var i = 0; i < f.elements.length; i++) {
385+ if (f.elements[i].name == field_name) {
386+ total++;
9fbd0c65 387+ }
f4782eee
ER
388+ }
389+ return total;
390+}
391+
392+function getTotalCheckboxesChecked(f, field_name)
393+{
394+ var total = 0;
395+ for (var i = 0; i < f.elements.length; i++) {
396+ if ((f.elements[i].name == field_name) && (f.elements[i].checked)) {
397+ total++;
398+ }
399+ }
400+ return total;
401+}
402+
403+function hideComboBoxes(except_field)
404+{
405+ for (var i = 0; i < document.forms.length; i++) {
406+ for (var y = 0; y < document.forms[i].elements.length; y++) {
407+ if (((document.forms[i].elements[y].type == 'select-one') ||
408+ (document.forms[i].elements[y].type == 'select-multiple')) &&
409+ (document.forms[i].elements[y].name != except_field) &&
410+ (document.forms[i].elements[y].name != 'lookup') &&
411+ (document.forms[i].elements[y].name != 'lookup[]')) {
412+ document.forms[i].elements[y].style.visibility = 'hidden';
413+ }
414+ }
415+ }
416+}
417+
418+function showComboBoxes()
419+{
420+ for (var i = 0; i < document.forms.length; i++) {
421+ for (var y = 0; y < document.forms[i].elements.length; y++) {
422+ if (((document.forms[i].elements[y].type == 'select-one') ||
423+ (document.forms[i].elements[y].type == 'select-multiple')) &&
424+ (document.forms[i].elements[y].name != 'lookup') &&
425+ (document.forms[i].elements[y].name != 'lookup[]')) {
426+ document.forms[i].elements[y].style.visibility = 'visible';
427+ }
428+ }
429+ }
430+}
431+
432+function getOverlibContents(options, target_form, target_field, is_multiple)
433+{
434+ hideComboBoxes(target_field);
435+ var html = '<form name="overlib_form" onSubmit="javascript:return lookupOption(this, \'' + target_form + '\', \'' + target_field + '\');">' + options + '<br /><input name="search" class="lookup_field_overlib" type="text" size="24" value="paste or start typing here" onBlur="javascript:this.value=\'paste or start typing here\';" onFocus="javascript:this.value=\'\';" onKeyUp="javascript:lookupField(this.form, this, \'lookup';
436+ if ((is_multiple != null) && (is_multiple == true)) {
437+ html += '[]';
438+ }
439+ html += '\');"><input class="button_overlib" type="submit" value="Lookup"><br />'
440+ + '<input type="text" name="id_number" size="24" class="lookup_field_overlib" value="id #" onFocus="javascript:this.value=\'\';">'
441+ + '<input type="button" class="button_overlib" value="Add By ID" onClick="lookupByID(document.forms[\'overlib_form\'].id_number, \'' + target_form + '\', \'' + target_field + '\')"></form>';
442+ return html;
443+}
444+
445+function getFillInput(options, target_form, target_field)
446+{
447+ hideComboBoxes(target_field);
448+ return '<form onSubmit="javascript:return fillInput(this, \'' + target_form + '\', \'' + target_field + '\');">' + options + '<input class="button_overlib" type="submit" value="Lookup"><br><input name="search" class="lookup_field_overlib" type="text" size="24" value="paste or start typing here" onBlur="javascript:this.value=\'paste or start typing here\';" onFocus="javascript:this.value=\'\';" onKeyUp="javascript:lookupField(this.form, this, \'lookup\');"></form>';
449+}
450+
451+function lookupOption(f, target_form, target_field)
452+{
453+ var w = document;
454+ for (var i = 0; i < w.forms.length; i++) {
455+ if (w.forms[i].name == target_form) {
456+ var test = getFormElement(f, 'lookup');
457+ if (!test) {
458+ var field = getFormElement(f, 'lookup[]');
459+ var target = getFormElement(getForm(target_form), target_field);
460+ clearSelectedOptions(target);
461+ selectOptions(w.forms[i], target_field, getSelectedItems(field));
462+ } else {
463+ options = getSelectedOption(f, 'lookup');
464+ if (options == -1) {
465+ return false;
9fbd0c65 466+ }
f4782eee 467+ selectOption(w.forms[i], target_field, options);
9fbd0c65 468+ }
f4782eee
ER
469+ nd();
470+ showComboBoxes();
471+ break;
9fbd0c65
ER
472+ }
473+ }
f4782eee
ER
474+ return false;
475+}
9fbd0c65 476+
f4782eee
ER
477+function lookupByID(field, target_form, target_field)
478+{
479+ if (!isNumberOnly(field.value)) {
480+ alert('Please enter numbers only');
481+ } else {
482+ // try to find value in targer field.
483+ target_obj = document.forms[target_form].elements[target_field];
484+ found = false;
485+ for (i = 0;i<target_obj.options.length; i++) {
486+ if (target_obj.options[i].value == field.value) {
487+ found = true;
488+ target_obj.options[i].selected = true;
489+ }
9fbd0c65 490+ }
f4782eee
ER
491+ if (found == false) {
492+ alert('ID #' + field.value + ' was not found');
493+ } else {
494+ field.value = '';
495+ // check if we should call "showSelection"
496+ if (document.getElementById('selection_' + target_field) != null) {
497+ showSelections(target_form, target_field)
498+ }
499+ }
500+ }
501+ return false;
502+}
503+
504+function fillInput(f, target_form, target_field)
505+{
506+ var exists = getFormElement(f, 'lookup');
507+ var target_f = getForm(target_form);
508+ if (!exists) {
509+ var field = getFormElement(f, 'lookup[]');
510+ var target_field = getFormElement(target_f, target_field);
511+ target_field.value = '';
512+ var values = getValues(getSelectedItems(field));
513+ target_field.value = values.join('; ');
514+ errorDetails(target_f, target_field, false);
515+ } else {
516+ var field_value = getSelectedOption(f, 'lookup');
517+ var field = getFormElement(target_f, target_field);
518+ field.value = field_value;
519+ errorDetails(target_f, target_field, false);
520+ }
521+ nd();
522+ showComboBoxes();
523+ return false;
524+}
525+
526+function lookupField(f, search_field, field_name, callbacks)
527+{
528+ var search = search_field.value;
529+ if (isWhitespace(search)) {
530+ return false;
531+ }
532+ var target_field = getFormElement(f, field_name);
533+ if (!target_field) {
534+ target_field = getFormElement(f, field_name + '[]');
535+ }
536+ for (var i = 0; i < target_field.options.length; i++) {
537+ var value = target_field.options[i].text.toUpperCase();
538+ if (target_field.type == 'select-multiple') {
539+ // if we are targetting a multiple select box, then unselect everything
540+ // before selecting the matched option
541+ if (startsWith(value, search.toUpperCase())) {
542+ clearSelectedOptions(target_field);
543+ target_field.options[i].selected = true;
544+ // handle calling any callbacks
545+ if (callbacks != null) {
546+ for (var y = 0; y < callbacks.length; y++) {
547+ eval(callbacks[y] + ';');
548+ }
9fbd0c65 549+ }
f4782eee
ER
550+ return true;
551+ }
552+ } else {
553+ // normal drop-down boxes will search across the option value, and
554+ // not just the beginning of it (e.g. '*hello*' instead of 'hello*')
555+ if (value.indexOf(search.toUpperCase()) != -1) {
556+ target_field.options[i].selected = true;
557+ // handle calling any callbacks
558+ if (callbacks != null) {
559+ for (var y = 0; y < callbacks.length; y++) {
560+ eval(callbacks[y] + ';');
561+ }
9fbd0c65 562+ }
f4782eee 563+ return true;
9fbd0c65
ER
564+ }
565+ }
9fbd0c65 566+ }
f4782eee
ER
567+ target_field.selectedIndex = 0;
568+}
9fbd0c65 569+
f4782eee
ER
570+function clearSelectedOptions(field)
571+{
572+ for (var i = 0; i < field.options.length; i++) {
573+ field.options[i].selected = false;
574+ }
575+}
576+
577+function selectAllOptions(f, field_name)
578+{
579+ var field = getFormElement(f, field_name);
580+ for (var y = 0; y < field.options.length; y++) {
581+ field.options[y].selected = true;
582+ }
583+}
584+
585+function selectOptions(f, field_name, values)
586+{
587+ var field = getFormElement(f, field_name);
588+ for (var i = 0; i < values.length; i++) {
589+ for (var y = 0; y < field.options.length; y++) {
590+ if (field.options[y].value == values[i].value) {
591+ field.options[y].selected = true;
592+ }
9fbd0c65 593+ }
f4782eee
ER
594+ }
595+}
9fbd0c65 596+
f4782eee
ER
597+function selectOption(f, field_name, value)
598+{
599+ field = getFormElement(f, field_name);
600+ for (var i = 0; i < field.options.length; i++) {
601+ if (field.options[i].value == value) {
602+ field.options[i].selected = true;
603+ return true;
9fbd0c65 604+ }
f4782eee
ER
605+ }
606+}
607+
608+function setHiddenFieldValue(f, field_name, value)
609+{
610+ var field = getFormElement(f, field_name);
611+ field.value = value;
612+}
613+
614+function getForm(form_name)
615+{
616+ for (var i = 0; i < document.forms.length; i++) {
617+ if (document.forms[i].name == form_name) {
618+ return document.forms[i];
9fbd0c65 619+ }
f4782eee
ER
620+ }
621+}
622+
623+function getPageElement(id)
624+{
625+ if (document.getElementById) {
626+ return document.getElementById(id);
627+ } else if (document.all) {
628+ return document.all[id];
629+ }
630+}
631+
632+function getOpenerPageElement(name)
633+{
634+ if (window.opener.document.getElementById) {
635+ return window.opener.document.getElementById(name);
636+ } else if (window.opener.document.all) {
637+ return window.opener.document.all[name];
638+ }
639+}
640+
641+function getFormElement(f, field_name, num)
642+{
643+ var elements = document.getElementsByName(field_name);
644+ var y = 0;
645+ for (var i = 0; i < elements.length; i++) {
646+ if (f != elements[i].form) {
647+ continue;
648+ }
649+ if (num != null) {
650+ if (y == num) {
651+ return elements[i];
652+ }
653+ y++;
9fbd0c65 654+ } else {
f4782eee 655+ return elements[i];
9fbd0c65 656+ }
f4782eee
ER
657+ }
658+ return false;
659+}
660+
661+function getSelectedItems(field)
662+{
663+ var selected = new Array();
664+ for (var i = 0; i < field.options.length; i++) {
665+ if (field.options[i].selected) {
666+ selected[selected.length] = field.options[i];
9fbd0c65 667+ }
f4782eee
ER
668+ }
669+ return selected;
670+}
671+
672+function getSelectedOptionValues(f, field_name)
673+{
674+ var field = getFormElement(f, field_name);
675+ var selected = new Array();
676+ for (var i = 0; i < field.options.length; i++) {
677+ if (field.options[i].selected) {
678+ selected[selected.length] = field.options[i].value;
9fbd0c65
ER
679+ }
680+ }
f4782eee
ER
681+ return selected;
682+}
9fbd0c65 683+
f4782eee
ER
684+function removeAllOptions(f, field_name)
685+{
686+ var field = getFormElement(f, field_name);
687+ if (field.options.length > 0) {
688+ field.options[0] = null;
689+ removeAllOptions(f, field_name);
690+ }
691+}
be762003 692+
f4782eee
ER
693+function getValues(list)
694+{
695+ var values = new Array();
696+ for (var i = 0; i < list.length; i++) {
697+ values[values.length] = list[i].value;
698+ }
699+ return values;
700+}
be762003 701+
f4782eee
ER
702+function optionExists(field, option)
703+{
704+ for (var i = 0; i < field.options.length; i++) {
705+ if (field.options[i].text == option.text) {
706+ return true;
be762003 707+ }
f4782eee
ER
708+ }
709+ return false;
710+}
be762003 711+
f4782eee
ER
712+function addOptions(f, field_name, options)
713+{
714+ var field = getFormElement(f, field_name);
715+ for (var i = 0; i < options.length; i++) {
716+ if (!optionExists(field, options[i])) {
717+ field.options.length = field.options.length + 1;
718+ field.options[field.options.length-1].text = options[i].text;
719+ field.options[field.options.length-1].value = options[i].value;
720+ }
721+ }
722+}
be762003 723+
f4782eee
ER
724+function replaceParam(str, param, new_value)
725+{
726+ if (str.indexOf("?") == -1) {
727+ return param + "=" + new_value;
728+ } else {
729+ var pieces = str.split("?");
730+ var params = pieces[1].split("&");
731+ var new_params = new Array();
732+ for (var i = 0; i < params.length; i++) {
733+ if (params[i].indexOf(param + "=") == 0) {
734+ params[i] = param + "=" + new_value;
735+ }
736+ new_params[i] = params[i];
737+ }
738+ // check if the parameter doesn't exist on the URL
739+ if ((str.indexOf("?" + param + "=") == -1) && (str.indexOf("&" + param + "=") == -1)) {
740+ new_params[new_params.length] = param + "=" + new_value;
741+ }
742+ return new_params.join("&");
743+ }
744+}
be762003 745+
f4782eee
ER
746+function checkRadio(form_name, field_name, num)
747+{
748+ var f = getForm(form_name);
749+ var field = getFormElement(f, field_name, num);
750+ if (!field.disabled) {
751+ field.checked = true;
752+ }
753+}
be762003 754+
f4782eee
ER
755+function toggleCheckbox(form_name, field_name, num)
756+{
757+ var f = getForm(form_name);
758+ var checkbox = getFormElement(f, field_name, num);
759+ if (checkbox.disabled) {
760+ return false;
761+ }
762+ if (checkbox.checked) {
763+ checkbox.checked = false;
764+ } else {
765+ checkbox.checked = true;
766+ }
767+}
768+
769+var toggle = 'off';
770+function toggleSelectAll(f, field_name)
771+{
772+ for (var i = 0; i < f.elements.length; i++) {
773+ if (f.elements[i].disabled) {
774+ continue;
775+ }
776+ if (f.elements[i].name == field_name) {
777+ if (toggle == 'off') {
778+ f.elements[i].checked = true;
779+ } else {
780+ f.elements[i].checked = false;
be762003
ER
781+ }
782+ }
be762003 783+ }
f4782eee
ER
784+ if (toggle == 'off') {
785+ toggle = 'on';
786+ } else {
787+ toggle = 'off';
788+ }
789+}
be762003 790+
f4782eee
ER
791+function getCookies()
792+{
793+ var t = new Array();
794+ var pieces = new Array();
795+ var cookies = new Object();
796+ if (document.cookie) {
797+ t = document.cookie.split(';');
798+ for (var i = 0; i < t.length; i++) {
799+ pieces = t[i].split('=');
800+ eval('cookies.' + pieces[0].replace('[', '_').replace(']', '_') + ' = "' + pieces[1] + '";');
9fbd0c65 801+ }
f4782eee 802+ return cookies;
9fbd0c65 803+ }
f4782eee 804+}
be762003 805+
f4782eee
ER
806+function isElementVisible(element)
807+{
808+ if ((!element.style.display) || (element.style.display == getDisplayStyle())) {
809+ return true;
810+ } else {
be762003 811+ return false;
be762003 812+ }
f4782eee 813+}
bf790e39 814+
f4782eee
ER
815+function toggleVisibility(title, create_cookie, use_inline)
816+{
817+ var element = getPageElement(title + '1');
818+ if (isElementVisible(element)) {
819+ var new_style = 'none';
820+ } else {
821+ var new_style = getDisplayStyle(use_inline);
822+ }
823+ var i = 1;
824+ while (1) {
825+ element = getPageElement(title + i);
826+ if (!element) {
827+ break;
828+ }
829+ element.style.display = new_style;
830+ i++;
831+ }
832+ // if any elements were found, then...
833+ if (i > 1) {
834+ var link_element = getPageElement(title + '_link');
835+ if (link_element) {
836+ if (new_style == 'none') {
837+ link_element.innerHTML = 'show';
838+ link_element.title = 'show details about this section';
839+ } else {
840+ link_element.innerHTML = 'hide';
841+ link_element.title = 'hide details about this section';
842+ }
843+ }
844+ }
845+ if (((create_cookie == null) || (create_cookie == false)) && (create_cookie != undefined)) {
846+ return false;
847+ } else {
848+ setCookie('visibility_' + title, new_style, expires);
849+ }
850+}
9fbd0c65 851+
f4782eee
ER
852+function changeVisibility(title, visibility, use_inline)
853+{
854+ var element = getPageElement(title);
855+ if (visibility) {
856+ var new_style = getDisplayStyle(use_inline);
9fbd0c65 857+ } else {
f4782eee 858+ var new_style = 'none';
9fbd0c65 859+ }
f4782eee 860+ element.style.display = new_style;
9fbd0c65 861+}
9fbd0c65 862+
f4782eee 863+function getDisplayStyle(use_inline)
9fbd0c65 864+{
f4782eee
ER
865+ // kind of hackish, but it works perfectly with IE6 and Mozilla 1.1
866+ if (is_ie5up) {
867+ if (use_inline == true) {
868+ return 'inline';
869+ } else {
870+ return 'block';
871+ }
872+ } else {
873+ return '';
874+ }
875+}
d95f9b26 876+
f4782eee
ER
877+function getCookie(name)
878+{
879+ var start = document.cookie.indexOf(name+"=");
880+ var len = start+name.length+1;
881+ if ((!start) && (name != document.cookie.substring(0,name.length))) return null;
882+ if (start == -1) return null;
883+ var end = document.cookie.indexOf(";",len);
884+ if (end == -1) end = document.cookie.length;
885+ return unescape(document.cookie.substring(len,end));
886+}
887+
888+function setCookie(name, value, expires, path, domain, secure)
889+{
890+ document.cookie = name + "=" +escape(value) +
891+ ( (expires) ? ";expires=" + expires.toGMTString() : "") +
892+ ( (path) ? ";path=" + path : "") +
893+ ( (domain) ? ";domain=" + domain : "") +
894+ ( (secure) ? ";secure" : "");
895+}
896+
897+function openHelp(rel_url, topic)
898+{
899+ var width = 500;
900+ var height = 450;
901+ var w_offset = 30;
902+ var h_offset = 30;
903+ var location = 'top=' + h_offset + ',left=' + w_offset + ',';
904+ if (screen.width) {
905+ location = 'top=' + h_offset + ',left=' + (screen.width - (width + w_offset)) + ',';
9fbd0c65 906+ }
f4782eee
ER
907+ var features = 'width=' + width + ',height=' + height + ',' + location + 'resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
908+ var helpWin = window.open(rel_url + 'help.php?topic=' + topic, '_help', features);
909+ helpWin.focus();
910+}
911+
912+function selectOnlyValidOption(selectObj)
913+{
914+ if (selectObj.selectedIndex < 1) {
915+ if (selectObj.length == 1) {
916+ selectObj.selectedIndex = 0;
917+ return;
918+ }
919+ if (selectObj.length <= 2 && selectObj.options[0].value == -1) {
920+ selectObj.selectedIndex = 1;
921+ return;
922+ }
9fbd0c65 923+ }
f4782eee 924+}
9fbd0c65 925+
f4782eee
ER
926+// this method will confirm that you want the window to close
927+var checkClose = false;
928+var closeConfirmMessage = 'Do you want to close this window?';
929+function handleClose()
930+{
931+ if (checkClose == true) {
932+ return closeConfirmMessage;
933+ } else {
934+ return;
935+ }
9fbd0c65 936+}
f4782eee
ER
937+
938+function checkWindowClose(msg)
be762003 939+{
f4782eee
ER
940+ if (msg == false) {
941+ checkClose = false;
942+ } else {
943+ checkClose = true;
944+ closeConfirmMessage = msg;
945+ }
be762003 946+}
f4782eee
ER
947+
948+// Replace special characters MS uses for quotes with normal versions
949+function replaceSpecialCharacters(e)
950+{
951+ var s = new String(e.value);
952+ var newString = '';
953+ var thisChar;
954+ var charCode;
955+ for (i = 0; i < s.length; i++) {
956+ thisChar = s.charAt(i);
957+ charCode = s.charCodeAt(i);
958+ if ((charCode == 8220) || (charCode == 8221)) {
959+ thisChar = '"';
960+ } else if (charCode == 8217) {
961+ thisChar = "'";
962+ } else if (charCode == 8230) {
963+ thisChar = "...";
964+ } else if (charCode == 8226) {
965+ thisChar = "*";
966+ } else if (charCode == 8211) {
967+ thisChar = "-";
968+ }
969+ newString = newString + thisChar;
be762003 970+ }
f4782eee
ER
971+ e.value = newString;
972+}
973+
974+
975+function getEventTarget(e)
976+{
977+ var targ;
978+ if (!e) var e = window.event;
979+ if (e.target) targ = e.target;
980+ else if (e.srcElement) targ = e.srcElement;
981+ if (targ.nodeType == 3) // defeat Safari bug
982+ targ = targ.parentNode;
983+
984+ return targ;
985+}
986+
987+// call when document ready
988+$(document).ready(function() {
989+ $('.date_picker').datepicker({
990+ dateFormat: 'yy-mm-dd',
991+ firstDay: user_prefs.week_firstday
992+ });
be762003 993+});
f4782eee
ER
994+//-->
995--- eventum-2.2/htdocs/js/jquery/jquery.tablednd.js 1970-01-01 02:00:00.000000000 +0200
996+++ eventum-2.2-order/htdocs/js/jquery/jquery.tablednd.js 2009-10-12 22:10:36.435851675 +0300
621b0b5e
ER
997@@ -0,0 +1,382 @@
998+/**
999+ * TableDnD plug-in for JQuery, allows you to drag and drop table rows
1000+ * You can set up various options to control how the system will work
1001+ * Copyright (c) Denis Howlett <denish@isocra.com>
1002+ * Licensed like jQuery, see http://docs.jquery.com/License.
1003+ *
1004+ * Configuration options:
1005+ *
1006+ * onDragStyle
1007+ * This is the style that is assigned to the row during drag. There are limitations to the styles that can be
1008+ * associated with a row (such as you can't assign a border--well you can, but it won't be
1009+ * displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
1010+ * a map (as used in the jQuery css(...) function).
1011+ * onDropStyle
1012+ * This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
1013+ * to what you can do. Also this replaces the original style, so again consider using onDragClass which
1014+ * is simply added and then removed on drop.
1015+ * onDragClass
1016+ * This class is added for the duration of the drag and then removed when the row is dropped. It is more
1017+ * flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
1018+ * is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
1019+ * stylesheet.
1020+ * onDrop
1021+ * Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
1022+ * and the row that was dropped. You can work out the new order of the rows by using
1023+ * table.rows.
1024+ * onDragStart
1025+ * Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
1026+ * table and the row which the user has started to drag.
1027+ * onAllowDrop
1028+ * Pass a function that will be called as a row is over another row. If the function returns true, allow
1029+ * dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
1030+ * the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
1031+ * scrollAmount
1032+ * This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
1033+ * window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
1034+ * FF3 beta
1035+ * dragHandle
1036+ * This is the name of a class that you assign to one or more cells in each row that is draggable. If you
1037+ * specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
1038+ * will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
1039+ * the whole row is draggable.
1040+ *
1041+ * Other ways to control behaviour:
1042+ *
1043+ * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
1044+ * that you don't want to be draggable.
1045+ *
1046+ * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
1047+ * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
1048+ * an ID as must all the rows.
1049+ *
1050+ * Other methods:
1051+ *
1052+ * $("...").tableDnDUpdate()
1053+ * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
1054+ * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
1055+ * The table maintains the original configuration (so you don't have to specify it again).
1056+ *
1057+ * $("...").tableDnDSerialize()
1058+ * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
1059+ * called from anywhere and isn't dependent on the currentTable being set up correctly before calling
1060+ *
1061+ * Known problems:
1062+ * - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
1063+ *
1064+ * Version 0.2: 2008-02-20 First public version
1065+ * Version 0.3: 2008-02-07 Added onDragStart option
1066+ * Made the scroll amount configurable (default is 5 as before)
1067+ * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
1068+ * Added onAllowDrop to control dropping
1069+ * Fixed a bug which meant that you couldn't set the scroll amount in both directions
1070+ * Added serialize method
1071+ * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
1072+ * draggable
1073+ * Improved the serialize method to use a default (and settable) regular expression.
1074+ * Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
1075+ */
1076+jQuery.tableDnD = {
1077+ /** Keep hold of the current table being dragged */
1078+ currentTable : null,
1079+ /** Keep hold of the current drag object if any */
1080+ dragObject: null,
1081+ /** The current mouse offset */
1082+ mouseOffset: null,
1083+ /** Remember the old value of Y so that we don't do too much processing */
1084+ oldY: 0,
1085+
1086+ /** Actually build the structure */
1087+ build: function(options) {
1088+ // Set up the defaults if any
1089+
1090+ this.each(function() {
1091+ // This is bound to each matching table, set up the defaults and override with user options
1092+ this.tableDnDConfig = jQuery.extend({
1093+ onDragStyle: null,
1094+ onDropStyle: null,
1095+ // Add in the default class for whileDragging
1096+ onDragClass: "tDnD_whileDrag",
1097+ onDrop: null,
1098+ onDragStart: null,
1099+ scrollAmount: 5,
1100+ serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
1101+ serializeParamName: null, // If you want to specify another parameter name instead of the table ID
1102+ dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
1103+ }, options || {});
1104+ // Now make the rows draggable
1105+ jQuery.tableDnD.makeDraggable(this);
1106+ });
1107+
1108+ // Now we need to capture the mouse up and mouse move event
1109+ // We can use bind so that we don't interfere with other event handlers
1110+ jQuery(document)
1111+ .bind('mousemove', jQuery.tableDnD.mousemove)
1112+ .bind('mouseup', jQuery.tableDnD.mouseup);
1113+
1114+ // Don't break the chain
1115+ return this;
1116+ },
1117+
1118+ /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
1119+ makeDraggable: function(table) {
1120+ var config = table.tableDnDConfig;
1121+ if (table.tableDnDConfig.dragHandle) {
1122+ // We only need to add the event to the specified cells
1123+ var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
1124+ cells.each(function() {
1125+ // The cell is bound to "this"
1126+ jQuery(this).mousedown(function(ev) {
1127+ jQuery.tableDnD.dragObject = this.parentNode;
1128+ jQuery.tableDnD.currentTable = table;
1129+ jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
1130+ if (config.onDragStart) {
1131+ // Call the onDrop method if there is one
1132+ config.onDragStart(table, this);
1133+ }
1134+ return false;
1135+ });
1136+ })
1137+ } else {
1138+ // For backwards compatibility, we add the event to the whole row
1139+ var rows = jQuery("tr", table); // get all the rows as a wrapped set
1140+ rows.each(function() {
1141+ // Iterate through each row, the row is bound to "this"
1142+ var row = jQuery(this);
1143+ if (! row.hasClass("nodrag")) {
1144+ row.mousedown(function(ev) {
1145+ if (ev.target.tagName == "TD") {
1146+ jQuery.tableDnD.dragObject = this;
1147+ jQuery.tableDnD.currentTable = table;
1148+ jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
1149+ if (config.onDragStart) {
1150+ // Call the onDrop method if there is one
1151+ config.onDragStart(table, this);
1152+ }
1153+ return false;
1154+ }
1155+ }).css("cursor", "move"); // Store the tableDnD object
1156+ }
1157+ });
1158+ }
1159+ },
1160+
1161+ updateTables: function() {
1162+ this.each(function() {
1163+ // this is now bound to each matching table
1164+ if (this.tableDnDConfig) {
1165+ jQuery.tableDnD.makeDraggable(this);
1166+ }
1167+ })
1168+ },
1169+
1170+ /** Get the mouse coordinates from the event (allowing for browser differences) */
1171+ mouseCoords: function(ev){
1172+ if(ev.pageX || ev.pageY){
1173+ return {x:ev.pageX, y:ev.pageY};
1174+ }
1175+ return {
1176+ x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
1177+ y:ev.clientY + document.body.scrollTop - document.body.clientTop
1178+ };
1179+ },
1180+
1181+ /** Given a target element and a mouse event, get the mouse offset from that element.
1182+ To do this we need the element's position and the mouse position */
1183+ getMouseOffset: function(target, ev) {
1184+ ev = ev || window.event;
1185+
1186+ var docPos = this.getPosition(target);
1187+ var mousePos = this.mouseCoords(ev);
1188+ return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
1189+ },
1190+
1191+ /** Get the position of an element by going up the DOM tree and adding up all the offsets */
1192+ getPosition: function(e){
1193+ var left = 0;
1194+ var top = 0;
1195+ /** Safari fix -- thanks to Luis Chato for this! */
1196+ if (e.offsetHeight == 0) {
1197+ /** Safari 2 doesn't correctly grab the offsetTop of a table row
1198+ this is detailed here:
1199+ http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
1200+ the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
1201+ note that firefox will return a text node as a first child, so designing a more thorough
1202+ solution may need to take that into account, for now this seems to work in firefox, safari, ie */
1203+ e = e.firstChild; // a table cell
1204+ }
1205+
1206+ while (e.offsetParent){
1207+ left += e.offsetLeft;
1208+ top += e.offsetTop;
1209+ e = e.offsetParent;
1210+ }
1211+
1212+ left += e.offsetLeft;
1213+ top += e.offsetTop;
1214+
1215+ return {x:left, y:top};
1216+ },
1217+
1218+ mousemove: function(ev) {
1219+ if (jQuery.tableDnD.dragObject == null) {
1220+ return;
1221+ }
1222+
1223+ var dragObj = jQuery(jQuery.tableDnD.dragObject);
1224+ var config = jQuery.tableDnD.currentTable.tableDnDConfig;
1225+ var mousePos = jQuery.tableDnD.mouseCoords(ev);
1226+ var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
1227+ //auto scroll the window
1228+ var yOffset = window.pageYOffset;
1229+ if (document.all) {
1230+ // Windows version
1231+ //yOffset=document.body.scrollTop;
1232+ if (typeof document.compatMode != 'undefined' &&
1233+ document.compatMode != 'BackCompat') {
1234+ yOffset = document.documentElement.scrollTop;
1235+ }
1236+ else if (typeof document.body != 'undefined') {
1237+ yOffset=document.body.scrollTop;
1238+ }
1239+
1240+ }
1241+
1242+ if (mousePos.y-yOffset < config.scrollAmount) {
1243+ window.scrollBy(0, -config.scrollAmount);
1244+ } else {
1245+ var windowHeight = window.innerHeight ? window.innerHeight
1246+ : document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
1247+ if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
1248+ window.scrollBy(0, config.scrollAmount);
1249+ }
1250+ }
1251+
1252+
1253+ if (y != jQuery.tableDnD.oldY) {
1254+ // work out if we're going up or down...
1255+ var movingDown = y > jQuery.tableDnD.oldY;
1256+ // update the old value
1257+ jQuery.tableDnD.oldY = y;
1258+ // update the style to show we're dragging
1259+ if (config.onDragClass) {
1260+ dragObj.addClass(config.onDragClass);
1261+ } else {
1262+ dragObj.css(config.onDragStyle);
1263+ }
1264+ // If we're over a row then move the dragged row to there so that the user sees the
1265+ // effect dynamically
1266+ var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
1267+ if (currentRow) {
1268+ // TODO worry about what happens when there are multiple TBODIES
1269+ if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
1270+ jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
1271+ } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
1272+ jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
1273+ }
1274+ }
1275+ }
1276+
1277+ return false;
1278+ },
1279+
1280+ /** We're only worried about the y position really, because we can only move rows up and down */
1281+ findDropTargetRow: function(draggedRow, y) {
1282+ var rows = jQuery.tableDnD.currentTable.rows;
1283+ for (var i=0; i<rows.length; i++) {
1284+ var row = rows[i];
1285+ var rowY = this.getPosition(row).y;
1286+ var rowHeight = parseInt(row.offsetHeight)/2;
1287+ if (row.offsetHeight == 0) {
1288+ rowY = this.getPosition(row.firstChild).y;
1289+ rowHeight = parseInt(row.firstChild.offsetHeight)/2;
1290+ }
1291+ // Because we always have to insert before, we need to offset the height a bit
1292+ if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
1293+ // that's the row we're over
1294+ // If it's the same as the current row, ignore it
1295+ if (row == draggedRow) {return null;}
1296+ var config = jQuery.tableDnD.currentTable.tableDnDConfig;
1297+ if (config.onAllowDrop) {
1298+ if (config.onAllowDrop(draggedRow, row)) {
1299+ return row;
1300+ } else {
1301+ return null;
1302+ }
1303+ } else {
1304+ // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
1305+ var nodrop = jQuery(row).hasClass("nodrop");
1306+ if (! nodrop) {
1307+ return row;
1308+ } else {
1309+ return null;
1310+ }
1311+ }
1312+ return row;
1313+ }
1314+ }
1315+ return null;
1316+ },
1317+
1318+ mouseup: function(e) {
1319+ if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
1320+ var droppedRow = jQuery.tableDnD.dragObject;
1321+ var config = jQuery.tableDnD.currentTable.tableDnDConfig;
1322+ // If we have a dragObject, then we need to release it,
1323+ // The row will already have been moved to the right place so we just reset stuff
1324+ if (config.onDragClass) {
1325+ jQuery(droppedRow).removeClass(config.onDragClass);
1326+ } else {
1327+ jQuery(droppedRow).css(config.onDropStyle);
1328+ }
1329+ jQuery.tableDnD.dragObject = null;
1330+ if (config.onDrop) {
1331+ // Call the onDrop method if there is one
1332+ config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
1333+ }
1334+ jQuery.tableDnD.currentTable = null; // let go of the table too
1335+ }
1336+ },
1337+
1338+ serialize: function() {
1339+ if (jQuery.tableDnD.currentTable) {
1340+ return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
1341+ } else {
1342+ return "Error: No Table id set, you need to set an id on your table and every row";
1343+ }
1344+ },
1345+
1346+ serializeTable: function(table) {
1347+ var result = "";
1348+ var tableId = table.id;
1349+ var rows = table.rows;
1350+ for (var i=0; i<rows.length; i++) {
1351+ if (result.length > 0) result += "&";
1352+ var rowId = rows[i].id;
1353+ if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
1354+ rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
1355+ }
1356+
1357+ result += tableId + '[]=' + rowId;
1358+ }
1359+ return result;
1360+ },
1361+
f4782eee
ER
1362+ serializeTables: function() {
1363+ var result = "";
1364+ this.each(function() {
1365+ // this is now bound to each matching table
1366+ result += jQuery.tableDnD.serializeTable(this);
1367+ });
1368+ return result;
1369+ }
1370+
1371+}
1372+
1373+jQuery.fn.extend(
1374+ {
1375+ tableDnD : jQuery.tableDnD.build,
1376+ tableDnDUpdate : jQuery.tableDnD.updateTables,
1377+ tableDnDSerialize: jQuery.tableDnD.serializeTables
1378+ }
1379+);
1380\ No newline at end of file
1381--- eventum-2.2/htdocs/list.php 2009-09-14 18:07:55.000000000 +0300
1382+++ eventum-2.2-order/htdocs/list.php 2009-10-12 22:10:36.435851675 +0300
1383@@ -53,6 +53,11 @@
1384 $profile['sort_by'] . "&sort_order=" . $profile['sort_order']);
1385 }
1386
1387+@$reorder_usr_id = $_REQUEST["reorder_user"];
1388+@$reorder_issue_id = $_REQUEST["reorder_source"];
1389+@$reorder_neworder = $_REQUEST["reorder_neworder"];
1390+Issue::reorderUserIssues($reorder_usr_id, $reorder_issue_id, $reorder_neworder);
1391+
1392 $options = Issue::saveSearchParams();
1393 $tpl->assign("options", $options);
1394 $tpl->assign("sorting", Issue::getSortingInfo($options));
1395@@ -78,6 +83,21 @@
1396 }
1397 $assign_options += $users;
1398
1399+// get the isu_order (assignated users) ordering user
1400+if (!empty($options["users"])) {
1401+ if ($options["users"] == -2) {
1402+ $isu_order_user = $usr_id;
1403+ } else
1404+ if ($options["users"] > 0) {
1405+ $isu_order_user = $options["users"];
1406+ } else {
1407+ unset($isu_order_user);
1408+ }
1409+} else {
1410+ unset($isu_order_user);
1411+}
1412+$tpl->assign("isu_order_user", $isu_order_user);
1413+
1414 $list = Issue::getListing($prj_id, $options, $pagerRow, $rows);
1415 $tpl->assign("list", $list["list"]);
1416 $tpl->assign("list_info", $list["info"]);
1417--- eventum-2.2/htdocs/list.php.~1~ 1970-01-01 02:00:00.000000000 +0200
1418+++ eventum-2.2-order/htdocs/list.php.~1~ 2009-09-14 18:07:55.000000000 +0300
1419@@ -0,0 +1,116 @@
1420+<?php
1421+/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
1422+// +----------------------------------------------------------------------+
1423+// | Eventum - Issue Tracking System |
1424+// +----------------------------------------------------------------------+
1425+// | Copyright (c) 2003 - 2008 MySQL AB |
1426+// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
1427+// | |
1428+// | This program is free software; you can redistribute it and/or modify |
1429+// | it under the terms of the GNU General Public License as published by |
1430+// | the Free Software Foundation; either version 2 of the License, or |
1431+// | (at your option) any later version. |
1432+// | |
1433+// | This program is distributed in the hope that it will be useful, |
1434+// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
1435+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1436+// | GNU General Public License for more details. |
1437+// | |
1438+// | You should have received a copy of the GNU General Public License |
1439+// | along with this program; if not, write to: |
1440+// | |
1441+// | Free Software Foundation, Inc. |
1442+// | 59 Temple Place - Suite 330 |
1443+// | Boston, MA 02111-1307, USA. |
1444+// +----------------------------------------------------------------------+
1445+// | Authors: João Prado Maia <jpm@mysql.com> |
1446+// +----------------------------------------------------------------------+
1447+//
1448+// @(#) $Id$
1449+
1450+require_once dirname(__FILE__) . '/../init.php';
1451+
1452+$tpl = new Template_Helper();
1453+$tpl->setTemplate("list.tpl.html");
1454+
1455+Auth::checkAuthentication(APP_COOKIE);
1456+$usr_id = Auth::getUserID();
1457+$prj_id = Auth::getCurrentProject();
1458+
1459+$pagerRow = Issue::getParam('pagerRow');
1460+if (empty($pagerRow)) {
1461+ $pagerRow = 0;
1462+}
1463+$rows = Issue::getParam('rows');
1464+if (empty($rows)) {
1465+ $rows = APP_DEFAULT_PAGER_SIZE;
1466+}
1467+
1468+if (@$_REQUEST['view'] == 'my_assignments') {
1469+ $profile = Search_Profile::getProfile($usr_id, $prj_id, 'issue');
1470+ Search_Profile::remove($usr_id, $prj_id, 'issue');
1471+ Auth::redirect("list.php?users=$usr_id&hide_closed=1&rows=$rows&sort_by=" .
1472+ $profile['sort_by'] . "&sort_order=" . $profile['sort_order']);
1473+}
1474+
1475+$options = Issue::saveSearchParams();
1476+$tpl->assign("options", $options);
1477+$tpl->assign("sorting", Issue::getSortingInfo($options));
1478+
1479+// generate options for assign list. If there are groups and user is above a customer, include groups
1480+$groups = Group::getAssocList($prj_id);
1481+$users = Project::getUserAssocList($prj_id, 'active', User::getRoleID('Customer'));
1482+$assign_options = array(
1483+ "" => ev_gettext("Any"),
1484+ "-1" => ev_gettext("un-assigned"),
1485+ "-2" => ev_gettext("myself and un-assigned")
1486+);
1487+if (Auth::isAnonUser())
1488+ unset($assign_options["-2"]);
1489+else if (User::getGroupID($usr_id) != '') {
1490+ $assign_options['-3'] = ev_gettext('myself and my group');
1491+ $assign_options['-4'] = ev_gettext('myself, un-assigned and my group');
1492+}
1493+if ((count($groups) > 0) && (Auth::getCurrentRole() > User::getRoleID("Customer"))) {
1494+ foreach ($groups as $grp_id => $grp_name) {
1495+ $assign_options["grp:$grp_id"] = ev_gettext("Group") . ": " . $grp_name;
1496+ }
1497+}
1498+$assign_options += $users;
1499+
1500+$list = Issue::getListing($prj_id, $options, $pagerRow, $rows);
1501+$tpl->assign("list", $list["list"]);
1502+$tpl->assign("list_info", $list["info"]);
1503+$tpl->assign("csv_data", base64_encode(@$list["csv"]));
1504+
1505+$tpl->assign("columns", Display_Column::getColumnsToDisplay($prj_id, 'list_issues'));
1506+$tpl->assign("priorities", Priority::getAssocList($prj_id));
1507+$tpl->assign("status", Status::getAssocStatusList($prj_id));
1508+$tpl->assign("assign_options", $assign_options);
1509+$tpl->assign("custom", Filter::getAssocList($prj_id));
1510+$tpl->assign("csts", Filter::getListing(true));
1511+$tpl->assign("active_filters", Filter::getActiveFilters($options));
1512+$tpl->assign("categories", Category::getAssocList($prj_id));
1513+$tpl->assign("releases", Release::getAssocList($prj_id, true));
1514+$tpl->assign("reporters", Project::getReporters($prj_id));
1515+
1516+$prefs = Prefs::get($usr_id);
1517+$tpl->assign("refresh_rate", $prefs['list_refresh_rate'] * 60);
1518+$tpl->assign("refresh_page", "list.php");
1519+
1520+// items needed for bulk update tool
1521+if (Auth::getCurrentRole() > User::getRoleID("Developer")) {
1522+ $tpl->assign("users", $users);
1523+
1524+ if (Workflow::hasWorkflowIntegration($prj_id)) {
1525+ $open_statuses = Workflow::getAllowedStatuses($prj_id);
1526+ } else {
1527+ $open_statuses = Status::getAssocStatusList($prj_id, false);
1528+ }
1529+
1530+ $tpl->assign("open_status", $open_statuses);
1531+ $tpl->assign("closed_status", Status::getClosedAssocList($prj_id));
1532+ $tpl->assign("available_releases", Release::getAssocList($prj_id));
1533+}
1534+
1535+$tpl->displayTemplate();
1536--- eventum-2.2/lib/eventum/class.display_column.php 2009-09-14 18:07:55.000000000 +0300
1537+++ eventum-2.2-order/lib/eventum/class.display_column.php 2009-10-12 22:10:36.429185594 +0300
1538@@ -230,7 +230,10 @@
1539 ),
1540 "iss_expected_resolution_date" => array(
1541 "title" => ev_gettext("Expected Resolution Date")
1542- )
1543+ ),
1544+ "isu_order" => array(
1545+ "title" => ev_gettext("Order")
1546+ ),
1547 )
1548 );
1549 return $columns[$page];
1550--- eventum-2.2/lib/eventum/class.display_column.php.~1~ 1970-01-01 02:00:00.000000000 +0200
1551+++ eventum-2.2-order/lib/eventum/class.display_column.php.~1~ 2009-09-14 18:07:55.000000000 +0300
1552@@ -0,0 +1,318 @@
1553+<?php
1554+/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
1555+// +----------------------------------------------------------------------+
1556+// | Eventum - Issue Tracking System |
1557+// +----------------------------------------------------------------------+
1558+// | Copyright (c) 2003 - 2008 MySQL AB |
1559+// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
1560+// | |
1561+// | This program is free software; you can redistribute it and/or modify |
1562+// | it under the terms of the GNU General Public License as published by |
1563+// | the Free Software Foundation; either version 2 of the License, or |
1564+// | (at your option) any later version. |
1565+// | |
1566+// | This program is distributed in the hope that it will be useful, |
1567+// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
1568+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1569+// | GNU General Public License for more details. |
1570+// | |
1571+// | You should have received a copy of the GNU General Public License |
1572+// | along with this program; if not, write to: |
1573+// | |
1574+// | Free Software Foundation, Inc. |
1575+// | 59 Temple Place - Suite 330 |
1576+// | Boston, MA 02111-1307, USA. |
1577+// +----------------------------------------------------------------------+
1578+// | Authors: Bryan Alsdorf <bryan@mysql.com> |
1579+// +----------------------------------------------------------------------+
1580+//
1581+//
1582+
1583+
1584+/**
1585+ * Class to handle determining which columns should be displayed and in what order
1586+ * on a page (e.g. Issue Listing page).
1587+ *
1588+ * @author Bryan Alsdorf <bryan@mysql.com>
1589+ * @version 1.0
1590+ */
1591+
1592+class Display_Column
1593+{
1594+ /**
1595+ * Returns the columns that should be displayed for the specified page.
1596+ * This method will remove columns that should not be displayed, due to
1597+ * lack of customer integration or insufficient role.
1598+ *
1599+ * @access public
1600+ * @param integer $prj_id The ID of the project.
1601+ * @param string $page The page to return columns for.
1602+ * @return array An array of columns that should be displayed.
1603+ */
1604+ function getColumnsToDisplay($prj_id, $page)
1605+ {
1606+ static $returns;
1607+
1608+ // poor man's caching system
1609+ if (!empty($returns[$prj_id][$page])) {
1610+ return $returns[$prj_id][$page];
1611+ }
1612+
1613+ $current_role = Auth::getCurrentRole();
1614+ $data = self::getSelectedColumns($prj_id, $page);
1615+ $has_customer_integration = Customer::hasCustomerIntegration($prj_id);
1616+ $only_with_customers = array('iss_customer_id', 'support_level');
1617+
1618+ // remove groups if there are no groups in the system.
1619+ if (count(Group::getAssocList($prj_id)) < 1) {
1620+ unset($data['iss_grp_id']);
1621+ }
1622+ // remove category column if there are no categories in the system
1623+ if (count(Category::getAssocList($prj_id)) < 1) {
1624+ unset($data['prc_title']);
1625+ }
1626+ // remove custom fields column if there are no custom fields
1627+ if (count(Custom_Field::getFieldsToBeListed($prj_id)) < 1) {
1628+ unset($data['custom_fields']);
1629+ }
1630+ // remove customer field if user has a role of customer
1631+ if ($current_role == User::getRoleID("Customer")) {
1632+ unset($data['iss_customer_id']);
1633+ }
1634+
1635+ foreach ($data as $field => $info) {
1636+ // remove fields based on role
1637+ if ($info['min_role'] > $current_role) {
1638+ unset($data[$field]);
1639+ continue;
1640+ }
1641+ // remove fields based on customer integration
1642+ if (!$has_customer_integration && (in_array($field, $only_with_customers))) {
1643+ unset($data[$field]);
1644+ continue;
1645+ }
1646+ // get title
1647+ $data[$field] = self::getColumnInfo($page, $field);
1648+ }
1649+ $returns[$prj_id][$page] = $data;
1650+ return $data;
1651+ }
1652+
1653+
1654+ /**
1655+ * Returns the columns that have been selected to be displayed on the specified page. This list
1656+ * contains all selected columns, even if they won't actually be displayed.
1657+ *
1658+ * @access public
1659+ * @param integer $prj_id The ID of the project.
1660+ * @param string $page The page to return columns for.
1661+ * @return array An array of columns that should be displayed.
1662+ */
1663+ function getSelectedColumns($prj_id, $page)
1664+ {
1665+ static $returns;
1666+
1667+ // poor man's caching system
1668+ if (!empty($returns[$prj_id][$page])) {
1669+ return $returns[$prj_id][$page];
1670+ }
1671+
1672+ $stmt = "SELECT
1673+ ctd_field,
1674+ ctd_min_role,
1675+ ctd_rank
1676+ FROM
1677+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1678+ WHERE
1679+ ctd_prj_id = $prj_id AND
1680+ ctd_page = '$page'
1681+ ORDER BY
1682+ ctd_rank";
1683+ $res = DB_Helper::getInstance()->getAssoc($stmt, false, array(), DB_FETCHMODE_ASSOC);
1684+ if (PEAR::isError($res)) {
1685+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1686+ return array();
1687+ } else {
1688+ $returns[$prj_id][$page] = array();
1689+ foreach ($res as $field_name => $row) {
1690+ $returns[$prj_id][$page][$field_name] = self::getColumnInfo($page, $field_name);
1691+ $returns[$prj_id][$page][$field_name]['min_role'] = $row['ctd_min_role'];
1692+ $returns[$prj_id][$page][$field_name]['rank'] = $row['ctd_rank'];
1693+ }
1694+ return $returns[$prj_id][$page];
1695+ }
1696+ }
1697+
1698+
1699+ /**
1700+ * Returns the info of the column
1701+ *
1702+ * @access public
1703+ * @param string $page The name of the page.
1704+ * @param string $column The name of the column
1705+ * @return string Info on the column
1706+ */
1707+ function getColumnInfo($page, $column)
1708+ {
1709+ $columns = self::getAllColumns($page);
1710+ return $columns[$column];
1711+ }
1712+
1713+
1714+ /**
1715+ * Returns all columns available for a page
1716+ *
1717+ * @access public
1718+ * @param string $page The name of the page
1719+ * @return array An array of columns
1720+ */
1721+ function getAllColumns($page)
1722+ {
1723+ $columns = array(
1724+ "list_issues" => array(
1725+ "pri_rank" => array(
1726+ "title" => ev_gettext("Priority")
1727+ ),
1728+ "iss_id" => array(
1729+ "title" => ev_gettext("Issue ID")
1730+ ),
1731+ "usr_full_name" => array(
1732+ "title" => ev_gettext("Reporter")
1733+ ),
1734+ "iss_created_date" => array(
1735+ "title" => ev_gettext("Created Date")
1736+ ),
1737+ "iss_grp_id" => array(
1738+ "title" => ev_gettext("Group")
1739+ ),
1740+ "assigned" => array(
1741+ "title" => ev_gettext("Assigned")
1742+ ),
1743+ "time_spent" => array(
1744+ "title" => ev_gettext("Time Spent")
1745+ ),
1746+ "iss_percent_complete" => array(
1747+ "title" => ev_gettext("% Complete"),
1748+ "default_role" => 9
1749+ ),
1750+ "iss_dev_time" => array(
1751+ "title" => ev_gettext("Est Dev Time"),
1752+ "default_role" => 9
1753+ ),
1754+ "prc_title" => array(
1755+ "title" => ev_gettext("Category")
1756+ ),
1757+ "pre_title" => array(
1758+ "title" => ev_gettext("Release")
1759+ ),
1760+ "iss_customer_id" => array(
1761+ "title" => ev_gettext("Customer")
1762+ ),
1763+ "support_level" => array(
1764+ "title" => ev_gettext("Support Level")
1765+ ),
1766+ "sta_rank" => array(
1767+ "title" => ev_gettext("Status")
1768+ ),
1769+ "sta_change_date" => array(
1770+ "title" => ev_gettext("Status Change Date")
1771+ ),
1772+ "last_action_date" => array(
1773+ "title" => ev_gettext("Last Action Date")
1774+ ),
1775+ "custom_fields" => array(
1776+ "title" => ev_gettext("Custom Fields")
1777+ ),
1778+ "iss_summary" => array(
1779+ "title" => ev_gettext("Summary"),
1780+ "align" => "left",
1781+ "width" => '30%'
1782+ ),
1783+ "iss_expected_resolution_date" => array(
1784+ "title" => ev_gettext("Expected Resolution Date")
1785+ )
1786+ )
1787+ );
1788+ return $columns[$page];
1789+ }
1790+
1791+
1792+ /**
1793+ * Saves settings on which columns should be displayed.
1794+ *
1795+ * @access public
1796+ * @return integer 1 if settings were saved successfully, -1 if there was an error.
1797+ */
1798+ function save()
1799+ {
1800+ $page = Misc::escapeString($_REQUEST['page']);
1801+ $prj_id = Misc::escapeInteger($_REQUEST['prj_id']);
1802+
1803+ $ranks = $_REQUEST['rank'];
1804+ asort($ranks);
1805+
1806+ // delete current entries
1807+ $stmt = "DELETE FROM
1808+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1809+ WHERE
1810+ ctd_prj_id = $prj_id AND
1811+ ctd_page = '$page'";
1812+ $res = DB_Helper::getInstance()->query($stmt);
1813+ if (PEAR::isError($res)) {
1814+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1815+ return -1;
1816+ }
1817+ $rank = 1;
1818+ foreach ($ranks as $field_name => $requested_rank) {
1819+ $sql = "INSERT INTO
1820+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1821+ SET
1822+ ctd_prj_id = $prj_id,
1823+ ctd_page = '$page',
1824+ ctd_field = '$field_name',
1825+ ctd_min_role = " . $_REQUEST['min_role'][$field_name] . ",
1826+ ctd_rank = $rank";
1827+ $res = DB_Helper::getInstance()->query($sql);
1828+ if (PEAR::isError($res)) {
1829+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1830+ return -1;
1831+ }
1832+ $rank++;
1833+ }
1834+ return 1;
1835+ }
1836+
1837+
1838+ /**
1839+ * Adds records in database for new project.
1840+ *
1841+ * @param integer $prj_id The ID of the project.
1842+ */
1843+ function setupNewProject($prj_id)
1844+ {
1845+ $page = 'list_issues';
1846+ $columns = self::getAllColumns($page);
1847+ $rank = 1;
1848+ foreach ($columns as $field_name => $column) {
1849+ if (!empty($column['default_role'])) {
1850+ $min_role = $column['default_role'];
1851+ } else {
1852+ $min_role = 1;
1853+ }
1854+ $stmt = "INSERT INTO
1855+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "columns_to_display
1856+ SET
1857+ ctd_prj_id = $prj_id,
1858+ ctd_page = '$page',
1859+ ctd_field = '$field_name',
1860+ ctd_min_role = $min_role,
1861+ ctd_rank = $rank";
1862+ $res = DB_Helper::getInstance()->query($stmt);
1863+ if (PEAR::isError($res)) {
1864+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1865+ return -1;
1866+ }
1867+ $rank++;
1868+ }
1869+ }
1870+}
1871--- eventum-2.2/lib/eventum/class.issue.php 2009-09-14 18:07:55.000000000 +0300
1872+++ eventum-2.2-order/lib/eventum/class.issue.php 2009-10-12 22:10:36.445851670 +0300
1873@@ -1333,6 +1333,7 @@
1874 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1875 return -1;
1876 } else {
1877+ self::moveOrderForAllUsers($issue_id, 1000);
1878 $prj_id = self::getProjectID($issue_id);
1879
1880 // record the change
1881@@ -1636,6 +1637,180 @@
1882 }
1883 }
1884
1885+ /**
1886+ * Method used to update the a single detail field of a specific issue.
1887+ *
1888+ * @param integer $issue_id
1889+ * @param string $field_name
1890+ * @param string $field_value
1891+ * @param string $field_type string or integer (for escape)
1892+ * @return integer 1 on success, -1 otherwise
1893+ */
1894+ function updateField($issue_id, $field_name, $filed_value) {
1895+
1896+ $issue_id = Misc::escapeInteger($issue_id);
1897+
1898+ $usr_id = Auth::getUserID();
1899+ $prj_id = self::getProjectID($issue_id);
1900+
1901+ // get all of the 'current' information of this issue
1902+ $current = self::getDetails($issue_id);
1903+
1904+ $stmt = "UPDATE
1905+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
1906+ SET
1907+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
1908+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
1909+ iss_last_public_action_type='updated'";
1910+
1911+ switch ($field_name) {
1912+ case 'category':
1913+ $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
1914+ break;
1915+ case 'release':
1916+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
1917+ break;
1918+ case 'expected_resolution_date':
1919+ if (is_null($filed_value)) {
1920+ $stmt .= ", iss_expected_resolution_date = null";
1921+ } else {
1922+ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
1923+ }
1924+ break;
1925+ case 'release':
1926+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
1927+ break;
1928+ case 'priority':
1929+ $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
1930+ break;
1931+ case 'status':
1932+ $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
1933+ break;
1934+ case 'resolution':
1935+ $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
1936+ break;
1937+ case 'summary':
1938+ $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
1939+ break;
1940+ case 'description':
1941+ $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
1942+ break;
1943+ case 'estimated_dev_time':
1944+ $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
1945+ break;
1946+ case 'percent_complete':
1947+ $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
1948+ break;
1949+ case 'trigger_reminders':
1950+ $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
1951+ break;
1952+ case 'group':
1953+ $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
1954+ break;
1955+ case 'private':
1956+ $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
1957+ break;
1958+ default:
1959+ Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
1960+ return -1;
1961+ break;
1962+ }
1963+
1964+ $stmt .= "
1965+ WHERE
1966+ iss_id=$issue_id";
1967+
1968+ $res = DB_Helper::getInstance()->query($stmt);
1969+ if (PEAR::isError($res)) {
1970+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
1971+ return -1;
1972+ } else {
1973+ $new = array(
1974+ 'category' => $current['iss_prc_id'],
1975+ 'release' => $current['iss_pre_id'],
1976+ 'expected_resolution_date' => $current['iss_expected_resolution_date'],
1977+ 'release' => $current['iss_pre_id'],
1978+ 'priority' => $current['iss_pri_id'],
1979+ 'status' => $current['iss_sta_id'],
1980+ 'resolution' => $current['iss_res_id'],
1981+ 'summary' => $current['iss_summary'],
1982+ 'description' => $current['iss_description'],
1983+ 'estimated_dev_time' => $current['iss_dev_time'],
1984+ 'percent_complete' => $current['iss_percent_complete'],
1985+ 'trigger_reminders' => $current['iss_trigger_reminders'],
1986+ 'group' => $current['iss_grp_id'],
1987+ 'iss_private' => $current['private']
1988+ );
1989+ $new[$field_name] = $filed_value;
1990+
1991+ // add change to the history (only for changes on specific fields?)
1992+ $updated_fields = array();
1993+ if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
1994+ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
1995+ }
1996+ if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
1997+ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
1998+ }
1999+ if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
2000+ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
2001+ }
2002+ if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
2003+ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
2004+ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
2005+ }
2006+ if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
2007+ // clear out the last-triggered-reminder flag when changing the status of an issue
2008+ Reminder_Action::clearLastTriggered($issue_id);
2009+
2010+ // if old status was closed and new status is not, clear closed data from issue.
2011+ $old_status_details = Status::getDetails($current['iss_sta_id']);
2012+ if ($old_status_details['sta_is_closed'] == 1) {
2013+ $new_status_details = Status::getDetails($filed_value);
2014+ if ($new_status_details['sta_is_closed'] != 1) {
2015+ self::clearClosed($issue_id);
2016+ }
2017+ }
2018+ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
2019+ }
2020+ if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
2021+ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
2022+ }
2023+ if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
2024+ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
2025+ }
2026+ if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
2027+ $updated_fields["Summary"] = '';
2028+ }
2029+ if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
2030+ $updated_fields["Description"] = '';
2031+ }
2032+ if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
2033+ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
2034+ }
2035+ if (count($updated_fields) > 0) {
2036+ // log the changes
2037+ $changes = '';
2038+ $i = 0;
2039+ foreach ($updated_fields as $key => $value) {
2040+ if ($i > 0) {
2041+ $changes .= "; ";
2042+ }
2043+ if (($key != "Summary") && ($key != "Description")) {
2044+ $changes .= "$key: $value";
2045+ } else {
2046+ $changes .= "$key";
2047+ }
2048+ $i++;
2049+ }
2050+
2051+ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
2052+ // send notifications for the issue being updated
2053+ Notification::notifyIssueUpdated($issue_id, $current, $new);
2054+ }
2055+ }
2056+ return 1;
2057+ }
2058+
2059
2060 /**
2061 * Move the issue to a new project
2062@@ -1800,16 +1975,33 @@
2063 {
2064 $issue_id = Misc::escapeInteger($issue_id);
2065 $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
2066+ $order = 1;
2067+ // move all orders down to free "order space" for this new association
2068+ $stmt = "UPDATE
2069+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2070+ SET
2071+ isu_order = isu_order + 1
2072+ WHERE
2073+ isu_usr_id = $assignee_usr_id AND
2074+ isu_order >= $order";
2075+ $res = DB_Helper::getInstance()->query($stmt);
2076+ if (PEAR::isError($res)) {
2077+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2078+ return -1;
2079+ }
2080+ // insert the new association
2081 $stmt = "INSERT INTO
2082 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2083 (
2084 isu_iss_id,
2085 isu_usr_id,
2086- isu_assigned_date
2087+ isu_assigned_date,
2088+ isu_order
2089 ) VALUES (
2090 $issue_id,
2091 $assignee_usr_id,
2092- '" . Date_Helper::getCurrentDateGMT() . "'
2093+ '" . Date_Helper::getCurrentDateGMT() . "',
2094+ $order
2095 )";
2096 $res = DB_Helper::getInstance()->query($stmt);
2097 if (PEAR::isError($res)) {
2098@@ -1824,6 +2016,78 @@
2099 }
2100 }
2101
2102+ /**
2103+ * Method used to get the order list to be rearranged
2104+ *
2105+ * @access private
2106+ * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
2107+ * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
2108+ * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
2109+ */
2110+ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
2111+ {
2112+ // find all affected associantion orders
2113+ $stmt = "SELECT isu_usr_id, isu_order FROM
2114+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2115+ WHERE
2116+ isu_iss_id IN ($issue_id)";
2117+ if (!empty($usr_id)) {
2118+ $stmt.= " AND isu_usr_id IN ($usr_id)";
2119+ }
2120+ $stmt.= "ORDER BY isu_order";
2121+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
2122+ if (PEAR::isError($res)) {
2123+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2124+ return -1;
2125+ } else {
2126+ $deleted_orders = array();
2127+ foreach ($res as $row) {
2128+ if (empty($deleted_orders[$row['isu_usr_id']])) {
2129+ $deleted_orders[$row['isu_usr_id']] = array();
2130+ }
2131+ $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
2132+ }
2133+ return $deleted_orders;
2134+ }
2135+ }
2136+
2137+ /**
2138+ *
2139+ * Method used to rearrange order list in the db according to known deleted records
2140+ *
2141+ * @access private
2142+ * @param mixed deleteorder list
2143+ * @return void
2144+ */
2145+ function rearrangeDeleteUserAssociationOrderList($delete_order_list)
2146+ {
2147+ if (empty($delete_order_list) || (!is_array($delete_order_list))) {
2148+ return -1;
2149+ }
2150+ foreach ($delete_order_list as $isu_usr_id => $orders) {
2151+ for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
2152+ // move the orders after them up to take the "order space" of the deleted records
2153+ $stmt = "UPDATE
2154+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2155+ SET
2156+ isu_order = isu_order - " . ($i+1) . "
2157+ WHERE
2158+ isu_usr_id = $isu_usr_id AND
2159+ isu_order > " . $orders[$i];
2160+ if ($i < count($orders) - 1) {
2161+ $stmt.= " AND
2162+ isu_order < " . $orders[$i+1];
2163+ }
2164+ $res = DB_Helper::getInstance()->query($stmt);
2165+ if (PEAR::isError($res)) {
2166+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2167+ return -1;
2168+ }
2169+ }
2170+ }
2171+ return 1;
2172+ }
2173+
2174
2175 /**
2176 * Method used to delete all user assignments for a specific issue.
2177@@ -1839,6 +2103,7 @@
2178 if (is_array($issue_id)) {
2179 $issue_id = implode(", ", $issue_id);
2180 }
2181+ $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
2182 $stmt = "DELETE FROM
2183 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2184 WHERE
2185@@ -1869,6 +2134,7 @@
2186 {
2187 $issue_id = Misc::escapeInteger($issue_id);
2188 $usr_id = Misc::escapeInteger($usr_id);
2189+ $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
2190 $stmt = "DELETE FROM
2191 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2192 WHERE
2193@@ -1883,6 +2149,7 @@
2194 History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
2195 User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
2196 }
2197+ self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
2198 return 1;
2199 }
2200 }
2201@@ -2342,6 +2609,11 @@
2202 {
2203 $sort_by = self::getParam('sort_by');
2204 $sort_order = self::getParam('sort_order');
2205+ $users = self::getParam('users');
2206+ if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
2207+ unset($sort_by);
2208+ unset($sort_order);
2209+ }
2210 $rows = self::getParam('rows');
2211 $hide_closed = self::getParam('hide_closed');
2212 if ($hide_closed === '') {
2213@@ -2448,6 +2720,7 @@
2214 "iss_expected_resolution_date" => "desc",
2215 "pre_title" => "asc",
2216 "assigned" => "asc",
2217+ "isu_order" => "desc",
2218 );
2219
2220 foreach ($custom_fields as $fld_id => $fld_name) {
2221@@ -3253,6 +3526,8 @@
2222 $ids = implode(", ", $ids);
2223 $stmt = "SELECT
2224 isu_iss_id,
2225+ isu_order,
2226+ isu_usr_id,
2227 usr_full_name
2228 FROM
2229 " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
2230@@ -3264,6 +3539,7 @@
2231 if (PEAR::isError($res)) {
2232 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2233 } else {
2234+ // gather names of the users assigned to each issue
2235 $t = array();
2236 for ($i = 0; $i < count($res); $i++) {
2237 if (!empty($t[$res[$i]['isu_iss_id']])) {
2238@@ -3272,9 +3548,18 @@
2239 $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
2240 }
2241 }
2242+ // gather orders
2243+ $o = array();
2244+ for ($i = 0; $i < count($res); $i++) {
2245+ if (empty($o[$res[$i]['isu_iss_id']])) {
2246+ $o[$res[$i]['isu_iss_id']] = array();
2247+ }
2248+ $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
2249+ }
2250 // now populate the $result variable again
2251 for ($i = 0; $i < count($result); $i++) {
2252 @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
2253+ @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
2254 }
2255 }
2256 }
2257@@ -4247,6 +4532,7 @@
2258 Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2259 return -1;
2260 }
2261+ self::moveOrderForAllUsers($issue_id, 1);
2262 }
2263
2264
2265@@ -4346,4 +4632,120 @@
2266 History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
2267 "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
2268 }
2269+
2270+ /**
2271+ * Reorders user's issues as requested by user
2272+ * @access public
2273+ * @param $usr_id User to be reordered
2274+ * @param $issue_id Issue or array of issues to be moved
2275+ * @param $neworder The new order of the issues
2276+ * @return void
2277+ */
2278+ function reorderUserIssues($usr_id, $issue_id, $neworder)
2279+ {
2280+ if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
2281+ return false;
2282+ }
2283+ if (!is_numeric($usr_id) || !is_numeric($neworder)) {
2284+ return false;
2285+ }
2286+ $usr_id = Misc::escapeInteger($usr_id);
2287+ $issue_id = Misc::escapeInteger($issue_id);
2288+ $neworder = Misc::escapeInteger($neworder);
2289+ if (is_array($issue_id)) {
2290+ $issue_count = count($issue_id);
2291+ $issue_id_str = implode(", ", $issue_id);
2292+ } else {
2293+ $issue_count = 1;
2294+ $issue_id_str = $issue_id;
2295+ $issue_id = array($issue_id);
2296+ }
2297+ // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
2298+ $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
2299+ self::rearrangeDeleteUserAssociationOrderList($orderlist);
2300+ // move down the orders to free the "order space" needed
2301+ $stmt = "UPDATE
2302+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2303+ SET
2304+ isu_order = isu_order + $issue_count
2305+ WHERE
2306+ isu_usr_id = $usr_id AND
2307+ isu_order >= $neworder";
2308+ $res = DB_Helper::getInstance()->query($stmt);
2309+ if (PEAR::isError($res)) {
2310+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2311+ return -1;
2312+ }
2313+ //update the order for the issues being moved
2314+ $i = 0;
2315+ foreach ($issue_id as $iss_id) {
2316+ $stmt = "UPDATE
2317+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2318+ SET
2319+ isu_order = " . ($neworder + $i) . "
2320+ WHERE
2321+ isu_usr_id = $usr_id AND
2322+ isu_iss_id = $iss_id";
2323+ $res = DB_Helper::getInstance()->query($stmt);
2324+ if (PEAR::isError($res)) {
2325+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2326+ return -1;
2327+ }
2328+ $i++;
2329+ }
2330+ }
2331+
2332+
2333+ /**
2334+ * Get users issue order list
2335+ * @access public
2336+ * @param $user_id User
2337+ * @param $order_list Order of the issues
2338+ * @return void
2339+ */
2340+ function getIssueOrderByUser($usr_id) {
2341+
2342+ if (!is_numeric($usr_id)) {
2343+ return false;
2344+ }
2345+
2346+ $stmt = "SELECT
2347+ isu_iss_id, isu_order
2348+ FROM
2349+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2350+ WHERE
2351+ isu_usr_id = " . $usr_id ;
2352+
2353+ $order_list = array();
2354+
2355+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
2356+
2357+ if (PEAR::isError($res)) {
2358+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2359+ return array();
2360+ } else {
2361+ foreach ($res as $row) {
2362+ $order_list[$row["isu_iss_id"]] = $row["isu_order"];
2363+ }
2364+ }
2365+ return $order_list;
2366+ }
2367+
2368+ function moveOrderForAllUsers($issue_id, $neworder)
2369+ {
2370+ // Move the issue to the top priority for the ppl it's assigned to
2371+ $stmt = "SELECT isu_usr_id FROM
2372+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
2373+ WHERE
2374+ isu_iss_id = " . Misc::escapeInteger($issue_id);
2375+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
2376+ if (PEAR::isError($res)) {
2377+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2378+ return -1;
2379+ }
2380+ foreach ($res as $row) {
2381+ self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
2382+ }
2383+ }
2384+
2385 }
2386--- eventum-2.2/lib/eventum/class.issue.php.~1~ 1970-01-01 02:00:00.000000000 +0200
2387+++ eventum-2.2-order/lib/eventum/class.issue.php.~1~ 2009-09-14 18:07:55.000000000 +0300
2388@@ -0,0 +1,4349 @@
2389+<?php
2390+/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
2391+// +----------------------------------------------------------------------+
2392+// | Eventum - Issue Tracking System |
2393+// +----------------------------------------------------------------------+
2394+// | Copyright (c) 2003 - 2008 MySQL AB |
2395+// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
2396+// | |
2397+// | This program is free software; you can redistribute it and/or modify |
2398+// | it under the terms of the GNU General Public License as published by |
2399+// | the Free Software Foundation; either version 2 of the License, or |
2400+// | (at your option) any later version. |
2401+// | |
2402+// | This program is distributed in the hope that it will be useful, |
2403+// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
2404+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2405+// | GNU General Public License for more details. |
2406+// | |
2407+// | You should have received a copy of the GNU General Public License |
2408+// | along with this program; if not, write to: |
2409+// | |
2410+// | Free Software Foundation, Inc. |
2411+// | 59 Temple Place - Suite 330 |
2412+// | Boston, MA 02111-1307, USA. |
2413+// +----------------------------------------------------------------------+
2414+// | Authors: João Prado Maia <jpm@mysql.com> |
2415+// +----------------------------------------------------------------------+
2416+//
2417+
2418+
2419+/**
2420+ * Class designed to handle all business logic related to the issues in the
2421+ * system, such as adding or updating them or listing them in the grid mode.
2422+ *
2423+ * @author João Prado Maia <jpm@mysql.com>
2424+ * @version $Revision$
2425+ */
2426+
2427+class Issue
2428+{
2429+ /**
2430+ * Method used to check whether a given issue ID exists or not.
2431+ *
2432+ * @access public
2433+ * @param integer $issue_id The issue ID
2434+ * @param boolean $check_project If we should check that this issue is in the current project
2435+ * @return boolean
2436+ */
2437+ function exists($issue_id, $check_project = true)
2438+ {
2439+ $stmt = "SELECT
2440+ COUNT(*)
2441+ FROM
2442+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2443+ WHERE
2444+ iss_id=" . Misc::escapeInteger($issue_id);
2445+ if ($check_project) {
2446+ $stmt .= " AND
2447+ iss_prj_id = " . Auth::getCurrentProject();
2448+ }
2449+ $res = DB_Helper::getInstance()->getOne($stmt);
2450+ if (PEAR::isError($res)) {
2451+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2452+ return false;
2453+ } else {
2454+ if ($res == 0) {
2455+ return false;
2456+ } else {
2457+ return true;
2458+ }
2459+ }
2460+ }
2461+
2462+
2463+ /**
2464+ * Method used to get the list of column heading titles for the
2465+ * CSV export functionality of the issue listing screen.
2466+ *
2467+ * @access public
2468+ * @param integer $prj_id The project ID
2469+ * @return array The list of column heading titles
2470+ */
2471+ function getColumnHeadings($prj_id)
2472+ {
2473+ $headings = array(
2474+ 'Priority',
2475+ 'Issue ID',
2476+ 'Reporter',
2477+ );
2478+ // hide the group column from the output if no
2479+ // groups are available in the database
2480+ $groups = Group::getAssocList($prj_id);
2481+ if (count($groups) > 0) {
2482+ $headings[] = 'Group';
2483+ }
2484+ $headings[] = 'Assigned';
2485+ $headings[] = 'Time Spent';
2486+ // hide the category column from the output if no
2487+ // categories are available in the database
2488+ $categories = Category::getAssocList($prj_id);
2489+ if (count($categories) > 0) {
2490+ $headings[] = 'Category';
2491+ }
2492+ if (Customer::hasCustomerIntegration($prj_id)) {
2493+ $headings[] = 'Customer';
2494+ }
2495+ $headings[] = 'Status';
2496+ $headings[] = 'Status Change Date';
2497+ $headings[] = 'Last Action Date';
2498+ $headings[] = 'Est. Dev. TIme';
2499+ $headings[] = 'Summary';
2500+ $headings[] = 'Expected Resolution Date';
2501+ return $headings;
2502+ }
2503+
2504+
2505+ /**
2506+ * Method used to get the full list of date fields available to issues, to
2507+ * be used when customizing the issue listing screen in the 'last status
2508+ * change date' column.
2509+ *
2510+ * @access public
2511+ * @param boolean $display_customer_fields Whether to include any customer related fields or not
2512+ * @return array The list of available date fields
2513+ */
2514+ function getDateFieldsAssocList($display_customer_fields = FALSE)
2515+ {
2516+ $fields = array(
2517+ 'iss_created_date' => 'Created Date',
2518+ 'iss_updated_date' => 'Last Updated Date',
2519+ 'iss_last_response_date' => 'Last Response Date',
2520+ 'iss_closed_date' => 'Closed Date'
2521+ );
2522+ if ($display_customer_fields) {
2523+ $fields['iss_last_customer_action_date'] = 'Customer Action Date';
2524+ }
2525+ asort($fields);
2526+ return $fields;
2527+ }
2528+
2529+
2530+ /**
2531+ * Method used to get the full list of issue IDs and their respective
2532+ * titles associated to a given project.
2533+ *
2534+ * @access public
2535+ * @param integer $prj_id The project ID
2536+ * @return array The list of issues
2537+ */
2538+ function getAssocListByProject($prj_id)
2539+ {
2540+ $stmt = "SELECT
2541+ iss_id,
2542+ iss_summary
2543+ FROM
2544+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2545+ WHERE
2546+ iss_prj_id=" . Misc::escapeInteger($prj_id) . "
2547+ ORDER BY
2548+ iss_id ASC";
2549+ $res = DB_Helper::getInstance()->getAssoc($stmt);
2550+ if (PEAR::isError($res)) {
2551+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2552+ return "";
2553+ } else {
2554+ return $res;
2555+ }
2556+ }
2557+
2558+
2559+ /**
2560+ * Method used to get the status of a given issue.
2561+ *
2562+ * @access public
2563+ * @param integer $issue_id The issue ID
2564+ * @return integer The status ID
2565+ */
2566+ function getStatusID($issue_id)
2567+ {
2568+ static $returns;
2569+
2570+ $issue_id = Misc::escapeInteger($issue_id);
2571+
2572+ if (!empty($returns[$issue_id])) {
2573+ return $returns[$issue_id];
2574+ }
2575+
2576+ $stmt = "SELECT
2577+ iss_sta_id
2578+ FROM
2579+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2580+ WHERE
2581+ iss_id=$issue_id";
2582+ $res = DB_Helper::getInstance()->getOne($stmt);
2583+ if (PEAR::isError($res)) {
2584+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2585+ return '';
2586+ } else {
2587+ $returns[$issue_id] = $res;
2588+ return $res;
2589+ }
2590+ }
2591+
2592+
2593+ /**
2594+ * Records the last customer action date for a given issue ID.
2595+ *
2596+ * @access public
2597+ * @param integer $issue_id The issue ID
2598+ * @return integer 1 if the update worked, -1 otherwise
2599+ */
2600+ function recordLastCustomerAction($issue_id)
2601+ {
2602+ $stmt = "UPDATE
2603+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2604+ SET
2605+ iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
2606+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
2607+ iss_last_public_action_type='customer action'
2608+ WHERE
2609+ iss_id=" . Misc::escapeInteger($issue_id);
2610+ $res = DB_Helper::getInstance()->query($stmt);
2611+ if (PEAR::isError($res)) {
2612+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2613+ return -1;
2614+ } else {
2615+ return 1;
2616+ }
2617+ }
2618+
2619+
2620+ /**
2621+ * Returns the customer ID associated with the given issue ID.
2622+ *
2623+ * @access public
2624+ * @param integer $issue_id The issue ID
2625+ * @return integer The customer ID associated with the issue
2626+ */
2627+ function getCustomerID($issue_id)
2628+ {
2629+ static $returns;
2630+
2631+ $issue_id = Misc::escapeInteger($issue_id);
2632+
2633+ if (!empty($returns[$issue_id])) {
2634+ return $returns[$issue_id];
2635+ }
2636+
2637+ $stmt = "SELECT
2638+ iss_customer_id
2639+ FROM
2640+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2641+ WHERE
2642+ iss_id=$issue_id";
2643+ $res = DB_Helper::getInstance()->getOne($stmt);
2644+ if (PEAR::isError($res)) {
2645+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2646+ return '';
2647+ } else {
2648+ $returns[$issue_id] = $res;
2649+ return $res;
2650+ }
2651+ }
2652+
2653+
2654+ /**
2655+ * Returns the contract ID associated with the given issue ID.
2656+ *
2657+ * @access public
2658+ * @param integer $issue_id The issue ID
2659+ * @return integer The customer ID associated with the issue
2660+ */
2661+ function getContractID($issue_id)
2662+ {
2663+ static $returns;
2664+
2665+ $issue_id = Misc::escapeInteger($issue_id);
2666+
2667+ if (!empty($returns[$issue_id])) {
2668+ return $returns[$issue_id];
2669+ }
2670+
2671+ $stmt = "SELECT
2672+ iss_customer_contract_id
2673+ FROM
2674+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2675+ WHERE
2676+ iss_id=$issue_id";
2677+ $res = DB_Helper::getInstance()->getOne($stmt);
2678+ if (PEAR::isError($res)) {
2679+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2680+ return '';
2681+ } else {
2682+ $returns[$issue_id] = $res;
2683+ return $res;
2684+ }
2685+ }
2686+
2687+
2688+ /**
2689+ * Sets the contract ID for a specific issue.
2690+ *
2691+ * @access public
2692+ * @param integer $issue_id The issue ID
2693+ * @param integer The contract ID
2694+ * @return integer 1 if the update worked, -1 otherwise
2695+ */
2696+ function setContractID($issue_id, $contract_id)
2697+ {
2698+ $issue_id = Misc::escapeInteger($issue_id);
2699+
2700+ $old_contract_id = self::getContractID($issue_id);
2701+
2702+ $stmt = "UPDATE
2703+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2704+ SET
2705+ iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
2706+ WHERE
2707+ iss_id=$issue_id";
2708+ $res = DB_Helper::getInstance()->query($stmt);
2709+ if (PEAR::isError($res)) {
2710+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2711+ return -1;
2712+ } else {
2713+ // log this
2714+ History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
2715+ return 1;
2716+ }
2717+ }
2718+
2719+
2720+ /**
2721+ * Returns the customer ID associated with the given issue ID.
2722+ *
2723+ * @access public
2724+ * @param integer $issue_id The issue ID
2725+ * @return integer The customer ID associated with the issue
2726+ */
2727+ function getContactID($issue_id)
2728+ {
2729+ static $returns;
2730+
2731+ $issue_id = Misc::escapeInteger($issue_id);
2732+
2733+ if (!empty($returns[$issue_id])) {
2734+ return $returns[$issue_id];
2735+ }
2736+
2737+ $stmt = "SELECT
2738+ iss_customer_contact_id
2739+ FROM
2740+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2741+ WHERE
2742+ iss_id=$issue_id";
2743+ $res = DB_Helper::getInstance()->getOne($stmt);
2744+ if (PEAR::isError($res)) {
2745+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2746+ return '';
2747+ } else {
2748+ $returns[$issue_id] = $res;
2749+ return $res;
2750+ }
2751+ }
2752+
2753+
2754+ /**
2755+ * Method used to get the project associated to a given issue.
2756+ *
2757+ * @access public
2758+ * @param integer $issue_id The issue ID
2759+ * @param boolean $force_refresh If the cache should not be used.
2760+ * @return integer The project ID
2761+ */
2762+ function getProjectID($issue_id, $force_refresh = false)
2763+ {
2764+ static $returns;
2765+
2766+ $issue_id = Misc::escapeInteger($issue_id);
2767+
2768+ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
2769+ return $returns[$issue_id];
2770+ }
2771+
2772+ $stmt = "SELECT
2773+ iss_prj_id
2774+ FROM
2775+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2776+ WHERE
2777+ iss_id=$issue_id";
2778+ $res = DB_Helper::getInstance()->getOne($stmt);
2779+ if (PEAR::isError($res)) {
2780+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2781+ return '';
2782+ } else {
2783+ $returns[$issue_id] = $res;
2784+ return $res;
2785+ }
2786+ }
2787+
2788+
2789+ /**
2790+ * Method used to remotely assign a given issue to an user.
2791+ *
2792+ * @access public
2793+ * @param integer $issue_id The issue ID
2794+ * @param integer $usr_id The user ID of the person performing the change
2795+ * @param boolean $assignee The user ID of the assignee
2796+ * @return integer The status ID
2797+ */
2798+ function remoteAssign($issue_id, $usr_id, $assignee)
2799+ {
2800+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
2801+ // clear up the assignments for this issue, and then assign it to the current user
2802+ self::deleteUserAssociations($issue_id, $usr_id);
2803+ $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
2804+ if ($res != -1) {
2805+ // save a history entry about this...
2806+ History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
2807+ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
2808+ if ($assignee != $usr_id) {
2809+ Notification::notifyNewAssignment(array($assignee), $issue_id);
2810+ }
2811+ }
2812+ return $res;
2813+ }
2814+
2815+
2816+ /**
2817+ * Method used to set the status of a given issue.
2818+ *
2819+ * @access public
2820+ * @param integer $issue_id The issue ID
2821+ * @param integer $status_id The new status ID
2822+ * @param boolean $notify If a notification should be sent about this change.
2823+ * @return integer 1 if the update worked, -1 otherwise
2824+ */
2825+ function setStatus($issue_id, $status_id, $notify = false)
2826+ {
2827+ $issue_id = Misc::escapeInteger($issue_id);
2828+ $status_id = Misc::escapeInteger($status_id);
2829+
2830+ $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
2831+ if ($workflow !== true) {
2832+ return $workflow;
2833+ }
2834+
2835+ // check if the status is already set to the 'new' one
2836+ if (self::getStatusID($issue_id) == $status_id) {
2837+ return -1;
2838+ }
2839+
2840+ $old_status = self::getStatusID($issue_id);
2841+ $old_details = Status::getDetails($old_status);
2842+
2843+ $stmt = "UPDATE
2844+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2845+ SET
2846+ iss_sta_id=$status_id,
2847+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
2848+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
2849+ iss_last_public_action_type='update'
2850+ WHERE
2851+ iss_id=$issue_id";
2852+ $res = DB_Helper::getInstance()->query($stmt);
2853+ if (PEAR::isError($res)) {
2854+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2855+ return -1;
2856+ } else {
2857+ // clear out the last-triggered-reminder flag when changing the status of an issue
2858+ Reminder_Action::clearLastTriggered($issue_id);
2859+
2860+ // if old status was closed and new status is not, clear closed data from issue.
2861+ if (@$old_details['sta_is_closed'] == 1) {
2862+ $new_details = Status::getDetails($status_id);
2863+ if ($new_details['sta_is_closed'] != 1) {
2864+ self::clearClosed($issue_id);
2865+ }
2866+ }
2867+
2868+ if ($notify) {
2869+ Notification::notifyStatusChange($issue_id, $old_status, $status_id);
2870+ }
2871+
2872+ return 1;
2873+ }
2874+ }
2875+
2876+
2877+ /**
2878+ * Method used to remotely set the status of a given issue.
2879+ *
2880+ * @access public
2881+ * @param integer $issue_id The issue ID
2882+ * @param integer $usr_id The user ID of the person performing this change
2883+ * @param integer $new_status The new status ID
2884+ * @return integer 1 if the update worked, -1 otherwise
2885+ */
2886+ function setRemoteStatus($issue_id, $usr_id, $new_status)
2887+ {
2888+ $sta_id = Status::getStatusID($new_status);
2889+
2890+ $res = self::setStatus($issue_id, $sta_id);
2891+ if ($res == 1) {
2892+ // record history entry
2893+ History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
2894+ }
2895+ return $res;
2896+ }
2897+
2898+
2899+ /**
2900+ * Method used to set the release of an issue
2901+ *
2902+ * @access public
2903+ * @param integer $issue_id The ID of the issue
2904+ * @param integer $pre_id The ID of the release to set this issue too
2905+ * @return integer 1 if the update worked, -1 otherwise
2906+ */
2907+ function setRelease($issue_id, $pre_id)
2908+ {
2909+ $issue_id = Misc::escapeInteger($issue_id);
2910+ $pre_id = Misc::escapeInteger($pre_id);
2911+
2912+ if ($pre_id != self::getRelease($issue_id)) {
2913+ $sql = "UPDATE
2914+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2915+ SET
2916+ iss_pre_id = $pre_id
2917+ WHERE
2918+ iss_id = $issue_id";
2919+ $res = DB_Helper::getInstance()->query($sql);
2920+ if (PEAR::isError($res)) {
2921+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2922+ return -1;
2923+ } else {
2924+ return 1;
2925+ }
2926+ }
2927+ }
2928+
2929+
2930+ /**
2931+ * Returns the current release of an issue
2932+ *
2933+ * @access public
2934+ * @param integer $issue_id The ID of the issue
2935+ * @return integer The release
2936+ */
2937+ function getRelease($issue_id)
2938+ {
2939+ $sql = "SELECT
2940+ iss_pre_id
2941+ FROM
2942+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2943+ WHERE
2944+ iss_id = " . Misc::escapeInteger($issue_id);
2945+ $res = DB_Helper::getInstance()->getOne($sql);
2946+ if (PEAR::isError($res)) {
2947+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2948+ return 0;
2949+ } else {
2950+ return $res;
2951+ }
2952+ }
2953+
2954+
2955+ /**
2956+ * Method used to set the priority of an issue
2957+ *
2958+ * @access public
2959+ * @param integer $issue_id The ID of the issue
2960+ * @param integer $pri_id The ID of the priority to set this issue too
2961+ * @return integer 1 if the update worked, -1 otherwise
2962+ */
2963+ function setPriority($issue_id, $pri_id)
2964+ {
2965+ $issue_id = Misc::escapeInteger($issue_id);
2966+ $pri_id = Misc::escapeInteger($pri_id);
2967+
2968+ if ($pri_id != self::getPriority($issue_id)) {
2969+ $sql = "UPDATE
2970+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2971+ SET
2972+ iss_pri_id = $pri_id
2973+ WHERE
2974+ iss_id = $issue_id";
2975+ $res = DB_Helper::getInstance()->query($sql);
2976+ if (PEAR::isError($res)) {
2977+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
2978+ return -1;
2979+ } else {
2980+ return 1;
2981+ }
2982+ }
2983+ }
2984+
2985+
2986+ /**
2987+ * Returns the current issue priority
2988+ *
2989+ * @access public
2990+ * @param integer $issue_id The ID of the issue
2991+ * @return integer The priority
2992+ */
2993+ function getPriority($issue_id)
2994+ {
2995+ $sql = "SELECT
2996+ iss_pri_id
2997+ FROM
2998+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
2999+ WHERE
3000+ iss_id = " . Misc::escapeInteger($issue_id);
3001+ $res = DB_Helper::getInstance()->getOne($sql);
3002+ if (PEAR::isError($res)) {
3003+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3004+ return 0;
3005+ } else {
3006+ return $res;
3007+ }
3008+ }
3009+
3010+
3011+ /**
3012+ * Method used to set the category of an issue
3013+ *
3014+ * @access public
3015+ * @param integer $issue_id The ID of the issue
3016+ * @param integer $prc_id The ID of the category to set this issue too
3017+ * @return integer 1 if the update worked, -1 otherwise
3018+ */
3019+ function setCategory($issue_id, $prc_id)
3020+ {
3021+ $issue_id = Misc::escapeInteger($issue_id);
3022+ $prc_id = Misc::escapeInteger($prc_id);
3023+
3024+ if ($prc_id != self::getPriority($issue_id)) {
3025+ $sql = "UPDATE
3026+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3027+ SET
3028+ iss_prc_id = $prc_id
3029+ WHERE
3030+ iss_id = $issue_id";
3031+ $res = DB_Helper::getInstance()->query($sql);
3032+ if (PEAR::isError($res)) {
3033+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3034+ return -1;
3035+ } else {
3036+ return 1;
3037+ }
3038+ }
3039+ }
3040+
3041+
3042+ /**
3043+ * Returns the current issue category
3044+ *
3045+ * @access public
3046+ * @param integer $issue_id The ID of the issue
3047+ * @return integer The category
3048+ */
3049+ function getCategory($issue_id)
3050+ {
3051+ $sql = "SELECT
3052+ iss_prc_id
3053+ FROM
3054+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3055+ WHERE
3056+ iss_id = " . Misc::escapeInteger($issue_id);
3057+ $res = DB_Helper::getInstance()->getOne($sql);
3058+ if (PEAR::isError($res)) {
3059+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3060+ return 0;
3061+ } else {
3062+ return $res;
3063+ }
3064+ }
3065+
3066+
3067+ /**
3068+ * Method used to get all issues associated with a status that doesn't have
3069+ * the 'closed' context.
3070+ *
3071+ * @access public
3072+ * @param integer $prj_id The project ID to list issues from
3073+ * @param integer $usr_id The user ID of the user requesting this information
3074+ * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
3075+ * @param integer $status_id The status ID to be used to restrict results
3076+ * @return array The list of open issues
3077+ */
3078+ function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
3079+ {
3080+ $prj_id = Misc::escapeInteger($prj_id);
3081+ $status_id = Misc::escapeInteger($status_id);
3082+ $projects = Project::getRemoteAssocListByUser($usr_id);
3083+ if (@count($projects) == 0) {
3084+ return '';
3085+ }
3086+
3087+ $stmt = "SELECT
3088+ iss_id,
3089+ iss_summary,
3090+ sta_title
3091+ FROM
3092+ (
3093+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
3094+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
3095+ )
3096+ LEFT JOIN
3097+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
3098+ ON
3099+ isu_iss_id=iss_id
3100+ WHERE ";
3101+ if (!empty($status_id)) {
3102+ $stmt .= " sta_id=$status_id AND ";
3103+ }
3104+ $stmt .= "
3105+ iss_prj_id=$prj_id AND
3106+ sta_id=iss_sta_id AND
3107+ sta_is_closed=0";
3108+ if ($show_all_issues == false) {
3109+ $stmt .= " AND
3110+ isu_usr_id=$usr_id";
3111+ }
3112+ $stmt .= "\nGROUP BY
3113+ iss_id";
3114+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
3115+ if (PEAR::isError($res)) {
3116+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3117+ return '';
3118+ } else {
3119+ if (count($res) > 0) {
3120+ self::getAssignedUsersByIssues($res);
3121+ }
3122+ return $res;
3123+ }
3124+ }
3125+
3126+
3127+ /**
3128+ * Method used to build the required parameters to simulate an email reply
3129+ * to the user who reported the issue, using the issue details like summary
3130+ * and description as email fields.
3131+ *
3132+ * @access public
3133+ * @param integer $issue_id The issue ID
3134+ * @return array The email parameters
3135+ */
3136+ function getReplyDetails($issue_id)
3137+ {
3138+ $issue_id = Misc::escapeInteger($issue_id);
3139+
3140+ $stmt = "SELECT
3141+ iss_created_date,
3142+ usr_full_name AS reporter,
3143+ usr_email AS reporter_email,
3144+ iss_description AS description,
3145+ iss_summary AS sup_subject
3146+ FROM
3147+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
3148+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
3149+ WHERE
3150+ iss_usr_id=usr_id AND
3151+ iss_id=$issue_id";
3152+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
3153+ if (PEAR::isError($res)) {
3154+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3155+ return '';
3156+ } else {
3157+ $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
3158+ $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
3159+ return $res;
3160+ }
3161+ }
3162+
3163+
3164+ /**
3165+ * Method used to record the last updated timestamp for a given
3166+ * issue ID.
3167+ *
3168+ * @access public
3169+ * @param integer $issue_id The issue ID
3170+ * @param string $type The type of update that was made (optional)
3171+ * @return boolean
3172+ */
3173+ function markAsUpdated($issue_id, $type = false)
3174+ {
3175+ $public = array("staff response", "customer action", "file uploaded", "user response");
3176+ $stmt = "UPDATE
3177+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3178+ SET
3179+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
3180+ if ($type != false) {
3181+ if (in_array($type, $public)) {
3182+ $field = "iss_last_public_action_";
3183+ } else {
3184+ $field = "iss_last_internal_action_";
3185+ }
3186+ $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
3187+ $field . "type ='" . Misc::escapeString($type) . "'\n";
3188+ }
3189+ $stmt .= "WHERE
3190+ iss_id=" . Misc::escapeInteger($issue_id);
3191+ $res = DB_Helper::getInstance()->query($stmt);
3192+ if (PEAR::isError($res)) {
3193+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3194+ return false;
3195+ } else {
3196+ // update last response dates if this is a staff response
3197+ if ($type == "staff response") {
3198+ $stmt = "UPDATE
3199+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3200+ SET
3201+ iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
3202+ WHERE
3203+ iss_id = " . Misc::escapeInteger($issue_id);
3204+ DB_Helper::getInstance()->query($stmt);
3205+ $stmt = "UPDATE
3206+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3207+ SET
3208+ iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
3209+ WHERE
3210+ iss_first_response_date IS NULL AND
3211+ iss_id = " . Misc::escapeInteger($issue_id);
3212+ DB_Helper::getInstance()->query($stmt);
3213+ }
3214+
3215+ return true;
3216+ }
3217+ }
3218+
3219+
3220+ /**
3221+ * Method used to check whether a given issue has duplicates
3222+ * or not.
3223+ *
3224+ * @access public
3225+ * @param integer $issue_id The issue ID
3226+ * @return boolean
3227+ */
3228+ function hasDuplicates($issue_id)
3229+ {
3230+ $stmt = "SELECT
3231+ COUNT(iss_id)
3232+ FROM
3233+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3234+ WHERE
3235+ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
3236+ $res = DB_Helper::getInstance()->getOne($stmt);
3237+ if (PEAR::isError($res)) {
3238+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3239+ return false;
3240+ } else {
3241+ if ($res == 0) {
3242+ return false;
3243+ } else {
3244+ return true;
3245+ }
3246+ }
3247+ }
3248+
3249+
3250+ /**
3251+ * Method used to update the duplicated issues for a given
3252+ * issue ID.
3253+ *
3254+ * @access public
3255+ * @param integer $issue_id The issue ID
3256+ * @return integer 1 if the update worked, -1 otherwise
3257+ */
3258+ function updateDuplicates($issue_id)
3259+ {
3260+ $issue_id = Misc::escapeInteger($issue_id);
3261+
3262+ $ids = self::getDuplicateList($issue_id);
3263+ if ($ids == '') {
3264+ return -1;
3265+ }
3266+ $ids = @array_keys($ids);
3267+ $stmt = "UPDATE
3268+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3269+ SET
3270+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3271+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3272+ iss_last_internal_action_type='updated',
3273+ iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
3274+ if (@$_POST["keep"] == "no") {
3275+ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
3276+ }
3277+ $stmt .= "
3278+ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
3279+ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
3280+ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
3281+ WHERE
3282+ iss_id IN (" . implode(", ", $ids) . ")";
3283+ $res = DB_Helper::getInstance()->query($stmt);
3284+ if (PEAR::isError($res)) {
3285+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3286+ return -1;
3287+ } else {
3288+ // record the change
3289+ for ($i = 0; $i < count($ids); $i++) {
3290+ History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
3291+ "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
3292+ }
3293+ return 1;
3294+ }
3295+ }
3296+
3297+
3298+ /**
3299+ * Method used to get a list of the duplicate issues for a given
3300+ * issue ID.
3301+ *
3302+ * @access public
3303+ * @param integer $issue_id The issue ID
3304+ * @return array The list of duplicates
3305+ */
3306+ function getDuplicateList($issue_id)
3307+ {
3308+ $res = self::getDuplicateDetailsList($issue_id);
3309+ if (@count($res) == 0) {
3310+ return '';
3311+ } else {
3312+ $list = array();
3313+ for ($i = 0; $i < count($res); $i++) {
3314+ $list[$res[$i]['issue_id']] = $res[$i]['title'];
3315+ }
3316+ return $list;
3317+ }
3318+ }
3319+
3320+
3321+ /**
3322+ * Method used to get a list of the duplicate issues (and their details)
3323+ * for a given issue ID.
3324+ *
3325+ * @access public
3326+ * @param integer $issue_id The issue ID
3327+ * @return array The list of duplicates
3328+ */
3329+ function getDuplicateDetailsList($issue_id)
3330+ {
3331+ static $returns;
3332+
3333+ if (!empty($returns[$issue_id])) {
3334+ return $returns[$issue_id];
3335+ }
3336+
3337+ $stmt = "SELECT
3338+ iss_id issue_id,
3339+ iss_summary title,
3340+ sta_title current_status,
3341+ sta_is_closed is_closed
3342+ FROM
3343+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
3344+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
3345+ WHERE
3346+ iss_sta_id=sta_id AND
3347+ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
3348+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
3349+ if (PEAR::isError($res)) {
3350+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3351+ return array();
3352+ } else {
3353+ $returns[$issue_id] = $res;
3354+ return $res;
3355+ }
3356+ }
3357+
3358+
3359+ /**
3360+ * Method used to clear the duplicate status of an issue.
3361+ *
3362+ * @access public
3363+ * @param integer $issue_id The issue ID
3364+ * @return integer 1 if the update worked, -1 otherwise
3365+ */
3366+ function clearDuplicateStatus($issue_id)
3367+ {
3368+ $issue_id = Misc::escapeInteger($issue_id);
3369+ $stmt = "UPDATE
3370+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3371+ SET
3372+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3373+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3374+ iss_last_internal_action_type='updated',
3375+ iss_duplicated_iss_id=NULL
3376+ WHERE
3377+ iss_id=$issue_id";
3378+ $res = DB_Helper::getInstance()->query($stmt);
3379+ if (PEAR::isError($res)) {
3380+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3381+ return -1;
3382+ } else {
3383+ // record the change
3384+ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
3385+ return 1;
3386+ }
3387+ }
3388+
3389+
3390+ /**
3391+ * Method used to mark an issue as a duplicate of an existing one.
3392+ *
3393+ * @access public
3394+ * @param integer $issue_id The issue ID
3395+ * @return integer 1 if the update worked, -1 otherwise
3396+ */
3397+ function markAsDuplicate($issue_id)
3398+ {
3399+ $issue_id = Misc::escapeInteger($issue_id);
3400+ if (!self::exists($issue_id)) {
3401+ return -1;
3402+ }
3403+
3404+ $stmt = "UPDATE
3405+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3406+ SET
3407+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3408+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3409+ iss_last_internal_action_type='updated',
3410+ iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
3411+ WHERE
3412+ iss_id=$issue_id";
3413+ $res = DB_Helper::getInstance()->query($stmt);
3414+ if (PEAR::isError($res)) {
3415+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3416+ return -1;
3417+ } else {
3418+ if (!empty($_POST["comments"])) {
3419+ // add note with the comments of marking an issue as a duplicate of another one
3420+ $_POST['title'] = 'Issue duplication comments';
3421+ $_POST["note"] = $_POST["comments"];
3422+ Note::insert(Auth::getUserID(), $issue_id);
3423+ }
3424+ // record the change
3425+ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
3426+ "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
3427+ return 1;
3428+ }
3429+ }
3430+
3431+
3432+ function isDuplicate($issue_id)
3433+ {
3434+ $sql = "SELECT
3435+ count(iss_id)
3436+ FROM
3437+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3438+ WHERE
3439+ iss_id = " . Misc::escapeInteger($issue_id) . " AND
3440+ iss_duplicated_iss_id IS NULL";
3441+ $res = DB_Helper::getInstance()->getOne($sql);
3442+ if (PEAR::isError($res)) {
3443+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3444+ return false;
3445+ }
3446+ if ($res > 0) {
3447+ return false;
3448+ } else {
3449+ return true;
3450+ }
3451+ }
3452+
3453+
3454+ /**
3455+ * Method used to get an associative array of user ID => user
3456+ * status associated with a given issue ID.
3457+ *
3458+ * @access public
3459+ * @param integer $issue_id The issue ID
3460+ * @return array The list of users
3461+ */
3462+ function getAssignedUsersStatus($issue_id)
3463+ {
3464+ $stmt = "SELECT
3465+ usr_id,
3466+ usr_status
3467+ FROM
3468+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
3469+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
3470+ WHERE
3471+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
3472+ isu_usr_id=usr_id";
3473+ $res = DB_Helper::getInstance()->getAssoc($stmt);
3474+ if (PEAR::isError($res)) {
3475+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3476+ return array();
3477+ } else {
3478+ return $res;
3479+ }
3480+ }
3481+
3482+
3483+ /**
3484+ * Method used to get the summary associated with a given issue ID.
3485+ *
3486+ * @access public
3487+ * @param integer $issue_id The issue ID
3488+ * @return string The issue summary
3489+ */
3490+ function getTitle($issue_id)
3491+ {
3492+ $stmt = "SELECT
3493+ iss_summary
3494+ FROM
3495+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3496+ WHERE
3497+ iss_id=" . Misc::escapeInteger($issue_id);
3498+ $res = DB_Helper::getInstance()->getOne($stmt);
3499+ if (PEAR::isError($res)) {
3500+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3501+ return "";
3502+ } else {
3503+ return $res;
3504+ }
3505+ }
3506+
3507+
3508+ /**
3509+ * Method used to get the issue ID associated with a specific summary.
3510+ *
3511+ * @access public
3512+ * @param string $summary The summary to look for
3513+ * @return integer The issue ID
3514+ */
3515+ function getIssueID($summary)
3516+ {
3517+ $stmt = "SELECT
3518+ iss_id
3519+ FROM
3520+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3521+ WHERE
3522+ iss_summary='" . Misc::escapeString($summary) . "'";
3523+ $res = DB_Helper::getInstance()->getOne($stmt);
3524+ if (PEAR::isError($res)) {
3525+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3526+ return 0;
3527+ } else {
3528+ if (empty($res)) {
3529+ return 0;
3530+ } else {
3531+ return $res;
3532+ }
3533+ }
3534+ }
3535+
3536+
3537+ /**
3538+ * Method used to add a new anonymous based issue in the system.
3539+ *
3540+ * @access public
3541+ * @return integer The new issue ID
3542+ */
3543+ function addAnonymousReport()
3544+ {
3545+ $options = Project::getAnonymousPostOptions($_POST["project"]);
3546+ $initial_status = Project::getInitialStatus($_POST["project"]);
3547+ $stmt = "INSERT INTO
3548+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3549+ (
3550+ iss_prj_id,
3551+ iss_prc_id,
3552+ iss_pre_id,
3553+ iss_pri_id,
3554+ iss_usr_id,";
3555+ if (!empty($initial_status)) {
3556+ $stmt .= "iss_sta_id,";
3557+ }
3558+ $stmt .= "
3559+ iss_created_date,
3560+ iss_last_public_action_date,
3561+ iss_last_public_action_type,
3562+ iss_summary,
3563+ iss_description,
3564+ iss_root_message_id
3565+ ) VALUES (
3566+ " . Misc::escapeInteger($_POST["project"]) . ",
3567+ " . $options["category"] . ",
3568+ 0,
3569+ " . $options["priority"] . ",
3570+ " . $options["reporter"] . ",";
3571+ if (!empty($initial_status)) {
3572+ $stmt .= "$initial_status,";
3573+ }
3574+ $stmt .= "
3575+ '" . Date_Helper::getCurrentDateGMT() . "',
3576+ '" . Date_Helper::getCurrentDateGMT() . "',
3577+ 'created',
3578+ '" . Misc::escapeString($_POST["summary"]) . "',
3579+ '" . Misc::escapeString($_POST["description"]) . "',
3580+ '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
3581+ )";
3582+ $res = DB_Helper::getInstance()->query($stmt);
3583+ if (PEAR::isError($res)) {
3584+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3585+ return $res;
3586+ } else {
3587+ $new_issue_id = DB_Helper::get_last_insert_id();
3588+ // log the creation of the issue
3589+ History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
3590+
3591+ // now process any files being uploaded
3592+ $found = 0;
3593+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
3594+ if (!@empty($_FILES["file"]["name"][$i])) {
3595+ $found = 1;
3596+ break;
3597+ }
3598+ }
3599+ if ($found) {
3600+ $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
3601+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
3602+ $filename = @$_FILES["file"]["name"][$i];
3603+ if (empty($filename)) {
3604+ continue;
3605+ }
3606+ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
3607+ if (!empty($blob)) {
3608+ Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
3609+ }
3610+ }
3611+ }
3612+ // need to process any custom fields ?
3613+ if (@count($_POST["custom_fields"]) > 0) {
3614+ foreach ($_POST["custom_fields"] as $fld_id => $value) {
3615+ Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
3616+ }
3617+ }
3618+
3619+ // now add the user/issue association
3620+ $assign = array();
3621+ $users = @$options["users"];
3622+ $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
3623+ for ($i = 0; $i < count($users); $i++) {
3624+ Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
3625+ self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
3626+ $assign[] = $users[$i];
3627+ }
3628+
3629+ Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
3630+
3631+ // also notify any users that want to receive emails anytime a new issue is created
3632+ Notification::notifyNewIssue($_POST['project'], $new_issue_id);
3633+
3634+ return $new_issue_id;
3635+ }
3636+ }
3637+
3638+
3639+ /**
3640+ * Method used to remove all issues associated with a specific list of
3641+ * projects.
3642+ *
3643+ * @access public
3644+ * @param array $ids The list of projects to look for
3645+ * @return boolean
3646+ */
3647+ function removeByProjects($ids)
3648+ {
3649+ $items = @implode(", ", Misc::escapeInteger($ids));
3650+ $stmt = "SELECT
3651+ iss_id
3652+ FROM
3653+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3654+ WHERE
3655+ iss_prj_id IN ($items)";
3656+ $res = DB_Helper::getInstance()->getCol($stmt);
3657+ if (PEAR::isError($res)) {
3658+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3659+ return false;
3660+ } else {
3661+ if (count($res) > 0) {
3662+ self::deleteAssociations($res);
3663+ Attachment::removeByIssues($res);
3664+ SCM::removeByIssues($res);
3665+ Impact_Analysis::removeByIssues($res);
3666+ self::deleteUserAssociations($res);
3667+ Note::removeByIssues($res);
3668+ Time_Tracking::removeByIssues($res);
3669+ Notification::removeByIssues($res);
3670+ Custom_Field::removeByIssues($res);
3671+ Phone_Support::removeByIssues($res);
3672+ History::removeByIssues($res);
3673+ // now really delete the issues
3674+ $items = implode(", ", $res);
3675+ $stmt = "DELETE FROM
3676+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3677+ WHERE
3678+ iss_id IN ($items)";
3679+ DB_Helper::getInstance()->query($stmt);
3680+ }
3681+ return true;
3682+ }
3683+ }
3684+
3685+
3686+ /**
3687+ * Method used to close off an issue.
3688+ *
3689+ * @access public
3690+ * @param integer $usr_id The user ID
3691+ * @param integer $issue_id The issue ID
3692+ * @param bool $send_notification Whether to send a notification about this action or not
3693+ * @param integer $resolution_id The resolution ID
3694+ * @param integer $status_id The status ID
3695+ * @param string $reason The reason for closing this issue
3696+ * @param string $send_notification_to Who this notification should be sent too
3697+ * @return integer 1 if the update worked, -1 otherwise
3698+ */
3699+ function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
3700+ {
3701+ $usr_id = Misc::escapeInteger($usr_id);
3702+ $issue_id = Misc::escapeInteger($issue_id);
3703+ $resolution_id = Misc::escapeInteger($resolution_id);
3704+ $status_id = Misc::escapeInteger($status_id);
3705+
3706+ $stmt = "UPDATE
3707+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3708+ SET
3709+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3710+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3711+ iss_last_public_action_type='closed',
3712+ iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
3713+ if (!empty($resolution_id)) {
3714+ $stmt .= "iss_res_id=$resolution_id,\n";
3715+ }
3716+ $stmt .= "iss_sta_id=$status_id
3717+ WHERE
3718+ iss_id=$issue_id";
3719+ $res = DB_Helper::getInstance()->query($stmt);
3720+ if (PEAR::isError($res)) {
3721+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3722+ return -1;
3723+ } else {
3724+ $prj_id = self::getProjectID($issue_id);
3725+
3726+ // record the change
3727+ History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
3728+
3729+ if ($send_notification_to == 'all') {
3730+
3731+ $from = User::getFromHeader($usr_id);
3732+ $message_id = User::getFromHeader($usr_id);
3733+ $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
3734+ '', '', 'Issue closed comments', $reason, '');
3735+
3736+ $structure = Mime_Helper::decode($full_email, true, false);
3737+
3738+ $email = array(
3739+ 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
3740+ 'issue_id' => $issue_id,
3741+ 'message_id' => $message_id,
3742+ 'date' => Date_Helper::getCurrentDateGMT(),
3743+ 'subject' => 'Issue closed comments',
3744+ 'from' => $from,
3745+ 'has_attachment'=> 0,
3746+ 'body' => $reason,
3747+ 'full_email' => $full_email,
3748+ 'headers' => $structure->headers
3749+ );
3750+ Support::insertEmail($email, $structure, $sup_id, true);
3751+ $ids = $sup_id;
3752+ } else {
3753+ // add note with the reason to close the issue
3754+ $_POST['title'] = 'Issue closed comments';
3755+ $_POST["note"] = $reason;
3756+ Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
3757+ $ids = false;
3758+ }
3759+
3760+ if ($send_notification) {
3761+ if (Customer::hasCustomerIntegration($prj_id)) {
3762+ // send a special confirmation email when customer issues are closed
3763+ $stmt = "SELECT
3764+ iss_customer_contact_id
3765+ FROM
3766+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3767+ WHERE
3768+ iss_id=$issue_id";
3769+ $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
3770+ if (!empty($customer_contact_id)) {
3771+ Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
3772+ }
3773+ }
3774+ // send notifications for the issue being closed
3775+ Notification::notify($issue_id, 'closed', $ids);
3776+ }
3777+ Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
3778+ return 1;
3779+ }
3780+ }
3781+
3782+
3783+ /**
3784+ * Method used to update the details of a specific issue.
3785+ *
3786+ * @access public
3787+ * @param integer $issue_id The issue ID
3788+ * @return integer 1 if the update worked, -1 or -2 otherwise
3789+ */
3790+ function update($issue_id)
3791+ {
3792+ global $errors;
3793+ $errors = array();
3794+
3795+ $issue_id = Misc::escapeInteger($issue_id);
3796+
3797+ $usr_id = Auth::getUserID();
3798+ $prj_id = self::getProjectID($issue_id);
3799+
3800+ $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
3801+ if ($workflow !== true) {
3802+ return $workflow;
3803+ }
3804+
3805+ // get all of the 'current' information of this issue
3806+ $current = self::getDetails($issue_id);
3807+ // update the issue associations
3808+ if (empty($_POST['associated_issues'])) {
3809+ $associated_issues = array();
3810+ } else {
3811+ $associated_issues = explode(',', @$_POST['associated_issues']);
3812+ // make sure all associated issues are valid (and in this project)
3813+ for ($i = 0; $i < count($associated_issues); $i++) {
3814+ if (!self::exists(trim($associated_issues[$i]), false)) {
3815+ $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
3816+ unset($associated_issues[$i]);
3817+ }
3818+ }
3819+ }
3820+ $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
3821+ if (count($association_diff) > 0) {
3822+ // go through the new assocations, if association already exists, skip it
3823+ $associations_to_remove = $current['associated_issues'];
3824+ if (count($associated_issues) > 0) {
3825+ foreach ($associated_issues as $index => $associated_id) {
3826+ if (!in_array($associated_id, $current['associated_issues'])) {
3827+ self::addAssociation($issue_id, $associated_id, $usr_id);
3828+ } else {
3829+ // already assigned, remove this user from list of users to remove
3830+ unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
3831+ }
3832+ }
3833+ }
3834+ if (count($associations_to_remove) > 0) {
3835+ foreach ($associations_to_remove as $associated_id) {
3836+ self::deleteAssociation($issue_id, $associated_id);
3837+ }
3838+ }
3839+ }
3840+ $assignments_changed = false;
3841+ if (@$_POST["keep_assignments"] == "no") {
3842+ // only change the issue-user associations if there really were any changes
3843+ $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
3844+ if (!empty($_POST['assignments'])) {
3845+ $new_assignees = @$_POST['assignments'];
3846+ } else {
3847+ $new_assignees = array();
3848+ }
3849+ $assignment_notifications = array();
3850+
3851+ // remove people from the assignment list, if appropriate
3852+ foreach ($old_assignees as $assignee) {
3853+ if (!in_array($assignee, $new_assignees)) {
3854+ self::deleteUserAssociation($issue_id, $assignee);
3855+ $assignments_changed = true;
3856+ }
3857+ }
3858+ // add people to the assignment list, if appropriate
3859+ foreach ($new_assignees as $assignee) {
3860+ if (!in_array($assignee, $old_assignees)) {
3861+ self::addUserAssociation($usr_id, $issue_id, $assignee);
3862+ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
3863+ $assignment_notifications[] = $assignee;
3864+ $assignments_changed = true;
3865+ }
3866+ }
3867+ if (count($assignment_notifications) > 0) {
3868+ Notification::notifyNewAssignment($assignment_notifications, $issue_id);
3869+ }
3870+ }
3871+ if (empty($_POST["estimated_dev_time"])) {
3872+ $_POST["estimated_dev_time"] = 0;
3873+ }
3874+ $stmt = "UPDATE
3875+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
3876+ SET
3877+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
3878+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
3879+ iss_last_public_action_type='updated',";
3880+ if (!empty($_POST["category"])) {
3881+ $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
3882+ }
3883+ if (@$_POST["keep"] == "no") {
3884+ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
3885+ }
3886+ if (!empty($_POST['expected_resolution_date'])) {
3887+ $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
3888+ } else {
3889+ $stmt .= "iss_expected_resolution_date=null,";
3890+ }
3891+ $stmt .= "
3892+ iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
3893+ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
3894+ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
3895+ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
3896+ iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
3897+ iss_description='" . Misc::escapeString($_POST["description"]) . "',
3898+ iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
3899+ iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
3900+ iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
3901+ iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
3902+ if (isset($_POST['private'])) {
3903+ $stmt .= ",
3904+ iss_private = " . Misc::escapeInteger($_POST['private']);
3905+ }
3906+ $stmt .= "
3907+ WHERE
3908+ iss_id=$issue_id";
3909+ $res = DB_Helper::getInstance()->query($stmt);
3910+ if (PEAR::isError($res)) {
3911+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
3912+ return -1;
3913+ } else {
3914+ // add change to the history (only for changes on specific fields?)
3915+ $updated_fields = array();
3916+ if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
3917+ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
3918+ }
3919+ if ($current["iss_prc_id"] != $_POST["category"]) {
3920+ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
3921+ }
3922+ if ($current["iss_pre_id"] != $_POST["release"]) {
3923+ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
3924+ }
3925+ if ($current["iss_pri_id"] != $_POST["priority"]) {
3926+ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
3927+ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
3928+ }
3929+ if ($current["iss_sta_id"] != $_POST["status"]) {
3930+ // clear out the last-triggered-reminder flag when changing the status of an issue
3931+ Reminder_Action::clearLastTriggered($issue_id);
3932+
3933+ // if old status was closed and new status is not, clear closed data from issue.
3934+ $old_status_details = Status::getDetails($current['iss_sta_id']);
3935+ if ($old_status_details['sta_is_closed'] == 1) {
3936+ $new_status_details = Status::getDetails($_POST["status"]);
3937+ if ($new_status_details['sta_is_closed'] != 1) {
3938+ self::clearClosed($issue_id);
3939+ }
3940+ }
3941+ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
3942+ }
3943+ if ($current["iss_res_id"] != $_POST["resolution"]) {
3944+ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
3945+ }
3946+ if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
3947+ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
3948+ }
3949+ if ($current["iss_summary"] != $_POST["summary"]) {
3950+ $updated_fields["Summary"] = '';
3951+ }
3952+ if ($current["iss_description"] != $_POST["description"]) {
3953+ $updated_fields["Description"] = '';
3954+ }
3955+ if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
3956+ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
3957+ }
3958+ if (count($updated_fields) > 0) {
3959+ // log the changes
3960+ $changes = '';
3961+ $i = 0;
3962+ foreach ($updated_fields as $key => $value) {
3963+ if ($i > 0) {
3964+ $changes .= "; ";
3965+ }
3966+ if (($key != "Summary") && ($key != "Description")) {
3967+ $changes .= "$key: $value";
3968+ } else {
3969+ $changes .= "$key";
3970+ }
3971+ $i++;
3972+ }
3973+ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
3974+ // send notifications for the issue being updated
3975+ Notification::notifyIssueUpdated($issue_id, $current, $_POST);
3976+ }
3977+
3978+ // record group change as a seperate change
3979+ if ($current["iss_grp_id"] != (int)$_POST["group"]) {
3980+ History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
3981+ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
3982+ }
3983+
3984+ // now update any duplicates, if any
3985+ $update_dupe = array(
3986+ 'Category',
3987+ 'Release',
3988+ 'Priority',
3989+ 'Release',
3990+ 'Resolution'
3991+ );
3992+ // COMPAT: the following line requires PHP > 4.0.4
3993+ $intersect = array_intersect($update_dupe, array_keys($updated_fields));
3994+ if (($current["duplicates"] != '') && (count($intersect) > 0)) {
3995+ self::updateDuplicates($issue_id);
3996+ }
3997+
3998+ // if there is customer integration, mark last customer action
3999+ if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
4000+ self::recordLastCustomerAction($issue_id);
4001+ }
4002+
4003+ if ($assignments_changed) {
4004+ // XXX: we may want to also send the email notification for those "new" assignees
4005+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
4006+ }
4007+
4008+ Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
4009+ // Move issue to another project
4010+ if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
4011+ $new_prj_id = (int)@$_POST['new_prj'];
4012+ if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
4013+ if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
4014+ $res = self::moveIssue($issue_id, $new_prj_id);
4015+ if ($res == -1) {
4016+ return $res;
4017+ }
4018+ } else {
4019+ return -1;
4020+ }
4021+ }
4022+ }
4023+ return 1;
4024+ }
4025+ }
4026+
4027+
4028+ /**
4029+ * Move the issue to a new project
4030+ *
4031+ * @param integer $issue_id
4032+ * @param integer $new_prj_id
4033+ * @return integer 1 on success, -1 otherwise
4034+ */
4035+ function moveIssue($issue_id, $new_prj_id)
4036+ {
4037+ $stmt = "UPDATE
4038+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
4039+ SET
4040+ iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
4041+ WHERE
4042+ iss_id = " . Misc::escapeInteger($issue_id);
4043+ $res = DB_Helper::getInstance()->query($stmt);
4044+ if (PEAR::isError($res)) {
4045+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4046+ return -1;
4047+ } else {
4048+ $currentDetails = self::getDetails($issue_id);
4049+
4050+ // set new category
4051+ $new_iss_prc_list = Category::getAssocList($new_prj_id);
4052+ $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
4053+ $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
4054+ if ($new_prc_id === false) {
4055+ // use the first category listed in the new project
4056+ $new_prc_id = key($new_iss_prc_list);
4057+ }
4058+
4059+ // set new priority
4060+ $new_iss_pri_list = Priority::getAssocList($new_prj_id);
4061+ $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
4062+ $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
4063+ if ($new_pri_id === false) {
4064+ // use the first category listed in the new project
4065+ $new_pri_id = key($new_iss_pri_list);
4066+ }
4067+
4068+ // XXX: Set status if needed when moving issue
4069+
4070+ $stmt = "UPDATE
4071+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
4072+ SET
4073+ iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
4074+ iss_pri_id=" . $new_pri_id . "
4075+ WHERE
4076+ iss_id=$issue_id";
4077+ $res = DB_Helper::getInstance()->query($stmt);
4078+ if (PEAR::isError($res)) {
4079+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4080+ }
4081+
4082+ // clear project cache
4083+ self::getProjectID($issue_id, true);
4084+
4085+ Notification::notifyNewIssue($new_prj_id, $issue_id);
4086+ }
4087+ }
4088+
4089+
4090+ /**
4091+ * Method used to associate an existing issue with another one.
4092+ *
4093+ * @access public
4094+ * @param integer $issue_id The issue ID
4095+ * @param integer $issue_id The other issue ID
4096+ * @return void
4097+ */
4098+ function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
4099+ {
4100+ $issue_id = Misc::escapeInteger($issue_id);
4101+ $associated_id = Misc::escapeInteger($associated_id);
4102+
4103+ $stmt = "INSERT INTO
4104+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
4105+ (
4106+ isa_issue_id,
4107+ isa_associated_id
4108+ ) VALUES (
4109+ $issue_id,
4110+ $associated_id
4111+ )";
4112+ DB_Helper::getInstance()->query($stmt);
4113+ History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
4114+ // link the associated issue back to this one
4115+ if ($link_issues) {
4116+ self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
4117+ }
4118+ }
4119+
4120+
4121+ /**
4122+ * Method used to remove the issue associations related to a specific issue.
4123+ *
4124+ * @access public
4125+ * @param integer $issue_id The issue ID
4126+ * @return void
4127+ */
4128+ function deleteAssociations($issue_id, $usr_id = FALSE)
4129+ {
4130+ $issue_id = Misc::escapeInteger($issue_id);
4131+ if (is_array($issue_id)) {
4132+ $issue_id = implode(", ", $issue_id);
4133+ }
4134+ $stmt = "DELETE FROM
4135+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
4136+ WHERE
4137+ isa_issue_id IN ($issue_id) OR
4138+ isa_associated_id IN ($issue_id)";
4139+ DB_Helper::getInstance()->query($stmt);
4140+ if ($usr_id) {
4141+ History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
4142+ }
4143+ }
4144+
4145+
4146+ /**
4147+ * Method used to remove a issue association from an issue.
4148+ *
4149+ * @access public
4150+ * @param integer $issue_id The issue ID
4151+ * @param integer $associated_id The associated issue ID to remove.
4152+ * @return void
4153+ */
4154+ function deleteAssociation($issue_id, $associated_id)
4155+ {
4156+ $issue_id = Misc::escapeInteger($issue_id);
4157+ $associated_id = Misc::escapeInteger($associated_id);
4158+ $stmt = "DELETE FROM
4159+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
4160+ WHERE
4161+ (
4162+ isa_issue_id = $issue_id AND
4163+ isa_associated_id = $associated_id
4164+ ) OR
4165+ (
4166+ isa_issue_id = $associated_id AND
4167+ isa_associated_id = $issue_id
4168+ )";
4169+ DB_Helper::getInstance()->query($stmt);
4170+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
4171+ "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
4172+ History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
4173+ "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
4174+ }
4175+
4176+
4177+ /**
4178+ * Method used to assign an issue with an user.
4179+ *
4180+ * @access public
4181+ * @param integer $usr_id The user ID of the person performing this change
4182+ * @param integer $issue_id The issue ID
4183+ * @param integer $assignee_usr_id The user ID of the assignee
4184+ * @param boolean $add_history Whether to add a history entry about this or not
4185+ * @return integer 1 if the update worked, -1 otherwise
4186+ */
4187+ function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
4188+ {
4189+ $issue_id = Misc::escapeInteger($issue_id);
4190+ $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
4191+ $stmt = "INSERT INTO
4192+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4193+ (
4194+ isu_iss_id,
4195+ isu_usr_id,
4196+ isu_assigned_date
4197+ ) VALUES (
4198+ $issue_id,
4199+ $assignee_usr_id,
4200+ '" . Date_Helper::getCurrentDateGMT() . "'
4201+ )";
4202+ $res = DB_Helper::getInstance()->query($stmt);
4203+ if (PEAR::isError($res)) {
4204+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4205+ return -1;
4206+ } else {
4207+ if ($add_history) {
4208+ History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
4209+ 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
4210+ }
4211+ return 1;
4212+ }
4213+ }
4214+
4215+
4216+ /**
4217+ * Method used to delete all user assignments for a specific issue.
4218+ *
4219+ * @access public
4220+ * @param integer $issue_id The issue ID
4221+ * @param integer $usr_id The user ID of the person performing the change
4222+ * @return void
4223+ */
4224+ function deleteUserAssociations($issue_id, $usr_id = FALSE)
4225+ {
4226+ $issue_id = Misc::escapeInteger($issue_id);
4227+ if (is_array($issue_id)) {
4228+ $issue_id = implode(", ", $issue_id);
4229+ }
4230+ $stmt = "DELETE FROM
4231+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4232+ WHERE
4233+ isu_iss_id IN ($issue_id)";
4234+ $res = DB_Helper::getInstance()->query($stmt);
4235+ if (PEAR::isError($res)) {
4236+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4237+ return -1;
4238+ } else {
4239+ if ($usr_id) {
4240+ History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
4241+ }
4242+ return 1;
4243+ }
4244+ }
4245+
4246+
4247+ /**
4248+ * Method used to delete a single user assignments for a specific issue.
4249+ *
4250+ * @access public
4251+ * @param integer $issue_id The issue ID
4252+ * @param integer $usr_id The user to remove.
4253+ * @param boolean $add_history Whether to add a history entry about this or not
4254+ * @return void
4255+ */
4256+ function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
4257+ {
4258+ $issue_id = Misc::escapeInteger($issue_id);
4259+ $usr_id = Misc::escapeInteger($usr_id);
4260+ $stmt = "DELETE FROM
4261+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4262+ WHERE
4263+ isu_iss_id = $issue_id AND
4264+ isu_usr_id = $usr_id";
4265+ $res = DB_Helper::getInstance()->query($stmt);
4266+ if (PEAR::isError($res)) {
4267+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4268+ return -1;
4269+ } else {
4270+ if ($add_history) {
4271+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
4272+ User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
4273+ }
4274+ return 1;
4275+ }
4276+ }
4277+
4278+
4279+ /**
4280+ * Creates an issue with the given email information.
4281+ *
4282+ * @access public
4283+ * @param integer $prj_id The project ID
4284+ * @param integer $usr_id The user responsible for this action
4285+ * @param string $sender The original sender of this email
4286+ * @param string $summary The issue summary
4287+ * @param string $description The issue description
4288+ * @param integer $category The category ID
4289+ * @param integer $priority The priority ID
4290+ * @param array $assignment The list of users to assign this issue to
4291+ * @param string $date The date the email was originally sent.
4292+ * @param string $msg_id The message ID of the email we are creating this issue from.
4293+ * @return void
4294+ */
4295+ function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
4296+ {
4297+ $data = array();
4298+ $exclude_list = array();
4299+
4300+ $sender_email = Mail_Helper::getEmailAddress($sender);
4301+ $sender_usr_id = User::getUserIDByEmail($sender_email, true);
4302+ if (!empty($sender_usr_id)) {
4303+ $reporter = $sender_usr_id;
4304+ $exclude_list[] = $sender_usr_id;
4305+ }
4306+
4307+ $data = array(
4308+ 'category' => $category,
4309+ 'priority' => $priority,
4310+ 'description' => $description,
4311+ 'summary' => $summary,
4312+ 'msg_id' => $msg_id,
4313+ );
4314+
4315+ if (Customer::hasCustomerIntegration($prj_id)) {
4316+ list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
4317+ if (!empty($customer_id)) {
4318+ $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
4319+ // overwrite the reporter with the customer contact
4320+ $reporter = User::getUserIDByContactID($customer_contact_id);
4321+ $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
4322+
4323+ $data['customer'] = $customer_id;
4324+ $data['contact'] = $customer_contact_id;
4325+# $data['contract'] = // XXX missing
4326+ $data['contact_person_lname'] = $contact['last_name'];
4327+ $data['contact_person_fname'] = $contact['first_name'];
4328+ $data['contact_email'] = $sender_email;
4329+ $data['contact_phone'] = $contact['phone'];
4330+ $data['contact_timezone'] = $contact_timezone;
4331+ }
4332+ } else {
4333+ $customer_id = FALSE;
4334+ }
4335+ if (empty($reporter)) {
4336+ $reporter = APP_SYSTEM_USER_ID;
4337+ }
4338+
4339+ $data['reporter'] = $reporter;
4340+
4341+ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
4342+ if ($issue_id == -1) {
4343+ return -1;
4344+ }
4345+
4346+ $has_TAM = false;
4347+ $has_RR = false;
4348+ // log the creation of the issue
4349+ History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
4350+
4351+ $emails = array();
4352+ $manager_usr_ids = array();
4353+ if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
4354+ // if there are any technical account managers associated with this customer, add these users to the notification list
4355+ $managers = Customer::getAccountManagers($prj_id, $customer_id);
4356+ $manager_usr_ids = array_keys($managers);
4357+ $manager_emails = array_values($managers);
4358+ $emails = array_merge($emails, $manager_emails);
4359+ }
4360+ // add the reporter to the notification list
4361+ $emails[] = $sender;
4362+ $emails = array_unique($emails);
4363+ $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
4364+ foreach ($emails as $address) {
4365+ Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
4366+ }
4367+
4368+ // only assign the issue to an user if the associated customer has any technical account managers
4369+ $users = array();
4370+ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
4371+ foreach ($manager_usr_ids as $manager_usr_id) {
4372+ $users[] = $manager_usr_id;
4373+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
4374+ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
4375+ }
4376+ $has_TAM = true;
4377+ }
4378+ // now add the user/issue association
4379+ if (@count($assignment) > 0) {
4380+ for ($i = 0; $i < count($assignment); $i++) {
4381+ Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
4382+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
4383+ if ($assignment[$i] != $usr_id) {
4384+ $users[] = $assignment[$i];
4385+ }
4386+ }
4387+ } else {
4388+ // only use the round-robin feature if this new issue was not
4389+ // already assigned to a customer account manager
4390+ if (@count($manager_usr_ids) < 1) {
4391+ $assignee = Round_Robin::getNextAssignee($prj_id);
4392+ // assign the issue to the round robin person
4393+ if (!empty($assignee)) {
4394+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
4395+ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
4396+ $users[] = $assignee;
4397+ $has_RR = true;
4398+ }
4399+ }
4400+ }
4401+ if (count($users) > 0) {
4402+ $has_assignee = true;
4403+ }
4404+
4405+ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
4406+
4407+ // send special 'an issue was auto-created for you' notification back to the sender
4408+ Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
4409+
4410+ // also notify any users that want to receive emails anytime a new issue is created
4411+ Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
4412+
4413+ return $issue_id;
4414+ }
4415+
4416+
4417+ /**
4418+ * Return errors that happened when creating new issue from POST method.
4419+ *
4420+ * @return array
4421+ */
4422+ private static $insert_errors = array();
4423+ static function getInsertErrors() {
4424+ return self::$insert_errors;
4425+ }
4426+
4427+ /**
4428+ * Method used to add a new issue using the normal report form.
4429+ *
4430+ * @access public
4431+ * @return integer The new issue ID
4432+ */
4433+ function createFromPost()
4434+ {
4435+ $keys = array(
4436+ 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
4437+ 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
4438+ 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
4439+ );
4440+ $data = array();
4441+ foreach ($keys as $key) {
4442+ if (isset($_POST[$key])) {
4443+ $data[$key] = $_POST[$key];
4444+ }
4445+ }
4446+
4447+ $prj_id = Auth::getCurrentProject();
4448+ $usr_id = Auth::getUserID();
4449+
4450+ // if we are creating an issue for a customer, put the
4451+ // main customer contact as the reporter for it
4452+ if (Customer::hasCustomerIntegration($prj_id)) {
4453+ $contact_usr_id = User::getUserIDByContactID($data['contact']);
4454+ if (empty($contact_usr_id)) {
4455+ $contact_usr_id = $usr_id;
4456+ }
4457+ $data['reporter'] = $contact_usr_id;
4458+ } else {
4459+ $data['reporter'] = $usr_id;
4460+ }
4461+
4462+ $data['msg_id'] = Mail_Helper::generateMessageID();
4463+
4464+ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
4465+ if ($issue_id == -1) {
4466+ return -1;
4467+ }
4468+
4469+ $has_TAM = false;
4470+ $has_RR = false;
4471+ $info = User::getNameEmail($usr_id);
4472+ // log the creation of the issue
4473+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
4474+
4475+ $emails = array();
4476+ if (Customer::hasCustomerIntegration($prj_id)) {
4477+ if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
4478+ $emails = $data['contact_extra_emails'];
4479+ }
4480+ // add the primary contact to the notification list
4481+ if ($data['add_primary_contact'] == 'yes') {
4482+ $contact_email = User::getEmailByContactID($data['contact']);
4483+ if (!empty($contact_email)) {
4484+ $emails[] = $contact_email;
4485+ }
4486+ }
4487+ // if there are any technical account managers associated with this customer, add these users to the notification list
4488+ $managers = Customer::getAccountManagers($prj_id, $data['customer']);
4489+ $manager_usr_ids = array_keys($managers);
4490+ $manager_emails = array_values($managers);
4491+ $emails = array_merge($emails, $manager_emails);
4492+ }
4493+ // add the reporter to the notification list
4494+ $emails[] = $info['usr_email'];
4495+ $emails = array_unique($emails);
4496+ foreach ($emails as $address) {
4497+ Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
4498+ }
4499+
4500+ // only assign the issue to an user if the associated customer has any technical account managers
4501+ $users = array();
4502+ $has_TAM = false;
4503+ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
4504+ foreach ($manager_usr_ids as $manager_usr_id) {
4505+ $users[] = $manager_usr_id;
4506+ self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
4507+ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
4508+ }
4509+ $has_TAM = true;
4510+ }
4511+ // now add the user/issue association (aka assignments)
4512+ if (!empty($data['users']) && count($data['users']) > 0) {
4513+ for ($i = 0; $i < count($data['users']); $i++) {
4514+ Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
4515+ Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
4516+ self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
4517+ if ($data['users'][$i] != $usr_id) {
4518+ $users[] = $data['users'][$i];
4519+ }
4520+ }
4521+ } else {
4522+ // only use the round-robin feature if this new issue was not
4523+ // already assigned to a customer account manager
4524+ if (@count($manager_usr_ids) < 1) {
4525+ $assignee = Round_Robin::getNextAssignee($prj_id);
4526+ // assign the issue to the round robin person
4527+ if (!empty($assignee)) {
4528+ $users[] = $assignee;
4529+ self::addUserAssociation($usr_id, $issue_id, $assignee, false);
4530+ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
4531+ $has_RR = true;
4532+ }
4533+ }
4534+ }
4535+
4536+ // now process any files being uploaded
4537+ $found = 0;
4538+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
4539+ if (!@empty($_FILES["file"]["name"][$i])) {
4540+ $found = 1;
4541+ break;
4542+ }
4543+ }
4544+ if ($found) {
4545+ $files = array();
4546+ for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
4547+ $filename = @$_FILES["file"]["name"][$i];
4548+ if (empty($filename)) {
4549+ continue;
4550+ }
4551+ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
4552+ if (empty($blob)) {
4553+ // error reading a file
4554+ self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
4555+ continue;
4556+ }
4557+ $files[] = array(
4558+ "filename" => $filename,
4559+ "type" => $_FILES['file']['type'][$i],
4560+ "blob" => $blob
4561+ );
4562+ }
4563+ if (count($files) > 0) {
4564+ $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
4565+ foreach ($files as $file) {
4566+ Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
4567+ }
4568+ }
4569+ }
4570+ // need to associate any emails ?
4571+ if (!empty($data['attached_emails'])) {
4572+ $items = explode(",", $data['attached_emails']);
4573+ Support::associate($usr_id, $issue_id, $items);
4574+ }
4575+ // need to notify any emails being converted into issues ?
4576+ if (@count($data['notify_senders']) > 0) {
4577+ $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
4578+ } else {
4579+ $recipients = array();
4580+ }
4581+ // need to process any custom fields ?
4582+ if (@count($data['custom_fields']) > 0) {
4583+ foreach ($data['custom_fields'] as $fld_id => $value) {
4584+ Custom_Field::associateIssue($issue_id, $fld_id, $value);
4585+ }
4586+ }
4587+ // also send a special confirmation email to the customer contact
4588+ if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
4589+ // also need to pass the list of sender emails already notified,
4590+ // so we can avoid notifying the same person again
4591+ $contact_email = User::getEmailByContactID($data['contact']);
4592+ if (@!in_array($contact_email, $recipients)) {
4593+ Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
4594+ }
4595+ }
4596+
4597+ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
4598+
4599+ // also notify any users that want to receive emails anytime a new issue is created
4600+ Notification::notifyNewIssue($prj_id, $issue_id);
4601+
4602+ return $issue_id;
4603+ }
4604+
4605+ /**
4606+ * Insert issue to database.
4607+ *
4608+ * @param integer $prj_id The project ID
4609+ * @param integer $usr_id The user responsible for this action
4610+ * @param array $data of issue to be inserted
4611+ * @return integer The new issue ID
4612+ */
4613+ private function insertIssue($prj_id, $usr_id, $data)
4614+ {
4615+
4616+ // XXX missing_fields never used
4617+ $missing_fields = array();
4618+ if ($data['category'] == -1) {
4619+ $missing_fields[] = 'Category';
4620+ }
4621+ if ($data['priority'] == -1) {
4622+ $missing_fields[] = 'Priority';
4623+ }
4624+
4625+ // if there is no reporter set, use the system user
4626+ if (empty($data['reporter'])) {
4627+ $data['reporter'] = APP_SYSTEM_USER_ID;
4628+ }
4629+
4630+ if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
4631+ $data['estimated_dev_time'] = 0;
4632+ }
4633+
4634+ // add new issue
4635+ $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
4636+ "SET ".
4637+ "iss_prj_id=" . $prj_id . ",";
4638+ if (!empty($data['group'])) {
4639+ $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
4640+ }
4641+ if (!empty($data['category'])) {
4642+ $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
4643+ }
4644+ if (!empty($data['release'])) {
4645+ $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
4646+ }
4647+ if (!empty($data['priority'])) {
4648+ $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
4649+ }
4650+
4651+ $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
4652+
4653+ $initial_status = Project::getInitialStatus($prj_id);
4654+ if (!empty($initial_status)) {
4655+ $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
4656+ }
4657+
4658+ if (Customer::hasCustomerIntegration($prj_id)) {
4659+ $stmt .= "
4660+ iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
4661+ if (!empty($data['contact'])) {
4662+ $stmt .= "
4663+ iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
4664+ }
4665+ $stmt .= "
4666+ iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
4667+ iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
4668+ iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
4669+ iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
4670+ iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
4671+ iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
4672+ }
4673+
4674+ $stmt .= "
4675+ iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
4676+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
4677+ iss_last_public_action_type='created',
4678+ iss_summary='" . Misc::escapeString($data['summary']) . "',
4679+ iss_description='" . Misc::escapeString($data['description']) . "',
4680+ iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
4681+ if (!empty($data['contact'])) {
4682+ $stmt .= "
4683+ iss_private=" . Misc::escapeInteger($data['private']) . " ,";
4684+ }
4685+ $stmt .= "
4686+ iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
4687+ ";
4688+
4689+ $res = DB_Helper::getInstance()->query($stmt);
4690+ if (PEAR::isError($res)) {
4691+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
4692+ return -1;
4693+ }
4694+
4695+ $issue_id = DB_Helper::get_last_insert_id();
4696+ return $issue_id;
4697+ }
4698+
4699+
4700+ /**
4701+ * Method used to get a specific parameter in the issue listing cookie.
4702+ *
4703+ * @access public
4704+ * @param string $name The name of the parameter
4705+ * @return mixed The value of the specified parameter
4706+ */
4707+ function getParam($name)
4708+ {
4709+ $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
4710+
4711+ if (isset($_GET[$name])) {
4712+ return $_GET[$name];
4713+ } elseif (isset($_POST[$name])) {
4714+ return $_POST[$name];
4715+ } elseif (isset($profile[$name])) {
4716+ return $profile[$name];
4717+ } else {
4718+ return "";
4719+ }
4720+ }
4721+
4722+
4723+ /**
4724+ * Method used to save the current search parameters in a cookie.
4725+ *
4726+ * @access public
4727+ * @return array The search parameters
4728+ */
4729+ function saveSearchParams()
4730+ {
4731+ $sort_by = self::getParam('sort_by');
4732+ $sort_order = self::getParam('sort_order');
4733+ $rows = self::getParam('rows');
4734+ $hide_closed = self::getParam('hide_closed');
4735+ if ($hide_closed === '') {
4736+ $hide_closed = 1;
4737+ }
4738+ $search_type = self::getParam('search_type');
4739+ if (empty($search_type)) {
4740+ $search_type = 'all_text';
4741+ }
4742+ $custom_field = self::getParam('custom_field');
4743+ if (is_string($custom_field)) {
4744+ $custom_field = unserialize(urldecode($custom_field));
4745+ }
4746+ $cookie = array(
4747+ 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
4748+ 'pagerRow' => self::getParam('pagerRow'),
4749+ 'hide_closed' => $hide_closed,
4750+ "sort_by" => $sort_by ? $sort_by : "pri_rank",
4751+ "sort_order" => $sort_order ? $sort_order : "ASC",
4752+ // quick filter form
4753+ 'keywords' => self::getParam('keywords'),
4754+ 'search_type' => $search_type,
4755+ 'users' => self::getParam('users'),
4756+ 'status' => self::getParam('status'),
4757+ 'priority' => self::getParam('priority'),
4758+ 'category' => self::getParam('category'),
4759+ 'customer_email' => self::getParam('customer_email'),
4760+ // advanced search form
4761+ 'show_authorized_issues' => self::getParam('show_authorized_issues'),
4762+ 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
4763+ 'reporter' => self::getParam('reporter'),
4764+ // other fields
4765+ 'release' => self::getParam('release'),
4766+ // custom fields
4767+ 'custom_field' => $custom_field
4768+ );
4769+ // now do some magic to properly format the date fields
4770+ $date_fields = array(
4771+ 'created_date',
4772+ 'updated_date',
4773+ 'last_response_date',
4774+ 'first_response_date',
4775+ 'closed_date'
4776+ );
4777+ foreach ($date_fields as $field_name) {
4778+ $field = self::getParam($field_name);
4779+ if (empty($field)) {
4780+ continue;
4781+ }
4782+ if (@$field['filter_type'] == 'in_past') {
4783+ @$cookie[$field_name] = array(
4784+ 'filter_type' => 'in_past',
4785+ 'time_period' => $field['time_period']
4786+ );
4787+ } else {
4788+ $end_field_name = $field_name . '_end';
4789+ $end_field = self::getParam($end_field_name);
4790+ @$cookie[$field_name] = array(
4791+ 'past_hour' => $field['past_hour'],
4792+ 'Year' => $field['Year'],
4793+ 'Month' => $field['Month'],
4794+ 'Day' => $field['Day'],
4795+ 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
4796+ 'filter_type' => $field['filter_type'],
4797+ 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
4798+ );
4799+ @$cookie[$end_field_name] = array(
4800+ 'Year' => $end_field['Year'],
4801+ 'Month' => $end_field['Month'],
4802+ 'Day' => $end_field['Day']
4803+ );
4804+ }
4805+ }
4806+ Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
4807+ return $cookie;
4808+ }
4809+
4810+
4811+ /**
4812+ * Method used to get the current sorting options used in the grid layout
4813+ * of the issue listing page.
4814+ *
4815+ * @access public
4816+ * @param array $options The current search parameters
4817+ * @return array The sorting options
4818+ */
4819+ function getSortingInfo($options)
4820+ {
4821+
4822+ $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
4823+
4824+ // default order for last action date, priority should be descending
4825+ // for textual fields, like summary, ascending is reasonable
4826+ $fields = array(
4827+ "pri_rank" => "desc",
4828+ "iss_id" => "desc",
4829+ "iss_customer_id" => "desc",
4830+ "prc_title" => "asc",
4831+ "sta_rank" => "asc",
4832+ "iss_created_date" => "desc",
4833+ "iss_summary" => "asc",
4834+ "last_action_date" => "desc",
4835+ "usr_full_name" => "asc",
4836+ "iss_expected_resolution_date" => "desc",
4837+ "pre_title" => "asc",
4838+ "assigned" => "asc",
4839+ );
4840+
4841+ foreach ($custom_fields as $fld_id => $fld_name) {
4842+ $fields['custom_field_' . $fld_id] = "desc";
4843+ }
4844+
4845+ $sortfields = array_combine(array_keys($fields), array_keys($fields));
4846+ $sortfields["pre_title"] = "pre_scheduled_date";
4847+ $sortfields["assigned"] = "isu_usr_id";
4848+
4849+ $items = array(
4850+ "links" => array(),
4851+ "images" => array()
4852+ );
4853+ foreach ($sortfields as $field => $sortfield) {
4854+ $sort_order = $fields[$field];
4855+ if ($options["sort_by"] == $sortfield) {
4856+ $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
4857+ if (strtolower($options["sort_order"]) == "asc") {
4858+ $sort_order = "desc";
4859+ } else {
4860+ $sort_order = "asc";
4861+ }
4862+ }
4863+ $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
4864+ }
4865+ return $items;
4866+ }
4867+
4868+
4869+ /**
4870+ * Returns the list of action date fields appropriate for the
4871+ * current user ID.
4872+ *
4873+ * @access public
4874+ * @return array The list of action date fields
4875+ */
4876+ function getLastActionFields()
4877+ {
4878+ $last_action_fields = array(
4879+ "iss_last_public_action_date"
4880+ );
4881+ if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
4882+ $last_action_fields[] = "iss_last_internal_action_date";
4883+ }
4884+ if (count($last_action_fields) > 1) {
4885+ return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
4886+ } else {
4887+ return $last_action_fields[0] . " AS last_action_date";
4888+ }
4889+ }
4890+
4891+
4892+ /**
4893+ * Method used to get the list of issues to be displayed in the grid layout.
4894+ *
4895+ * @access public
4896+ * @param integer $prj_id The current project ID
4897+ * @param array $options The search parameters
4898+ * @param integer $current_row The current page number
4899+ * @param integer $max The maximum number of rows per page
4900+ * @return array The list of issues to be displayed
4901+ */
4902+ function getListing($prj_id, $options, $current_row = 0, $max = 5)
4903+ {
4904+ if (strtoupper($max) == "ALL") {
4905+ $max = 9999999;
4906+ }
4907+ $start = $current_row * $max;
4908+ // get the current user's role
4909+ $usr_id = Auth::getUserID();
4910+ $role_id = User::getRoleByUser($usr_id, $prj_id);
4911+
4912+ // get any custom fields that should be displayed
4913+ $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
4914+
4915+ $stmt = "SELECT
4916+ iss_id,
4917+ iss_grp_id,
4918+ iss_prj_id,
4919+ iss_sta_id,
4920+ iss_customer_id,
4921+ iss_customer_contract_id,
4922+ iss_created_date,
4923+ iss_updated_date,
4924+ iss_last_response_date,
4925+ iss_closed_date,
4926+ iss_last_customer_action_date,
4927+ iss_usr_id,
4928+ iss_summary,
4929+ pri_title,
4930+ prc_title,
4931+ sta_title,
4932+ sta_color status_color,
4933+ sta_id,
4934+ iqu_status,
4935+ grp_name `group`,
4936+ pre_title,
4937+ iss_last_public_action_date,
4938+ iss_last_public_action_type,
4939+ iss_last_internal_action_date,
4940+ iss_last_internal_action_type,
4941+ " . self::getLastActionFields() . ",
4942+ IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
4943+ iss_private,
4944+ usr_full_name,
4945+ iss_percent_complete,
4946+ iss_dev_time,
4947+ iss_expected_resolution_date
4948+ FROM
4949+ (
4950+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
4951+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
4952+ // join custom fields if we are searching by custom fields
4953+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
4954+ foreach ($options['custom_field'] as $fld_id => $search_value) {
4955+ if (empty($search_value)) {
4956+ continue;
4957+ }
4958+ $field = Custom_Field::getDetails($fld_id);
4959+ if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
4960+ continue;
4961+ }
4962+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
4963+ continue;
4964+ }
4965+ if ($field['fld_type'] == 'multiple') {
4966+ $search_value = Misc::escapeInteger($search_value);
4967+ foreach ($search_value as $cfo_id) {
4968+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
4969+ }
4970+ } else {
4971+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
4972+ }
4973+ }
4974+ }
4975+ $stmt .= ")";
4976+ // check for the custom fields we want to sort by
4977+ if (strstr($options['sort_by'], 'custom_field') !== false) {
4978+ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
4979+ $stmt .= "\n LEFT JOIN \n" .
4980+ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
4981+ ON
4982+ (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
4983+ }
4984+ if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
4985+ $stmt .= "
4986+ LEFT JOIN
4987+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
4988+ ON
4989+ isu_iss_id=iss_id";
4990+ }
4991+ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
4992+ $stmt .= "
4993+ LEFT JOIN
4994+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
4995+ ON
4996+ iur_iss_id=iss_id";
4997+ }
4998+ if (!empty($options["show_notification_list_issues"])) {
4999+ $stmt .= "
5000+ LEFT JOIN
5001+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
5002+ ON
5003+ sub_iss_id=iss_id";
5004+ }
5005+ $stmt .= "
5006+ LEFT JOIN
5007+ " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
5008+ ON
5009+ iss_grp_id=grp_id
5010+ LEFT JOIN
5011+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
5012+ ON
5013+ iss_prc_id=prc_id
5014+ LEFT JOIN
5015+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
5016+ ON
5017+ iss_pre_id = pre_id
5018+ LEFT JOIN
5019+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5020+ ON
5021+ iss_sta_id=sta_id
5022+ LEFT JOIN
5023+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
5024+ ON
5025+ iss_pri_id=pri_id
5026+ LEFT JOIN
5027+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
5028+ ON
5029+ iss_id=iqu_iss_id AND
5030+ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
5031+ WHERE
5032+ iss_prj_id= " . Misc::escapeInteger($prj_id);
5033+ $stmt .= self::buildWhereClause($options);
5034+
5035+ if (strstr($options["sort_by"], 'custom_field') !== false) {
5036+ $fld_details = Custom_Field::getDetails($fld_id);
5037+ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
5038+ } else {
5039+ $sort_by = Misc::escapeString($options["sort_by"]);
5040+ }
5041+
5042+ $stmt .= "
5043+ GROUP BY
5044+ iss_id
5045+ ORDER BY
5046+ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
5047+ iss_id DESC";
5048+ $total_rows = Pager::getTotalRows($stmt);
5049+ $stmt .= "
5050+ LIMIT
5051+ " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
5052+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
5053+ if (PEAR::isError($res)) {
5054+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5055+ return array(
5056+ "list" => "",
5057+ "info" => ""
5058+ );
5059+ } else {
5060+ if (count($res) > 0) {
5061+ self::getAssignedUsersByIssues($res);
5062+ Time_Tracking::getTimeSpentByIssues($res);
5063+ // need to get the customer titles for all of these issues...
5064+ if (Customer::hasCustomerIntegration($prj_id)) {
5065+ Customer::getCustomerTitlesByIssues($prj_id, $res);
5066+ Customer::getSupportLevelsByIssues($prj_id, $res);
5067+ }
5068+ self::formatLastActionDates($res);
5069+ self::getLastStatusChangeDates($prj_id, $res);
5070+ } elseif ($current_row > 0) {
5071+ // if there are no results, and the page is not the first page reset page to one and reload results
5072+ Auth::redirect("list.php?pagerRow=0&rows=$max");
5073+ }
5074+ $groups = Group::getAssocList($prj_id);
5075+ $categories = Category::getAssocList($prj_id);
5076+ $column_headings = self::getColumnHeadings($prj_id);
5077+ if (count($custom_fields) > 0) {
5078+ $column_headings = array_merge($column_headings,$custom_fields);
5079+ }
5080+ $csv[] = @implode("\t", $column_headings);
5081+ for ($i = 0; $i < count($res); $i++) {
5082+ $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
5083+ $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
5084+ $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
5085+ $fields = array(
5086+ $res[$i]['pri_title'],
5087+ $res[$i]['iss_id'],
5088+ $res[$i]['usr_full_name'],
5089+ );
5090+ // hide the group column from the output if no
5091+ // groups are available in the database
5092+ if (count($groups) > 0) {
5093+ $fields[] = $res[$i]['group'];
5094+ }
5095+ $fields[] = $res[$i]['assigned_users'];
5096+ $fields[] = $res[$i]['time_spent'];
5097+ // hide the category column from the output if no
5098+ // categories are available in the database
5099+ if (count($categories) > 0) {
5100+ $fields[] = $res[$i]['prc_title'];
5101+ }
5102+ if (Customer::hasCustomerIntegration($prj_id)) {
5103+ $fields[] = @$res[$i]['customer_title'];
5104+ // check if current user is acustomer and has a per incident contract.
5105+ // if so, check if issue is redeemed.
5106+ if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
5107+ if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
5108+ (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
5109+ $res[$i]['redeemed'] = true;
5110+ }
5111+ }
5112+ }
5113+ $fields[] = $res[$i]['sta_title'];
5114+ $fields[] = $res[$i]["status_change_date"];
5115+ $fields[] = $res[$i]["last_action_date"];
5116+ $fields[] = $res[$i]['iss_dev_time'];
5117+ $fields[] = $res[$i]['iss_summary'];
5118+ $fields[] = $res[$i]['iss_expected_resolution_date'];
5119+
5120+ if (count($custom_fields) > 0) {
5121+ $res[$i]['custom_field'] = array();
5122+ $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
5123+ foreach ($custom_field_values as $this_field) {
5124+ if (!empty($custom_fields[$this_field['fld_id']])) {
5125+ $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
5126+ $fields[] = $this_field['value'];
5127+ }
5128+ }
5129+ }
5130+
5131+ $csv[] = @implode("\t", $fields);
5132+ }
5133+ $total_pages = ceil($total_rows / $max);
5134+ $last_page = $total_pages - 1;
5135+ return array(
5136+ "list" => $res,
5137+ "info" => array(
5138+ "current_page" => $current_row,
5139+ "start_offset" => $start,
5140+ "end_offset" => $start + count($res),
5141+ "total_rows" => $total_rows,
5142+ "total_pages" => $total_pages,
5143+ "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
5144+ "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
5145+ "last_page" => $last_page,
5146+ "custom_fields" => $custom_fields
5147+ ),
5148+ "csv" => @implode("\n", $csv)
5149+ );
5150+ }
5151+ }
5152+
5153+
5154+ /**
5155+ * Processes a result set to format the "Last Action Date" column.
5156+ *
5157+ * @access public
5158+ * @param array $result The result set
5159+ */
5160+ function formatLastActionDates(&$result)
5161+ {
5162+ for ($i = 0; $i < count($result); $i++) {
5163+ if (($result[$i]['action_type'] == "internal") &&
5164+ (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
5165+ $label = $result[$i]["iss_last_internal_action_type"];
5166+ $last_date = $result[$i]["iss_last_internal_action_date"];
5167+ } else {
5168+ $label = $result[$i]["iss_last_public_action_type"];
5169+ $last_date = $result[$i]["iss_last_public_action_date"];
5170+ }
5171+ $date = new Date($last_date);
5172+ $current = new Date(Date_Helper::getCurrentDateGMT());
5173+ $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
5174+ Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
5175+ }
5176+ }
5177+
5178+
5179+ /**
5180+ * Retrieves the last status change date for the given issue.
5181+ *
5182+ * @access public
5183+ * @param integer $prj_id The project ID
5184+ * @param array $result The associative array of data
5185+ * @see self::getListing()
5186+ */
5187+ function getLastStatusChangeDates($prj_id, &$result)
5188+ {
5189+ $ids = array();
5190+ for ($i = 0; $i < count($result); $i++) {
5191+ $ids[] = $result[$i]["iss_sta_id"];
5192+ }
5193+ if (count($ids) == 0) {
5194+ return false;
5195+ }
5196+ $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
5197+ for ($i = 0; $i < count($result); $i++) {
5198+ if (empty($result[$i]['iss_sta_id'])) {
5199+ $result[$i]['status_change_date'] = '';
5200+ } else {
5201+ list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
5202+ if ((empty($label)) || (empty($date_field_name))) {
5203+ $result[$i]['status_change_date'] = '';
5204+ continue;
5205+ }
5206+ $current = new Date(Date_Helper::getCurrentDateGMT());
5207+ $desc = "$label: %s ago";
5208+ $target_date = $result[$i][$date_field_name];
5209+ if (empty($target_date)) {
5210+ $result[$i]['status_change_date'] = '';
5211+ continue;
5212+ }
5213+ $date = new Date($target_date);
5214+ $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
5215+ }
5216+ }
5217+ }
5218+
5219+
5220+ /**
5221+ * Method used to get the list of issues to be displayed in the grid layout.
5222+ *
5223+ * @access public
5224+ * @param array $options The search parameters
5225+ * @return string The where clause
5226+ */
5227+ function buildWhereClause($options)
5228+ {
5229+ $usr_id = Auth::getUserID();
5230+ $prj_id = Auth::getCurrentProject();
5231+ $role_id = User::getRoleByUser($usr_id, $prj_id);
5232+
5233+ $stmt = ' AND iss_usr_id = usr_id';
5234+ if ($role_id == User::getRoleID('Customer')) {
5235+ $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
5236+ } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
5237+ $stmt .= " AND (
5238+ iss_usr_id = $usr_id OR
5239+ iur_usr_id = $usr_id
5240+ )";
5241+ }
5242+
5243+ if (!empty($options["users"])) {
5244+ $stmt .= " AND (\n";
5245+ if (stristr($options["users"], "grp") !== false) {
5246+ $chunks = explode(":", $options["users"]);
5247+ $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
5248+ } else {
5249+ if ($options['users'] == '-1') {
5250+ $stmt .= 'isu_usr_id IS NULL';
5251+ } elseif ($options['users'] == '-2') {
5252+ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
5253+ } elseif ($options['users'] == '-3') {
5254+ $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
5255+ } elseif ($options['users'] == '-4') {
5256+ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
5257+ } else {
5258+ $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
5259+ }
5260+ }
5261+ $stmt .= ')';
5262+ }
5263+ if (!empty($options["reporter"])) {
5264+ $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
5265+ }
5266+ if (!empty($options["show_authorized_issues"])) {
5267+ $stmt .= " AND (iur_usr_id=$usr_id)";
5268+ }
5269+ if (!empty($options["show_notification_list_issues"])) {
5270+ $stmt .= " AND (sub_usr_id=$usr_id)";
5271+ }
5272+ if (!empty($options["keywords"])) {
5273+ $stmt .= " AND (\n";
5274+ if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
5275+ $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
5276+ } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
5277+ // check if the user is trying to search by customer email
5278+ $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
5279+ if (count($customer_ids) > 0) {
5280+ $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
5281+ } else {
5282+ // no results, kill query
5283+ $stmt .= " iss_customer_id = -1";
5284+ }
5285+ } else {
5286+ $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
5287+ $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
5288+ }
5289+ $stmt .= "\n) ";
5290+ }
5291+ if (!empty($options["priority"])) {
5292+ $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
5293+ }
5294+ if (!empty($options["status"])) {
5295+ $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
5296+ }
5297+ if (!empty($options["category"])) {
5298+ if (!is_array($options['category'])) {
5299+ $options['category'] = array($options['category']);
5300+ }
5301+ $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
5302+ }
5303+ if (!empty($options["hide_closed"])) {
5304+ $stmt .= " AND sta_is_closed=0";
5305+ }
5306+ if (!empty($options['release'])) {
5307+ $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
5308+ }
5309+ // now for the date fields
5310+ $date_fields = array(
5311+ 'created_date',
5312+ 'updated_date',
5313+ 'last_response_date',
5314+ 'first_response_date',
5315+ 'closed_date'
5316+ );
5317+ foreach ($date_fields as $field_name) {
5318+ if (!empty($options[$field_name])) {
5319+ switch ($options[$field_name]['filter_type']) {
5320+ case 'greater':
5321+ $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
5322+ break;
5323+ case 'less':
5324+ $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
5325+ break;
5326+ case 'between':
5327+ $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
5328+ break;
5329+ case 'null':
5330+ $stmt .= " AND iss_$field_name IS NULL";
5331+ break;
5332+ case 'in_past':
5333+ if (strlen($options[$field_name]['time_period']) == 0) {
5334+ $options[$field_name]['time_period'] = 0;
5335+ }
5336+ $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
5337+ Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
5338+ break;
5339+ }
5340+ }
5341+ }
5342+ // custom fields
5343+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
5344+ foreach ($options['custom_field'] as $fld_id => $search_value) {
5345+ if (empty($search_value)) {
5346+ continue;
5347+ }
5348+ $field = Custom_Field::getDetails($fld_id);
5349+ $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
5350+ if (($field['fld_type'] == 'date') &&
5351+ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
5352+ continue;
5353+ }
5354+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
5355+ continue;
5356+ }
5357+
5358+ if ($field['fld_type'] == 'multiple') {
5359+ $search_value = Misc::escapeInteger($search_value);
5360+ foreach ($search_value as $cfo_id) {
5361+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
5362+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
5363+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
5364+ }
5365+ } elseif ($field['fld_type'] == 'date') {
5366+ if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
5367+ continue;
5368+ }
5369+ $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
5370+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
5371+ cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
5372+ } else if ($field['fld_type'] == 'integer') {
5373+ $value = $search_value['value'];
5374+ switch ($search_value['filter_type']) {
5375+ case 'ge': $cmp = '>='; break;
5376+ case 'le': $cmp = '<='; break;
5377+ case 'gt': $cmp = '>'; break;
5378+ case 'lt': $cmp = '<'; break;
5379+ default: $cmp = '='; break;
5380+ }
5381+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
5382+ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
5383+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
5384+ } else {
5385+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
5386+ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
5387+ if ($field['fld_type'] == 'combo') {
5388+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
5389+ } else {
5390+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
5391+ }
5392+ $stmt .= ')';
5393+ }
5394+ }
5395+ }
5396+ // clear cached full-text values if we are not searching fulltext anymore
5397+ if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
5398+ Session::set('fulltext_string', '');
5399+ Session::set('fulltext_issues', '');
5400+ }
5401+ return $stmt;
5402+ }
5403+
5404+
5405+ /**
5406+ * Method used to get the previous and next issues that are available
5407+ * according to the current search parameters.
5408+ *
5409+ * @access public
5410+ * @param integer $issue_id The issue ID
5411+ * @param array $options The search parameters
5412+ * @return array The list of issues
5413+ */
5414+ function getSides($issue_id, $options)
5415+ {
5416+ $usr_id = Auth::getUserID();
5417+ $role_id = Auth::getCurrentRole();
5418+
5419+ $stmt = "SELECT
5420+ iss_id,
5421+ " . self::getLastActionFields() . "
5422+ FROM
5423+ (
5424+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5425+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
5426+ // join custom fields if we are searching by custom fields
5427+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
5428+ foreach ($options['custom_field'] as $fld_id => $search_value) {
5429+ if (empty($search_value)) {
5430+ continue;
5431+ }
5432+ $field = Custom_Field::getDetails($fld_id);
5433+ if (($field['fld_type'] == 'date') &&
5434+ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
5435+ continue;
5436+ }
5437+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
5438+ continue;
5439+ }
5440+
5441+ if ($field['fld_type'] == 'multiple') {
5442+ $search_value = Misc::escapeInteger($search_value);
5443+ foreach ($search_value as $cfo_id) {
5444+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
5445+ }
5446+ } else {
5447+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
5448+ }
5449+ }
5450+ }
5451+ $stmt .= ")";
5452+ // check for the custom fields we want to sort by
5453+ if (strstr($options['sort_by'], 'custom_field') !== false) {
5454+ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
5455+ $stmt .= "\n LEFT JOIN \n" .
5456+ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
5457+ ON
5458+ (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
5459+ }
5460+ if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
5461+ $stmt .= "
5462+ LEFT JOIN
5463+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
5464+ ON
5465+ isu_iss_id=iss_id";
5466+ }
5467+ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
5468+ $stmt .= "
5469+ LEFT JOIN
5470+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
5471+ ON
5472+ iur_iss_id=iss_id";
5473+ }
5474+ if (!empty($options["show_notification_list_issues"])) {
5475+ $stmt .= "
5476+ LEFT JOIN
5477+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
5478+ ON
5479+ sub_iss_id=iss_id";
5480+ }
5481+ if (@$options["sort_by"] == "pre_scheduled_date") {
5482+ $stmt .= "
5483+ LEFT JOIN
5484+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
5485+ ON
5486+ iss_pre_id = pre_id";
5487+ }
5488+ if (@$options['sort_by'] == 'prc_title') {
5489+ $stmt .= "
5490+ LEFT JOIN
5491+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
5492+ ON
5493+ iss_prc_id = prc_id";
5494+ }
5495+ $stmt .= "
5496+ LEFT JOIN
5497+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5498+ ON
5499+ iss_sta_id=sta_id
5500+ LEFT JOIN
5501+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
5502+ ON
5503+ iss_pri_id=pri_id
5504+ WHERE
5505+ iss_prj_id=" . Auth::getCurrentProject();
5506+ $stmt .= self::buildWhereClause($options);
5507+ if (strstr($options["sort_by"], 'custom_field') !== false) {
5508+ $fld_details = Custom_Field::getDetails($fld_id);
5509+ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
5510+ } else {
5511+ $sort_by = Misc::escapeString($options["sort_by"]);
5512+ }
5513+ $stmt .= "
5514+ GROUP BY
5515+ iss_id
5516+ ORDER BY
5517+ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
5518+ iss_id DESC";
5519+ $res = DB_Helper::getInstance()->getCol($stmt);
5520+ if (PEAR::isError($res)) {
5521+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5522+ return "";
5523+ } else {
5524+ // COMPAT: the next line requires PHP >= 4.0.5
5525+ $index = array_search($issue_id, $res);
5526+ if (!empty($res[$index+1])) {
5527+ $next = $res[$index+1];
5528+ }
5529+ if (!empty($res[$index-1])) {
5530+ $previous = $res[$index-1];
5531+ }
5532+ return array(
5533+ "next" => @$next,
5534+ "previous" => @$previous
5535+ );
5536+ }
5537+ }
5538+
5539+
5540+ /**
5541+ * Method used to get the full list of user IDs assigned to a specific
5542+ * issue.
5543+ *
5544+ * @access public
5545+ * @param integer $issue_id The issue ID
5546+ * @return array The list of user IDs
5547+ */
5548+ function getAssignedUserIDs($issue_id)
5549+ {
5550+ $stmt = "SELECT
5551+ usr_id
5552+ FROM
5553+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5554+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5555+ WHERE
5556+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
5557+ isu_usr_id=usr_id";
5558+ $res = DB_Helper::getInstance()->getCol($stmt);
5559+ if (PEAR::isError($res)) {
5560+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5561+ return array();
5562+ } else {
5563+ return $res;
5564+ }
5565+ }
5566+
5567+
5568+ /**
5569+ * Method used to see if a user is assigned to an issue.
5570+ *
5571+ * @access public
5572+ * @param integer $issue_id The issue ID
5573+ * @param integer $usr_id An integer containg the ID of the user.
5574+ * @return boolean true if the user(s) are assigned to the issue.
5575+ */
5576+ function isAssignedToUser($issue_id, $usr_id)
5577+ {
5578+ $assigned_users = self::getAssignedUserIDs($issue_id);
5579+ if (in_array($usr_id, $assigned_users)) {
5580+ return true;
5581+ } else {
5582+ return false;
5583+ }
5584+ }
5585+
5586+
5587+ /**
5588+ * Method used to get the full list of reporters associated with a given
5589+ * list of issues.
5590+ *
5591+ * @access public
5592+ * @param array $result The result set
5593+ * @return void
5594+ */
5595+ function getReportersByIssues(&$result)
5596+ {
5597+ $ids = array();
5598+ for ($i = 0; $i < count($result); $i++) {
5599+ $ids[] = $result[$i]["iss_id"];
5600+ }
5601+ $ids = implode(", ", $ids);
5602+ $stmt = "SELECT
5603+ iss_id,
5604+ CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
5605+ FROM
5606+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5607+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5608+ WHERE
5609+ iss_usr_id=usr_id AND
5610+ iss_id IN ($ids)";
5611+ $res = DB_Helper::getInstance()->getAssoc($stmt);
5612+ if (PEAR::isError($res)) {
5613+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5614+ } else {
5615+ // now populate the $result variable again
5616+ for ($i = 0; $i < count($result); $i++) {
5617+ @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
5618+ }
5619+ }
5620+ }
5621+
5622+
5623+ /**
5624+ * Method used to get the full list of assigned users by a list
5625+ * of issues. This was originally created to optimize the issue
5626+ * listing page.
5627+ *
5628+ * @access public
5629+ * @param array $result The result set
5630+ * @return void
5631+ */
5632+ function getAssignedUsersByIssues(&$result)
5633+ {
5634+ $ids = array();
5635+ for ($i = 0; $i < count($result); $i++) {
5636+ $ids[] = $result[$i]["iss_id"];
5637+ }
5638+ if (count($ids) < 1) {
5639+ return;
5640+ }
5641+ $ids = implode(", ", $ids);
5642+ $stmt = "SELECT
5643+ isu_iss_id,
5644+ usr_full_name
5645+ FROM
5646+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5647+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5648+ WHERE
5649+ isu_usr_id=usr_id AND
5650+ isu_iss_id IN ($ids)";
5651+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
5652+ if (PEAR::isError($res)) {
5653+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5654+ } else {
5655+ $t = array();
5656+ for ($i = 0; $i < count($res); $i++) {
5657+ if (!empty($t[$res[$i]['isu_iss_id']])) {
5658+ $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
5659+ } else {
5660+ $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
5661+ }
5662+ }
5663+ // now populate the $result variable again
5664+ for ($i = 0; $i < count($result); $i++) {
5665+ @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
5666+ }
5667+ }
5668+ }
5669+
5670+
5671+ /**
5672+ * Method used to add the issue description to a list of issues.
5673+ *
5674+ * @access public
5675+ * @param array $result The result set
5676+ * @return void
5677+ */
5678+ function getDescriptionByIssues(&$result)
5679+ {
5680+ if (count($result) == 0) {
5681+ return;
5682+ }
5683+
5684+ $ids = array();
5685+ for ($i = 0; $i < count($result); $i++) {
5686+ $ids[] = $result[$i]["iss_id"];
5687+ }
5688+ $ids = implode(", ", $ids);
5689+
5690+ $stmt = "SELECT
5691+ iss_id,
5692+ iss_description
5693+ FROM
5694+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
5695+ WHERE
5696+ iss_id in ($ids)";
5697+ $res = DB_Helper::getInstance()->getAssoc($stmt);
5698+ if (PEAR::isError($res)) {
5699+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5700+ } else {
5701+ for ($i = 0; $i < count($result); $i++) {
5702+ @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
5703+ }
5704+ }
5705+ }
5706+
5707+
5708+ /**
5709+ * Method used to get the full list of users (the full names) assigned to a
5710+ * specific issue.
5711+ *
5712+ * @access public
5713+ * @param integer $issue_id The issue ID
5714+ * @return array The list of users
5715+ */
5716+ function getAssignedUsers($issue_id)
5717+ {
5718+ $stmt = "SELECT
5719+ usr_full_name
5720+ FROM
5721+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5722+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5723+ WHERE
5724+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
5725+ isu_usr_id=usr_id";
5726+ $res = DB_Helper::getInstance()->getCol($stmt);
5727+ if (PEAR::isError($res)) {
5728+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5729+ return array();
5730+ } else {
5731+ return $res;
5732+ }
5733+ }
5734+
5735+
5736+ /**
5737+ * Method used to get the details for a specific issue.
5738+ *
5739+ * @access public
5740+ * @param integer $issue_id The issue ID
5741+ * @param boolean $force_refresh If the cache should not be used.
5742+ * @return array The details for the specified issue
5743+ */
5744+ function getDetails($issue_id, $force_refresh = false)
5745+ {
5746+ static $returns;
5747+
5748+ $issue_id = Misc::escapeInteger($issue_id);
5749+
5750+ if (empty($issue_id)) {
5751+ return '';
5752+ }
5753+
5754+ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
5755+ return $returns[$issue_id];
5756+ }
5757+
5758+ $stmt = "SELECT
5759+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
5760+ prj_title,
5761+ prc_title,
5762+ pre_title,
5763+ pri_title,
5764+ sta_title,
5765+ sta_abbreviation,
5766+ sta_color status_color,
5767+ sta_is_closed
5768+ FROM
5769+ (
5770+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5771+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
5772+ )
5773+ LEFT JOIN
5774+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
5775+ ON
5776+ iss_pri_id=pri_id
5777+ LEFT JOIN
5778+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5779+ ON
5780+ iss_sta_id=sta_id
5781+ LEFT JOIN
5782+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
5783+ ON
5784+ iss_prc_id=prc_id
5785+ LEFT JOIN
5786+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
5787+ ON
5788+ iss_pre_id=pre_id
5789+ WHERE
5790+ iss_id=$issue_id AND
5791+ iss_prj_id=prj_id";
5792+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
5793+ if (PEAR::isError($res)) {
5794+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5795+ return "";
5796+ } else {
5797+ if (empty($res)) {
5798+ return "";
5799+ } else {
5800+ $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
5801+ // get customer information, if any
5802+ if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
5803+ $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
5804+ $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
5805+ $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
5806+ $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
5807+ $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
5808+ $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
5809+ if (empty($res['iss_first_response_date'])) {
5810+ $first_response_deadline = $created_date_ts + $max_first_response_time;
5811+ if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
5812+ $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
5813+ } else {
5814+ $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
5815+ }
5816+ }
5817+ }
5818+ $res['iss_original_description'] = $res["iss_description"];
5819+ if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
5820+ $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
5821+ $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
5822+ }
5823+ $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
5824+ $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
5825+ $res['iss_created_date_ts'] = $created_date_ts;
5826+ $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
5827+ list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
5828+ $temp = self::getAssignedUsersStatus($res["iss_id"]);
5829+ $res["has_inactive_users"] = 0;
5830+ $res["assigned_users"] = array();
5831+ $res["assigned_inactive_users"] = array();
5832+ foreach ($temp as $usr_id => $usr_status) {
5833+ if (!User::isActiveStatus($usr_status)) {
5834+ $res["assigned_inactive_users"][] = $usr_id;
5835+ $res["has_inactive_users"] = 1;
5836+ } else {
5837+ $res["assigned_users"][] = $usr_id;
5838+ }
5839+ }
5840+ if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
5841+ $res["is_current_user_assigned"] = 1;
5842+ } else {
5843+ $res["is_current_user_assigned"] = 0;
5844+ }
5845+ $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
5846+ $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
5847+ $res["reporter"] = User::getFullName($res["iss_usr_id"]);
5848+ if (empty($res["iss_updated_date"])) {
5849+ $res["iss_updated_date"] = 'not updated yet';
5850+ } else {
5851+ $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
5852+ }
5853+ $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
5854+ if (Release::isAssignable($res["iss_pre_id"])) {
5855+ $release = Release::getDetails($res["iss_pre_id"]);
5856+ $res["pre_title"] = $release["pre_title"];
5857+ $res["pre_status"] = $release["pre_status"];
5858+ }
5859+ // need to return the list of issues that are duplicates of this one
5860+ $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
5861+ $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
5862+ // also get the issue title of the duplicated issue
5863+ if (!empty($res['iss_duplicated_iss_id'])) {
5864+ $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
5865+ }
5866+
5867+ // get group information
5868+ if (!empty($res["iss_grp_id"])) {
5869+ $res["group"] = Group::getDetails($res["iss_grp_id"]);
5870+ }
5871+
5872+ // get quarantine issue
5873+ $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
5874+
5875+ $returns[$issue_id] = $res;
5876+ return $res;
5877+ }
5878+ }
5879+ }
5880+
5881+
5882+ /**
5883+ * Method used to get some simple details about the given duplicated issue.
5884+ *
5885+ * @access public
5886+ * @param integer $issue_id The issue ID
5887+ * @return array The duplicated issue details
5888+ */
5889+ function getDuplicatedDetails($issue_id)
5890+ {
5891+ $stmt = "SELECT
5892+ iss_summary title,
5893+ sta_title current_status,
5894+ sta_is_closed is_closed
5895+ FROM
5896+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
5897+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
5898+ WHERE
5899+ iss_sta_id=sta_id AND
5900+ iss_id=$issue_id";
5901+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
5902+ if (PEAR::isError($res)) {
5903+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
5904+ return array();
5905+ } else {
5906+ return $res;
5907+ }
5908+ }
5909+
5910+
5911+ /**
5912+ * Method used to bulk update a list of issues
5913+ *
5914+ * @access public
5915+ * @return boolean
5916+ */
5917+ function bulkUpdate()
5918+ {
5919+ // check if user performing this chance has the proper role
5920+ if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
5921+ return -1;
5922+ }
5923+
5924+ $items = Misc::escapeInteger($_POST['item']);
5925+ $new_status_id = Misc::escapeInteger($_POST['status']);
5926+ $new_release_id = Misc::escapeInteger(@$_POST['release']);
5927+ $new_priority_id = Misc::escapeInteger($_POST['priority']);
5928+ $new_category_id = Misc::escapeInteger($_POST['category']);
5929+
5930+ for ($i = 0; $i < count($items); $i++) {
5931+ if (!self::canAccess($items[$i], Auth::getUserID())) {
5932+ continue;
5933+ } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
5934+ // make sure issue is not in another project
5935+ continue;
5936+ }
5937+
5938+ $updated_fields = array();
5939+
5940+ // update assignment
5941+ if (count(@$_POST['users']) > 0) {
5942+ $users = Misc::escapeInteger($_POST['users']);
5943+ // get who this issue is currently assigned too
5944+ $stmt = "SELECT
5945+ isu_usr_id,
5946+ usr_full_name
5947+ FROM
5948+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
5949+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
5950+ WHERE
5951+ isu_usr_id = usr_id AND
5952+ isu_iss_id = " . $items[$i];
5953+ $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
5954+ if (PEAR::isError($current_assignees)) {
5955+ Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
5956+ return -1;
5957+ }
5958+ foreach ($current_assignees as $usr_id => $usr_name) {
5959+ if (!in_array($usr_id, $users)) {
5960+ self::deleteUserAssociation($items[$i], $usr_id, false);
5961+ }
5962+ }
5963+ $new_user_names = array();
5964+ $new_assignees = array();
5965+ foreach ($users as $usr_id) {
5966+ $new_user_names[$usr_id] = User::getFullName($usr_id);
5967+
5968+ // check if the issue is already assigned to this person
5969+ $stmt = "SELECT
5970+ COUNT(*) AS total
5971+ FROM
5972+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
5973+ WHERE
5974+ isu_iss_id=" . $items[$i] . " AND
5975+ isu_usr_id=" . $usr_id;
5976+ $total = DB_Helper::getInstance()->getOne($stmt);
5977+ if ($total > 0) {
5978+ continue;
5979+ } else {
5980+ $new_assignees[] = $usr_id;
5981+ // add the assignment
5982+ self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
5983+ Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
5984+ Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
5985+ }
5986+ }
5987+ Notification::notifyNewAssignment($new_assignees, $items[$i]);
5988+ $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
5989+ }
5990+
5991+ // update status
5992+ if (!empty($new_status_id)) {
5993+ $old_status_id = self::getStatusID($items[$i]);
5994+ $res = self::setStatus($items[$i], $new_status_id, false);
5995+ if ($res == 1) {
5996+ $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
5997+ }
5998+ }
5999+
6000+ // update release
6001+ if (!empty($new_release_id)) {
6002+ $old_release_id = self::getRelease($items[$i]);
6003+ $res = self::setRelease($items[$i], $new_release_id);
6004+ if ($res == 1) {
6005+ $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
6006+ }
6007+ }
6008+
6009+ // update priority
6010+ if (!empty($new_priority_id)) {
6011+ $old_priority_id = self::getPriority($items[$i]);
6012+ $res = self::setPriority($items[$i], $new_priority_id);
6013+ if ($res == 1) {
6014+ $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
6015+ }
6016+ }
6017+
6018+ // update category
6019+ if (!empty($new_category_id)) {
6020+ $old_category_id = self::getCategory($items[$i]);
6021+ $res = self::setCategory($items[$i], $new_category_id);
6022+ if ($res == 1) {
6023+ $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
6024+ }
6025+ }
6026+
6027+ if (count($updated_fields) > 0) {
6028+ // log the changes
6029+ $changes = '';
6030+ $k = 0;
6031+ foreach ($updated_fields as $key => $value) {
6032+ if ($k > 0) {
6033+ $changes .= "; ";
6034+ }
6035+ $changes .= "$key: $value";
6036+ $k++;
6037+ }
6038+ History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
6039+ }
6040+
6041+ // close if request
6042+ if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
6043+ self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
6044+ }
6045+ }
6046+ return true;
6047+ }
6048+
6049+
6050+ /**
6051+ * Method used to set the initial impact analysis for a specific issue
6052+ *
6053+ * @access public
6054+ * @param integer $issue_id The issue ID
6055+ * @return integer 1 if the update worked, -1 otherwise
6056+ */
6057+ function setImpactAnalysis($issue_id)
6058+ {
6059+ $stmt = "UPDATE
6060+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6061+ SET
6062+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
6063+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
6064+ iss_last_internal_action_type='update',
6065+ iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
6066+ iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
6067+ WHERE
6068+ iss_id=" . Misc::escapeInteger($issue_id);
6069+ $res = DB_Helper::getInstance()->query($stmt);
6070+ if (PEAR::isError($res)) {
6071+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6072+ return -1;
6073+ } else {
6074+ // add the impact analysis to the history of the issue
6075+ $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
6076+ History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
6077+ return 1;
6078+ }
6079+ }
6080+
6081+
6082+ /**
6083+ * Method used to get the full list of issue IDs that area available in the
6084+ * system.
6085+ *
6086+ * @access public
6087+ * @param string $extra_condition An extra condition in the WHERE clause
6088+ * @return array The list of issue IDs
6089+ */
6090+ function getColList($extra_condition = NULL)
6091+ {
6092+ $stmt = "SELECT
6093+ iss_id
6094+ FROM
6095+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6096+ WHERE
6097+ iss_prj_id=" . Auth::getCurrentProject();
6098+ if (!empty($extra_condition)) {
6099+ $stmt .= " AND $extra_condition ";
6100+ }
6101+ $stmt .= "
6102+ ORDER BY
6103+ iss_id DESC";
6104+ $res = DB_Helper::getInstance()->getCol($stmt);
6105+ if (PEAR::isError($res)) {
6106+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6107+ return "";
6108+ } else {
6109+ return $res;
6110+ }
6111+ }
6112+
6113+
6114+ /**
6115+ * Method used to get the full list of issue IDs and their respective
6116+ * titles.
6117+ *
6118+ * @access public
6119+ * @param string $extra_condition An extra condition in the WHERE clause
6120+ * @return array The list of issues
6121+ */
6122+ function getAssocList($extra_condition = NULL)
6123+ {
6124+ $stmt = "SELECT
6125+ iss_id,
6126+ iss_summary
6127+ FROM
6128+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6129+ WHERE
6130+ iss_prj_id=" . Auth::getCurrentProject();
6131+ if (!empty($extra_condition)) {
6132+ $stmt .= " AND $extra_condition ";
6133+ }
6134+ $stmt .= "
6135+ ORDER BY
6136+ iss_id ASC";
6137+ $res = DB_Helper::getInstance()->getAssoc($stmt);
6138+ if (PEAR::isError($res)) {
6139+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6140+ return "";
6141+ } else {
6142+ return $res;
6143+ }
6144+ }
6145+
6146+
6147+ /**
6148+ * Method used to get the list of issues associated to a specific issue.
6149+ *
6150+ * @access public
6151+ * @param integer $issue_id The issue ID
6152+ * @return array The list of associated issues
6153+ */
6154+ function getAssociatedIssues($issue_id)
6155+ {
6156+ $issues = self::getAssociatedIssuesDetails($issue_id);
6157+ $associated = array();
6158+ for ($i = 0; $i < count($issues); $i++) {
6159+ $associated[] = $issues[$i]['associated_issue'];
6160+ }
6161+ return $associated;
6162+ }
6163+
6164+
6165+ /**
6166+ * Method used to get the list of issues associated details to a
6167+ * specific issue.
6168+ *
6169+ * @access public
6170+ * @param integer $issue_id The issue ID
6171+ * @return array The list of associated issues
6172+ */
6173+ function getAssociatedIssuesDetails($issue_id)
6174+ {
6175+ static $returns;
6176+
6177+ if (!empty($returns[$issue_id])) {
6178+ return $returns[$issue_id];
6179+ }
6180+
6181+ $stmt = "SELECT
6182+ isa_associated_id associated_issue,
6183+ iss_summary associated_title,
6184+ sta_title current_status,
6185+ sta_is_closed is_closed
6186+ FROM
6187+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
6188+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
6189+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
6190+ WHERE
6191+ isa_associated_id=iss_id AND
6192+ iss_sta_id=sta_id AND
6193+ isa_issue_id=$issue_id";
6194+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
6195+ if (PEAR::isError($res)) {
6196+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6197+ return array();
6198+ } else {
6199+ $returns[$issue_id] = $res;
6200+ return $res;
6201+ }
6202+ }
6203+
6204+
6205+ /**
6206+ * Method used to check whether an issue was already closed or not.
6207+ *
6208+ * @access public
6209+ * @param integer $issue_id The issue ID
6210+ * @return boolean
6211+ */
6212+ function isClosed($issue_id)
6213+ {
6214+ $stmt = "SELECT
6215+ COUNT(*)
6216+ FROM
6217+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
6218+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
6219+ WHERE
6220+ iss_id=" . Misc::escapeInteger($issue_id) . " AND
6221+ iss_sta_id=sta_id AND
6222+ sta_is_closed=1";
6223+ $res = DB_Helper::getInstance()->getOne($stmt);
6224+ if (PEAR::isError($res)) {
6225+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6226+ return false;
6227+ } else {
6228+ if ($res == 0) {
6229+ return false;
6230+ } else {
6231+ return true;
6232+ }
6233+ }
6234+ }
6235+
6236+
6237+ /**
6238+ * Returns a simple list of issues that are currently set to some
6239+ * form of quarantine. This is mainly used by the IRC interface.
6240+ *
6241+ * @access public
6242+ * @return array List of quarantined issues
6243+ */
6244+ function getQuarantinedIssueList()
6245+ {
6246+ // XXX: would be nice to restrict the result list to only one project
6247+ $stmt = "SELECT
6248+ iss_id,
6249+ iss_summary
6250+ FROM
6251+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
6252+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6253+ WHERE
6254+ iqu_iss_id=iss_id AND
6255+ iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
6256+ iqu_expiration IS NOT NULL";
6257+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
6258+ if (PEAR::isError($res)) {
6259+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6260+ return array();
6261+ } else {
6262+ self::getAssignedUsersByIssues($res);
6263+ return $res;
6264+ }
6265+ }
6266+
6267+
6268+ /**
6269+ * Returns the status of a quarantine.
6270+ *
6271+ * @param integer $issue_id The issue ID
6272+ * @return integer Indicates what the current state of quarantine is.
6273+ */
6274+ function getQuarantineInfo($issue_id)
6275+ {
6276+ $stmt = "SELECT
6277+ iqu_status,
6278+ iqu_expiration
6279+ FROM
6280+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6281+ WHERE
6282+ iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
6283+ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
6284+ iqu_expiration IS NULL)";
6285+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
6286+ if (PEAR::isError($res)) {
6287+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6288+ return array();
6289+ } else {
6290+ if (!empty($res["iqu_expiration"])) {
6291+ $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
6292+ $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
6293+ }
6294+ return $res;
6295+ }
6296+ }
6297+
6298+
6299+ /**
6300+ * Sets the quarantine status. Optionally an expiration date can be set
6301+ * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
6302+ *
6303+ * @access public
6304+ * @param integer $issue_id The issue ID
6305+ * @param integer $status The quarantine status
6306+ * @param string $expiration The expiration date of quarantine (default empty)
6307+ */
6308+ function setQuarantine($issue_id, $status, $expiration = '')
6309+ {
6310+ $issue_id = Misc::escapeInteger($issue_id);
6311+ $status = Misc::escapeInteger($status);
6312+
6313+ // see if there is an existing record
6314+ $stmt = "SELECT
6315+ COUNT(*)
6316+ FROM
6317+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6318+ WHERE
6319+ iqu_iss_id = $issue_id";
6320+ $res = DB_Helper::getInstance()->getOne($stmt);
6321+ if (PEAR::isError($res)) {
6322+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6323+ return -1;
6324+ }
6325+ if ($res > 0) {
6326+ // update
6327+ $stmt = "UPDATE
6328+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6329+ SET
6330+ iqu_status = $status";
6331+ if (!empty($expiration)) {
6332+ $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
6333+ }
6334+ $stmt .= "\nWHERE
6335+ iqu_iss_id = $issue_id";
6336+ $res = DB_Helper::getInstance()->query($stmt);
6337+ if (PEAR::isError($res)) {
6338+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6339+ return -1;
6340+ } else {
6341+ // add history entry about this change taking place
6342+ if ($status == 0) {
6343+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
6344+ "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
6345+ }
6346+ }
6347+ } else {
6348+ // insert
6349+ $stmt = "INSERT INTO
6350+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
6351+ (
6352+ iqu_iss_id,
6353+ iqu_status";
6354+ if (!empty($expiration)) {
6355+ $stmt .= ",\niqu_expiration\n";
6356+ }
6357+ $stmt .= ") VALUES (
6358+ $issue_id,
6359+ $status";
6360+ if (!empty($expiration)) {
6361+ $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
6362+ }
6363+ $stmt .= ")";
6364+ $res = DB_Helper::getInstance()->query($stmt);
6365+ if (PEAR::isError($res)) {
6366+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6367+ return -1;
6368+ }
6369+ }
6370+ return 1;
6371+ }
6372+
6373+
6374+ /**
6375+ * Sets the group of the issue.
6376+ *
6377+ * @access public
6378+ * @param integer $issue_id The ID of the issue
6379+ * @param integer $group_id The ID of the group
6380+ * @return integer 1 if successful, -1 or -2 otherwise
6381+ */
6382+ function setGroup($issue_id, $group_id)
6383+ {
6384+ $issue_id = Misc::escapeInteger($issue_id);
6385+ $group_id = Misc::escapeInteger($group_id);
6386+
6387+ $current = self::getDetails($issue_id);
6388+ if ($current["iss_grp_id"] == $group_id) {
6389+ return -2;
6390+ }
6391+ $stmt = "UPDATE
6392+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6393+ SET
6394+ iss_grp_id = $group_id
6395+ WHERE
6396+ iss_id = $issue_id";
6397+ $res = DB_Helper::getInstance()->query($stmt);
6398+ if (PEAR::isError($res)) {
6399+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6400+ return -1;
6401+ }
6402+ $current_user = Auth::getUserID();
6403+ if (empty($current_user)) {
6404+ $current_user = APP_SYSTEM_USER_ID;
6405+ }
6406+ History::add($issue_id, $current_user, History::getTypeID('group_changed'),
6407+ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
6408+ return 1;
6409+ }
6410+
6411+
6412+ /**
6413+ * Returns the group ID associated with the given issue ID.
6414+ *
6415+ * @access public
6416+ * @param integer $issue_id The issue ID
6417+ * @return integer The associated group ID
6418+ */
6419+ function getGroupID($issue_id)
6420+ {
6421+ $stmt = "SELECT
6422+ iss_grp_id
6423+ FROM
6424+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6425+ WHERE
6426+ iss_id=" . Misc::escapeInteger($issue_id);
6427+ $res = DB_Helper::getInstance()->getOne($stmt);
6428+ if (PEAR::isError($res)) {
6429+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6430+ return 0;
6431+ } else {
6432+ return $res;
6433+ }
6434+ }
6435+
6436+
6437+ /**
6438+ * Returns an array of issues based on full text search results.
6439+ *
6440+ * @param array $options An array of search options
6441+ * @return array An array of issue IDS
6442+ */
6443+ function getFullTextIssues($options)
6444+ {
6445+ // check if a list of issues for this full text search is already cached
6446+ $fulltext_string = Session::get('fulltext_string');
6447+ if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
6448+ return Session::get('fulltext_issues');
6449+ }
6450+
6451+ // no pre-existing list, generate them
6452+ $stmt = "(SELECT
6453+ DISTINCT(iss_id)
6454+ FROM
6455+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6456+ WHERE
6457+ MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6458+ ) UNION (
6459+ SELECT
6460+ DISTINCT(not_iss_id)
6461+ FROM
6462+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
6463+ WHERE
6464+ MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6465+ ) UNION (
6466+ SELECT
6467+ DISTINCT(ttr_iss_id)
6468+ FROM
6469+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
6470+ WHERE
6471+ MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6472+ ) UNION (
6473+ SELECT
6474+ DISTINCT(phs_iss_id)
6475+ FROM
6476+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
6477+ WHERE
6478+ MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6479+ ) UNION (
6480+ SELECT
6481+ DISTINCT(sup_iss_id)
6482+ FROM
6483+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
6484+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
6485+ WHERE
6486+ sup_id = seb_sup_id AND
6487+ MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
6488+ )";
6489+ $res = DB_Helper::getInstance()->getCol($stmt);
6490+ if (PEAR::isError($res)) {
6491+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6492+ return array(-1);
6493+ } else {
6494+ $stmt = "SELECT
6495+ DISTINCT(icf_iss_id)
6496+ FROM
6497+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
6498+ WHERE
6499+ MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
6500+ $custom_res = DB_Helper::getInstance()->getCol($stmt);
6501+ if (PEAR::isError($custom_res)) {
6502+ Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
6503+ return array(-1);
6504+ }
6505+ $issues = array_merge($res, $custom_res);
6506+ // we kill the query results on purpose to flag that no
6507+ // issues could be found with fulltext search
6508+ if (count($issues) < 1) {
6509+ $issues = array(-1);
6510+ }
6511+ Session::set('fulltext_string', $options['keywords']);
6512+ Session::set('fulltext_issues', $issues);
6513+ return $issues;
6514+ }
6515+ }
6516+
6517+
6518+ /**
6519+ * Method to determine if user can access a particular issue
6520+ *
6521+ * @access public
6522+ * @param integer $issue_id The ID of the issue.
6523+ * @param integer $usr_id The ID of the user
6524+ * @return boolean If the user can access the issue
6525+ */
6526+ function canAccess($issue_id, $usr_id)
6527+ {
6528+ static $access;
6529+
6530+ if (empty($issue_id)) {
6531+ return true;
6532+ }
6533+
6534+ if (isset($access[$issue_id . "-" . $usr_id])) {
6535+ return $access[$issue_id . "-" . $usr_id];
6536+ }
6537+
6538+ $details = self::getDetails($issue_id);
6539+ if (empty($details)) {
6540+ return true;
6541+ }
6542+ $usr_details = User::getDetails($usr_id);
6543+ $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
6544+ $prj_id = self::getProjectID($issue_id);
6545+
6546+
6547+ if (empty($usr_role)) {
6548+ // check if they are even allowed to access the project
6549+ $return = false;
6550+ } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
6551+ ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
6552+ // check customer permissions
6553+ $return = false;
6554+ } elseif ($details['iss_private'] == 1) {
6555+ // check if the issue is even private
6556+
6557+ // check role, reporter, assigment and group
6558+ if ($usr_role > User::getRoleID("Developer")) {
6559+ $return = true;
6560+ } elseif ($details['iss_usr_id'] == $usr_id) {
6561+ $return = true;
6562+ } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
6563+ $return = true;
6564+ } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
6565+ ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
6566+ $return = true;
6567+ } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
6568+ $return = true;
6569+ } else {
6570+ $return = false;
6571+ }
6572+ } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
6573+ ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
6574+ return false;
6575+ } else {
6576+ $return = true;
6577+ }
6578+
6579+ $access[$issue_id . "-" . $usr_id] = $return;
6580+ return $return;
6581+ }
6582+
6583+
6584+ /**
6585+ * Returns true if the specified issue is private, false otherwise
6586+ *
6587+ * @access public
6588+ * @param integer $issue_id The ID of the issue
6589+ * @return boolean If the issue is private or not
6590+ */
6591+ function isPrivate($issue_id)
6592+ {
6593+ static $returns;
6594+
6595+ if (!isset($returns[$issue_id])) {
6596+ $sql = "SELECT
6597+ iss_private
6598+ FROM
6599+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6600+ WHERE
6601+ iss_id=$issue_id";
6602+ $res = DB_Helper::getInstance()->getOne($sql);
6603+ if (PEAR::isError($res)) {
6604+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6605+ return true;
6606+ } else {
6607+ if ($res == 1) {
6608+ $returns[$issue_id] = true;
6609+ } else {
6610+ $returns[$issue_id] = false;
6611+ }
6612+ }
6613+ }
6614+ return $returns[$issue_id];
6615+ }
6616+
6617+
6618+ /**
6619+ * Clears closed information from an issues.
6620+ *
6621+ * @access public
6622+ * @param integer $issue_id The ID of the issue
6623+ */
6624+ function clearClosed($issue_id)
6625+ {
6626+ $stmt = "UPDATE
6627+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6628+ SET
6629+ iss_closed_date = null,
6630+ iss_res_id = null
6631+ WHERE
6632+ iss_id=" . Misc::escapeInteger($issue_id);
6633+ $res = DB_Helper::getInstance()->query($stmt);
6634+ if (PEAR::isError($res)) {
6635+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6636+ return -1;
6637+ }
6638+ }
6639+
6640+
6641+ /**
6642+ * Returns the message ID that should be used as the parent ID for all messages
6643+ *
6644+ * @access public
6645+ * @param integer $issue_id The ID of the issue
6646+ */
6647+ function getRootMessageID($issue_id)
6648+ {
6649+ $sql = "SELECT
6650+ iss_root_message_id
6651+ FROM
6652+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6653+ WHERE
6654+ iss_id=" . Misc::escapeInteger($issue_id);
6655+ $res = DB_Helper::getInstance()->getOne($sql);
6656+ if (PEAR::isError($res)) {
6657+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6658+ return false;
6659+ } else {
6660+ return $res;
6661+ }
6662+ }
6663+
6664+
6665+ /**
6666+ * Returns the issue ID of the issue with the specified root message ID, or false
6667+ * @access public
6668+ * @param string $msg_id The Message ID
6669+ * @return integer The ID of the issue
6670+ */
6671+ function getIssueByRootMessageID($msg_id)
6672+ {
6673+ static $returns;
6674+
6675+ if (!empty($returns[$msg_id])) {
6676+ return $returns[$msg_id];
6677+ }
6678+ $sql = "SELECT
6679+ iss_id
6680+ FROM
6681+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6682+ WHERE
6683+ iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
6684+ $res = DB_Helper::getInstance()->getOne($sql);
6685+ if (PEAR::isError($res)) {
6686+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6687+ return false;
6688+ }
6689+ if (empty($res)) {
6690+ $returns[$msg_id] = false;
6691+ } else {
6692+ $returns[$msg_id] = $res;
6693+ }
6694+ return $returns[$msg_id];
6695+ }
6696+
6697+
6698+ /**
6699+ * Sets the assignees for the issue
6700+ *
6701+ * @param integer $issue_id
6702+ * @param array $assignees
6703+ */
6704+ function setAssignees($issue_id, $assignees)
6705+ {
6706+ if (!is_array($assignees)) {
6707+ $assignees = array();
6708+ }
6709+
6710+ // see if there is anything to change
6711+ $old_assignees = self::getAssignedUserIDs($issue_id);
6712+ if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
6713+ return;
6714+ }
6715+
6716+ $old_assignee_names = self::getAssignedUsers($issue_id);
6717+
6718+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
6719+ // clear up the assignments for this issue, and then assign it to the current user
6720+ self::deleteUserAssociations($issue_id);
6721+ $assignee_names = array();
6722+ foreach ($assignees as $assignee) {
6723+ $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
6724+ if ($res == -1) {
6725+ return false;
6726+ }
6727+ $assignee_names[] = User::getFullName($assignee);
6728+ Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
6729+ }
6730+
6731+ Notification::notifyNewAssignment($assignees, $issue_id);
6732+
6733+ // save a history entry about this...
6734+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
6735+ "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
6736+ }
6737+}
6738--- eventum-2.2/lib/eventum/class.issue.php.~2~ 1970-01-01 02:00:00.000000000 +0200
6739+++ eventum-2.2-order/lib/eventum/class.issue.php.~2~ 2009-10-12 22:10:36.432519927 +0300
6740@@ -0,0 +1,4748 @@
6741+<?php
6742+/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
6743+// +----------------------------------------------------------------------+
6744+// | Eventum - Issue Tracking System |
6745+// +----------------------------------------------------------------------+
6746+// | Copyright (c) 2003 - 2008 MySQL AB |
6747+// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
6748+// | |
6749+// | This program is free software; you can redistribute it and/or modify |
6750+// | it under the terms of the GNU General Public License as published by |
6751+// | the Free Software Foundation; either version 2 of the License, or |
6752+// | (at your option) any later version. |
6753+// | |
6754+// | This program is distributed in the hope that it will be useful, |
6755+// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
6756+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
6757+// | GNU General Public License for more details. |
6758+// | |
6759+// | You should have received a copy of the GNU General Public License |
6760+// | along with this program; if not, write to: |
6761+// | |
6762+// | Free Software Foundation, Inc. |
6763+// | 59 Temple Place - Suite 330 |
6764+// | Boston, MA 02111-1307, USA. |
6765+// +----------------------------------------------------------------------+
6766+// | Authors: João Prado Maia <jpm@mysql.com> |
6767+// +----------------------------------------------------------------------+
6768+//
6769+
6770+
6771+/**
6772+ * Class designed to handle all business logic related to the issues in the
6773+ * system, such as adding or updating them or listing them in the grid mode.
6774+ *
6775+ * @author João Prado Maia <jpm@mysql.com>
6776+ * @version $Revision$
6777+ */
6778+
6779+class Issue
6780+{
6781+ /**
6782+ * Method used to check whether a given issue ID exists or not.
6783+ *
6784+ * @access public
6785+ * @param integer $issue_id The issue ID
6786+ * @param boolean $check_project If we should check that this issue is in the current project
6787+ * @return boolean
6788+ */
6789+ function exists($issue_id, $check_project = true)
6790+ {
6791+ $stmt = "SELECT
6792+ COUNT(*)
6793+ FROM
6794+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6795+ WHERE
6796+ iss_id=" . Misc::escapeInteger($issue_id);
6797+ if ($check_project) {
6798+ $stmt .= " AND
6799+ iss_prj_id = " . Auth::getCurrentProject();
6800+ }
6801+ $res = DB_Helper::getInstance()->getOne($stmt);
6802+ if (PEAR::isError($res)) {
6803+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6804+ return false;
6805+ } else {
6806+ if ($res == 0) {
6807+ return false;
6808+ } else {
6809+ return true;
6810+ }
6811+ }
6812+ }
6813+
6814+
6815+ /**
6816+ * Method used to get the list of column heading titles for the
6817+ * CSV export functionality of the issue listing screen.
6818+ *
6819+ * @access public
6820+ * @param integer $prj_id The project ID
6821+ * @return array The list of column heading titles
6822+ */
6823+ function getColumnHeadings($prj_id)
6824+ {
6825+ $headings = array(
6826+ 'Priority',
6827+ 'Issue ID',
6828+ 'Reporter',
6829+ );
6830+ // hide the group column from the output if no
6831+ // groups are available in the database
6832+ $groups = Group::getAssocList($prj_id);
6833+ if (count($groups) > 0) {
6834+ $headings[] = 'Group';
6835+ }
6836+ $headings[] = 'Assigned';
6837+ $headings[] = 'Time Spent';
6838+ // hide the category column from the output if no
6839+ // categories are available in the database
6840+ $categories = Category::getAssocList($prj_id);
6841+ if (count($categories) > 0) {
6842+ $headings[] = 'Category';
6843+ }
6844+ if (Customer::hasCustomerIntegration($prj_id)) {
6845+ $headings[] = 'Customer';
6846+ }
6847+ $headings[] = 'Status';
6848+ $headings[] = 'Status Change Date';
6849+ $headings[] = 'Last Action Date';
6850+ $headings[] = 'Est. Dev. TIme';
6851+ $headings[] = 'Summary';
6852+ $headings[] = 'Expected Resolution Date';
6853+ return $headings;
6854+ }
6855+
6856+
6857+ /**
6858+ * Method used to get the full list of date fields available to issues, to
6859+ * be used when customizing the issue listing screen in the 'last status
6860+ * change date' column.
6861+ *
6862+ * @access public
6863+ * @param boolean $display_customer_fields Whether to include any customer related fields or not
6864+ * @return array The list of available date fields
6865+ */
6866+ function getDateFieldsAssocList($display_customer_fields = FALSE)
6867+ {
6868+ $fields = array(
6869+ 'iss_created_date' => 'Created Date',
6870+ 'iss_updated_date' => 'Last Updated Date',
6871+ 'iss_last_response_date' => 'Last Response Date',
6872+ 'iss_closed_date' => 'Closed Date'
6873+ );
6874+ if ($display_customer_fields) {
6875+ $fields['iss_last_customer_action_date'] = 'Customer Action Date';
6876+ }
6877+ asort($fields);
6878+ return $fields;
6879+ }
6880+
6881+
6882+ /**
6883+ * Method used to get the full list of issue IDs and their respective
6884+ * titles associated to a given project.
6885+ *
6886+ * @access public
6887+ * @param integer $prj_id The project ID
6888+ * @return array The list of issues
6889+ */
6890+ function getAssocListByProject($prj_id)
6891+ {
6892+ $stmt = "SELECT
6893+ iss_id,
6894+ iss_summary
6895+ FROM
6896+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6897+ WHERE
6898+ iss_prj_id=" . Misc::escapeInteger($prj_id) . "
6899+ ORDER BY
6900+ iss_id ASC";
6901+ $res = DB_Helper::getInstance()->getAssoc($stmt);
6902+ if (PEAR::isError($res)) {
6903+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6904+ return "";
6905+ } else {
6906+ return $res;
6907+ }
6908+ }
6909+
6910+
6911+ /**
6912+ * Method used to get the status of a given issue.
6913+ *
6914+ * @access public
6915+ * @param integer $issue_id The issue ID
6916+ * @return integer The status ID
6917+ */
6918+ function getStatusID($issue_id)
6919+ {
6920+ static $returns;
6921+
6922+ $issue_id = Misc::escapeInteger($issue_id);
6923+
6924+ if (!empty($returns[$issue_id])) {
6925+ return $returns[$issue_id];
6926+ }
6927+
6928+ $stmt = "SELECT
6929+ iss_sta_id
6930+ FROM
6931+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6932+ WHERE
6933+ iss_id=$issue_id";
6934+ $res = DB_Helper::getInstance()->getOne($stmt);
6935+ if (PEAR::isError($res)) {
6936+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6937+ return '';
6938+ } else {
6939+ $returns[$issue_id] = $res;
6940+ return $res;
6941+ }
6942+ }
6943+
6944+
6945+ /**
6946+ * Records the last customer action date for a given issue ID.
6947+ *
6948+ * @access public
6949+ * @param integer $issue_id The issue ID
6950+ * @return integer 1 if the update worked, -1 otherwise
6951+ */
6952+ function recordLastCustomerAction($issue_id)
6953+ {
6954+ $stmt = "UPDATE
6955+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6956+ SET
6957+ iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
6958+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
6959+ iss_last_public_action_type='customer action'
6960+ WHERE
6961+ iss_id=" . Misc::escapeInteger($issue_id);
6962+ $res = DB_Helper::getInstance()->query($stmt);
6963+ if (PEAR::isError($res)) {
6964+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6965+ return -1;
6966+ } else {
6967+ return 1;
6968+ }
6969+ }
6970+
6971+
6972+ /**
6973+ * Returns the customer ID associated with the given issue ID.
6974+ *
6975+ * @access public
6976+ * @param integer $issue_id The issue ID
6977+ * @return integer The customer ID associated with the issue
6978+ */
6979+ function getCustomerID($issue_id)
6980+ {
6981+ static $returns;
6982+
6983+ $issue_id = Misc::escapeInteger($issue_id);
6984+
6985+ if (!empty($returns[$issue_id])) {
6986+ return $returns[$issue_id];
6987+ }
6988+
6989+ $stmt = "SELECT
6990+ iss_customer_id
6991+ FROM
6992+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
6993+ WHERE
6994+ iss_id=$issue_id";
6995+ $res = DB_Helper::getInstance()->getOne($stmt);
6996+ if (PEAR::isError($res)) {
6997+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
6998+ return '';
6999+ } else {
7000+ $returns[$issue_id] = $res;
7001+ return $res;
7002+ }
7003+ }
7004+
7005+
7006+ /**
7007+ * Returns the contract ID associated with the given issue ID.
7008+ *
7009+ * @access public
7010+ * @param integer $issue_id The issue ID
7011+ * @return integer The customer ID associated with the issue
7012+ */
7013+ function getContractID($issue_id)
7014+ {
7015+ static $returns;
7016+
7017+ $issue_id = Misc::escapeInteger($issue_id);
7018+
7019+ if (!empty($returns[$issue_id])) {
7020+ return $returns[$issue_id];
7021+ }
7022+
7023+ $stmt = "SELECT
7024+ iss_customer_contract_id
7025+ FROM
7026+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7027+ WHERE
7028+ iss_id=$issue_id";
7029+ $res = DB_Helper::getInstance()->getOne($stmt);
7030+ if (PEAR::isError($res)) {
7031+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7032+ return '';
7033+ } else {
7034+ $returns[$issue_id] = $res;
7035+ return $res;
7036+ }
7037+ }
7038+
7039+
7040+ /**
7041+ * Sets the contract ID for a specific issue.
7042+ *
7043+ * @access public
7044+ * @param integer $issue_id The issue ID
7045+ * @param integer The contract ID
7046+ * @return integer 1 if the update worked, -1 otherwise
7047+ */
7048+ function setContractID($issue_id, $contract_id)
7049+ {
7050+ $issue_id = Misc::escapeInteger($issue_id);
7051+
7052+ $old_contract_id = self::getContractID($issue_id);
7053+
7054+ $stmt = "UPDATE
7055+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7056+ SET
7057+ iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
7058+ WHERE
7059+ iss_id=$issue_id";
7060+ $res = DB_Helper::getInstance()->query($stmt);
7061+ if (PEAR::isError($res)) {
7062+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7063+ return -1;
7064+ } else {
7065+ // log this
7066+ History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
7067+ return 1;
7068+ }
7069+ }
7070+
7071+
7072+ /**
7073+ * Returns the customer ID associated with the given issue ID.
7074+ *
7075+ * @access public
7076+ * @param integer $issue_id The issue ID
7077+ * @return integer The customer ID associated with the issue
7078+ */
7079+ function getContactID($issue_id)
7080+ {
7081+ static $returns;
7082+
7083+ $issue_id = Misc::escapeInteger($issue_id);
7084+
7085+ if (!empty($returns[$issue_id])) {
7086+ return $returns[$issue_id];
7087+ }
7088+
7089+ $stmt = "SELECT
7090+ iss_customer_contact_id
7091+ FROM
7092+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7093+ WHERE
7094+ iss_id=$issue_id";
7095+ $res = DB_Helper::getInstance()->getOne($stmt);
7096+ if (PEAR::isError($res)) {
7097+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7098+ return '';
7099+ } else {
7100+ $returns[$issue_id] = $res;
7101+ return $res;
7102+ }
7103+ }
7104+
7105+
7106+ /**
7107+ * Method used to get the project associated to a given issue.
7108+ *
7109+ * @access public
7110+ * @param integer $issue_id The issue ID
7111+ * @param boolean $force_refresh If the cache should not be used.
7112+ * @return integer The project ID
7113+ */
7114+ function getProjectID($issue_id, $force_refresh = false)
7115+ {
7116+ static $returns;
7117+
7118+ $issue_id = Misc::escapeInteger($issue_id);
7119+
7120+ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
7121+ return $returns[$issue_id];
7122+ }
7123+
7124+ $stmt = "SELECT
7125+ iss_prj_id
7126+ FROM
7127+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7128+ WHERE
7129+ iss_id=$issue_id";
7130+ $res = DB_Helper::getInstance()->getOne($stmt);
7131+ if (PEAR::isError($res)) {
7132+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7133+ return '';
7134+ } else {
7135+ $returns[$issue_id] = $res;
7136+ return $res;
7137+ }
7138+ }
7139+
7140+
7141+ /**
7142+ * Method used to remotely assign a given issue to an user.
7143+ *
7144+ * @access public
7145+ * @param integer $issue_id The issue ID
7146+ * @param integer $usr_id The user ID of the person performing the change
7147+ * @param boolean $assignee The user ID of the assignee
7148+ * @return integer The status ID
7149+ */
7150+ function remoteAssign($issue_id, $usr_id, $assignee)
7151+ {
7152+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
7153+ // clear up the assignments for this issue, and then assign it to the current user
7154+ self::deleteUserAssociations($issue_id, $usr_id);
7155+ $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
7156+ if ($res != -1) {
7157+ // save a history entry about this...
7158+ History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
7159+ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
7160+ if ($assignee != $usr_id) {
7161+ Notification::notifyNewAssignment(array($assignee), $issue_id);
7162+ }
7163+ }
7164+ return $res;
7165+ }
7166+
7167+
7168+ /**
7169+ * Method used to set the status of a given issue.
7170+ *
7171+ * @access public
7172+ * @param integer $issue_id The issue ID
7173+ * @param integer $status_id The new status ID
7174+ * @param boolean $notify If a notification should be sent about this change.
7175+ * @return integer 1 if the update worked, -1 otherwise
7176+ */
7177+ function setStatus($issue_id, $status_id, $notify = false)
7178+ {
7179+ $issue_id = Misc::escapeInteger($issue_id);
7180+ $status_id = Misc::escapeInteger($status_id);
7181+
7182+ $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
7183+ if ($workflow !== true) {
7184+ return $workflow;
7185+ }
7186+
7187+ // check if the status is already set to the 'new' one
7188+ if (self::getStatusID($issue_id) == $status_id) {
7189+ return -1;
7190+ }
7191+
7192+ $old_status = self::getStatusID($issue_id);
7193+ $old_details = Status::getDetails($old_status);
7194+
7195+ $stmt = "UPDATE
7196+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7197+ SET
7198+ iss_sta_id=$status_id,
7199+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7200+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7201+ iss_last_public_action_type='update'
7202+ WHERE
7203+ iss_id=$issue_id";
7204+ $res = DB_Helper::getInstance()->query($stmt);
7205+ if (PEAR::isError($res)) {
7206+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7207+ return -1;
7208+ } else {
7209+ // clear out the last-triggered-reminder flag when changing the status of an issue
7210+ Reminder_Action::clearLastTriggered($issue_id);
7211+
7212+ // if old status was closed and new status is not, clear closed data from issue.
7213+ if (@$old_details['sta_is_closed'] == 1) {
7214+ $new_details = Status::getDetails($status_id);
7215+ if ($new_details['sta_is_closed'] != 1) {
7216+ self::clearClosed($issue_id);
7217+ }
7218+ }
7219+
7220+ if ($notify) {
7221+ Notification::notifyStatusChange($issue_id, $old_status, $status_id);
7222+ }
7223+
7224+ return 1;
7225+ }
7226+ }
7227+
7228+
7229+ /**
7230+ * Method used to remotely set the status of a given issue.
7231+ *
7232+ * @access public
7233+ * @param integer $issue_id The issue ID
7234+ * @param integer $usr_id The user ID of the person performing this change
7235+ * @param integer $new_status The new status ID
7236+ * @return integer 1 if the update worked, -1 otherwise
7237+ */
7238+ function setRemoteStatus($issue_id, $usr_id, $new_status)
7239+ {
7240+ $sta_id = Status::getStatusID($new_status);
7241+
7242+ $res = self::setStatus($issue_id, $sta_id);
7243+ if ($res == 1) {
7244+ // record history entry
7245+ History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
7246+ }
7247+ return $res;
7248+ }
7249+
7250+
7251+ /**
7252+ * Method used to set the release of an issue
7253+ *
7254+ * @access public
7255+ * @param integer $issue_id The ID of the issue
7256+ * @param integer $pre_id The ID of the release to set this issue too
7257+ * @return integer 1 if the update worked, -1 otherwise
7258+ */
7259+ function setRelease($issue_id, $pre_id)
7260+ {
7261+ $issue_id = Misc::escapeInteger($issue_id);
7262+ $pre_id = Misc::escapeInteger($pre_id);
7263+
7264+ if ($pre_id != self::getRelease($issue_id)) {
7265+ $sql = "UPDATE
7266+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7267+ SET
7268+ iss_pre_id = $pre_id
7269+ WHERE
7270+ iss_id = $issue_id";
7271+ $res = DB_Helper::getInstance()->query($sql);
7272+ if (PEAR::isError($res)) {
7273+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7274+ return -1;
7275+ } else {
7276+ return 1;
7277+ }
7278+ }
7279+ }
7280+
7281+
7282+ /**
7283+ * Returns the current release of an issue
7284+ *
7285+ * @access public
7286+ * @param integer $issue_id The ID of the issue
7287+ * @return integer The release
7288+ */
7289+ function getRelease($issue_id)
7290+ {
7291+ $sql = "SELECT
7292+ iss_pre_id
7293+ FROM
7294+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7295+ WHERE
7296+ iss_id = " . Misc::escapeInteger($issue_id);
7297+ $res = DB_Helper::getInstance()->getOne($sql);
7298+ if (PEAR::isError($res)) {
7299+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7300+ return 0;
7301+ } else {
7302+ return $res;
7303+ }
7304+ }
7305+
7306+
7307+ /**
7308+ * Method used to set the priority of an issue
7309+ *
7310+ * @access public
7311+ * @param integer $issue_id The ID of the issue
7312+ * @param integer $pri_id The ID of the priority to set this issue too
7313+ * @return integer 1 if the update worked, -1 otherwise
7314+ */
7315+ function setPriority($issue_id, $pri_id)
7316+ {
7317+ $issue_id = Misc::escapeInteger($issue_id);
7318+ $pri_id = Misc::escapeInteger($pri_id);
7319+
7320+ if ($pri_id != self::getPriority($issue_id)) {
7321+ $sql = "UPDATE
7322+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7323+ SET
7324+ iss_pri_id = $pri_id
7325+ WHERE
7326+ iss_id = $issue_id";
7327+ $res = DB_Helper::getInstance()->query($sql);
7328+ if (PEAR::isError($res)) {
7329+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7330+ return -1;
7331+ } else {
7332+ return 1;
7333+ }
7334+ }
7335+ }
7336+
7337+
7338+ /**
7339+ * Returns the current issue priority
7340+ *
7341+ * @access public
7342+ * @param integer $issue_id The ID of the issue
7343+ * @return integer The priority
7344+ */
7345+ function getPriority($issue_id)
7346+ {
7347+ $sql = "SELECT
7348+ iss_pri_id
7349+ FROM
7350+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7351+ WHERE
7352+ iss_id = " . Misc::escapeInteger($issue_id);
7353+ $res = DB_Helper::getInstance()->getOne($sql);
7354+ if (PEAR::isError($res)) {
7355+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7356+ return 0;
7357+ } else {
7358+ return $res;
7359+ }
7360+ }
7361+
7362+
7363+ /**
7364+ * Method used to set the category of an issue
7365+ *
7366+ * @access public
7367+ * @param integer $issue_id The ID of the issue
7368+ * @param integer $prc_id The ID of the category to set this issue too
7369+ * @return integer 1 if the update worked, -1 otherwise
7370+ */
7371+ function setCategory($issue_id, $prc_id)
7372+ {
7373+ $issue_id = Misc::escapeInteger($issue_id);
7374+ $prc_id = Misc::escapeInteger($prc_id);
7375+
7376+ if ($prc_id != self::getPriority($issue_id)) {
7377+ $sql = "UPDATE
7378+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7379+ SET
7380+ iss_prc_id = $prc_id
7381+ WHERE
7382+ iss_id = $issue_id";
7383+ $res = DB_Helper::getInstance()->query($sql);
7384+ if (PEAR::isError($res)) {
7385+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7386+ return -1;
7387+ } else {
7388+ return 1;
7389+ }
7390+ }
7391+ }
7392+
7393+
7394+ /**
7395+ * Returns the current issue category
7396+ *
7397+ * @access public
7398+ * @param integer $issue_id The ID of the issue
7399+ * @return integer The category
7400+ */
7401+ function getCategory($issue_id)
7402+ {
7403+ $sql = "SELECT
7404+ iss_prc_id
7405+ FROM
7406+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7407+ WHERE
7408+ iss_id = " . Misc::escapeInteger($issue_id);
7409+ $res = DB_Helper::getInstance()->getOne($sql);
7410+ if (PEAR::isError($res)) {
7411+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7412+ return 0;
7413+ } else {
7414+ return $res;
7415+ }
7416+ }
7417+
7418+
7419+ /**
7420+ * Method used to get all issues associated with a status that doesn't have
7421+ * the 'closed' context.
7422+ *
7423+ * @access public
7424+ * @param integer $prj_id The project ID to list issues from
7425+ * @param integer $usr_id The user ID of the user requesting this information
7426+ * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
7427+ * @param integer $status_id The status ID to be used to restrict results
7428+ * @return array The list of open issues
7429+ */
7430+ function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
7431+ {
7432+ $prj_id = Misc::escapeInteger($prj_id);
7433+ $status_id = Misc::escapeInteger($status_id);
7434+ $projects = Project::getRemoteAssocListByUser($usr_id);
7435+ if (@count($projects) == 0) {
7436+ return '';
7437+ }
7438+
7439+ $stmt = "SELECT
7440+ iss_id,
7441+ iss_summary,
7442+ sta_title
7443+ FROM
7444+ (
7445+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
7446+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
7447+ )
7448+ LEFT JOIN
7449+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
7450+ ON
7451+ isu_iss_id=iss_id
7452+ WHERE ";
7453+ if (!empty($status_id)) {
7454+ $stmt .= " sta_id=$status_id AND ";
7455+ }
7456+ $stmt .= "
7457+ iss_prj_id=$prj_id AND
7458+ sta_id=iss_sta_id AND
7459+ sta_is_closed=0";
7460+ if ($show_all_issues == false) {
7461+ $stmt .= " AND
7462+ isu_usr_id=$usr_id";
7463+ }
7464+ $stmt .= "\nGROUP BY
7465+ iss_id";
7466+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
7467+ if (PEAR::isError($res)) {
7468+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7469+ return '';
7470+ } else {
7471+ if (count($res) > 0) {
7472+ self::getAssignedUsersByIssues($res);
7473+ }
7474+ return $res;
7475+ }
7476+ }
7477+
7478+
7479+ /**
7480+ * Method used to build the required parameters to simulate an email reply
7481+ * to the user who reported the issue, using the issue details like summary
7482+ * and description as email fields.
7483+ *
7484+ * @access public
7485+ * @param integer $issue_id The issue ID
7486+ * @return array The email parameters
7487+ */
7488+ function getReplyDetails($issue_id)
7489+ {
7490+ $issue_id = Misc::escapeInteger($issue_id);
7491+
7492+ $stmt = "SELECT
7493+ iss_created_date,
7494+ usr_full_name AS reporter,
7495+ usr_email AS reporter_email,
7496+ iss_description AS description,
7497+ iss_summary AS sup_subject
7498+ FROM
7499+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
7500+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
7501+ WHERE
7502+ iss_usr_id=usr_id AND
7503+ iss_id=$issue_id";
7504+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
7505+ if (PEAR::isError($res)) {
7506+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7507+ return '';
7508+ } else {
7509+ $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
7510+ $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
7511+ return $res;
7512+ }
7513+ }
7514+
7515+
7516+ /**
7517+ * Method used to record the last updated timestamp for a given
7518+ * issue ID.
7519+ *
7520+ * @access public
7521+ * @param integer $issue_id The issue ID
7522+ * @param string $type The type of update that was made (optional)
7523+ * @return boolean
7524+ */
7525+ function markAsUpdated($issue_id, $type = false)
7526+ {
7527+ $public = array("staff response", "customer action", "file uploaded", "user response");
7528+ $stmt = "UPDATE
7529+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7530+ SET
7531+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
7532+ if ($type != false) {
7533+ if (in_array($type, $public)) {
7534+ $field = "iss_last_public_action_";
7535+ } else {
7536+ $field = "iss_last_internal_action_";
7537+ }
7538+ $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
7539+ $field . "type ='" . Misc::escapeString($type) . "'\n";
7540+ }
7541+ $stmt .= "WHERE
7542+ iss_id=" . Misc::escapeInteger($issue_id);
7543+ $res = DB_Helper::getInstance()->query($stmt);
7544+ if (PEAR::isError($res)) {
7545+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7546+ return false;
7547+ } else {
7548+ // update last response dates if this is a staff response
7549+ if ($type == "staff response") {
7550+ $stmt = "UPDATE
7551+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7552+ SET
7553+ iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
7554+ WHERE
7555+ iss_id = " . Misc::escapeInteger($issue_id);
7556+ DB_Helper::getInstance()->query($stmt);
7557+ $stmt = "UPDATE
7558+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7559+ SET
7560+ iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
7561+ WHERE
7562+ iss_first_response_date IS NULL AND
7563+ iss_id = " . Misc::escapeInteger($issue_id);
7564+ DB_Helper::getInstance()->query($stmt);
7565+ }
7566+
7567+ return true;
7568+ }
7569+ }
7570+
7571+
7572+ /**
7573+ * Method used to check whether a given issue has duplicates
7574+ * or not.
7575+ *
7576+ * @access public
7577+ * @param integer $issue_id The issue ID
7578+ * @return boolean
7579+ */
7580+ function hasDuplicates($issue_id)
7581+ {
7582+ $stmt = "SELECT
7583+ COUNT(iss_id)
7584+ FROM
7585+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7586+ WHERE
7587+ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
7588+ $res = DB_Helper::getInstance()->getOne($stmt);
7589+ if (PEAR::isError($res)) {
7590+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7591+ return false;
7592+ } else {
7593+ if ($res == 0) {
7594+ return false;
7595+ } else {
7596+ return true;
7597+ }
7598+ }
7599+ }
7600+
7601+
7602+ /**
7603+ * Method used to update the duplicated issues for a given
7604+ * issue ID.
7605+ *
7606+ * @access public
7607+ * @param integer $issue_id The issue ID
7608+ * @return integer 1 if the update worked, -1 otherwise
7609+ */
7610+ function updateDuplicates($issue_id)
7611+ {
7612+ $issue_id = Misc::escapeInteger($issue_id);
7613+
7614+ $ids = self::getDuplicateList($issue_id);
7615+ if ($ids == '') {
7616+ return -1;
7617+ }
7618+ $ids = @array_keys($ids);
7619+ $stmt = "UPDATE
7620+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7621+ SET
7622+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7623+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7624+ iss_last_internal_action_type='updated',
7625+ iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
7626+ if (@$_POST["keep"] == "no") {
7627+ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
7628+ }
7629+ $stmt .= "
7630+ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
7631+ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
7632+ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
7633+ WHERE
7634+ iss_id IN (" . implode(", ", $ids) . ")";
7635+ $res = DB_Helper::getInstance()->query($stmt);
7636+ if (PEAR::isError($res)) {
7637+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7638+ return -1;
7639+ } else {
7640+ // record the change
7641+ for ($i = 0; $i < count($ids); $i++) {
7642+ History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
7643+ "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
7644+ }
7645+ return 1;
7646+ }
7647+ }
7648+
7649+
7650+ /**
7651+ * Method used to get a list of the duplicate issues for a given
7652+ * issue ID.
7653+ *
7654+ * @access public
7655+ * @param integer $issue_id The issue ID
7656+ * @return array The list of duplicates
7657+ */
7658+ function getDuplicateList($issue_id)
7659+ {
7660+ $res = self::getDuplicateDetailsList($issue_id);
7661+ if (@count($res) == 0) {
7662+ return '';
7663+ } else {
7664+ $list = array();
7665+ for ($i = 0; $i < count($res); $i++) {
7666+ $list[$res[$i]['issue_id']] = $res[$i]['title'];
7667+ }
7668+ return $list;
7669+ }
7670+ }
7671+
7672+
7673+ /**
7674+ * Method used to get a list of the duplicate issues (and their details)
7675+ * for a given issue ID.
7676+ *
7677+ * @access public
7678+ * @param integer $issue_id The issue ID
7679+ * @return array The list of duplicates
7680+ */
7681+ function getDuplicateDetailsList($issue_id)
7682+ {
7683+ static $returns;
7684+
7685+ if (!empty($returns[$issue_id])) {
7686+ return $returns[$issue_id];
7687+ }
7688+
7689+ $stmt = "SELECT
7690+ iss_id issue_id,
7691+ iss_summary title,
7692+ sta_title current_status,
7693+ sta_is_closed is_closed
7694+ FROM
7695+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
7696+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
7697+ WHERE
7698+ iss_sta_id=sta_id AND
7699+ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
7700+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
7701+ if (PEAR::isError($res)) {
7702+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7703+ return array();
7704+ } else {
7705+ $returns[$issue_id] = $res;
7706+ return $res;
7707+ }
7708+ }
7709+
7710+
7711+ /**
7712+ * Method used to clear the duplicate status of an issue.
7713+ *
7714+ * @access public
7715+ * @param integer $issue_id The issue ID
7716+ * @return integer 1 if the update worked, -1 otherwise
7717+ */
7718+ function clearDuplicateStatus($issue_id)
7719+ {
7720+ $issue_id = Misc::escapeInteger($issue_id);
7721+ $stmt = "UPDATE
7722+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7723+ SET
7724+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7725+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7726+ iss_last_internal_action_type='updated',
7727+ iss_duplicated_iss_id=NULL
7728+ WHERE
7729+ iss_id=$issue_id";
7730+ $res = DB_Helper::getInstance()->query($stmt);
7731+ if (PEAR::isError($res)) {
7732+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7733+ return -1;
7734+ } else {
7735+ // record the change
7736+ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
7737+ return 1;
7738+ }
7739+ }
7740+
7741+
7742+ /**
7743+ * Method used to mark an issue as a duplicate of an existing one.
7744+ *
7745+ * @access public
7746+ * @param integer $issue_id The issue ID
7747+ * @return integer 1 if the update worked, -1 otherwise
7748+ */
7749+ function markAsDuplicate($issue_id)
7750+ {
7751+ $issue_id = Misc::escapeInteger($issue_id);
7752+ if (!self::exists($issue_id)) {
7753+ return -1;
7754+ }
7755+
7756+ $stmt = "UPDATE
7757+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7758+ SET
7759+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
7760+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
7761+ iss_last_internal_action_type='updated',
7762+ iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
7763+ WHERE
7764+ iss_id=$issue_id";
7765+ $res = DB_Helper::getInstance()->query($stmt);
7766+ if (PEAR::isError($res)) {
7767+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7768+ return -1;
7769+ } else {
7770+ if (!empty($_POST["comments"])) {
7771+ // add note with the comments of marking an issue as a duplicate of another one
7772+ $_POST['title'] = 'Issue duplication comments';
7773+ $_POST["note"] = $_POST["comments"];
7774+ Note::insert(Auth::getUserID(), $issue_id);
7775+ }
7776+ // record the change
7777+ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
7778+ "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
7779+ return 1;
7780+ }
7781+ }
7782+
7783+
7784+ function isDuplicate($issue_id)
7785+ {
7786+ $sql = "SELECT
7787+ count(iss_id)
7788+ FROM
7789+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7790+ WHERE
7791+ iss_id = " . Misc::escapeInteger($issue_id) . " AND
7792+ iss_duplicated_iss_id IS NULL";
7793+ $res = DB_Helper::getInstance()->getOne($sql);
7794+ if (PEAR::isError($res)) {
7795+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7796+ return false;
7797+ }
7798+ if ($res > 0) {
7799+ return false;
7800+ } else {
7801+ return true;
7802+ }
7803+ }
7804+
7805+
7806+ /**
7807+ * Method used to get an associative array of user ID => user
7808+ * status associated with a given issue ID.
7809+ *
7810+ * @access public
7811+ * @param integer $issue_id The issue ID
7812+ * @return array The list of users
7813+ */
7814+ function getAssignedUsersStatus($issue_id)
7815+ {
7816+ $stmt = "SELECT
7817+ usr_id,
7818+ usr_status
7819+ FROM
7820+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
7821+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
7822+ WHERE
7823+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
7824+ isu_usr_id=usr_id";
7825+ $res = DB_Helper::getInstance()->getAssoc($stmt);
7826+ if (PEAR::isError($res)) {
7827+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7828+ return array();
7829+ } else {
7830+ return $res;
7831+ }
7832+ }
7833+
7834+
7835+ /**
7836+ * Method used to get the summary associated with a given issue ID.
7837+ *
7838+ * @access public
7839+ * @param integer $issue_id The issue ID
7840+ * @return string The issue summary
7841+ */
7842+ function getTitle($issue_id)
7843+ {
7844+ $stmt = "SELECT
7845+ iss_summary
7846+ FROM
7847+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7848+ WHERE
7849+ iss_id=" . Misc::escapeInteger($issue_id);
7850+ $res = DB_Helper::getInstance()->getOne($stmt);
7851+ if (PEAR::isError($res)) {
7852+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7853+ return "";
7854+ } else {
7855+ return $res;
7856+ }
7857+ }
7858+
7859+
7860+ /**
7861+ * Method used to get the issue ID associated with a specific summary.
7862+ *
7863+ * @access public
7864+ * @param string $summary The summary to look for
7865+ * @return integer The issue ID
7866+ */
7867+ function getIssueID($summary)
7868+ {
7869+ $stmt = "SELECT
7870+ iss_id
7871+ FROM
7872+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7873+ WHERE
7874+ iss_summary='" . Misc::escapeString($summary) . "'";
7875+ $res = DB_Helper::getInstance()->getOne($stmt);
7876+ if (PEAR::isError($res)) {
7877+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7878+ return 0;
7879+ } else {
7880+ if (empty($res)) {
7881+ return 0;
7882+ } else {
7883+ return $res;
7884+ }
7885+ }
7886+ }
7887+
7888+
7889+ /**
7890+ * Method used to add a new anonymous based issue in the system.
7891+ *
7892+ * @access public
7893+ * @return integer The new issue ID
7894+ */
7895+ function addAnonymousReport()
7896+ {
7897+ $options = Project::getAnonymousPostOptions($_POST["project"]);
7898+ $initial_status = Project::getInitialStatus($_POST["project"]);
7899+ $stmt = "INSERT INTO
7900+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
7901+ (
7902+ iss_prj_id,
7903+ iss_prc_id,
7904+ iss_pre_id,
7905+ iss_pri_id,
7906+ iss_usr_id,";
7907+ if (!empty($initial_status)) {
7908+ $stmt .= "iss_sta_id,";
7909+ }
7910+ $stmt .= "
7911+ iss_created_date,
7912+ iss_last_public_action_date,
7913+ iss_last_public_action_type,
7914+ iss_summary,
7915+ iss_description,
7916+ iss_root_message_id
7917+ ) VALUES (
7918+ " . Misc::escapeInteger($_POST["project"]) . ",
7919+ " . $options["category"] . ",
7920+ 0,
7921+ " . $options["priority"] . ",
7922+ " . $options["reporter"] . ",";
7923+ if (!empty($initial_status)) {
7924+ $stmt .= "$initial_status,";
7925+ }
7926+ $stmt .= "
7927+ '" . Date_Helper::getCurrentDateGMT() . "',
7928+ '" . Date_Helper::getCurrentDateGMT() . "',
7929+ 'created',
7930+ '" . Misc::escapeString($_POST["summary"]) . "',
7931+ '" . Misc::escapeString($_POST["description"]) . "',
7932+ '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
7933+ )";
7934+ $res = DB_Helper::getInstance()->query($stmt);
7935+ if (PEAR::isError($res)) {
7936+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
7937+ return $res;
7938+ } else {
7939+ $new_issue_id = DB_Helper::get_last_insert_id();
7940+ // log the creation of the issue
7941+ History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
7942+
7943+ // now process any files being uploaded
7944+ $found = 0;
7945+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
7946+ if (!@empty($_FILES["file"]["name"][$i])) {
7947+ $found = 1;
7948+ break;
7949+ }
7950+ }
7951+ if ($found) {
7952+ $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
7953+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
7954+ $filename = @$_FILES["file"]["name"][$i];
7955+ if (empty($filename)) {
7956+ continue;
7957+ }
7958+ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
7959+ if (!empty($blob)) {
7960+ Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
7961+ }
7962+ }
7963+ }
7964+ // need to process any custom fields ?
7965+ if (@count($_POST["custom_fields"]) > 0) {
7966+ foreach ($_POST["custom_fields"] as $fld_id => $value) {
7967+ Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
7968+ }
7969+ }
7970+
7971+ // now add the user/issue association
7972+ $assign = array();
7973+ $users = @$options["users"];
7974+ $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
7975+ for ($i = 0; $i < count($users); $i++) {
7976+ Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
7977+ self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
7978+ $assign[] = $users[$i];
7979+ }
7980+
7981+ Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
7982+
7983+ // also notify any users that want to receive emails anytime a new issue is created
7984+ Notification::notifyNewIssue($_POST['project'], $new_issue_id);
7985+
7986+ return $new_issue_id;
7987+ }
7988+ }
7989+
7990+
7991+ /**
7992+ * Method used to remove all issues associated with a specific list of
7993+ * projects.
7994+ *
7995+ * @access public
7996+ * @param array $ids The list of projects to look for
7997+ * @return boolean
7998+ */
7999+ function removeByProjects($ids)
8000+ {
8001+ $items = @implode(", ", Misc::escapeInteger($ids));
8002+ $stmt = "SELECT
8003+ iss_id
8004+ FROM
8005+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8006+ WHERE
8007+ iss_prj_id IN ($items)";
8008+ $res = DB_Helper::getInstance()->getCol($stmt);
8009+ if (PEAR::isError($res)) {
8010+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8011+ return false;
8012+ } else {
8013+ if (count($res) > 0) {
8014+ self::deleteAssociations($res);
8015+ Attachment::removeByIssues($res);
8016+ SCM::removeByIssues($res);
8017+ Impact_Analysis::removeByIssues($res);
8018+ self::deleteUserAssociations($res);
8019+ Note::removeByIssues($res);
8020+ Time_Tracking::removeByIssues($res);
8021+ Notification::removeByIssues($res);
8022+ Custom_Field::removeByIssues($res);
8023+ Phone_Support::removeByIssues($res);
8024+ History::removeByIssues($res);
8025+ // now really delete the issues
8026+ $items = implode(", ", $res);
8027+ $stmt = "DELETE FROM
8028+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8029+ WHERE
8030+ iss_id IN ($items)";
8031+ DB_Helper::getInstance()->query($stmt);
8032+ }
8033+ return true;
8034+ }
8035+ }
8036+
8037+
8038+ /**
8039+ * Method used to close off an issue.
8040+ *
8041+ * @access public
8042+ * @param integer $usr_id The user ID
8043+ * @param integer $issue_id The issue ID
8044+ * @param bool $send_notification Whether to send a notification about this action or not
8045+ * @param integer $resolution_id The resolution ID
8046+ * @param integer $status_id The status ID
8047+ * @param string $reason The reason for closing this issue
8048+ * @param string $send_notification_to Who this notification should be sent too
8049+ * @return integer 1 if the update worked, -1 otherwise
8050+ */
8051+ function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
8052+ {
8053+ $usr_id = Misc::escapeInteger($usr_id);
8054+ $issue_id = Misc::escapeInteger($issue_id);
8055+ $resolution_id = Misc::escapeInteger($resolution_id);
8056+ $status_id = Misc::escapeInteger($status_id);
8057+
8058+ $stmt = "UPDATE
8059+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8060+ SET
8061+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
8062+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
8063+ iss_last_public_action_type='closed',
8064+ iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
8065+ if (!empty($resolution_id)) {
8066+ $stmt .= "iss_res_id=$resolution_id,\n";
8067+ }
8068+ $stmt .= "iss_sta_id=$status_id
8069+ WHERE
8070+ iss_id=$issue_id";
8071+ $res = DB_Helper::getInstance()->query($stmt);
8072+ if (PEAR::isError($res)) {
8073+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8074+ return -1;
8075+ } else {
8076+ self::moveOrderForAllUsers($issue_id, 1000);
8077+ $prj_id = self::getProjectID($issue_id);
8078+
8079+ // record the change
8080+ History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
8081+
8082+ if ($send_notification_to == 'all') {
8083+
8084+ $from = User::getFromHeader($usr_id);
8085+ $message_id = User::getFromHeader($usr_id);
8086+ $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
8087+ '', '', 'Issue closed comments', $reason, '');
8088+
8089+ $structure = Mime_Helper::decode($full_email, true, false);
8090+
8091+ $email = array(
8092+ 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
8093+ 'issue_id' => $issue_id,
8094+ 'message_id' => $message_id,
8095+ 'date' => Date_Helper::getCurrentDateGMT(),
8096+ 'subject' => 'Issue closed comments',
8097+ 'from' => $from,
8098+ 'has_attachment'=> 0,
8099+ 'body' => $reason,
8100+ 'full_email' => $full_email,
8101+ 'headers' => $structure->headers
8102+ );
8103+ Support::insertEmail($email, $structure, $sup_id, true);
8104+ $ids = $sup_id;
8105+ } else {
8106+ // add note with the reason to close the issue
8107+ $_POST['title'] = 'Issue closed comments';
8108+ $_POST["note"] = $reason;
8109+ Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
8110+ $ids = false;
8111+ }
8112+
8113+ if ($send_notification) {
8114+ if (Customer::hasCustomerIntegration($prj_id)) {
8115+ // send a special confirmation email when customer issues are closed
8116+ $stmt = "SELECT
8117+ iss_customer_contact_id
8118+ FROM
8119+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8120+ WHERE
8121+ iss_id=$issue_id";
8122+ $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
8123+ if (!empty($customer_contact_id)) {
8124+ Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
8125+ }
8126+ }
8127+ // send notifications for the issue being closed
8128+ Notification::notify($issue_id, 'closed', $ids);
8129+ }
8130+ Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
8131+ return 1;
8132+ }
8133+ }
8134+
8135+
8136+ /**
8137+ * Method used to update the details of a specific issue.
8138+ *
8139+ * @access public
8140+ * @param integer $issue_id The issue ID
8141+ * @return integer 1 if the update worked, -1 or -2 otherwise
8142+ */
8143+ function update($issue_id)
8144+ {
8145+ global $errors;
8146+ $errors = array();
8147+
8148+ $issue_id = Misc::escapeInteger($issue_id);
8149+
8150+ $usr_id = Auth::getUserID();
8151+ $prj_id = self::getProjectID($issue_id);
8152+
8153+ $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
8154+ if ($workflow !== true) {
8155+ return $workflow;
8156+ }
8157+
8158+ // get all of the 'current' information of this issue
8159+ $current = self::getDetails($issue_id);
8160+ // update the issue associations
8161+ if (empty($_POST['associated_issues'])) {
8162+ $associated_issues = array();
8163+ } else {
8164+ $associated_issues = explode(',', @$_POST['associated_issues']);
8165+ // make sure all associated issues are valid (and in this project)
8166+ for ($i = 0; $i < count($associated_issues); $i++) {
8167+ if (!self::exists(trim($associated_issues[$i]), false)) {
8168+ $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
8169+ unset($associated_issues[$i]);
8170+ }
8171+ }
8172+ }
8173+ $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
8174+ if (count($association_diff) > 0) {
8175+ // go through the new assocations, if association already exists, skip it
8176+ $associations_to_remove = $current['associated_issues'];
8177+ if (count($associated_issues) > 0) {
8178+ foreach ($associated_issues as $index => $associated_id) {
8179+ if (!in_array($associated_id, $current['associated_issues'])) {
8180+ self::addAssociation($issue_id, $associated_id, $usr_id);
8181+ } else {
8182+ // already assigned, remove this user from list of users to remove
8183+ unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
8184+ }
8185+ }
8186+ }
8187+ if (count($associations_to_remove) > 0) {
8188+ foreach ($associations_to_remove as $associated_id) {
8189+ self::deleteAssociation($issue_id, $associated_id);
8190+ }
8191+ }
8192+ }
8193+ $assignments_changed = false;
8194+ if (@$_POST["keep_assignments"] == "no") {
8195+ // only change the issue-user associations if there really were any changes
8196+ $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
8197+ if (!empty($_POST['assignments'])) {
8198+ $new_assignees = @$_POST['assignments'];
8199+ } else {
8200+ $new_assignees = array();
8201+ }
8202+ $assignment_notifications = array();
8203+
8204+ // remove people from the assignment list, if appropriate
8205+ foreach ($old_assignees as $assignee) {
8206+ if (!in_array($assignee, $new_assignees)) {
8207+ self::deleteUserAssociation($issue_id, $assignee);
8208+ $assignments_changed = true;
8209+ }
8210+ }
8211+ // add people to the assignment list, if appropriate
8212+ foreach ($new_assignees as $assignee) {
8213+ if (!in_array($assignee, $old_assignees)) {
8214+ self::addUserAssociation($usr_id, $issue_id, $assignee);
8215+ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
8216+ $assignment_notifications[] = $assignee;
8217+ $assignments_changed = true;
8218+ }
8219+ }
8220+ if (count($assignment_notifications) > 0) {
8221+ Notification::notifyNewAssignment($assignment_notifications, $issue_id);
8222+ }
8223+ }
8224+ if (empty($_POST["estimated_dev_time"])) {
8225+ $_POST["estimated_dev_time"] = 0;
8226+ }
8227+ $stmt = "UPDATE
8228+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8229+ SET
8230+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
8231+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
8232+ iss_last_public_action_type='updated',";
8233+ if (!empty($_POST["category"])) {
8234+ $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
8235+ }
8236+ if (@$_POST["keep"] == "no") {
8237+ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
8238+ }
8239+ if (!empty($_POST['expected_resolution_date'])) {
8240+ $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
8241+ } else {
8242+ $stmt .= "iss_expected_resolution_date=null,";
8243+ }
8244+ $stmt .= "
8245+ iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
8246+ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
8247+ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
8248+ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
8249+ iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
8250+ iss_description='" . Misc::escapeString($_POST["description"]) . "',
8251+ iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
8252+ iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
8253+ iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
8254+ iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
8255+ if (isset($_POST['private'])) {
8256+ $stmt .= ",
8257+ iss_private = " . Misc::escapeInteger($_POST['private']);
8258+ }
8259+ $stmt .= "
8260+ WHERE
8261+ iss_id=$issue_id";
8262+ $res = DB_Helper::getInstance()->query($stmt);
8263+ if (PEAR::isError($res)) {
8264+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8265+ return -1;
8266+ } else {
8267+ // add change to the history (only for changes on specific fields?)
8268+ $updated_fields = array();
8269+ if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
8270+ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
8271+ }
8272+ if ($current["iss_prc_id"] != $_POST["category"]) {
8273+ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
8274+ }
8275+ if ($current["iss_pre_id"] != $_POST["release"]) {
8276+ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
8277+ }
8278+ if ($current["iss_pri_id"] != $_POST["priority"]) {
8279+ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
8280+ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
8281+ }
8282+ if ($current["iss_sta_id"] != $_POST["status"]) {
8283+ // clear out the last-triggered-reminder flag when changing the status of an issue
8284+ Reminder_Action::clearLastTriggered($issue_id);
8285+
8286+ // if old status was closed and new status is not, clear closed data from issue.
8287+ $old_status_details = Status::getDetails($current['iss_sta_id']);
8288+ if ($old_status_details['sta_is_closed'] == 1) {
8289+ $new_status_details = Status::getDetails($_POST["status"]);
8290+ if ($new_status_details['sta_is_closed'] != 1) {
8291+ self::clearClosed($issue_id);
8292+ }
8293+ }
8294+ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
8295+ }
8296+ if ($current["iss_res_id"] != $_POST["resolution"]) {
8297+ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
8298+ }
8299+ if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
8300+ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
8301+ }
8302+ if ($current["iss_summary"] != $_POST["summary"]) {
8303+ $updated_fields["Summary"] = '';
8304+ }
8305+ if ($current["iss_description"] != $_POST["description"]) {
8306+ $updated_fields["Description"] = '';
8307+ }
8308+ if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
8309+ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
8310+ }
8311+ if (count($updated_fields) > 0) {
8312+ // log the changes
8313+ $changes = '';
8314+ $i = 0;
8315+ foreach ($updated_fields as $key => $value) {
8316+ if ($i > 0) {
8317+ $changes .= "; ";
8318+ }
8319+ if (($key != "Summary") && ($key != "Description")) {
8320+ $changes .= "$key: $value";
8321+ } else {
8322+ $changes .= "$key";
8323+ }
8324+ $i++;
8325+ }
8326+ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
8327+ // send notifications for the issue being updated
8328+ Notification::notifyIssueUpdated($issue_id, $current, $_POST);
8329+ }
8330+
8331+ // record group change as a seperate change
8332+ if ($current["iss_grp_id"] != (int)$_POST["group"]) {
8333+ History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
8334+ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
8335+ }
8336+
8337+ // now update any duplicates, if any
8338+ $update_dupe = array(
8339+ 'Category',
8340+ 'Release',
8341+ 'Priority',
8342+ 'Release',
8343+ 'Resolution'
8344+ );
8345+ // COMPAT: the following line requires PHP > 4.0.4
8346+ $intersect = array_intersect($update_dupe, array_keys($updated_fields));
8347+ if (($current["duplicates"] != '') && (count($intersect) > 0)) {
8348+ self::updateDuplicates($issue_id);
8349+ }
8350+
8351+ // if there is customer integration, mark last customer action
8352+ if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
8353+ self::recordLastCustomerAction($issue_id);
8354+ }
8355+
8356+ if ($assignments_changed) {
8357+ // XXX: we may want to also send the email notification for those "new" assignees
8358+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
8359+ }
8360+
8361+ Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
8362+ // Move issue to another project
8363+ if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
8364+ $new_prj_id = (int)@$_POST['new_prj'];
8365+ if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
8366+ if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
8367+ $res = self::moveIssue($issue_id, $new_prj_id);
8368+ if ($res == -1) {
8369+ return $res;
8370+ }
8371+ } else {
8372+ return -1;
8373+ }
8374+ }
8375+ }
8376+ return 1;
8377+ }
8378+ }
8379+
8380+ /**
8381+ * Method used to update the a single detail field of a specific issue.
8382+ *
8383+ * @param integer $issue_id
8384+ * @param string $field_name
8385+ * @param string $field_value
8386+ * @param string $field_type string or integer (for escape)
8387+ * @return integer 1 on success, -1 otherwise
8388+ */
8389+ function updateField($issue_id, $field_name, $filed_value) {
8390+
8391+ $issue_id = Misc::escapeInteger($issue_id);
8392+
8393+ $usr_id = Auth::getUserID();
8394+ $prj_id = self::getProjectID($issue_id);
8395+
8396+ // get all of the 'current' information of this issue
8397+ $current = self::getDetails($issue_id);
8398+
8399+ $stmt = "UPDATE
8400+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8401+ SET
8402+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
8403+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
8404+ iss_last_public_action_type='updated'";
8405+
8406+ switch ($field_name) {
8407+ case 'category':
8408+ $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
8409+ break;
8410+ case 'release':
8411+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
8412+ break;
8413+ case 'expected_resolution_date':
8414+ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
8415+ break;
8416+ case 'release':
8417+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
8418+ break;
8419+ case 'priority':
8420+ $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
8421+ break;
8422+ case 'status':
8423+ $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
8424+ break;
8425+ case 'resolution':
8426+ $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
8427+ break;
8428+ case 'summary':
8429+ $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
8430+ break;
8431+ case 'description':
8432+ $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
8433+ break;
8434+ case 'estimated_dev_time':
8435+ $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
8436+ break;
8437+ case 'percent_complete':
8438+ $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
8439+ break;
8440+ case 'trigger_reminders':
8441+ $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
8442+ break;
8443+ case 'group':
8444+ $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
8445+ break;
8446+ case 'private':
8447+ $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
8448+ break;
8449+ default:
8450+ Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
8451+ return -1;
8452+ break;
8453+ }
8454+
8455+ $stmt .= "
8456+ WHERE
8457+ iss_id=$issue_id";
8458+
8459+ $res = DB_Helper::getInstance()->query($stmt);
8460+ if (PEAR::isError($res)) {
8461+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8462+ return -1;
8463+ } else {
8464+ $new = array(
8465+ 'category' => $current['iss_prc_id'],
8466+ 'release' => $current['iss_pre_id'],
8467+ 'expected_resolution_date' => $current['iss_expected_resolution_date'],
8468+ 'release' => $current['iss_pre_id'],
8469+ 'priority' => $current['iss_pri_id'],
8470+ 'status' => $current['iss_sta_id'],
8471+ 'resolution' => $current['iss_res_id'],
8472+ 'summary' => $current['iss_summary'],
8473+ 'description' => $current['iss_description'],
8474+ 'estimated_dev_time' => $current['iss_dev_time'],
8475+ 'percent_complete' => $current['iss_percent_complete'],
8476+ 'trigger_reminders' => $current['iss_trigger_reminders'],
8477+ 'group' => $current['iss_grp_id'],
8478+ 'iss_private' => $current['private']
8479+ );
8480+ $new[$field_name] = $filed_value;
8481+
8482+ // add change to the history (only for changes on specific fields?)
8483+ $updated_fields = array();
8484+ if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
8485+ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
8486+ }
8487+ if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
8488+ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
8489+ }
8490+ if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
8491+ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
8492+ }
8493+ if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
8494+ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
8495+ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
8496+ }
8497+ if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
8498+ // clear out the last-triggered-reminder flag when changing the status of an issue
8499+ Reminder_Action::clearLastTriggered($issue_id);
8500+
8501+ // if old status was closed and new status is not, clear closed data from issue.
8502+ $old_status_details = Status::getDetails($current['iss_sta_id']);
8503+ if ($old_status_details['sta_is_closed'] == 1) {
8504+ $new_status_details = Status::getDetails($filed_value);
8505+ if ($new_status_details['sta_is_closed'] != 1) {
8506+ self::clearClosed($issue_id);
8507+ }
8508+ }
8509+ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
8510+ }
8511+ if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
8512+ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
8513+ }
8514+ if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
8515+ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
8516+ }
8517+ if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
8518+ $updated_fields["Summary"] = '';
8519+ }
8520+ if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
8521+ $updated_fields["Description"] = '';
8522+ }
8523+ if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
8524+ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
8525+ }
8526+ if (count($updated_fields) > 0) {
8527+ // log the changes
8528+ $changes = '';
8529+ $i = 0;
8530+ foreach ($updated_fields as $key => $value) {
8531+ if ($i > 0) {
8532+ $changes .= "; ";
8533+ }
8534+ if (($key != "Summary") && ($key != "Description")) {
8535+ $changes .= "$key: $value";
8536+ } else {
8537+ $changes .= "$key";
8538+ }
8539+ $i++;
8540+ }
8541+
8542+ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
8543+ // send notifications for the issue being updated
8544+ Notification::notifyIssueUpdated($issue_id, $current, $new);
8545+ }
8546+ }
8547+ return 1;
8548+ }
8549+
8550+
8551+ /**
8552+ * Move the issue to a new project
8553+ *
8554+ * @param integer $issue_id
8555+ * @param integer $new_prj_id
8556+ * @return integer 1 on success, -1 otherwise
8557+ */
8558+ function moveIssue($issue_id, $new_prj_id)
8559+ {
8560+ $stmt = "UPDATE
8561+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8562+ SET
8563+ iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
8564+ WHERE
8565+ iss_id = " . Misc::escapeInteger($issue_id);
8566+ $res = DB_Helper::getInstance()->query($stmt);
8567+ if (PEAR::isError($res)) {
8568+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8569+ return -1;
8570+ } else {
8571+ $currentDetails = self::getDetails($issue_id);
8572+
8573+ // set new category
8574+ $new_iss_prc_list = Category::getAssocList($new_prj_id);
8575+ $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
8576+ $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
8577+ if ($new_prc_id === false) {
8578+ // use the first category listed in the new project
8579+ $new_prc_id = key($new_iss_prc_list);
8580+ }
8581+
8582+ // set new priority
8583+ $new_iss_pri_list = Priority::getAssocList($new_prj_id);
8584+ $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
8585+ $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
8586+ if ($new_pri_id === false) {
8587+ // use the first category listed in the new project
8588+ $new_pri_id = key($new_iss_pri_list);
8589+ }
8590+
8591+ // XXX: Set status if needed when moving issue
8592+
8593+ $stmt = "UPDATE
8594+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
8595+ SET
8596+ iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
8597+ iss_pri_id=" . $new_pri_id . "
8598+ WHERE
8599+ iss_id=$issue_id";
8600+ $res = DB_Helper::getInstance()->query($stmt);
8601+ if (PEAR::isError($res)) {
8602+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8603+ }
8604+
8605+ // clear project cache
8606+ self::getProjectID($issue_id, true);
8607+
8608+ Notification::notifyNewIssue($new_prj_id, $issue_id);
8609+ }
8610+ }
8611+
8612+
8613+ /**
8614+ * Method used to associate an existing issue with another one.
8615+ *
8616+ * @access public
8617+ * @param integer $issue_id The issue ID
8618+ * @param integer $issue_id The other issue ID
8619+ * @return void
8620+ */
8621+ function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
8622+ {
8623+ $issue_id = Misc::escapeInteger($issue_id);
8624+ $associated_id = Misc::escapeInteger($associated_id);
8625+
8626+ $stmt = "INSERT INTO
8627+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
8628+ (
8629+ isa_issue_id,
8630+ isa_associated_id
8631+ ) VALUES (
8632+ $issue_id,
8633+ $associated_id
8634+ )";
8635+ DB_Helper::getInstance()->query($stmt);
8636+ History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
8637+ // link the associated issue back to this one
8638+ if ($link_issues) {
8639+ self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
8640+ }
8641+ }
8642+
8643+
8644+ /**
8645+ * Method used to remove the issue associations related to a specific issue.
8646+ *
8647+ * @access public
8648+ * @param integer $issue_id The issue ID
8649+ * @return void
8650+ */
8651+ function deleteAssociations($issue_id, $usr_id = FALSE)
8652+ {
8653+ $issue_id = Misc::escapeInteger($issue_id);
8654+ if (is_array($issue_id)) {
8655+ $issue_id = implode(", ", $issue_id);
8656+ }
8657+ $stmt = "DELETE FROM
8658+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
8659+ WHERE
8660+ isa_issue_id IN ($issue_id) OR
8661+ isa_associated_id IN ($issue_id)";
8662+ DB_Helper::getInstance()->query($stmt);
8663+ if ($usr_id) {
8664+ History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
8665+ }
8666+ }
8667+
8668+
8669+ /**
8670+ * Method used to remove a issue association from an issue.
8671+ *
8672+ * @access public
8673+ * @param integer $issue_id The issue ID
8674+ * @param integer $associated_id The associated issue ID to remove.
8675+ * @return void
8676+ */
8677+ function deleteAssociation($issue_id, $associated_id)
8678+ {
8679+ $issue_id = Misc::escapeInteger($issue_id);
8680+ $associated_id = Misc::escapeInteger($associated_id);
8681+ $stmt = "DELETE FROM
8682+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
8683+ WHERE
8684+ (
8685+ isa_issue_id = $issue_id AND
8686+ isa_associated_id = $associated_id
8687+ ) OR
8688+ (
8689+ isa_issue_id = $associated_id AND
8690+ isa_associated_id = $issue_id
8691+ )";
8692+ DB_Helper::getInstance()->query($stmt);
8693+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
8694+ "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
8695+ History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
8696+ "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
8697+ }
8698+
8699+
8700+ /**
8701+ * Method used to assign an issue with an user.
8702+ *
8703+ * @access public
8704+ * @param integer $usr_id The user ID of the person performing this change
8705+ * @param integer $issue_id The issue ID
8706+ * @param integer $assignee_usr_id The user ID of the assignee
8707+ * @param boolean $add_history Whether to add a history entry about this or not
8708+ * @return integer 1 if the update worked, -1 otherwise
8709+ */
8710+ function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
8711+ {
8712+ $issue_id = Misc::escapeInteger($issue_id);
8713+ $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
8714+ $order = 1;
8715+ // move all orders down to free "order space" for this new association
8716+ $stmt = "UPDATE
8717+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8718+ SET
8719+ isu_order = isu_order + 1
8720+ WHERE
8721+ isu_usr_id = $assignee_usr_id AND
8722+ isu_order >= $order";
8723+ $res = DB_Helper::getInstance()->query($stmt);
8724+ if (PEAR::isError($res)) {
8725+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8726+ return -1;
8727+ }
8728+ // insert the new association
8729+ $stmt = "INSERT INTO
8730+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8731+ (
8732+ isu_iss_id,
8733+ isu_usr_id,
8734+ isu_assigned_date,
8735+ isu_order
8736+ ) VALUES (
8737+ $issue_id,
8738+ $assignee_usr_id,
8739+ '" . Date_Helper::getCurrentDateGMT() . "',
8740+ $order
8741+ )";
8742+ $res = DB_Helper::getInstance()->query($stmt);
8743+ if (PEAR::isError($res)) {
8744+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8745+ return -1;
8746+ } else {
8747+ if ($add_history) {
8748+ History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
8749+ 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
8750+ }
8751+ return 1;
8752+ }
8753+ }
8754+
8755+ /**
8756+ * Method used to get the order list to be rearranged
8757+ *
8758+ * @access private
8759+ * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
8760+ * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
8761+ * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
8762+ */
8763+ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
8764+ {
8765+ // find all affected associantion orders
8766+ $stmt = "SELECT isu_usr_id, isu_order FROM
8767+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8768+ WHERE
8769+ isu_iss_id IN ($issue_id)";
8770+ if ($usr_id !== FALSE) {
8771+ $stmt.= " AND isu_usr_id IN ($usr_id)";
8772+ }
8773+ $stmt.= "ORDER BY isu_order";
8774+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
8775+ if (PEAR::isError($res)) {
8776+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8777+ return -1;
8778+ } else {
8779+ $deleted_orders = array();
8780+ foreach ($res as $row) {
8781+ if (empty($deleted_orders[$row['isu_usr_id']])) {
8782+ $deleted_orders[$row['isu_usr_id']] = array();
8783+ }
8784+ $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
8785+ }
8786+ return $deleted_orders;
8787+ }
8788+ }
8789+
8790+ /**
8791+ *
8792+ * Method used to rearrange order list in the db according to known deleted records
8793+ *
8794+ * @access private
8795+ * @param mixed deleteorder list
8796+ * @return void
8797+ */
8798+ function rearrangeDeleteUserAssociationOrderList($delete_order_list)
8799+ {
8800+ if (empty($delete_order_list) || (!is_array($delete_order_list))) {
8801+ return -1;
8802+ }
8803+ foreach ($delete_order_list as $isu_usr_id => $orders) {
8804+ for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
8805+ // move the orders after them up to take the "order space" of the deleted records
8806+ $stmt = "UPDATE
8807+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8808+ SET
8809+ isu_order = isu_order - " . ($i+1) . "
8810+ WHERE
8811+ isu_usr_id = $isu_usr_id AND
8812+ isu_order > " . $orders[$i];
8813+ if ($i < count($orders) - 1) {
8814+ $stmt.= " AND
8815+ isu_order < " . $orders[$i+1];
8816+ }
8817+ $res = DB_Helper::getInstance()->query($stmt);
8818+ if (PEAR::isError($res)) {
8819+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8820+ return -1;
8821+ }
8822+ }
8823+ }
8824+ return 1;
8825+ }
8826+
8827+
8828+ /**
8829+ * Method used to delete all user assignments for a specific issue.
8830+ *
8831+ * @access public
8832+ * @param integer $issue_id The issue ID
8833+ * @param integer $usr_id The user ID of the person performing the change
8834+ * @return void
8835+ */
8836+ function deleteUserAssociations($issue_id, $usr_id = FALSE)
8837+ {
8838+ $issue_id = Misc::escapeInteger($issue_id);
8839+ if (is_array($issue_id)) {
8840+ $issue_id = implode(", ", $issue_id);
8841+ }
8842+ $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
8843+ $stmt = "DELETE FROM
8844+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8845+ WHERE
8846+ isu_iss_id IN ($issue_id)";
8847+ $res = DB_Helper::getInstance()->query($stmt);
8848+ if (PEAR::isError($res)) {
8849+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8850+ return -1;
8851+ } else {
8852+ if ($usr_id) {
8853+ History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
8854+ }
8855+ self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
8856+ return 1;
8857+ }
8858+ }
8859+
8860+
8861+ /**
8862+ * Method used to delete a single user assignments for a specific issue.
8863+ *
8864+ * @access public
8865+ * @param integer $issue_id The issue ID
8866+ * @param integer $usr_id The user to remove.
8867+ * @param boolean $add_history Whether to add a history entry about this or not
8868+ * @return void
8869+ */
8870+ function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
8871+ {
8872+ $issue_id = Misc::escapeInteger($issue_id);
8873+ $usr_id = Misc::escapeInteger($usr_id);
8874+ $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
8875+ $stmt = "DELETE FROM
8876+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
8877+ WHERE
8878+ isu_iss_id = $issue_id AND
8879+ isu_usr_id = $usr_id";
8880+ $res = DB_Helper::getInstance()->query($stmt);
8881+ if (PEAR::isError($res)) {
8882+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
8883+ return -1;
8884+ } else {
8885+ if ($add_history) {
8886+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
8887+ User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
8888+ }
8889+ self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
8890+ return 1;
8891+ }
8892+ }
8893+
8894+
8895+ /**
8896+ * Creates an issue with the given email information.
8897+ *
8898+ * @access public
8899+ * @param integer $prj_id The project ID
8900+ * @param integer $usr_id The user responsible for this action
8901+ * @param string $sender The original sender of this email
8902+ * @param string $summary The issue summary
8903+ * @param string $description The issue description
8904+ * @param integer $category The category ID
8905+ * @param integer $priority The priority ID
8906+ * @param array $assignment The list of users to assign this issue to
8907+ * @param string $date The date the email was originally sent.
8908+ * @param string $msg_id The message ID of the email we are creating this issue from.
8909+ * @return void
8910+ */
8911+ function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
8912+ {
8913+ $data = array();
8914+ $exclude_list = array();
8915+
8916+ $sender_email = Mail_Helper::getEmailAddress($sender);
8917+ $sender_usr_id = User::getUserIDByEmail($sender_email, true);
8918+ if (!empty($sender_usr_id)) {
8919+ $reporter = $sender_usr_id;
8920+ $exclude_list[] = $sender_usr_id;
8921+ }
8922+
8923+ $data = array(
8924+ 'category' => $category,
8925+ 'priority' => $priority,
8926+ 'description' => $description,
8927+ 'summary' => $summary,
8928+ 'msg_id' => $msg_id,
8929+ );
8930+
8931+ if (Customer::hasCustomerIntegration($prj_id)) {
8932+ list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
8933+ if (!empty($customer_id)) {
8934+ $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
8935+ // overwrite the reporter with the customer contact
8936+ $reporter = User::getUserIDByContactID($customer_contact_id);
8937+ $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
8938+
8939+ $data['customer'] = $customer_id;
8940+ $data['contact'] = $customer_contact_id;
8941+# $data['contract'] = // XXX missing
8942+ $data['contact_person_lname'] = $contact['last_name'];
8943+ $data['contact_person_fname'] = $contact['first_name'];
8944+ $data['contact_email'] = $sender_email;
8945+ $data['contact_phone'] = $contact['phone'];
8946+ $data['contact_timezone'] = $contact_timezone;
8947+ }
8948+ } else {
8949+ $customer_id = FALSE;
8950+ }
8951+ if (empty($reporter)) {
8952+ $reporter = APP_SYSTEM_USER_ID;
8953+ }
8954+
8955+ $data['reporter'] = $reporter;
8956+
8957+ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
8958+ if ($issue_id == -1) {
8959+ return -1;
8960+ }
8961+
8962+ $has_TAM = false;
8963+ $has_RR = false;
8964+ // log the creation of the issue
8965+ History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
8966+
8967+ $emails = array();
8968+ $manager_usr_ids = array();
8969+ if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
8970+ // if there are any technical account managers associated with this customer, add these users to the notification list
8971+ $managers = Customer::getAccountManagers($prj_id, $customer_id);
8972+ $manager_usr_ids = array_keys($managers);
8973+ $manager_emails = array_values($managers);
8974+ $emails = array_merge($emails, $manager_emails);
8975+ }
8976+ // add the reporter to the notification list
8977+ $emails[] = $sender;
8978+ $emails = array_unique($emails);
8979+ $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
8980+ foreach ($emails as $address) {
8981+ Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
8982+ }
8983+
8984+ // only assign the issue to an user if the associated customer has any technical account managers
8985+ $users = array();
8986+ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
8987+ foreach ($manager_usr_ids as $manager_usr_id) {
8988+ $users[] = $manager_usr_id;
8989+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
8990+ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
8991+ }
8992+ $has_TAM = true;
8993+ }
8994+ // now add the user/issue association
8995+ if (@count($assignment) > 0) {
8996+ for ($i = 0; $i < count($assignment); $i++) {
8997+ Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
8998+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
8999+ if ($assignment[$i] != $usr_id) {
9000+ $users[] = $assignment[$i];
9001+ }
9002+ }
9003+ } else {
9004+ // only use the round-robin feature if this new issue was not
9005+ // already assigned to a customer account manager
9006+ if (@count($manager_usr_ids) < 1) {
9007+ $assignee = Round_Robin::getNextAssignee($prj_id);
9008+ // assign the issue to the round robin person
9009+ if (!empty($assignee)) {
9010+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
9011+ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
9012+ $users[] = $assignee;
9013+ $has_RR = true;
9014+ }
9015+ }
9016+ }
9017+ if (count($users) > 0) {
9018+ $has_assignee = true;
9019+ }
9020+
9021+ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
9022+
9023+ // send special 'an issue was auto-created for you' notification back to the sender
9024+ Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
9025+
9026+ // also notify any users that want to receive emails anytime a new issue is created
9027+ Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
9028+
9029+ return $issue_id;
9030+ }
9031+
9032+
9033+ /**
9034+ * Return errors that happened when creating new issue from POST method.
9035+ *
9036+ * @return array
9037+ */
9038+ private static $insert_errors = array();
9039+ static function getInsertErrors() {
9040+ return self::$insert_errors;
9041+ }
9042+
9043+ /**
9044+ * Method used to add a new issue using the normal report form.
9045+ *
9046+ * @access public
9047+ * @return integer The new issue ID
9048+ */
9049+ function createFromPost()
9050+ {
9051+ $keys = array(
9052+ 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
9053+ 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
9054+ 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
9055+ );
9056+ $data = array();
9057+ foreach ($keys as $key) {
9058+ if (isset($_POST[$key])) {
9059+ $data[$key] = $_POST[$key];
9060+ }
9061+ }
9062+
9063+ $prj_id = Auth::getCurrentProject();
9064+ $usr_id = Auth::getUserID();
9065+
9066+ // if we are creating an issue for a customer, put the
9067+ // main customer contact as the reporter for it
9068+ if (Customer::hasCustomerIntegration($prj_id)) {
9069+ $contact_usr_id = User::getUserIDByContactID($data['contact']);
9070+ if (empty($contact_usr_id)) {
9071+ $contact_usr_id = $usr_id;
9072+ }
9073+ $data['reporter'] = $contact_usr_id;
9074+ } else {
9075+ $data['reporter'] = $usr_id;
9076+ }
9077+
9078+ $data['msg_id'] = Mail_Helper::generateMessageID();
9079+
9080+ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
9081+ if ($issue_id == -1) {
9082+ return -1;
9083+ }
9084+
9085+ $has_TAM = false;
9086+ $has_RR = false;
9087+ $info = User::getNameEmail($usr_id);
9088+ // log the creation of the issue
9089+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
9090+
9091+ $emails = array();
9092+ if (Customer::hasCustomerIntegration($prj_id)) {
9093+ if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
9094+ $emails = $data['contact_extra_emails'];
9095+ }
9096+ // add the primary contact to the notification list
9097+ if ($data['add_primary_contact'] == 'yes') {
9098+ $contact_email = User::getEmailByContactID($data['contact']);
9099+ if (!empty($contact_email)) {
9100+ $emails[] = $contact_email;
9101+ }
9102+ }
9103+ // if there are any technical account managers associated with this customer, add these users to the notification list
9104+ $managers = Customer::getAccountManagers($prj_id, $data['customer']);
9105+ $manager_usr_ids = array_keys($managers);
9106+ $manager_emails = array_values($managers);
9107+ $emails = array_merge($emails, $manager_emails);
9108+ }
9109+ // add the reporter to the notification list
9110+ $emails[] = $info['usr_email'];
9111+ $emails = array_unique($emails);
9112+ foreach ($emails as $address) {
9113+ Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
9114+ }
9115+
9116+ // only assign the issue to an user if the associated customer has any technical account managers
9117+ $users = array();
9118+ $has_TAM = false;
9119+ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
9120+ foreach ($manager_usr_ids as $manager_usr_id) {
9121+ $users[] = $manager_usr_id;
9122+ self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
9123+ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
9124+ }
9125+ $has_TAM = true;
9126+ }
9127+ // now add the user/issue association (aka assignments)
9128+ if (!empty($data['users']) && count($data['users']) > 0) {
9129+ for ($i = 0; $i < count($data['users']); $i++) {
9130+ Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
9131+ Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
9132+ self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
9133+ if ($data['users'][$i] != $usr_id) {
9134+ $users[] = $data['users'][$i];
9135+ }
9136+ }
9137+ } else {
9138+ // only use the round-robin feature if this new issue was not
9139+ // already assigned to a customer account manager
9140+ if (@count($manager_usr_ids) < 1) {
9141+ $assignee = Round_Robin::getNextAssignee($prj_id);
9142+ // assign the issue to the round robin person
9143+ if (!empty($assignee)) {
9144+ $users[] = $assignee;
9145+ self::addUserAssociation($usr_id, $issue_id, $assignee, false);
9146+ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
9147+ $has_RR = true;
9148+ }
9149+ }
9150+ }
9151+
9152+ // now process any files being uploaded
9153+ $found = 0;
9154+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
9155+ if (!@empty($_FILES["file"]["name"][$i])) {
9156+ $found = 1;
9157+ break;
9158+ }
9159+ }
9160+ if ($found) {
9161+ $files = array();
9162+ for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
9163+ $filename = @$_FILES["file"]["name"][$i];
9164+ if (empty($filename)) {
9165+ continue;
9166+ }
9167+ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
9168+ if (empty($blob)) {
9169+ // error reading a file
9170+ self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
9171+ continue;
9172+ }
9173+ $files[] = array(
9174+ "filename" => $filename,
9175+ "type" => $_FILES['file']['type'][$i],
9176+ "blob" => $blob
9177+ );
9178+ }
9179+ if (count($files) > 0) {
9180+ $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
9181+ foreach ($files as $file) {
9182+ Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
9183+ }
9184+ }
9185+ }
9186+ // need to associate any emails ?
9187+ if (!empty($data['attached_emails'])) {
9188+ $items = explode(",", $data['attached_emails']);
9189+ Support::associate($usr_id, $issue_id, $items);
9190+ }
9191+ // need to notify any emails being converted into issues ?
9192+ if (@count($data['notify_senders']) > 0) {
9193+ $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
9194+ } else {
9195+ $recipients = array();
9196+ }
9197+ // need to process any custom fields ?
9198+ if (@count($data['custom_fields']) > 0) {
9199+ foreach ($data['custom_fields'] as $fld_id => $value) {
9200+ Custom_Field::associateIssue($issue_id, $fld_id, $value);
9201+ }
9202+ }
9203+ // also send a special confirmation email to the customer contact
9204+ if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
9205+ // also need to pass the list of sender emails already notified,
9206+ // so we can avoid notifying the same person again
9207+ $contact_email = User::getEmailByContactID($data['contact']);
9208+ if (@!in_array($contact_email, $recipients)) {
9209+ Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
9210+ }
9211+ }
9212+
9213+ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
9214+
9215+ // also notify any users that want to receive emails anytime a new issue is created
9216+ Notification::notifyNewIssue($prj_id, $issue_id);
9217+
9218+ return $issue_id;
9219+ }
9220+
9221+ /**
9222+ * Insert issue to database.
9223+ *
9224+ * @param integer $prj_id The project ID
9225+ * @param integer $usr_id The user responsible for this action
9226+ * @param array $data of issue to be inserted
9227+ * @return integer The new issue ID
9228+ */
9229+ private function insertIssue($prj_id, $usr_id, $data)
9230+ {
9231+
9232+ // XXX missing_fields never used
9233+ $missing_fields = array();
9234+ if ($data['category'] == -1) {
9235+ $missing_fields[] = 'Category';
9236+ }
9237+ if ($data['priority'] == -1) {
9238+ $missing_fields[] = 'Priority';
9239+ }
9240+
9241+ // if there is no reporter set, use the system user
9242+ if (empty($data['reporter'])) {
9243+ $data['reporter'] = APP_SYSTEM_USER_ID;
9244+ }
9245+
9246+ if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
9247+ $data['estimated_dev_time'] = 0;
9248+ }
9249+
9250+ // add new issue
9251+ $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
9252+ "SET ".
9253+ "iss_prj_id=" . $prj_id . ",";
9254+ if (!empty($data['group'])) {
9255+ $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
9256+ }
9257+ if (!empty($data['category'])) {
9258+ $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
9259+ }
9260+ if (!empty($data['release'])) {
9261+ $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
9262+ }
9263+ if (!empty($data['priority'])) {
9264+ $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
9265+ }
9266+
9267+ $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
9268+
9269+ $initial_status = Project::getInitialStatus($prj_id);
9270+ if (!empty($initial_status)) {
9271+ $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
9272+ }
9273+
9274+ if (Customer::hasCustomerIntegration($prj_id)) {
9275+ $stmt .= "
9276+ iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
9277+ if (!empty($data['contact'])) {
9278+ $stmt .= "
9279+ iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
9280+ }
9281+ $stmt .= "
9282+ iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
9283+ iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
9284+ iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
9285+ iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
9286+ iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
9287+ iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
9288+ }
9289+
9290+ $stmt .= "
9291+ iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
9292+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
9293+ iss_last_public_action_type='created',
9294+ iss_summary='" . Misc::escapeString($data['summary']) . "',
9295+ iss_description='" . Misc::escapeString($data['description']) . "',
9296+ iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
9297+ if (!empty($data['contact'])) {
9298+ $stmt .= "
9299+ iss_private=" . Misc::escapeInteger($data['private']) . " ,";
9300+ }
9301+ $stmt .= "
9302+ iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
9303+ ";
9304+
9305+ $res = DB_Helper::getInstance()->query($stmt);
9306+ if (PEAR::isError($res)) {
9307+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
9308+ return -1;
9309+ }
9310+
9311+ $issue_id = DB_Helper::get_last_insert_id();
9312+ return $issue_id;
9313+ }
9314+
9315+
9316+ /**
9317+ * Method used to get a specific parameter in the issue listing cookie.
9318+ *
9319+ * @access public
9320+ * @param string $name The name of the parameter
9321+ * @return mixed The value of the specified parameter
9322+ */
9323+ function getParam($name)
9324+ {
9325+ $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
9326+
9327+ if (isset($_GET[$name])) {
9328+ return $_GET[$name];
9329+ } elseif (isset($_POST[$name])) {
9330+ return $_POST[$name];
9331+ } elseif (isset($profile[$name])) {
9332+ return $profile[$name];
9333+ } else {
9334+ return "";
9335+ }
9336+ }
9337+
9338+
9339+ /**
9340+ * Method used to save the current search parameters in a cookie.
9341+ *
9342+ * @access public
9343+ * @return array The search parameters
9344+ */
9345+ function saveSearchParams()
9346+ {
9347+ $sort_by = self::getParam('sort_by');
9348+ $sort_order = self::getParam('sort_order');
9349+ $users = self::getParam('users');
9350+ if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
9351+ unset($sort_by);
9352+ unset($sort_order);
9353+ }
9354+ $rows = self::getParam('rows');
9355+ $hide_closed = self::getParam('hide_closed');
9356+ if ($hide_closed === '') {
9357+ $hide_closed = 1;
9358+ }
9359+ $search_type = self::getParam('search_type');
9360+ if (empty($search_type)) {
9361+ $search_type = 'all_text';
9362+ }
9363+ $custom_field = self::getParam('custom_field');
9364+ if (is_string($custom_field)) {
9365+ $custom_field = unserialize(urldecode($custom_field));
9366+ }
9367+ $cookie = array(
9368+ 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
9369+ 'pagerRow' => self::getParam('pagerRow'),
9370+ 'hide_closed' => $hide_closed,
9371+ "sort_by" => $sort_by ? $sort_by : "pri_rank",
9372+ "sort_order" => $sort_order ? $sort_order : "ASC",
9373+ // quick filter form
9374+ 'keywords' => self::getParam('keywords'),
9375+ 'search_type' => $search_type,
9376+ 'users' => self::getParam('users'),
9377+ 'status' => self::getParam('status'),
9378+ 'priority' => self::getParam('priority'),
9379+ 'category' => self::getParam('category'),
9380+ 'customer_email' => self::getParam('customer_email'),
9381+ // advanced search form
9382+ 'show_authorized_issues' => self::getParam('show_authorized_issues'),
9383+ 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
9384+ 'reporter' => self::getParam('reporter'),
9385+ // other fields
9386+ 'release' => self::getParam('release'),
9387+ // custom fields
9388+ 'custom_field' => $custom_field
9389+ );
9390+ // now do some magic to properly format the date fields
9391+ $date_fields = array(
9392+ 'created_date',
9393+ 'updated_date',
9394+ 'last_response_date',
9395+ 'first_response_date',
9396+ 'closed_date'
9397+ );
9398+ foreach ($date_fields as $field_name) {
9399+ $field = self::getParam($field_name);
9400+ if (empty($field)) {
9401+ continue;
9402+ }
9403+ if (@$field['filter_type'] == 'in_past') {
9404+ @$cookie[$field_name] = array(
9405+ 'filter_type' => 'in_past',
9406+ 'time_period' => $field['time_period']
9407+ );
9408+ } else {
9409+ $end_field_name = $field_name . '_end';
9410+ $end_field = self::getParam($end_field_name);
9411+ @$cookie[$field_name] = array(
9412+ 'past_hour' => $field['past_hour'],
9413+ 'Year' => $field['Year'],
9414+ 'Month' => $field['Month'],
9415+ 'Day' => $field['Day'],
9416+ 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
9417+ 'filter_type' => $field['filter_type'],
9418+ 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
9419+ );
9420+ @$cookie[$end_field_name] = array(
9421+ 'Year' => $end_field['Year'],
9422+ 'Month' => $end_field['Month'],
9423+ 'Day' => $end_field['Day']
9424+ );
9425+ }
9426+ }
9427+ Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
9428+ return $cookie;
9429+ }
9430+
9431+
9432+ /**
9433+ * Method used to get the current sorting options used in the grid layout
9434+ * of the issue listing page.
9435+ *
9436+ * @access public
9437+ * @param array $options The current search parameters
9438+ * @return array The sorting options
9439+ */
9440+ function getSortingInfo($options)
9441+ {
9442+
9443+ $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
9444+
9445+ // default order for last action date, priority should be descending
9446+ // for textual fields, like summary, ascending is reasonable
9447+ $fields = array(
9448+ "pri_rank" => "desc",
9449+ "iss_id" => "desc",
9450+ "iss_customer_id" => "desc",
9451+ "prc_title" => "asc",
9452+ "sta_rank" => "asc",
9453+ "iss_created_date" => "desc",
9454+ "iss_summary" => "asc",
9455+ "last_action_date" => "desc",
9456+ "usr_full_name" => "asc",
9457+ "iss_expected_resolution_date" => "desc",
9458+ "pre_title" => "asc",
9459+ "assigned" => "asc",
9460+ "isu_order" => "desc",
9461+ );
9462+
9463+ foreach ($custom_fields as $fld_id => $fld_name) {
9464+ $fields['custom_field_' . $fld_id] = "desc";
9465+ }
9466+
9467+ $sortfields = array_combine(array_keys($fields), array_keys($fields));
9468+ $sortfields["pre_title"] = "pre_scheduled_date";
9469+ $sortfields["assigned"] = "isu_usr_id";
9470+
9471+ $items = array(
9472+ "links" => array(),
9473+ "images" => array()
9474+ );
9475+ foreach ($sortfields as $field => $sortfield) {
9476+ $sort_order = $fields[$field];
9477+ if ($options["sort_by"] == $sortfield) {
9478+ $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
9479+ if (strtolower($options["sort_order"]) == "asc") {
9480+ $sort_order = "desc";
9481+ } else {
9482+ $sort_order = "asc";
9483+ }
9484+ }
9485+ $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
9486+ }
9487+ return $items;
9488+ }
9489+
9490+
9491+ /**
9492+ * Returns the list of action date fields appropriate for the
9493+ * current user ID.
9494+ *
9495+ * @access public
9496+ * @return array The list of action date fields
9497+ */
9498+ function getLastActionFields()
9499+ {
9500+ $last_action_fields = array(
9501+ "iss_last_public_action_date"
9502+ );
9503+ if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
9504+ $last_action_fields[] = "iss_last_internal_action_date";
9505+ }
9506+ if (count($last_action_fields) > 1) {
9507+ return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
9508+ } else {
9509+ return $last_action_fields[0] . " AS last_action_date";
9510+ }
9511+ }
9512+
9513+
9514+ /**
9515+ * Method used to get the list of issues to be displayed in the grid layout.
9516+ *
9517+ * @access public
9518+ * @param integer $prj_id The current project ID
9519+ * @param array $options The search parameters
9520+ * @param integer $current_row The current page number
9521+ * @param integer $max The maximum number of rows per page
9522+ * @return array The list of issues to be displayed
9523+ */
9524+ function getListing($prj_id, $options, $current_row = 0, $max = 5)
9525+ {
9526+ if (strtoupper($max) == "ALL") {
9527+ $max = 9999999;
9528+ }
9529+ $start = $current_row * $max;
9530+ // get the current user's role
9531+ $usr_id = Auth::getUserID();
9532+ $role_id = User::getRoleByUser($usr_id, $prj_id);
9533+
9534+ // get any custom fields that should be displayed
9535+ $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
9536+
9537+ $stmt = "SELECT
9538+ iss_id,
9539+ iss_grp_id,
9540+ iss_prj_id,
9541+ iss_sta_id,
9542+ iss_customer_id,
9543+ iss_customer_contract_id,
9544+ iss_created_date,
9545+ iss_updated_date,
9546+ iss_last_response_date,
9547+ iss_closed_date,
9548+ iss_last_customer_action_date,
9549+ iss_usr_id,
9550+ iss_summary,
9551+ pri_title,
9552+ prc_title,
9553+ sta_title,
9554+ sta_color status_color,
9555+ sta_id,
9556+ iqu_status,
9557+ grp_name `group`,
9558+ pre_title,
9559+ iss_last_public_action_date,
9560+ iss_last_public_action_type,
9561+ iss_last_internal_action_date,
9562+ iss_last_internal_action_type,
9563+ " . self::getLastActionFields() . ",
9564+ IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
9565+ iss_private,
9566+ usr_full_name,
9567+ iss_percent_complete,
9568+ iss_dev_time,
9569+ iss_expected_resolution_date
9570+ FROM
9571+ (
9572+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
9573+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
9574+ // join custom fields if we are searching by custom fields
9575+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
9576+ foreach ($options['custom_field'] as $fld_id => $search_value) {
9577+ if (empty($search_value)) {
9578+ continue;
9579+ }
9580+ $field = Custom_Field::getDetails($fld_id);
9581+ if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
9582+ continue;
9583+ }
9584+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
9585+ continue;
9586+ }
9587+ if ($field['fld_type'] == 'multiple') {
9588+ $search_value = Misc::escapeInteger($search_value);
9589+ foreach ($search_value as $cfo_id) {
9590+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
9591+ }
9592+ } else {
9593+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
9594+ }
9595+ }
9596+ }
9597+ $stmt .= ")";
9598+ // check for the custom fields we want to sort by
9599+ if (strstr($options['sort_by'], 'custom_field') !== false) {
9600+ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
9601+ $stmt .= "\n LEFT JOIN \n" .
9602+ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
9603+ ON
9604+ (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
9605+ }
9606+ if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
9607+ $stmt .= "
9608+ LEFT JOIN
9609+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
9610+ ON
9611+ isu_iss_id=iss_id";
9612+ }
9613+ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
9614+ $stmt .= "
9615+ LEFT JOIN
9616+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
9617+ ON
9618+ iur_iss_id=iss_id";
9619+ }
9620+ if (!empty($options["show_notification_list_issues"])) {
9621+ $stmt .= "
9622+ LEFT JOIN
9623+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
9624+ ON
9625+ sub_iss_id=iss_id";
9626+ }
9627+ $stmt .= "
9628+ LEFT JOIN
9629+ " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
9630+ ON
9631+ iss_grp_id=grp_id
9632+ LEFT JOIN
9633+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
9634+ ON
9635+ iss_prc_id=prc_id
9636+ LEFT JOIN
9637+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
9638+ ON
9639+ iss_pre_id = pre_id
9640+ LEFT JOIN
9641+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
9642+ ON
9643+ iss_sta_id=sta_id
9644+ LEFT JOIN
9645+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
9646+ ON
9647+ iss_pri_id=pri_id
9648+ LEFT JOIN
9649+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
9650+ ON
9651+ iss_id=iqu_iss_id AND
9652+ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
9653+ WHERE
9654+ iss_prj_id= " . Misc::escapeInteger($prj_id);
9655+ $stmt .= self::buildWhereClause($options);
9656+
9657+ if (strstr($options["sort_by"], 'custom_field') !== false) {
9658+ $fld_details = Custom_Field::getDetails($fld_id);
9659+ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
9660+ } else {
9661+ $sort_by = Misc::escapeString($options["sort_by"]);
9662+ }
9663+
9664+ $stmt .= "
9665+ GROUP BY
9666+ iss_id
9667+ ORDER BY
9668+ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
9669+ iss_id DESC";
9670+ $total_rows = Pager::getTotalRows($stmt);
9671+ $stmt .= "
9672+ LIMIT
9673+ " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
9674+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
9675+ if (PEAR::isError($res)) {
9676+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
9677+ return array(
9678+ "list" => "",
9679+ "info" => ""
9680+ );
9681+ } else {
9682+ if (count($res) > 0) {
9683+ self::getAssignedUsersByIssues($res);
9684+ Time_Tracking::getTimeSpentByIssues($res);
9685+ // need to get the customer titles for all of these issues...
9686+ if (Customer::hasCustomerIntegration($prj_id)) {
9687+ Customer::getCustomerTitlesByIssues($prj_id, $res);
9688+ Customer::getSupportLevelsByIssues($prj_id, $res);
9689+ }
9690+ self::formatLastActionDates($res);
9691+ self::getLastStatusChangeDates($prj_id, $res);
9692+ } elseif ($current_row > 0) {
9693+ // if there are no results, and the page is not the first page reset page to one and reload results
9694+ Auth::redirect("list.php?pagerRow=0&rows=$max");
9695+ }
9696+ $groups = Group::getAssocList($prj_id);
9697+ $categories = Category::getAssocList($prj_id);
9698+ $column_headings = self::getColumnHeadings($prj_id);
9699+ if (count($custom_fields) > 0) {
9700+ $column_headings = array_merge($column_headings,$custom_fields);
9701+ }
9702+ $csv[] = @implode("\t", $column_headings);
9703+ for ($i = 0; $i < count($res); $i++) {
9704+ $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
9705+ $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
9706+ $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
9707+ $fields = array(
9708+ $res[$i]['pri_title'],
9709+ $res[$i]['iss_id'],
9710+ $res[$i]['usr_full_name'],
9711+ );
9712+ // hide the group column from the output if no
9713+ // groups are available in the database
9714+ if (count($groups) > 0) {
9715+ $fields[] = $res[$i]['group'];
9716+ }
9717+ $fields[] = $res[$i]['assigned_users'];
9718+ $fields[] = $res[$i]['time_spent'];
9719+ // hide the category column from the output if no
9720+ // categories are available in the database
9721+ if (count($categories) > 0) {
9722+ $fields[] = $res[$i]['prc_title'];
9723+ }
9724+ if (Customer::hasCustomerIntegration($prj_id)) {
9725+ $fields[] = @$res[$i]['customer_title'];
9726+ // check if current user is acustomer and has a per incident contract.
9727+ // if so, check if issue is redeemed.
9728+ if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
9729+ if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
9730+ (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
9731+ $res[$i]['redeemed'] = true;
9732+ }
9733+ }
9734+ }
9735+ $fields[] = $res[$i]['sta_title'];
9736+ $fields[] = $res[$i]["status_change_date"];
9737+ $fields[] = $res[$i]["last_action_date"];
9738+ $fields[] = $res[$i]['iss_dev_time'];
9739+ $fields[] = $res[$i]['iss_summary'];
9740+ $fields[] = $res[$i]['iss_expected_resolution_date'];
9741+
9742+ if (count($custom_fields) > 0) {
9743+ $res[$i]['custom_field'] = array();
9744+ $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
9745+ foreach ($custom_field_values as $this_field) {
9746+ if (!empty($custom_fields[$this_field['fld_id']])) {
9747+ $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
9748+ $fields[] = $this_field['value'];
9749+ }
9750+ }
9751+ }
9752+
9753+ $csv[] = @implode("\t", $fields);
9754+ }
9755+ $total_pages = ceil($total_rows / $max);
9756+ $last_page = $total_pages - 1;
9757+ return array(
9758+ "list" => $res,
9759+ "info" => array(
9760+ "current_page" => $current_row,
9761+ "start_offset" => $start,
9762+ "end_offset" => $start + count($res),
9763+ "total_rows" => $total_rows,
9764+ "total_pages" => $total_pages,
9765+ "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
9766+ "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
9767+ "last_page" => $last_page,
9768+ "custom_fields" => $custom_fields
9769+ ),
9770+ "csv" => @implode("\n", $csv)
9771+ );
9772+ }
9773+ }
9774+
9775+
9776+ /**
9777+ * Processes a result set to format the "Last Action Date" column.
9778+ *
9779+ * @access public
9780+ * @param array $result The result set
9781+ */
9782+ function formatLastActionDates(&$result)
9783+ {
9784+ for ($i = 0; $i < count($result); $i++) {
9785+ if (($result[$i]['action_type'] == "internal") &&
9786+ (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
9787+ $label = $result[$i]["iss_last_internal_action_type"];
9788+ $last_date = $result[$i]["iss_last_internal_action_date"];
9789+ } else {
9790+ $label = $result[$i]["iss_last_public_action_type"];
9791+ $last_date = $result[$i]["iss_last_public_action_date"];
9792+ }
9793+ $date = new Date($last_date);
9794+ $current = new Date(Date_Helper::getCurrentDateGMT());
9795+ $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
9796+ Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
9797+ }
9798+ }
9799+
9800+
9801+ /**
9802+ * Retrieves the last status change date for the given issue.
9803+ *
9804+ * @access public
9805+ * @param integer $prj_id The project ID
9806+ * @param array $result The associative array of data
9807+ * @see self::getListing()
9808+ */
9809+ function getLastStatusChangeDates($prj_id, &$result)
9810+ {
9811+ $ids = array();
9812+ for ($i = 0; $i < count($result); $i++) {
9813+ $ids[] = $result[$i]["iss_sta_id"];
9814+ }
9815+ if (count($ids) == 0) {
9816+ return false;
9817+ }
9818+ $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
9819+ for ($i = 0; $i < count($result); $i++) {
9820+ if (empty($result[$i]['iss_sta_id'])) {
9821+ $result[$i]['status_change_date'] = '';
9822+ } else {
9823+ list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
9824+ if ((empty($label)) || (empty($date_field_name))) {
9825+ $result[$i]['status_change_date'] = '';
9826+ continue;
9827+ }
9828+ $current = new Date(Date_Helper::getCurrentDateGMT());
9829+ $desc = "$label: %s ago";
9830+ $target_date = $result[$i][$date_field_name];
9831+ if (empty($target_date)) {
9832+ $result[$i]['status_change_date'] = '';
9833+ continue;
9834+ }
9835+ $date = new Date($target_date);
9836+ $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
9837+ }
9838+ }
9839+ }
9840+
9841+
9842+ /**
9843+ * Method used to get the list of issues to be displayed in the grid layout.
9844+ *
9845+ * @access public
9846+ * @param array $options The search parameters
9847+ * @return string The where clause
9848+ */
9849+ function buildWhereClause($options)
9850+ {
9851+ $usr_id = Auth::getUserID();
9852+ $prj_id = Auth::getCurrentProject();
9853+ $role_id = User::getRoleByUser($usr_id, $prj_id);
9854+
9855+ $stmt = ' AND iss_usr_id = usr_id';
9856+ if ($role_id == User::getRoleID('Customer')) {
9857+ $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
9858+ } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
9859+ $stmt .= " AND (
9860+ iss_usr_id = $usr_id OR
9861+ iur_usr_id = $usr_id
9862+ )";
9863+ }
9864+
9865+ if (!empty($options["users"])) {
9866+ $stmt .= " AND (\n";
9867+ if (stristr($options["users"], "grp") !== false) {
9868+ $chunks = explode(":", $options["users"]);
9869+ $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
9870+ } else {
9871+ if ($options['users'] == '-1') {
9872+ $stmt .= 'isu_usr_id IS NULL';
9873+ } elseif ($options['users'] == '-2') {
9874+ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
9875+ } elseif ($options['users'] == '-3') {
9876+ $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
9877+ } elseif ($options['users'] == '-4') {
9878+ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
9879+ } else {
9880+ $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
9881+ }
9882+ }
9883+ $stmt .= ')';
9884+ }
9885+ if (!empty($options["reporter"])) {
9886+ $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
9887+ }
9888+ if (!empty($options["show_authorized_issues"])) {
9889+ $stmt .= " AND (iur_usr_id=$usr_id)";
9890+ }
9891+ if (!empty($options["show_notification_list_issues"])) {
9892+ $stmt .= " AND (sub_usr_id=$usr_id)";
9893+ }
9894+ if (!empty($options["keywords"])) {
9895+ $stmt .= " AND (\n";
9896+ if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
9897+ $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
9898+ } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
9899+ // check if the user is trying to search by customer email
9900+ $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
9901+ if (count($customer_ids) > 0) {
9902+ $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
9903+ } else {
9904+ // no results, kill query
9905+ $stmt .= " iss_customer_id = -1";
9906+ }
9907+ } else {
9908+ $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
9909+ $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
9910+ }
9911+ $stmt .= "\n) ";
9912+ }
9913+ if (!empty($options["priority"])) {
9914+ $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
9915+ }
9916+ if (!empty($options["status"])) {
9917+ $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
9918+ }
9919+ if (!empty($options["category"])) {
9920+ if (!is_array($options['category'])) {
9921+ $options['category'] = array($options['category']);
9922+ }
9923+ $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
9924+ }
9925+ if (!empty($options["hide_closed"])) {
9926+ $stmt .= " AND sta_is_closed=0";
9927+ }
9928+ if (!empty($options['release'])) {
9929+ $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
9930+ }
9931+ // now for the date fields
9932+ $date_fields = array(
9933+ 'created_date',
9934+ 'updated_date',
9935+ 'last_response_date',
9936+ 'first_response_date',
9937+ 'closed_date'
9938+ );
9939+ foreach ($date_fields as $field_name) {
9940+ if (!empty($options[$field_name])) {
9941+ switch ($options[$field_name]['filter_type']) {
9942+ case 'greater':
9943+ $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
9944+ break;
9945+ case 'less':
9946+ $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
9947+ break;
9948+ case 'between':
9949+ $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
9950+ break;
9951+ case 'null':
9952+ $stmt .= " AND iss_$field_name IS NULL";
9953+ break;
9954+ case 'in_past':
9955+ if (strlen($options[$field_name]['time_period']) == 0) {
9956+ $options[$field_name]['time_period'] = 0;
9957+ }
9958+ $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
9959+ Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
9960+ break;
9961+ }
9962+ }
9963+ }
9964+ // custom fields
9965+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
9966+ foreach ($options['custom_field'] as $fld_id => $search_value) {
9967+ if (empty($search_value)) {
9968+ continue;
9969+ }
9970+ $field = Custom_Field::getDetails($fld_id);
9971+ $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
9972+ if (($field['fld_type'] == 'date') &&
9973+ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
9974+ continue;
9975+ }
9976+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
9977+ continue;
9978+ }
9979+
9980+ if ($field['fld_type'] == 'multiple') {
9981+ $search_value = Misc::escapeInteger($search_value);
9982+ foreach ($search_value as $cfo_id) {
9983+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
9984+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
9985+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
9986+ }
9987+ } elseif ($field['fld_type'] == 'date') {
9988+ if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
9989+ continue;
9990+ }
9991+ $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
9992+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
9993+ cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
9994+ } else if ($field['fld_type'] == 'integer') {
9995+ $value = $search_value['value'];
9996+ switch ($search_value['filter_type']) {
9997+ case 'ge': $cmp = '>='; break;
9998+ case 'le': $cmp = '<='; break;
9999+ case 'gt': $cmp = '>'; break;
10000+ case 'lt': $cmp = '<'; break;
10001+ default: $cmp = '='; break;
10002+ }
10003+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
10004+ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
10005+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
10006+ } else {
10007+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
10008+ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
10009+ if ($field['fld_type'] == 'combo') {
10010+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
10011+ } else {
10012+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
10013+ }
10014+ $stmt .= ')';
10015+ }
10016+ }
10017+ }
10018+ // clear cached full-text values if we are not searching fulltext anymore
10019+ if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
10020+ Session::set('fulltext_string', '');
10021+ Session::set('fulltext_issues', '');
10022+ }
10023+ return $stmt;
10024+ }
10025+
10026+
10027+ /**
10028+ * Method used to get the previous and next issues that are available
10029+ * according to the current search parameters.
10030+ *
10031+ * @access public
10032+ * @param integer $issue_id The issue ID
10033+ * @param array $options The search parameters
10034+ * @return array The list of issues
10035+ */
10036+ function getSides($issue_id, $options)
10037+ {
10038+ $usr_id = Auth::getUserID();
10039+ $role_id = Auth::getCurrentRole();
10040+
10041+ $stmt = "SELECT
10042+ iss_id,
10043+ " . self::getLastActionFields() . "
10044+ FROM
10045+ (
10046+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10047+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
10048+ // join custom fields if we are searching by custom fields
10049+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
10050+ foreach ($options['custom_field'] as $fld_id => $search_value) {
10051+ if (empty($search_value)) {
10052+ continue;
10053+ }
10054+ $field = Custom_Field::getDetails($fld_id);
10055+ if (($field['fld_type'] == 'date') &&
10056+ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
10057+ continue;
10058+ }
10059+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
10060+ continue;
10061+ }
10062+
10063+ if ($field['fld_type'] == 'multiple') {
10064+ $search_value = Misc::escapeInteger($search_value);
10065+ foreach ($search_value as $cfo_id) {
10066+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
10067+ }
10068+ } else {
10069+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
10070+ }
10071+ }
10072+ }
10073+ $stmt .= ")";
10074+ // check for the custom fields we want to sort by
10075+ if (strstr($options['sort_by'], 'custom_field') !== false) {
10076+ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
10077+ $stmt .= "\n LEFT JOIN \n" .
10078+ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
10079+ ON
10080+ (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
10081+ }
10082+ if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
10083+ $stmt .= "
10084+ LEFT JOIN
10085+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
10086+ ON
10087+ isu_iss_id=iss_id";
10088+ }
10089+ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
10090+ $stmt .= "
10091+ LEFT JOIN
10092+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
10093+ ON
10094+ iur_iss_id=iss_id";
10095+ }
10096+ if (!empty($options["show_notification_list_issues"])) {
10097+ $stmt .= "
10098+ LEFT JOIN
10099+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
10100+ ON
10101+ sub_iss_id=iss_id";
10102+ }
10103+ if (@$options["sort_by"] == "pre_scheduled_date") {
10104+ $stmt .= "
10105+ LEFT JOIN
10106+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
10107+ ON
10108+ iss_pre_id = pre_id";
10109+ }
10110+ if (@$options['sort_by'] == 'prc_title') {
10111+ $stmt .= "
10112+ LEFT JOIN
10113+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
10114+ ON
10115+ iss_prc_id = prc_id";
10116+ }
10117+ $stmt .= "
10118+ LEFT JOIN
10119+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10120+ ON
10121+ iss_sta_id=sta_id
10122+ LEFT JOIN
10123+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
10124+ ON
10125+ iss_pri_id=pri_id
10126+ WHERE
10127+ iss_prj_id=" . Auth::getCurrentProject();
10128+ $stmt .= self::buildWhereClause($options);
10129+ if (strstr($options["sort_by"], 'custom_field') !== false) {
10130+ $fld_details = Custom_Field::getDetails($fld_id);
10131+ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
10132+ } else {
10133+ $sort_by = Misc::escapeString($options["sort_by"]);
10134+ }
10135+ $stmt .= "
10136+ GROUP BY
10137+ iss_id
10138+ ORDER BY
10139+ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
10140+ iss_id DESC";
10141+ $res = DB_Helper::getInstance()->getCol($stmt);
10142+ if (PEAR::isError($res)) {
10143+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10144+ return "";
10145+ } else {
10146+ // COMPAT: the next line requires PHP >= 4.0.5
10147+ $index = array_search($issue_id, $res);
10148+ if (!empty($res[$index+1])) {
10149+ $next = $res[$index+1];
10150+ }
10151+ if (!empty($res[$index-1])) {
10152+ $previous = $res[$index-1];
10153+ }
10154+ return array(
10155+ "next" => @$next,
10156+ "previous" => @$previous
10157+ );
10158+ }
10159+ }
10160+
10161+
10162+ /**
10163+ * Method used to get the full list of user IDs assigned to a specific
10164+ * issue.
10165+ *
10166+ * @access public
10167+ * @param integer $issue_id The issue ID
10168+ * @return array The list of user IDs
10169+ */
10170+ function getAssignedUserIDs($issue_id)
10171+ {
10172+ $stmt = "SELECT
10173+ usr_id
10174+ FROM
10175+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10176+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10177+ WHERE
10178+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
10179+ isu_usr_id=usr_id";
10180+ $res = DB_Helper::getInstance()->getCol($stmt);
10181+ if (PEAR::isError($res)) {
10182+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10183+ return array();
10184+ } else {
10185+ return $res;
10186+ }
10187+ }
10188+
10189+
10190+ /**
10191+ * Method used to see if a user is assigned to an issue.
10192+ *
10193+ * @access public
10194+ * @param integer $issue_id The issue ID
10195+ * @param integer $usr_id An integer containg the ID of the user.
10196+ * @return boolean true if the user(s) are assigned to the issue.
10197+ */
10198+ function isAssignedToUser($issue_id, $usr_id)
10199+ {
10200+ $assigned_users = self::getAssignedUserIDs($issue_id);
10201+ if (in_array($usr_id, $assigned_users)) {
10202+ return true;
10203+ } else {
10204+ return false;
10205+ }
10206+ }
10207+
10208+
10209+ /**
10210+ * Method used to get the full list of reporters associated with a given
10211+ * list of issues.
10212+ *
10213+ * @access public
10214+ * @param array $result The result set
10215+ * @return void
10216+ */
10217+ function getReportersByIssues(&$result)
10218+ {
10219+ $ids = array();
10220+ for ($i = 0; $i < count($result); $i++) {
10221+ $ids[] = $result[$i]["iss_id"];
10222+ }
10223+ $ids = implode(", ", $ids);
10224+ $stmt = "SELECT
10225+ iss_id,
10226+ CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
10227+ FROM
10228+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10229+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10230+ WHERE
10231+ iss_usr_id=usr_id AND
10232+ iss_id IN ($ids)";
10233+ $res = DB_Helper::getInstance()->getAssoc($stmt);
10234+ if (PEAR::isError($res)) {
10235+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10236+ } else {
10237+ // now populate the $result variable again
10238+ for ($i = 0; $i < count($result); $i++) {
10239+ @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
10240+ }
10241+ }
10242+ }
10243+
10244+
10245+ /**
10246+ * Method used to get the full list of assigned users by a list
10247+ * of issues. This was originally created to optimize the issue
10248+ * listing page.
10249+ *
10250+ * @access public
10251+ * @param array $result The result set
10252+ * @return void
10253+ */
10254+ function getAssignedUsersByIssues(&$result)
10255+ {
10256+ $ids = array();
10257+ for ($i = 0; $i < count($result); $i++) {
10258+ $ids[] = $result[$i]["iss_id"];
10259+ }
10260+ if (count($ids) < 1) {
10261+ return;
10262+ }
10263+ $ids = implode(", ", $ids);
10264+ $stmt = "SELECT
10265+ isu_iss_id,
10266+ isu_order,
10267+ isu_usr_id,
10268+ usr_full_name
10269+ FROM
10270+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10271+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10272+ WHERE
10273+ isu_usr_id=usr_id AND
10274+ isu_iss_id IN ($ids)";
10275+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
10276+ if (PEAR::isError($res)) {
10277+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10278+ } else {
10279+ // gather names of the users assigned to each issue
10280+ $t = array();
10281+ for ($i = 0; $i < count($res); $i++) {
10282+ if (!empty($t[$res[$i]['isu_iss_id']])) {
10283+ $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
10284+ } else {
10285+ $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
10286+ }
10287+ }
10288+ // gather orders
10289+ $o = array();
10290+ for ($i = 0; $i < count($res); $i++) {
10291+ if (empty($o[$res[$i]['isu_iss_id']])) {
10292+ $o[$res[$i]['isu_iss_id']] = array();
10293+ }
10294+ $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
10295+ }
10296+ // now populate the $result variable again
10297+ for ($i = 0; $i < count($result); $i++) {
10298+ @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
10299+ @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
10300+ }
10301+ }
10302+ }
10303+
10304+
10305+ /**
10306+ * Method used to add the issue description to a list of issues.
10307+ *
10308+ * @access public
10309+ * @param array $result The result set
10310+ * @return void
10311+ */
10312+ function getDescriptionByIssues(&$result)
10313+ {
10314+ if (count($result) == 0) {
10315+ return;
10316+ }
10317+
10318+ $ids = array();
10319+ for ($i = 0; $i < count($result); $i++) {
10320+ $ids[] = $result[$i]["iss_id"];
10321+ }
10322+ $ids = implode(", ", $ids);
10323+
10324+ $stmt = "SELECT
10325+ iss_id,
10326+ iss_description
10327+ FROM
10328+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10329+ WHERE
10330+ iss_id in ($ids)";
10331+ $res = DB_Helper::getInstance()->getAssoc($stmt);
10332+ if (PEAR::isError($res)) {
10333+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10334+ } else {
10335+ for ($i = 0; $i < count($result); $i++) {
10336+ @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
10337+ }
10338+ }
10339+ }
10340+
10341+
10342+ /**
10343+ * Method used to get the full list of users (the full names) assigned to a
10344+ * specific issue.
10345+ *
10346+ * @access public
10347+ * @param integer $issue_id The issue ID
10348+ * @return array The list of users
10349+ */
10350+ function getAssignedUsers($issue_id)
10351+ {
10352+ $stmt = "SELECT
10353+ usr_full_name
10354+ FROM
10355+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10356+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10357+ WHERE
10358+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
10359+ isu_usr_id=usr_id";
10360+ $res = DB_Helper::getInstance()->getCol($stmt);
10361+ if (PEAR::isError($res)) {
10362+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10363+ return array();
10364+ } else {
10365+ return $res;
10366+ }
10367+ }
10368+
10369+
10370+ /**
10371+ * Method used to get the details for a specific issue.
10372+ *
10373+ * @access public
10374+ * @param integer $issue_id The issue ID
10375+ * @param boolean $force_refresh If the cache should not be used.
10376+ * @return array The details for the specified issue
10377+ */
10378+ function getDetails($issue_id, $force_refresh = false)
10379+ {
10380+ static $returns;
10381+
10382+ $issue_id = Misc::escapeInteger($issue_id);
10383+
10384+ if (empty($issue_id)) {
10385+ return '';
10386+ }
10387+
10388+ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
10389+ return $returns[$issue_id];
10390+ }
10391+
10392+ $stmt = "SELECT
10393+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
10394+ prj_title,
10395+ prc_title,
10396+ pre_title,
10397+ pri_title,
10398+ sta_title,
10399+ sta_abbreviation,
10400+ sta_color status_color,
10401+ sta_is_closed
10402+ FROM
10403+ (
10404+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10405+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
10406+ )
10407+ LEFT JOIN
10408+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
10409+ ON
10410+ iss_pri_id=pri_id
10411+ LEFT JOIN
10412+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10413+ ON
10414+ iss_sta_id=sta_id
10415+ LEFT JOIN
10416+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
10417+ ON
10418+ iss_prc_id=prc_id
10419+ LEFT JOIN
10420+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
10421+ ON
10422+ iss_pre_id=pre_id
10423+ WHERE
10424+ iss_id=$issue_id AND
10425+ iss_prj_id=prj_id";
10426+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
10427+ if (PEAR::isError($res)) {
10428+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10429+ return "";
10430+ } else {
10431+ if (empty($res)) {
10432+ return "";
10433+ } else {
10434+ $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
10435+ // get customer information, if any
10436+ if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
10437+ $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
10438+ $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
10439+ $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
10440+ $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
10441+ $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
10442+ $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
10443+ if (empty($res['iss_first_response_date'])) {
10444+ $first_response_deadline = $created_date_ts + $max_first_response_time;
10445+ if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
10446+ $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
10447+ } else {
10448+ $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
10449+ }
10450+ }
10451+ }
10452+ $res['iss_original_description'] = $res["iss_description"];
10453+ if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
10454+ $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
10455+ $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
10456+ }
10457+ $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
10458+ $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
10459+ $res['iss_created_date_ts'] = $created_date_ts;
10460+ $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
10461+ list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
10462+ $temp = self::getAssignedUsersStatus($res["iss_id"]);
10463+ $res["has_inactive_users"] = 0;
10464+ $res["assigned_users"] = array();
10465+ $res["assigned_inactive_users"] = array();
10466+ foreach ($temp as $usr_id => $usr_status) {
10467+ if (!User::isActiveStatus($usr_status)) {
10468+ $res["assigned_inactive_users"][] = $usr_id;
10469+ $res["has_inactive_users"] = 1;
10470+ } else {
10471+ $res["assigned_users"][] = $usr_id;
10472+ }
10473+ }
10474+ if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
10475+ $res["is_current_user_assigned"] = 1;
10476+ } else {
10477+ $res["is_current_user_assigned"] = 0;
10478+ }
10479+ $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
10480+ $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
10481+ $res["reporter"] = User::getFullName($res["iss_usr_id"]);
10482+ if (empty($res["iss_updated_date"])) {
10483+ $res["iss_updated_date"] = 'not updated yet';
10484+ } else {
10485+ $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
10486+ }
10487+ $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
10488+ if (Release::isAssignable($res["iss_pre_id"])) {
10489+ $release = Release::getDetails($res["iss_pre_id"]);
10490+ $res["pre_title"] = $release["pre_title"];
10491+ $res["pre_status"] = $release["pre_status"];
10492+ }
10493+ // need to return the list of issues that are duplicates of this one
10494+ $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
10495+ $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
10496+ // also get the issue title of the duplicated issue
10497+ if (!empty($res['iss_duplicated_iss_id'])) {
10498+ $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
10499+ }
10500+
10501+ // get group information
10502+ if (!empty($res["iss_grp_id"])) {
10503+ $res["group"] = Group::getDetails($res["iss_grp_id"]);
10504+ }
10505+
10506+ // get quarantine issue
10507+ $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
10508+
10509+ $returns[$issue_id] = $res;
10510+ return $res;
10511+ }
10512+ }
10513+ }
10514+
10515+
10516+ /**
10517+ * Method used to get some simple details about the given duplicated issue.
10518+ *
10519+ * @access public
10520+ * @param integer $issue_id The issue ID
10521+ * @return array The duplicated issue details
10522+ */
10523+ function getDuplicatedDetails($issue_id)
10524+ {
10525+ $stmt = "SELECT
10526+ iss_summary title,
10527+ sta_title current_status,
10528+ sta_is_closed is_closed
10529+ FROM
10530+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10531+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10532+ WHERE
10533+ iss_sta_id=sta_id AND
10534+ iss_id=$issue_id";
10535+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
10536+ if (PEAR::isError($res)) {
10537+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10538+ return array();
10539+ } else {
10540+ return $res;
10541+ }
10542+ }
10543+
10544+
10545+ /**
10546+ * Method used to bulk update a list of issues
10547+ *
10548+ * @access public
10549+ * @return boolean
10550+ */
10551+ function bulkUpdate()
10552+ {
10553+ // check if user performing this chance has the proper role
10554+ if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
10555+ return -1;
10556+ }
10557+
10558+ $items = Misc::escapeInteger($_POST['item']);
10559+ $new_status_id = Misc::escapeInteger($_POST['status']);
10560+ $new_release_id = Misc::escapeInteger(@$_POST['release']);
10561+ $new_priority_id = Misc::escapeInteger($_POST['priority']);
10562+ $new_category_id = Misc::escapeInteger($_POST['category']);
10563+
10564+ for ($i = 0; $i < count($items); $i++) {
10565+ if (!self::canAccess($items[$i], Auth::getUserID())) {
10566+ continue;
10567+ } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
10568+ // make sure issue is not in another project
10569+ continue;
10570+ }
10571+
10572+ $updated_fields = array();
10573+
10574+ // update assignment
10575+ if (count(@$_POST['users']) > 0) {
10576+ $users = Misc::escapeInteger($_POST['users']);
10577+ // get who this issue is currently assigned too
10578+ $stmt = "SELECT
10579+ isu_usr_id,
10580+ usr_full_name
10581+ FROM
10582+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
10583+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
10584+ WHERE
10585+ isu_usr_id = usr_id AND
10586+ isu_iss_id = " . $items[$i];
10587+ $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
10588+ if (PEAR::isError($current_assignees)) {
10589+ Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
10590+ return -1;
10591+ }
10592+ foreach ($current_assignees as $usr_id => $usr_name) {
10593+ if (!in_array($usr_id, $users)) {
10594+ self::deleteUserAssociation($items[$i], $usr_id, false);
10595+ }
10596+ }
10597+ $new_user_names = array();
10598+ $new_assignees = array();
10599+ foreach ($users as $usr_id) {
10600+ $new_user_names[$usr_id] = User::getFullName($usr_id);
10601+
10602+ // check if the issue is already assigned to this person
10603+ $stmt = "SELECT
10604+ COUNT(*) AS total
10605+ FROM
10606+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
10607+ WHERE
10608+ isu_iss_id=" . $items[$i] . " AND
10609+ isu_usr_id=" . $usr_id;
10610+ $total = DB_Helper::getInstance()->getOne($stmt);
10611+ if ($total > 0) {
10612+ continue;
10613+ } else {
10614+ $new_assignees[] = $usr_id;
10615+ // add the assignment
10616+ self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
10617+ Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
10618+ Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
10619+ }
10620+ }
10621+ Notification::notifyNewAssignment($new_assignees, $items[$i]);
10622+ $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
10623+ }
10624+
10625+ // update status
10626+ if (!empty($new_status_id)) {
10627+ $old_status_id = self::getStatusID($items[$i]);
10628+ $res = self::setStatus($items[$i], $new_status_id, false);
10629+ if ($res == 1) {
10630+ $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
10631+ }
10632+ }
10633+
10634+ // update release
10635+ if (!empty($new_release_id)) {
10636+ $old_release_id = self::getRelease($items[$i]);
10637+ $res = self::setRelease($items[$i], $new_release_id);
10638+ if ($res == 1) {
10639+ $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
10640+ }
10641+ }
10642+
10643+ // update priority
10644+ if (!empty($new_priority_id)) {
10645+ $old_priority_id = self::getPriority($items[$i]);
10646+ $res = self::setPriority($items[$i], $new_priority_id);
10647+ if ($res == 1) {
10648+ $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
10649+ }
10650+ }
10651+
10652+ // update category
10653+ if (!empty($new_category_id)) {
10654+ $old_category_id = self::getCategory($items[$i]);
10655+ $res = self::setCategory($items[$i], $new_category_id);
10656+ if ($res == 1) {
10657+ $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
10658+ }
10659+ }
10660+
10661+ if (count($updated_fields) > 0) {
10662+ // log the changes
10663+ $changes = '';
10664+ $k = 0;
10665+ foreach ($updated_fields as $key => $value) {
10666+ if ($k > 0) {
10667+ $changes .= "; ";
10668+ }
10669+ $changes .= "$key: $value";
10670+ $k++;
10671+ }
10672+ History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
10673+ }
10674+
10675+ // close if request
10676+ if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
10677+ self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
10678+ }
10679+ }
10680+ return true;
10681+ }
10682+
10683+
10684+ /**
10685+ * Method used to set the initial impact analysis for a specific issue
10686+ *
10687+ * @access public
10688+ * @param integer $issue_id The issue ID
10689+ * @return integer 1 if the update worked, -1 otherwise
10690+ */
10691+ function setImpactAnalysis($issue_id)
10692+ {
10693+ $stmt = "UPDATE
10694+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10695+ SET
10696+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
10697+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
10698+ iss_last_internal_action_type='update',
10699+ iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
10700+ iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
10701+ WHERE
10702+ iss_id=" . Misc::escapeInteger($issue_id);
10703+ $res = DB_Helper::getInstance()->query($stmt);
10704+ if (PEAR::isError($res)) {
10705+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10706+ return -1;
10707+ } else {
10708+ // add the impact analysis to the history of the issue
10709+ $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
10710+ History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
10711+ return 1;
10712+ }
10713+ }
10714+
10715+
10716+ /**
10717+ * Method used to get the full list of issue IDs that area available in the
10718+ * system.
10719+ *
10720+ * @access public
10721+ * @param string $extra_condition An extra condition in the WHERE clause
10722+ * @return array The list of issue IDs
10723+ */
10724+ function getColList($extra_condition = NULL)
10725+ {
10726+ $stmt = "SELECT
10727+ iss_id
10728+ FROM
10729+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10730+ WHERE
10731+ iss_prj_id=" . Auth::getCurrentProject();
10732+ if (!empty($extra_condition)) {
10733+ $stmt .= " AND $extra_condition ";
10734+ }
10735+ $stmt .= "
10736+ ORDER BY
10737+ iss_id DESC";
10738+ $res = DB_Helper::getInstance()->getCol($stmt);
10739+ if (PEAR::isError($res)) {
10740+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10741+ return "";
10742+ } else {
10743+ return $res;
10744+ }
10745+ }
10746+
10747+
10748+ /**
10749+ * Method used to get the full list of issue IDs and their respective
10750+ * titles.
10751+ *
10752+ * @access public
10753+ * @param string $extra_condition An extra condition in the WHERE clause
10754+ * @return array The list of issues
10755+ */
10756+ function getAssocList($extra_condition = NULL)
10757+ {
10758+ $stmt = "SELECT
10759+ iss_id,
10760+ iss_summary
10761+ FROM
10762+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
10763+ WHERE
10764+ iss_prj_id=" . Auth::getCurrentProject();
10765+ if (!empty($extra_condition)) {
10766+ $stmt .= " AND $extra_condition ";
10767+ }
10768+ $stmt .= "
10769+ ORDER BY
10770+ iss_id ASC";
10771+ $res = DB_Helper::getInstance()->getAssoc($stmt);
10772+ if (PEAR::isError($res)) {
10773+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10774+ return "";
10775+ } else {
10776+ return $res;
10777+ }
10778+ }
10779+
10780+
10781+ /**
10782+ * Method used to get the list of issues associated to a specific issue.
10783+ *
10784+ * @access public
10785+ * @param integer $issue_id The issue ID
10786+ * @return array The list of associated issues
10787+ */
10788+ function getAssociatedIssues($issue_id)
10789+ {
10790+ $issues = self::getAssociatedIssuesDetails($issue_id);
10791+ $associated = array();
10792+ for ($i = 0; $i < count($issues); $i++) {
10793+ $associated[] = $issues[$i]['associated_issue'];
10794+ }
10795+ return $associated;
10796+ }
10797+
10798+
10799+ /**
10800+ * Method used to get the list of issues associated details to a
10801+ * specific issue.
10802+ *
10803+ * @access public
10804+ * @param integer $issue_id The issue ID
10805+ * @return array The list of associated issues
10806+ */
10807+ function getAssociatedIssuesDetails($issue_id)
10808+ {
10809+ static $returns;
10810+
10811+ if (!empty($returns[$issue_id])) {
10812+ return $returns[$issue_id];
10813+ }
10814+
10815+ $stmt = "SELECT
10816+ isa_associated_id associated_issue,
10817+ iss_summary associated_title,
10818+ sta_title current_status,
10819+ sta_is_closed is_closed
10820+ FROM
10821+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
10822+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10823+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10824+ WHERE
10825+ isa_associated_id=iss_id AND
10826+ iss_sta_id=sta_id AND
10827+ isa_issue_id=$issue_id";
10828+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
10829+ if (PEAR::isError($res)) {
10830+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10831+ return array();
10832+ } else {
10833+ $returns[$issue_id] = $res;
10834+ return $res;
10835+ }
10836+ }
10837+
10838+
10839+ /**
10840+ * Method used to check whether an issue was already closed or not.
10841+ *
10842+ * @access public
10843+ * @param integer $issue_id The issue ID
10844+ * @return boolean
10845+ */
10846+ function isClosed($issue_id)
10847+ {
10848+ $stmt = "SELECT
10849+ COUNT(*)
10850+ FROM
10851+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10852+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
10853+ WHERE
10854+ iss_id=" . Misc::escapeInteger($issue_id) . " AND
10855+ iss_sta_id=sta_id AND
10856+ sta_is_closed=1";
10857+ $res = DB_Helper::getInstance()->getOne($stmt);
10858+ if (PEAR::isError($res)) {
10859+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10860+ return false;
10861+ } else {
10862+ if ($res == 0) {
10863+ return false;
10864+ } else {
10865+ return true;
10866+ }
10867+ }
10868+ }
10869+
10870+
10871+ /**
10872+ * Returns a simple list of issues that are currently set to some
10873+ * form of quarantine. This is mainly used by the IRC interface.
10874+ *
10875+ * @access public
10876+ * @return array List of quarantined issues
10877+ */
10878+ function getQuarantinedIssueList()
10879+ {
10880+ // XXX: would be nice to restrict the result list to only one project
10881+ $stmt = "SELECT
10882+ iss_id,
10883+ iss_summary
10884+ FROM
10885+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
10886+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10887+ WHERE
10888+ iqu_iss_id=iss_id AND
10889+ iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
10890+ iqu_expiration IS NOT NULL";
10891+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
10892+ if (PEAR::isError($res)) {
10893+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10894+ return array();
10895+ } else {
10896+ self::getAssignedUsersByIssues($res);
10897+ return $res;
10898+ }
10899+ }
10900+
10901+
10902+ /**
10903+ * Returns the status of a quarantine.
10904+ *
10905+ * @param integer $issue_id The issue ID
10906+ * @return integer Indicates what the current state of quarantine is.
10907+ */
10908+ function getQuarantineInfo($issue_id)
10909+ {
10910+ $stmt = "SELECT
10911+ iqu_status,
10912+ iqu_expiration
10913+ FROM
10914+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10915+ WHERE
10916+ iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
10917+ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
10918+ iqu_expiration IS NULL)";
10919+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
10920+ if (PEAR::isError($res)) {
10921+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10922+ return array();
10923+ } else {
10924+ if (!empty($res["iqu_expiration"])) {
10925+ $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
10926+ $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
10927+ }
10928+ return $res;
10929+ }
10930+ }
10931+
10932+
10933+ /**
10934+ * Sets the quarantine status. Optionally an expiration date can be set
10935+ * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
10936+ *
10937+ * @access public
10938+ * @param integer $issue_id The issue ID
10939+ * @param integer $status The quarantine status
10940+ * @param string $expiration The expiration date of quarantine (default empty)
10941+ */
10942+ function setQuarantine($issue_id, $status, $expiration = '')
10943+ {
10944+ $issue_id = Misc::escapeInteger($issue_id);
10945+ $status = Misc::escapeInteger($status);
10946+
10947+ // see if there is an existing record
10948+ $stmt = "SELECT
10949+ COUNT(*)
10950+ FROM
10951+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10952+ WHERE
10953+ iqu_iss_id = $issue_id";
10954+ $res = DB_Helper::getInstance()->getOne($stmt);
10955+ if (PEAR::isError($res)) {
10956+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10957+ return -1;
10958+ }
10959+ if ($res > 0) {
10960+ // update
10961+ $stmt = "UPDATE
10962+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10963+ SET
10964+ iqu_status = $status";
10965+ if (!empty($expiration)) {
10966+ $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
10967+ }
10968+ $stmt .= "\nWHERE
10969+ iqu_iss_id = $issue_id";
10970+ $res = DB_Helper::getInstance()->query($stmt);
10971+ if (PEAR::isError($res)) {
10972+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
10973+ return -1;
10974+ } else {
10975+ // add history entry about this change taking place
10976+ if ($status == 0) {
10977+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
10978+ "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
10979+ }
10980+ }
10981+ } else {
10982+ // insert
10983+ $stmt = "INSERT INTO
10984+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
10985+ (
10986+ iqu_iss_id,
10987+ iqu_status";
10988+ if (!empty($expiration)) {
10989+ $stmt .= ",\niqu_expiration\n";
10990+ }
10991+ $stmt .= ") VALUES (
10992+ $issue_id,
10993+ $status";
10994+ if (!empty($expiration)) {
10995+ $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
10996+ }
10997+ $stmt .= ")";
10998+ $res = DB_Helper::getInstance()->query($stmt);
10999+ if (PEAR::isError($res)) {
11000+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11001+ return -1;
11002+ }
11003+ }
11004+ return 1;
11005+ }
11006+
11007+
11008+ /**
11009+ * Sets the group of the issue.
11010+ *
11011+ * @access public
11012+ * @param integer $issue_id The ID of the issue
11013+ * @param integer $group_id The ID of the group
11014+ * @return integer 1 if successful, -1 or -2 otherwise
11015+ */
11016+ function setGroup($issue_id, $group_id)
11017+ {
11018+ $issue_id = Misc::escapeInteger($issue_id);
11019+ $group_id = Misc::escapeInteger($group_id);
11020+
11021+ $current = self::getDetails($issue_id);
11022+ if ($current["iss_grp_id"] == $group_id) {
11023+ return -2;
11024+ }
11025+ $stmt = "UPDATE
11026+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11027+ SET
11028+ iss_grp_id = $group_id
11029+ WHERE
11030+ iss_id = $issue_id";
11031+ $res = DB_Helper::getInstance()->query($stmt);
11032+ if (PEAR::isError($res)) {
11033+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11034+ return -1;
11035+ }
11036+ $current_user = Auth::getUserID();
11037+ if (empty($current_user)) {
11038+ $current_user = APP_SYSTEM_USER_ID;
11039+ }
11040+ History::add($issue_id, $current_user, History::getTypeID('group_changed'),
11041+ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
11042+ return 1;
11043+ }
11044+
11045+
11046+ /**
11047+ * Returns the group ID associated with the given issue ID.
11048+ *
11049+ * @access public
11050+ * @param integer $issue_id The issue ID
11051+ * @return integer The associated group ID
11052+ */
11053+ function getGroupID($issue_id)
11054+ {
11055+ $stmt = "SELECT
11056+ iss_grp_id
11057+ FROM
11058+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11059+ WHERE
11060+ iss_id=" . Misc::escapeInteger($issue_id);
11061+ $res = DB_Helper::getInstance()->getOne($stmt);
11062+ if (PEAR::isError($res)) {
11063+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11064+ return 0;
11065+ } else {
11066+ return $res;
11067+ }
11068+ }
11069+
11070+
11071+ /**
11072+ * Returns an array of issues based on full text search results.
11073+ *
11074+ * @param array $options An array of search options
11075+ * @return array An array of issue IDS
11076+ */
11077+ function getFullTextIssues($options)
11078+ {
11079+ // check if a list of issues for this full text search is already cached
11080+ $fulltext_string = Session::get('fulltext_string');
11081+ if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
11082+ return Session::get('fulltext_issues');
11083+ }
11084+
11085+ // no pre-existing list, generate them
11086+ $stmt = "(SELECT
11087+ DISTINCT(iss_id)
11088+ FROM
11089+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11090+ WHERE
11091+ MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11092+ ) UNION (
11093+ SELECT
11094+ DISTINCT(not_iss_id)
11095+ FROM
11096+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
11097+ WHERE
11098+ MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11099+ ) UNION (
11100+ SELECT
11101+ DISTINCT(ttr_iss_id)
11102+ FROM
11103+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
11104+ WHERE
11105+ MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11106+ ) UNION (
11107+ SELECT
11108+ DISTINCT(phs_iss_id)
11109+ FROM
11110+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
11111+ WHERE
11112+ MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11113+ ) UNION (
11114+ SELECT
11115+ DISTINCT(sup_iss_id)
11116+ FROM
11117+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
11118+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
11119+ WHERE
11120+ sup_id = seb_sup_id AND
11121+ MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
11122+ )";
11123+ $res = DB_Helper::getInstance()->getCol($stmt);
11124+ if (PEAR::isError($res)) {
11125+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11126+ return array(-1);
11127+ } else {
11128+ $stmt = "SELECT
11129+ DISTINCT(icf_iss_id)
11130+ FROM
11131+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
11132+ WHERE
11133+ MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
11134+ $custom_res = DB_Helper::getInstance()->getCol($stmt);
11135+ if (PEAR::isError($custom_res)) {
11136+ Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
11137+ return array(-1);
11138+ }
11139+ $issues = array_merge($res, $custom_res);
11140+ // we kill the query results on purpose to flag that no
11141+ // issues could be found with fulltext search
11142+ if (count($issues) < 1) {
11143+ $issues = array(-1);
11144+ }
11145+ Session::set('fulltext_string', $options['keywords']);
11146+ Session::set('fulltext_issues', $issues);
11147+ return $issues;
11148+ }
11149+ }
11150+
11151+
11152+ /**
11153+ * Method to determine if user can access a particular issue
11154+ *
11155+ * @access public
11156+ * @param integer $issue_id The ID of the issue.
11157+ * @param integer $usr_id The ID of the user
11158+ * @return boolean If the user can access the issue
11159+ */
11160+ function canAccess($issue_id, $usr_id)
11161+ {
11162+ static $access;
11163+
11164+ if (empty($issue_id)) {
11165+ return true;
11166+ }
11167+
11168+ if (isset($access[$issue_id . "-" . $usr_id])) {
11169+ return $access[$issue_id . "-" . $usr_id];
11170+ }
11171+
11172+ $details = self::getDetails($issue_id);
11173+ if (empty($details)) {
11174+ return true;
11175+ }
11176+ $usr_details = User::getDetails($usr_id);
11177+ $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
11178+ $prj_id = self::getProjectID($issue_id);
11179+
11180+
11181+ if (empty($usr_role)) {
11182+ // check if they are even allowed to access the project
11183+ $return = false;
11184+ } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
11185+ ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
11186+ // check customer permissions
11187+ $return = false;
11188+ } elseif ($details['iss_private'] == 1) {
11189+ // check if the issue is even private
11190+
11191+ // check role, reporter, assigment and group
11192+ if ($usr_role > User::getRoleID("Developer")) {
11193+ $return = true;
11194+ } elseif ($details['iss_usr_id'] == $usr_id) {
11195+ $return = true;
11196+ } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
11197+ $return = true;
11198+ } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
11199+ ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
11200+ $return = true;
11201+ } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
11202+ $return = true;
11203+ } else {
11204+ $return = false;
11205+ }
11206+ } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
11207+ ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
11208+ return false;
11209+ } else {
11210+ $return = true;
11211+ }
11212+
11213+ $access[$issue_id . "-" . $usr_id] = $return;
11214+ return $return;
11215+ }
11216+
11217+
11218+ /**
11219+ * Returns true if the specified issue is private, false otherwise
11220+ *
11221+ * @access public
11222+ * @param integer $issue_id The ID of the issue
11223+ * @return boolean If the issue is private or not
11224+ */
11225+ function isPrivate($issue_id)
11226+ {
11227+ static $returns;
11228+
11229+ if (!isset($returns[$issue_id])) {
11230+ $sql = "SELECT
11231+ iss_private
11232+ FROM
11233+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11234+ WHERE
11235+ iss_id=$issue_id";
11236+ $res = DB_Helper::getInstance()->getOne($sql);
11237+ if (PEAR::isError($res)) {
11238+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11239+ return true;
11240+ } else {
11241+ if ($res == 1) {
11242+ $returns[$issue_id] = true;
11243+ } else {
11244+ $returns[$issue_id] = false;
11245+ }
11246+ }
11247+ }
11248+ return $returns[$issue_id];
11249+ }
11250+
11251+
11252+ /**
11253+ * Clears closed information from an issues.
11254+ *
11255+ * @access public
11256+ * @param integer $issue_id The ID of the issue
11257+ */
11258+ function clearClosed($issue_id)
11259+ {
11260+ $stmt = "UPDATE
11261+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11262+ SET
11263+ iss_closed_date = null,
11264+ iss_res_id = null
11265+ WHERE
11266+ iss_id=" . Misc::escapeInteger($issue_id);
11267+ $res = DB_Helper::getInstance()->query($stmt);
11268+ if (PEAR::isError($res)) {
11269+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11270+ return -1;
11271+ }
11272+ self::moveOrderForAllUsers($issue_id, 1);
11273+ }
11274+
11275+
11276+ /**
11277+ * Returns the message ID that should be used as the parent ID for all messages
11278+ *
11279+ * @access public
11280+ * @param integer $issue_id The ID of the issue
11281+ */
11282+ function getRootMessageID($issue_id)
11283+ {
11284+ $sql = "SELECT
11285+ iss_root_message_id
11286+ FROM
11287+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11288+ WHERE
11289+ iss_id=" . Misc::escapeInteger($issue_id);
11290+ $res = DB_Helper::getInstance()->getOne($sql);
11291+ if (PEAR::isError($res)) {
11292+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11293+ return false;
11294+ } else {
11295+ return $res;
11296+ }
11297+ }
11298+
11299+
11300+ /**
11301+ * Returns the issue ID of the issue with the specified root message ID, or false
11302+ * @access public
11303+ * @param string $msg_id The Message ID
11304+ * @return integer The ID of the issue
11305+ */
11306+ function getIssueByRootMessageID($msg_id)
11307+ {
11308+ static $returns;
11309+
11310+ if (!empty($returns[$msg_id])) {
11311+ return $returns[$msg_id];
11312+ }
11313+ $sql = "SELECT
11314+ iss_id
11315+ FROM
11316+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11317+ WHERE
11318+ iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
11319+ $res = DB_Helper::getInstance()->getOne($sql);
11320+ if (PEAR::isError($res)) {
11321+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11322+ return false;
11323+ }
11324+ if (empty($res)) {
11325+ $returns[$msg_id] = false;
11326+ } else {
11327+ $returns[$msg_id] = $res;
11328+ }
11329+ return $returns[$msg_id];
11330+ }
11331+
11332+
11333+ /**
11334+ * Sets the assignees for the issue
11335+ *
11336+ * @param integer $issue_id
11337+ * @param array $assignees
11338+ */
11339+ function setAssignees($issue_id, $assignees)
11340+ {
11341+ if (!is_array($assignees)) {
11342+ $assignees = array();
11343+ }
11344+
11345+ // see if there is anything to change
11346+ $old_assignees = self::getAssignedUserIDs($issue_id);
11347+ if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
11348+ return;
11349+ }
11350+
11351+ $old_assignee_names = self::getAssignedUsers($issue_id);
11352+
11353+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
11354+ // clear up the assignments for this issue, and then assign it to the current user
11355+ self::deleteUserAssociations($issue_id);
11356+ $assignee_names = array();
11357+ foreach ($assignees as $assignee) {
11358+ $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
11359+ if ($res == -1) {
11360+ return false;
11361+ }
11362+ $assignee_names[] = User::getFullName($assignee);
11363+ Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
11364+ }
11365+
11366+ Notification::notifyNewAssignment($assignees, $issue_id);
11367+
11368+ // save a history entry about this...
11369+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
11370+ "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
11371+ }
11372+
11373+ /**
11374+ * Reorders user's issues as requested by user
11375+ * @access public
11376+ * @param $usr_id User to be reordered
11377+ * @param $issue_id Issue or array of issues to be moved
11378+ * @param $neworder The new order of the issues
11379+ * @return void
11380+ */
11381+ function reorderUserIssues($usr_id, $issue_id, $neworder)
11382+ {
11383+ if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
11384+ return false;
11385+ }
11386+ if (!is_numeric($usr_id) || !is_numeric($neworder)) {
11387+ return false;
11388+ }
11389+ $usr_id = Misc::escapeInteger($usr_id);
11390+ $issue_id = Misc::escapeInteger($issue_id);
11391+ $neworder = Misc::escapeInteger($neworder);
11392+ if (is_array($issue_id)) {
11393+ $issue_count = count($issue_id);
11394+ $issue_id_str = implode(", ", $issue_id);
11395+ } else {
11396+ $issue_count = 1;
11397+ $issue_id_str = $issue_id;
11398+ $issue_id = array($issue_id);
11399+ }
11400+ // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
11401+ $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
11402+ self::rearrangeDeleteUserAssociationOrderList($orderlist);
11403+ // move down the orders to free the "order space" needed
11404+ $stmt = "UPDATE
11405+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11406+ SET
11407+ isu_order = isu_order + $issue_count
11408+ WHERE
11409+ isu_usr_id = $usr_id AND
11410+ isu_order >= $neworder";
11411+ $res = DB_Helper::getInstance()->query($stmt);
11412+ if (PEAR::isError($res)) {
11413+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11414+ return -1;
11415+ }
11416+ //update the order for the issues being moved
11417+ $i = 0;
11418+ foreach ($issue_id as $iss_id) {
11419+ $stmt = "UPDATE
11420+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11421+ SET
11422+ isu_order = " . ($neworder + $i) . "
11423+ WHERE
11424+ isu_usr_id = $usr_id AND
11425+ isu_iss_id = $iss_id";
11426+ $res = DB_Helper::getInstance()->query($stmt);
11427+ if (PEAR::isError($res)) {
11428+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11429+ return -1;
11430+ }
11431+ $i++;
11432+ }
11433+ }
11434+
11435+
11436+ /**
11437+ * Get users issue order list
11438+ * @access public
11439+ * @param $user_id User
11440+ * @param $order_list Order of the issues
11441+ * @return void
11442+ */
11443+ function getIssueOrderByUser($usr_id) {
11444+
11445+ if (!is_numeric($usr_id)) {
11446+ return false;
11447+ }
11448+
11449+ $stmt = "SELECT
11450+ isu_iss_id, isu_order
11451+ FROM
11452+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11453+ WHERE
11454+ isu_usr_id = " . $usr_id ;
11455+
11456+ $order_list = array();
11457+
11458+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
11459+
11460+ if (PEAR::isError($res)) {
11461+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11462+ return array();
11463+ } else {
11464+ foreach ($res as $row) {
11465+ $order_list[$row["isu_iss_id"]] = $row["isu_order"];
11466+ }
11467+ }
11468+ return $order_list;
11469+ }
11470+
11471+ function moveOrderForAllUsers($issue_id, $neworder)
11472+ {
11473+ // Move the issue to the top priority for the ppl it's assigned to
11474+ $stmt = "SELECT isu_usr_id FROM
11475+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
11476+ WHERE
11477+ isu_iss_id = " . Misc::escapeInteger($issue_id);
11478+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
11479+ if (PEAR::isError($res)) {
11480+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11481+ return -1;
11482+ }
11483+ foreach ($res as $row) {
11484+ self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
11485+ }
11486+ }
11487+
11488+}
11489--- eventum-2.2/lib/eventum/class.issue.php.~3~ 1970-01-01 02:00:00.000000000 +0200
11490+++ eventum-2.2-order/lib/eventum/class.issue.php.~3~ 2009-10-12 22:10:36.442518536 +0300
11491@@ -0,0 +1,4752 @@
11492+<?php
11493+/* vim: set expandtab tabstop=4 shiftwidth=4 encoding=utf-8: */
11494+// +----------------------------------------------------------------------+
11495+// | Eventum - Issue Tracking System |
11496+// +----------------------------------------------------------------------+
11497+// | Copyright (c) 2003 - 2008 MySQL AB |
11498+// | Copyright (c) 2008 - 2009 Sun Microsystem Inc. |
11499+// | |
11500+// | This program is free software; you can redistribute it and/or modify |
11501+// | it under the terms of the GNU General Public License as published by |
11502+// | the Free Software Foundation; either version 2 of the License, or |
11503+// | (at your option) any later version. |
11504+// | |
11505+// | This program is distributed in the hope that it will be useful, |
11506+// | but WITHOUT ANY WARRANTY; without even the implied warranty of |
11507+// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11508+// | GNU General Public License for more details. |
11509+// | |
11510+// | You should have received a copy of the GNU General Public License |
11511+// | along with this program; if not, write to: |
11512+// | |
11513+// | Free Software Foundation, Inc. |
11514+// | 59 Temple Place - Suite 330 |
11515+// | Boston, MA 02111-1307, USA. |
11516+// +----------------------------------------------------------------------+
11517+// | Authors: João Prado Maia <jpm@mysql.com> |
11518+// +----------------------------------------------------------------------+
11519+//
11520+
11521+
11522+/**
11523+ * Class designed to handle all business logic related to the issues in the
11524+ * system, such as adding or updating them or listing them in the grid mode.
11525+ *
11526+ * @author João Prado Maia <jpm@mysql.com>
11527+ * @version $Revision$
11528+ */
11529+
11530+class Issue
11531+{
11532+ /**
11533+ * Method used to check whether a given issue ID exists or not.
11534+ *
11535+ * @access public
11536+ * @param integer $issue_id The issue ID
11537+ * @param boolean $check_project If we should check that this issue is in the current project
11538+ * @return boolean
11539+ */
11540+ function exists($issue_id, $check_project = true)
11541+ {
11542+ $stmt = "SELECT
11543+ COUNT(*)
11544+ FROM
11545+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11546+ WHERE
11547+ iss_id=" . Misc::escapeInteger($issue_id);
11548+ if ($check_project) {
11549+ $stmt .= " AND
11550+ iss_prj_id = " . Auth::getCurrentProject();
11551+ }
11552+ $res = DB_Helper::getInstance()->getOne($stmt);
11553+ if (PEAR::isError($res)) {
11554+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11555+ return false;
11556+ } else {
11557+ if ($res == 0) {
11558+ return false;
11559+ } else {
11560+ return true;
11561+ }
11562+ }
11563+ }
11564+
11565+
11566+ /**
11567+ * Method used to get the list of column heading titles for the
11568+ * CSV export functionality of the issue listing screen.
11569+ *
11570+ * @access public
11571+ * @param integer $prj_id The project ID
11572+ * @return array The list of column heading titles
11573+ */
11574+ function getColumnHeadings($prj_id)
11575+ {
11576+ $headings = array(
11577+ 'Priority',
11578+ 'Issue ID',
11579+ 'Reporter',
11580+ );
11581+ // hide the group column from the output if no
11582+ // groups are available in the database
11583+ $groups = Group::getAssocList($prj_id);
11584+ if (count($groups) > 0) {
11585+ $headings[] = 'Group';
11586+ }
11587+ $headings[] = 'Assigned';
11588+ $headings[] = 'Time Spent';
11589+ // hide the category column from the output if no
11590+ // categories are available in the database
11591+ $categories = Category::getAssocList($prj_id);
11592+ if (count($categories) > 0) {
11593+ $headings[] = 'Category';
11594+ }
11595+ if (Customer::hasCustomerIntegration($prj_id)) {
11596+ $headings[] = 'Customer';
11597+ }
11598+ $headings[] = 'Status';
11599+ $headings[] = 'Status Change Date';
11600+ $headings[] = 'Last Action Date';
11601+ $headings[] = 'Est. Dev. TIme';
11602+ $headings[] = 'Summary';
11603+ $headings[] = 'Expected Resolution Date';
11604+ return $headings;
11605+ }
11606+
11607+
11608+ /**
11609+ * Method used to get the full list of date fields available to issues, to
11610+ * be used when customizing the issue listing screen in the 'last status
11611+ * change date' column.
11612+ *
11613+ * @access public
11614+ * @param boolean $display_customer_fields Whether to include any customer related fields or not
11615+ * @return array The list of available date fields
11616+ */
11617+ function getDateFieldsAssocList($display_customer_fields = FALSE)
11618+ {
11619+ $fields = array(
11620+ 'iss_created_date' => 'Created Date',
11621+ 'iss_updated_date' => 'Last Updated Date',
11622+ 'iss_last_response_date' => 'Last Response Date',
11623+ 'iss_closed_date' => 'Closed Date'
11624+ );
11625+ if ($display_customer_fields) {
11626+ $fields['iss_last_customer_action_date'] = 'Customer Action Date';
11627+ }
11628+ asort($fields);
11629+ return $fields;
11630+ }
11631+
11632+
11633+ /**
11634+ * Method used to get the full list of issue IDs and their respective
11635+ * titles associated to a given project.
11636+ *
11637+ * @access public
11638+ * @param integer $prj_id The project ID
11639+ * @return array The list of issues
11640+ */
11641+ function getAssocListByProject($prj_id)
11642+ {
11643+ $stmt = "SELECT
11644+ iss_id,
11645+ iss_summary
11646+ FROM
11647+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11648+ WHERE
11649+ iss_prj_id=" . Misc::escapeInteger($prj_id) . "
11650+ ORDER BY
11651+ iss_id ASC";
11652+ $res = DB_Helper::getInstance()->getAssoc($stmt);
11653+ if (PEAR::isError($res)) {
11654+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11655+ return "";
11656+ } else {
11657+ return $res;
11658+ }
11659+ }
11660+
11661+
11662+ /**
11663+ * Method used to get the status of a given issue.
11664+ *
11665+ * @access public
11666+ * @param integer $issue_id The issue ID
11667+ * @return integer The status ID
11668+ */
11669+ function getStatusID($issue_id)
11670+ {
11671+ static $returns;
11672+
11673+ $issue_id = Misc::escapeInteger($issue_id);
11674+
11675+ if (!empty($returns[$issue_id])) {
11676+ return $returns[$issue_id];
11677+ }
11678+
11679+ $stmt = "SELECT
11680+ iss_sta_id
11681+ FROM
11682+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11683+ WHERE
11684+ iss_id=$issue_id";
11685+ $res = DB_Helper::getInstance()->getOne($stmt);
11686+ if (PEAR::isError($res)) {
11687+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11688+ return '';
11689+ } else {
11690+ $returns[$issue_id] = $res;
11691+ return $res;
11692+ }
11693+ }
11694+
11695+
11696+ /**
11697+ * Records the last customer action date for a given issue ID.
11698+ *
11699+ * @access public
11700+ * @param integer $issue_id The issue ID
11701+ * @return integer 1 if the update worked, -1 otherwise
11702+ */
11703+ function recordLastCustomerAction($issue_id)
11704+ {
11705+ $stmt = "UPDATE
11706+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11707+ SET
11708+ iss_last_customer_action_date='" . Date_Helper::getCurrentDateGMT() . "',
11709+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
11710+ iss_last_public_action_type='customer action'
11711+ WHERE
11712+ iss_id=" . Misc::escapeInteger($issue_id);
11713+ $res = DB_Helper::getInstance()->query($stmt);
11714+ if (PEAR::isError($res)) {
11715+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11716+ return -1;
11717+ } else {
11718+ return 1;
11719+ }
11720+ }
11721+
11722+
11723+ /**
11724+ * Returns the customer ID associated with the given issue ID.
11725+ *
11726+ * @access public
11727+ * @param integer $issue_id The issue ID
11728+ * @return integer The customer ID associated with the issue
11729+ */
11730+ function getCustomerID($issue_id)
11731+ {
11732+ static $returns;
11733+
11734+ $issue_id = Misc::escapeInteger($issue_id);
11735+
11736+ if (!empty($returns[$issue_id])) {
11737+ return $returns[$issue_id];
11738+ }
11739+
11740+ $stmt = "SELECT
11741+ iss_customer_id
11742+ FROM
11743+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11744+ WHERE
11745+ iss_id=$issue_id";
11746+ $res = DB_Helper::getInstance()->getOne($stmt);
11747+ if (PEAR::isError($res)) {
11748+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11749+ return '';
11750+ } else {
11751+ $returns[$issue_id] = $res;
11752+ return $res;
11753+ }
11754+ }
11755+
11756+
11757+ /**
11758+ * Returns the contract ID associated with the given issue ID.
11759+ *
11760+ * @access public
11761+ * @param integer $issue_id The issue ID
11762+ * @return integer The customer ID associated with the issue
11763+ */
11764+ function getContractID($issue_id)
11765+ {
11766+ static $returns;
11767+
11768+ $issue_id = Misc::escapeInteger($issue_id);
11769+
11770+ if (!empty($returns[$issue_id])) {
11771+ return $returns[$issue_id];
11772+ }
11773+
11774+ $stmt = "SELECT
11775+ iss_customer_contract_id
11776+ FROM
11777+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11778+ WHERE
11779+ iss_id=$issue_id";
11780+ $res = DB_Helper::getInstance()->getOne($stmt);
11781+ if (PEAR::isError($res)) {
11782+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11783+ return '';
11784+ } else {
11785+ $returns[$issue_id] = $res;
11786+ return $res;
11787+ }
11788+ }
11789+
11790+
11791+ /**
11792+ * Sets the contract ID for a specific issue.
11793+ *
11794+ * @access public
11795+ * @param integer $issue_id The issue ID
11796+ * @param integer The contract ID
11797+ * @return integer 1 if the update worked, -1 otherwise
11798+ */
11799+ function setContractID($issue_id, $contract_id)
11800+ {
11801+ $issue_id = Misc::escapeInteger($issue_id);
11802+
11803+ $old_contract_id = self::getContractID($issue_id);
11804+
11805+ $stmt = "UPDATE
11806+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11807+ SET
11808+ iss_customer_contract_id = " . Misc::escapeInteger($contract_id) . "
11809+ WHERE
11810+ iss_id=$issue_id";
11811+ $res = DB_Helper::getInstance()->query($stmt);
11812+ if (PEAR::isError($res)) {
11813+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11814+ return -1;
11815+ } else {
11816+ // log this
11817+ History::add($issue_id, Auth::getUserID(), History::getTypeID("contract_changed"), "Contract changed from $old_contract_id to $contract_id by " . User::getFullName(Auth::getUserID()));
11818+ return 1;
11819+ }
11820+ }
11821+
11822+
11823+ /**
11824+ * Returns the customer ID associated with the given issue ID.
11825+ *
11826+ * @access public
11827+ * @param integer $issue_id The issue ID
11828+ * @return integer The customer ID associated with the issue
11829+ */
11830+ function getContactID($issue_id)
11831+ {
11832+ static $returns;
11833+
11834+ $issue_id = Misc::escapeInteger($issue_id);
11835+
11836+ if (!empty($returns[$issue_id])) {
11837+ return $returns[$issue_id];
11838+ }
11839+
11840+ $stmt = "SELECT
11841+ iss_customer_contact_id
11842+ FROM
11843+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11844+ WHERE
11845+ iss_id=$issue_id";
11846+ $res = DB_Helper::getInstance()->getOne($stmt);
11847+ if (PEAR::isError($res)) {
11848+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11849+ return '';
11850+ } else {
11851+ $returns[$issue_id] = $res;
11852+ return $res;
11853+ }
11854+ }
11855+
11856+
11857+ /**
11858+ * Method used to get the project associated to a given issue.
11859+ *
11860+ * @access public
11861+ * @param integer $issue_id The issue ID
11862+ * @param boolean $force_refresh If the cache should not be used.
11863+ * @return integer The project ID
11864+ */
11865+ function getProjectID($issue_id, $force_refresh = false)
11866+ {
11867+ static $returns;
11868+
11869+ $issue_id = Misc::escapeInteger($issue_id);
11870+
11871+ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
11872+ return $returns[$issue_id];
11873+ }
11874+
11875+ $stmt = "SELECT
11876+ iss_prj_id
11877+ FROM
11878+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11879+ WHERE
11880+ iss_id=$issue_id";
11881+ $res = DB_Helper::getInstance()->getOne($stmt);
11882+ if (PEAR::isError($res)) {
11883+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11884+ return '';
11885+ } else {
11886+ $returns[$issue_id] = $res;
11887+ return $res;
11888+ }
11889+ }
11890+
11891+
11892+ /**
11893+ * Method used to remotely assign a given issue to an user.
11894+ *
11895+ * @access public
11896+ * @param integer $issue_id The issue ID
11897+ * @param integer $usr_id The user ID of the person performing the change
11898+ * @param boolean $assignee The user ID of the assignee
11899+ * @return integer The status ID
11900+ */
11901+ function remoteAssign($issue_id, $usr_id, $assignee)
11902+ {
11903+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), array($assignee), true);
11904+ // clear up the assignments for this issue, and then assign it to the current user
11905+ self::deleteUserAssociations($issue_id, $usr_id);
11906+ $res = self::addUserAssociation($usr_id, $issue_id, $assignee, false);
11907+ if ($res != -1) {
11908+ // save a history entry about this...
11909+ History::add($issue_id, $usr_id, History::getTypeID('remote_assigned'), "Issue remotely assigned to " . User::getFullName($assignee) . " by " . User::getFullName($usr_id));
11910+ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'remote_assign'), false);
11911+ if ($assignee != $usr_id) {
11912+ Notification::notifyNewAssignment(array($assignee), $issue_id);
11913+ }
11914+ }
11915+ return $res;
11916+ }
11917+
11918+
11919+ /**
11920+ * Method used to set the status of a given issue.
11921+ *
11922+ * @access public
11923+ * @param integer $issue_id The issue ID
11924+ * @param integer $status_id The new status ID
11925+ * @param boolean $notify If a notification should be sent about this change.
11926+ * @return integer 1 if the update worked, -1 otherwise
11927+ */
11928+ function setStatus($issue_id, $status_id, $notify = false)
11929+ {
11930+ $issue_id = Misc::escapeInteger($issue_id);
11931+ $status_id = Misc::escapeInteger($status_id);
11932+
11933+ $workflow = Workflow::preStatusChange(self::getProjectID($issue_id), $issue_id, $status_id, $notify);
11934+ if ($workflow !== true) {
11935+ return $workflow;
11936+ }
11937+
11938+ // check if the status is already set to the 'new' one
11939+ if (self::getStatusID($issue_id) == $status_id) {
11940+ return -1;
11941+ }
11942+
11943+ $old_status = self::getStatusID($issue_id);
11944+ $old_details = Status::getDetails($old_status);
11945+
11946+ $stmt = "UPDATE
11947+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
11948+ SET
11949+ iss_sta_id=$status_id,
11950+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
11951+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
11952+ iss_last_public_action_type='update'
11953+ WHERE
11954+ iss_id=$issue_id";
11955+ $res = DB_Helper::getInstance()->query($stmt);
11956+ if (PEAR::isError($res)) {
11957+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
11958+ return -1;
11959+ } else {
11960+ // clear out the last-triggered-reminder flag when changing the status of an issue
11961+ Reminder_Action::clearLastTriggered($issue_id);
11962+
11963+ // if old status was closed and new status is not, clear closed data from issue.
11964+ if (@$old_details['sta_is_closed'] == 1) {
11965+ $new_details = Status::getDetails($status_id);
11966+ if ($new_details['sta_is_closed'] != 1) {
11967+ self::clearClosed($issue_id);
11968+ }
11969+ }
11970+
11971+ if ($notify) {
11972+ Notification::notifyStatusChange($issue_id, $old_status, $status_id);
11973+ }
11974+
11975+ return 1;
11976+ }
11977+ }
11978+
11979+
11980+ /**
11981+ * Method used to remotely set the status of a given issue.
11982+ *
11983+ * @access public
11984+ * @param integer $issue_id The issue ID
11985+ * @param integer $usr_id The user ID of the person performing this change
11986+ * @param integer $new_status The new status ID
11987+ * @return integer 1 if the update worked, -1 otherwise
11988+ */
11989+ function setRemoteStatus($issue_id, $usr_id, $new_status)
11990+ {
11991+ $sta_id = Status::getStatusID($new_status);
11992+
11993+ $res = self::setStatus($issue_id, $sta_id);
11994+ if ($res == 1) {
11995+ // record history entry
11996+ History::add($issue_id, $usr_id, History::getTypeID('remote_status_change'), "Status remotely changed to '$new_status' by " . User::getFullName($usr_id));
11997+ }
11998+ return $res;
11999+ }
12000+
12001+
12002+ /**
12003+ * Method used to set the release of an issue
12004+ *
12005+ * @access public
12006+ * @param integer $issue_id The ID of the issue
12007+ * @param integer $pre_id The ID of the release to set this issue too
12008+ * @return integer 1 if the update worked, -1 otherwise
12009+ */
12010+ function setRelease($issue_id, $pre_id)
12011+ {
12012+ $issue_id = Misc::escapeInteger($issue_id);
12013+ $pre_id = Misc::escapeInteger($pre_id);
12014+
12015+ if ($pre_id != self::getRelease($issue_id)) {
12016+ $sql = "UPDATE
12017+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12018+ SET
12019+ iss_pre_id = $pre_id
12020+ WHERE
12021+ iss_id = $issue_id";
12022+ $res = DB_Helper::getInstance()->query($sql);
12023+ if (PEAR::isError($res)) {
12024+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12025+ return -1;
12026+ } else {
12027+ return 1;
12028+ }
12029+ }
12030+ }
12031+
12032+
12033+ /**
12034+ * Returns the current release of an issue
12035+ *
12036+ * @access public
12037+ * @param integer $issue_id The ID of the issue
12038+ * @return integer The release
12039+ */
12040+ function getRelease($issue_id)
12041+ {
12042+ $sql = "SELECT
12043+ iss_pre_id
12044+ FROM
12045+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12046+ WHERE
12047+ iss_id = " . Misc::escapeInteger($issue_id);
12048+ $res = DB_Helper::getInstance()->getOne($sql);
12049+ if (PEAR::isError($res)) {
12050+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12051+ return 0;
12052+ } else {
12053+ return $res;
12054+ }
12055+ }
12056+
12057+
12058+ /**
12059+ * Method used to set the priority of an issue
12060+ *
12061+ * @access public
12062+ * @param integer $issue_id The ID of the issue
12063+ * @param integer $pri_id The ID of the priority to set this issue too
12064+ * @return integer 1 if the update worked, -1 otherwise
12065+ */
12066+ function setPriority($issue_id, $pri_id)
12067+ {
12068+ $issue_id = Misc::escapeInteger($issue_id);
12069+ $pri_id = Misc::escapeInteger($pri_id);
12070+
12071+ if ($pri_id != self::getPriority($issue_id)) {
12072+ $sql = "UPDATE
12073+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12074+ SET
12075+ iss_pri_id = $pri_id
12076+ WHERE
12077+ iss_id = $issue_id";
12078+ $res = DB_Helper::getInstance()->query($sql);
12079+ if (PEAR::isError($res)) {
12080+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12081+ return -1;
12082+ } else {
12083+ return 1;
12084+ }
12085+ }
12086+ }
12087+
12088+
12089+ /**
12090+ * Returns the current issue priority
12091+ *
12092+ * @access public
12093+ * @param integer $issue_id The ID of the issue
12094+ * @return integer The priority
12095+ */
12096+ function getPriority($issue_id)
12097+ {
12098+ $sql = "SELECT
12099+ iss_pri_id
12100+ FROM
12101+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12102+ WHERE
12103+ iss_id = " . Misc::escapeInteger($issue_id);
12104+ $res = DB_Helper::getInstance()->getOne($sql);
12105+ if (PEAR::isError($res)) {
12106+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12107+ return 0;
12108+ } else {
12109+ return $res;
12110+ }
12111+ }
12112+
12113+
12114+ /**
12115+ * Method used to set the category of an issue
12116+ *
12117+ * @access public
12118+ * @param integer $issue_id The ID of the issue
12119+ * @param integer $prc_id The ID of the category to set this issue too
12120+ * @return integer 1 if the update worked, -1 otherwise
12121+ */
12122+ function setCategory($issue_id, $prc_id)
12123+ {
12124+ $issue_id = Misc::escapeInteger($issue_id);
12125+ $prc_id = Misc::escapeInteger($prc_id);
12126+
12127+ if ($prc_id != self::getPriority($issue_id)) {
12128+ $sql = "UPDATE
12129+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12130+ SET
12131+ iss_prc_id = $prc_id
12132+ WHERE
12133+ iss_id = $issue_id";
12134+ $res = DB_Helper::getInstance()->query($sql);
12135+ if (PEAR::isError($res)) {
12136+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12137+ return -1;
12138+ } else {
12139+ return 1;
12140+ }
12141+ }
12142+ }
12143+
12144+
12145+ /**
12146+ * Returns the current issue category
12147+ *
12148+ * @access public
12149+ * @param integer $issue_id The ID of the issue
12150+ * @return integer The category
12151+ */
12152+ function getCategory($issue_id)
12153+ {
12154+ $sql = "SELECT
12155+ iss_prc_id
12156+ FROM
12157+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12158+ WHERE
12159+ iss_id = " . Misc::escapeInteger($issue_id);
12160+ $res = DB_Helper::getInstance()->getOne($sql);
12161+ if (PEAR::isError($res)) {
12162+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12163+ return 0;
12164+ } else {
12165+ return $res;
12166+ }
12167+ }
12168+
12169+
12170+ /**
12171+ * Method used to get all issues associated with a status that doesn't have
12172+ * the 'closed' context.
12173+ *
12174+ * @access public
12175+ * @param integer $prj_id The project ID to list issues from
12176+ * @param integer $usr_id The user ID of the user requesting this information
12177+ * @param boolean $show_all_issues Whether to show all open issues, or just the ones assigned to the given email address
12178+ * @param integer $status_id The status ID to be used to restrict results
12179+ * @return array The list of open issues
12180+ */
12181+ function getOpenIssues($prj_id, $usr_id, $show_all_issues, $status_id)
12182+ {
12183+ $prj_id = Misc::escapeInteger($prj_id);
12184+ $status_id = Misc::escapeInteger($status_id);
12185+ $projects = Project::getRemoteAssocListByUser($usr_id);
12186+ if (@count($projects) == 0) {
12187+ return '';
12188+ }
12189+
12190+ $stmt = "SELECT
12191+ iss_id,
12192+ iss_summary,
12193+ sta_title
12194+ FROM
12195+ (
12196+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
12197+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
12198+ )
12199+ LEFT JOIN
12200+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
12201+ ON
12202+ isu_iss_id=iss_id
12203+ WHERE ";
12204+ if (!empty($status_id)) {
12205+ $stmt .= " sta_id=$status_id AND ";
12206+ }
12207+ $stmt .= "
12208+ iss_prj_id=$prj_id AND
12209+ sta_id=iss_sta_id AND
12210+ sta_is_closed=0";
12211+ if ($show_all_issues == false) {
12212+ $stmt .= " AND
12213+ isu_usr_id=$usr_id";
12214+ }
12215+ $stmt .= "\nGROUP BY
12216+ iss_id";
12217+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
12218+ if (PEAR::isError($res)) {
12219+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12220+ return '';
12221+ } else {
12222+ if (count($res) > 0) {
12223+ self::getAssignedUsersByIssues($res);
12224+ }
12225+ return $res;
12226+ }
12227+ }
12228+
12229+
12230+ /**
12231+ * Method used to build the required parameters to simulate an email reply
12232+ * to the user who reported the issue, using the issue details like summary
12233+ * and description as email fields.
12234+ *
12235+ * @access public
12236+ * @param integer $issue_id The issue ID
12237+ * @return array The email parameters
12238+ */
12239+ function getReplyDetails($issue_id)
12240+ {
12241+ $issue_id = Misc::escapeInteger($issue_id);
12242+
12243+ $stmt = "SELECT
12244+ iss_created_date,
12245+ usr_full_name AS reporter,
12246+ usr_email AS reporter_email,
12247+ iss_description AS description,
12248+ iss_summary AS sup_subject
12249+ FROM
12250+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
12251+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
12252+ WHERE
12253+ iss_usr_id=usr_id AND
12254+ iss_id=$issue_id";
12255+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
12256+ if (PEAR::isError($res)) {
12257+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12258+ return '';
12259+ } else {
12260+ $res['reply_subject'] = 'Re: [#' . $issue_id . '] ' . $res["sup_subject"];
12261+ $res['created_date_ts'] = Date_Helper::getUnixTimestamp($res['iss_created_date'], 'GMT');
12262+ return $res;
12263+ }
12264+ }
12265+
12266+
12267+ /**
12268+ * Method used to record the last updated timestamp for a given
12269+ * issue ID.
12270+ *
12271+ * @access public
12272+ * @param integer $issue_id The issue ID
12273+ * @param string $type The type of update that was made (optional)
12274+ * @return boolean
12275+ */
12276+ function markAsUpdated($issue_id, $type = false)
12277+ {
12278+ $public = array("staff response", "customer action", "file uploaded", "user response");
12279+ $stmt = "UPDATE
12280+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12281+ SET
12282+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "'\n";
12283+ if ($type != false) {
12284+ if (in_array($type, $public)) {
12285+ $field = "iss_last_public_action_";
12286+ } else {
12287+ $field = "iss_last_internal_action_";
12288+ }
12289+ $stmt .= ",\n " . $field . "date = '" . Date_Helper::getCurrentDateGMT() . "',\n" .
12290+ $field . "type ='" . Misc::escapeString($type) . "'\n";
12291+ }
12292+ $stmt .= "WHERE
12293+ iss_id=" . Misc::escapeInteger($issue_id);
12294+ $res = DB_Helper::getInstance()->query($stmt);
12295+ if (PEAR::isError($res)) {
12296+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12297+ return false;
12298+ } else {
12299+ // update last response dates if this is a staff response
12300+ if ($type == "staff response") {
12301+ $stmt = "UPDATE
12302+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12303+ SET
12304+ iss_last_response_date='" . Date_Helper::getCurrentDateGMT() . "'
12305+ WHERE
12306+ iss_id = " . Misc::escapeInteger($issue_id);
12307+ DB_Helper::getInstance()->query($stmt);
12308+ $stmt = "UPDATE
12309+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12310+ SET
12311+ iss_first_response_date='" . Date_Helper::getCurrentDateGMT() . "'
12312+ WHERE
12313+ iss_first_response_date IS NULL AND
12314+ iss_id = " . Misc::escapeInteger($issue_id);
12315+ DB_Helper::getInstance()->query($stmt);
12316+ }
12317+
12318+ return true;
12319+ }
12320+ }
12321+
12322+
12323+ /**
12324+ * Method used to check whether a given issue has duplicates
12325+ * or not.
12326+ *
12327+ * @access public
12328+ * @param integer $issue_id The issue ID
12329+ * @return boolean
12330+ */
12331+ function hasDuplicates($issue_id)
12332+ {
12333+ $stmt = "SELECT
12334+ COUNT(iss_id)
12335+ FROM
12336+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12337+ WHERE
12338+ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
12339+ $res = DB_Helper::getInstance()->getOne($stmt);
12340+ if (PEAR::isError($res)) {
12341+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12342+ return false;
12343+ } else {
12344+ if ($res == 0) {
12345+ return false;
12346+ } else {
12347+ return true;
12348+ }
12349+ }
12350+ }
12351+
12352+
12353+ /**
12354+ * Method used to update the duplicated issues for a given
12355+ * issue ID.
12356+ *
12357+ * @access public
12358+ * @param integer $issue_id The issue ID
12359+ * @return integer 1 if the update worked, -1 otherwise
12360+ */
12361+ function updateDuplicates($issue_id)
12362+ {
12363+ $issue_id = Misc::escapeInteger($issue_id);
12364+
12365+ $ids = self::getDuplicateList($issue_id);
12366+ if ($ids == '') {
12367+ return -1;
12368+ }
12369+ $ids = @array_keys($ids);
12370+ $stmt = "UPDATE
12371+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12372+ SET
12373+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12374+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12375+ iss_last_internal_action_type='updated',
12376+ iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
12377+ if (@$_POST["keep"] == "no") {
12378+ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
12379+ }
12380+ $stmt .= "
12381+ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
12382+ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
12383+ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . "
12384+ WHERE
12385+ iss_id IN (" . implode(", ", $ids) . ")";
12386+ $res = DB_Helper::getInstance()->query($stmt);
12387+ if (PEAR::isError($res)) {
12388+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12389+ return -1;
12390+ } else {
12391+ // record the change
12392+ for ($i = 0; $i < count($ids); $i++) {
12393+ History::add($ids[$i], Auth::getUserID(), History::getTypeID('duplicate_update'),
12394+ "The details for issue #$issue_id were updated by " . User::getFullName(Auth::getUserID()) . " and the changes propagated to the duplicated issues.");
12395+ }
12396+ return 1;
12397+ }
12398+ }
12399+
12400+
12401+ /**
12402+ * Method used to get a list of the duplicate issues for a given
12403+ * issue ID.
12404+ *
12405+ * @access public
12406+ * @param integer $issue_id The issue ID
12407+ * @return array The list of duplicates
12408+ */
12409+ function getDuplicateList($issue_id)
12410+ {
12411+ $res = self::getDuplicateDetailsList($issue_id);
12412+ if (@count($res) == 0) {
12413+ return '';
12414+ } else {
12415+ $list = array();
12416+ for ($i = 0; $i < count($res); $i++) {
12417+ $list[$res[$i]['issue_id']] = $res[$i]['title'];
12418+ }
12419+ return $list;
12420+ }
12421+ }
12422+
12423+
12424+ /**
12425+ * Method used to get a list of the duplicate issues (and their details)
12426+ * for a given issue ID.
12427+ *
12428+ * @access public
12429+ * @param integer $issue_id The issue ID
12430+ * @return array The list of duplicates
12431+ */
12432+ function getDuplicateDetailsList($issue_id)
12433+ {
12434+ static $returns;
12435+
12436+ if (!empty($returns[$issue_id])) {
12437+ return $returns[$issue_id];
12438+ }
12439+
12440+ $stmt = "SELECT
12441+ iss_id issue_id,
12442+ iss_summary title,
12443+ sta_title current_status,
12444+ sta_is_closed is_closed
12445+ FROM
12446+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
12447+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
12448+ WHERE
12449+ iss_sta_id=sta_id AND
12450+ iss_duplicated_iss_id=" . Misc::escapeInteger($issue_id);
12451+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
12452+ if (PEAR::isError($res)) {
12453+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12454+ return array();
12455+ } else {
12456+ $returns[$issue_id] = $res;
12457+ return $res;
12458+ }
12459+ }
12460+
12461+
12462+ /**
12463+ * Method used to clear the duplicate status of an issue.
12464+ *
12465+ * @access public
12466+ * @param integer $issue_id The issue ID
12467+ * @return integer 1 if the update worked, -1 otherwise
12468+ */
12469+ function clearDuplicateStatus($issue_id)
12470+ {
12471+ $issue_id = Misc::escapeInteger($issue_id);
12472+ $stmt = "UPDATE
12473+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12474+ SET
12475+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12476+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12477+ iss_last_internal_action_type='updated',
12478+ iss_duplicated_iss_id=NULL
12479+ WHERE
12480+ iss_id=$issue_id";
12481+ $res = DB_Helper::getInstance()->query($stmt);
12482+ if (PEAR::isError($res)) {
12483+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12484+ return -1;
12485+ } else {
12486+ // record the change
12487+ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_removed'), "Duplicate flag was reset by " . User::getFullName(Auth::getUserID()));
12488+ return 1;
12489+ }
12490+ }
12491+
12492+
12493+ /**
12494+ * Method used to mark an issue as a duplicate of an existing one.
12495+ *
12496+ * @access public
12497+ * @param integer $issue_id The issue ID
12498+ * @return integer 1 if the update worked, -1 otherwise
12499+ */
12500+ function markAsDuplicate($issue_id)
12501+ {
12502+ $issue_id = Misc::escapeInteger($issue_id);
12503+ if (!self::exists($issue_id)) {
12504+ return -1;
12505+ }
12506+
12507+ $stmt = "UPDATE
12508+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12509+ SET
12510+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12511+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12512+ iss_last_internal_action_type='updated',
12513+ iss_duplicated_iss_id=" . Misc::escapeInteger($_POST["duplicated_issue"]) . "
12514+ WHERE
12515+ iss_id=$issue_id";
12516+ $res = DB_Helper::getInstance()->query($stmt);
12517+ if (PEAR::isError($res)) {
12518+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12519+ return -1;
12520+ } else {
12521+ if (!empty($_POST["comments"])) {
12522+ // add note with the comments of marking an issue as a duplicate of another one
12523+ $_POST['title'] = 'Issue duplication comments';
12524+ $_POST["note"] = $_POST["comments"];
12525+ Note::insert(Auth::getUserID(), $issue_id);
12526+ }
12527+ // record the change
12528+ History::add($issue_id, Auth::getUserID(), History::getTypeID('duplicate_added'),
12529+ "Issue marked as a duplicate of issue #" . $_POST["duplicated_issue"] . " by " . User::getFullName(Auth::getUserID()));
12530+ return 1;
12531+ }
12532+ }
12533+
12534+
12535+ function isDuplicate($issue_id)
12536+ {
12537+ $sql = "SELECT
12538+ count(iss_id)
12539+ FROM
12540+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12541+ WHERE
12542+ iss_id = " . Misc::escapeInteger($issue_id) . " AND
12543+ iss_duplicated_iss_id IS NULL";
12544+ $res = DB_Helper::getInstance()->getOne($sql);
12545+ if (PEAR::isError($res)) {
12546+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12547+ return false;
12548+ }
12549+ if ($res > 0) {
12550+ return false;
12551+ } else {
12552+ return true;
12553+ }
12554+ }
12555+
12556+
12557+ /**
12558+ * Method used to get an associative array of user ID => user
12559+ * status associated with a given issue ID.
12560+ *
12561+ * @access public
12562+ * @param integer $issue_id The issue ID
12563+ * @return array The list of users
12564+ */
12565+ function getAssignedUsersStatus($issue_id)
12566+ {
12567+ $stmt = "SELECT
12568+ usr_id,
12569+ usr_status
12570+ FROM
12571+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
12572+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
12573+ WHERE
12574+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
12575+ isu_usr_id=usr_id";
12576+ $res = DB_Helper::getInstance()->getAssoc($stmt);
12577+ if (PEAR::isError($res)) {
12578+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12579+ return array();
12580+ } else {
12581+ return $res;
12582+ }
12583+ }
12584+
12585+
12586+ /**
12587+ * Method used to get the summary associated with a given issue ID.
12588+ *
12589+ * @access public
12590+ * @param integer $issue_id The issue ID
12591+ * @return string The issue summary
12592+ */
12593+ function getTitle($issue_id)
12594+ {
12595+ $stmt = "SELECT
12596+ iss_summary
12597+ FROM
12598+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12599+ WHERE
12600+ iss_id=" . Misc::escapeInteger($issue_id);
12601+ $res = DB_Helper::getInstance()->getOne($stmt);
12602+ if (PEAR::isError($res)) {
12603+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12604+ return "";
12605+ } else {
12606+ return $res;
12607+ }
12608+ }
12609+
12610+
12611+ /**
12612+ * Method used to get the issue ID associated with a specific summary.
12613+ *
12614+ * @access public
12615+ * @param string $summary The summary to look for
12616+ * @return integer The issue ID
12617+ */
12618+ function getIssueID($summary)
12619+ {
12620+ $stmt = "SELECT
12621+ iss_id
12622+ FROM
12623+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12624+ WHERE
12625+ iss_summary='" . Misc::escapeString($summary) . "'";
12626+ $res = DB_Helper::getInstance()->getOne($stmt);
12627+ if (PEAR::isError($res)) {
12628+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12629+ return 0;
12630+ } else {
12631+ if (empty($res)) {
12632+ return 0;
12633+ } else {
12634+ return $res;
12635+ }
12636+ }
12637+ }
12638+
12639+
12640+ /**
12641+ * Method used to add a new anonymous based issue in the system.
12642+ *
12643+ * @access public
12644+ * @return integer The new issue ID
12645+ */
12646+ function addAnonymousReport()
12647+ {
12648+ $options = Project::getAnonymousPostOptions($_POST["project"]);
12649+ $initial_status = Project::getInitialStatus($_POST["project"]);
12650+ $stmt = "INSERT INTO
12651+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12652+ (
12653+ iss_prj_id,
12654+ iss_prc_id,
12655+ iss_pre_id,
12656+ iss_pri_id,
12657+ iss_usr_id,";
12658+ if (!empty($initial_status)) {
12659+ $stmt .= "iss_sta_id,";
12660+ }
12661+ $stmt .= "
12662+ iss_created_date,
12663+ iss_last_public_action_date,
12664+ iss_last_public_action_type,
12665+ iss_summary,
12666+ iss_description,
12667+ iss_root_message_id
12668+ ) VALUES (
12669+ " . Misc::escapeInteger($_POST["project"]) . ",
12670+ " . $options["category"] . ",
12671+ 0,
12672+ " . $options["priority"] . ",
12673+ " . $options["reporter"] . ",";
12674+ if (!empty($initial_status)) {
12675+ $stmt .= "$initial_status,";
12676+ }
12677+ $stmt .= "
12678+ '" . Date_Helper::getCurrentDateGMT() . "',
12679+ '" . Date_Helper::getCurrentDateGMT() . "',
12680+ 'created',
12681+ '" . Misc::escapeString($_POST["summary"]) . "',
12682+ '" . Misc::escapeString($_POST["description"]) . "',
12683+ '" . Misc::escapeString(Mail_Helper::generateMessageID()) . "'
12684+ )";
12685+ $res = DB_Helper::getInstance()->query($stmt);
12686+ if (PEAR::isError($res)) {
12687+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12688+ return $res;
12689+ } else {
12690+ $new_issue_id = DB_Helper::get_last_insert_id();
12691+ // log the creation of the issue
12692+ History::add($new_issue_id, APP_SYSTEM_USER_ID, History::getTypeID('issue_opened_anon'), 'Issue opened anonymously');
12693+
12694+ // now process any files being uploaded
12695+ $found = 0;
12696+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
12697+ if (!@empty($_FILES["file"]["name"][$i])) {
12698+ $found = 1;
12699+ break;
12700+ }
12701+ }
12702+ if ($found) {
12703+ $attachment_id = Attachment::add($new_issue_id, $options["reporter"], 'files uploaded anonymously');
12704+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
12705+ $filename = @$_FILES["file"]["name"][$i];
12706+ if (empty($filename)) {
12707+ continue;
12708+ }
12709+ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
12710+ if (!empty($blob)) {
12711+ Attachment::addFile($attachment_id, $filename, $_FILES["file"]["type"][$i], $blob);
12712+ }
12713+ }
12714+ }
12715+ // need to process any custom fields ?
12716+ if (@count($_POST["custom_fields"]) > 0) {
12717+ foreach ($_POST["custom_fields"] as $fld_id => $value) {
12718+ Custom_Field::associateIssue($new_issue_id, $fld_id, $value);
12719+ }
12720+ }
12721+
12722+ // now add the user/issue association
12723+ $assign = array();
12724+ $users = @$options["users"];
12725+ $actions = Notification::getDefaultActions($new_issue_id, false, 'anon_issue');
12726+ for ($i = 0; $i < count($users); $i++) {
12727+ Notification::subscribeUser(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i], $actions);
12728+ self::addUserAssociation(APP_SYSTEM_USER_ID, $new_issue_id, $users[$i]);
12729+ $assign[] = $users[$i];
12730+ }
12731+
12732+ Workflow::handleNewIssue(Misc::escapeInteger($_POST["project"]), $new_issue_id, false, false);
12733+
12734+ // also notify any users that want to receive emails anytime a new issue is created
12735+ Notification::notifyNewIssue($_POST['project'], $new_issue_id);
12736+
12737+ return $new_issue_id;
12738+ }
12739+ }
12740+
12741+
12742+ /**
12743+ * Method used to remove all issues associated with a specific list of
12744+ * projects.
12745+ *
12746+ * @access public
12747+ * @param array $ids The list of projects to look for
12748+ * @return boolean
12749+ */
12750+ function removeByProjects($ids)
12751+ {
12752+ $items = @implode(", ", Misc::escapeInteger($ids));
12753+ $stmt = "SELECT
12754+ iss_id
12755+ FROM
12756+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12757+ WHERE
12758+ iss_prj_id IN ($items)";
12759+ $res = DB_Helper::getInstance()->getCol($stmt);
12760+ if (PEAR::isError($res)) {
12761+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12762+ return false;
12763+ } else {
12764+ if (count($res) > 0) {
12765+ self::deleteAssociations($res);
12766+ Attachment::removeByIssues($res);
12767+ SCM::removeByIssues($res);
12768+ Impact_Analysis::removeByIssues($res);
12769+ self::deleteUserAssociations($res);
12770+ Note::removeByIssues($res);
12771+ Time_Tracking::removeByIssues($res);
12772+ Notification::removeByIssues($res);
12773+ Custom_Field::removeByIssues($res);
12774+ Phone_Support::removeByIssues($res);
12775+ History::removeByIssues($res);
12776+ // now really delete the issues
12777+ $items = implode(", ", $res);
12778+ $stmt = "DELETE FROM
12779+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12780+ WHERE
12781+ iss_id IN ($items)";
12782+ DB_Helper::getInstance()->query($stmt);
12783+ }
12784+ return true;
12785+ }
12786+ }
12787+
12788+
12789+ /**
12790+ * Method used to close off an issue.
12791+ *
12792+ * @access public
12793+ * @param integer $usr_id The user ID
12794+ * @param integer $issue_id The issue ID
12795+ * @param bool $send_notification Whether to send a notification about this action or not
12796+ * @param integer $resolution_id The resolution ID
12797+ * @param integer $status_id The status ID
12798+ * @param string $reason The reason for closing this issue
12799+ * @param string $send_notification_to Who this notification should be sent too
12800+ * @return integer 1 if the update worked, -1 otherwise
12801+ */
12802+ function close($usr_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason, $send_notification_to = 'internal')
12803+ {
12804+ $usr_id = Misc::escapeInteger($usr_id);
12805+ $issue_id = Misc::escapeInteger($issue_id);
12806+ $resolution_id = Misc::escapeInteger($resolution_id);
12807+ $status_id = Misc::escapeInteger($status_id);
12808+
12809+ $stmt = "UPDATE
12810+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12811+ SET
12812+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12813+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12814+ iss_last_public_action_type='closed',
12815+ iss_closed_date='" . Date_Helper::getCurrentDateGMT() . "',\n";
12816+ if (!empty($resolution_id)) {
12817+ $stmt .= "iss_res_id=$resolution_id,\n";
12818+ }
12819+ $stmt .= "iss_sta_id=$status_id
12820+ WHERE
12821+ iss_id=$issue_id";
12822+ $res = DB_Helper::getInstance()->query($stmt);
12823+ if (PEAR::isError($res)) {
12824+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
12825+ return -1;
12826+ } else {
12827+ self::moveOrderForAllUsers($issue_id, 1000);
12828+ $prj_id = self::getProjectID($issue_id);
12829+
12830+ // record the change
12831+ History::add($issue_id, $usr_id, History::getTypeID('issue_closed'), "Issue updated to status '" . Status::getStatusTitle($status_id) . "' by " . User::getFullName($usr_id));
12832+
12833+ if ($send_notification_to == 'all') {
12834+
12835+ $from = User::getFromHeader($usr_id);
12836+ $message_id = User::getFromHeader($usr_id);
12837+ $full_email = Support::buildFullHeaders($issue_id, $message_id, $from,
12838+ '', '', 'Issue closed comments', $reason, '');
12839+
12840+ $structure = Mime_Helper::decode($full_email, true, false);
12841+
12842+ $email = array(
12843+ 'ema_id' => Email_Account::getEmailAccount(self::getProjectID($issue_id)),
12844+ 'issue_id' => $issue_id,
12845+ 'message_id' => $message_id,
12846+ 'date' => Date_Helper::getCurrentDateGMT(),
12847+ 'subject' => 'Issue closed comments',
12848+ 'from' => $from,
12849+ 'has_attachment'=> 0,
12850+ 'body' => $reason,
12851+ 'full_email' => $full_email,
12852+ 'headers' => $structure->headers
12853+ );
12854+ Support::insertEmail($email, $structure, $sup_id, true);
12855+ $ids = $sup_id;
12856+ } else {
12857+ // add note with the reason to close the issue
12858+ $_POST['title'] = 'Issue closed comments';
12859+ $_POST["note"] = $reason;
12860+ Note::insert($usr_id, $issue_id, false, true, true, $send_notification);
12861+ $ids = false;
12862+ }
12863+
12864+ if ($send_notification) {
12865+ if (Customer::hasCustomerIntegration($prj_id)) {
12866+ // send a special confirmation email when customer issues are closed
12867+ $stmt = "SELECT
12868+ iss_customer_contact_id
12869+ FROM
12870+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12871+ WHERE
12872+ iss_id=$issue_id";
12873+ $customer_contact_id = DB_Helper::getInstance()->getOne($stmt);
12874+ if (!empty($customer_contact_id)) {
12875+ Customer::notifyIssueClosed($prj_id, $issue_id, $customer_contact_id, $send_notification, $resolution_id, $status_id, $reason);
12876+ }
12877+ }
12878+ // send notifications for the issue being closed
12879+ Notification::notify($issue_id, 'closed', $ids);
12880+ }
12881+ Workflow::handleIssueClosed($prj_id, $issue_id, $send_notification, $resolution_id, $status_id, $reason);
12882+ return 1;
12883+ }
12884+ }
12885+
12886+
12887+ /**
12888+ * Method used to update the details of a specific issue.
12889+ *
12890+ * @access public
12891+ * @param integer $issue_id The issue ID
12892+ * @return integer 1 if the update worked, -1 or -2 otherwise
12893+ */
12894+ function update($issue_id)
12895+ {
12896+ global $errors;
12897+ $errors = array();
12898+
12899+ $issue_id = Misc::escapeInteger($issue_id);
12900+
12901+ $usr_id = Auth::getUserID();
12902+ $prj_id = self::getProjectID($issue_id);
12903+
12904+ $workflow = Workflow::preIssueUpdated($prj_id, $issue_id, $usr_id, $_POST);
12905+ if ($workflow !== true) {
12906+ return $workflow;
12907+ }
12908+
12909+ // get all of the 'current' information of this issue
12910+ $current = self::getDetails($issue_id);
12911+ // update the issue associations
12912+ if (empty($_POST['associated_issues'])) {
12913+ $associated_issues = array();
12914+ } else {
12915+ $associated_issues = explode(',', @$_POST['associated_issues']);
12916+ // make sure all associated issues are valid (and in this project)
12917+ for ($i = 0; $i < count($associated_issues); $i++) {
12918+ if (!self::exists(trim($associated_issues[$i]), false)) {
12919+ $errors['Associated Issues'][] = 'Issue #' . $associated_issues[$i] . ' does not exist and was removed from the list of associated issues.';
12920+ unset($associated_issues[$i]);
12921+ }
12922+ }
12923+ }
12924+ $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
12925+ if (count($association_diff) > 0) {
12926+ // go through the new assocations, if association already exists, skip it
12927+ $associations_to_remove = $current['associated_issues'];
12928+ if (count($associated_issues) > 0) {
12929+ foreach ($associated_issues as $index => $associated_id) {
12930+ if (!in_array($associated_id, $current['associated_issues'])) {
12931+ self::addAssociation($issue_id, $associated_id, $usr_id);
12932+ } else {
12933+ // already assigned, remove this user from list of users to remove
12934+ unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
12935+ }
12936+ }
12937+ }
12938+ if (count($associations_to_remove) > 0) {
12939+ foreach ($associations_to_remove as $associated_id) {
12940+ self::deleteAssociation($issue_id, $associated_id);
12941+ }
12942+ }
12943+ }
12944+ $assignments_changed = false;
12945+ if (@$_POST["keep_assignments"] == "no") {
12946+ // only change the issue-user associations if there really were any changes
12947+ $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
12948+ if (!empty($_POST['assignments'])) {
12949+ $new_assignees = @$_POST['assignments'];
12950+ } else {
12951+ $new_assignees = array();
12952+ }
12953+ $assignment_notifications = array();
12954+
12955+ // remove people from the assignment list, if appropriate
12956+ foreach ($old_assignees as $assignee) {
12957+ if (!in_array($assignee, $new_assignees)) {
12958+ self::deleteUserAssociation($issue_id, $assignee);
12959+ $assignments_changed = true;
12960+ }
12961+ }
12962+ // add people to the assignment list, if appropriate
12963+ foreach ($new_assignees as $assignee) {
12964+ if (!in_array($assignee, $old_assignees)) {
12965+ self::addUserAssociation($usr_id, $issue_id, $assignee);
12966+ Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'issue_update'), TRUE);
12967+ $assignment_notifications[] = $assignee;
12968+ $assignments_changed = true;
12969+ }
12970+ }
12971+ if (count($assignment_notifications) > 0) {
12972+ Notification::notifyNewAssignment($assignment_notifications, $issue_id);
12973+ }
12974+ }
12975+ if (empty($_POST["estimated_dev_time"])) {
12976+ $_POST["estimated_dev_time"] = 0;
12977+ }
12978+ $stmt = "UPDATE
12979+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
12980+ SET
12981+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
12982+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
12983+ iss_last_public_action_type='updated',";
12984+ if (!empty($_POST["category"])) {
12985+ $stmt .= "iss_prc_id=" . Misc::escapeInteger($_POST["category"]) . ",";
12986+ }
12987+ if (@$_POST["keep"] == "no") {
12988+ $stmt .= "iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",";
12989+ }
12990+ if (!empty($_POST['expected_resolution_date'])) {
12991+ $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($_POST['expected_resolution_date']) . "',";
12992+ } else {
12993+ $stmt .= "iss_expected_resolution_date=null,";
12994+ }
12995+ $stmt .= "
12996+ iss_pre_id=" . Misc::escapeInteger($_POST["release"]) . ",
12997+ iss_pri_id=" . Misc::escapeInteger($_POST["priority"]) . ",
12998+ iss_sta_id=" . Misc::escapeInteger($_POST["status"]) . ",
12999+ iss_res_id=" . Misc::escapeInteger($_POST["resolution"]) . ",
13000+ iss_summary='" . Misc::escapeString($_POST["summary"]) . "',
13001+ iss_description='" . Misc::escapeString($_POST["description"]) . "',
13002+ iss_dev_time='" . Misc::escapeString($_POST["estimated_dev_time"]) . "',
13003+ iss_percent_complete= '" . Misc::escapeString($_POST["percent_complete"]) . "',
13004+ iss_trigger_reminders=" . Misc::escapeInteger($_POST["trigger_reminders"]) . ",
13005+ iss_grp_id ='" . Misc::escapeInteger($_POST["group"]) . "'";
13006+ if (isset($_POST['private'])) {
13007+ $stmt .= ",
13008+ iss_private = " . Misc::escapeInteger($_POST['private']);
13009+ }
13010+ $stmt .= "
13011+ WHERE
13012+ iss_id=$issue_id";
13013+ $res = DB_Helper::getInstance()->query($stmt);
13014+ if (PEAR::isError($res)) {
13015+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13016+ return -1;
13017+ } else {
13018+ // add change to the history (only for changes on specific fields?)
13019+ $updated_fields = array();
13020+ if ($current["iss_expected_resolution_date"] != $_POST['expected_resolution_date']) {
13021+ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $_POST['expected_resolution_date']);
13022+ }
13023+ if ($current["iss_prc_id"] != $_POST["category"]) {
13024+ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($_POST["category"]));
13025+ }
13026+ if ($current["iss_pre_id"] != $_POST["release"]) {
13027+ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($_POST["release"]));
13028+ }
13029+ if ($current["iss_pri_id"] != $_POST["priority"]) {
13030+ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($_POST["priority"]));
13031+ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $_POST);
13032+ }
13033+ if ($current["iss_sta_id"] != $_POST["status"]) {
13034+ // clear out the last-triggered-reminder flag when changing the status of an issue
13035+ Reminder_Action::clearLastTriggered($issue_id);
13036+
13037+ // if old status was closed and new status is not, clear closed data from issue.
13038+ $old_status_details = Status::getDetails($current['iss_sta_id']);
13039+ if ($old_status_details['sta_is_closed'] == 1) {
13040+ $new_status_details = Status::getDetails($_POST["status"]);
13041+ if ($new_status_details['sta_is_closed'] != 1) {
13042+ self::clearClosed($issue_id);
13043+ }
13044+ }
13045+ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($_POST["status"]));
13046+ }
13047+ if ($current["iss_res_id"] != $_POST["resolution"]) {
13048+ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($_POST["resolution"]));
13049+ }
13050+ if ($current["iss_dev_time"] != $_POST["estimated_dev_time"]) {
13051+ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($_POST["estimated_dev_time"]*60)));
13052+ }
13053+ if ($current["iss_summary"] != $_POST["summary"]) {
13054+ $updated_fields["Summary"] = '';
13055+ }
13056+ if ($current["iss_description"] != $_POST["description"]) {
13057+ $updated_fields["Description"] = '';
13058+ }
13059+ if ((isset($_POST['private'])) && ($_POST['private'] != $current['iss_private'])) {
13060+ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($_POST['private']));
13061+ }
13062+ if (count($updated_fields) > 0) {
13063+ // log the changes
13064+ $changes = '';
13065+ $i = 0;
13066+ foreach ($updated_fields as $key => $value) {
13067+ if ($i > 0) {
13068+ $changes .= "; ";
13069+ }
13070+ if (($key != "Summary") && ($key != "Description")) {
13071+ $changes .= "$key: $value";
13072+ } else {
13073+ $changes .= "$key";
13074+ }
13075+ $i++;
13076+ }
13077+ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
13078+ // send notifications for the issue being updated
13079+ Notification::notifyIssueUpdated($issue_id, $current, $_POST);
13080+ }
13081+
13082+ // record group change as a seperate change
13083+ if ($current["iss_grp_id"] != (int)$_POST["group"]) {
13084+ History::add($issue_id, $usr_id, History::getTypeID('group_changed'),
13085+ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($_POST["group"])) . ") by " . User::getFullName($usr_id));
13086+ }
13087+
13088+ // now update any duplicates, if any
13089+ $update_dupe = array(
13090+ 'Category',
13091+ 'Release',
13092+ 'Priority',
13093+ 'Release',
13094+ 'Resolution'
13095+ );
13096+ // COMPAT: the following line requires PHP > 4.0.4
13097+ $intersect = array_intersect($update_dupe, array_keys($updated_fields));
13098+ if (($current["duplicates"] != '') && (count($intersect) > 0)) {
13099+ self::updateDuplicates($issue_id);
13100+ }
13101+
13102+ // if there is customer integration, mark last customer action
13103+ if ((Customer::hasCustomerIntegration($prj_id)) && (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer'))) {
13104+ self::recordLastCustomerAction($issue_id);
13105+ }
13106+
13107+ if ($assignments_changed) {
13108+ // XXX: we may want to also send the email notification for those "new" assignees
13109+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, $usr_id, self::getDetails($issue_id), @$_POST['assignments'], false);
13110+ }
13111+
13112+ Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $_POST);
13113+ // Move issue to another project
13114+ if (isset($_POST['move_issue']) and (User::getRoleByUser($usr_id, $prj_id) >= User::getRoleID("Developer"))) {
13115+ $new_prj_id = (int)@$_POST['new_prj'];
13116+ if (($prj_id != $new_prj_id) && (array_key_exists($new_prj_id, Project::getAssocList($usr_id)))) {
13117+ if(User::getRoleByUser($usr_id, $new_prj_id) >= User::getRoleID("Reporter")) {
13118+ $res = self::moveIssue($issue_id, $new_prj_id);
13119+ if ($res == -1) {
13120+ return $res;
13121+ }
13122+ } else {
13123+ return -1;
13124+ }
13125+ }
13126+ }
13127+ return 1;
13128+ }
13129+ }
13130+
13131+ /**
13132+ * Method used to update the a single detail field of a specific issue.
13133+ *
13134+ * @param integer $issue_id
13135+ * @param string $field_name
13136+ * @param string $field_value
13137+ * @param string $field_type string or integer (for escape)
13138+ * @return integer 1 on success, -1 otherwise
13139+ */
13140+ function updateField($issue_id, $field_name, $filed_value) {
13141+
13142+ $issue_id = Misc::escapeInteger($issue_id);
13143+
13144+ $usr_id = Auth::getUserID();
13145+ $prj_id = self::getProjectID($issue_id);
13146+
13147+ // get all of the 'current' information of this issue
13148+ $current = self::getDetails($issue_id);
13149+
13150+ $stmt = "UPDATE
13151+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
13152+ SET
13153+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
13154+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
13155+ iss_last_public_action_type='updated'";
13156+
13157+ switch ($field_name) {
13158+ case 'category':
13159+ $stmt .= ", iss_prc_id = " . Misc::escapeInteger($filed_value);
13160+ break;
13161+ case 'release':
13162+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
13163+ break;
13164+ case 'expected_resolution_date':
13165+ if (is_null($filed_value)) {
13166+ $stmt .= ", iss_expected_resolution_date = null";
13167+ } else {
13168+ $stmt .= ", iss_expected_resolution_date = '" . Misc::escapeString($filed_value) . "'";
13169+ }
13170+ break;
13171+ case 'release':
13172+ $stmt .= ", iss_pre_id = " . Misc::escapeInteger($filed_value);
13173+ break;
13174+ case 'priority':
13175+ $stmt .= ", iss_pri_id = " . Misc::escapeInteger($filed_value);
13176+ break;
13177+ case 'status':
13178+ $stmt .= ", iss_sta_id = " . Misc::escapeInteger($filed_value);
13179+ break;
13180+ case 'resolution':
13181+ $stmt .= ", iss_res_id = " . Misc::escapeInteger($filed_value);
13182+ break;
13183+ case 'summary':
13184+ $stmt .= ", iss_summary = '" . Misc::escapeString($filed_value) . "'";
13185+ break;
13186+ case 'description':
13187+ $stmt .= ", iss_description = '" . Misc::escapeString($filed_value) . "'";
13188+ break;
13189+ case 'estimated_dev_time':
13190+ $stmt .= ", iss_dev_time = '" . Misc::escapeString($filed_value) . "'";
13191+ break;
13192+ case 'percent_complete':
13193+ $stmt .= ", iss_percent_complete = '" . Misc::escapeString($filed_value) . "'";
13194+ break;
13195+ case 'trigger_reminders':
13196+ $stmt .= ", iss_trigger_reminders = " . Misc::escapeInteger($filed_value);
13197+ break;
13198+ case 'group':
13199+ $stmt .= ", iss_grp_id = " . Misc::escapeInteger($filed_value);
13200+ break;
13201+ case 'private':
13202+ $stmt .= ", iss_private = " . Misc::escapeInteger($filed_value);
13203+ break;
13204+ default:
13205+ Error_Handler::logError("Unknown field name $field_name", __FILE__, __LINE__);
13206+ return -1;
13207+ break;
13208+ }
13209+
13210+ $stmt .= "
13211+ WHERE
13212+ iss_id=$issue_id";
13213+
13214+ $res = DB_Helper::getInstance()->query($stmt);
13215+ if (PEAR::isError($res)) {
13216+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13217+ return -1;
13218+ } else {
13219+ $new = array(
13220+ 'category' => $current['iss_prc_id'],
13221+ 'release' => $current['iss_pre_id'],
13222+ 'expected_resolution_date' => $current['iss_expected_resolution_date'],
13223+ 'release' => $current['iss_pre_id'],
13224+ 'priority' => $current['iss_pri_id'],
13225+ 'status' => $current['iss_sta_id'],
13226+ 'resolution' => $current['iss_res_id'],
13227+ 'summary' => $current['iss_summary'],
13228+ 'description' => $current['iss_description'],
13229+ 'estimated_dev_time' => $current['iss_dev_time'],
13230+ 'percent_complete' => $current['iss_percent_complete'],
13231+ 'trigger_reminders' => $current['iss_trigger_reminders'],
13232+ 'group' => $current['iss_grp_id'],
13233+ 'iss_private' => $current['private']
13234+ );
13235+ $new[$field_name] = $filed_value;
13236+
13237+ // add change to the history (only for changes on specific fields?)
13238+ $updated_fields = array();
13239+ if ($field_name == 'expected_resolution_date' && $current["iss_expected_resolution_date"] != $filed_value) {
13240+ $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $filed_value);
13241+ }
13242+ if ($field_name == 'category' && $current["iss_prc_id"] != $filed_value) {
13243+ $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($filed_value));
13244+ }
13245+ if ($field_name == 'release' && $current["iss_pre_id"] != $filed_value) {
13246+ $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($filed_value));
13247+ }
13248+ if ($field_name == 'priority' && $current["iss_pri_id"] != $filed_value) {
13249+ $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($filed_value));
13250+ Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $new);
13251+ }
13252+ if ($field_name == 'status' && $current["iss_sta_id"] != $filed_value) {
13253+ // clear out the last-triggered-reminder flag when changing the status of an issue
13254+ Reminder_Action::clearLastTriggered($issue_id);
13255+
13256+ // if old status was closed and new status is not, clear closed data from issue.
13257+ $old_status_details = Status::getDetails($current['iss_sta_id']);
13258+ if ($old_status_details['sta_is_closed'] == 1) {
13259+ $new_status_details = Status::getDetails($filed_value);
13260+ if ($new_status_details['sta_is_closed'] != 1) {
13261+ self::clearClosed($issue_id);
13262+ }
13263+ }
13264+ $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($filed_value));
13265+ }
13266+ if ($field_name == 'resolution' && $current["iss_res_id"] != $filed_value) {
13267+ $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($filed_value));
13268+ }
13269+ if ($field_name == 'estimated_dev_time' && $current["iss_dev_time"] != $filed_value) {
13270+ $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime(($current["iss_dev_time"]*60)), Misc::getFormattedTime(($filed_value*60)));
13271+ }
13272+ if ($field_name == 'summary' && $current["iss_summary"] != $filed_value) {
13273+ $updated_fields["Summary"] = '';
13274+ }
13275+ if ($field_name == 'description' && $current["iss_description"] != $filed_value) {
13276+ $updated_fields["Description"] = '';
13277+ }
13278+ if ($field_name == 'private' && ($filed_value != $current['iss_private'])) {
13279+ $updated_fields["Private"] = History::formatChanges(Misc::getBooleanDisplayValue($current['iss_private']), Misc::getBooleanDisplayValue($filed_value));
13280+ }
13281+ if (count($updated_fields) > 0) {
13282+ // log the changes
13283+ $changes = '';
13284+ $i = 0;
13285+ foreach ($updated_fields as $key => $value) {
13286+ if ($i > 0) {
13287+ $changes .= "; ";
13288+ }
13289+ if (($key != "Summary") && ($key != "Description")) {
13290+ $changes .= "$key: $value";
13291+ } else {
13292+ $changes .= "$key";
13293+ }
13294+ $i++;
13295+ }
13296+
13297+ History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ($changes) by " . User::getFullName($usr_id));
13298+ // send notifications for the issue being updated
13299+ Notification::notifyIssueUpdated($issue_id, $current, $new);
13300+ }
13301+ }
13302+ return 1;
13303+ }
13304+
13305+
13306+ /**
13307+ * Move the issue to a new project
13308+ *
13309+ * @param integer $issue_id
13310+ * @param integer $new_prj_id
13311+ * @return integer 1 on success, -1 otherwise
13312+ */
13313+ function moveIssue($issue_id, $new_prj_id)
13314+ {
13315+ $stmt = "UPDATE
13316+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
13317+ SET
13318+ iss_prj_id = " . Misc::escapeInteger($new_prj_id) . "
13319+ WHERE
13320+ iss_id = " . Misc::escapeInteger($issue_id);
13321+ $res = DB_Helper::getInstance()->query($stmt);
13322+ if (PEAR::isError($res)) {
13323+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13324+ return -1;
13325+ } else {
13326+ $currentDetails = self::getDetails($issue_id);
13327+
13328+ // set new category
13329+ $new_iss_prc_list = Category::getAssocList($new_prj_id);
13330+ $iss_prc_title = Category::getTitle($currentDetails['iss_prc_id']);
13331+ $new_prc_id = array_search($iss_prc_title, $new_iss_prc_list);
13332+ if ($new_prc_id === false) {
13333+ // use the first category listed in the new project
13334+ $new_prc_id = key($new_iss_prc_list);
13335+ }
13336+
13337+ // set new priority
13338+ $new_iss_pri_list = Priority::getAssocList($new_prj_id);
13339+ $iss_pri_title = Priority::getTitle($currentDetails['iss_pri_id']);
13340+ $new_pri_id = array_search($iss_pri_title, $new_iss_pri_list);
13341+ if ($new_pri_id === false) {
13342+ // use the first category listed in the new project
13343+ $new_pri_id = key($new_iss_pri_list);
13344+ }
13345+
13346+ // XXX: Set status if needed when moving issue
13347+
13348+ $stmt = "UPDATE
13349+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
13350+ SET
13351+ iss_prc_id=" . Misc::escapeInteger($new_prc_id) . ",
13352+ iss_pri_id=" . $new_pri_id . "
13353+ WHERE
13354+ iss_id=$issue_id";
13355+ $res = DB_Helper::getInstance()->query($stmt);
13356+ if (PEAR::isError($res)) {
13357+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13358+ }
13359+
13360+ // clear project cache
13361+ self::getProjectID($issue_id, true);
13362+
13363+ Notification::notifyNewIssue($new_prj_id, $issue_id);
13364+ }
13365+ }
13366+
13367+
13368+ /**
13369+ * Method used to associate an existing issue with another one.
13370+ *
13371+ * @access public
13372+ * @param integer $issue_id The issue ID
13373+ * @param integer $issue_id The other issue ID
13374+ * @return void
13375+ */
13376+ function addAssociation($issue_id, $associated_id, $usr_id, $link_issues = TRUE)
13377+ {
13378+ $issue_id = Misc::escapeInteger($issue_id);
13379+ $associated_id = Misc::escapeInteger($associated_id);
13380+
13381+ $stmt = "INSERT INTO
13382+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
13383+ (
13384+ isa_issue_id,
13385+ isa_associated_id
13386+ ) VALUES (
13387+ $issue_id,
13388+ $associated_id
13389+ )";
13390+ DB_Helper::getInstance()->query($stmt);
13391+ History::add($issue_id, $usr_id, History::getTypeID('issue_associated'), "Issue associated to #$associated_id by " . User::getFullName($usr_id));
13392+ // link the associated issue back to this one
13393+ if ($link_issues) {
13394+ self::addAssociation($associated_id, $issue_id, $usr_id, FALSE);
13395+ }
13396+ }
13397+
13398+
13399+ /**
13400+ * Method used to remove the issue associations related to a specific issue.
13401+ *
13402+ * @access public
13403+ * @param integer $issue_id The issue ID
13404+ * @return void
13405+ */
13406+ function deleteAssociations($issue_id, $usr_id = FALSE)
13407+ {
13408+ $issue_id = Misc::escapeInteger($issue_id);
13409+ if (is_array($issue_id)) {
13410+ $issue_id = implode(", ", $issue_id);
13411+ }
13412+ $stmt = "DELETE FROM
13413+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
13414+ WHERE
13415+ isa_issue_id IN ($issue_id) OR
13416+ isa_associated_id IN ($issue_id)";
13417+ DB_Helper::getInstance()->query($stmt);
13418+ if ($usr_id) {
13419+ History::add($issue_id, $usr_id, History::getTypeID('issue_all_unassociated'), 'Issue associations removed by ' . User::getFullName($usr_id));
13420+ }
13421+ }
13422+
13423+
13424+ /**
13425+ * Method used to remove a issue association from an issue.
13426+ *
13427+ * @access public
13428+ * @param integer $issue_id The issue ID
13429+ * @param integer $associated_id The associated issue ID to remove.
13430+ * @return void
13431+ */
13432+ function deleteAssociation($issue_id, $associated_id)
13433+ {
13434+ $issue_id = Misc::escapeInteger($issue_id);
13435+ $associated_id = Misc::escapeInteger($associated_id);
13436+ $stmt = "DELETE FROM
13437+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association
13438+ WHERE
13439+ (
13440+ isa_issue_id = $issue_id AND
13441+ isa_associated_id = $associated_id
13442+ ) OR
13443+ (
13444+ isa_issue_id = $associated_id AND
13445+ isa_associated_id = $issue_id
13446+ )";
13447+ DB_Helper::getInstance()->query($stmt);
13448+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
13449+ "Issue association #$associated_id removed by " . User::getFullName(Auth::getUserID()));
13450+ History::add($associated_id, Auth::getUserID(), History::getTypeID('issue_unassociated'),
13451+ "Issue association #$issue_id removed by " . User::getFullName(Auth::getUserID()));
13452+ }
13453+
13454+
13455+ /**
13456+ * Method used to assign an issue with an user.
13457+ *
13458+ * @access public
13459+ * @param integer $usr_id The user ID of the person performing this change
13460+ * @param integer $issue_id The issue ID
13461+ * @param integer $assignee_usr_id The user ID of the assignee
13462+ * @param boolean $add_history Whether to add a history entry about this or not
13463+ * @return integer 1 if the update worked, -1 otherwise
13464+ */
13465+ function addUserAssociation($usr_id, $issue_id, $assignee_usr_id, $add_history = TRUE)
13466+ {
13467+ $issue_id = Misc::escapeInteger($issue_id);
13468+ $assignee_usr_id = Misc::escapeInteger($assignee_usr_id);
13469+ $order = 1;
13470+ // move all orders down to free "order space" for this new association
13471+ $stmt = "UPDATE
13472+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13473+ SET
13474+ isu_order = isu_order + 1
13475+ WHERE
13476+ isu_usr_id = $assignee_usr_id AND
13477+ isu_order >= $order";
13478+ $res = DB_Helper::getInstance()->query($stmt);
13479+ if (PEAR::isError($res)) {
13480+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13481+ return -1;
13482+ }
13483+ // insert the new association
13484+ $stmt = "INSERT INTO
13485+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13486+ (
13487+ isu_iss_id,
13488+ isu_usr_id,
13489+ isu_assigned_date,
13490+ isu_order
13491+ ) VALUES (
13492+ $issue_id,
13493+ $assignee_usr_id,
13494+ '" . Date_Helper::getCurrentDateGMT() . "',
13495+ $order
13496+ )";
13497+ $res = DB_Helper::getInstance()->query($stmt);
13498+ if (PEAR::isError($res)) {
13499+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13500+ return -1;
13501+ } else {
13502+ if ($add_history) {
13503+ History::add($issue_id, $usr_id, History::getTypeID('user_associated'),
13504+ 'Issue assigned to ' . User::getFullName($assignee_usr_id) . ' by ' . User::getFullName($usr_id));
13505+ }
13506+ return 1;
13507+ }
13508+ }
13509+
13510+ /**
13511+ * Method used to get the order list to be rearranged
13512+ *
13513+ * @access private
13514+ * @param string $issue_id The issue ID or a comma seperated list of IDs already prepared for giving to mysql
13515+ * @param string $usr_id The user to remove. When not specified, all users are taken as to be removed for that issue
13516+ * @return mixed delete order list to be rearranged. Used as a parameter to the method of rearranging the order.
13517+ */
13518+ function getDeleteUserAssociationOrderList($issue_id, $usr_id = "")
13519+ {
13520+ // find all affected associantion orders
13521+ $stmt = "SELECT isu_usr_id, isu_order FROM
13522+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13523+ WHERE
13524+ isu_iss_id IN ($issue_id)";
13525+ if ($usr_id !== FALSE) {
13526+ $stmt.= " AND isu_usr_id IN ($usr_id)";
13527+ }
13528+ $stmt.= "ORDER BY isu_order";
13529+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
13530+ if (PEAR::isError($res)) {
13531+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13532+ return -1;
13533+ } else {
13534+ $deleted_orders = array();
13535+ foreach ($res as $row) {
13536+ if (empty($deleted_orders[$row['isu_usr_id']])) {
13537+ $deleted_orders[$row['isu_usr_id']] = array();
13538+ }
13539+ $deleted_orders[$row['isu_usr_id']] [] = $row['isu_order'];
13540+ }
13541+ return $deleted_orders;
13542+ }
13543+ }
13544+
13545+ /**
13546+ *
13547+ * Method used to rearrange order list in the db according to known deleted records
13548+ *
13549+ * @access private
13550+ * @param mixed deleteorder list
13551+ * @return void
13552+ */
13553+ function rearrangeDeleteUserAssociationOrderList($delete_order_list)
13554+ {
13555+ if (empty($delete_order_list) || (!is_array($delete_order_list))) {
13556+ return -1;
13557+ }
13558+ foreach ($delete_order_list as $isu_usr_id => $orders) {
13559+ for ($i = 0; $i < count($orders); $i++) { // traverse all deleted orders
13560+ // move the orders after them up to take the "order space" of the deleted records
13561+ $stmt = "UPDATE
13562+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13563+ SET
13564+ isu_order = isu_order - " . ($i+1) . "
13565+ WHERE
13566+ isu_usr_id = $isu_usr_id AND
13567+ isu_order > " . $orders[$i];
13568+ if ($i < count($orders) - 1) {
13569+ $stmt.= " AND
13570+ isu_order < " . $orders[$i+1];
13571+ }
13572+ $res = DB_Helper::getInstance()->query($stmt);
13573+ if (PEAR::isError($res)) {
13574+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13575+ return -1;
13576+ }
13577+ }
13578+ }
13579+ return 1;
13580+ }
13581+
13582+
13583+ /**
13584+ * Method used to delete all user assignments for a specific issue.
13585+ *
13586+ * @access public
13587+ * @param integer $issue_id The issue ID
13588+ * @param integer $usr_id The user ID of the person performing the change
13589+ * @return void
13590+ */
13591+ function deleteUserAssociations($issue_id, $usr_id = FALSE)
13592+ {
13593+ $issue_id = Misc::escapeInteger($issue_id);
13594+ if (is_array($issue_id)) {
13595+ $issue_id = implode(", ", $issue_id);
13596+ }
13597+ $deleted_order_list = self::getDeleteUserAssociationOrderList($issue_id);
13598+ $stmt = "DELETE FROM
13599+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13600+ WHERE
13601+ isu_iss_id IN ($issue_id)";
13602+ $res = DB_Helper::getInstance()->query($stmt);
13603+ if (PEAR::isError($res)) {
13604+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13605+ return -1;
13606+ } else {
13607+ if ($usr_id) {
13608+ History::add($issue_id, $usr_id, History::getTypeID('user_all_unassociated'), 'Issue assignments removed by ' . User::getFullName($usr_id));
13609+ }
13610+ self::rearrangeDeleteUserAsssociationOrderList($deleted_order_list);
13611+ return 1;
13612+ }
13613+ }
13614+
13615+
13616+ /**
13617+ * Method used to delete a single user assignments for a specific issue.
13618+ *
13619+ * @access public
13620+ * @param integer $issue_id The issue ID
13621+ * @param integer $usr_id The user to remove.
13622+ * @param boolean $add_history Whether to add a history entry about this or not
13623+ * @return void
13624+ */
13625+ function deleteUserAssociation($issue_id, $usr_id, $add_history = true)
13626+ {
13627+ $issue_id = Misc::escapeInteger($issue_id);
13628+ $usr_id = Misc::escapeInteger($usr_id);
13629+ $delete_order_list = self::getDeleteUserAssociationOrderList($issue_id, $usr_id);
13630+ $stmt = "DELETE FROM
13631+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
13632+ WHERE
13633+ isu_iss_id = $issue_id AND
13634+ isu_usr_id = $usr_id";
13635+ $res = DB_Helper::getInstance()->query($stmt);
13636+ if (PEAR::isError($res)) {
13637+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
13638+ return -1;
13639+ } else {
13640+ if ($add_history) {
13641+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_unassociated'),
13642+ User::getFullName($usr_id) . ' removed from issue by ' . User::getFullName(Auth::getUserID()));
13643+ }
13644+ self::rearrangeDeleteUserAssociationOrderList($delete_order_list);
13645+ return 1;
13646+ }
13647+ }
13648+
13649+
13650+ /**
13651+ * Creates an issue with the given email information.
13652+ *
13653+ * @access public
13654+ * @param integer $prj_id The project ID
13655+ * @param integer $usr_id The user responsible for this action
13656+ * @param string $sender The original sender of this email
13657+ * @param string $summary The issue summary
13658+ * @param string $description The issue description
13659+ * @param integer $category The category ID
13660+ * @param integer $priority The priority ID
13661+ * @param array $assignment The list of users to assign this issue to
13662+ * @param string $date The date the email was originally sent.
13663+ * @param string $msg_id The message ID of the email we are creating this issue from.
13664+ * @return void
13665+ */
13666+ function createFromEmail($prj_id, $usr_id, $sender, $summary, $description, $category, $priority, $assignment, $date, $msg_id)
13667+ {
13668+ $data = array();
13669+ $exclude_list = array();
13670+
13671+ $sender_email = Mail_Helper::getEmailAddress($sender);
13672+ $sender_usr_id = User::getUserIDByEmail($sender_email, true);
13673+ if (!empty($sender_usr_id)) {
13674+ $reporter = $sender_usr_id;
13675+ $exclude_list[] = $sender_usr_id;
13676+ }
13677+
13678+ $data = array(
13679+ 'category' => $category,
13680+ 'priority' => $priority,
13681+ 'description' => $description,
13682+ 'summary' => $summary,
13683+ 'msg_id' => $msg_id,
13684+ );
13685+
13686+ if (Customer::hasCustomerIntegration($prj_id)) {
13687+ list($customer_id, $customer_contact_id) = Customer::getCustomerIDByEmails($prj_id, array($sender_email));
13688+ if (!empty($customer_id)) {
13689+ $contact = Customer::getContactDetails($prj_id, $customer_contact_id);
13690+ // overwrite the reporter with the customer contact
13691+ $reporter = User::getUserIDByContactID($customer_contact_id);
13692+ $contact_timezone = Date_Helper::getPreferredTimezone($reporter);
13693+
13694+ $data['customer'] = $customer_id;
13695+ $data['contact'] = $customer_contact_id;
13696+# $data['contract'] = // XXX missing
13697+ $data['contact_person_lname'] = $contact['last_name'];
13698+ $data['contact_person_fname'] = $contact['first_name'];
13699+ $data['contact_email'] = $sender_email;
13700+ $data['contact_phone'] = $contact['phone'];
13701+ $data['contact_timezone'] = $contact_timezone;
13702+ }
13703+ } else {
13704+ $customer_id = FALSE;
13705+ }
13706+ if (empty($reporter)) {
13707+ $reporter = APP_SYSTEM_USER_ID;
13708+ }
13709+
13710+ $data['reporter'] = $reporter;
13711+
13712+ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
13713+ if ($issue_id == -1) {
13714+ return -1;
13715+ }
13716+
13717+ $has_TAM = false;
13718+ $has_RR = false;
13719+ // log the creation of the issue
13720+ History::add($issue_id, $usr_id, History::getTypeID('issue_opened'), 'Issue opened by ' . $sender);
13721+
13722+ $emails = array();
13723+ $manager_usr_ids = array();
13724+ if ((Customer::hasCustomerIntegration($prj_id)) && (!empty($customer_id))) {
13725+ // if there are any technical account managers associated with this customer, add these users to the notification list
13726+ $managers = Customer::getAccountManagers($prj_id, $customer_id);
13727+ $manager_usr_ids = array_keys($managers);
13728+ $manager_emails = array_values($managers);
13729+ $emails = array_merge($emails, $manager_emails);
13730+ }
13731+ // add the reporter to the notification list
13732+ $emails[] = $sender;
13733+ $emails = array_unique($emails);
13734+ $actions = Notification::getDefaultActions($issue_id, false, 'issue_from_email');
13735+ foreach ($emails as $address) {
13736+ Notification::subscribeEmail($reporter, $issue_id, $address, $actions);
13737+ }
13738+
13739+ // only assign the issue to an user if the associated customer has any technical account managers
13740+ $users = array();
13741+ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
13742+ foreach ($manager_usr_ids as $manager_usr_id) {
13743+ $users[] = $manager_usr_id;
13744+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $manager_usr_id, false);
13745+ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
13746+ }
13747+ $has_TAM = true;
13748+ }
13749+ // now add the user/issue association
13750+ if (@count($assignment) > 0) {
13751+ for ($i = 0; $i < count($assignment); $i++) {
13752+ Notification::subscribeUser($reporter, $issue_id, $assignment[$i], $actions);
13753+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignment[$i]);
13754+ if ($assignment[$i] != $usr_id) {
13755+ $users[] = $assignment[$i];
13756+ }
13757+ }
13758+ } else {
13759+ // only use the round-robin feature if this new issue was not
13760+ // already assigned to a customer account manager
13761+ if (@count($manager_usr_ids) < 1) {
13762+ $assignee = Round_Robin::getNextAssignee($prj_id);
13763+ // assign the issue to the round robin person
13764+ if (!empty($assignee)) {
13765+ self::addUserAssociation(APP_SYSTEM_USER_ID, $issue_id, $assignee, false);
13766+ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
13767+ $users[] = $assignee;
13768+ $has_RR = true;
13769+ }
13770+ }
13771+ }
13772+ if (count($users) > 0) {
13773+ $has_assignee = true;
13774+ }
13775+
13776+ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
13777+
13778+ // send special 'an issue was auto-created for you' notification back to the sender
13779+ Notification::notifyAutoCreatedIssue($prj_id, $issue_id, $sender, $date, $summary);
13780+
13781+ // also notify any users that want to receive emails anytime a new issue is created
13782+ Notification::notifyNewIssue($prj_id, $issue_id, $exclude_list);
13783+
13784+ return $issue_id;
13785+ }
13786+
13787+
13788+ /**
13789+ * Return errors that happened when creating new issue from POST method.
13790+ *
13791+ * @return array
13792+ */
13793+ private static $insert_errors = array();
13794+ static function getInsertErrors() {
13795+ return self::$insert_errors;
13796+ }
13797+
13798+ /**
13799+ * Method used to add a new issue using the normal report form.
13800+ *
13801+ * @access public
13802+ * @return integer The new issue ID
13803+ */
13804+ function createFromPost()
13805+ {
13806+ $keys = array(
13807+ 'add_primary_contact', 'attached_emails', 'category', 'contact', 'contact_email', 'contact_extra_emails', 'contact_person_fname',
13808+ 'contact_person_lname', 'contact_phone', 'contact_timezone', 'contract', 'customer', 'custom_fields', 'description',
13809+ 'estimated_dev_time', 'group', 'notify_customer', 'notify_senders', 'priority', 'private', 'release', 'summary', 'users',
13810+ );
13811+ $data = array();
13812+ foreach ($keys as $key) {
13813+ if (isset($_POST[$key])) {
13814+ $data[$key] = $_POST[$key];
13815+ }
13816+ }
13817+
13818+ $prj_id = Auth::getCurrentProject();
13819+ $usr_id = Auth::getUserID();
13820+
13821+ // if we are creating an issue for a customer, put the
13822+ // main customer contact as the reporter for it
13823+ if (Customer::hasCustomerIntegration($prj_id)) {
13824+ $contact_usr_id = User::getUserIDByContactID($data['contact']);
13825+ if (empty($contact_usr_id)) {
13826+ $contact_usr_id = $usr_id;
13827+ }
13828+ $data['reporter'] = $contact_usr_id;
13829+ } else {
13830+ $data['reporter'] = $usr_id;
13831+ }
13832+
13833+ $data['msg_id'] = Mail_Helper::generateMessageID();
13834+
13835+ $issue_id = self::insertIssue($prj_id, $usr_id, $data);
13836+ if ($issue_id == -1) {
13837+ return -1;
13838+ }
13839+
13840+ $has_TAM = false;
13841+ $has_RR = false;
13842+ $info = User::getNameEmail($usr_id);
13843+ // log the creation of the issue
13844+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_opened'), 'Issue opened by ' . User::getFullName(Auth::getUserID()));
13845+
13846+ $emails = array();
13847+ if (Customer::hasCustomerIntegration($prj_id)) {
13848+ if (!empty($data['contact_extra_emails']) && count($data['contact_extra_emails']) > 0) {
13849+ $emails = $data['contact_extra_emails'];
13850+ }
13851+ // add the primary contact to the notification list
13852+ if ($data['add_primary_contact'] == 'yes') {
13853+ $contact_email = User::getEmailByContactID($data['contact']);
13854+ if (!empty($contact_email)) {
13855+ $emails[] = $contact_email;
13856+ }
13857+ }
13858+ // if there are any technical account managers associated with this customer, add these users to the notification list
13859+ $managers = Customer::getAccountManagers($prj_id, $data['customer']);
13860+ $manager_usr_ids = array_keys($managers);
13861+ $manager_emails = array_values($managers);
13862+ $emails = array_merge($emails, $manager_emails);
13863+ }
13864+ // add the reporter to the notification list
13865+ $emails[] = $info['usr_email'];
13866+ $emails = array_unique($emails);
13867+ foreach ($emails as $address) {
13868+ Notification::subscribeEmail($usr_id, $issue_id, $address, Notification::getDefaultActions($issue_id, $address, 'new_issue'));
13869+ }
13870+
13871+ // only assign the issue to an user if the associated customer has any technical account managers
13872+ $users = array();
13873+ $has_TAM = false;
13874+ if ((Customer::hasCustomerIntegration($prj_id)) && (count($manager_usr_ids) > 0)) {
13875+ foreach ($manager_usr_ids as $manager_usr_id) {
13876+ $users[] = $manager_usr_id;
13877+ self::addUserAssociation($usr_id, $issue_id, $manager_usr_id, false);
13878+ History::add($issue_id, $usr_id, History::getTypeID('issue_auto_assigned'), 'Issue auto-assigned to ' . User::getFullName($manager_usr_id) . ' (TAM)');
13879+ }
13880+ $has_TAM = true;
13881+ }
13882+ // now add the user/issue association (aka assignments)
13883+ if (!empty($data['users']) && count($data['users']) > 0) {
13884+ for ($i = 0; $i < count($data['users']); $i++) {
13885+ Notification::subscribeUser($usr_id, $issue_id, $data['users'][$i],
13886+ Notification::getDefaultActions($issue_id, User::getEmail($data['users'][$i]), 'new_issue'));
13887+ self::addUserAssociation($usr_id, $issue_id, $data['users'][$i]);
13888+ if ($data['users'][$i] != $usr_id) {
13889+ $users[] = $data['users'][$i];
13890+ }
13891+ }
13892+ } else {
13893+ // only use the round-robin feature if this new issue was not
13894+ // already assigned to a customer account manager
13895+ if (@count($manager_usr_ids) < 1) {
13896+ $assignee = Round_Robin::getNextAssignee($prj_id);
13897+ // assign the issue to the round robin person
13898+ if (!empty($assignee)) {
13899+ $users[] = $assignee;
13900+ self::addUserAssociation($usr_id, $issue_id, $assignee, false);
13901+ History::add($issue_id, APP_SYSTEM_USER_ID, History::getTypeID('rr_issue_assigned'), 'Issue auto-assigned to ' . User::getFullName($assignee) . ' (RR)');
13902+ $has_RR = true;
13903+ }
13904+ }
13905+ }
13906+
13907+ // now process any files being uploaded
13908+ $found = 0;
13909+ for ($i = 0; $i < count(@$_FILES["file"]["name"]); $i++) {
13910+ if (!@empty($_FILES["file"]["name"][$i])) {
13911+ $found = 1;
13912+ break;
13913+ }
13914+ }
13915+ if ($found) {
13916+ $files = array();
13917+ for ($i = 0; $i < count($_FILES["file"]["name"]); $i++) {
13918+ $filename = @$_FILES["file"]["name"][$i];
13919+ if (empty($filename)) {
13920+ continue;
13921+ }
13922+ $blob = file_get_contents($_FILES["file"]["tmp_name"][$i]);
13923+ if (empty($blob)) {
13924+ // error reading a file
13925+ self::$insert_errors["file[$i]"] = "There was an error uploading the file '$filename'.";
13926+ continue;
13927+ }
13928+ $files[] = array(
13929+ "filename" => $filename,
13930+ "type" => $_FILES['file']['type'][$i],
13931+ "blob" => $blob
13932+ );
13933+ }
13934+ if (count($files) > 0) {
13935+ $attachment_id = Attachment::add($issue_id, $usr_id, 'Files uploaded at issue creation time');
13936+ foreach ($files as $file) {
13937+ Attachment::addFile($attachment_id, $file["filename"], $file["type"], $file["blob"]);
13938+ }
13939+ }
13940+ }
13941+ // need to associate any emails ?
13942+ if (!empty($data['attached_emails'])) {
13943+ $items = explode(",", $data['attached_emails']);
13944+ Support::associate($usr_id, $issue_id, $items);
13945+ }
13946+ // need to notify any emails being converted into issues ?
13947+ if (@count($data['notify_senders']) > 0) {
13948+ $recipients = Notification::notifyEmailConvertedIntoIssue($prj_id, $issue_id, $data['notify_senders'], @$data['customer']);
13949+ } else {
13950+ $recipients = array();
13951+ }
13952+ // need to process any custom fields ?
13953+ if (@count($data['custom_fields']) > 0) {
13954+ foreach ($data['custom_fields'] as $fld_id => $value) {
13955+ Custom_Field::associateIssue($issue_id, $fld_id, $value);
13956+ }
13957+ }
13958+ // also send a special confirmation email to the customer contact
13959+ if ((@$data['notify_customer'] == 'yes') && (!empty($data['contact']))) {
13960+ // also need to pass the list of sender emails already notified,
13961+ // so we can avoid notifying the same person again
13962+ $contact_email = User::getEmailByContactID($data['contact']);
13963+ if (@!in_array($contact_email, $recipients)) {
13964+ Customer::notifyCustomerIssue($prj_id, $issue_id, $data['contact']);
13965+ }
13966+ }
13967+
13968+ Workflow::handleNewIssue($prj_id, $issue_id, $has_TAM, $has_RR);
13969+
13970+ // also notify any users that want to receive emails anytime a new issue is created
13971+ Notification::notifyNewIssue($prj_id, $issue_id);
13972+
13973+ return $issue_id;
13974+ }
13975+
13976+ /**
13977+ * Insert issue to database.
13978+ *
13979+ * @param integer $prj_id The project ID
13980+ * @param integer $usr_id The user responsible for this action
13981+ * @param array $data of issue to be inserted
13982+ * @return integer The new issue ID
13983+ */
13984+ private function insertIssue($prj_id, $usr_id, $data)
13985+ {
13986+
13987+ // XXX missing_fields never used
13988+ $missing_fields = array();
13989+ if ($data['category'] == -1) {
13990+ $missing_fields[] = 'Category';
13991+ }
13992+ if ($data['priority'] == -1) {
13993+ $missing_fields[] = 'Priority';
13994+ }
13995+
13996+ // if there is no reporter set, use the system user
13997+ if (empty($data['reporter'])) {
13998+ $data['reporter'] = APP_SYSTEM_USER_ID;
13999+ }
14000+
14001+ if ((!isset($data['estimated_dev_time'])) || ($data['estimated_dev_time'] == '')) {
14002+ $data['estimated_dev_time'] = 0;
14003+ }
14004+
14005+ // add new issue
14006+ $stmt = "INSERT INTO " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue ".
14007+ "SET ".
14008+ "iss_prj_id=" . $prj_id . ",";
14009+ if (!empty($data['group'])) {
14010+ $stmt .= "iss_grp_id=" . Misc::escapeInteger($data['group']) . ",\n";
14011+ }
14012+ if (!empty($data['category'])) {
14013+ $stmt .= "iss_prc_id=". Misc::escapeInteger($data['category']) . ",\n";
14014+ }
14015+ if (!empty($data['release'])) {
14016+ $stmt .= "iss_pre_id=". Misc::escapeInteger($data['release']) . ",\n";
14017+ }
14018+ if (!empty($data['priority'])) {
14019+ $stmt .= "iss_pri_id=". Misc::escapeInteger($data['priority']) . ",";
14020+ }
14021+
14022+ $stmt .= "iss_usr_id=". Misc::escapeInteger($data['reporter']) .",";
14023+
14024+ $initial_status = Project::getInitialStatus($prj_id);
14025+ if (!empty($initial_status)) {
14026+ $stmt .= "iss_sta_id=" . Misc::escapeInteger($initial_status) . ",";
14027+ }
14028+
14029+ if (Customer::hasCustomerIntegration($prj_id)) {
14030+ $stmt .= "
14031+ iss_customer_id=". Misc::escapeInteger($data['customer']) . ",";
14032+ if (!empty($data['contact'])) {
14033+ $stmt .= "
14034+ iss_customer_contract_id='". Misc::escapeString($data['contract']) . "',";
14035+ }
14036+ $stmt .= "
14037+ iss_customer_contact_id=". Misc::escapeInteger($data['contact']) . ",
14038+ iss_contact_person_lname='". Misc::escapeString($data['contact_person_lname']) . "',
14039+ iss_contact_person_fname='". Misc::escapeString($data['contact_person_fname']) . "',
14040+ iss_contact_email='". Misc::escapeString($data['contact_email']) . "',
14041+ iss_contact_phone='". Misc::escapeString($data['contact_phone']) . "',
14042+ iss_contact_timezone='". Misc::escapeString($data['contact_timezone']) . "',";
14043+ }
14044+
14045+ $stmt .= "
14046+ iss_created_date='". Date_Helper::getCurrentDateGMT() . "',
14047+ iss_last_public_action_date='" . Date_Helper::getCurrentDateGMT() . "',
14048+ iss_last_public_action_type='created',
14049+ iss_summary='" . Misc::escapeString($data['summary']) . "',
14050+ iss_description='" . Misc::escapeString($data['description']) . "',
14051+ iss_dev_time='" . Misc::escapeString($data['estimated_dev_time']) . "',";
14052+ if (!empty($data['contact'])) {
14053+ $stmt .= "
14054+ iss_private=" . Misc::escapeInteger($data['private']) . " ,";
14055+ }
14056+ $stmt .= "
14057+ iss_root_message_id='". Misc::escapeString($data['msg_id']) ."'
14058+ ";
14059+
14060+ $res = DB_Helper::getInstance()->query($stmt);
14061+ if (PEAR::isError($res)) {
14062+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14063+ return -1;
14064+ }
14065+
14066+ $issue_id = DB_Helper::get_last_insert_id();
14067+ return $issue_id;
14068+ }
14069+
14070+
14071+ /**
14072+ * Method used to get a specific parameter in the issue listing cookie.
14073+ *
14074+ * @access public
14075+ * @param string $name The name of the parameter
14076+ * @return mixed The value of the specified parameter
14077+ */
14078+ function getParam($name)
14079+ {
14080+ $profile = Search_Profile::getProfile(Auth::getUserID(), Auth::getCurrentProject(), 'issue');
14081+
14082+ if (isset($_GET[$name])) {
14083+ return $_GET[$name];
14084+ } elseif (isset($_POST[$name])) {
14085+ return $_POST[$name];
14086+ } elseif (isset($profile[$name])) {
14087+ return $profile[$name];
14088+ } else {
14089+ return "";
14090+ }
14091+ }
14092+
14093+
14094+ /**
14095+ * Method used to save the current search parameters in a cookie.
14096+ *
14097+ * @access public
14098+ * @return array The search parameters
14099+ */
14100+ function saveSearchParams()
14101+ {
14102+ $sort_by = self::getParam('sort_by');
14103+ $sort_order = self::getParam('sort_order');
14104+ $users = self::getParam('users');
14105+ if (empty($users) && ($sort_by == 'isu_order')) { // Sorting by isu_order is impossible when no user specified
14106+ unset($sort_by);
14107+ unset($sort_order);
14108+ }
14109+ $rows = self::getParam('rows');
14110+ $hide_closed = self::getParam('hide_closed');
14111+ if ($hide_closed === '') {
14112+ $hide_closed = 1;
14113+ }
14114+ $search_type = self::getParam('search_type');
14115+ if (empty($search_type)) {
14116+ $search_type = 'all_text';
14117+ }
14118+ $custom_field = self::getParam('custom_field');
14119+ if (is_string($custom_field)) {
14120+ $custom_field = unserialize(urldecode($custom_field));
14121+ }
14122+ $cookie = array(
14123+ 'rows' => $rows ? $rows : APP_DEFAULT_PAGER_SIZE,
14124+ 'pagerRow' => self::getParam('pagerRow'),
14125+ 'hide_closed' => $hide_closed,
14126+ "sort_by" => $sort_by ? $sort_by : "pri_rank",
14127+ "sort_order" => $sort_order ? $sort_order : "ASC",
14128+ // quick filter form
14129+ 'keywords' => self::getParam('keywords'),
14130+ 'search_type' => $search_type,
14131+ 'users' => self::getParam('users'),
14132+ 'status' => self::getParam('status'),
14133+ 'priority' => self::getParam('priority'),
14134+ 'category' => self::getParam('category'),
14135+ 'customer_email' => self::getParam('customer_email'),
14136+ // advanced search form
14137+ 'show_authorized_issues' => self::getParam('show_authorized_issues'),
14138+ 'show_notification_list_issues' => self::getParam('show_notification_list_issues'),
14139+ 'reporter' => self::getParam('reporter'),
14140+ // other fields
14141+ 'release' => self::getParam('release'),
14142+ // custom fields
14143+ 'custom_field' => $custom_field
14144+ );
14145+ // now do some magic to properly format the date fields
14146+ $date_fields = array(
14147+ 'created_date',
14148+ 'updated_date',
14149+ 'last_response_date',
14150+ 'first_response_date',
14151+ 'closed_date'
14152+ );
14153+ foreach ($date_fields as $field_name) {
14154+ $field = self::getParam($field_name);
14155+ if (empty($field)) {
14156+ continue;
14157+ }
14158+ if (@$field['filter_type'] == 'in_past') {
14159+ @$cookie[$field_name] = array(
14160+ 'filter_type' => 'in_past',
14161+ 'time_period' => $field['time_period']
14162+ );
14163+ } else {
14164+ $end_field_name = $field_name . '_end';
14165+ $end_field = self::getParam($end_field_name);
14166+ @$cookie[$field_name] = array(
14167+ 'past_hour' => $field['past_hour'],
14168+ 'Year' => $field['Year'],
14169+ 'Month' => $field['Month'],
14170+ 'Day' => $field['Day'],
14171+ 'start' => $field['Year'] . '-' . $field['Month'] . '-' . $field['Day'],
14172+ 'filter_type' => $field['filter_type'],
14173+ 'end' => $end_field['Year'] . '-' . $end_field['Month'] . '-' . $end_field['Day']
14174+ );
14175+ @$cookie[$end_field_name] = array(
14176+ 'Year' => $end_field['Year'],
14177+ 'Month' => $end_field['Month'],
14178+ 'Day' => $end_field['Day']
14179+ );
14180+ }
14181+ }
14182+ Search_Profile::save(Auth::getUserID(), Auth::getCurrentProject(), 'issue', $cookie);
14183+ return $cookie;
14184+ }
14185+
14186+
14187+ /**
14188+ * Method used to get the current sorting options used in the grid layout
14189+ * of the issue listing page.
14190+ *
14191+ * @access public
14192+ * @param array $options The current search parameters
14193+ * @return array The sorting options
14194+ */
14195+ function getSortingInfo($options)
14196+ {
14197+
14198+ $custom_fields = Custom_Field::getFieldsToBeListed(Auth::getCurrentProject());
14199+
14200+ // default order for last action date, priority should be descending
14201+ // for textual fields, like summary, ascending is reasonable
14202+ $fields = array(
14203+ "pri_rank" => "desc",
14204+ "iss_id" => "desc",
14205+ "iss_customer_id" => "desc",
14206+ "prc_title" => "asc",
14207+ "sta_rank" => "asc",
14208+ "iss_created_date" => "desc",
14209+ "iss_summary" => "asc",
14210+ "last_action_date" => "desc",
14211+ "usr_full_name" => "asc",
14212+ "iss_expected_resolution_date" => "desc",
14213+ "pre_title" => "asc",
14214+ "assigned" => "asc",
14215+ "isu_order" => "desc",
14216+ );
14217+
14218+ foreach ($custom_fields as $fld_id => $fld_name) {
14219+ $fields['custom_field_' . $fld_id] = "desc";
14220+ }
14221+
14222+ $sortfields = array_combine(array_keys($fields), array_keys($fields));
14223+ $sortfields["pre_title"] = "pre_scheduled_date";
14224+ $sortfields["assigned"] = "isu_usr_id";
14225+
14226+ $items = array(
14227+ "links" => array(),
14228+ "images" => array()
14229+ );
14230+ foreach ($sortfields as $field => $sortfield) {
14231+ $sort_order = $fields[$field];
14232+ if ($options["sort_by"] == $sortfield) {
14233+ $items["images"][$field] = "images/" . strtolower($options["sort_order"]) . ".gif";
14234+ if (strtolower($options["sort_order"]) == "asc") {
14235+ $sort_order = "desc";
14236+ } else {
14237+ $sort_order = "asc";
14238+ }
14239+ }
14240+ $items["links"][$field] = $_SERVER["PHP_SELF"] . "?sort_by=" . $sortfield . "&sort_order=" . $sort_order;
14241+ }
14242+ return $items;
14243+ }
14244+
14245+
14246+ /**
14247+ * Returns the list of action date fields appropriate for the
14248+ * current user ID.
14249+ *
14250+ * @access public
14251+ * @return array The list of action date fields
14252+ */
14253+ function getLastActionFields()
14254+ {
14255+ $last_action_fields = array(
14256+ "iss_last_public_action_date"
14257+ );
14258+ if (Auth::getCurrentRole() > User::getRoleID('Customer')) {
14259+ $last_action_fields[] = "iss_last_internal_action_date";
14260+ }
14261+ if (count($last_action_fields) > 1) {
14262+ return "GREATEST(" . implode(', IFNULL(', $last_action_fields) . ", '0000-00-00')) AS last_action_date";
14263+ } else {
14264+ return $last_action_fields[0] . " AS last_action_date";
14265+ }
14266+ }
14267+
14268+
14269+ /**
14270+ * Method used to get the list of issues to be displayed in the grid layout.
14271+ *
14272+ * @access public
14273+ * @param integer $prj_id The current project ID
14274+ * @param array $options The search parameters
14275+ * @param integer $current_row The current page number
14276+ * @param integer $max The maximum number of rows per page
14277+ * @return array The list of issues to be displayed
14278+ */
14279+ function getListing($prj_id, $options, $current_row = 0, $max = 5)
14280+ {
14281+ if (strtoupper($max) == "ALL") {
14282+ $max = 9999999;
14283+ }
14284+ $start = $current_row * $max;
14285+ // get the current user's role
14286+ $usr_id = Auth::getUserID();
14287+ $role_id = User::getRoleByUser($usr_id, $prj_id);
14288+
14289+ // get any custom fields that should be displayed
14290+ $custom_fields = Custom_Field::getFieldsToBeListed($prj_id);
14291+
14292+ $stmt = "SELECT
14293+ iss_id,
14294+ iss_grp_id,
14295+ iss_prj_id,
14296+ iss_sta_id,
14297+ iss_customer_id,
14298+ iss_customer_contract_id,
14299+ iss_created_date,
14300+ iss_updated_date,
14301+ iss_last_response_date,
14302+ iss_closed_date,
14303+ iss_last_customer_action_date,
14304+ iss_usr_id,
14305+ iss_summary,
14306+ pri_title,
14307+ prc_title,
14308+ sta_title,
14309+ sta_color status_color,
14310+ sta_id,
14311+ iqu_status,
14312+ grp_name `group`,
14313+ pre_title,
14314+ iss_last_public_action_date,
14315+ iss_last_public_action_type,
14316+ iss_last_internal_action_date,
14317+ iss_last_internal_action_type,
14318+ " . self::getLastActionFields() . ",
14319+ IF(iss_last_internal_action_date > iss_last_public_action_date, 'internal', 'public') AS action_type,
14320+ iss_private,
14321+ usr_full_name,
14322+ iss_percent_complete,
14323+ iss_dev_time,
14324+ iss_expected_resolution_date
14325+ FROM
14326+ (
14327+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
14328+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
14329+ // join custom fields if we are searching by custom fields
14330+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
14331+ foreach ($options['custom_field'] as $fld_id => $search_value) {
14332+ if (empty($search_value)) {
14333+ continue;
14334+ }
14335+ $field = Custom_Field::getDetails($fld_id);
14336+ if (($field['fld_type'] == 'date') && ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
14337+ continue;
14338+ }
14339+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
14340+ continue;
14341+ }
14342+ if ($field['fld_type'] == 'multiple') {
14343+ $search_value = Misc::escapeInteger($search_value);
14344+ foreach ($search_value as $cfo_id) {
14345+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
14346+ }
14347+ } else {
14348+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
14349+ }
14350+ }
14351+ }
14352+ $stmt .= ")";
14353+ // check for the custom fields we want to sort by
14354+ if (strstr($options['sort_by'], 'custom_field') !== false) {
14355+ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
14356+ $stmt .= "\n LEFT JOIN \n" .
14357+ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
14358+ ON
14359+ (cf_sort.icf_iss_id = iss_id AND cf_sort.icf_fld_id = $fld_id) \n";
14360+ }
14361+ if (!empty($options["users"]) || $options["sort_by"] === "isu_usr_id") {
14362+ $stmt .= "
14363+ LEFT JOIN
14364+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
14365+ ON
14366+ isu_iss_id=iss_id";
14367+ }
14368+ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)))) {
14369+ $stmt .= "
14370+ LEFT JOIN
14371+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
14372+ ON
14373+ iur_iss_id=iss_id";
14374+ }
14375+ if (!empty($options["show_notification_list_issues"])) {
14376+ $stmt .= "
14377+ LEFT JOIN
14378+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
14379+ ON
14380+ sub_iss_id=iss_id";
14381+ }
14382+ $stmt .= "
14383+ LEFT JOIN
14384+ " . APP_DEFAULT_DB . ".`" . APP_TABLE_PREFIX . "group`
14385+ ON
14386+ iss_grp_id=grp_id
14387+ LEFT JOIN
14388+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
14389+ ON
14390+ iss_prc_id=prc_id
14391+ LEFT JOIN
14392+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
14393+ ON
14394+ iss_pre_id = pre_id
14395+ LEFT JOIN
14396+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
14397+ ON
14398+ iss_sta_id=sta_id
14399+ LEFT JOIN
14400+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
14401+ ON
14402+ iss_pri_id=pri_id
14403+ LEFT JOIN
14404+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
14405+ ON
14406+ iss_id=iqu_iss_id AND
14407+ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR iqu_expiration IS NULL)
14408+ WHERE
14409+ iss_prj_id= " . Misc::escapeInteger($prj_id);
14410+ $stmt .= self::buildWhereClause($options);
14411+
14412+ if (strstr($options["sort_by"], 'custom_field') !== false) {
14413+ $fld_details = Custom_Field::getDetails($fld_id);
14414+ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
14415+ } else {
14416+ $sort_by = Misc::escapeString($options["sort_by"]);
14417+ }
14418+
14419+ $stmt .= "
14420+ GROUP BY
14421+ iss_id
14422+ ORDER BY
14423+ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
14424+ iss_id DESC";
14425+ $total_rows = Pager::getTotalRows($stmt);
14426+ $stmt .= "
14427+ LIMIT
14428+ " . Misc::escapeInteger($start) . ", " . Misc::escapeInteger($max);
14429+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
14430+ if (PEAR::isError($res)) {
14431+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14432+ return array(
14433+ "list" => "",
14434+ "info" => ""
14435+ );
14436+ } else {
14437+ if (count($res) > 0) {
14438+ self::getAssignedUsersByIssues($res);
14439+ Time_Tracking::getTimeSpentByIssues($res);
14440+ // need to get the customer titles for all of these issues...
14441+ if (Customer::hasCustomerIntegration($prj_id)) {
14442+ Customer::getCustomerTitlesByIssues($prj_id, $res);
14443+ Customer::getSupportLevelsByIssues($prj_id, $res);
14444+ }
14445+ self::formatLastActionDates($res);
14446+ self::getLastStatusChangeDates($prj_id, $res);
14447+ } elseif ($current_row > 0) {
14448+ // if there are no results, and the page is not the first page reset page to one and reload results
14449+ Auth::redirect("list.php?pagerRow=0&rows=$max");
14450+ }
14451+ $groups = Group::getAssocList($prj_id);
14452+ $categories = Category::getAssocList($prj_id);
14453+ $column_headings = self::getColumnHeadings($prj_id);
14454+ if (count($custom_fields) > 0) {
14455+ $column_headings = array_merge($column_headings,$custom_fields);
14456+ }
14457+ $csv[] = @implode("\t", $column_headings);
14458+ for ($i = 0; $i < count($res); $i++) {
14459+ $res[$i]["time_spent"] = Misc::getFormattedTime($res[$i]["time_spent"]);
14460+ $res[$i]["iss_created_date"] = Date_Helper::getFormattedDate($res[$i]["iss_created_date"]);
14461+ $res[$i]["iss_expected_resolution_date"] = Date_Helper::getSimpleDate($res[$i]["iss_expected_resolution_date"], false);
14462+ $fields = array(
14463+ $res[$i]['pri_title'],
14464+ $res[$i]['iss_id'],
14465+ $res[$i]['usr_full_name'],
14466+ );
14467+ // hide the group column from the output if no
14468+ // groups are available in the database
14469+ if (count($groups) > 0) {
14470+ $fields[] = $res[$i]['group'];
14471+ }
14472+ $fields[] = $res[$i]['assigned_users'];
14473+ $fields[] = $res[$i]['time_spent'];
14474+ // hide the category column from the output if no
14475+ // categories are available in the database
14476+ if (count($categories) > 0) {
14477+ $fields[] = $res[$i]['prc_title'];
14478+ }
14479+ if (Customer::hasCustomerIntegration($prj_id)) {
14480+ $fields[] = @$res[$i]['customer_title'];
14481+ // check if current user is acustomer and has a per incident contract.
14482+ // if so, check if issue is redeemed.
14483+ if (User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
14484+ if ((Customer::hasPerIncidentContract($prj_id, self::getCustomerID($res[$i]['iss_id'])) &&
14485+ (Customer::isRedeemedIncident($prj_id, $res[$i]['iss_id'])))) {
14486+ $res[$i]['redeemed'] = true;
14487+ }
14488+ }
14489+ }
14490+ $fields[] = $res[$i]['sta_title'];
14491+ $fields[] = $res[$i]["status_change_date"];
14492+ $fields[] = $res[$i]["last_action_date"];
14493+ $fields[] = $res[$i]['iss_dev_time'];
14494+ $fields[] = $res[$i]['iss_summary'];
14495+ $fields[] = $res[$i]['iss_expected_resolution_date'];
14496+
14497+ if (count($custom_fields) > 0) {
14498+ $res[$i]['custom_field'] = array();
14499+ $custom_field_values = Custom_Field::getListByIssue($prj_id, $res[$i]['iss_id']);
14500+ foreach ($custom_field_values as $this_field) {
14501+ if (!empty($custom_fields[$this_field['fld_id']])) {
14502+ $res[$i]['custom_field'][$this_field['fld_id']] = $this_field['value'];
14503+ $fields[] = $this_field['value'];
14504+ }
14505+ }
14506+ }
14507+
14508+ $csv[] = @implode("\t", $fields);
14509+ }
14510+ $total_pages = ceil($total_rows / $max);
14511+ $last_page = $total_pages - 1;
14512+ return array(
14513+ "list" => $res,
14514+ "info" => array(
14515+ "current_page" => $current_row,
14516+ "start_offset" => $start,
14517+ "end_offset" => $start + count($res),
14518+ "total_rows" => $total_rows,
14519+ "total_pages" => $total_pages,
14520+ "previous_page" => ($current_row == 0) ? "-1" : ($current_row - 1),
14521+ "next_page" => ($current_row == $last_page) ? "-1" : ($current_row + 1),
14522+ "last_page" => $last_page,
14523+ "custom_fields" => $custom_fields
14524+ ),
14525+ "csv" => @implode("\n", $csv)
14526+ );
14527+ }
14528+ }
14529+
14530+
14531+ /**
14532+ * Processes a result set to format the "Last Action Date" column.
14533+ *
14534+ * @access public
14535+ * @param array $result The result set
14536+ */
14537+ function formatLastActionDates(&$result)
14538+ {
14539+ for ($i = 0; $i < count($result); $i++) {
14540+ if (($result[$i]['action_type'] == "internal") &&
14541+ (Auth::getCurrentRole() > User::getRoleID('Customer'))) {
14542+ $label = $result[$i]["iss_last_internal_action_type"];
14543+ $last_date = $result[$i]["iss_last_internal_action_date"];
14544+ } else {
14545+ $label = $result[$i]["iss_last_public_action_type"];
14546+ $last_date = $result[$i]["iss_last_public_action_date"];
14547+ }
14548+ $date = new Date($last_date);
14549+ $current = new Date(Date_Helper::getCurrentDateGMT());
14550+ $result[$i]['last_action_date'] = sprintf("%s: %s ago", ucwords($label),
14551+ Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
14552+ }
14553+ }
14554+
14555+
14556+ /**
14557+ * Retrieves the last status change date for the given issue.
14558+ *
14559+ * @access public
14560+ * @param integer $prj_id The project ID
14561+ * @param array $result The associative array of data
14562+ * @see self::getListing()
14563+ */
14564+ function getLastStatusChangeDates($prj_id, &$result)
14565+ {
14566+ $ids = array();
14567+ for ($i = 0; $i < count($result); $i++) {
14568+ $ids[] = $result[$i]["iss_sta_id"];
14569+ }
14570+ if (count($ids) == 0) {
14571+ return false;
14572+ }
14573+ $customizations = Status::getProjectStatusCustomization($prj_id, $ids);
14574+ for ($i = 0; $i < count($result); $i++) {
14575+ if (empty($result[$i]['iss_sta_id'])) {
14576+ $result[$i]['status_change_date'] = '';
14577+ } else {
14578+ list($label, $date_field_name) = @$customizations[$result[$i]['iss_sta_id']];
14579+ if ((empty($label)) || (empty($date_field_name))) {
14580+ $result[$i]['status_change_date'] = '';
14581+ continue;
14582+ }
14583+ $current = new Date(Date_Helper::getCurrentDateGMT());
14584+ $desc = "$label: %s ago";
14585+ $target_date = $result[$i][$date_field_name];
14586+ if (empty($target_date)) {
14587+ $result[$i]['status_change_date'] = '';
14588+ continue;
14589+ }
14590+ $date = new Date($target_date);
14591+ $result[$i]['status_change_date'] = sprintf($desc, Date_Helper::getFormattedDateDiff($current->getDate(DATE_FORMAT_UNIXTIME), $date->getDate(DATE_FORMAT_UNIXTIME)));
14592+ }
14593+ }
14594+ }
14595+
14596+
14597+ /**
14598+ * Method used to get the list of issues to be displayed in the grid layout.
14599+ *
14600+ * @access public
14601+ * @param array $options The search parameters
14602+ * @return string The where clause
14603+ */
14604+ function buildWhereClause($options)
14605+ {
14606+ $usr_id = Auth::getUserID();
14607+ $prj_id = Auth::getCurrentProject();
14608+ $role_id = User::getRoleByUser($usr_id, $prj_id);
14609+
14610+ $stmt = ' AND iss_usr_id = usr_id';
14611+ if ($role_id == User::getRoleID('Customer')) {
14612+ $stmt .= " AND iss_customer_id=" . User::getCustomerID($usr_id);
14613+ } elseif (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id))) {
14614+ $stmt .= " AND (
14615+ iss_usr_id = $usr_id OR
14616+ iur_usr_id = $usr_id
14617+ )";
14618+ }
14619+
14620+ if (!empty($options["users"])) {
14621+ $stmt .= " AND (\n";
14622+ if (stristr($options["users"], "grp") !== false) {
14623+ $chunks = explode(":", $options["users"]);
14624+ $stmt .= 'iss_grp_id = ' . Misc::escapeInteger($chunks[1]);
14625+ } else {
14626+ if ($options['users'] == '-1') {
14627+ $stmt .= 'isu_usr_id IS NULL';
14628+ } elseif ($options['users'] == '-2') {
14629+ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id=' . $usr_id;
14630+ } elseif ($options['users'] == '-3') {
14631+ $stmt .= 'isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
14632+ } elseif ($options['users'] == '-4') {
14633+ $stmt .= 'isu_usr_id IS NULL OR isu_usr_id = ' . $usr_id . ' OR iss_grp_id = ' . User::getGroupID($usr_id);
14634+ } else {
14635+ $stmt .= 'isu_usr_id =' . Misc::escapeInteger($options["users"]);
14636+ }
14637+ }
14638+ $stmt .= ')';
14639+ }
14640+ if (!empty($options["reporter"])) {
14641+ $stmt .= " AND iss_usr_id = " . Misc::escapeInteger($options["reporter"]);
14642+ }
14643+ if (!empty($options["show_authorized_issues"])) {
14644+ $stmt .= " AND (iur_usr_id=$usr_id)";
14645+ }
14646+ if (!empty($options["show_notification_list_issues"])) {
14647+ $stmt .= " AND (sub_usr_id=$usr_id)";
14648+ }
14649+ if (!empty($options["keywords"])) {
14650+ $stmt .= " AND (\n";
14651+ if (($options['search_type'] == 'all_text') && (APP_ENABLE_FULLTEXT)) {
14652+ $stmt .= "iss_id IN(" . join(', ', self::getFullTextIssues($options)) . ")";
14653+ } elseif (($options['search_type'] == 'customer') && (Customer::hasCustomerIntegration($prj_id))) {
14654+ // check if the user is trying to search by customer email
14655+ $customer_ids = Customer::getCustomerIDsLikeEmail($prj_id, $options['keywords']);
14656+ if (count($customer_ids) > 0) {
14657+ $stmt .= " iss_customer_id IN (" . implode(', ', $customer_ids) . ")";
14658+ } else {
14659+ // no results, kill query
14660+ $stmt .= " iss_customer_id = -1";
14661+ }
14662+ } else {
14663+ $stmt .= "(" . Misc::prepareBooleanSearch('iss_summary', $options["keywords"]);
14664+ $stmt .= " OR " . Misc::prepareBooleanSearch('iss_description', $options["keywords"]) . ")";
14665+ }
14666+ $stmt .= "\n) ";
14667+ }
14668+ if (!empty($options["priority"])) {
14669+ $stmt .= " AND iss_pri_id=" . Misc::escapeInteger($options["priority"]);
14670+ }
14671+ if (!empty($options["status"])) {
14672+ $stmt .= " AND iss_sta_id=" . Misc::escapeInteger($options["status"]);
14673+ }
14674+ if (!empty($options["category"])) {
14675+ if (!is_array($options['category'])) {
14676+ $options['category'] = array($options['category']);
14677+ }
14678+ $stmt .= " AND iss_prc_id IN(" . join(', ', Misc::escapeInteger($options["category"])) . ")";
14679+ }
14680+ if (!empty($options["hide_closed"])) {
14681+ $stmt .= " AND sta_is_closed=0";
14682+ }
14683+ if (!empty($options['release'])) {
14684+ $stmt .= " AND iss_pre_id = " . Misc::escapeInteger($options['release']);
14685+ }
14686+ // now for the date fields
14687+ $date_fields = array(
14688+ 'created_date',
14689+ 'updated_date',
14690+ 'last_response_date',
14691+ 'first_response_date',
14692+ 'closed_date'
14693+ );
14694+ foreach ($date_fields as $field_name) {
14695+ if (!empty($options[$field_name])) {
14696+ switch ($options[$field_name]['filter_type']) {
14697+ case 'greater':
14698+ $stmt .= " AND iss_$field_name >= '" . Misc::escapeString($options[$field_name]['start']) . "'";
14699+ break;
14700+ case 'less':
14701+ $stmt .= " AND iss_$field_name <= '" . Misc::escapeString($options[$field_name]['start']) . "'";
14702+ break;
14703+ case 'between':
14704+ $stmt .= " AND iss_$field_name BETWEEN '" . Misc::escapeString($options[$field_name]['start']) . "' AND '" . Misc::escapeString($options[$field_name]['end']) . "'";
14705+ break;
14706+ case 'null':
14707+ $stmt .= " AND iss_$field_name IS NULL";
14708+ break;
14709+ case 'in_past':
14710+ if (strlen($options[$field_name]['time_period']) == 0) {
14711+ $options[$field_name]['time_period'] = 0;
14712+ }
14713+ $stmt .= " AND (UNIX_TIMESTAMP('" . Date_Helper::getCurrentDateGMT() . "') - UNIX_TIMESTAMP(iss_$field_name)) <= (" .
14714+ Misc::escapeInteger($options[$field_name]['time_period']) . "*3600)";
14715+ break;
14716+ }
14717+ }
14718+ }
14719+ // custom fields
14720+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
14721+ foreach ($options['custom_field'] as $fld_id => $search_value) {
14722+ if (empty($search_value)) {
14723+ continue;
14724+ }
14725+ $field = Custom_Field::getDetails($fld_id);
14726+ $fld_db_name = Custom_Field::getDBValueFieldNameByType($field['fld_type']);
14727+ if (($field['fld_type'] == 'date') &&
14728+ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
14729+ continue;
14730+ }
14731+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
14732+ continue;
14733+ }
14734+
14735+ if ($field['fld_type'] == 'multiple') {
14736+ $search_value = Misc::escapeInteger($search_value);
14737+ foreach ($search_value as $cfo_id) {
14738+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_iss_id = iss_id";
14739+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . ".icf_fld_id = $fld_id";
14740+ $stmt .= " AND\n cf" . $fld_id . '_' . $cfo_id . "." . $fld_db_name . " = $cfo_id";
14741+ }
14742+ } elseif ($field['fld_type'] == 'date') {
14743+ if ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day']))) {
14744+ continue;
14745+ }
14746+ $search_value = $search_value['Year'] . "-" . $search_value['Month'] . "-" . $search_value['Day'];
14747+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id AND
14748+ cf" . $fld_id . "." . $fld_db_name . " = '" . Misc::escapeString($search_value) . "')";
14749+ } else if ($field['fld_type'] == 'integer') {
14750+ $value = $search_value['value'];
14751+ switch ($search_value['filter_type']) {
14752+ case 'ge': $cmp = '>='; break;
14753+ case 'le': $cmp = '<='; break;
14754+ case 'gt': $cmp = '>'; break;
14755+ case 'lt': $cmp = '<'; break;
14756+ default: $cmp = '='; break;
14757+ }
14758+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
14759+ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
14760+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . $cmp . Misc::escapeString($value) . ')';
14761+ } else {
14762+ $stmt .= " AND\n (iss_id = cf" . $fld_id . ".icf_iss_id";
14763+ $stmt .= " AND\n cf" . $fld_id . ".icf_fld_id = $fld_id";
14764+ if ($field['fld_type'] == 'combo') {
14765+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " IN(" . join(', ', Misc::escapeInteger($search_value)) . ")";
14766+ } else {
14767+ $stmt .= " AND cf" . $fld_id . "." . $fld_db_name . " LIKE '%" . Misc::escapeString($search_value) . "%'";
14768+ }
14769+ $stmt .= ')';
14770+ }
14771+ }
14772+ }
14773+ // clear cached full-text values if we are not searching fulltext anymore
14774+ if ((APP_ENABLE_FULLTEXT) && (@$options['search_type'] != 'all_text')) {
14775+ Session::set('fulltext_string', '');
14776+ Session::set('fulltext_issues', '');
14777+ }
14778+ return $stmt;
14779+ }
14780+
14781+
14782+ /**
14783+ * Method used to get the previous and next issues that are available
14784+ * according to the current search parameters.
14785+ *
14786+ * @access public
14787+ * @param integer $issue_id The issue ID
14788+ * @param array $options The search parameters
14789+ * @return array The list of issues
14790+ */
14791+ function getSides($issue_id, $options)
14792+ {
14793+ $usr_id = Auth::getUserID();
14794+ $role_id = Auth::getCurrentRole();
14795+
14796+ $stmt = "SELECT
14797+ iss_id,
14798+ " . self::getLastActionFields() . "
14799+ FROM
14800+ (
14801+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
14802+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user";
14803+ // join custom fields if we are searching by custom fields
14804+ if ((is_array($options['custom_field'])) && (count($options['custom_field']) > 0)) {
14805+ foreach ($options['custom_field'] as $fld_id => $search_value) {
14806+ if (empty($search_value)) {
14807+ continue;
14808+ }
14809+ $field = Custom_Field::getDetails($fld_id);
14810+ if (($field['fld_type'] == 'date') &&
14811+ ((empty($search_value['Year'])) || (empty($search_value['Month'])) || (empty($search_value['Day'])))) {
14812+ continue;
14813+ }
14814+ if (($field['fld_type'] == 'integer') && empty($search_value['value'])) {
14815+ continue;
14816+ }
14817+
14818+ if ($field['fld_type'] == 'multiple') {
14819+ $search_value = Misc::escapeInteger($search_value);
14820+ foreach ($search_value as $cfo_id) {
14821+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . '_' . $cfo_id . "\n";
14822+ }
14823+ } else {
14824+ $stmt .= ",\n" . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf" . $fld_id . "\n";
14825+ }
14826+ }
14827+ }
14828+ $stmt .= ")";
14829+ // check for the custom fields we want to sort by
14830+ if (strstr($options['sort_by'], 'custom_field') !== false) {
14831+ $fld_id = str_replace("custom_field_", '', $options['sort_by']);
14832+ $stmt .= "\n LEFT JOIN \n" .
14833+ APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field as cf_sort
14834+ ON
14835+ (icf_iss_id = iss_id AND icf_fld_id = $fld_id) \n";
14836+ }
14837+ if (!empty($options["users"]) || @$options["sort_by"] == "isu_usr_id") {
14838+ $stmt .= "
14839+ LEFT JOIN
14840+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
14841+ ON
14842+ isu_iss_id=iss_id";
14843+ }
14844+ if ((!empty($options["show_authorized_issues"])) || (($role_id == User::getRoleID("Reporter")) && (Project::getSegregateReporters(Auth::getCurrentProject())))) {
14845+ $stmt .= "
14846+ LEFT JOIN
14847+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user_replier
14848+ ON
14849+ iur_iss_id=iss_id";
14850+ }
14851+ if (!empty($options["show_notification_list_issues"])) {
14852+ $stmt .= "
14853+ LEFT JOIN
14854+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "subscription
14855+ ON
14856+ sub_iss_id=iss_id";
14857+ }
14858+ if (@$options["sort_by"] == "pre_scheduled_date") {
14859+ $stmt .= "
14860+ LEFT JOIN
14861+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
14862+ ON
14863+ iss_pre_id = pre_id";
14864+ }
14865+ if (@$options['sort_by'] == 'prc_title') {
14866+ $stmt .= "
14867+ LEFT JOIN
14868+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
14869+ ON
14870+ iss_prc_id = prc_id";
14871+ }
14872+ $stmt .= "
14873+ LEFT JOIN
14874+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
14875+ ON
14876+ iss_sta_id=sta_id
14877+ LEFT JOIN
14878+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
14879+ ON
14880+ iss_pri_id=pri_id
14881+ WHERE
14882+ iss_prj_id=" . Auth::getCurrentProject();
14883+ $stmt .= self::buildWhereClause($options);
14884+ if (strstr($options["sort_by"], 'custom_field') !== false) {
14885+ $fld_details = Custom_Field::getDetails($fld_id);
14886+ $sort_by = 'cf_sort.' . Custom_Field::getDBValueFieldNameByType($fld_details['fld_type']);
14887+ } else {
14888+ $sort_by = Misc::escapeString($options["sort_by"]);
14889+ }
14890+ $stmt .= "
14891+ GROUP BY
14892+ iss_id
14893+ ORDER BY
14894+ " . $sort_by . " " . Misc::escapeString($options["sort_order"]) . ",
14895+ iss_id DESC";
14896+ $res = DB_Helper::getInstance()->getCol($stmt);
14897+ if (PEAR::isError($res)) {
14898+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14899+ return "";
14900+ } else {
14901+ // COMPAT: the next line requires PHP >= 4.0.5
14902+ $index = array_search($issue_id, $res);
14903+ if (!empty($res[$index+1])) {
14904+ $next = $res[$index+1];
14905+ }
14906+ if (!empty($res[$index-1])) {
14907+ $previous = $res[$index-1];
14908+ }
14909+ return array(
14910+ "next" => @$next,
14911+ "previous" => @$previous
14912+ );
14913+ }
14914+ }
14915+
14916+
14917+ /**
14918+ * Method used to get the full list of user IDs assigned to a specific
14919+ * issue.
14920+ *
14921+ * @access public
14922+ * @param integer $issue_id The issue ID
14923+ * @return array The list of user IDs
14924+ */
14925+ function getAssignedUserIDs($issue_id)
14926+ {
14927+ $stmt = "SELECT
14928+ usr_id
14929+ FROM
14930+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
14931+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
14932+ WHERE
14933+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
14934+ isu_usr_id=usr_id";
14935+ $res = DB_Helper::getInstance()->getCol($stmt);
14936+ if (PEAR::isError($res)) {
14937+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14938+ return array();
14939+ } else {
14940+ return $res;
14941+ }
14942+ }
14943+
14944+
14945+ /**
14946+ * Method used to see if a user is assigned to an issue.
14947+ *
14948+ * @access public
14949+ * @param integer $issue_id The issue ID
14950+ * @param integer $usr_id An integer containg the ID of the user.
14951+ * @return boolean true if the user(s) are assigned to the issue.
14952+ */
14953+ function isAssignedToUser($issue_id, $usr_id)
14954+ {
14955+ $assigned_users = self::getAssignedUserIDs($issue_id);
14956+ if (in_array($usr_id, $assigned_users)) {
14957+ return true;
14958+ } else {
14959+ return false;
14960+ }
14961+ }
14962+
14963+
14964+ /**
14965+ * Method used to get the full list of reporters associated with a given
14966+ * list of issues.
14967+ *
14968+ * @access public
14969+ * @param array $result The result set
14970+ * @return void
14971+ */
14972+ function getReportersByIssues(&$result)
14973+ {
14974+ $ids = array();
14975+ for ($i = 0; $i < count($result); $i++) {
14976+ $ids[] = $result[$i]["iss_id"];
14977+ }
14978+ $ids = implode(", ", $ids);
14979+ $stmt = "SELECT
14980+ iss_id,
14981+ CONCAT(usr_full_name, ' <', usr_email, '>') AS usr_full_name
14982+ FROM
14983+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
14984+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
14985+ WHERE
14986+ iss_usr_id=usr_id AND
14987+ iss_id IN ($ids)";
14988+ $res = DB_Helper::getInstance()->getAssoc($stmt);
14989+ if (PEAR::isError($res)) {
14990+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
14991+ } else {
14992+ // now populate the $result variable again
14993+ for ($i = 0; $i < count($result); $i++) {
14994+ @$result[$i]['reporter'] = $res[$result[$i]['iss_id']];
14995+ }
14996+ }
14997+ }
14998+
14999+
15000+ /**
15001+ * Method used to get the full list of assigned users by a list
15002+ * of issues. This was originally created to optimize the issue
15003+ * listing page.
15004+ *
15005+ * @access public
15006+ * @param array $result The result set
15007+ * @return void
15008+ */
15009+ function getAssignedUsersByIssues(&$result)
15010+ {
15011+ $ids = array();
15012+ for ($i = 0; $i < count($result); $i++) {
15013+ $ids[] = $result[$i]["iss_id"];
15014+ }
15015+ if (count($ids) < 1) {
15016+ return;
15017+ }
15018+ $ids = implode(", ", $ids);
15019+ $stmt = "SELECT
15020+ isu_iss_id,
15021+ isu_order,
15022+ isu_usr_id,
15023+ usr_full_name
15024+ FROM
15025+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
15026+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
15027+ WHERE
15028+ isu_usr_id=usr_id AND
15029+ isu_iss_id IN ($ids)";
15030+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
15031+ if (PEAR::isError($res)) {
15032+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15033+ } else {
15034+ // gather names of the users assigned to each issue
15035+ $t = array();
15036+ for ($i = 0; $i < count($res); $i++) {
15037+ if (!empty($t[$res[$i]['isu_iss_id']])) {
15038+ $t[$res[$i]['isu_iss_id']] .= ', ' . $res[$i]['usr_full_name'];
15039+ } else {
15040+ $t[$res[$i]['isu_iss_id']] = $res[$i]['usr_full_name'];
15041+ }
15042+ }
15043+ // gather orders
15044+ $o = array();
15045+ for ($i = 0; $i < count($res); $i++) {
15046+ if (empty($o[$res[$i]['isu_iss_id']])) {
15047+ $o[$res[$i]['isu_iss_id']] = array();
15048+ }
15049+ $o[$res[$i]['isu_iss_id']][$res[$i]['isu_usr_id']] = $res[$i]['isu_order'];
15050+ }
15051+ // now populate the $result variable again
15052+ for ($i = 0; $i < count($result); $i++) {
15053+ @$result[$i]['assigned_users'] = $t[$result[$i]['iss_id']];
15054+ @$result[$i]['assigned_users_order'] = $o[$result[$i]['iss_id']];
15055+ }
15056+ }
15057+ }
15058+
15059+
15060+ /**
15061+ * Method used to add the issue description to a list of issues.
15062+ *
15063+ * @access public
15064+ * @param array $result The result set
15065+ * @return void
15066+ */
15067+ function getDescriptionByIssues(&$result)
15068+ {
15069+ if (count($result) == 0) {
15070+ return;
15071+ }
15072+
15073+ $ids = array();
15074+ for ($i = 0; $i < count($result); $i++) {
15075+ $ids[] = $result[$i]["iss_id"];
15076+ }
15077+ $ids = implode(", ", $ids);
15078+
15079+ $stmt = "SELECT
15080+ iss_id,
15081+ iss_description
15082+ FROM
15083+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15084+ WHERE
15085+ iss_id in ($ids)";
15086+ $res = DB_Helper::getInstance()->getAssoc($stmt);
15087+ if (PEAR::isError($res)) {
15088+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15089+ } else {
15090+ for ($i = 0; $i < count($result); $i++) {
15091+ @$result[$i]['iss_description'] = $res[$result[$i]['iss_id']];
15092+ }
15093+ }
15094+ }
15095+
15096+
15097+ /**
15098+ * Method used to get the full list of users (the full names) assigned to a
15099+ * specific issue.
15100+ *
15101+ * @access public
15102+ * @param integer $issue_id The issue ID
15103+ * @return array The list of users
15104+ */
15105+ function getAssignedUsers($issue_id)
15106+ {
15107+ $stmt = "SELECT
15108+ usr_full_name
15109+ FROM
15110+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
15111+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
15112+ WHERE
15113+ isu_iss_id=" . Misc::escapeInteger($issue_id) . " AND
15114+ isu_usr_id=usr_id";
15115+ $res = DB_Helper::getInstance()->getCol($stmt);
15116+ if (PEAR::isError($res)) {
15117+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15118+ return array();
15119+ } else {
15120+ return $res;
15121+ }
15122+ }
15123+
15124+
15125+ /**
15126+ * Method used to get the details for a specific issue.
15127+ *
15128+ * @access public
15129+ * @param integer $issue_id The issue ID
15130+ * @param boolean $force_refresh If the cache should not be used.
15131+ * @return array The details for the specified issue
15132+ */
15133+ function getDetails($issue_id, $force_refresh = false)
15134+ {
15135+ static $returns;
15136+
15137+ $issue_id = Misc::escapeInteger($issue_id);
15138+
15139+ if (empty($issue_id)) {
15140+ return '';
15141+ }
15142+
15143+ if ((!empty($returns[$issue_id])) && ($force_refresh != true)) {
15144+ return $returns[$issue_id];
15145+ }
15146+
15147+ $stmt = "SELECT
15148+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue.*,
15149+ prj_title,
15150+ prc_title,
15151+ pre_title,
15152+ pri_title,
15153+ sta_title,
15154+ sta_abbreviation,
15155+ sta_color status_color,
15156+ sta_is_closed
15157+ FROM
15158+ (
15159+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15160+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project
15161+ )
15162+ LEFT JOIN
15163+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_priority
15164+ ON
15165+ iss_pri_id=pri_id
15166+ LEFT JOIN
15167+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15168+ ON
15169+ iss_sta_id=sta_id
15170+ LEFT JOIN
15171+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_category
15172+ ON
15173+ iss_prc_id=prc_id
15174+ LEFT JOIN
15175+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "project_release
15176+ ON
15177+ iss_pre_id=pre_id
15178+ WHERE
15179+ iss_id=$issue_id AND
15180+ iss_prj_id=prj_id";
15181+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
15182+ if (PEAR::isError($res)) {
15183+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15184+ return "";
15185+ } else {
15186+ if (empty($res)) {
15187+ return "";
15188+ } else {
15189+ $created_date_ts = Date_Helper::getUnixTimestamp($res['iss_created_date'], Date_Helper::getDefaultTimezone());
15190+ // get customer information, if any
15191+ if ((!empty($res['iss_customer_id'])) && (Customer::hasCustomerIntegration($res['iss_prj_id']))) {
15192+ $res['customer_business_hours'] = Customer::getBusinessHours($res['iss_prj_id'], $res['iss_customer_id']);
15193+ $res['contact_local_time'] = Date_Helper::getFormattedDate(Date_Helper::getCurrentDateGMT(), $res['iss_contact_timezone']);
15194+ $res['customer_info'] = Customer::getDetails($res['iss_prj_id'], $res['iss_customer_id'], false, $res['iss_customer_contract_id']);
15195+ $res['redeemed_incidents'] = Customer::getRedeemedIncidentDetails($res['iss_prj_id'], $res['iss_id']);
15196+ $max_first_response_time = Customer::getMaximumFirstResponseTime($res['iss_prj_id'], $res['iss_customer_id'], $res['iss_customer_contract_id']);
15197+ $res['max_first_response_time'] = Misc::getFormattedTime($max_first_response_time / 60);
15198+ if (empty($res['iss_first_response_date'])) {
15199+ $first_response_deadline = $created_date_ts + $max_first_response_time;
15200+ if (Date_Helper::getCurrentUnixTimestampGMT() <= $first_response_deadline) {
15201+ $res['max_first_response_time_left'] = Date_Helper::getFormattedDateDiff($first_response_deadline, Date_Helper::getCurrentUnixTimestampGMT());
15202+ } else {
15203+ $res['overdue_first_response_time'] = Date_Helper::getFormattedDateDiff(Date_Helper::getCurrentUnixTimestampGMT(), $first_response_deadline);
15204+ }
15205+ }
15206+ }
15207+ $res['iss_original_description'] = $res["iss_description"];
15208+ if (!strstr($_SERVER["PHP_SELF"], 'update.php')) {
15209+ $res["iss_description"] = nl2br(htmlspecialchars($res["iss_description"]));
15210+ $res["iss_resolution"] = Resolution::getTitle($res["iss_res_id"]);
15211+ }
15212+ $res["iss_impact_analysis"] = nl2br(htmlspecialchars($res["iss_impact_analysis"]));
15213+ $res["iss_created_date"] = Date_Helper::getFormattedDate($res["iss_created_date"]);
15214+ $res['iss_created_date_ts'] = $created_date_ts;
15215+ $res["assignments"] = @implode(", ", array_values(self::getAssignedUsers($res["iss_id"])));
15216+ list($res['authorized_names'], $res['authorized_repliers']) = Authorized_Replier::getAuthorizedRepliers($res["iss_id"]);
15217+ $temp = self::getAssignedUsersStatus($res["iss_id"]);
15218+ $res["has_inactive_users"] = 0;
15219+ $res["assigned_users"] = array();
15220+ $res["assigned_inactive_users"] = array();
15221+ foreach ($temp as $usr_id => $usr_status) {
15222+ if (!User::isActiveStatus($usr_status)) {
15223+ $res["assigned_inactive_users"][] = $usr_id;
15224+ $res["has_inactive_users"] = 1;
15225+ } else {
15226+ $res["assigned_users"][] = $usr_id;
15227+ }
15228+ }
15229+ if (@in_array(Auth::getUserID(), $res["assigned_users"])) {
15230+ $res["is_current_user_assigned"] = 1;
15231+ } else {
15232+ $res["is_current_user_assigned"] = 0;
15233+ }
15234+ $res["associated_issues_details"] = self::getAssociatedIssuesDetails($res["iss_id"]);
15235+ $res["associated_issues"] = self::getAssociatedIssues($res["iss_id"]);
15236+ $res["reporter"] = User::getFullName($res["iss_usr_id"]);
15237+ if (empty($res["iss_updated_date"])) {
15238+ $res["iss_updated_date"] = 'not updated yet';
15239+ } else {
15240+ $res["iss_updated_date"] = Date_Helper::getFormattedDate($res["iss_updated_date"]);
15241+ }
15242+ $res["estimated_formatted_time"] = Misc::getFormattedTime($res["iss_dev_time"]);
15243+ if (Release::isAssignable($res["iss_pre_id"])) {
15244+ $release = Release::getDetails($res["iss_pre_id"]);
15245+ $res["pre_title"] = $release["pre_title"];
15246+ $res["pre_status"] = $release["pre_status"];
15247+ }
15248+ // need to return the list of issues that are duplicates of this one
15249+ $res["duplicates"] = self::getDuplicateList($res["iss_id"]);
15250+ $res["duplicates_details"] = self::getDuplicateDetailsList($res["iss_id"]);
15251+ // also get the issue title of the duplicated issue
15252+ if (!empty($res['iss_duplicated_iss_id'])) {
15253+ $res['duplicated_issue'] = self::getDuplicatedDetails($res['iss_duplicated_iss_id']);
15254+ }
15255+
15256+ // get group information
15257+ if (!empty($res["iss_grp_id"])) {
15258+ $res["group"] = Group::getDetails($res["iss_grp_id"]);
15259+ }
15260+
15261+ // get quarantine issue
15262+ $res["quarantine"] = self::getQuarantineInfo($res["iss_id"]);
15263+
15264+ $returns[$issue_id] = $res;
15265+ return $res;
15266+ }
15267+ }
15268+ }
15269+
15270+
15271+ /**
15272+ * Method used to get some simple details about the given duplicated issue.
15273+ *
15274+ * @access public
15275+ * @param integer $issue_id The issue ID
15276+ * @return array The duplicated issue details
15277+ */
15278+ function getDuplicatedDetails($issue_id)
15279+ {
15280+ $stmt = "SELECT
15281+ iss_summary title,
15282+ sta_title current_status,
15283+ sta_is_closed is_closed
15284+ FROM
15285+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15286+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15287+ WHERE
15288+ iss_sta_id=sta_id AND
15289+ iss_id=$issue_id";
15290+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
15291+ if (PEAR::isError($res)) {
15292+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15293+ return array();
15294+ } else {
15295+ return $res;
15296+ }
15297+ }
15298+
15299+
15300+ /**
15301+ * Method used to bulk update a list of issues
15302+ *
15303+ * @access public
15304+ * @return boolean
15305+ */
15306+ function bulkUpdate()
15307+ {
15308+ // check if user performing this chance has the proper role
15309+ if (Auth::getCurrentRole() < User::getRoleID('Manager')) {
15310+ return -1;
15311+ }
15312+
15313+ $items = Misc::escapeInteger($_POST['item']);
15314+ $new_status_id = Misc::escapeInteger($_POST['status']);
15315+ $new_release_id = Misc::escapeInteger(@$_POST['release']);
15316+ $new_priority_id = Misc::escapeInteger($_POST['priority']);
15317+ $new_category_id = Misc::escapeInteger($_POST['category']);
15318+
15319+ for ($i = 0; $i < count($items); $i++) {
15320+ if (!self::canAccess($items[$i], Auth::getUserID())) {
15321+ continue;
15322+ } elseif (self::getProjectID($_POST['item'][$i]) != Auth::getCurrentProject()) {
15323+ // make sure issue is not in another project
15324+ continue;
15325+ }
15326+
15327+ $updated_fields = array();
15328+
15329+ // update assignment
15330+ if (count(@$_POST['users']) > 0) {
15331+ $users = Misc::escapeInteger($_POST['users']);
15332+ // get who this issue is currently assigned too
15333+ $stmt = "SELECT
15334+ isu_usr_id,
15335+ usr_full_name
15336+ FROM
15337+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user,
15338+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "user
15339+ WHERE
15340+ isu_usr_id = usr_id AND
15341+ isu_iss_id = " . $items[$i];
15342+ $current_assignees = DB_Helper::getInstance()->getAssoc($stmt);
15343+ if (PEAR::isError($current_assignees)) {
15344+ Error_Handler::logError(array($current_assignees->getMessage(), $current_assignees->getDebugInfo()), __FILE__, __LINE__);
15345+ return -1;
15346+ }
15347+ foreach ($current_assignees as $usr_id => $usr_name) {
15348+ if (!in_array($usr_id, $users)) {
15349+ self::deleteUserAssociation($items[$i], $usr_id, false);
15350+ }
15351+ }
15352+ $new_user_names = array();
15353+ $new_assignees = array();
15354+ foreach ($users as $usr_id) {
15355+ $new_user_names[$usr_id] = User::getFullName($usr_id);
15356+
15357+ // check if the issue is already assigned to this person
15358+ $stmt = "SELECT
15359+ COUNT(*) AS total
15360+ FROM
15361+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
15362+ WHERE
15363+ isu_iss_id=" . $items[$i] . " AND
15364+ isu_usr_id=" . $usr_id;
15365+ $total = DB_Helper::getInstance()->getOne($stmt);
15366+ if ($total > 0) {
15367+ continue;
15368+ } else {
15369+ $new_assignees[] = $usr_id;
15370+ // add the assignment
15371+ self::addUserAssociation(Auth::getUserID(), $items[$i], $usr_id, false);
15372+ Notification::subscribeUser(Auth::getUserID(), $items[$i], $usr_id, Notification::getAllActions());
15373+ Workflow::handleAssignment(Auth::getCurrentProject(), $items[$i], Auth::getUserID());
15374+ }
15375+ }
15376+ Notification::notifyNewAssignment($new_assignees, $items[$i]);
15377+ $updated_fields['Assignment'] = History::formatChanges(join(', ', $current_assignees), join(', ', $new_user_names));
15378+ }
15379+
15380+ // update status
15381+ if (!empty($new_status_id)) {
15382+ $old_status_id = self::getStatusID($items[$i]);
15383+ $res = self::setStatus($items[$i], $new_status_id, false);
15384+ if ($res == 1) {
15385+ $updated_fields['Status'] = History::formatChanges(Status::getStatusTitle($old_status_id), Status::getStatusTitle($new_status_id));
15386+ }
15387+ }
15388+
15389+ // update release
15390+ if (!empty($new_release_id)) {
15391+ $old_release_id = self::getRelease($items[$i]);
15392+ $res = self::setRelease($items[$i], $new_release_id);
15393+ if ($res == 1) {
15394+ $updated_fields['Release'] = History::formatChanges(Release::getTitle($old_release_id), Release::getTitle($new_release_id));
15395+ }
15396+ }
15397+
15398+ // update priority
15399+ if (!empty($new_priority_id)) {
15400+ $old_priority_id = self::getPriority($items[$i]);
15401+ $res = self::setPriority($items[$i], $new_priority_id);
15402+ if ($res == 1) {
15403+ $updated_fields['Priority'] = History::formatChanges(Priority::getTitle($old_priority_id), Priority::getTitle($new_priority_id));
15404+ }
15405+ }
15406+
15407+ // update category
15408+ if (!empty($new_category_id)) {
15409+ $old_category_id = self::getCategory($items[$i]);
15410+ $res = self::setCategory($items[$i], $new_category_id);
15411+ if ($res == 1) {
15412+ $updated_fields['Category'] = History::formatChanges(Category::getTitle($old_category_id), Category::getTitle($new_category_id));
15413+ }
15414+ }
15415+
15416+ if (count($updated_fields) > 0) {
15417+ // log the changes
15418+ $changes = '';
15419+ $k = 0;
15420+ foreach ($updated_fields as $key => $value) {
15421+ if ($k > 0) {
15422+ $changes .= "; ";
15423+ }
15424+ $changes .= "$key: $value";
15425+ $k++;
15426+ }
15427+ History::add($items[$i], Auth::getUserID(), History::getTypeID('issue_bulk_updated'), "Issue updated ($changes) by " . User::getFullName(Auth::getUserID()));
15428+ }
15429+
15430+ // close if request
15431+ if ((isset($_REQUEST['closed_status'])) && (!empty($_REQUEST['closed_status']))) {
15432+ self::close(Auth::getUserID(), $items[$i], true, 0, Misc::escapeInteger($_REQUEST['closed_status']), Misc::escapeString($_REQUEST['closed_message']), $_REQUEST['notification_list']);
15433+ }
15434+ }
15435+ return true;
15436+ }
15437+
15438+
15439+ /**
15440+ * Method used to set the initial impact analysis for a specific issue
15441+ *
15442+ * @access public
15443+ * @param integer $issue_id The issue ID
15444+ * @return integer 1 if the update worked, -1 otherwise
15445+ */
15446+ function setImpactAnalysis($issue_id)
15447+ {
15448+ $stmt = "UPDATE
15449+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15450+ SET
15451+ iss_updated_date='" . Date_Helper::getCurrentDateGMT() . "',
15452+ iss_last_internal_action_date='" . Date_Helper::getCurrentDateGMT() . "',
15453+ iss_last_internal_action_type='update',
15454+ iss_developer_est_time=" . Misc::escapeInteger($_POST["dev_time"]) . ",
15455+ iss_impact_analysis='" . Misc::escapeString($_POST["impact_analysis"]) . "'
15456+ WHERE
15457+ iss_id=" . Misc::escapeInteger($issue_id);
15458+ $res = DB_Helper::getInstance()->query($stmt);
15459+ if (PEAR::isError($res)) {
15460+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15461+ return -1;
15462+ } else {
15463+ // add the impact analysis to the history of the issue
15464+ $summary = 'Initial Impact Analysis for issue set by ' . User::getFullName(Auth::getUserID());
15465+ History::add($issue_id, Auth::getUserID(), History::getTypeID('impact_analysis_added'), $summary);
15466+ return 1;
15467+ }
15468+ }
15469+
15470+
15471+ /**
15472+ * Method used to get the full list of issue IDs that area available in the
15473+ * system.
15474+ *
15475+ * @access public
15476+ * @param string $extra_condition An extra condition in the WHERE clause
15477+ * @return array The list of issue IDs
15478+ */
15479+ function getColList($extra_condition = NULL)
15480+ {
15481+ $stmt = "SELECT
15482+ iss_id
15483+ FROM
15484+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15485+ WHERE
15486+ iss_prj_id=" . Auth::getCurrentProject();
15487+ if (!empty($extra_condition)) {
15488+ $stmt .= " AND $extra_condition ";
15489+ }
15490+ $stmt .= "
15491+ ORDER BY
15492+ iss_id DESC";
15493+ $res = DB_Helper::getInstance()->getCol($stmt);
15494+ if (PEAR::isError($res)) {
15495+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15496+ return "";
15497+ } else {
15498+ return $res;
15499+ }
15500+ }
15501+
15502+
15503+ /**
15504+ * Method used to get the full list of issue IDs and their respective
15505+ * titles.
15506+ *
15507+ * @access public
15508+ * @param string $extra_condition An extra condition in the WHERE clause
15509+ * @return array The list of issues
15510+ */
15511+ function getAssocList($extra_condition = NULL)
15512+ {
15513+ $stmt = "SELECT
15514+ iss_id,
15515+ iss_summary
15516+ FROM
15517+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15518+ WHERE
15519+ iss_prj_id=" . Auth::getCurrentProject();
15520+ if (!empty($extra_condition)) {
15521+ $stmt .= " AND $extra_condition ";
15522+ }
15523+ $stmt .= "
15524+ ORDER BY
15525+ iss_id ASC";
15526+ $res = DB_Helper::getInstance()->getAssoc($stmt);
15527+ if (PEAR::isError($res)) {
15528+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15529+ return "";
15530+ } else {
15531+ return $res;
15532+ }
15533+ }
15534+
15535+
15536+ /**
15537+ * Method used to get the list of issues associated to a specific issue.
15538+ *
15539+ * @access public
15540+ * @param integer $issue_id The issue ID
15541+ * @return array The list of associated issues
15542+ */
15543+ function getAssociatedIssues($issue_id)
15544+ {
15545+ $issues = self::getAssociatedIssuesDetails($issue_id);
15546+ $associated = array();
15547+ for ($i = 0; $i < count($issues); $i++) {
15548+ $associated[] = $issues[$i]['associated_issue'];
15549+ }
15550+ return $associated;
15551+ }
15552+
15553+
15554+ /**
15555+ * Method used to get the list of issues associated details to a
15556+ * specific issue.
15557+ *
15558+ * @access public
15559+ * @param integer $issue_id The issue ID
15560+ * @return array The list of associated issues
15561+ */
15562+ function getAssociatedIssuesDetails($issue_id)
15563+ {
15564+ static $returns;
15565+
15566+ if (!empty($returns[$issue_id])) {
15567+ return $returns[$issue_id];
15568+ }
15569+
15570+ $stmt = "SELECT
15571+ isa_associated_id associated_issue,
15572+ iss_summary associated_title,
15573+ sta_title current_status,
15574+ sta_is_closed is_closed
15575+ FROM
15576+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_association,
15577+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15578+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15579+ WHERE
15580+ isa_associated_id=iss_id AND
15581+ iss_sta_id=sta_id AND
15582+ isa_issue_id=$issue_id";
15583+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
15584+ if (PEAR::isError($res)) {
15585+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15586+ return array();
15587+ } else {
15588+ $returns[$issue_id] = $res;
15589+ return $res;
15590+ }
15591+ }
15592+
15593+
15594+ /**
15595+ * Method used to check whether an issue was already closed or not.
15596+ *
15597+ * @access public
15598+ * @param integer $issue_id The issue ID
15599+ * @return boolean
15600+ */
15601+ function isClosed($issue_id)
15602+ {
15603+ $stmt = "SELECT
15604+ COUNT(*)
15605+ FROM
15606+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15607+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "status
15608+ WHERE
15609+ iss_id=" . Misc::escapeInteger($issue_id) . " AND
15610+ iss_sta_id=sta_id AND
15611+ sta_is_closed=1";
15612+ $res = DB_Helper::getInstance()->getOne($stmt);
15613+ if (PEAR::isError($res)) {
15614+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15615+ return false;
15616+ } else {
15617+ if ($res == 0) {
15618+ return false;
15619+ } else {
15620+ return true;
15621+ }
15622+ }
15623+ }
15624+
15625+
15626+ /**
15627+ * Returns a simple list of issues that are currently set to some
15628+ * form of quarantine. This is mainly used by the IRC interface.
15629+ *
15630+ * @access public
15631+ * @return array List of quarantined issues
15632+ */
15633+ function getQuarantinedIssueList()
15634+ {
15635+ // XXX: would be nice to restrict the result list to only one project
15636+ $stmt = "SELECT
15637+ iss_id,
15638+ iss_summary
15639+ FROM
15640+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue,
15641+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15642+ WHERE
15643+ iqu_iss_id=iss_id AND
15644+ iqu_expiration >= '" . Date_Helper::getCurrentDateGMT() . "' AND
15645+ iqu_expiration IS NOT NULL";
15646+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
15647+ if (PEAR::isError($res)) {
15648+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15649+ return array();
15650+ } else {
15651+ self::getAssignedUsersByIssues($res);
15652+ return $res;
15653+ }
15654+ }
15655+
15656+
15657+ /**
15658+ * Returns the status of a quarantine.
15659+ *
15660+ * @param integer $issue_id The issue ID
15661+ * @return integer Indicates what the current state of quarantine is.
15662+ */
15663+ function getQuarantineInfo($issue_id)
15664+ {
15665+ $stmt = "SELECT
15666+ iqu_status,
15667+ iqu_expiration
15668+ FROM
15669+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15670+ WHERE
15671+ iqu_iss_id = " . Misc::escapeInteger($issue_id) . " AND
15672+ (iqu_expiration > '" . Date_Helper::getCurrentDateGMT() . "' OR
15673+ iqu_expiration IS NULL)";
15674+ $res = DB_Helper::getInstance()->getRow($stmt, DB_FETCHMODE_ASSOC);
15675+ if (PEAR::isError($res)) {
15676+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15677+ return array();
15678+ } else {
15679+ if (!empty($res["iqu_expiration"])) {
15680+ $expiration_ts = Date_Helper::getUnixTimestamp($res['iqu_expiration'], Date_Helper::getDefaultTimezone());
15681+ $res["time_till_expiration"] = Date_Helper::getFormattedDateDiff($expiration_ts, Date_Helper::getCurrentUnixTimestampGMT());
15682+ }
15683+ return $res;
15684+ }
15685+ }
15686+
15687+
15688+ /**
15689+ * Sets the quarantine status. Optionally an expiration date can be set
15690+ * to indicate when the quarantine expires. A status > 0 indicates that quarantine is active.
15691+ *
15692+ * @access public
15693+ * @param integer $issue_id The issue ID
15694+ * @param integer $status The quarantine status
15695+ * @param string $expiration The expiration date of quarantine (default empty)
15696+ */
15697+ function setQuarantine($issue_id, $status, $expiration = '')
15698+ {
15699+ $issue_id = Misc::escapeInteger($issue_id);
15700+ $status = Misc::escapeInteger($status);
15701+
15702+ // see if there is an existing record
15703+ $stmt = "SELECT
15704+ COUNT(*)
15705+ FROM
15706+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15707+ WHERE
15708+ iqu_iss_id = $issue_id";
15709+ $res = DB_Helper::getInstance()->getOne($stmt);
15710+ if (PEAR::isError($res)) {
15711+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15712+ return -1;
15713+ }
15714+ if ($res > 0) {
15715+ // update
15716+ $stmt = "UPDATE
15717+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15718+ SET
15719+ iqu_status = $status";
15720+ if (!empty($expiration)) {
15721+ $stmt .= ",\niqu_expiration = '" . Misc::escapeString($expiration) . "'";
15722+ }
15723+ $stmt .= "\nWHERE
15724+ iqu_iss_id = $issue_id";
15725+ $res = DB_Helper::getInstance()->query($stmt);
15726+ if (PEAR::isError($res)) {
15727+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15728+ return -1;
15729+ } else {
15730+ // add history entry about this change taking place
15731+ if ($status == 0) {
15732+ History::add($issue_id, Auth::getUserID(), History::getTypeID('issue_quarantine_removed'),
15733+ "Issue quarantine status cleared by " . User::getFullName(Auth::getUserID()));
15734+ }
15735+ }
15736+ } else {
15737+ // insert
15738+ $stmt = "INSERT INTO
15739+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_quarantine
15740+ (
15741+ iqu_iss_id,
15742+ iqu_status";
15743+ if (!empty($expiration)) {
15744+ $stmt .= ",\niqu_expiration\n";
15745+ }
15746+ $stmt .= ") VALUES (
15747+ $issue_id,
15748+ $status";
15749+ if (!empty($expiration)) {
15750+ $stmt .= ",\n'" . Misc::escapeString($expiration) . "'\n";
15751+ }
15752+ $stmt .= ")";
15753+ $res = DB_Helper::getInstance()->query($stmt);
15754+ if (PEAR::isError($res)) {
15755+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15756+ return -1;
15757+ }
15758+ }
15759+ return 1;
15760+ }
15761+
15762+
15763+ /**
15764+ * Sets the group of the issue.
15765+ *
15766+ * @access public
15767+ * @param integer $issue_id The ID of the issue
15768+ * @param integer $group_id The ID of the group
15769+ * @return integer 1 if successful, -1 or -2 otherwise
15770+ */
15771+ function setGroup($issue_id, $group_id)
15772+ {
15773+ $issue_id = Misc::escapeInteger($issue_id);
15774+ $group_id = Misc::escapeInteger($group_id);
15775+
15776+ $current = self::getDetails($issue_id);
15777+ if ($current["iss_grp_id"] == $group_id) {
15778+ return -2;
15779+ }
15780+ $stmt = "UPDATE
15781+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15782+ SET
15783+ iss_grp_id = $group_id
15784+ WHERE
15785+ iss_id = $issue_id";
15786+ $res = DB_Helper::getInstance()->query($stmt);
15787+ if (PEAR::isError($res)) {
15788+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15789+ return -1;
15790+ }
15791+ $current_user = Auth::getUserID();
15792+ if (empty($current_user)) {
15793+ $current_user = APP_SYSTEM_USER_ID;
15794+ }
15795+ History::add($issue_id, $current_user, History::getTypeID('group_changed'),
15796+ "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($group_id)) . ") by " . User::getFullName($current_user));
15797+ return 1;
15798+ }
15799+
15800+
15801+ /**
15802+ * Returns the group ID associated with the given issue ID.
15803+ *
15804+ * @access public
15805+ * @param integer $issue_id The issue ID
15806+ * @return integer The associated group ID
15807+ */
15808+ function getGroupID($issue_id)
15809+ {
15810+ $stmt = "SELECT
15811+ iss_grp_id
15812+ FROM
15813+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15814+ WHERE
15815+ iss_id=" . Misc::escapeInteger($issue_id);
15816+ $res = DB_Helper::getInstance()->getOne($stmt);
15817+ if (PEAR::isError($res)) {
15818+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15819+ return 0;
15820+ } else {
15821+ return $res;
15822+ }
15823+ }
15824+
15825+
15826+ /**
15827+ * Returns an array of issues based on full text search results.
15828+ *
15829+ * @param array $options An array of search options
15830+ * @return array An array of issue IDS
15831+ */
15832+ function getFullTextIssues($options)
15833+ {
15834+ // check if a list of issues for this full text search is already cached
15835+ $fulltext_string = Session::get('fulltext_string');
15836+ if ((!empty($fulltext_string)) && ($fulltext_string == $options['keywords'])) {
15837+ return Session::get('fulltext_issues');
15838+ }
15839+
15840+ // no pre-existing list, generate them
15841+ $stmt = "(SELECT
15842+ DISTINCT(iss_id)
15843+ FROM
15844+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15845+ WHERE
15846+ MATCH(iss_summary, iss_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15847+ ) UNION (
15848+ SELECT
15849+ DISTINCT(not_iss_id)
15850+ FROM
15851+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "note
15852+ WHERE
15853+ MATCH(not_note) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15854+ ) UNION (
15855+ SELECT
15856+ DISTINCT(ttr_iss_id)
15857+ FROM
15858+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "time_tracking
15859+ WHERE
15860+ MATCH(ttr_summary) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15861+ ) UNION (
15862+ SELECT
15863+ DISTINCT(phs_iss_id)
15864+ FROM
15865+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "phone_support
15866+ WHERE
15867+ MATCH(phs_description) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15868+ ) UNION (
15869+ SELECT
15870+ DISTINCT(sup_iss_id)
15871+ FROM
15872+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email,
15873+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "support_email_body
15874+ WHERE
15875+ sup_id = seb_sup_id AND
15876+ MATCH(seb_body) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)
15877+ )";
15878+ $res = DB_Helper::getInstance()->getCol($stmt);
15879+ if (PEAR::isError($res)) {
15880+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15881+ return array(-1);
15882+ } else {
15883+ $stmt = "SELECT
15884+ DISTINCT(icf_iss_id)
15885+ FROM
15886+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_custom_field
15887+ WHERE
15888+ MATCH (icf_value) AGAINST ('" . Misc::escapeString($options['keywords']) . "' IN BOOLEAN MODE)";
15889+ $custom_res = DB_Helper::getInstance()->getCol($stmt);
15890+ if (PEAR::isError($custom_res)) {
15891+ Error_Handler::logError(array($custom_res->getMessage(), $custom_res->getDebugInfo()), __FILE__, __LINE__);
15892+ return array(-1);
15893+ }
15894+ $issues = array_merge($res, $custom_res);
15895+ // we kill the query results on purpose to flag that no
15896+ // issues could be found with fulltext search
15897+ if (count($issues) < 1) {
15898+ $issues = array(-1);
15899+ }
15900+ Session::set('fulltext_string', $options['keywords']);
15901+ Session::set('fulltext_issues', $issues);
15902+ return $issues;
15903+ }
15904+ }
15905+
15906+
15907+ /**
15908+ * Method to determine if user can access a particular issue
15909+ *
15910+ * @access public
15911+ * @param integer $issue_id The ID of the issue.
15912+ * @param integer $usr_id The ID of the user
15913+ * @return boolean If the user can access the issue
15914+ */
15915+ function canAccess($issue_id, $usr_id)
15916+ {
15917+ static $access;
15918+
15919+ if (empty($issue_id)) {
15920+ return true;
15921+ }
15922+
15923+ if (isset($access[$issue_id . "-" . $usr_id])) {
15924+ return $access[$issue_id . "-" . $usr_id];
15925+ }
15926+
15927+ $details = self::getDetails($issue_id);
15928+ if (empty($details)) {
15929+ return true;
15930+ }
15931+ $usr_details = User::getDetails($usr_id);
15932+ $usr_role = User::getRoleByUser($usr_id, $details['iss_prj_id']);
15933+ $prj_id = self::getProjectID($issue_id);
15934+
15935+
15936+ if (empty($usr_role)) {
15937+ // check if they are even allowed to access the project
15938+ $return = false;
15939+ } elseif ((Customer::hasCustomerIntegration($details['iss_prj_id'])) && ($usr_role == User::getRoleID("Customer")) &&
15940+ ($details['iss_customer_id'] != $usr_details['usr_customer_id'])) {
15941+ // check customer permissions
15942+ $return = false;
15943+ } elseif ($details['iss_private'] == 1) {
15944+ // check if the issue is even private
15945+
15946+ // check role, reporter, assigment and group
15947+ if ($usr_role > User::getRoleID("Developer")) {
15948+ $return = true;
15949+ } elseif ($details['iss_usr_id'] == $usr_id) {
15950+ $return = true;
15951+ } elseif (self::isAssignedToUser($issue_id, $usr_id)) {
15952+ $return = true;
15953+ } elseif ((!empty($details['iss_grp_id'])) && (!empty($usr_details['usr_grp_id'])) &&
15954+ ($details['iss_grp_id'] == $usr_details['usr_grp_id'])) {
15955+ $return = true;
15956+ } elseif (Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id)) {
15957+ $return = true;
15958+ } else {
15959+ $return = false;
15960+ }
15961+ } elseif ((Auth::getCurrentRole() == User::getRoleID("Reporter")) && (Project::getSegregateReporters($prj_id)) &&
15962+ ($details['iss_usr_id'] != $usr_id) && (!Authorized_Replier::isUserAuthorizedReplier($issue_id, $usr_id))) {
15963+ return false;
15964+ } else {
15965+ $return = true;
15966+ }
15967+
15968+ $access[$issue_id . "-" . $usr_id] = $return;
15969+ return $return;
15970+ }
15971+
15972+
15973+ /**
15974+ * Returns true if the specified issue is private, false otherwise
15975+ *
15976+ * @access public
15977+ * @param integer $issue_id The ID of the issue
15978+ * @return boolean If the issue is private or not
15979+ */
15980+ function isPrivate($issue_id)
15981+ {
15982+ static $returns;
15983+
15984+ if (!isset($returns[$issue_id])) {
15985+ $sql = "SELECT
15986+ iss_private
15987+ FROM
15988+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
15989+ WHERE
15990+ iss_id=$issue_id";
15991+ $res = DB_Helper::getInstance()->getOne($sql);
15992+ if (PEAR::isError($res)) {
15993+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
15994+ return true;
15995+ } else {
15996+ if ($res == 1) {
15997+ $returns[$issue_id] = true;
15998+ } else {
15999+ $returns[$issue_id] = false;
16000+ }
16001+ }
16002+ }
16003+ return $returns[$issue_id];
16004+ }
16005+
16006+
16007+ /**
16008+ * Clears closed information from an issues.
16009+ *
16010+ * @access public
16011+ * @param integer $issue_id The ID of the issue
16012+ */
16013+ function clearClosed($issue_id)
16014+ {
16015+ $stmt = "UPDATE
16016+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
16017+ SET
16018+ iss_closed_date = null,
16019+ iss_res_id = null
16020+ WHERE
16021+ iss_id=" . Misc::escapeInteger($issue_id);
16022+ $res = DB_Helper::getInstance()->query($stmt);
16023+ if (PEAR::isError($res)) {
16024+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16025+ return -1;
16026+ }
16027+ self::moveOrderForAllUsers($issue_id, 1);
16028+ }
16029+
16030+
16031+ /**
16032+ * Returns the message ID that should be used as the parent ID for all messages
16033+ *
16034+ * @access public
16035+ * @param integer $issue_id The ID of the issue
16036+ */
16037+ function getRootMessageID($issue_id)
16038+ {
16039+ $sql = "SELECT
16040+ iss_root_message_id
16041+ FROM
16042+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
16043+ WHERE
16044+ iss_id=" . Misc::escapeInteger($issue_id);
16045+ $res = DB_Helper::getInstance()->getOne($sql);
16046+ if (PEAR::isError($res)) {
16047+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16048+ return false;
16049+ } else {
16050+ return $res;
16051+ }
16052+ }
16053+
16054+
16055+ /**
16056+ * Returns the issue ID of the issue with the specified root message ID, or false
16057+ * @access public
16058+ * @param string $msg_id The Message ID
16059+ * @return integer The ID of the issue
16060+ */
16061+ function getIssueByRootMessageID($msg_id)
16062+ {
16063+ static $returns;
16064+
16065+ if (!empty($returns[$msg_id])) {
16066+ return $returns[$msg_id];
16067+ }
16068+ $sql = "SELECT
16069+ iss_id
16070+ FROM
16071+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue
16072+ WHERE
16073+ iss_root_message_id = '" . Misc::escapeString($msg_id) . "'";
16074+ $res = DB_Helper::getInstance()->getOne($sql);
16075+ if (PEAR::isError($res)) {
16076+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16077+ return false;
16078+ }
16079+ if (empty($res)) {
16080+ $returns[$msg_id] = false;
16081+ } else {
16082+ $returns[$msg_id] = $res;
16083+ }
16084+ return $returns[$msg_id];
16085+ }
16086+
16087+
16088+ /**
16089+ * Sets the assignees for the issue
16090+ *
16091+ * @param integer $issue_id
16092+ * @param array $assignees
16093+ */
16094+ function setAssignees($issue_id, $assignees)
16095+ {
16096+ if (!is_array($assignees)) {
16097+ $assignees = array();
16098+ }
16099+
16100+ // see if there is anything to change
16101+ $old_assignees = self::getAssignedUserIDs($issue_id);
16102+ if ((count(array_diff($old_assignees, $assignees)) == 0) && (count(array_diff($assignees, $old_assignees)) == 0)) {
16103+ return;
16104+ }
16105+
16106+ $old_assignee_names = self::getAssignedUsers($issue_id);
16107+
16108+ Workflow::handleAssignmentChange(self::getProjectID($issue_id), $issue_id, Auth::getUserID(), self::getDetails($issue_id), $assignees, true);
16109+ // clear up the assignments for this issue, and then assign it to the current user
16110+ self::deleteUserAssociations($issue_id);
16111+ $assignee_names = array();
16112+ foreach ($assignees as $assignee) {
16113+ $res = self::addUserAssociation(Auth::getUserID(), $issue_id, $assignee, false);
16114+ if ($res == -1) {
16115+ return false;
16116+ }
16117+ $assignee_names[] = User::getFullName($assignee);
16118+ Notification::subscribeUser(Auth::getUserID(), $issue_id, $assignee, Notification::getDefaultActions($issue_id, User::getEmail($assignee), 'set_assignees'), false);
16119+ }
16120+
16121+ Notification::notifyNewAssignment($assignees, $issue_id);
16122+
16123+ // save a history entry about this...
16124+ History::add($issue_id, Auth::getUserID(), History::getTypeID('user_associated'),
16125+ "Issue assignment to changed (" . History::formatChanges(join(', ', $old_assignee_names), join(', ', $assignee_names)) . ") by " . User::getFullName(Auth::getUserID()));
16126+ }
16127+
16128+ /**
16129+ * Reorders user's issues as requested by user
16130+ * @access public
16131+ * @param $usr_id User to be reordered
16132+ * @param $issue_id Issue or array of issues to be moved
16133+ * @param $neworder The new order of the issues
16134+ * @return void
16135+ */
16136+ function reorderUserIssues($usr_id, $issue_id, $neworder)
16137+ {
16138+ if (!isset($usr_id) || !isset($issue_id) || !isset($neworder)) {
16139+ return false;
16140+ }
16141+ if (!is_numeric($usr_id) || !is_numeric($neworder)) {
16142+ return false;
16143+ }
16144+ $usr_id = Misc::escapeInteger($usr_id);
16145+ $issue_id = Misc::escapeInteger($issue_id);
16146+ $neworder = Misc::escapeInteger($neworder);
16147+ if (is_array($issue_id)) {
16148+ $issue_count = count($issue_id);
16149+ $issue_id_str = implode(", ", $issue_id);
16150+ } else {
16151+ $issue_count = 1;
16152+ $issue_id_str = $issue_id;
16153+ $issue_id = array($issue_id);
16154+ }
16155+ // do a nasty pretending to be deleting stuff so that reordering happens as if these elements were deleted
16156+ $orderlist = self::getDeleteUserAssociationOrderList($issue_id_str, $usr_id);
16157+ self::rearrangeDeleteUserAssociationOrderList($orderlist);
16158+ // move down the orders to free the "order space" needed
16159+ $stmt = "UPDATE
16160+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16161+ SET
16162+ isu_order = isu_order + $issue_count
16163+ WHERE
16164+ isu_usr_id = $usr_id AND
16165+ isu_order >= $neworder";
16166+ $res = DB_Helper::getInstance()->query($stmt);
16167+ if (PEAR::isError($res)) {
16168+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16169+ return -1;
16170+ }
16171+ //update the order for the issues being moved
16172+ $i = 0;
16173+ foreach ($issue_id as $iss_id) {
16174+ $stmt = "UPDATE
16175+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16176+ SET
16177+ isu_order = " . ($neworder + $i) . "
16178+ WHERE
16179+ isu_usr_id = $usr_id AND
16180+ isu_iss_id = $iss_id";
16181+ $res = DB_Helper::getInstance()->query($stmt);
16182+ if (PEAR::isError($res)) {
16183+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16184+ return -1;
16185+ }
16186+ $i++;
16187+ }
16188+ }
16189+
16190+
16191+ /**
16192+ * Get users issue order list
16193+ * @access public
16194+ * @param $user_id User
16195+ * @param $order_list Order of the issues
16196+ * @return void
16197+ */
16198+ function getIssueOrderByUser($usr_id) {
16199+
16200+ if (!is_numeric($usr_id)) {
16201+ return false;
16202+ }
16203+
16204+ $stmt = "SELECT
16205+ isu_iss_id, isu_order
16206+ FROM
16207+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16208+ WHERE
16209+ isu_usr_id = " . $usr_id ;
16210+
16211+ $order_list = array();
16212+
16213+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
16214+
16215+ if (PEAR::isError($res)) {
16216+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16217+ return array();
16218+ } else {
16219+ foreach ($res as $row) {
16220+ $order_list[$row["isu_iss_id"]] = $row["isu_order"];
16221+ }
16222+ }
16223+ return $order_list;
16224+ }
16225+
16226+ function moveOrderForAllUsers($issue_id, $neworder)
16227+ {
16228+ // Move the issue to the top priority for the ppl it's assigned to
16229+ $stmt = "SELECT isu_usr_id FROM
16230+ " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue_user
16231+ WHERE
16232+ isu_iss_id = " . Misc::escapeInteger($issue_id);
16233+ $res = DB_Helper::getInstance()->getAll($stmt, DB_FETCHMODE_ASSOC);
16234+ if (PEAR::isError($res)) {
16235+ Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
16236+ return -1;
16237+ }
16238+ foreach ($res as $row) {
16239+ self::reorderUserIssues($row["isu_usr_id"], $issue_id, $neworder);
16240+ }
16241+ }
16242+
16243+}
16244--- eventum-2.2/templates/header.tpl.html 2009-09-14 18:07:55.000000000 +0300
16245+++ eventum-2.2-order/templates/header.tpl.html 2009-10-12 22:10:36.435851675 +0300
16246@@ -8,6 +8,7 @@
16247 <script type="text/javascript" src="{$rel_url}js/jquery/form.js?c=9984"></script>
16248 <script type="text/javascript" src="{$rel_url}js/jquery/blockui.js?c=eb13"></script>
16249 <script type="text/javascript" src="{$rel_url}js/jquery/ui.datepicker.js?c=a911"></script>
16250+<script type="text/javascript" src="{$rel_url}js/jquery/jquery.tablednd.js"></script>
16251 <link rel="stylesheet" href="{$rel_url}js/jquery/ui.datepicker.css?c=5096">
16252 <script type="text/javascript">
16253 <!--
16254--- eventum-2.2/templates/list.tpl.html 2009-09-14 18:07:55.000000000 +0300
16255+++ eventum-2.2-order/templates/list.tpl.html 2009-10-12 22:10:36.439185157 +0300
16256@@ -92,6 +92,28 @@
16257 f.target = '_popup';
16258 f.submit();
16259 }
16260+function reorderBulk(order_user, neworder)
16261+{
16262+ url = page_url + "?";
16263+ url += "reorder_user=" + order_user;
16264+
16265+ items = document.getElementsByName("item[]");
16266+ checkedcount = 0;
16267+ for (var i = 0; i < items.length; i++) {
16268+ if (items[i].checked) {
16269+ url += "&reorder_source[" + checkedcount + "]=" + items[i].value;
16270+ checkedcount++;
16271+ }
16272+ }
16273+ if (checkedcount == 0) {
16274+ alert('{/literal}{t escape=js}Please choose which issues to move to the new place.{/t}{literal}');
16275+ return false;
16276+ }
16277+
16278+ url += "&reorder_neworder=" + neworder;
16279+
16280+ window.location.href = url;
16281+}
16282 function hideClosed(f)
16283 {
16284 if (f.hide_closed.checked) {
16285@@ -153,6 +175,13 @@
16286 f.go.disabled = true;
16287 }
16288 }
16289+function updateCustomFields(issue_id)
16290+{
16291+ var features = 'width=560,height=460,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16292+ var customWin = window.open('custom_fields.php?issue_id=' + issue_id, '_custom_fields', features);
16293+ customWin.focus();
16294+ return false;
16295+}
16296 //-->
16297 </script>
16298 {/literal}
16299@@ -169,11 +198,11 @@
16300 <input type="hidden" name="cat" value="bulk_update">
16301 <tr>
16302 <td>
16303- <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0">
16304- <tr>
16305+ <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0" id="issue_list_table">
16306+ <tr class="nodrag">
16307 <td colspan="{$col_count}" class="default">
16308 <table width="100%" cellspacing="0" cellpadding="0" border="0">
16309- <tr>
16310+ <tr class="nodrag">
16311 <td class="default">
16312 <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
16313 {include file="help_link.tpl.html" topic="list"}
16314@@ -193,7 +222,7 @@
16315 </table>
16316 </td>
16317 </tr>
16318- <tr bgcolor="{$cell_color}">
16319+ <tr bgcolor="{$cell_color}" class="nodrag">
16320 {if $current_role > $roles.developer}
16321 <td width="1%">
16322 <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
16323@@ -208,7 +237,7 @@
16324 {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
16325 </td>
16326 {/foreach}
16327- {else}
16328+ {elseif $field_name != 'isu_order' || $isu_order_user}
16329 <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
16330 {if $field_name == 'iss_summary'}
16331 <table cellspacing="0" cellpadding="1" width="100%">
16332@@ -232,20 +261,21 @@
16333 {/if}
16334 {/foreach}
16335 </tr>
16336+ <tbody>
16337 {section name="i" loop=$list}
16338- <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}>
16339+ <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if} id="{$list[i].iss_id}" {if !$list[i].assigned_users_order[$current_user_id]}class="nodrag"{/if}>
16340 {if $current_role > $roles.developer}
16341 <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
16342 {/if}
16343 {foreach from=$columns item=column key=field_name}
16344 {if $field_name == 'custom_fields'}
16345 {foreach from=$list[i].custom_field key=fld_id item=fld_value}
16346- <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16347- {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
16348+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default custom_field" onclick="return updateCustomFields({$list[i].iss_id});">
16349+ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
16350 </td>
16351 {/foreach}
16352- {else}
16353- <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16354+ {elseif $field_name != 'isu_order' || $isu_order_user}
16355+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default{if $field_name == 'isu_order'} dragHandle{/if}">
16356 {if $field_name == 'iss_id'}
16357 <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
16358 {elseif $field_name == 'pri_rank'}
16359@@ -279,7 +309,7 @@
16360 {elseif $field_name == 'iss_percent_complete'}
16361 {$list[i].iss_percent_complete|escape:"html"}%
16362 {elseif $field_name == 'iss_expected_resolution_date'}
16363- {$list[i].iss_expected_resolution_date|escape:"html"}
16364+ <div class="inline_date_pick" id="expected_resolution_date|{$list[i].iss_id}">{$list[i].iss_expected_resolution_date|escape:"html"}&nbsp;</div>
16365 {elseif $field_name == 'iss_summary'}
16366 <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
16367 {if $list[i].redeemed}
16368@@ -288,6 +318,8 @@
16369 {if $list[i].iss_private == 1}
16370 <b>[Private]</b>
16371 {/if}
16372+ {elseif $field_name == 'isu_order'}
16373+ {if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}&nbsp;{/if}
16374 {/if}
16375 </td>
16376 {/if}
16377@@ -300,10 +332,11 @@
16378 </td>
16379 </tr>
16380 {/section}
16381- <tr bgcolor="{$cell_color}">
16382+ </tbody>
16383+ <tr bgcolor="{$cell_color}" class="nodrag">
16384 <td colspan="{$col_count}">
16385 <table width="100%" cellspacing="0" cellpadding="0">
16386- <tr>
16387+ <tr class="nodrag">
16388 <td width="30%" nowrap>
16389 {if $current_role > $roles.developer}
16390 <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
16391@@ -355,6 +388,35 @@
16392 </form>
16393 </table>
16394 <br />
16395-
16396+<script type="text/javascript">
16397+{*
16398+ * Order issues by drag and drop:
16399+ * only if sorted by order and viewing your own issues
16400+ *}
16401+{if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}
16402+{literal}
16403+var before = ''; // make it global variable
16404+$('#issue_list_table').tableDnD({
16405+ onDragClass: "tDnD_whileDrag",
16406+ onDragStart: function(table, row) {
16407+ before = $.tableDnD.serialize('id');
16408+ },
16409+ onDrop: function(table, row) {
16410+ $.post("/ajax/order.php", {before: before, after: $.tableDnD.serialize('id')}, function(data) {
16411+ if (data.length > 0) {
16412+ alert(data);
16413+ }
16414+ }, "text");
16415+ },
16416+ dragHandle: "dragHandle"
16417+});
16418+$("#issue_list_table tr").hover(function() {
16419+ $('#' + this.id + ' .dragHandle').addClass('showDragHandle');
16420+}, function() {
16421+ $('#' + this.id + ' .dragHandle').removeClass('showDragHandle');
16422+});
16423+{/literal}
16424+{/if}
16425+</script>
16426 {include file="app_info.tpl.html"}
16427 {include file="footer.tpl.html"}
16428--- eventum-2.2/templates/list.tpl.html.~1~ 1970-01-01 02:00:00.000000000 +0200
16429+++ eventum-2.2-order/templates/list.tpl.html.~1~ 2009-09-14 18:07:55.000000000 +0300
16430@@ -0,0 +1,360 @@
16431+{include file="header.tpl.html" extra_title="List of Issues"}
16432+{include file="navigation.tpl.html"}
16433+
16434+{if $current_role != $roles.customer}
16435+{include file="quick_filter_form.tpl.html"}
16436+{include file="current_filters.tpl.html"}
16437+{/if}
16438+<script type="text/javascript">
16439+<!--
16440+var page_url = '{$smarty.server.PHP_SELF}';
16441+var current_page = {if $list_info.current_page == ''}0{else}{$list_info.current_page}{/if};
16442+var last_page = {if $list_info.last_page == ''}0{else}{$list_info.last_page}{/if};
16443+{literal}
16444+// this function will display the bulk update form if any issues are selected.
16445+// if no issues are selected it will be hidden.
16446+function toggleBulkUpdate()
16447+{
16448+ var items = document.getElementsByName('item[]');
16449+
16450+ // loop through looking to see which are checked
16451+ var show = false;
16452+ for (var i = 0; i < items.length; i++) {
16453+ if (items[i].checked) {
16454+ show = true;
16455+ break;
16456+ }
16457+ }
16458+ if (show) {
16459+ changeVisibility('bulk_update1', show);
16460+ }
16461+}
16462+function resetBulkUpdate()
16463+{
16464+ var f = getForm('list_form');
16465+ clearSelectedOptions(getFormElement(f, 'users[]'));
16466+ clearSelectedOptions(getFormElement(f, 'status'));
16467+ if (getFormElement(f, 'release')) {
16468+ clearSelectedOptions(getFormElement(f, 'release'));
16469+ }
16470+}
16471+function bulkUpdate()
16472+{
16473+ var f = getForm('list_form');
16474+ if (!hasOneChecked(f, 'item[]')) {
16475+ alert('{/literal}{t escape=js}Please choose which issues to update.{/t}{literal}');
16476+ return false;
16477+ }
16478+
16479+ // figure out what is changing
16480+ var changed = new Array();
16481+ if (hasOneSelected(f, 'users[]')) {
16482+ changed[changed.length] = 'Assignment';
16483+ }
16484+ if (f.elements.status.selectedIndex != 0) {
16485+ changed[changed.length] = 'Status';
16486+ }
16487+ if ((f.elements.release) && (f.elements.release.selectedIndex != 0)) {
16488+ changed[changed.length] = 'Release';
16489+ }
16490+ if ((f.elements.priority) && (f.elements.priority.selectedIndex != 0)) {
16491+ changed[changed.length] = 'Priority';
16492+ }
16493+ if ((f.elements.category) && (f.elements.category.selectedIndex != 0)) {
16494+ changed[changed.length] = 'Category';
16495+ }
16496+ if ((f.elements.closed_status) && (f.elements.closed_status.selectedIndex != 0)) {
16497+ changed[changed.length] = 'Closed Status';
16498+ }
16499+ if (changed.length < 1) {
16500+ alert('{/literal}{t escape=js}Please choose new values for the selected issues{/t}{literal}');
16501+ return false;
16502+ }
16503+ var msg = '{/literal}{t escape=js}Warning: If you continue, you will change the {/t}{literal}';
16504+ for (var i = 0; i < changed.length; i++) {
16505+ msg += changed[i];
16506+ if ((changed.length > 1) && (i == (changed.length-2))) {
16507+ msg += ' and ';
16508+ } else {
16509+ if (i != (changed.length-1)) {
16510+ msg += ', ';
16511+ }
16512+ }
16513+ }
16514+ msg += ' {/literal}{t escape=js}for all selected issues. Are you sure you want to continue?{/t}{literal}';
16515+ if (!confirm(msg)) {
16516+ return false;
16517+ }
16518+ var features = 'width=420,height=200,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16519+ var popupWin = window.open('', '_popup', features);
16520+ popupWin.focus();
16521+ f.action = 'popup.php';
16522+ f.target = '_popup';
16523+ f.submit();
16524+}
16525+function hideClosed(f)
16526+{
16527+ if (f.hide_closed.checked) {
16528+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '1');
16529+ } else {
16530+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '0');
16531+ }
16532+}
16533+function resizePager(f)
16534+{
16535+ var pagesize = f.page_size.options[f.page_size.selectedIndex].value;
16536+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'rows', pagesize);
16537+}
16538+function checkPageField(ev)
16539+{
16540+ // check if the user is trying to submit the form by hitting <enter>
16541+ if (((window.event) && (window.event.keyCode == 13)) ||
16542+ ((ev) && (ev.which == 13))) {
16543+ return false;
16544+ }
16545+}
16546+function goPage(f, new_page)
16547+{
16548+ if ((new_page > last_page+1) || (new_page <= 0) ||
16549+ (new_page == current_page+1) || (!isNumberOnly(new_page))) {
16550+ f.page.value = current_page+1;
16551+ return false;
16552+ }
16553+ setPage(new_page-1);
16554+}
16555+function setPage(new_page)
16556+{
16557+ if ((new_page > last_page) || (new_page < 0) ||
16558+ (new_page == current_page)) {
16559+ return false;
16560+ }
16561+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'pagerRow', new_page);
16562+}
16563+function downloadCSV()
16564+{
16565+ var f = this.document.csv_form;
16566+ f.submit();
16567+ return false;
16568+}
16569+window.onload = disableFields;
16570+function disableFields()
16571+{
16572+ var f = document.list_form;
16573+ if (current_page == 0) {
16574+ f.first.disabled = true;
16575+ f.previous.disabled = true;
16576+ }
16577+ if ((current_page == last_page) || (last_page <= 0)) {
16578+ f.next.disabled = true;
16579+ f.last.disabled = true;
16580+ }
16581+ if ((current_page == 0) && (last_page <= 0)) {
16582+ f.page.disabled = true;
16583+ f.go.disabled = true;
16584+ }
16585+}
16586+//-->
16587+</script>
16588+{/literal}
16589+{* Figure out how many columns there are *}
16590+{assign var="col_count" value=$columns|@count}
16591+{if $list_info.custom_fields|@count > 1}
16592+{math assign="col_count" equation="x+y-1" x=$col_count y=$list_info.custom_fields|@count}
16593+{/if}
16594+{if $current_role > $roles.developer}
16595+ {math assign="col_count" equation="x+1" x=$col_count}
16596+{/if}
16597+<table width="100%" bgcolor="{$cell_color}" border="0" cellspacing="0" cellpadding="1" align="center">
16598+ <form name="list_form" method="post" action="{$smarty.server.PHP_SELF}">
16599+ <input type="hidden" name="cat" value="bulk_update">
16600+ <tr>
16601+ <td>
16602+ <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0">
16603+ <tr>
16604+ <td colspan="{$col_count}" class="default">
16605+ <table width="100%" cellspacing="0" cellpadding="0" border="0">
16606+ <tr>
16607+ <td class="default">
16608+ <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
16609+ {include file="help_link.tpl.html" topic="list"}
16610+ </td>
16611+ {if $current_role != $roles.customer}
16612+ <td align="right" class="default" nowrap>
16613+ {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up}
16614+ {t}hide/show{/t}:
16615+ [ <a class="link" title="{t}hide / show the quick search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('filter_form');">{t}quick search{/t}</a> ]
16616+ {if $current_role > $roles.reporter}[ <a class="link" title="{t}hide / show the advanced search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('custom_filter_form');">{t}advanced search{/t}</a> ]{/if}
16617+ {if $current_role > $roles.reporter}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('current_filters');"> {t}current filters{/t}</a> ]{/if}
16618+ {if $current_role > $roles.developer}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('bulk_update');"> {t}bulk update tool{/t}</a> ]{/if}
16619+ {/if}
16620+ </td>
16621+ {/if}
16622+ </tr>
16623+ </table>
16624+ </td>
16625+ </tr>
16626+ <tr bgcolor="{$cell_color}">
16627+ {if $current_role > $roles.developer}
16628+ <td width="1%">
16629+ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
16630+ </td>
16631+ {/if}
16632+ {foreach from=$columns item=column key=field_name}
16633+ {if $field_name == 'custom_fields'}
16634+ {foreach from=$list_info.custom_fields key=fld_id item=fld_title}
16635+ {assign var="fld_name_id" value="custom_field_`$fld_id`"}
16636+ <td align="{$column.align|default:'center'}" class="default_white" nowrap>
16637+ <a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link">{$fld_title|escape:"html"}</a>
16638+ {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
16639+ </td>
16640+ {/foreach}
16641+ {else}
16642+ <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
16643+ {if $field_name == 'iss_summary'}
16644+ <table cellspacing="0" cellpadding="1" width="100%">
16645+ <tr>
16646+ <td class="default_white">
16647+ <a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link">{t}Summary{/t}</a>
16648+ {if $sorting.images.iss_summary != ""}<a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link"><img border="0" src="{$sorting.images.iss_summary}"></a>{/if}
16649+ </td>
16650+ <td align="right">
16651+ <span class="default_white">{t}Export Data:{/t}</span> <input alt="{t}generate excel-friendly report{/t}" type="image" src="{$rel_url}images/excel.jpg" class="shortcut" value="{t}Export to Excel{/t}" onClick="javascript:return downloadCSV();">
16652+ </td>
16653+ </tr>
16654+ </table>
16655+ {elseif $sorting.links[$field_name] != ''}
16656+ <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
16657+ {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
16658+ {else}
16659+ {$column.title}
16660+ {/if}
16661+ </td>
16662+ {/if}
16663+ {/foreach}
16664+ </tr>
16665+ {section name="i" loop=$list}
16666+ <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if}>
16667+ {if $current_role > $roles.developer}
16668+ <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
16669+ {/if}
16670+ {foreach from=$columns item=column key=field_name}
16671+ {if $field_name == 'custom_fields'}
16672+ {foreach from=$list[i].custom_field key=fld_id item=fld_value}
16673+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16674+ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
16675+ </td>
16676+ {/foreach}
16677+ {else}
16678+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
16679+ {if $field_name == 'iss_id'}
16680+ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
16681+ {elseif $field_name == 'pri_rank'}
16682+ {$list[i].pri_title|escape:"html"}
16683+ {elseif $field_name == 'iss_grp_id'}
16684+ {$list[i].group}
16685+ {elseif $field_name == 'assigned'}
16686+ {$list[i].assigned_users}
16687+ {elseif $field_name == 'time_spent'}
16688+ {$list[i].time_spent}
16689+ {elseif $field_name == 'prc_title'}
16690+ {$list[i].prc_title}
16691+ {elseif $field_name == 'pre_title'}
16692+ {$list[i].pre_title|escape:"html"}
16693+ {elseif $field_name == 'iss_customer_id'}
16694+ {$list[i].customer_title|escape:"html"}
16695+ {elseif $field_name == 'support_level'}
16696+ {$list[i].support_level|escape:"html"}
16697+ {elseif $field_name == 'sta_rank'}
16698+ {$list[i].sta_title|escape:"html"}
16699+ {elseif $field_name == 'iss_created_date'}
16700+ {$list[i].iss_created_date|escape:"html"}
16701+ {elseif $field_name == 'iss_dev_time'}
16702+ {$list[i].iss_dev_time|escape:"html"}
16703+ {elseif $field_name == 'sta_change_date'}
16704+ {$list[i].status_change_date|escape:"html"}
16705+ {elseif $field_name == 'last_action_date'}
16706+ {$list[i].last_action_date|escape:"html"}
16707+ {elseif $field_name == 'usr_full_name'}
16708+ {$list[i].usr_full_name|escape:"html"}
16709+ {elseif $field_name == 'iss_percent_complete'}
16710+ {$list[i].iss_percent_complete|escape:"html"}%
16711+ {elseif $field_name == 'iss_expected_resolution_date'}
16712+ {$list[i].iss_expected_resolution_date|escape:"html"}
16713+ {elseif $field_name == 'iss_summary'}
16714+ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
16715+ {if $list[i].redeemed}
16716+ [Redeemed]
16717+ {/if}
16718+ {if $list[i].iss_private == 1}
16719+ <b>[Private]</b>
16720+ {/if}
16721+ {/if}
16722+ </td>
16723+ {/if}
16724+ {/foreach}
16725+ </tr>
16726+ {sectionelse}
16727+ <tr bgcolor="gray">
16728+ <td colspan="{$col_count}" class="default_white" align="center">
16729+ <i>{t}No issues could be found.{/t}</i>
16730+ </td>
16731+ </tr>
16732+ {/section}
16733+ <tr bgcolor="{$cell_color}">
16734+ <td colspan="{$col_count}">
16735+ <table width="100%" cellspacing="0" cellpadding="0">
16736+ <tr>
16737+ <td width="30%" nowrap>
16738+ {if $current_role > $roles.developer}
16739+ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
16740+ {/if}
16741+ </td>
16742+ <td width="40%" align="center" nowrap>
16743+ <nobr>
16744+ <input name="first" type="button" value="|&lt;" class="shortcut" onClick="javascript:setPage(0);">
16745+ <input name="previous" type="button" value="&lt;&lt;" class="shortcut" onClick="javascript:setPage({$list_info.previous_page});">
16746+ <input type="text" name="page" size="3" maxlength="3" value="{if $list_info.current_page == ''}1{else}{math equation="x + 1" x=$list_info.current_page}{/if}" style="background: {$cell_color};" class="paging_input" onKeyPress="javascript:return checkPageField(event);">
16747+ <input name="go" type="button" value="{t}Go{/t}" class="shortcut" onClick="javascript:goPage(this.form, this.form.page.value);">
16748+ <input name="next" type="button" value="&gt;&gt;" class="shortcut" onClick="javascript:setPage({$list_info.next_page});">
16749+ <input name="last" type="button" value="&gt;|" class="shortcut" onClick="javascript:setPage({$list_info.last_page});">
16750+ </nobr>
16751+ </td>
16752+ <td nowrap>
16753+ <span class="default_white">{t}Rows per Page:{/t}</span>
16754+ <select name="page_size" class="default" onChange="javascript:resizePager(this.form);">
16755+ <option value="5" {if $options.rows == 5}selected{/if}>5</option>
16756+ <option value="10" {if $options.rows == 10}selected{/if}>10</option>
16757+ <option value="25" {if $options.rows == 25}selected{/if}>25</option>
16758+ <option value="50" {if $options.rows == 50}selected{/if}>50</option>
16759+ <option value="100" {if $options.rows == 100}selected{/if}>100</option>
16760+ <option value="ALL" {if $options.rows == "ALL"}selected{/if}>{t}ALL{/t}</option>
16761+ </select>
16762+ <input type="button" value="{t}Set{/t}" class="shortcut" onClick="javascript:resizePager(this.form);">
16763+ </td>
16764+ <td width="30%" class="default_white" align="right">
16765+ <input type="checkbox" id="hide_closed" name="hide_closed" {if $options.hide_closed}checked{/if} onClick="javascript:hideClosed(this.form);"> <label for="hide_closed">{t}Hide Closed Issues{/t}</label>&nbsp;
16766+ </td>
16767+ </tr>
16768+ </table>
16769+ </td>
16770+ </tr>
16771+ </table>
16772+ </td>
16773+ </tr>
16774+ {if $current_role > $roles.developer}
16775+ <tr>
16776+ <td bgcolor="#FFFFFF">
16777+ <br />
16778+ {include file="bulk_update.tpl.html"}
16779+ </td>
16780+ </tr>
16781+ {/if}
16782+ </form>
16783+ <form target="_csvWindow" method="post" action="csv.php" name="csv_form">
16784+ <input type="hidden" name="csv_data" value="{$csv_data}">
16785+ </form>
16786+</table>
16787+<br />
16788+
16789+{include file="app_info.tpl.html"}
16790+{include file="footer.tpl.html"}
16791--- eventum-2.2/templates/list.tpl.html.~2~ 1970-01-01 02:00:00.000000000 +0200
16792+++ eventum-2.2-order/templates/list.tpl.html.~2~ 2009-10-12 22:10:36.435851675 +0300
16793@@ -0,0 +1,421 @@
16794+{include file="header.tpl.html" extra_title="List of Issues"}
16795+{include file="navigation.tpl.html"}
16796+
16797+{if $current_role != $roles.customer}
16798+{include file="quick_filter_form.tpl.html"}
16799+{include file="current_filters.tpl.html"}
16800+{/if}
16801+<script type="text/javascript">
16802+<!--
16803+var page_url = '{$smarty.server.PHP_SELF}';
16804+var current_page = {if $list_info.current_page == ''}0{else}{$list_info.current_page}{/if};
16805+var last_page = {if $list_info.last_page == ''}0{else}{$list_info.last_page}{/if};
16806+{literal}
16807+// this function will display the bulk update form if any issues are selected.
16808+// if no issues are selected it will be hidden.
16809+function toggleBulkUpdate()
16810+{
16811+ var items = document.getElementsByName('item[]');
16812+
16813+ // loop through looking to see which are checked
16814+ var show = false;
16815+ for (var i = 0; i < items.length; i++) {
16816+ if (items[i].checked) {
16817+ show = true;
16818+ break;
16819+ }
16820+ }
16821+ if (show) {
16822+ changeVisibility('bulk_update1', show);
16823+ }
16824+}
16825+function resetBulkUpdate()
16826+{
16827+ var f = getForm('list_form');
16828+ clearSelectedOptions(getFormElement(f, 'users[]'));
16829+ clearSelectedOptions(getFormElement(f, 'status'));
16830+ if (getFormElement(f, 'release')) {
16831+ clearSelectedOptions(getFormElement(f, 'release'));
16832+ }
16833+}
16834+function bulkUpdate()
16835+{
16836+ var f = getForm('list_form');
16837+ if (!hasOneChecked(f, 'item[]')) {
16838+ alert('{/literal}{t escape=js}Please choose which issues to update.{/t}{literal}');
16839+ return false;
16840+ }
16841+
16842+ // figure out what is changing
16843+ var changed = new Array();
16844+ if (hasOneSelected(f, 'users[]')) {
16845+ changed[changed.length] = 'Assignment';
16846+ }
16847+ if (f.elements.status.selectedIndex != 0) {
16848+ changed[changed.length] = 'Status';
16849+ }
16850+ if ((f.elements.release) && (f.elements.release.selectedIndex != 0)) {
16851+ changed[changed.length] = 'Release';
16852+ }
16853+ if ((f.elements.priority) && (f.elements.priority.selectedIndex != 0)) {
16854+ changed[changed.length] = 'Priority';
16855+ }
16856+ if ((f.elements.category) && (f.elements.category.selectedIndex != 0)) {
16857+ changed[changed.length] = 'Category';
16858+ }
16859+ if ((f.elements.closed_status) && (f.elements.closed_status.selectedIndex != 0)) {
16860+ changed[changed.length] = 'Closed Status';
16861+ }
16862+ if (changed.length < 1) {
16863+ alert('{/literal}{t escape=js}Please choose new values for the selected issues{/t}{literal}');
16864+ return false;
16865+ }
16866+ var msg = '{/literal}{t escape=js}Warning: If you continue, you will change the {/t}{literal}';
16867+ for (var i = 0; i < changed.length; i++) {
16868+ msg += changed[i];
16869+ if ((changed.length > 1) && (i == (changed.length-2))) {
16870+ msg += ' and ';
16871+ } else {
16872+ if (i != (changed.length-1)) {
16873+ msg += ', ';
16874+ }
16875+ }
16876+ }
16877+ msg += ' {/literal}{t escape=js}for all selected issues. Are you sure you want to continue?{/t}{literal}';
16878+ if (!confirm(msg)) {
16879+ return false;
16880+ }
16881+ var features = 'width=420,height=200,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16882+ var popupWin = window.open('', '_popup', features);
16883+ popupWin.focus();
16884+ f.action = 'popup.php';
16885+ f.target = '_popup';
16886+ f.submit();
16887+}
16888+function reorderBulk(order_user, neworder)
16889+{
16890+ url = page_url + "?";
16891+ url += "reorder_user=" + order_user;
16892+
16893+ items = document.getElementsByName("item[]");
16894+ checkedcount = 0;
16895+ for (var i = 0; i < items.length; i++) {
16896+ if (items[i].checked) {
16897+ url += "&reorder_source[" + checkedcount + "]=" + items[i].value;
16898+ checkedcount++;
16899+ }
16900+ }
16901+ if (checkedcount == 0) {
16902+ alert('{/literal}{t escape=js}Please choose which issues to move to the new place.{/t}{literal}');
16903+ return false;
621b0b5e
ER
16904+ }
16905+
f4782eee
ER
16906+ url += "&reorder_neworder=" + neworder;
16907+
16908+ window.location.href = url;
621b0b5e 16909+}
f4782eee
ER
16910+function hideClosed(f)
16911+{
16912+ if (f.hide_closed.checked) {
16913+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '1');
16914+ } else {
16915+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'hide_closed', '0');
16916+ }
c2cb0c93 16917+}
f4782eee
ER
16918+function resizePager(f)
16919+{
16920+ var pagesize = f.page_size.options[f.page_size.selectedIndex].value;
16921+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'rows', pagesize);
c2cb0c93 16922+}
f4782eee
ER
16923+function checkPageField(ev)
16924+{
16925+ // check if the user is trying to submit the form by hitting <enter>
16926+ if (((window.event) && (window.event.keyCode == 13)) ||
16927+ ((ev) && (ev.which == 13))) {
16928+ return false;
16929+ }
d0dabd47 16930+}
f4782eee
ER
16931+function goPage(f, new_page)
16932+{
16933+ if ((new_page > last_page+1) || (new_page <= 0) ||
16934+ (new_page == current_page+1) || (!isNumberOnly(new_page))) {
16935+ f.page.value = current_page+1;
16936+ return false;
16937+ }
16938+ setPage(new_page-1);
d0dabd47 16939+}
f4782eee
ER
16940+function setPage(new_page)
16941+{
16942+ if ((new_page > last_page) || (new_page < 0) ||
16943+ (new_page == current_page)) {
16944+ return false;
16945+ }
16946+ window.location.href = page_url + "?" + replaceParam(window.location.href, 'pagerRow', new_page);
d0dabd47 16947+}
f4782eee
ER
16948+function downloadCSV()
16949+{
16950+ var f = this.document.csv_form;
16951+ f.submit();
16952+ return false;
d0dabd47 16953+}
f4782eee
ER
16954+window.onload = disableFields;
16955+function disableFields()
16956+{
16957+ var f = document.list_form;
16958+ if (current_page == 0) {
16959+ f.first.disabled = true;
16960+ f.previous.disabled = true;
16961+ }
16962+ if ((current_page == last_page) || (last_page <= 0)) {
16963+ f.next.disabled = true;
16964+ f.last.disabled = true;
16965+ }
16966+ if ((current_page == 0) && (last_page <= 0)) {
16967+ f.page.disabled = true;
16968+ f.go.disabled = true;
16969+ }
16970+}
16971+function updateCustomFields(issue_id)
16972+{
16973+ var features = 'width=560,height=460,top=30,left=30,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no';
16974+ var customWin = window.open('custom_fields.php?issue_id=' + issue_id, '_custom_fields', features);
16975+ customWin.focus();
16976+ return false;
16977+}
16978+//-->
16979+</script>
16980+{/literal}
16981+{* Figure out how many columns there are *}
16982+{assign var="col_count" value=$columns|@count}
16983+{if $list_info.custom_fields|@count > 1}
16984+{math assign="col_count" equation="x+y-1" x=$col_count y=$list_info.custom_fields|@count}
16985+{/if}
16986+{if $current_role > $roles.developer}
16987+ {math assign="col_count" equation="x+1" x=$col_count}
16988+{/if}
16989+<table width="100%" bgcolor="{$cell_color}" border="0" cellspacing="0" cellpadding="1" align="center">
16990+ <form name="list_form" method="post" action="{$smarty.server.PHP_SELF}">
16991+ <input type="hidden" name="cat" value="bulk_update">
16992+ <tr>
16993+ <td>
16994+ <table bgcolor="#FFFFFF" width="100%" cellspacing="1" cellpadding="2" border="0" id="issue_list_table">
16995+ <tr class="nodrag">
16996+ <td colspan="{$col_count}" class="default">
16997+ <table width="100%" cellspacing="0" cellpadding="0" border="0">
16998+ <tr class="nodrag">
16999+ <td class="default">
17000+ <b>{t}Search Results{/t} ({$list_info.total_rows} {t}issues found{/t}{if $list_info.end_offset > 0}, {math equation="x + 1" x=$list_info.start_offset} - {$list_info.end_offset} {t}shown{/t}{/if})</b>
17001+ {include file="help_link.tpl.html" topic="list"}
17002+ </td>
17003+ {if $current_role != $roles.customer}
17004+ <td align="right" class="default" nowrap>
17005+ {if $browser.ie5up or $browser.ns6up or $browser.gecko or $browser.safari or $browser.opera5up}
17006+ {t}hide/show{/t}:
17007+ [ <a class="link" title="{t}hide / show the quick search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('filter_form');">{t}quick search{/t}</a> ]
17008+ {if $current_role > $roles.reporter}[ <a class="link" title="{t}hide / show the advanced search form{/t}" href="javascript:void(null);" onClick="javascript:toggleVisibility('custom_filter_form');">{t}advanced search{/t}</a> ]{/if}
17009+ {if $current_role > $roles.reporter}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('current_filters');"> {t}current filters{/t}</a> ]{/if}
17010+ {if $current_role > $roles.developer}[ <a class="link" href="javascript:void(null);" onClick="javascript:toggleVisibility('bulk_update');"> {t}bulk update tool{/t}</a> ]{/if}
17011+ {/if}
17012+ </td>
17013+ {/if}
17014+ </tr>
17015+ </table>
17016+ </td>
17017+ </tr>
17018+ <tr bgcolor="{$cell_color}" class="nodrag">
17019+ {if $current_role > $roles.developer}
17020+ <td width="1%">
17021+ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');toggleBulkUpdate();">
17022+ </td>
17023+ {/if}
17024+ {foreach from=$columns item=column key=field_name}
17025+ {if $field_name == 'custom_fields'}
17026+ {foreach from=$list_info.custom_fields key=fld_id item=fld_title}
17027+ {assign var="fld_name_id" value="custom_field_`$fld_id`"}
17028+ <td align="{$column.align|default:'center'}" class="default_white" nowrap>
17029+ <a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link">{$fld_title|escape:"html"}</a>
17030+ {if $sorting.images[$fld_name_id] != ""}<a title="{t}sort by{/t} {$fld_title|escape:"html"}" href="{$sorting.links[$fld_name_id]}" class="white_link"><img border="0" src="{$sorting.images[$fld_name_id]}"></a>{/if}
17031+ </td>
17032+ {/foreach}
17033+ {elseif $field_name != 'isu_order' || $isu_order_user}
17034+ <td align="{$column.align|default:'center'}" class="default_white" nowrap {if $column.width != ''}width="{$column.width}"{/if}>
17035+ {if $field_name == 'iss_summary'}
17036+ <table cellspacing="0" cellpadding="1" width="100%">
17037+ <tr>
17038+ <td class="default_white">
17039+ <a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link">{t}Summary{/t}</a>
17040+ {if $sorting.images.iss_summary != ""}<a title="{t}sort by summary{/t}" href="{$sorting.links.iss_summary}" class="white_link"><img border="0" src="{$sorting.images.iss_summary}"></a>{/if}
17041+ </td>
17042+ <td align="right">
17043+ <span class="default_white">{t}Export Data:{/t}</span> <input alt="{t}generate excel-friendly report{/t}" type="image" src="{$rel_url}images/excel.jpg" class="shortcut" value="{t}Export to Excel{/t}" onClick="javascript:return downloadCSV();">
17044+ </td>
17045+ </tr>
17046+ </table>
17047+ {elseif $sorting.links[$field_name] != ''}
17048+ <a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link">{$column.title}</a>
17049+ {if $field_name == 'isu_order'}
17050+ <br>{$users[$isu_order_user]}
17051+ {/if}
17052+ {if $sorting.images[$field_name] != ""}<a title="{t}sort by{/t} {$column.title}" href="{$sorting.links[$field_name]}" class="white_link"><img border="0" src="{$sorting.images[$field_name]}"></a>{/if}
17053+ {else}
17054+ {$column.title}
17055+ {/if}
17056+ </td>
17057+ {/if}
17058+ {/foreach}
17059+ </tr>
17060+ <tbody>
17061+ {section name="i" loop=$list}
17062+ <tr {if $current_role >= $roles.developer AND $list[i].iqu_status > 0}style="text-decoration: line-through;"{/if} id="{$list[i].iss_id}" {if !$list[i].assigned_users_order[$current_user_id]}class="nodrag"{/if}>
17063+ {if $current_role > $roles.developer}
17064+ <td bgcolor="{$list[i].status_color}" width="1%" class="default" align="center"><input type="checkbox" name="item[]" value="{$list[i].iss_id}" onchange="toggleBulkUpdate();"></td>
17065+ {/if}
17066+ {foreach from=$columns item=column key=field_name}
17067+ {if $field_name == 'custom_fields'}
17068+ {foreach from=$list[i].custom_field key=fld_id item=fld_value}
17069+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default custom_field" onclick="return updateCustomFields({$list[i].iss_id});">
17070+ {$fld_value|formatCustomValue:$fld_id:$list[i].iss_id}
17071+ </td>
17072+ {/foreach}
17073+ {elseif $field_name != 'isu_order' || $isu_order_user}
17074+ <td bgcolor="{$list[i].status_color}" align="{$column.align|default:'center'}" class="default">
17075+ {if $field_name == 'iss_id'}
17076+ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_id}</a>
17077+ {elseif $field_name == 'pri_rank'}
17078+ {$list[i].pri_title|escape:"html"}
17079+ {elseif $field_name == 'iss_grp_id'}
17080+ {$list[i].group}
17081+ {elseif $field_name == 'assigned'}
17082+ {$list[i].assigned_users}
17083+ {elseif $field_name == 'time_spent'}
17084+ {$list[i].time_spent}
17085+ {elseif $field_name == 'prc_title'}
17086+ {$list[i].prc_title}
17087+ {elseif $field_name == 'pre_title'}
17088+ {$list[i].pre_title|escape:"html"}
17089+ {elseif $field_name == 'iss_customer_id'}
17090+ {$list[i].customer_title|escape:"html"}
17091+ {elseif $field_name == 'support_level'}
17092+ {$list[i].support_level|escape:"html"}
17093+ {elseif $field_name == 'sta_rank'}
17094+ {$list[i].sta_title|escape:"html"}
17095+ {elseif $field_name == 'iss_created_date'}
17096+ {$list[i].iss_created_date|escape:"html"}
17097+ {elseif $field_name == 'iss_dev_time'}
17098+ {$list[i].iss_dev_time|escape:"html"}
17099+ {elseif $field_name == 'sta_change_date'}
17100+ {$list[i].status_change_date|escape:"html"}
17101+ {elseif $field_name == 'last_action_date'}
17102+ {$list[i].last_action_date|escape:"html"}
17103+ {elseif $field_name == 'usr_full_name'}
17104+ {$list[i].usr_full_name|escape:"html"}
17105+ {elseif $field_name == 'iss_percent_complete'}
17106+ {$list[i].iss_percent_complete|escape:"html"}%
17107+ {elseif $field_name == 'iss_expected_resolution_date'}
17108+ <div class="inline_date_pick" id="expected_resolution_date|{$list[i].iss_id}">{$list[i].iss_expected_resolution_date|escape:"html"}&nbsp;</div>
17109+ {elseif $field_name == 'iss_summary'}
17110+ <a href="view.php?id={$list[i].iss_id}" class="link" title="{t}view issue details{/t}">{$list[i].iss_summary|escape:"html"}</a>
17111+ {if $list[i].redeemed}
17112+ [Redeemed]
17113+ {/if}
17114+ {if $list[i].iss_private == 1}
17115+ <b>[Private]</b>
17116+ {/if}
17117+ {elseif $field_name == 'isu_order'}
17118+ {if $list[i].assigned_users_order[$current_user_id]}
17119+ <img src="{$rel_url}images/updown.gif" alt="move">
17120+ {/if}
17121+ {/if}
17122+ </td>
17123+ {/if}
17124+ {/foreach}
17125+ </tr>
17126+ {sectionelse}
17127+ <tr bgcolor="gray">
17128+ <td colspan="{$col_count}" class="default_white" align="center">
17129+ <i>{t}No issues could be found.{/t}</i>
17130+ </td>
17131+ </tr>
17132+ {/section}
17133+ </tbody>
17134+ <tr bgcolor="{$cell_color}" class="nodrag">
17135+ <td colspan="{$col_count}">
17136+ <table width="100%" cellspacing="0" cellpadding="0">
17137+ <tr class="nodrag">
17138+ <td width="30%" nowrap>
17139+ {if $current_role > $roles.developer}
17140+ <input type="button" value="{t}All{/t}" class="shortcut" onClick="javascript:toggleSelectAll(this.form, 'item[]');">
17141+ {/if}
17142+ </td>
17143+ <td width="40%" align="center" nowrap>
17144+ <nobr>
17145+ <input name="first" type="button" value="|&lt;" class="shortcut" onClick="javascript:setPage(0);">
17146+ <input name="previous" type="button" value="&lt;&lt;" class="shortcut" onClick="javascript:setPage({$list_info.previous_page});">
17147+ <input type="text" name="page" size="3" maxlength="3" value="{if $list_info.current_page == ''}1{else}{math equation="x + 1" x=$list_info.current_page}{/if}" style="background: {$cell_color};" class="paging_input" onKeyPress="javascript:return checkPageField(event);">
17148+ <input name="go" type="button" value="{t}Go{/t}" class="shortcut" onClick="javascript:goPage(this.form, this.form.page.value);">
17149+ <input name="next" type="button" value="&gt;&gt;" class="shortcut" onClick="javascript:setPage({$list_info.next_page});">
17150+ <input name="last" type="button" value="&gt;|" class="shortcut" onClick="javascript:setPage({$list_info.last_page});">
17151+ </nobr>
17152+ </td>
17153+ <td nowrap>
17154+ <span class="default_white">{t}Rows per Page:{/t}</span>
17155+ <select name="page_size" class="default" onChange="javascript:resizePager(this.form);">
17156+ <option value="5" {if $options.rows == 5}selected{/if}>5</option>
17157+ <option value="10" {if $options.rows == 10}selected{/if}>10</option>
17158+ <option value="25" {if $options.rows == 25}selected{/if}>25</option>
17159+ <option value="50" {if $options.rows == 50}selected{/if}>50</option>
17160+ <option value="100" {if $options.rows == 100}selected{/if}>100</option>
17161+ <option value="ALL" {if $options.rows == "ALL"}selected{/if}>{t}ALL{/t}</option>
17162+ </select>
17163+ <input type="button" value="{t}Set{/t}" class="shortcut" onClick="javascript:resizePager(this.form);">
17164+ </td>
17165+ <td width="30%" class="default_white" align="right">
17166+ <input type="checkbox" id="hide_closed" name="hide_closed" {if $options.hide_closed}checked{/if} onClick="javascript:hideClosed(this.form);"> <label for="hide_closed">{t}Hide Closed Issues{/t}</label>&nbsp;
17167+ </td>
17168+ </tr>
17169+ </table>
17170+ </td>
17171+ </tr>
17172+ </table>
17173+ </td>
17174+ </tr>
17175+ {if $current_role > $roles.developer}
17176+ <tr>
17177+ <td bgcolor="#FFFFFF">
17178+ <br />
17179+ {include file="bulk_update.tpl.html"}
17180+ </td>
17181+ </tr>
17182+ {/if}
17183+ </form>
17184+ <form target="_csvWindow" method="post" action="csv.php" name="csv_form">
17185+ <input type="hidden" name="csv_data" value="{$csv_data}">
17186+ </form>
17187+</table>
17188+<br />
17189+<script type="text/javascript">
17190+{*
17191+ * Order issues by drag and drop:
17192+ * only if sorted by order and viewing your own issues
17193+ *}
17194+{if $options.sort_by == "isu_order" and $current_user_id == $isu_order_user}
17195+{literal}
17196+var before = ''; // make it global variable
17197+$('#issue_list_table').tableDnD({
17198+ onDragClass: "tDnD_whileDrag",
17199+ onDragStart: function(table, row) {
17200+ before = $.tableDnD.serialize('id');
d0dabd47 17201+ },
f4782eee
ER
17202+ onDrop: function(table, row) {
17203+ $.post("/ajax/order.php", {before: before, after: $.tableDnD.serialize('id')}, function(data) {
17204+ if (data.length > 0) {
17205+ alert(data);
17206+ }
17207+ }, "text");
17208+ }
d0dabd47 17209+});
f4782eee
ER
17210+{/literal}
17211+{/if}
17212+</script>
17213+{include file="app_info.tpl.html"}
d0dabd47 17214+{include file="footer.tpl.html"}
f4782eee
ER
17215\ No newline at end of file
17216--- eventum-2.2/upgrade/patches/04_isu_order.php 1970-01-01 02:00:00.000000000 +0200
17217+++ eventum-2.2-order/upgrade/patches/04_isu_order.php 2009-10-12 22:10:36.439185157 +0300
1f67a110
ER
17218@@ -0,0 +1,15 @@
17219+<?php
17220+
17221+function db_patch_4() {
17222+ $stmts = array();
17223+
17224+ $columns = db_getCol('DESC %TABLE_PREFIX%issue_user');
17225+ if (in_array('isu_order', $columns)) {
17226+ return $stmts;
17227+ }
17228+
17229+ $stmts[] = "ALTER TABLE %TABLE_PREFIX%issue_user ADD isu_order int(11) NOT NULL DEFAULT '0' AFTER isu_assigned_date, ADD INDEX isu_order (isu_order)";
17230+ $stmts[] = "UPDATE %TABLE_PREFIX%issue_user set isu_order=isu_iss_id";
17231+
17232+ return $stmts;
17233+}
f4782eee
ER
17234--- eventum-2.2/upgrade/update-database.php 2009-09-14 18:07:55.000000000 +0300
17235+++ eventum-2.2-order/upgrade/update-database.php 2009-10-12 22:10:36.439185157 +0300
17236@@ -73,6 +73,7 @@
17237 1 => '01_notes.php',
17238 2 => '02_usr_alias.php',
17239 3 => '03_prj_mail_aliases.php',
17240+ 4 => '04_isu_order.php',
17241 );
17242
17243 // sanity check. check that the version table exists.
17244--- eventum-2.2/upgrade/update-database.php.~1~ 1970-01-01 02:00:00.000000000 +0200
17245+++ eventum-2.2-order/upgrade/update-database.php.~1~ 2009-09-14 18:07:55.000000000 +0300
17246@@ -0,0 +1,133 @@
17247+#!/usr/bin/php
17248+<?php
17249+require_once 'init.php';
17250+
17251+// on fresh install config is empty or missing
17252+if (!defined('APP_SQL_DBNAME')) {
17253+ fwrite(STDERR, "Eventum not configured. Please run setup.\n");
17254+ exit(1);
17255+}
17256+
17257+define('EXIT_OK', 0);
17258+define('EXIT_ERROR', 1);
17259+
17260+function db_getAll($query) {
17261+ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17262+ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17263+
17264+ $res = DB_Helper::getInstance()->getAll($query, DB_FETCHMODE_ASSOC);
17265+ if (PEAR::isError($res)) {
17266+ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17267+ exit(1);
8022c588 17268+ }
f4782eee
ER
17269+ return $res;
17270+}
17271+
17272+function db_getOne($query) {
17273+ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17274+ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17275+
17276+ $res = DB_Helper::getInstance()->getOne($query);
17277+ if (PEAR::isError($res)) {
17278+ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17279+ exit(1);
17280+ }
17281+ return $res;
17282+}
17283+
17284+function db_getCol($query) {
17285+ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17286+ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17287+
17288+ $res = DB_Helper::getInstance()->getCol($query);
17289+ if (PEAR::isError($res)) {
17290+ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17291+ exit(1);
17292+ }
17293+ return $res;
17294+}
17295+
17296+function db_query($query) {
17297+ $query = str_replace('%TABLE_PREFIX%', APP_TABLE_PREFIX, $query);
17298+ $query = str_replace('%DBNAME%', APP_SQL_DBNAME, $query);
17299+
17300+ $res = DB_Helper::getInstance()->query($query);
17301+ if (PEAR::isError($res)) {
17302+ echo $res->getMessage(), ': ', $res->getDebugInfo(), "\n";
17303+ exit(1);
17304+ }
17305+ return $res;
17306+}
17307+
17308+function apply_db_changes($stmts) {
17309+ foreach ($stmts as $stmt) {
17310+ db_query($stmt);
17311+ }
17312+}
17313+
17314+function patch_database() {
17315+ /*
17316+ * database versions. each version script can create it's dynamic queries
17317+ */
17318+ $versions = array(
17319+ 1 => '01_notes.php',
17320+ 2 => '02_usr_alias.php',
17321+ 3 => '03_prj_mail_aliases.php',
17322+ );
17323+
17324+ // sanity check. check that the version table exists.
17325+ $version = db_getOne("SELECT ver_version FROM %TABLE_PREFIX%version");
17326+ if (!isset($version)) {
17327+ # insert initial value
17328+ db_query("INSERT INTO %TABLE_PREFIX%version SET ver_version=0");
17329+ $version = 0;
17330+ }
17331+ $target = max(array_keys($versions));
17332+ echo "Current database version: $version; Versions available: $target\n";
17333+ if ($target < $version) {
17334+ echo "ERROR: Your database version is greater ($version) than this upgrade supports ($target)!\n";
17335+ return EXIT_ERROR;
17336+ }
17337+ if ($target == $version) {
17338+ echo "Database already at version $version. Nothing to upgrade.\n";
17339+ return EXIT_OK;
17340+ }
17341+
17342+ echo "Upgrading database to version $target\n";
17343+ for ($i = $version + 1; $i <= $target; $i++) {
17344+ if (empty($versions[$i])) {
17345+ echo "ERROR: patch $i is not recorded in upgrade script.\n";
17346+ return EXIT_ERROR;
8022c588 17347+ }
f4782eee
ER
17348+ $patch = APP_SQL_PATCHES_PATH . '/' . $versions[$i];
17349+ echo "Checking patch $patch\n";
17350+ if (!file_exists($patch)) {
17351+ echo "ERROR: Patch file doesn't exist\n";
17352+ return EXIT_ERROR;
17353+ }
17354+ require $patch;
17355+ $func = "db_patch_$i";
17356+ if (!function_exists($func)) {
17357+ echo "ERROR: Patch did not define '$func' function\n";
17358+ return EXIT_ERROR;
17359+ }
17360+ $patchset = $func();
17361+ echo "Applying patch ", $i, ": ", count($patchset), " queries\n";
17362+ apply_db_changes($patchset);
17363+ db_query("UPDATE %TABLE_PREFIX%version SET ver_version=$i");
17364+ }
17365+
17366+ return EXIT_OK;
17367+}
17368+
17369+if (php_sapi_name() != 'cli') {
17370+ echo "<pre>\n";
17371+}
17372+
17373+$ret = patch_database();
17374+
17375+if (php_sapi_name() != 'cli') {
17376+ echo "</pre>\n";
17377+}
17378+
17379+exit($ret);
This page took 2.778373 seconds and 4 git commands to generate.