]> git.pld-linux.org Git - packages/eventum.git/blob - eventum-order.patch
0594dabf28342fb7295a4b01cca7007ae5839641
[packages/eventum.git] / eventum-order.patch
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
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 +
20 +parse_str($_POST['before'], $before);
21 +parse_str($_POST['after'], $after);
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";
68 +            $res = DB_Helper::getInstance()->query($stmt);
69 +            if (PEAR::isError($res)) {
70 +                Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
71 +                die('update failed');
72 +            }
73 +        }
74 +    }
75 +}
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 @@
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'];
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 +               }
108 +    } else {
109 +        echo 'update failed';
110 +    }
111 +    exit;
112 +  break;
113 +  default:
114 +      die('object type not supported');
115 +  break;
116 +}
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  });
150 +
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 +});
189 +
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));
198 +
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;
210 +
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 +    }
220 +
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;
242 +        }
243 +    }
244 +    return false;
245 +}
246 +
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 +}
257 +
258 +function closeAndRefresh()
259 +{
260 +    opener.location.href = opener.location;
261 +    window.close();
262 +}
263 +
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;
272 +
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 +    }
281 +
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;
327 +        }
328 +    }
329 +    return words.join(' ');
330 +}
331 +
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;
377 +        }
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++;
387 +        }
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;
466 +                }
467 +                selectOption(w.forms[i], target_field, options);
468 +            }
469 +            nd();
470 +            showComboBoxes();
471 +            break;
472 +        }
473 +    }
474 +    return false;
475 +}
476 +
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 +            }
490 +        }
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 +                    }
549 +                }
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 +                    }
562 +                }
563 +                return true;
564 +            }
565 +        }
566 +    }
567 +    target_field.selectedIndex = 0;
568 +}
569 +
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 +            }
593 +        }
594 +    }
595 +}
596 +
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;
604 +        }
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];
619 +        }
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++;
654 +        } else {
655 +            return elements[i];
656 +        }
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];
667 +        }
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;
679 +        }
680 +    }
681 +    return selected;
682 +}
683 +
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 +}
692 +
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 +}
701 +
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;
707 +        }
708 +    }
709 +    return false;
710 +}
711 +
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 +}
723 +
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 +}
745 +
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 +}
754 +
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;
781 +            }
782 +        }
783 +    }
784 +    if (toggle == 'off') {
785 +        toggle = 'on';
786 +    } else {
787 +        toggle = 'off';
788 +    }
789 +}
790 +
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] + '";');
801 +        }
802 +        return cookies;
803 +    }
804 +}
805 +
806 +function isElementVisible(element)
807 +{
808 +    if ((!element.style.display) || (element.style.display == getDisplayStyle())) {
809 +        return true;
810 +    } else {
811 +        return false;
812 +    }
813 +}
814 +
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 +}
851 +
852 +function changeVisibility(title, visibility, use_inline)
853 +{
854 +    var element = getPageElement(title);
855 +    if (visibility) {
856 +        var new_style = getDisplayStyle(use_inline);
857 +    } else {
858 +        var new_style = 'none';
859 +    }
860 +    element.style.display = new_style;
861 +}
862 +
863 +function getDisplayStyle(use_inline)
864 +{
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 +}
876 +
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)) + ',';
906 +    }
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 +        }
923 +    }
924 +}
925 +
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 +    }
936 +}
937 +
938 +function checkWindowClose(msg)
939 +{
940 +    if (msg == false) {
941 +        checkClose = false;
942 +    } else {
943 +        checkClose = true;
944 +        closeConfirmMessage = msg;
945 +    }
946 +}
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;
970 +    }
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 +    });
993 +});
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
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 +
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;
16904 +    }
16905 +
16906 +    url += "&reorder_neworder=" + neworder;
16907 +    
16908 +    window.location.href = url;
16909 +}
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 +    }
16917 +}
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);
16922 +}
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 +    }
16930 +}
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);
16939 +}
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);
16947 +}
16948 +function downloadCSV()
16949 +{
16950 +    var f = this.document.csv_form;
16951 +    f.submit();
16952 +    return false;
16953 +}
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');
17201 +    },
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 +    }
17209 +});
17210 +{/literal}
17211 +{/if}
17212 +</script>
17213 +{include file="app_info.tpl.html"}
17214 +{include file="footer.tpl.html"}
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
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 +}
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);
17268 +       }
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;
17347 +               }
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 1.334597 seconds and 3 git commands to generate.