2 # MantisBT - A PHP based bugtracking system
4 # MantisBT is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 2 of the License, or
7 # (at your option) any later version.
9 # MantisBT is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with MantisBT. If not, see <http://www.gnu.org/licenses/>.
18 * Update bug data then redirect to the appropriate viewing page
21 * @copyright Copyright (C) 2000 - 2002 Kenzaburo Ito - kenito@300baud.org
22 * @copyright Copyright (C) 2002 - 2011 MantisBT Team - mantisbt-dev@lists.sourceforge.net
23 * @link http://www.mantisbt.org
26 * @uses access_api.php
27 * @uses authentication_api.php
29 * @uses bugnote_api.php
30 * @uses config_api.php
31 * @uses constant_inc.php
32 * @uses custom_field_api.php
38 * @uses helper_api.php
39 * @uses history_api.php
42 * @uses relationship_api.php
43 * @uses twitter_api.php
49 require_once( 'core.php' );
50 require_api( 'access_api.php' );
51 require_api( 'authentication_api.php' );
52 require_api( 'bug_api.php' );
53 require_api( 'bugnote_api.php' );
54 require_api( 'config_api.php' );
55 require_api( 'constant_inc.php' );
56 require_api( 'custom_field_api.php' );
57 require_api( 'email_api.php' );
58 require_api( 'error_api.php' );
59 require_api( 'event_api.php' );
60 require_api( 'form_api.php' );
61 require_api( 'gpc_api.php' );
62 require_api( 'helper_api.php' );
63 require_api( 'history_api.php' );
64 require_api( 'lang_api.php' );
65 require_api( 'print_api.php' );
66 require_api( 'relationship_api.php' );
67 require_api( 'twitter_api.php' );
69 form_security_validate( 'bug_update' );
71 $f_bug_id = gpc_get_int( 'bug_id' );
72 $t_existing_bug = bug_get( $f_bug_id, true );
74 if ( helper_get_current_project() !== $t_existing_bug->project_id
) {
75 $g_project_override = $t_existing_bug->project_id
;
78 # Ensure that the user has permission to update bugs. This check also factors
79 # in whether the user has permission to view private bugs. The
80 # $g_limit_reporters option is also taken into consideration.
81 access_ensure_bug_level( config_get( 'update_bug_threshold' ), $f_bug_id );
83 # Check if the bug is in a read-only state and whether the current user has
84 # permission to update read-only bugs.
85 if ( bug_is_readonly( $f_bug_id ) ) {
86 error_parameters( $f_bug_id );
87 trigger_error( ERROR_BUG_READ_ONLY_ACTION_DENIED
, ERROR
);
90 $t_updated_bug = clone $t_existing_bug;
92 $t_updated_bug->additional_information
= gpc_get_string( 'additional_information', $t_existing_bug->additional_information
);
93 $t_updated_bug->build
= gpc_get_string( 'build', $t_existing_bug->build
);
94 $t_updated_bug->category_id
= gpc_get_int( 'category_id', $t_existing_bug->category_id
);
95 $t_updated_bug->description
= gpc_get_string( 'description', $t_existing_bug->description
);
96 $t_due_date = gpc_get_string( 'due_date', null );
97 if ( $t_due_date !== null) {
98 if ( is_blank ( $t_due_date ) ) {
99 $t_updated_bug->due_date
= 1;
101 $t_updated_bug->due_date
= strtotime( $t_due_date );
104 $t_updated_bug->duplicate_id
= gpc_get_int( 'duplicate_id', 0 );
105 $t_updated_bug->eta
= gpc_get_int( 'eta', $t_existing_bug->eta
);
106 $t_updated_bug->fixed_in_version
= gpc_get_string( 'fixed_in_version', $t_existing_bug->fixed_in_version
);
107 $t_updated_bug->handler_id
= gpc_get_int( 'handler_id', $t_existing_bug->handler_id
);
108 $t_updated_bug->os
= gpc_get_string( 'os', $t_existing_bug->os
);
109 $t_updated_bug->os_build
= gpc_get_string( 'os_build', $t_existing_bug->os_build
);
110 $t_updated_bug->platform
= gpc_get_string( 'platform', $t_existing_bug->platform
);
111 $t_updated_bug->priority
= gpc_get_int( 'priority', $t_existing_bug->priority
);
112 $t_updated_bug->projection
= gpc_get_int( 'projection', $t_existing_bug->projection
);
113 $t_updated_bug->reporter_id
= gpc_get_int( 'reporter_id', $t_existing_bug->reporter_id
);
114 $t_updated_bug->reproducibility
= gpc_get_int( 'reproducibility', $t_existing_bug->reproducibility
);
115 $t_updated_bug->resolution
= gpc_get_int( 'resolution', $t_existing_bug->resolution
);
116 $t_updated_bug->severity
= gpc_get_int( 'severity', $t_existing_bug->severity
);
117 $t_updated_bug->status
= gpc_get_int( 'status', $t_existing_bug->status
);
118 $t_updated_bug->steps_to_reproduce
= gpc_get_string( 'steps_to_reproduce', $t_existing_bug->steps_to_reproduce
);
119 $t_updated_bug->summary
= gpc_get_string( 'summary', $t_existing_bug->summary
);
120 $t_updated_bug->target_version
= gpc_get_string( 'target_version', $t_existing_bug->target_version
);
121 $t_updated_bug->version
= gpc_get_string( 'version', $t_existing_bug->version
);
122 $t_updated_bug->view_state
= gpc_get_int( 'view_state', $t_existing_bug->view_state
);
124 $t_bug_note = new BugNoteData();
125 $t_bug_note->note
= gpc_get_string( 'bugnote_text', '' );
126 $t_bug_note->view_state
= gpc_get_bool( 'private', config_get( 'default_bugnote_view_status' ) == VS_PRIVATE
) ? VS_PRIVATE
: VS_PUBLIC
;
127 $t_bug_note->time_tracking
= gpc_get_string( 'time_tracking', '0:00' );
129 # Determine whether the new status will reopen, resolve or close the issue.
130 # Note that multiple resolved or closed states can exist and thus we need to
131 # look at a range of statuses when performing this check.
132 $t_resolved_status = config_get( 'bug_resolved_status_threshold' );
133 $t_closed_status = config_get( 'bug_closed_status_threshold' );
134 $t_resolve_issue = false;
135 $t_close_issue = false;
136 $t_reopen_issue = false;
137 if ( $t_existing_bug->status
< $t_resolved_status &&
138 $t_updated_bug->status
>= $t_resolved_status &&
139 $t_updated_bug->status
< $t_closed_status ) {
140 $t_resolve_issue = true;
141 } else if ( $t_existing_bug->status
< $t_closed_status &&
142 $t_updated_bug->status
>= $t_closed_status ) {
143 $t_close_issue = true;
144 } else if ( $t_existing_bug->status
>= $t_resolved_status &&
145 $t_updated_bug->status
<= config_get( 'bug_reopen_status' ) ) {
146 $t_reopen_issue = true;
149 # If resolving or closing, ensure that all dependant issues have been resolved.
150 if ( ( $t_resolve_issue ||
$t_close_issue ) &&
151 !relationship_can_resolve_bug( $f_bug_id ) ) {
152 trigger_error( ERROR_BUG_RESOLVE_DEPENDANTS_BLOCKING
, ERROR
);
155 # Validate any change to the status of the issue.
156 if ( $t_existing_bug->status
!== $t_updated_bug->status
) {
157 access_ensure_bug_level( config_get( 'update_bug_status_threshold' ), $f_bug_id );
158 if ( !bug_check_workflow( $t_existing_bug->status
, $t_updated_bug->status
) ) {
159 error_parameters( lang_get( 'status' ) );
160 trigger_error( ERROR_CUSTOM_FIELD_INVALID_VALUE
, ERROR
);
162 if ( !access_has_bug_level( access_get_status_threshold( $t_updated_bug->status
, $t_updated_bug->project_id
), $f_bug_id ) ) {
163 # The reporter may be allowed to close or reopen the issue regardless.
164 $t_can_bypass_status_access_thresholds = false;
165 if ( $t_close_issue &&
166 $t_existing_bug->status
>= $t_resolved_status &&
167 $t_existing_bug->reporter_id
=== auth_get_current_user_id() &&
168 config_get( 'allow_reporter_close' ) ) {
169 $t_can_bypass_status_access_thresholds = true;
170 } else if ( $t_reopen_issue &&
171 $t_existing_bug->status
< $t_closed_status &&
172 $t_existing_bug->reporter_id
=== auth_get_current_user_id() &&
173 config_get( 'allow_reporter_reopen' ) ) {
174 $t_can_bypass_status_access_thresholds = true;
176 if ( !$t_can_bypass_status_access_thresholds ) {
177 trigger_error( ERROR_ACCESS_DENIED
, ERROR
);
180 if( $t_reopen_issue ) {
181 # for everyone allowed to reopen an issue, set the reopen resolution
182 $t_updated_bug->resolution
= config_get( 'bug_reopen_resolution' );
186 # Validate any change to the handler of an issue.
187 $t_issue_is_sponsored = sponsorship_get_amount( sponsorship_get_all_ids( $f_bug_id ) ) > 0;
188 if ( $t_existing_bug->handler_id
!== $t_updated_bug->handler_id
) {
189 access_ensure_bug_level( config_get( 'update_bug_assign_threshold' ), $f_bug_id );
190 if ( $t_issue_is_sponsored && !access_has_bug_level( config_get( 'handle_sponsored_bugs_threshold' ), $f_bug_id ) ) {
191 trigger_error( ERROR_SPONSORSHIP_HANDLER_ACCESS_LEVEL_TOO_LOW
, ERROR
);
193 if ( $t_updated_bug->handler_id
!== NO_USER
) {
194 if ( !access_has_bug_level( config_get( 'handle_bug_threshold' ), $f_bug_id, $t_updated_bug->handler_id
) ) {
195 trigger_error( ERROR_HANDLER_ACCESS_TOO_LOW
, ERROR
);
197 if ( $t_issue_is_sponsored && !access_has_bug_level( config_get( 'assign_sponsored_bugs_threshold' ), $f_bug_id ) ) {
198 trigger_error( ERROR_SPONSORSHIP_ASSIGNER_ACCESS_LEVEL_TOO_LOW
, ERROR
);
203 # Check whether the category has been undefined when it's compulsory.
204 if ( $t_existing_bug->category_id
!== $t_updated_bug->category_id
) {
205 if ( $t_updated_bug->category_id
=== 0 &&
206 !config_get( 'allow_no_category' ) ) {
207 error_parameters( lang_get( 'category' ) );
208 trigger_error( ERROR_EMPTY_FIELD
, ERROR
);
212 # Don't allow resolutions denoting completion of issue to be used if the issue
213 # has yet to be resolved or closed.
214 if ( $t_existing_bug->resolution
!== $t_updated_bug->resolution
&&
215 $t_updated_bug->resolution
>= config_get( 'bug_resolution_fixed_threshold' ) &&
216 $t_updated_bug->status
< $t_resolved_status ) {
217 error_parameters( lang_get( 'resolution' ) );
218 trigger_error( ERROR_CUSTOM_FIELD_INVALID_VALUE
, ERROR
);
221 # Ensure that the user has permission to change the target version of the issue.
222 if ( $t_existing_bug->target_version
!== $t_updated_bug->target_version
) {
223 access_ensure_bug_level( config_get( 'roadmap_update_threshold' ), $f_bug_id );
226 # Ensure that the user has permission to change the view status of the issue.
227 if ( $t_existing_bug->view_state
!== $t_updated_bug->view_state
) {
228 access_ensure_bug_level( config_get( 'change_view_status_threshold' ), $f_bug_id );
231 # Determine the custom field "require check" to use for validating
232 # whether fields can be undefined during this bug update.
233 if ( $t_close_issue ) {
234 $t_cf_require_check = 'require_closed';
235 } else if ( $t_resolve_issue ) {
236 $t_cf_require_check = 'require_resolved';
238 $t_cf_require_check = 'require_update';
241 $t_related_custom_field_ids = custom_field_get_linked_ids( $t_existing_bug->project_id
);
242 $t_custom_fields_to_set = array();
243 foreach ( $t_related_custom_field_ids as $t_cf_id ) {
244 $t_cf_def = custom_field_get_definition( $t_cf_id );
246 if ( !gpc_isset_custom_field( $t_cf_id, $t_cf_def['type'] ) ) {
247 if ( $t_cf_def[$t_cf_require_check] ) {
248 # A value for the custom field was expected however
249 # no value was given by the user.
250 error_parameters( lang_get_defaulted( custom_field_get_field( $t_cf_id, 'name' ) ) );
251 trigger_error( ERROR_EMPTY_FIELD
, ERROR
);
253 # The custom field isn't compulsory and the user did
254 # not supply a value. Therefore we can just ignore this
255 # custom field completely (ie. don't attempt to update
261 if( !custom_field_has_write_access( $t_cf_id, $f_bug_id ) ) {
262 trigger_error( ERROR_ACCESS_DENIED
, ERROR
);
265 $t_new_custom_field_value = gpc_get_custom_field( "custom_field_$t_cf_id", $t_cf_def['type'], null );
266 $t_old_custom_field_value = custom_field_get_value( $t_cf_id, $f_bug_id );
268 # Validate the value of the field against current validation rules.
269 # This may cause an error if validation rules have recently been
270 # modified such that old values that were once OK are now considered
272 if ( !custom_field_validate( $t_cf_id, $t_new_custom_field_value ) ) {
273 error_parameters( lang_get_defaulted( custom_field_get_field( $t_cf_id, 'name' ) ) );
274 trigger_error( ERROR_CUSTOM_FIELD_INVALID_VALUE
, ERROR
);
277 # Remember the new custom field values so we can set them when updating
278 # the bug (done after all data passed to this update page has been
280 $t_custom_fields_to_set[] = array( 'id' => $t_cf_id, 'value' => $t_new_custom_field_value );
283 # Perform validation of the duplicate ID of the bug.
284 if ( $t_updated_bug->duplicate_id
!== 0 ) {
285 if ( $t_updated_bug->duplicate_id
=== $f_bug_id ) {
286 trigger_error( ERROR_BUG_DUPLICATE_SELF
, ERROR
);
288 bug_ensure_exists( $t_updated_bug->duplicate_id
);
289 if ( !access_has_bug_level( config_get( 'update_bug_threshold' ), $t_updated_bug->duplicate_id
) ) {
290 trigger_error( ERROR_RELATIONSHIP_ACCESS_LEVEL_TO_DEST_BUG_TOO_LOW
, ERROR
);
292 if ( relationship_exists( $f_bug_id, $t_updated_bug->duplicate_id
) ) {
293 trigger_error( ERROR_RELATIONSHIP_ALREADY_EXISTS
, ERROR
);
297 # Validate the new bug note (if any is provided).
298 if ( $t_bug_note->note ||
299 ( config_get( 'time_tracking_enabled' ) &&
300 helper_duration_to_minutes( $t_bug_note->time_tracking
) > 0 ) ) {
301 access_ensure_bug_level( config_get( 'add_bugnote_threshold' ), $f_bug_id );
302 if ( !$t_bug_note->note
&&
303 !config_get( 'time_tracking_without_note' ) ) {
304 error_parameters( lang_get( 'bugnote' ) );
305 trigger_error( ERROR_EMPTY_FIELD
, ERROR
);
307 if ( $t_bug_note->view_state
!== config_get( 'default_bugnote_view_status' ) ) {
308 access_ensure_bug_level( config_get( 'set_view_status_threshold' ), $f_bug_id );
312 # Handle the reassign on feedback feature. Note that this feature generally
313 # won't work very well with custom workflows as it makes a lot of assumptions
314 # that may not be true. It assumes you don't have any statuses in the workflow
315 # between 'bug_submit_status' and 'bug_feedback_status'. It assumes you only
316 # have one feedback, assigned and submitted status.
317 if ( $t_bug_note->note
&&
318 config_get( 'reassign_on_feedback' ) &&
319 $t_existing_bug->status
=== config_get( 'bug_feedback_status' ) &&
320 $t_updated_bug->status
!== $t_existing_bug->status
&&
321 $t_updated_bug->handler_id
!== auth_get_current_user_id() &&
322 $t_updated_bug->reporter_id
=== auth_get_current_user_id() ) {
323 if ( $t_updated_bug->handler_id
!== NO_USER
) {
324 $t_updated_bug->status
= config_get( 'bug_assigned_status' );
326 $t_updated_bug->status
= config_get( 'bug_submit_status' );
330 # Handle automatic assignment of issues.
331 if ( $t_existing_bug->handler_id
=== NO_USER
&&
332 $t_updated_bug->handler_id
!== NO_USER
&&
333 $t_updated_bug->status
< config_get( 'bug_assigned_status' ) &&
334 config_get( 'auto_set_status_to_assigned' ) ) {
335 $t_updated_bug->status
= config_get( 'bug_assigned_status' );
338 # Allow a custom function to validate the proposed bug updates. Note that
339 # custom functions are being deprecated in MantisBT. You should migrate to
340 # the new plugin system instead.
341 helper_call_custom_function( 'issue_update_validate', array( $f_bug_id, $t_updated_bug, $t_bug_note->note
) );
343 # Allow plugins to validate/modify the update prior to it being committed.
344 $t_updated_bug = event_signal( 'EVENT_UPDATE_BUG_DATA', $t_updated_bug, $t_existing_bug );
346 # Commit the bug updates to the database.
347 $t_text_field_update_required = ( $t_existing_bug->description
!== $t_updated_bug->description
) ||
348 ( $t_existing_bug->additional_information
!== $t_updated_bug->additional_information
) ||
349 ( $t_existing_bug->steps_to_reproduce
!== $t_updated_bug->steps_to_reproduce
);
350 $t_updated_bug->update( $t_text_field_update_required, true );
352 # Update custom field values.
353 foreach ( $t_custom_fields_to_set as $t_custom_field_to_set ) {
354 custom_field_set_value( $t_custom_field_to_set['id'], $f_bug_id, $t_custom_field_to_set['value'] );
357 # Add a bug note if there is one.
358 if ( $t_bug_note->note ||
helper_duration_to_minutes( $t_bug_note->time_tracking
) > 0 ) {
359 bugnote_add( $f_bug_id, $t_bug_note->note
, $t_bug_note->time_tracking
, $t_bug_note->view_state
== VS_PRIVATE
, 0, '', null, false );
362 # Add a duplicate relationship if requested.
363 if ( $t_updated_bug->duplicate_id
!== 0 ) {
364 relationship_add( $f_bug_id, $t_updated_bug->duplicate_id
, BUG_DUPLICATE
);
365 history_log_event_special( $f_bug_id, BUG_ADD_RELATIONSHIP
, BUG_DUPLICATE
, $t_updated_bug->duplicate_id
);
366 history_log_event_special( $t_updated_bug->duplicate_id
, BUG_ADD_RELATIONSHIP
, BUG_HAS_DUPLICATE
, $f_bug_id );
367 if ( user_exists( $t_existing_bug->reporter_id
) ) {
368 bug_monitor( $f_bug_id, $t_existing_bug->reporter_id
);
370 if ( user_exists ( $t_existing_bug->handler_id
) ) {
371 bug_monitor( $f_bug_id, $t_existing_bug->handler_id
);
373 bug_monitor_copy( $f_bug_id, $t_updated_bug->duplicate_id
);
376 event_signal( 'EVENT_UPDATE_BUG', array( $t_existing_bug, $t_updated_bug ) );
378 # Allow a custom function to respond to the modifications made to the bug. Note
379 # that custom functions are being deprecated in MantisBT. You should migrate to
380 # the new plugin system instead.
381 helper_call_custom_function( 'issue_update_notify', array( $f_bug_id ) );
383 # Send a notification of changes via email.
384 if ( $t_resolve_issue ) {
385 email_resolved( $f_bug_id );
386 email_relationship_child_resolved( $f_bug_id );
387 } else if ( $t_close_issue ) {
388 email_close( $f_bug_id );
389 email_relationship_child_closed( $f_bug_id );
390 } else if ( $t_reopen_issue ) {
391 email_reopen( $f_bug_id );
392 } else if ( $t_existing_bug->handler_id
=== NO_USER
&&
393 $t_updated_bug->handler_id
!== NO_USER
) {
394 email_assign( $f_bug_id );
395 } else if ( $t_existing_bug->status
!== $t_updated_bug->status
) {
396 $t_new_status_label = MantisEnum
::getLabel( config_get( 'status_enum_string' ), $t_updated_bug->status
);
397 $t_new_status_label = str_replace( ' ', '_', $t_new_status_label );
398 email_generic( $f_bug_id, $t_new_status_label, 'email_notification_title_for_status_bug_' . $t_new_status_label );
400 email_generic( $f_bug_id, 'updated', 'email_notification_title_for_action_bug_updated' );
403 # Twitter notification of bug update.
404 if ( $t_resolve_issue &&
405 $t_updated_bug->resolution
>= config_get( 'bug_resolution_fixed_threshold' ) &&
406 $t_updated_bug->resolution
< config_get( 'bug_resolution_not_fixed_threshold' ) ) {
407 twitter_issue_resolved( $f_bug_id );
410 form_security_purge( 'bug_update' );
412 print_successful_redirect_to_bug( $f_bug_id );