Beispiel #1
0
 /**
  * Update the issue associations
  *
  * @param int $issue_id issue to associate
  * @param array $associated_issues issue_id's to associate with
  */
 private function updateAssociatedIssuesRelations($issue_id, $associated_issues)
 {
     global $errors;
     // trim and remove empty values
     $associated_issues = array_filter(Misc::trim($associated_issues));
     // make sure all associated issues are valid (and in this project)
     foreach ($associated_issues as $i => $iss_id) {
         if ($iss_id == $issue_id) {
             // skip issue itself
             unset($associated_issues[$i]);
             continue;
         }
         if (!self::exists($iss_id, false)) {
             $error = ev_gettext('Issue #%s does not exist and was removed from the list of associated issues.', $iss_id);
             $errors['Associated Issues'][] = $error;
             unset($associated_issues[$i]);
         }
     }
     // this reindexes the array and removes duplicates filled by user
     $associated_issues = array_unique($associated_issues);
     $current = self::getDetails($issue_id);
     $association_diff = Misc::arrayDiff($current['associated_issues'], $associated_issues);
     if (!$association_diff) {
         // no diffs, return back
         return;
     }
     $usr_id = Auth::getUserID();
     // go through the new associations, if association already exists, skip it
     $associations_to_remove = $current['associated_issues'];
     if (count($associated_issues) > 0) {
         foreach ($associated_issues as $associated_id) {
             if (!in_array($associated_id, $current['associated_issues'])) {
                 self::addAssociation($issue_id, $associated_id, $usr_id);
             } else {
                 // already assigned, remove this user from list of issues to remove
                 unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
             }
         }
     }
     if ($associations_to_remove) {
         foreach ($associations_to_remove as $associated_id) {
             self::deleteAssociation($issue_id, $associated_id);
         }
     }
 }
