1#!/usr/local/bin/perl -wT 2# This Source Code Form is subject to the terms of the Mozilla Public 3# License, v. 2.0. If a copy of the MPL was not distributed with this 4# file, You can obtain one at http://mozilla.org/MPL/2.0/. 5# 6# This Source Code Form is "Incompatible With Secondary Licenses", as 7# defined by the Mozilla Public License, v. 2.0. 8 9# Implementation notes for this file: 10# 11# 1) the 'id' form parameter is validated early on, and if it is not a valid 12# bugid an error will be reported, so it is OK for later code to simply check 13# for a defined form 'id' value, and it can assume a valid bugid. 14# 15# 2) If the 'id' form parameter is not defined (after the initial validation), 16# then we are processing multiple bugs, and @idlist will contain the ids. 17# 18# 3) If we are processing just the one id, then it is stored in @idlist for 19# later processing. 20 21use strict; 22 23use lib qw(. lib); 24 25use Bugzilla; 26use Bugzilla::Constants; 27use Bugzilla::Bug; 28use Bugzilla::User; 29use Bugzilla::Util; 30use Bugzilla::Error; 31use Bugzilla::Flag; 32use Bugzilla::Status; 33use Bugzilla::Token; 34 35use List::MoreUtils qw(firstidx); 36use Storable qw(dclone); 37 38my $user = Bugzilla->login(LOGIN_REQUIRED); 39 40my $cgi = Bugzilla->cgi; 41my $dbh = Bugzilla->dbh; 42my $template = Bugzilla->template; 43my $vars = {}; 44 45###################################################################### 46# Subroutines 47###################################################################### 48 49# Tells us whether or not a field should be changed by process_bug. 50sub should_set { 51 # check_defined is used for fields where there's another field 52 # whose name starts with "defined_" and then the field name--it's used 53 # to know when we did things like empty a multi-select or deselect 54 # a checkbox. 55 my ($field, $check_defined) = @_; 56 my $cgi = Bugzilla->cgi; 57 if ( defined $cgi->param($field) 58 || ($check_defined && defined $cgi->param("defined_$field")) ) 59 { 60 return 1; 61 } 62 return 0; 63} 64 65###################################################################### 66# Begin Data/Security Validation 67###################################################################### 68 69# Create a list of objects for all bugs being modified in this request. 70my @bug_objects; 71if (defined $cgi->param('id')) { 72 my $bug = Bugzilla::Bug->check_for_edit(scalar $cgi->param('id')); 73 $cgi->param('id', $bug->id); 74 push(@bug_objects, $bug); 75} else { 76 foreach my $i ($cgi->param()) { 77 if ($i =~ /^id_([1-9][0-9]*)/) { 78 my $id = $1; 79 push(@bug_objects, Bugzilla::Bug->check_for_edit($id)); 80 } 81 } 82} 83 84# Make sure there are bugs to process. 85scalar(@bug_objects) || ThrowUserError("no_bugs_chosen", {action => 'modify'}); 86 87my $first_bug = $bug_objects[0]; # Used when we're only updating a single bug. 88 89# Delete any parameter set to 'dontchange'. 90if (defined $cgi->param('dontchange')) { 91 foreach my $name ($cgi->param) { 92 next if $name eq 'dontchange'; # But don't delete dontchange itself! 93 # Skip ones we've already deleted (such as "defined_$name"). 94 next if !defined $cgi->param($name); 95 if ($cgi->param($name) eq $cgi->param('dontchange')) { 96 $cgi->delete($name); 97 $cgi->delete("defined_$name"); 98 } 99 } 100} 101 102# do a match on the fields if applicable 103Bugzilla::User::match_field({ 104 'qa_contact' => { 'type' => 'single' }, 105 'newcc' => { 'type' => 'multi' }, 106 'masscc' => { 'type' => 'multi' }, 107 'assigned_to' => { 'type' => 'single' }, 108}); 109 110print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL; 111 112# Check for a mid-air collision. Currently this only works when updating 113# an individual bug. 114my $delta_ts = $cgi->param('delta_ts') || ''; 115 116if ($delta_ts) { 117 my $delta_ts_z = datetime_from($delta_ts) 118 or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts }); 119 120 my $first_delta_tz_z = datetime_from($first_bug->delta_ts); 121 122 if ($first_delta_tz_z ne $delta_ts_z) { 123 ($vars->{'operations'}) = $first_bug->get_activity(undef, $delta_ts); 124 125 my $start_at = $cgi->param('longdesclength') 126 or ThrowCodeError('undefined_field', { field => 'longdesclength' }); 127 128 # Always sort midair collision comments oldest to newest, 129 # regardless of the user's personal preference. 130 my $comments = $first_bug->comments({ order => "oldest_to_newest" }); 131 132 # Show midair if previous changes made other than CC 133 # and/or one or more comments were made 134 my $do_midair = scalar @$comments > $start_at ? 1 : 0; 135 136 if (!$do_midair) { 137 foreach my $operation (@{ $vars->{'operations'} }) { 138 foreach my $change (@{ $operation->{'changes'} }) { 139 if ($change->{'fieldname'} ne 'cc') { 140 $do_midair = 1; 141 last; 142 } 143 } 144 last if $do_midair; 145 } 146 } 147 148 if ($do_midair) { 149 $vars->{'title_tag'} = "mid_air"; 150 $vars->{'start_at'} = $start_at; 151 $vars->{'comments'} = $comments; 152 $vars->{'bug'} = $first_bug; 153 # The token contains the old delta_ts. We need a new one. 154 $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts])); 155 156 # Warn the user about the mid-air collision and ask them what to do. 157 $template->process("bug/process/midair.html.tmpl", $vars) 158 || ThrowTemplateError($template->error()); 159 exit; 160 } 161 } 162} 163 164# We couldn't do this check earlier as we first had to validate bug IDs 165# and display the mid-air collision page if delta_ts changed. 166# If we do a mass-change, we use session tokens. 167my $token = $cgi->param('token'); 168 169if ($cgi->param('id')) { 170 check_hash_token($token, [$first_bug->id, $delta_ts || $first_bug->delta_ts]); 171} 172else { 173 check_token_data($token, 'buglist_mass_change', 'query.cgi'); 174} 175 176###################################################################### 177# End Data/Security Validation 178###################################################################### 179 180$vars->{'title_tag'} = "bug_processed"; 181 182my $action; 183if (defined $cgi->param('id')) { 184 $action = $user->setting('post_bug_submit_action'); 185 186 if ($action eq 'next_bug') { 187 my $bug_list_obj = $user->recent_search_for($first_bug); 188 my @bug_list = $bug_list_obj ? @{$bug_list_obj->bug_list} : (); 189 my $cur = firstidx { $_ eq $cgi->param('id') } @bug_list; 190 if ($cur >= 0 && $cur < $#bug_list) { 191 my $next_bug_id = $bug_list[$cur + 1]; 192 detaint_natural($next_bug_id); 193 if ($next_bug_id and $user->can_see_bug($next_bug_id)) { 194 # We create an object here so that $bug->send_changes can use it 195 # when displaying the header. 196 $vars->{'bug'} = new Bugzilla::Bug($next_bug_id); 197 } 198 } 199 } 200 # Include both action = 'same_bug' and 'nothing'. 201 else { 202 $vars->{'bug'} = $first_bug; 203 } 204} 205else { 206 # param('id') is not defined when changing multiple bugs at once. 207 $action = 'nothing'; 208} 209 210# Component, target_milestone, and version are in here just in case 211# the 'product' field wasn't defined in the CGI. It doesn't hurt to set 212# them twice. 213my @set_fields = qw(op_sys rep_platform priority bug_severity 214 component target_milestone version 215 bug_file_loc status_whiteboard short_desc 216 deadline remaining_time estimated_time 217 work_time set_default_assignee set_default_qa_contact 218 cclist_accessible reporter_accessible 219 product confirm_product_change 220 bug_status resolution dup_id); 221push(@set_fields, 'assigned_to') if !$cgi->param('set_default_assignee'); 222push(@set_fields, 'qa_contact') if !$cgi->param('set_default_qa_contact'); 223my %field_translation = ( 224 bug_severity => 'severity', 225 rep_platform => 'platform', 226 short_desc => 'summary', 227 bug_file_loc => 'url', 228 set_default_assignee => 'reset_assigned_to', 229 set_default_qa_contact => 'reset_qa_contact', 230 confirm_product_change => 'product_change_confirmed', 231); 232 233my %set_all_fields = ( other_bugs => \@bug_objects ); 234foreach my $field_name (@set_fields) { 235 if (should_set($field_name, 1)) { 236 my $param_name = $field_translation{$field_name} || $field_name; 237 $set_all_fields{$param_name} = $cgi->param($field_name); 238 } 239} 240 241if (should_set('keywords')) { 242 my $action = $cgi->param('keywordaction') || ''; 243 # Backward-compatibility for Bugzilla 3.x and older. 244 $action = 'remove' if $action eq 'delete'; 245 $action = 'set' if $action eq 'makeexact'; 246 $set_all_fields{keywords}->{$action} = $cgi->param('keywords'); 247} 248if (should_set('comment')) { 249 $set_all_fields{comment} = { 250 body => scalar $cgi->param('comment'), 251 is_private => scalar $cgi->param('comment_is_private'), 252 }; 253} 254if (should_set('see_also')) { 255 $set_all_fields{'see_also'}->{add} = 256 [split(/[\s,]+/, $cgi->param('see_also'))]; 257} 258if (should_set('remove_see_also')) { 259 $set_all_fields{'see_also'}->{remove} = [$cgi->param('remove_see_also')]; 260} 261foreach my $dep_field (qw(dependson blocked)) { 262 if (should_set($dep_field)) { 263 if (my $dep_action = $cgi->param("${dep_field}_action")) { 264 $set_all_fields{$dep_field}->{$dep_action} = 265 [split(/[\s,]+/, $cgi->param($dep_field))]; 266 } 267 else { 268 $set_all_fields{$dep_field}->{set} = $cgi->param($dep_field); 269 } 270 } 271} 272# Formulate the CC data into two arrays of users involved in this CC change. 273if (defined $cgi->param('newcc') 274 or defined $cgi->param('addselfcc') 275 or defined $cgi->param('removecc') 276 or defined $cgi->param('masscc')) 277{ 278 my (@cc_add, @cc_remove); 279 # If masscc is defined, then we came from buglist and need to either add or 280 # remove cc's... otherwise, we came from show_bug and may need to do both. 281 if (defined $cgi->param('masscc')) { 282 if ($cgi->param('ccaction') eq 'add') { 283 @cc_add = $cgi->param('masscc'); 284 } elsif ($cgi->param('ccaction') eq 'remove') { 285 @cc_remove = $cgi->param('masscc'); 286 } 287 } else { 288 @cc_add = $cgi->param('newcc'); 289 push(@cc_add, $user) if $cgi->param('addselfcc'); 290 291 # We came from show_bug which uses a select box to determine what cc's 292 # need to be removed... 293 if ($cgi->param('removecc') && $cgi->param('cc')) { 294 @cc_remove = $cgi->param('cc'); 295 } 296 } 297 298 $set_all_fields{cc} = { add => \@cc_add, remove => \@cc_remove }; 299} 300 301# Fields that can only be set on one bug at a time. 302if (defined $cgi->param('id')) { 303 # Since aliases are unique (like bug numbers), they can only be changed 304 # for one bug at a time. 305 if (defined $cgi->param('alias')) { 306 $set_all_fields{alias} = $cgi->param('alias'); 307 } 308} 309 310my %is_private; 311foreach my $field (grep(/^defined_isprivate/, $cgi->param())) { 312 $field =~ /(\d+)$/; 313 my $comment_id = $1; 314 $is_private{$comment_id} = $cgi->param("isprivate_$comment_id"); 315} 316$set_all_fields{comment_is_private} = \%is_private; 317 318my @check_groups = $cgi->param('defined_groups'); 319my @set_groups = $cgi->param('groups'); 320my ($removed_groups) = diff_arrays(\@check_groups, \@set_groups); 321$set_all_fields{groups} = { add => \@set_groups, remove => $removed_groups }; 322 323my @custom_fields = Bugzilla->active_custom_fields; 324foreach my $field (@custom_fields) { 325 my $fname = $field->name; 326 if (should_set($fname, 1)) { 327 $set_all_fields{$fname} = [$cgi->param($fname)]; 328 } 329} 330 331# We are going to alter the list of removed groups, so we keep a copy here. 332my @unchecked_groups = @$removed_groups; 333foreach my $b (@bug_objects) { 334 # Don't blindly ask to remove unchecked groups available in the UI. 335 # A group can be already unchecked, and the user didn't try to remove it. 336 # In this case, we don't want remove_group() to complain. 337 my @remove_groups; 338 foreach my $g (@{$b->groups_in}) { 339 push(@remove_groups, $g->name) if grep { $_ eq $g->name } @unchecked_groups; 340 } 341 local $set_all_fields{groups}->{remove} = \@remove_groups; 342 $b->set_all(\%set_all_fields); 343} 344 345if (defined $cgi->param('id')) { 346 # Flags should be set AFTER the bug has been moved into another 347 # product/component. The structure of flags code doesn't currently 348 # allow them to be set using set_all. 349 my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi( 350 $first_bug, undef, $vars); 351 $first_bug->set_flags($flags, $new_flags); 352 353 # Tags can only be set to one bug at once. 354 if (should_set('tag')) { 355 my @new_tags = split(/[\s,]+/, $cgi->param('tag')); 356 my ($tags_removed, $tags_added) = diff_arrays($first_bug->tags, \@new_tags); 357 $first_bug->remove_tag($_) foreach @$tags_removed; 358 $first_bug->add_tag($_) foreach @$tags_added; 359 } 360} 361 362############################## 363# Do Actual Database Updates # 364############################## 365foreach my $bug (@bug_objects) { 366 my $changes = $bug->update(); 367 368 if ($changes->{'bug_status'}) { 369 my $new_status = $changes->{'bug_status'}->[1]; 370 # We may have zeroed the remaining time, if we moved into a closed 371 # status, so we should inform the user about that. 372 if (!is_open_state($new_status) && $changes->{'remaining_time'}) { 373 $vars->{'message'} = "remaining_time_zeroed" 374 if $user->is_timetracker; 375 } 376 } 377 378 $bug->send_changes($changes, $vars); 379} 380 381# Delete the session token used for the mass-change. 382delete_token($token) unless $cgi->param('id'); 383 384if (Bugzilla->usage_mode == USAGE_MODE_EMAIL) { 385 # Do nothing. 386} 387elsif ($action eq 'next_bug' or $action eq 'same_bug') { 388 my $bug = $vars->{'bug'}; 389 if ($bug and $user->can_see_bug($bug)) { 390 if ($action eq 'same_bug') { 391 # $bug->update() does not update the internal structure of 392 # the bug sufficiently to display the bug with the new values. 393 # (That is, if we just passed in the old Bug object, we'd get 394 # a lot of old values displayed.) 395 $bug = new Bugzilla::Bug($bug->id); 396 $vars->{'bug'} = $bug; 397 } 398 $vars->{'bugs'} = [$bug]; 399 if ($action eq 'next_bug') { 400 $vars->{'nextbug'} = $bug->id; 401 } 402 $template->process("bug/show.html.tmpl", $vars) 403 || ThrowTemplateError($template->error()); 404 exit; 405 } 406} elsif ($action ne 'nothing') { 407 ThrowCodeError("invalid_post_bug_submit_action"); 408} 409 410# End the response page. 411unless (Bugzilla->usage_mode == USAGE_MODE_EMAIL) { 412 $template->process("bug/navigate.html.tmpl", $vars) 413 || ThrowTemplateError($template->error()); 414 $template->process("global/footer.html.tmpl", $vars) 415 || ThrowTemplateError($template->error()); 416} 417 4181; 419