Beispiel #2
0
 /**
  * Method used to update the details of a specific issue.
  *
  * @access  public
  * @param   integer $issue_id The issue ID
  * @return  integer 1 if the update worked, -1 or -2 otherwise
  */
 function update($issue_id)
 {
     global $HTTP_POST_VARS;
     $issue_id = Misc::escapeInteger($issue_id);
     $usr_id = Auth::getUserID();
     $prj_id = Issue::getProjectID($issue_id);
     // get all of the 'current' information of this issue
     $current = Issue::getDetails($issue_id);
     // update the issue associations
     $association_diff = Misc::arrayDiff($current['associated_issues'], @$HTTP_POST_VARS['associated_issues']);
     if (count($association_diff) > 0) {
         // go through the new assocations, if association already exists, skip it
         $associations_to_remove = $current['associated_issues'];
         if (count(@$HTTP_POST_VARS['associated_issues']) > 0) {
             foreach ($HTTP_POST_VARS['associated_issues'] as $index => $associated_id) {
                 if (!in_array($associated_id, $current['associated_issues'])) {
                     Issue::addAssociation($issue_id, $associated_id, $usr_id);
                 } else {
                     // already assigned, remove this user from list of users to remove
                     unset($associations_to_remove[array_search($associated_id, $associations_to_remove)]);
                 }
             }
         }
         if (count($associations_to_remove) > 0) {
             foreach ($associations_to_remove as $associated_id) {
                 Issue::deleteAssociation($issue_id, $associated_id);
             }
         }
     }
     if (!empty($HTTP_POST_VARS['expected_resolution_date']['Year']) && !empty($HTTP_POST_VARS['expected_resolution_date']['Month']) && !empty($HTTP_POST_VARS['expected_resolution_date']['Day'])) {
         $HTTP_POST_VARS['expected_resolution_date'] = sprintf('%s-%s-%s', $HTTP_POST_VARS['expected_resolution_date']['Year'], $HTTP_POST_VARS['expected_resolution_date']['Month'], $HTTP_POST_VARS['expected_resolution_date']['Day']);
     } else {
         $HTTP_POST_VARS['expected_resolution_date'] = '';
     }
     $assignments_changed = false;
     if (@$HTTP_POST_VARS["keep_assignments"] == "no") {
         // only change the issue-user associations if there really were any changes
         $old_assignees = array_merge($current['assigned_users'], $current['assigned_inactive_users']);
         if (!empty($HTTP_POST_VARS['assignments'])) {
             $new_assignees = @$HTTP_POST_VARS['assignments'];
         } else {
             $new_assignees = array();
         }
         $assignment_notifications = array();
         // remove people from the assignment list, if appropriate
         foreach ($old_assignees as $assignee) {
             if (!in_array($assignee, $new_assignees)) {
                 Issue::deleteUserAssociation($issue_id, $assignee);
                 $assignments_changed = true;
             }
         }
         // add people to the assignment list, if appropriate
         foreach ($new_assignees as $assignee) {
             if (!in_array($assignee, $old_assignees)) {
                 Issue::addUserAssociation($usr_id, $issue_id, $assignee);
                 Notification::subscribeUser($usr_id, $issue_id, $assignee, Notification::getDefaultActions(), TRUE);
                 $assignment_notifications[] = $assignee;
                 $assignments_changed = true;
             }
         }
         if (count($assignment_notifications) > 0) {
             Notification::notifyNewAssignment($assignment_notifications, $issue_id);
         }
     }
     if (empty($HTTP_POST_VARS["estimated_dev_time"])) {
         $HTTP_POST_VARS["estimated_dev_time"] = 0;
     }
     $stmt = "UPDATE\n                    " . APP_DEFAULT_DB . "." . APP_TABLE_PREFIX . "issue\n                 SET\n                    iss_updated_date='" . Date_API::getCurrentDateGMT() . "',\n                    iss_last_public_action_date='" . Date_API::getCurrentDateGMT() . "',\n                    iss_last_public_action_type='updated',";
     if (!empty($HTTP_POST_VARS["category"])) {
         $stmt .= "iss_prc_id=" . Misc::escapeInteger($HTTP_POST_VARS["category"]) . ",";
     }
     if (@$HTTP_POST_VARS["keep"] == "no") {
         $stmt .= "iss_pre_id=" . Misc::escapeInteger($HTTP_POST_VARS["release"]) . ",";
     }
     if (!empty($HTTP_POST_VARS['expected_resolution_date'])) {
         $stmt .= "iss_expected_resolution_date='" . Misc::escapeString($HTTP_POST_VARS['expected_resolution_date']) . "',";
     } else {
         $stmt .= "iss_expected_resolution_date=null,";
     }
     $stmt .= "\n                    iss_pre_id=" . Misc::escapeInteger($HTTP_POST_VARS["release"]) . ",\n                    iss_pri_id=" . Misc::escapeInteger($HTTP_POST_VARS["priority"]) . ",\n                    iss_sta_id=" . Misc::escapeInteger($HTTP_POST_VARS["status"]) . ",\n                    iss_res_id=" . Misc::escapeInteger($HTTP_POST_VARS["resolution"]) . ",\n                    iss_summary='" . Misc::escapeString($HTTP_POST_VARS["summary"]) . "',\n                    iss_description='" . Misc::escapeString($HTTP_POST_VARS["description"]) . "',\n                    iss_dev_time='" . Misc::escapeString($HTTP_POST_VARS["estimated_dev_time"]) . "',\n                    iss_percent_complete= '" . Misc::escapeString($HTTP_POST_VARS["percent_complete"]) . "',\n                    iss_trigger_reminders=" . Misc::escapeInteger($HTTP_POST_VARS["trigger_reminders"]) . ",\n                    iss_grp_id ='" . Misc::escapeInteger($HTTP_POST_VARS["group"]) . "'";
     if (isset($HTTP_POST_VARS['private'])) {
         $stmt .= ",\n                    iss_private = " . Misc::escapeInteger($HTTP_POST_VARS['private']);
     }
     $stmt .= "\n                 WHERE\n                    iss_id={$issue_id}";
     $res = $GLOBALS["db_api"]->dbh->query($stmt);
     if (PEAR::isError($res)) {
         Error_Handler::logError(array($res->getMessage(), $res->getDebugInfo()), __FILE__, __LINE__);
         return -1;
     } else {
         // add change to the history (only for changes on specific fields?)
         $updated_fields = array();
         if ($current["iss_expected_resolution_date"] != $HTTP_POST_VARS['expected_resolution_date']) {
             $updated_fields["Expected Resolution Date"] = History::formatChanges($current["iss_expected_resolution_date"], $HTTP_POST_VARS['expected_resolution_date']);
         }
         if ($current["iss_prc_id"] != $HTTP_POST_VARS["category"]) {
             $updated_fields["Category"] = History::formatChanges(Category::getTitle($current["iss_prc_id"]), Category::getTitle($HTTP_POST_VARS["category"]));
         }
         if ($current["iss_pre_id"] != $HTTP_POST_VARS["release"]) {
             $updated_fields["Release"] = History::formatChanges(Release::getTitle($current["iss_pre_id"]), Release::getTitle($HTTP_POST_VARS["release"]));
         }
         if ($current["iss_pri_id"] != $HTTP_POST_VARS["priority"]) {
             $updated_fields["Priority"] = History::formatChanges(Priority::getTitle($current["iss_pri_id"]), Priority::getTitle($HTTP_POST_VARS["priority"]));
             Workflow::handlePriorityChange($prj_id, $issue_id, $usr_id, $current, $HTTP_POST_VARS);
         }
         if ($current["iss_sta_id"] != $HTTP_POST_VARS["status"]) {
             // clear out the last-triggered-reminder flag when changing the status of an issue
             Reminder_Action::clearLastTriggered($issue_id);
             // if old status was closed and new status is not, clear closed data from issue.
             $old_status_details = Status::getDetails($current['iss_sta_id']);
             if ($old_status_details['sta_is_closed'] == 1) {
                 $new_status_details = Status::getDetails($HTTP_POST_VARS["status"]);
                 if ($new_status_details['sta_is_closed'] != 1) {
                     Issue::clearClosed($issue_id);
                 }
             }
             $updated_fields["Status"] = History::formatChanges(Status::getStatusTitle($current["iss_sta_id"]), Status::getStatusTitle($HTTP_POST_VARS["status"]));
         }
         if ($current["iss_res_id"] != $HTTP_POST_VARS["resolution"]) {
             $updated_fields["Resolution"] = History::formatChanges(Resolution::getTitle($current["iss_res_id"]), Resolution::getTitle($HTTP_POST_VARS["resolution"]));
         }
         if ($current["iss_dev_time"] != $HTTP_POST_VARS["estimated_dev_time"]) {
             $updated_fields["Estimated Dev. Time"] = History::formatChanges(Misc::getFormattedTime($current["iss_dev_time"] * 60), Misc::getFormattedTime($HTTP_POST_VARS["estimated_dev_time"] * 60));
         }
         if ($current["iss_summary"] != $HTTP_POST_VARS["summary"]) {
             $updated_fields["Summary"] = '';
         }
         if ($current["iss_description"] != $HTTP_POST_VARS["description"]) {
             $updated_fields["Description"] = '';
         }
         if (count($updated_fields) > 0) {
             // log the changes
             $changes = '';
             $i = 0;
             foreach ($updated_fields as $key => $value) {
                 if ($i > 0) {
                     $changes .= "; ";
                 }
                 if ($key != "Summary" && $key != "Description") {
                     $changes .= "{$key}: {$value}";
                 } else {
                     $changes .= "{$key}";
                 }
                 $i++;
             }
             History::add($issue_id, $usr_id, History::getTypeID('issue_updated'), "Issue updated ({$changes}) by " . User::getFullName($usr_id));
             // send notifications for the issue being updated
             Notification::notifyIssueUpdated($issue_id, $current, $HTTP_POST_VARS);
         }
         // record group change as a seperate change
         if ($current["iss_grp_id"] != $HTTP_POST_VARS["group"]) {
             History::add($issue_id, $usr_id, History::getTypeID('group_changed'), "Group changed (" . History::formatChanges(Group::getName($current["iss_grp_id"]), Group::getName($HTTP_POST_VARS["group"])) . ") by " . User::getFullName($usr_id));
         }
         // now update any duplicates, if any
         $update_dupe = array('Category', 'Release', 'Priority', 'Release', 'Resolution');
         // COMPAT: the following line requires PHP > 4.0.4
         $intersect = array_intersect($update_dupe, array_keys($updated_fields));
         if ($current["duplicates"] != '' && count($intersect) > 0) {
             Issue::updateDuplicates($issue_id);
         }
         // if there is customer integration, mark last customer action
         if (Customer::hasCustomerIntegration($prj_id) && User::getRoleByUser($usr_id, $prj_id) == User::getRoleID('Customer')) {
             Issue::recordLastCustomerAction($issue_id);
         }
         if ($assignments_changed) {
             // XXX: we may want to also send the email notification for those "new" assignees
             Workflow::handleAssignmentChange(Issue::getProjectID($issue_id), $issue_id, $usr_id, Issue::getDetails($issue_id), @$HTTP_POST_VARS['assignments'], false);
         }
         Workflow::handleIssueUpdated($prj_id, $issue_id, $usr_id, $current, $HTTP_POST_VARS);
         return 1;
     }
 }
 /**
  * Method used to send an IRC notification about changes in the assignment
  * list of an issue.
  *
  * @access  public
  * @param   integer $issue_id The issue ID
  * @param   integer $usr_id The person who is performing this change
  * @param   array $old The old issue assignment list
  * @param   array $new The new issue assignment list
  * @param   boolean $is_remote Whether this change was made remotely or not
  */
 function notifyIRCAssignmentChange($issue_id, $usr_id, $old, $new, $is_remote = FALSE)
 {
     // do not notify about clearing the assignment of an issue
     if (count($new) == 0) {
         return false;
     }
     // only notify on irc if the assignment is being done to more than one person,
     // or in the case of a one-person-assignment-change, if the person doing it
     // is different than the actual assignee
     if (count($new) == 1 && $new[0] == $usr_id) {
         return false;
     }
     $assign_diff = Misc::arrayDiff($old, $new);
     if (count($new) != count($old) || count($assign_diff) > 0) {
         $notice = "Issue #{$issue_id} ";
         if ($is_remote) {
             $notice .= "remotely ";
         }
         if (count($old) == 0) {
             $old_assignees = '[empty]';
         } else {
             $old_assignees = implode(', ', User::getFullName($old));
         }
         $notice .= "updated (Old Assignment: " . $old_assignees . "; New Assignment: " . implode(', ', User::getFullName($new)) . ")";
         Notification::notifyIRC(Issue::getProjectID($issue_id), $notice, $issue_id);
     }
 }
 /**
  * Method used to send a diff-style notification email to the issue
  * subscribers about updates to its attributes.
  *
  * @param   integer $issue_id The issue ID
  * @param   array $old The old issue details
  * @param   array $new The new issue details
  * @param   array $updated_custom_fields An array of the custom fields that were changed.
  */
 public static function notifyIssueUpdated($issue_id, $old, $new, $updated_custom_fields)
 {
     $prj_id = Issue::getProjectID($issue_id);
     $diffs = array();
     if (@$new['keep_assignments'] == 'no') {
         if (empty($new['assignments'])) {
             $new['assignments'] = array();
         }
         $assign_diff = Misc::arrayDiff($old['assigned_users'], $new['assignments']);
         if (count($assign_diff) > 0) {
             $diffs[] = '-' . ev_gettext('Assignment List') . ': ' . $old['assignments'];
             @($diffs[] = '+' . ev_gettext('Assignment List') . ': ' . implode(', ', User::getFullName($new['assignments'])));
         }
     }
     if (isset($new['expected_resolution_date']) && @$old['iss_expected_resolution_date'] != $new['expected_resolution_date']) {
         $diffs[] = '-' . ev_gettext('Expected Resolution Date') . ': ' . $old['iss_expected_resolution_date'];
         $diffs[] = '+' . ev_gettext('Expected Resolution Date') . ': ' . $new['expected_resolution_date'];
     }
     if (isset($new['category']) && $old['iss_prc_id'] != $new['category']) {
         $diffs[] = '-' . ev_gettext('Category') . ': ' . Category::getTitle($old['iss_prc_id']);
         $diffs[] = '+' . ev_gettext('Category') . ': ' . Category::getTitle($new['category']);
     }
     if (isset($new['release']) && $old['iss_pre_id'] != $new['release']) {
         $diffs[] = '-' . ev_gettext('Release') . ': ' . Release::getTitle($old['iss_pre_id']);
         $diffs[] = '+' . ev_gettext('Release') . ': ' . Release::getTitle($new['release']);
     }
     if (isset($new['priority']) && $old['iss_pri_id'] != $new['priority']) {
         $diffs[] = '-' . ev_gettext('Priority') . ': ' . Priority::getTitle($old['iss_pri_id']);
         $diffs[] = '+' . ev_gettext('Priority') . ': ' . Priority::getTitle($new['priority']);
     }
     if (isset($new['severity']) && $old['iss_sev_id'] != $new['severity']) {
         $diffs[] = '-' . ev_gettext('Severity') . ': ' . Severity::getTitle($old['iss_sev_id']);
         $diffs[] = '+' . ev_gettext('Severity') . ': ' . Severity::getTitle($new['severity']);
     }
     if (isset($new['status']) && $old['iss_sta_id'] != $new['status']) {
         $diffs[] = '-' . ev_gettext('Status') . ': ' . Status::getStatusTitle($old['iss_sta_id']);
         $diffs[] = '+' . ev_gettext('Status') . ': ' . Status::getStatusTitle($new['status']);
     }
     if (isset($new['resolution']) && $old['iss_res_id'] != $new['resolution']) {
         $diffs[] = '-' . ev_gettext('Resolution') . ': ' . Resolution::getTitle($old['iss_res_id']);
         $diffs[] = '+' . ev_gettext('Resolution') . ': ' . Resolution::getTitle($new['resolution']);
     }
     if (isset($new['estimated_dev_time']) && $old['iss_dev_time'] != $new['estimated_dev_time']) {
         $diffs[] = '-' . ev_gettext('Estimated Dev. Time') . ': ' . Misc::getFormattedTime($old['iss_dev_time'] * 60);
         $diffs[] = '+' . ev_gettext('Estimated Dev. Time') . ': ' . Misc::getFormattedTime($new['estimated_dev_time'] * 60);
     }
     if (isset($new['summary']) && $old['iss_summary'] != $new['summary']) {
         $diffs[] = '-' . ev_gettext('Summary') . ': ' . $old['iss_summary'];
         $diffs[] = '+' . ev_gettext('Summary') . ': ' . $new['summary'];
     }
     if (isset($new['percent_complete']) && $old['iss_original_percent_complete'] != $new['percent_complete']) {
         $diffs[] = '-' . ev_gettext('Percent complete') . ': ' . $old['iss_original_percent_complete'];
         $diffs[] = '+' . ev_gettext('Percent complete') . ': ' . $new['percent_complete'];
     }
     if (isset($new['description']) && $old['iss_description'] != $new['description']) {
         $old['iss_description'] = explode("\n", $old['iss_original_description']);
         $new['description'] = explode("\n", $new['description']);
         $diff = new Text_Diff($old['iss_description'], $new['description']);
         $renderer = new Text_Diff_Renderer_unified();
         $desc_diff = explode("\n", trim($renderer->render($diff)));
         $diffs[] = 'Description:';
         foreach ($desc_diff as $diff) {
             $diffs[] = $diff;
         }
     }
     $data = Issue::getDetails($issue_id);
     $data['diffs'] = implode("\n", $diffs);
     $data['updated_by'] = User::getFullName(Auth::getUserID());
     $all_emails = array();
     $role_emails = array(User::ROLE_VIEWER => array(), User::ROLE_REPORTER => array(), User::ROLE_CUSTOMER => array(), User::ROLE_USER => array(), User::ROLE_DEVELOPER => array(), User::ROLE_MANAGER => array(), User::ROLE_ADMINISTRATOR => array());
     $users = self::getUsersByIssue($issue_id, 'updated');
     foreach ($users as $user) {
         if (empty($user['sub_usr_id'])) {
             $email = $user['sub_email'];
             // non users are treated as "Viewers" for permission checks
             $role = User::ROLE_VIEWER;
         } else {
             $prefs = Prefs::get($user['sub_usr_id']);
             if (Auth::getUserID() == $user['sub_usr_id'] && (empty($prefs['receive_copy_of_own_action'][$prj_id]) || $prefs['receive_copy_of_own_action'][$prj_id] == false)) {
                 continue;
             }
             $email = User::getFromHeader($user['sub_usr_id']);
             $role = $user['pru_role'];
         }
         // now add it to the list of emails
         if (!empty($email) && !in_array($email, $all_emails)) {
             $all_emails[] = $email;
             $role_emails[$role][] = $email;
         }
     }
     // get additional email addresses to notify
     $additional_emails = Workflow::getAdditionalEmailAddresses($prj_id, $issue_id, 'issue_updated', array('old' => $old, 'new' => $new));
     $data['custom_field_diffs'] = implode("\n", Custom_Field::formatUpdatesToDiffs($updated_custom_fields, User::ROLE_VIEWER));
     foreach ($additional_emails as $email) {
         if (!in_array($email, $all_emails)) {
             $role_emails[User::ROLE_VIEWER][] = $email;
         }
     }
     // send email to each role separately due to custom field restrictions
     foreach ($role_emails as $role => $emails) {
         if (count($emails) > 0) {
             $data['custom_field_diffs'] = implode("\n", Custom_Field::formatUpdatesToDiffs($updated_custom_fields, $role));
             if (!empty($data['custom_field_diffs']) || !empty($data['diffs'])) {
                 self::notifySubscribers($issue_id, $emails, 'updated', $data, ev_gettext('Updated'), false);
             }
         }
     }
 }
 /**
  * Method used to send a diff-style notification email to the issue
  * subscribers about updates to its attributes.
  *
  * @param   integer $issue_id The issue ID
  * @param   array $old The old issue details
  * @param   array $new The new issue details
  */
 public static function notifyIssueUpdated($issue_id, $old, $new)
 {
     $prj_id = Issue::getProjectID($issue_id);
     $diffs = array();
     if (@$new['keep_assignments'] == 'no') {
         if (empty($new['assignments'])) {
             $new['assignments'] = array();
         }
         $assign_diff = Misc::arrayDiff($old['assigned_users'], $new['assignments']);
         if (count($assign_diff) > 0) {
             $diffs[] = '-' . ev_gettext('Assignment List') . ': ' . $old['assignments'];
             @($diffs[] = '+' . ev_gettext('Assignment List') . ': ' . implode(', ', User::getFullName($new['assignments'])));
         }
     }
     if (isset($new['expected_resolution_date']) && @$old['iss_expected_resolution_date'] != $new['expected_resolution_date']) {
         $diffs[] = '-' . ev_gettext('Expected Resolution Date') . ': ' . $old['iss_expected_resolution_date'];
         $diffs[] = '+' . ev_gettext('Expected Resolution Date') . ': ' . $new['expected_resolution_date'];
     }
     if (isset($new['category']) && $old['iss_prc_id'] != $new['category']) {
         $diffs[] = '-' . ev_gettext('Category') . ': ' . Category::getTitle($old['iss_prc_id']);
         $diffs[] = '+' . ev_gettext('Category') . ': ' . Category::getTitle($new['category']);
     }
     if (isset($new['release']) && $old['iss_pre_id'] != $new['release']) {
         $diffs[] = '-' . ev_gettext('Release') . ': ' . Release::getTitle($old['iss_pre_id']);
         $diffs[] = '+' . ev_gettext('Release') . ': ' . Release::getTitle($new['release']);
     }
     if (isset($new['priority']) && $old['iss_pri_id'] != $new['priority']) {
         $diffs[] = '-' . ev_gettext('Priority') . ': ' . Priority::getTitle($old['iss_pri_id']);
         $diffs[] = '+' . ev_gettext('Priority') . ': ' . Priority::getTitle($new['priority']);
     }
     if (isset($new['severity']) && $old['iss_sev_id'] != $new['severity']) {
         $diffs[] = '-' . ev_gettext('Severity') . ': ' . Severity::getTitle($old['iss_sev_id']);
         $diffs[] = '+' . ev_gettext('Severity') . ': ' . Severity::getTitle($new['severity']);
     }
     if (isset($new['status']) && $old['iss_sta_id'] != $new['status']) {
         $diffs[] = '-' . ev_gettext('Status') . ': ' . Status::getStatusTitle($old['iss_sta_id']);
         $diffs[] = '+' . ev_gettext('Status') . ': ' . Status::getStatusTitle($new['status']);
     }
     if (isset($new['resolution']) && $old['iss_res_id'] != $new['resolution']) {
         $diffs[] = '-' . ev_gettext('Resolution') . ': ' . Resolution::getTitle($old['iss_res_id']);
         $diffs[] = '+' . ev_gettext('Resolution') . ': ' . Resolution::getTitle($new['resolution']);
     }
     if (isset($new['estimated_dev_time']) && $old['iss_dev_time'] != $new['estimated_dev_time']) {
         $diffs[] = '-' . ev_gettext('Estimated Dev. Time') . ': ' . Misc::getFormattedTime($old['iss_dev_time'] * 60);
         $diffs[] = '+' . ev_gettext('Estimated Dev. Time') . ': ' . Misc::getFormattedTime($new['estimated_dev_time'] * 60);
     }
     if (isset($new['summary']) && $old['iss_summary'] != $new['summary']) {
         $diffs[] = '-' . ev_gettext('Summary') . ': ' . $old['iss_summary'];
         $diffs[] = '+' . ev_gettext('Summary') . ': ' . $new['summary'];
     }
     if (isset($new['percent_complete']) && $old['iss_original_percent_complete'] != $new['percent_complete']) {
         $diffs[] = '-' . ev_gettext('Percent complete') . ': ' . $old['iss_original_percent_complete'];
         $diffs[] = '+' . ev_gettext('Percent complete') . ': ' . $new['percent_complete'];
     }
     if (isset($new['description']) && $old['iss_description'] != $new['description']) {
         $old['iss_description'] = explode("\n", $old['iss_original_description']);
         $new['description'] = explode("\n", $new['description']);
         $diff = new Text_Diff($old['iss_description'], $new['description']);
         $renderer = new Text_Diff_Renderer_unified();
         $desc_diff = explode("\n", trim($renderer->render($diff)));
         $diffs[] = 'Description:';
         foreach ($desc_diff as $diff) {
             $diffs[] = $diff;
         }
     }
     $emails = array();
     $users = self::getUsersByIssue($issue_id, 'updated');
     $user_emails = Project::getUserEmailAssocList(Issue::getProjectID($issue_id), 'active', User::getRoleID('Customer'));
     // FIXME: $user_emails unused
     $user_emails = array_map(function ($s) {
         return strtolower($s);
     }, $user_emails);
     foreach ($users as $user) {
         if (empty($user['sub_usr_id'])) {
             $email = $user['sub_email'];
         } else {
             $prefs = Prefs::get($user['sub_usr_id']);
             if (Auth::getUserID() == $user['sub_usr_id'] && (empty($prefs['receive_copy_of_own_action'][$prj_id]) || $prefs['receive_copy_of_own_action'][$prj_id] == false)) {
                 continue;
             }
             $email = User::getFromHeader($user['sub_usr_id']);
         }
         // now add it to the list of emails
         if (!empty($email) && !in_array($email, $emails)) {
             $emails[] = $email;
         }
     }
     // get additional email addresses to notify
     $emails = array_merge($emails, Workflow::getAdditionalEmailAddresses($prj_id, $issue_id, 'issue_updated', array('old' => $old, 'new' => $new)));
     $data = Issue::getDetails($issue_id);
     $data['diffs'] = implode("\n", $diffs);
     $data['updated_by'] = User::getFullName(Auth::getUserID());
     self::notifySubscribers($issue_id, $emails, 'updated', $data, ev_gettext('Updated'), false);
 }