process_bug.cgi 82.3 KB
Newer Older
1
#!/usr/bin/perl -wT
2
# -*- Mode: perl; indent-tabs-mode: nil -*-
terry%netscape.com's avatar
terry%netscape.com committed
3
#
4 5 6 7 8 9 10 11 12 13
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
terry%netscape.com's avatar
terry%netscape.com committed
14
# The Original Code is the Bugzilla Bug Tracking System.
15
#
terry%netscape.com's avatar
terry%netscape.com committed
16
# The Initial Developer of the Original Code is Netscape Communications
17 18 19 20
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
terry%netscape.com's avatar
terry%netscape.com committed
21
# Contributor(s): Terry Weissman <terry@mozilla.org>
22
#                 Dan Mosedale <dmose@mozilla.org>
23
#                 Dave Miller <justdave@syndicomm.com>
24
#                 Christopher Aillon <christopher@aillon.com>
25
#                 Myk Melez <myk@mozilla.org>
26
#                 Jeff Hedlund <jeff.hedlund@matrixsi.com>
27
#                 Frédéric Buclin <LpSolit@gmail.com>
28
#                 Lance Larsh <lance.larsh@oracle.com>
terry%netscape.com's avatar
terry%netscape.com committed
29

30 31 32 33 34 35 36 37 38 39 40 41
# Implementation notes for this file:
#
# 1) the 'id' form parameter is validated early on, and if it is not a valid
# bugid an error will be reported, so it is OK for later code to simply check
# for a defined form 'id' value, and it can assume a valid bugid.
#
# 2) If the 'id' form parameter is not defined (after the initial validation),
# then we are processing multiple bugs, and @idlist will contain the ids.
#
# 3) If we are processing just the one id, then it is stored in @idlist for
# later processing.

42 43
use strict;

44 45
use lib qw(.);

46
use Bugzilla;
47
use Bugzilla::Constants;
48
use Bugzilla::Bug;
49
use Bugzilla::Mailer;
50
use Bugzilla::User;
51
use Bugzilla::Util;
52
use Bugzilla::Error;
53
use Bugzilla::Field;
54
use Bugzilla::Product;
55
use Bugzilla::Component;
56
use Bugzilla::Keyword;
57

58 59
# Use the Flag module to modify flag data if the user set flags.
use Bugzilla::Flag;
60
use Bugzilla::FlagType;
61

62
my $user = Bugzilla->login(LOGIN_REQUIRED);
63
local our $whoid = $user->id;
64
my $grouplist = $user->groups_as_string;
65

66
my $cgi = Bugzilla->cgi;
67
my $dbh = Bugzilla->dbh;
68
my $template = Bugzilla->template;
69
local our $vars = {};
70
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
71

72 73
my @editable_bug_fields = editable_bug_fields();

74
my $requiremilestone = 0;
75
my $PrivilegesRequired = 0;
76

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
######################################################################
# Subroutines
######################################################################

sub BugInGroupId {
    my ($bug_id, $group_id) = @_;
    detaint_natural($bug_id);
    detaint_natural($group_id);
    my ($in_group) = Bugzilla->dbh->selectrow_array(
        "SELECT CASE WHEN bug_id != 0 THEN 1 ELSE 0 END
           FROM bug_group_map
          WHERE bug_id = ? AND group_id = ?", undef, ($bug_id, $group_id));
    return $in_group;
}

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
# This function checks if there are any default groups defined.
# If so, then groups may have to be changed when bugs move from
# one bug to another.
sub AnyDefaultGroups {
    my $dbh = Bugzilla->dbh;
    my $any_default =
        $dbh->selectrow_array('SELECT 1
                                 FROM group_control_map
                           INNER JOIN groups
                                   ON groups.id = group_control_map.group_id
                                WHERE isactive != 0
                                  AND (membercontrol = ? OR othercontrol = ?) ' .
                                 $dbh->sql_limit(1),
                                 undef, (CONTROLMAPDEFAULT, CONTROLMAPDEFAULT));
    return $any_default;
}

109 110 111 112 113 114 115 116
######################################################################
# Begin Data/Security Validation
######################################################################

# Create a list of IDs of all bugs being modified in this request.
# This list will either consist of a single bug number from the "id"
# form/URL field or a series of numbers from multiple form/URL fields
# named "id_x" where "x" is the bug number.
117 118
# For each bug being modified, make sure its ID is a valid bug number 
# representing an existing bug that the user is authorized to access.
119
my @idlist;
120 121 122 123 124 125 126 127
if (defined $cgi->param('id')) {
  my $id = $cgi->param('id');
  ValidateBugID($id);

  # Store the validated, and detainted id back in the cgi data, as
  # lots of later code will need it, and will obtain it from there
  $cgi->param('id', $id);
  push @idlist, $id;
128
} else {
129
    foreach my $i ($cgi->param()) {
130
        if ($i =~ /^id_([1-9][0-9]*)/) {
131 132 133
            my $id = $1;
            ValidateBugID($id);
            push @idlist, $id;
134
        }
135 136 137
    }
}

138
# Make sure there are bugs to process.
139
scalar(@idlist) || ThrowUserError("no_bugs_chosen");
140

141 142 143 144 145
# Build a bug object using $cgi->param('id') as ID.
# If there are more than one bug changed at once, the bug object will be
# empty, which doesn't matter.
my $bug = new Bugzilla::Bug(scalar $cgi->param('id'), $whoid);

146 147
# Make sure form param 'dontchange' is defined so it can be compared to easily.
$cgi->param('dontchange','') unless defined $cgi->param('dontchange');
148

149 150 151
# Make sure the 'knob' param is defined; else set it to 'none'.
$cgi->param('knob', 'none') unless defined $cgi->param('knob');

152 153
# Validate all timetracking fields
foreach my $field ("estimated_time", "work_time", "remaining_time") {
154 155 156
    if (defined $cgi->param($field)) {
        my $er_time = trim($cgi->param($field));
        if ($er_time ne $cgi->param('dontchange')) {
157 158 159 160 161
            Bugzilla::Bug::ValidateTime($er_time, $field);
        }
    }
}

162
if (UserInGroup(Bugzilla->params->{'timetrackinggroup'})) {
163 164
    my $wk_time = $cgi->param('work_time');
    if ($cgi->param('comment') =~ /^\s*$/ && $wk_time && $wk_time != 0) {
165
        ThrowUserError('comment_required');
166
    }
167 168
}

169
ValidateComment(scalar $cgi->param('comment'));
170

171 172 173 174 175 176 177
# If the bug(s) being modified have dependencies, validate them
# and rebuild the list with the validated values.  This is important
# because there are situations where validation changes the value
# instead of throwing an error, f.e. when one or more of the values
# is a bug alias that gets converted to its corresponding bug ID
# during validation.
foreach my $field ("dependson", "blocked") {
178 179 180
    if ($cgi->param('id')) {
        my @old = @{$bug->$field};
        my @new;
181
        foreach my $id (split(/[\s,]+/, $cgi->param($field))) {
182
            next unless $id;
183
            ValidateBugID($id, $field);
184
            push @new, $id;
185
        }
186 187 188 189 190 191
        $cgi->param($field, join(",", @new));
        my ($added, $removed) = Bugzilla::Util::diff_arrays(\@old, \@new);
        foreach my $id (@$added , @$removed) {
            # ValidateBugID is called without $field here so that it will
            # throw an error if any of the changed bugs are not visible.
            ValidateBugID($id);
192
            if (Bugzilla->params->{"strict_isolation"}) {
193
                my $deltabug = new Bugzilla::Bug($id, $user->id);
194 195 196 197 198
                if (!$user->can_edit_product($deltabug->{'product_id'})) {
                    $vars->{'field'} = $field;
                    ThrowUserError("illegal_change_deps", $vars);
                }
            }
199
        }
200 201 202
        if ((@$added || @$removed)
            && !$bug->check_can_change_field($field, 0, 1, \$PrivilegesRequired))
        {
203 204 205 206
            $vars->{'privs'} = $PrivilegesRequired;
            $vars->{'field'} = $field;
            ThrowUserError("illegal_change", $vars);
        }
207 208 209 210 211
    } else {
        # Bugzilla does not support mass-change of dependencies so they
        # are not validated.  To prevent a URL-hacking risk, the dependencies
        # are deleted for mass-changes.
        $cgi->delete($field);
212 213 214
    }
}

215 216 217 218 219
# do a match on the fields if applicable

# The order of these function calls is important, as both Flag::validate
# and FlagType::validate assume User::match_field has ensured that the values
# in the requestee fields are legitimate user email addresses.
220
&Bugzilla::User::match_field($cgi, {
221 222 223 224
    'qa_contact'                => { 'type' => 'single' },
    'newcc'                     => { 'type' => 'multi'  },
    'masscc'                    => { 'type' => 'multi'  },
    'assigned_to'               => { 'type' => 'single' },
225
    '^requestee(_type)?-(\d+)$' => { 'type' => 'multi'  },
226
});
227 228 229 230 231

# Validate flags in all cases. validate() should not detect any
# reference to flags if $cgi->param('id') is undefined.
Bugzilla::Flag::validate($cgi, $cgi->param('id'));
Bugzilla::FlagType::validate($cgi, $cgi->param('id'));
232

233 234 235 236
######################################################################
# End Data/Security Validation
######################################################################

237
print $cgi->header();
238
$vars->{'title_tag'} = "bug_processed";
239 240 241 242

# Set the title if we can see a mid-air coming. This test may have false
# negatives, but never false positives, and should catch the majority of cases.
# It only works at all in the single bug case.
243
if (defined $cgi->param('id')) {
244 245 246
    my $delta_ts = $dbh->selectrow_array(
        q{SELECT delta_ts FROM bugs WHERE bug_id = ?},
        undef, $cgi->param('id'));
247
    
248
    if (defined $cgi->param('delta_ts') && $cgi->param('delta_ts') ne $delta_ts)
249
    {
250
        $vars->{'title_tag'} = "mid_air";
251 252
    }
}
253

254
# Set up the vars for navigational <link> elements
255
my @bug_list;
256
if ($cgi->cookie("BUGLIST") && defined $cgi->param('id')) {
257
    @bug_list = split(/:/, $cgi->cookie("BUGLIST"));
258 259 260
    $vars->{'bug_list'} = \@bug_list;
}

261 262 263 264
foreach my $field_name ('product', 'component', 'version') {
    defined($cgi->param($field_name))
      || ThrowCodeError('undefined_field', { field => $field_name });
}
265

266 267
# This function checks if there is a comment required for a specific
# function and tests, if the comment was given.
268
# If comments are required for functions is defined by params.
269
#
270
sub CheckonComment {
271
    my ($function) = (@_);
272
    my $cgi = Bugzilla->cgi;
273 274
    
    # Param is 1 if comment should be added !
275
    my $ret = Bugzilla->params->{ "commenton" . $function };
276 277 278 279 280

    # Allow without comment in case of undefined Params.
    $ret = 0 unless ( defined( $ret ));

    if( $ret ) {
281 282
        if (!defined $cgi->param('comment')
            || $cgi->param('comment') =~ /^\s*$/) {
283
            # No comment - sorry, action not allowed !
284
            ThrowUserError("comment_required");
285 286 287 288 289 290 291
        } else {
            $ret = 0;
        }
    }
    return( ! $ret ); # Return val has to be inverted
}

292 293 294 295 296
# Figure out whether or not the user is trying to change the product
# (either the "product" variable is not set to "don't change" or the
# user is changing a single bug and has changed the bug's product),
# and make the user verify the version, component, target milestone,
# and bug groups if so.
297 298
my $oldproduct = '';
if (defined $cgi->param('id')) {
299 300 301 302
    $oldproduct = $dbh->selectrow_array(
        q{SELECT name FROM products INNER JOIN bugs
        ON products.id = bugs.product_id WHERE bug_id = ?},
        undef, $cgi->param('id'));
303
}
304 305 306 307

if (((defined $cgi->param('id') && $cgi->param('product') ne $oldproduct) 
     || (!$cgi->param('id')
         && $cgi->param('product') ne $cgi->param('dontchange')))
308 309
    && CheckonComment( "reassignbycomponent" ))
{
310
    # Check to make sure they actually have the right to change the product
311 312 313
    if (!$bug->check_can_change_field('product', $oldproduct, $cgi->param('product'),
                                      \$PrivilegesRequired))
    {
314 315
        $vars->{'oldvalue'} = $oldproduct;
        $vars->{'newvalue'} = $cgi->param('product');
316
        $vars->{'field'} = 'product';
317
        $vars->{'privs'} = $PrivilegesRequired;
318
        ThrowUserError("illegal_change", $vars);
319
    }
320

321
    my $prod = $cgi->param('product');
322
    my $prod_obj = new Bugzilla::Product({name => $prod});
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
    trick_taint($prod);

    # If at least one bug does not belong to the product we are
    # moving to, we have to check whether or not the user is
    # allowed to enter bugs into that product.
    # Note that this check must be done early to avoid the leakage
    # of component, version and target milestone names.
    my $check_can_enter =
        $dbh->selectrow_array("SELECT 1 FROM bugs
                               INNER JOIN products
                               ON bugs.product_id = products.id
                               WHERE products.name != ?
                               AND bugs.bug_id IN
                               (" . join(',', @idlist) . ") " .
                               $dbh->sql_limit(1),
                               undef, $prod);

340
    if ($check_can_enter) { $user->can_enter_product($prod, 1) }
341 342 343 344 345 346 347 348 349

    # note that when this script is called from buglist.cgi (rather
    # than show_bug.cgi), it's possible that the product will be changed
    # but that the version and/or component will be set to 
    # "--dont_change--" but still happen to be correct.  in this case,
    # the if statement will incorrectly trigger anyway.  this is a 
    # pretty weird case, and not terribly unreasonable behavior, but 
    # worthy of a comment, perhaps.
    #
350
    my @version_names = map($_->name, @{$prod_obj->versions});
351
    my @component_names = map($_->name, @{$prod_obj->components});
352
    my $vok = lsearch(\@version_names, $cgi->param('version')) >= 0;
353
    my $cok = lsearch(\@component_names, $cgi->param('component')) >= 0;
354 355

    my $mok = 1;   # so it won't affect the 'if' statement if milestones aren't used
356
    my @milestone_names = ();
357
    if ( Bugzilla->params->{"usetargetmilestone"} ) {
358 359 360
       defined($cgi->param('target_milestone'))
         || ThrowCodeError('undefined_field', { field => 'target_milestone' });

361 362
       @milestone_names = map($_->name, @{$prod_obj->milestones});
       $mok = lsearch(\@milestone_names, $cgi->param('target_milestone')) >= 0;
363 364
    }

365 366 367
    # If the product-specific fields need to be verified, or we need to verify
    # whether or not to add the bugs to their new product's group, display
    # a verification form.
368 369
    if (!$vok || !$cok || !$mok || (AnyDefaultGroups()
        && !defined $cgi->param('addtonewgroup'))) {
370
        
371
        if (!$vok || !$cok || !$mok) {
372
            $vars->{'verify_fields'} = 1;
373 374
            my %defaults;
            # We set the defaults to these fields to the old value,
375
            # if it's a valid option, otherwise we use the default where
376
            # that's appropriate
377
            $vars->{'versions'} = \@version_names;
378
            if ($vok) {
379
                $defaults{'version'} = $cgi->param('version');
380
            }
381
            $vars->{'components'} = \@component_names;
382
            if ($cok) {
383
                $defaults{'component'} = $cgi->param('component');
384
            }
385
            if (Bugzilla->params->{"usetargetmilestone"}) {
386
                $vars->{'use_target_milestone'} = 1;
387
                $vars->{'milestones'} = \@milestone_names;
388
                if ($mok) {
389
                    $defaults{'target_milestone'} = $cgi->param('target_milestone');
390
                } else {
391 392 393 394
                    $defaults{'target_milestone'} = $dbh->selectrow_array(
                        q{SELECT defaultmilestone FROM products 
                        WHERE name = ?}, undef, $prod);
;
395
                }
396
            }
397 398
            else {
                $vars->{'use_target_milestone'} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
399
            }
400
            $vars->{'defaults'} = \%defaults;
401
        }
402
        else {
403
            $vars->{'verify_fields'} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
404
        }
405
        
406
        $vars->{'verify_bug_group'} = (AnyDefaultGroups() 
407
                                       && !defined $cgi->param('addtonewgroup'));
408
        
409
        $template->process("bug/process/verify-new-product.html.tmpl", $vars)
410
          || ThrowTemplateError($template->error());
411
        exit;
terry%netscape.com's avatar
terry%netscape.com committed
412 413 414
    }
}

415 416
# Confirm that the reporter of the current bug can access the bug we are duping to.
sub DuplicateUserConfirm {
417 418 419 420
    my $cgi = Bugzilla->cgi;
    my $dbh = Bugzilla->dbh;
    my $template = Bugzilla->template;

421
    # if we've already been through here, then exit
422
    if (defined $cgi->param('confirm_add_duplicate')) {
423 424 425
        return;
    }

426 427 428 429
    # Remember that we validated both these ids earlier, so we know
    # they are both valid bug ids
    my $dupe = $cgi->param('id');
    my $original = $cgi->param('dup_id');
430
    
431 432
    my $reporter = $dbh->selectrow_array(
        q{SELECT reporter FROM bugs WHERE bug_id = ?}, undef, $dupe);
433
    my $rep_user = Bugzilla::User->new($reporter);
434

435
    if ($rep_user->can_see_bug($original)) {
436
        $cgi->param('confirm_add_duplicate', '1');
437 438
        return;
    }
439

440 441 442
    $vars->{'cclist_accessible'} = $dbh->selectrow_array(
        q{SELECT cclist_accessible FROM bugs WHERE bug_id = ?},
        undef, $original);
443
    
444 445 446
    # Once in this part of the subroutine, the user has not been auto-validated
    # and the duper has not chosen whether or not to add to CC list, so let's
    # ask the duper what he/she wants to do.
447
    
448 449 450 451 452
    $vars->{'original_bug_id'} = $original;
    $vars->{'duplicate_bug_id'} = $dupe;
    
    # Confirm whether or not to add the reporter to the cc: list
    # of the original bug (the one this bug is being duped against).
453
    print Bugzilla->cgi->header();
454
    $template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
455
      || ThrowTemplateError($template->error());
456
    exit;
457
}
458

459
if (defined $cgi->param('id')) {
460 461 462 463 464 465
    # since this means that we were called from show_bug.cgi, now is a good
    # time to do a whole bunch of error checking that can't easily happen when
    # we've been called from buglist.cgi, because buglist.cgi only tweaks
    # values that have been changed instead of submitting all the new values.
    # (XXX those error checks need to happen too, but implementing them 
    # is more work in the current architecture of this script...)
466
    my $prod_obj = Bugzilla::Product::check_product($cgi->param('product'));
467
    check_field('component', scalar $cgi->param('component'), 
468
                [map($_->name, @{$prod_obj->components})]);
469
    check_field('version', scalar $cgi->param('version'),
470
                [map($_->name, @{$prod_obj->versions})]);
471
    if ( Bugzilla->params->{"usetargetmilestone"} ) {
472
        check_field('target_milestone', scalar $cgi->param('target_milestone'), 
473
                    [map($_->name, @{$prod_obj->milestones})]);
474
    }
475 476 477 478
    check_field('rep_platform', scalar $cgi->param('rep_platform'));
    check_field('op_sys',       scalar $cgi->param('op_sys'));
    check_field('priority',     scalar $cgi->param('priority'));
    check_field('bug_severity', scalar $cgi->param('bug_severity'));
479 480 481 482 483 484

    # Those fields only have to exist. We don't validate their value here.
    foreach my $field_name ('bug_file_loc', 'short_desc', 'longdesclength') {
        defined($cgi->param($field_name))
          || ThrowCodeError('undefined_field', { field => $field_name });
    }
485
    $cgi->param('short_desc', clean_text($cgi->param('short_desc')));
486

487
    if (trim($cgi->param('short_desc')) eq "") {
488
        ThrowUserError("require_summary");
489
    }
terry%netscape.com's avatar
terry%netscape.com committed
490 491
}

492 493
my $action = trim($cgi->param('action') || '');

494 495
if ($action eq Bugzilla->params->{'move-button-text'}) {
    Bugzilla->params->{'move-enabled'} || ThrowUserError("move_bugs_disabled");
496

497 498
    $user->is_mover || ThrowUserError("auth_failure", {action => 'move',
                                                       object => 'bugs'});
499 500 501 502 503 504 505 506 507 508 509 510 511 512

    # Moved bugs are marked as RESOLVED MOVED.
    my $sth = $dbh->prepare("UPDATE bugs
                                SET bug_status = 'RESOLVED',
                                    resolution = 'MOVED',
                                    delta_ts = ?
                              WHERE bug_id = ?");
    # Bugs cannot be a dupe and moved at the same time.
    my $sth2 = $dbh->prepare("DELETE FROM duplicates WHERE dupe = ?");

    my $comment = "";
    if (defined $cgi->param('comment') && $cgi->param('comment') !~ /^\s*$/) {
        $comment = $cgi->param('comment') . "\n\n";
    }
513
    $comment .= "Bug moved to " . Bugzilla->params->{'move-to-url'} . ".\n\n";
514
    $comment .= "If the move succeeded, " . $user->login . " will receive a mail\n";
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
    $comment .= "containing the number of the new bug in the other database.\n";
    $comment .= "If all went well,  please mark this bug verified, and paste\n";
    $comment .= "in a link to the new bug. Otherwise, reopen this bug.\n";

    $dbh->bz_lock_tables('bugs WRITE', 'bugs_activity WRITE', 'duplicates WRITE',
                         'longdescs WRITE', 'profiles READ', 'groups READ',
                         'bug_group_map READ', 'group_group_map READ',
                         'user_group_map READ', 'classifications READ',
                         'products READ', 'components READ', 'votes READ',
                         'cc READ', 'fielddefs READ');

    my $timestamp = $dbh->selectrow_array("SELECT NOW()");
    my @bugs;
    # First update all moved bugs.
    foreach my $id (@idlist) {
        my $bug = new Bugzilla::Bug($id, $whoid);
        push(@bugs, $bug);

        $sth->execute($timestamp, $id);
        $sth2->execute($id);

        AppendComment($id, $whoid, $comment, 0, $timestamp);

        if ($bug->bug_status ne 'RESOLVED') {
            LogActivityEntry($id, 'bug_status', $bug->bug_status,
                             'RESOLVED', $whoid, $timestamp);
        }
        if ($bug->resolution ne 'MOVED') {
            LogActivityEntry($id, 'resolution', $bug->resolution,
                             'MOVED', $whoid, $timestamp);
        }
    }
    $dbh->bz_unlock_tables();

    # Now send emails.
    foreach my $id (@idlist) {
551
        $vars->{'mailrecipients'} = { 'changer' => $user->login };
552 553 554 555 556 557 558 559
        $vars->{'id'} = $id;
        $vars->{'type'} = "move";

        $template->process("bug/process/results.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
        $vars->{'header_done'} = 1;
    }
    # Prepare and send all data about these bugs to the new database
560
    my $to = Bugzilla->params->{'move-to-address'};
561
    $to =~ s/@/\@/;
562
    my $from = Bugzilla->params->{'moved-from-address'};
563 564 565 566 567
    $from =~ s/@/\@/;
    my $msg = "To: $to\n";
    $msg .= "From: Bugzilla <" . $from . ">\n";
    $msg .= "Subject: Moving bug(s) " . join(', ', @idlist) . "\n\n";

568
    my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc',
569
                     'attachment', 'attachmentdata');
570 571 572 573 574 575 576 577 578 579 580
    my %displayfields;
    foreach (@fieldlist) {
        $displayfields{$_} = 1;
    }

    $template->process("bug/show.xml.tmpl", { bugs => \@bugs,
                                              displayfields => \%displayfields,
                                            }, \$msg)
      || ThrowTemplateError($template->error());

    $msg .= "\n";
581
    MessageToMTA($msg);
582 583 584 585 586 587 588

    # End the response page.
    $template->process("bug/navigate.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
    $template->process("global/footer.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
    exit;
589 590 591
}


592
$::query = "UPDATE bugs SET";
593
$::comma = "";
594
local our @values;
595 596
umask(0);

597
sub _remove_remaining_time {
598
    my $cgi = Bugzilla->cgi;
599
    if (UserInGroup(Bugzilla->params->{'timetrackinggroup'})) {
600 601
        if ( defined $cgi->param('remaining_time') 
             && $cgi->param('remaining_time') > 0 )
602
        {
603
            $cgi->param('remaining_time', 0);
604 605 606 607 608 609 610 611 612
            $vars->{'message'} = "remaining_time_zeroed";
        }
    }
    else {
        DoComma();
        $::query .= "remaining_time = 0";
    }
}

613 614 615
sub DoComma {
    $::query .= "$::comma\n    ";
    $::comma = ",";
terry%netscape.com's avatar
terry%netscape.com committed
616 617
}

618 619
# $everconfirmed is used by ChangeStatus() to determine whether we are
# confirming the bug or not.
620
local our $everconfirmed;
621
sub DoConfirm {
622 623 624 625
    my $bug = shift;
    if ($bug->check_can_change_field("canconfirm", 0, 1, 
                                     \$PrivilegesRequired)) 
    {
626 627
        DoComma();
        $::query .= "everconfirmed = 1";
628
        $everconfirmed = 1;
629 630 631
    }
}

632 633
sub ChangeStatus {
    my ($str) = (@_);
634 635 636
    my $cgi = Bugzilla->cgi;
    my $dbh = Bugzilla->dbh;

637 638
    if (!$cgi->param('dontchange')
        || $str ne $cgi->param('dontchange')) {
639
        DoComma();
640
        if ($cgi->param('knob') eq 'reopen') {
641 642
            # When reopening, we need to check whether the bug was ever
            # confirmed or not
643
            $::query .= "bug_status = CASE WHEN everconfirmed = 1 THEN " .
644
                        $dbh->quote($str) . " ELSE 'UNCONFIRMED' END";
645
        } elsif (is_open_state($str)) {
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666
            # Note that we cannot combine this with the above branch - here we
            # need to check if bugs.bug_status is open, (since we don't want to
            # reopen closed bugs when reassigning), while above the whole point
            # is to reopen a closed bug.
            # Currently, the UI doesn't permit a user to reassign a closed bug
            # from the single bug page (only during a mass change), but they
            # could still hack the submit, so don't restrict this extended
            # check to the mass change page for safety/sanity/consistency
            # purposes.

            # The logic for this block is:
            # If the new state is open:
            #   If the old state was open
            #     If the bug was confirmed
            #       - move it to the new state
            #     Else
            #       - Set the state to unconfirmed
            #   Else
            #     - leave it as it was

            # This is valid only because 'reopen' is the only thing which moves
667
            # from closed to open, and it's handled above
668 669 670
            # This also relies on the fact that confirming and accepting have
            # already called DoConfirm before this is called

671
            my @open_state = map($dbh->quote($_), BUG_STATE_OPEN);
672
            my $open_state = join(", ", @open_state);
673 674 675

            # If we are changing everconfirmed to 1, we have to take this change
            # into account and the new bug status is given by $str.
676
            my $cond = $dbh->quote($str);
677 678 679 680 681 682
            # If we are not setting everconfirmed, the new bug status depends on
            # the actual value of everconfirmed, which is bug-specific.
            unless ($everconfirmed) {
                $cond = "(CASE WHEN everconfirmed = 1 THEN " . $cond .
                        " ELSE 'UNCONFIRMED' END)";
            }
683
            $::query .= "bug_status = CASE WHEN bug_status IN($open_state) THEN " .
684
                                      $cond . " ELSE bug_status END";
685
        } else {
686 687
            $::query .= "bug_status = ?";
            push(@values, $str);
688
        }
689 690 691
        # If bugs are reassigned and their status is "UNCONFIRMED", they
        # should keep this status instead of "NEW" as suggested here.
        # This point is checked for each bug later in the code.
692
        $cgi->param('bug_status', $str);
terry%netscape.com's avatar
terry%netscape.com committed
693 694 695
    }
}

696 697
sub ChangeResolution {
    my ($str) = (@_);
698 699 700
    my $dbh = Bugzilla->dbh;
    my $cgi = Bugzilla->cgi;

701 702
    if (!$cgi->param('dontchange')
        || $str ne $cgi->param('dontchange'))
703
    {
704 705 706 707 708 709 710 711 712 713 714 715
        # Make sure the user is allowed to change the resolution.
        # If the user is changing several bugs at once using the UI,
        # then he has enough privs to do so. In the case he is hacking
        # the URL, we don't care if he reads --UNKNOWN-- as a resolution
        # in the error message.
        my $old_resolution = '-- UNKNOWN --';
        my $bug_id = $cgi->param('id');
        if ($bug_id) {
            $old_resolution =
                $dbh->selectrow_array('SELECT resolution FROM bugs WHERE bug_id = ?',
                                       undef, $bug_id);
        }
716 717 718
        unless ($bug->check_can_change_field('resolution', $old_resolution, $str,
                                             \$PrivilegesRequired))
        {
719 720 721 722 723 724 725
            $vars->{'oldvalue'} = $old_resolution;
            $vars->{'newvalue'} = $str;
            $vars->{'field'} = 'resolution';
            $vars->{'privs'} = $PrivilegesRequired;
            ThrowUserError("illegal_change", $vars);
        }

726
        DoComma();
727 728 729
        $::query .= "resolution = ?";
        trick_taint($str);
        push(@values, $str);
730
        # We define this variable here so that customized installations
731
        # may set rules based on the resolution in Bug::check_can_change_field().
732
        $cgi->param('resolution', $str);
terry%netscape.com's avatar
terry%netscape.com committed
733 734 735
    }
}

736 737 738 739
# Changing this so that it will process groups from checkboxes instead of
# select lists.  This means that instead of looking for the bit-X values in
# the form, we need to loop through all the bug groups this user has access
# to, and for each one, see if it's selected.
740 741
# If the form element isn't present, or the user isn't in the group, leave
# it as-is
742

743 744 745
my @groupAdd = ();
my @groupDel = ();

746 747 748 749 750
my $groups = $dbh->selectall_arrayref(
    qq{SELECT groups.id, isactive FROM groups
        WHERE id IN($grouplist) AND isbuggroup = 1});
foreach my $group (@$groups) {
    my ($b, $isactive) = @$group;
751 752 753 754
    # The multiple change page may not show all groups a bug is in
    # (eg product groups when listing more than one product)
    # Only consider groups which were present on the form. We can't do this
    # for single bug changes because non-checked checkboxes aren't present.
755
    # All the checkboxes should be shown in that case, though, so it isn't
756
    # an issue there
757 758
    if (defined $cgi->param('id') || defined $cgi->param("bit-$b")) {
        if (!$cgi->param("bit-$b")) {
759
            push(@groupDel, $b);
760
        } elsif ($cgi->param("bit-$b") == 1 && $isactive) {
761
            push(@groupAdd, $b);
762 763
        }
    }
764 765
}

766 767
foreach my $field ("rep_platform", "priority", "bug_severity",
                   "bug_file_loc", "short_desc", "version", "op_sys",
768
                   "target_milestone", "status_whiteboard") {
769 770 771
    if (defined $cgi->param($field)) {
        if (!$cgi->param('dontchange')
            || $cgi->param($field) ne $cgi->param('dontchange')) {
772
            DoComma();
773 774 775 776
            $::query .= "$field = ?";
            my $value = trim($cgi->param($field));
            trick_taint($value);
            push(@values, $value);
terry%netscape.com's avatar
terry%netscape.com committed
777 778 779 780
        }
    }
}

781 782 783 784 785 786 787
# Add custom fields data to the query that will update the database.
foreach my $field (Bugzilla->custom_field_names) {
    if (defined $cgi->param($field)
        && (!$cgi->param('dontchange')
            || $cgi->param($field) ne $cgi->param('dontchange')))
    {
        DoComma();
788 789 790 791
        $::query .= "$field = ?";
        my $value = $cgi->param($field);
        trick_taint($value);
        push(@values, $value);
792 793 794
    }
}

795
my $product;
796 797
my $prod_changed;
my @newprod_ids;
798
if ($cgi->param('product') ne $cgi->param('dontchange')) {
799 800
    $product = Bugzilla::Product::check_product(scalar $cgi->param('product'));

801
    DoComma();
802
    $::query .= "product_id = ?";
803 804
    push(@values, $product->id);
    @newprod_ids = ($product->id);
805
    $prod_changed = 1;
806
} else {
807 808 809 810 811 812
    @newprod_ids = @{$dbh->selectcol_arrayref("SELECT DISTINCT product_id
                                               FROM bugs 
                                               WHERE bug_id IN (" .
                                                   join(',', @idlist) . 
                                               ")")};
    if (scalar(@newprod_ids) == 1) {
813
        $product = new Bugzilla::Product($newprod_ids[0]);
814
    }
815 816
}

817
my $component;
818
if ($cgi->param('component') ne $cgi->param('dontchange')) {
819
    if (scalar(@newprod_ids) > 1) {
820
        ThrowUserError("no_component_change_for_multiple_products");
821
    }
822 823 824 825 826
    $component =
        Bugzilla::Component::check_component($product, scalar $cgi->param('component'));

    # This parameter is required later when checking fields the user can change.
    $cgi->param('component_id', $component->id);
827
    DoComma();
828
    $::query .= "component_id = ?";
829
    push(@values, $component->id);
830 831
}

832 833
# If this installation uses bug aliases, and the user is changing the alias,
# add this change to the query.
834
if (Bugzilla->params->{"usebugaliases"} && defined $cgi->param('alias')) {
835
    my $alias = trim($cgi->param('alias'));
836 837 838 839 840 841 842 843 844 845
    
    # Since aliases are unique (like bug numbers), they can only be changed
    # for one bug at a time, so ignore the alias change unless only a single
    # bug is being changed.
    if (scalar(@idlist) == 1) {
        # Add the alias change to the query.  If the field contains the blank 
        # value, make the field be NULL to indicate that the bug has no alias.
        # Otherwise, if the field contains a value, update the record 
        # with that value.
        DoComma();
846 847
        if ($alias ne "") {
            ValidateBugAlias($alias, $idlist[0]);
848 849
            $::query .= "alias = ?";
            push(@values, $alias);
850
        } else {
851
            $::query .= "alias = NULL";
852
        }
853 854
    }
}
855

856 857
# If the user is submitting changes from show_bug.cgi for a single bug,
# and that bug is restricted to a group, process the checkboxes that
858
# allowed the user to set whether or not the reporter
859 860
# and cc list can see the bug even if they are not members of all groups 
# to which the bug is restricted.
861
if (defined $cgi->param('id')) {
862 863 864
    my ($havegroup) = $dbh->selectrow_array(
        q{SELECT group_id FROM bug_group_map WHERE bug_id = ?},
        undef, $cgi->param('id'));
865
    if ( $havegroup ) {
866
        DoComma();
867 868
        $cgi->param('reporter_accessible',
                    $cgi->param('reporter_accessible') ? '1' : '0');
869 870
        $::query .= "reporter_accessible = ?";
        push(@values, $cgi->param('reporter_accessible'));
871 872

        DoComma();
873 874
        $cgi->param('cclist_accessible',
                    $cgi->param('cclist_accessible') ? '1' : '0');
875 876
        $::query .= "cclist_accessible = ?";
        push(@values, $cgi->param('cclist_accessible'));
877 878 879
    }
}

880 881 882 883
if ( defined $cgi->param('id') &&
     (Bugzilla->params->{"insidergroup"} 
      && UserInGroup(Bugzilla->params->{"insidergroup"})) ) 
{
884

885 886 887
    my $sth = $dbh->prepare('UPDATE longdescs SET isprivate = ?
                             WHERE bug_id = ? AND bug_when = ?');

888
    foreach my $field ($cgi->param()) {
889 890
        if ($field =~ /when-([0-9]+)/) {
            my $sequence = $1;
891 892 893
            my $private = $cgi->param("isprivate-$sequence") ? 1 : 0 ;
            if ($private != $cgi->param("oisprivate-$sequence")) {
                my $field_data = $cgi->param("$field");
894 895 896
                # Make sure a valid date is given.
                $field_data = format_time($field_data, '%Y-%m-%d %T');
                $sth->execute($private, $cgi->param('id'), $field_data);
897 898 899 900 901
            }
        }

    }
}
902

903
my $duplicate = 0;
904

905 906 907 908
# We need to check the addresses involved in a CC change before we touch any bugs.
# What we'll do here is formulate the CC data into two hashes of ID's involved
# in this CC change.  Then those hashes can be used later on for the actual change.
my (%cc_add, %cc_remove);
909 910 911 912
if (defined $cgi->param('newcc')
    || defined $cgi->param('addselfcc')
    || defined $cgi->param('removecc')
    || defined $cgi->param('masscc')) {
913 914 915
    # If masscc is defined, then we came from buglist and need to either add or
    # remove cc's... otherwise, we came from bugform and may need to do both.
    my ($cc_add, $cc_remove) = "";
916 917 918 919 920
    if (defined $cgi->param('masscc')) {
        if ($cgi->param('ccaction') eq 'add') {
            $cc_add = join(' ',$cgi->param('masscc'));
        } elsif ($cgi->param('ccaction') eq 'remove') {
            $cc_remove = join(' ',$cgi->param('masscc'));
921 922
        }
    } else {
923
        $cc_add = join(' ',$cgi->param('newcc'));
924 925
        # We came from bug_form which uses a select box to determine what cc's
        # need to be removed...
926 927
        if (defined $cgi->param('removecc') && $cgi->param('cc')) {
            $cc_remove = join (",", $cgi->param('cc'));
928 929 930 931
        }
    }

    if ($cc_add) {
932 933
        $cc_add =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_add) ) {
934
            my $pid = login_to_id($person, THROW_ERROR);
935 936 937
            $cc_add{$pid} = $person;
        }
    }
938
    if ($cgi->param('addselfcc')) {
939 940
        $cc_add{$whoid} = $user->login;
    }
941
    if ($cc_remove) {
942 943
        $cc_remove =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_remove) ) {
944
            my $pid = login_to_id($person, THROW_ERROR);
945 946 947 948 949
            $cc_remove{$pid} = $person;
        }
    }
}

950 951
# Store the new assignee and QA contact IDs (if any). This is the
# only way to keep these informations when bugs are reassigned by
952
# component as $cgi->param('assigned_to') and $cgi->param('qa_contact')
953
# are not the right fields to look at.
954 955 956
# If the assignee or qacontact is changed, the new one is checked when
# changed information is validated.  If not, then the unchanged assignee
# or qacontact may have to be validated later.
957 958 959

my $assignee;
my $qacontact;
960 961 962 963
my $qacontact_checked = 0;
my $assignee_checked = 0;

my %usercache = ();
964

965 966
if (defined $cgi->param('qa_contact')
    && $cgi->param('knob') ne "reassignbycomponent")
967
{
968
    my $name = trim($cgi->param('qa_contact'));
969
    # The QA contact cannot be deleted from show_bug.cgi for a single bug!
970
    if ($name ne $cgi->param('dontchange')) {
971
        $qacontact = login_to_id($name, THROW_ERROR) if ($name ne "");
972
        if ($qacontact && Bugzilla->params->{"strict_isolation"}) {
973 974 975 976
                $usercache{$qacontact} ||= Bugzilla::User->new($qacontact);
                my $qa_user = $usercache{$qacontact};
                foreach my $product_id (@newprod_ids) {
                    if (!$qa_user->can_edit_product($product_id)) {
977
                        my $product_name = Bugzilla::Product->new($product_id)->name;
978 979 980 981 982 983 984 985
                        ThrowUserError('invalid_user_group',
                                          {'users'   => $qa_user->login,
                                           'product' => $product_name,
                                           'bug_id' => (scalar(@idlist) > 1)
                                                         ? undef : $idlist[0]
                                          });
                    }
                }
986
        }
987
        $qacontact_checked = 1;
988
        DoComma();
989
        if($qacontact) {
990 991
            $::query .= "qa_contact = ?";
            push(@values, $qacontact);
992 993 994 995
        }
        else {
            $::query .= "qa_contact = NULL";
        }
996 997
    }
}
998

999
SWITCH: for ($cgi->param('knob')) {
1000 1001 1002
    /^none$/ && do {
        last SWITCH;
    };
1003
    /^confirm$/ && CheckonComment( "confirm" ) && do {
1004
        DoConfirm($bug);
1005 1006 1007
        ChangeStatus('NEW');
        last SWITCH;
    };
1008
    /^accept$/ && CheckonComment( "accept" ) && do {
1009
        DoConfirm($bug);
1010
        ChangeStatus('ASSIGNED');
1011 1012 1013
        if (Bugzilla->params->{"usetargetmilestone"} 
            && Bugzilla->params->{"musthavemilestoneonaccept"}) 
        {
1014
            $requiremilestone = 1;
1015
        }
1016 1017
        last SWITCH;
    };
1018
    /^clearresolution$/ && CheckonComment( "clearresolution" ) && do {
1019 1020 1021
        ChangeResolution('');
        last SWITCH;
    };
1022
    /^(resolve|change_resolution)$/ && CheckonComment( "resolve" ) && do {
1023
        # Check here, because it's the only place we require the resolution
1024
        check_field('resolution', scalar $cgi->param('resolution'),
1025
                    Bugzilla::Bug->settable_resolutions);
1026

1027
        # don't resolve as fixed while still unresolved blocking bugs
1028
        if (Bugzilla->params->{"noresolveonopenblockers"}
1029
            && $cgi->param('resolution') eq 'FIXED')
1030
        {
1031
            my @dependencies = Bugzilla::Bug::CountOpenDependencies(@idlist);
1032 1033
            if (scalar @dependencies > 0) {
                ThrowUserError("still_unresolved_bugs",
1034 1035
                               { dependencies     => \@dependencies,
                                 dependency_count => scalar @dependencies });
1036
            }
1037
        }
1038

1039 1040 1041 1042 1043 1044 1045
        if ($cgi->param('knob') eq 'resolve') {
            # RESOLVED bugs should have no time remaining;
            # more time can be added for the VERIFY step, if needed.
            _remove_remaining_time();

            ChangeStatus('RESOLVED');
        }
1046

1047
        ChangeResolution($cgi->param('resolution'));
1048 1049
        last SWITCH;
    };
1050
    /^reassign$/ && CheckonComment( "reassign" ) && do {
1051
        if ($cgi->param('andconfirm')) {
1052
            DoConfirm($bug);
1053
        }
1054 1055
        ChangeStatus('NEW');
        DoComma();
1056 1057
        if (defined $cgi->param('assigned_to')
            && trim($cgi->param('assigned_to')) ne "") { 
1058
            $assignee = login_to_id(trim($cgi->param('assigned_to')), THROW_ERROR);
1059
            if (Bugzilla->params->{"strict_isolation"}) {
1060 1061 1062 1063
                $usercache{$assignee} ||= Bugzilla::User->new($assignee);
                my $assign_user = $usercache{$assignee};
                foreach my $product_id (@newprod_ids) {
                    if (!$assign_user->can_edit_product($product_id)) {
1064
                        my $product_name = Bugzilla::Product->new($product_id)->name;
1065 1066 1067 1068 1069 1070 1071 1072
                        ThrowUserError('invalid_user_group',
                                          {'users'   => $assign_user->login,
                                           'product' => $product_name,
                                           'bug_id' => (scalar(@idlist) > 1)
                                                         ? undef : $idlist[0]
                                          });
                    }
                }
1073
            }
1074
        } else {
1075
            ThrowUserError("reassign_to_empty");
1076
        }
1077 1078
        $::query .= "assigned_to = ?";
        push(@values, $assignee);
1079
        $assignee_checked = 1;
1080 1081
        last SWITCH;
    };
1082
    /^reassignbycomponent$/  && CheckonComment( "reassignbycomponent" ) && do {
1083
        if ($cgi->param('compconfirm')) {
1084
            DoConfirm($bug);
1085
        }
1086 1087
        ChangeStatus('NEW');
        last SWITCH;
1088
    };
1089
    /^reopen$/  && CheckonComment( "reopen" ) && do {
1090
        ChangeStatus('REOPENED');
1091
        ChangeResolution('');
1092 1093
        last SWITCH;
    };
1094
    /^verify$/ && CheckonComment( "verify" ) && do {
1095 1096 1097
        ChangeStatus('VERIFIED');
        last SWITCH;
    };
1098
    /^close$/ && CheckonComment( "close" ) && do {
1099 1100 1101
        # CLOSED bugs should have no time remaining.
        _remove_remaining_time();

1102 1103 1104
        ChangeStatus('CLOSED');
        last SWITCH;
    };
1105
    /^duplicate$/ && CheckonComment( "duplicate" ) && do {
1106 1107 1108 1109 1110 1111
        # You cannot mark bugs as duplicates when changing
        # several bugs at once.
        unless (defined $cgi->param('id')) {
            ThrowUserError('dupe_not_allowed');
        }

1112
        # Make sure we can change the original bug (issue A on bug 96085)
1113 1114 1115
        defined($cgi->param('dup_id'))
          || ThrowCodeError('undefined_field', { field => 'dup_id' });

1116 1117 1118
        $duplicate = $cgi->param('dup_id');
        ValidateBugID($duplicate, 'dup_id');
        $cgi->param('dup_id', $duplicate);
1119

1120 1121 1122
        # Make sure a loop isn't created when marking this bug
        # as duplicate.
        my %dupes;
1123
        my $dupe_of = $duplicate;
1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
        my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates
                                 WHERE dupe = ?');

        while ($dupe_of) {
            if ($dupe_of == $cgi->param('id')) {
                ThrowUserError('dupe_loop_detected', { bug_id  => $cgi->param('id'),
                                                       dupe_of => $duplicate });
            }
            # If $dupes{$dupe_of} is already set to 1, then a loop
            # already exists which does not involve this bug.
            # As the user is not responsible for this loop, do not
            # prevent him from marking this bug as a duplicate.
            last if exists $dupes{"$dupe_of"};
            $dupes{"$dupe_of"} = 1;
            $sth->execute($dupe_of);
            $dupe_of = $sth->fetchrow_array;
        }

        # Also, let's see if the reporter has authorization to see
        # the bug to which we are duping. If not we need to prompt.
        DuplicateUserConfirm();

1146 1147 1148
        # DUPLICATE bugs should have no time remaining.
        _remove_remaining_time();

1149 1150
        ChangeStatus('RESOLVED');
        ChangeResolution('DUPLICATE');
1151 1152
        my $comment = $cgi->param('comment');
        $comment .= "\n\n*** This bug has been marked " .
1153
                    "as a duplicate of bug $duplicate ***";
1154
        $cgi->param('comment', $comment);
1155 1156
        last SWITCH;
    };
1157

1158
    ThrowCodeError("unknown_action", { action => $cgi->param('knob') });
terry%netscape.com's avatar
terry%netscape.com committed
1159 1160
}

1161 1162 1163
my @keywordlist;
my %keywordseen;

1164 1165
if (defined $cgi->param('keywords')) {
    foreach my $keyword (split(/[\s,]+/, $cgi->param('keywords'))) {
1166 1167 1168
        if ($keyword eq '') {
            next;
        }
1169 1170
        my $keyword_obj = new Bugzilla::Keyword({name => $keyword});
        if (!$keyword_obj) {
1171 1172
            ThrowUserError("unknown_keyword",
                           { keyword => $keyword });
1173
        }
1174 1175 1176
        if (!$keywordseen{$keyword_obj->id}) {
            push(@keywordlist, $keyword_obj->id);
            $keywordseen{$keyword_obj->id} = 1;
1177 1178 1179 1180
        }
    }
}

1181
my $keywordaction = $cgi->param('keywordaction') || "makeexact";
1182 1183 1184
if (!grep($keywordaction eq $_, qw(add delete makeexact))) {
    $keywordaction = "makeexact";
}
1185

1186
if ($::comma eq ""
1187
    && (! @groupAdd) && (! @groupDel)
1188 1189
    && (!Bugzilla::Keyword::keyword_count() 
        || (0 == @keywordlist && $keywordaction ne "makeexact"))
1190
    && defined $cgi->param('masscc') && ! $cgi->param('masscc')
1191
    ) {
1192
    if (!defined $cgi->param('comment') || $cgi->param('comment') =~ /^\s*$/) {
1193
        ThrowUserError("bugs_not_changed");
terry%netscape.com's avatar
terry%netscape.com committed
1194 1195 1196
    }
}

1197
# Process data for Time Tracking fields
1198
if (UserInGroup(Bugzilla->params->{'timetrackinggroup'})) {
1199
    foreach my $field ("estimated_time", "remaining_time") {
1200 1201 1202
        if (defined $cgi->param($field)) {
            my $er_time = trim($cgi->param($field));
            if ($er_time ne $cgi->param('dontchange')) {
1203
                DoComma();
1204 1205 1206
                $::query .= "$field = ?";
                trick_taint($er_time);
                push(@values, $er_time);
1207 1208 1209 1210
            }
        }
    }

1211
    if (defined $cgi->param('deadline')) {
1212
        DoComma();
1213
        if ($cgi->param('deadline')) {
1214 1215 1216
            validate_date($cgi->param('deadline'))
              || ThrowUserError('illegal_date', {date => $cgi->param('deadline'),
                                                 format => 'YYYY-MM-DD'});
1217 1218 1219 1220
            $::query .= "deadline = ?";
            my $deadline = $cgi->param('deadline');
            trick_taint($deadline);
            push(@values, $deadline);
1221
        } else {
1222
            $::query .= "deadline = NULL";
1223 1224 1225 1226
        }
    }
}

1227
my $basequery = $::query;
1228

1229
local our $delta_ts;
1230 1231
sub SnapShotBug {
    my ($id) = (@_);
1232
    my $dbh = Bugzilla->dbh;
1233
    my @row = $dbh->selectrow_array(q{SELECT delta_ts, } .
1234
                join(',', editable_bug_fields()).q{ FROM bugs WHERE bug_id = ?},
1235
                undef, $id);
1236
    $delta_ts = shift @row;
1237

1238
    return @row;
terry%netscape.com's avatar
terry%netscape.com committed
1239 1240 1241
}


1242 1243
sub SnapShotDeps {
    my ($i, $target, $me) = (@_);
1244
    my $dbh = Bugzilla->dbh;
1245 1246 1247 1248
    my $list = $dbh->selectcol_arrayref(qq{SELECT $target FROM dependencies
                                        WHERE $me = ? ORDER BY $target},
                                        undef, $i);
    return join(',', @$list);
1249 1250 1251 1252 1253
}


my $timestamp;

1254
local our $bug_changed;
1255
sub LogDependencyActivity {
1256
    my ($i, $oldstr, $target, $me, $timestamp) = (@_);
1257
    my $dbh = Bugzilla->dbh;
1258 1259
    my $newstr = SnapShotDeps($i, $target, $me);
    if ($oldstr ne $newstr) {
1260
        # Figure out what's really different...
1261
        my ($removed, $added) = diff_strings($oldstr, $newstr);
1262
        LogActivityEntry($i,$target,$removed,$added,$whoid,$timestamp);
1263
        # update timestamp on target bug so midairs will be triggered
1264 1265
        $dbh->do(q{UPDATE bugs SET delta_ts = ? WHERE bug_id = ?},
                 undef, $timestamp, $i);
1266
        $bug_changed = 1;
1267 1268 1269 1270 1271
        return 1;
    }
    return 0;
}

1272
if (Bugzilla->params->{"strict_isolation"}) {
1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290
    my @blocked_cc = ();
    foreach my $pid (keys %cc_add) {
        $usercache{$pid} ||= Bugzilla::User->new($pid);
        my $cc_user = $usercache{$pid};
        foreach my $product_id (@newprod_ids) {
            if (!$cc_user->can_edit_product($product_id)) {
                push (@blocked_cc, $cc_user->login);
                last;
            }
        }
    }
    if (scalar(@blocked_cc)) {
        ThrowUserError("invalid_user_group", 
            {'users' => \@blocked_cc,
             'bug_id' => (scalar(@idlist) > 1) ? undef : $idlist[0]});
    }
}

1291
if ($prod_changed && Bugzilla->params->{"strict_isolation"}) {
1292 1293 1294 1295 1296 1297
    my $sth_cc = $dbh->prepare("SELECT who
                                FROM cc
                                WHERE bug_id = ?");
    my $sth_bug = $dbh->prepare("SELECT assigned_to, qa_contact
                                 FROM bugs
                                 WHERE bug_id = ?");
1298

1299 1300 1301 1302 1303 1304
    foreach my $id (@idlist) {
        $sth_cc->execute($id);
        my @blocked_cc = ();
        while (my ($pid) = $sth_cc->fetchrow_array) {
            $usercache{$pid} ||= Bugzilla::User->new($pid);
            my $cc_user = $usercache{$pid};
1305
            if (!$cc_user->can_edit_product($product->id)) {
1306 1307 1308 1309 1310 1311 1312
                push (@blocked_cc, $cc_user->login);
            }
        }
        if (scalar(@blocked_cc)) {
            ThrowUserError('invalid_user_group',
                              {'users'   => \@blocked_cc,
                               'bug_id' => $id,
1313
                               'product' => $product->name});
1314 1315 1316 1317 1318 1319
        }
        $sth_bug->execute($id);
        my ($assignee, $qacontact) = $sth_bug->fetchrow_array;
        if (!$assignee_checked) {
            $usercache{$assignee} ||= Bugzilla::User->new($assignee);
            my $assign_user = $usercache{$assignee};
1320
            if (!$assign_user->can_edit_product($product->id)) {
1321 1322 1323
                    ThrowUserError('invalid_user_group',
                                      {'users'   => $assign_user->login,
                                       'bug_id' => $id,
1324
                                       'product' => $product->name});
1325 1326 1327 1328 1329
            }
        }
        if (!$qacontact_checked && $qacontact) {
            $usercache{$qacontact} ||= Bugzilla::User->new($qacontact);
            my $qa_user = $usercache{$qacontact};
1330
            if (!$qa_user->can_edit_product($product->id)) {
1331 1332 1333
                    ThrowUserError('invalid_user_group',
                                      {'users'   => $qa_user->login,
                                       'bug_id' => $id,
1334
                                       'product' => $product->name});
1335 1336 1337 1338 1339 1340
            }
        }
    }
}


1341 1342 1343
# This loop iterates once for each bug to be processed (i.e. all the
# bugs selected when this script is called with multiple bugs selected
# from buglist.cgi, or just the one bug when called from
1344 1345
# show_bug.cgi).
#
1346
foreach my $id (@idlist) {
1347
    my $query = $basequery;
1348
    my @bug_values = @values;
1349
    my $old_bug_obj = new Bugzilla::Bug($id, $whoid);
1350 1351 1352

    if ($cgi->param('knob') eq 'reassignbycomponent') {
        # We have to check whether the bug is moved to another product
1353
        # and/or component before reassigning. If $component is defined,
1354
        # use it; else use the product/component the bug is already in.
1355
        my $new_comp_id = $component ? $component->id : $old_bug_obj->{'component_id'};
1356 1357 1358 1359
        $assignee = $dbh->selectrow_array('SELECT initialowner
                                           FROM components
                                           WHERE components.id = ?',
                                           undef, $new_comp_id);
1360
        $query .= ", assigned_to = ?";
1361
        push(@bug_values, $assignee);
1362
        if (Bugzilla->params->{"useqacontact"}) {
1363 1364 1365 1366 1367
            $qacontact = $dbh->selectrow_array('SELECT initialqacontact
                                                FROM components
                                                WHERE components.id = ?',
                                                undef, $new_comp_id);
            if ($qacontact) {
1368
                $query .= ", qa_contact = ?";
1369
                push(@bug_values, $qacontact);
1370 1371 1372 1373 1374 1375 1376
            }
            else {
                $query .= ", qa_contact = NULL";
            }
        }
    }

1377
    my %dependencychanged;
1378
    $bug_changed = 0;
1379 1380
    my $write = "WRITE";        # Might want to make a param to control
                                # whether we do LOW_PRIORITY ...
1381
    $dbh->bz_lock_tables("bugs $write", "bugs_activity $write", "cc $write",
1382
            "profiles READ", "dependencies $write", "votes $write",
1383
            "products READ", "components READ", "milestones READ",
1384
            "keywords $write", "longdescs $write", "fielddefs READ",
1385
            "bug_group_map $write", "flags $write", "duplicates $write",
1386
            "user_group_map READ", "group_group_map READ", "flagtypes READ",
1387 1388 1389 1390
            "flaginclusions AS i READ", "flagexclusions AS e READ",
            "keyworddefs READ", "groups READ", "attachments READ",
            "group_control_map AS oldcontrolmap READ",
            "group_control_map AS newcontrolmap READ",
1391
            "group_control_map READ", "email_setting READ", "classifications READ");
1392

1393
    # It may sound crazy to set %formhash for each bug as $cgi->param()
1394 1395
    # will not change, but %formhash is modified below and we prefer
    # to set it again.
1396
    my $i = 0;
1397 1398 1399
    my @oldvalues = SnapShotBug($id);
    my %oldhash;
    my %formhash;
1400
    foreach my $col (@editable_bug_fields) {
1401
        # Consider NULL db entries to be equivalent to the empty string
1402 1403 1404
        $oldvalues[$i] = defined($oldvalues[$i]) ? $oldvalues[$i] : '';
        # Convert the deadline taken from the DB into the YYYY-MM-DD format
        # for consistency with the deadline provided by the user, if any.
1405 1406
        # Else Bug::check_can_change_field() would see them as different
        # in all cases.
1407 1408 1409
        if ($col eq 'deadline') {
            $oldvalues[$i] = format_time($oldvalues[$i], "%Y-%m-%d");
        }
1410
        $oldhash{$col} = $oldvalues[$i];
1411
        $formhash{$col} = $cgi->param($col) if defined $cgi->param($col);
1412 1413 1414 1415 1416 1417 1418
        $i++;
    }
    # If the user is reassigning bugs, we need to:
    # - convert $newhash{'assigned_to'} and $newhash{'qa_contact'}
    #   email addresses into their corresponding IDs;
    # - update $newhash{'bug_status'} to its real state if the bug
    #   is in the unconfirmed state.
1419
    $formhash{'qa_contact'} = $qacontact if Bugzilla->params->{'useqacontact'};
1420 1421
    if ($cgi->param('knob') eq 'reassignbycomponent'
        || $cgi->param('knob') eq 'reassign') {
1422
        $formhash{'assigned_to'} = $assignee;
1423
        if ($oldhash{'bug_status'} eq 'UNCONFIRMED') {
1424 1425 1426
            $formhash{'bug_status'} = $oldhash{'bug_status'};
        }
    }
1427 1428 1429 1430 1431
    # This hash is required by Bug::check_can_change_field().
    my $cgi_hash = {
        'dontchange' => scalar $cgi->param('dontchange'),
        'knob'       => scalar $cgi->param('knob')
    };
1432
    foreach my $col (@editable_bug_fields) {
1433 1434 1435
        # The 'resolution' field is checked by ChangeResolution(),
        # i.e. only if we effectively use it.
        next if ($col eq 'resolution');
1436
        if (exists $formhash{$col}
1437 1438
            && !$old_bug_obj->check_can_change_field($col, $oldhash{$col}, $formhash{$col},
                                                     \$PrivilegesRequired, $cgi_hash))
1439 1440 1441 1442
        {
            my $vars;
            if ($col eq 'component_id') {
                # Display the component name
1443
                $vars->{'oldvalue'} = $old_bug_obj->component;
1444
                $vars->{'newvalue'} = $cgi->param('component');
1445 1446 1447
                $vars->{'field'} = 'component';
            } elsif ($col eq 'assigned_to' || $col eq 'qa_contact') {
                # Display the assignee or QA contact email address
1448 1449
                $vars->{'oldvalue'} = user_id_to_login($oldhash{$col});
                $vars->{'newvalue'} = user_id_to_login($formhash{$col});
1450 1451 1452 1453 1454
                $vars->{'field'} = $col;
            } else {
                $vars->{'oldvalue'} = $oldhash{$col};
                $vars->{'newvalue'} = $formhash{$col};
                $vars->{'field'} = $col;
1455
            }
1456
            $vars->{'privs'} = $PrivilegesRequired;
1457
            ThrowUserError("illegal_change", $vars);
1458 1459
        }
    }
1460
    
1461 1462
    # When editing multiple bugs, users can specify a list of keywords to delete
    # from bugs.  If the list matches the current set of keywords on those bugs,
1463 1464
    # Bug::check_can_change_field will fail to check permissions because it thinks
    # the list hasn't changed. To fix that, we have to call Bug::check_can_change_field
1465 1466
    # again with old!=new if the keyword action is "delete" and old=new.
    if ($keywordaction eq "delete"
1467
        && defined $cgi->param('keywords')
1468
        && length(@keywordlist) > 0
1469
        && $cgi->param('keywords') eq $oldhash{keywords}
1470 1471
        && !$old_bug_obj->check_can_change_field("keywords", "old is not", "equal to new",
                                                 \$PrivilegesRequired))
1472 1473 1474 1475
    {
        $vars->{'oldvalue'} = $oldhash{keywords};
        $vars->{'newvalue'} = "no keywords";
        $vars->{'field'} = "keywords";
1476
        $vars->{'privs'} = $PrivilegesRequired;
1477
        ThrowUserError("illegal_change", $vars);
1478 1479
    }

1480
    $oldhash{'product'} = $old_bug_obj->product;
1481
    if (!Bugzilla->user->can_edit_product($oldhash{'product_id'})) {
1482
        ThrowUserError("product_edit_denied",
1483
                      { product => $oldhash{'product'} });
1484 1485
    }

1486
    if ($requiremilestone) {
1487 1488
        # musthavemilestoneonaccept applies only if at least two
        # target milestones are defined for the current product.
1489 1490
        my $prod_obj = new Bugzilla::Product({'name' => $oldhash{'product'}});
        my $nb_milestones = scalar(@{$prod_obj->milestones});
1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502
        if ($nb_milestones > 1) {
            my $value = $cgi->param('target_milestone');
            if (!defined $value || $value eq $cgi->param('dontchange')) {
                $value = $oldhash{'target_milestone'};
            }
            my $defaultmilestone =
                $dbh->selectrow_array("SELECT defaultmilestone
                                       FROM products WHERE id = ?",
                                       undef, $oldhash{'product_id'});
            # if musthavemilestoneonaccept == 1, then the target
            # milestone must be different from the default one.
            if ($value eq $defaultmilestone) {
1503
                ThrowUserError("milestone_required", { bug_id => $id });
1504
            }
1505 1506
        }
    }   
1507 1508
    if (defined $cgi->param('delta_ts') && $cgi->param('delta_ts') ne $delta_ts)
    {
1509 1510
        ($vars->{'operations'}) =
            Bugzilla::Bug::GetBugActivity($id, $cgi->param('delta_ts'));
1511

1512
        $vars->{'start_at'} = $cgi->param('longdesclength');
1513 1514 1515 1516

        # Always sort midair collision comments oldest to newest,
        # regardless of the user's personal preference.
        $vars->{'comments'} = Bugzilla::Bug::GetComments($id, "oldest_to_newest");
1517

1518
        $cgi->param('delta_ts', $delta_ts);
1519 1520 1521
        
        $vars->{'bug_id'} = $id;
        
1522
        $dbh->bz_unlock_tables(UNLOCK_ABORT);
1523 1524
        
        # Warn the user about the mid-air collision and ask them what to do.
1525
        $template->process("bug/process/midair.html.tmpl", $vars)
1526
          || ThrowTemplateError($template->error());
1527 1528 1529
        exit;
    }

1530
    # Gather the dependency list, and make sure there are no circular refs
1531 1532
    my %deps = Bugzilla::Bug::ValidateDependencies(scalar($cgi->param('dependson')),
                                                   scalar($cgi->param('blocked')),
1533
                                                   $id);
1534

1535 1536 1537 1538
    #
    # Start updating the relevant database entries
    #

1539
    $timestamp = $dbh->selectrow_array(q{SELECT NOW()});
1540

1541
    my $work_time;
1542
    if (UserInGroup(Bugzilla->params->{'timetrackinggroup'})) {
1543
        $work_time = $cgi->param('work_time');
1544 1545 1546 1547
        if ($work_time) {
            # AppendComment (called below) can in theory raise an error,
            # but because we've already validated work_time here it's
            # safe to log the entry before adding the comment.
1548
            LogActivityEntry($id, "work_time", "", $work_time,
1549
                             $whoid, $timestamp);
1550 1551 1552
        }
    }

1553
    if ($cgi->param('comment') || $work_time) {
1554 1555
        AppendComment($id, $whoid, scalar($cgi->param('comment')),
                      scalar($cgi->param('commentprivacy')), $timestamp, $work_time);
1556 1557 1558
        $bug_changed = 1;
    }

1559 1560 1561
    if (Bugzilla::Keyword::keyword_count() 
        && defined $cgi->param('keywords')) 
    {
1562 1563 1564 1565 1566
        # There are three kinds of "keywordsaction": makeexact, add, delete.
        # For makeexact, we delete everything, and then add our things.
        # For add, we delete things we're adding (to make sure we don't
        # end up having them twice), and then we add them.
        # For delete, we just delete things on the list.
1567
        my $changed = 0;
1568
        if ($keywordaction eq "makeexact") {
1569 1570
            $dbh->do(q{DELETE FROM keywords WHERE bug_id = ?},
                     undef, $id);
1571
            $changed = 1;
1572
        }
1573 1574 1575 1576 1577 1578
        my $sth_delete = $dbh->prepare(q{DELETE FROM keywords
                                               WHERE bug_id = ?
                                                 AND keywordid = ?});
        my $sth_insert =
            $dbh->prepare(q{INSERT INTO keywords (bug_id, keywordid)
                                 VALUES (?, ?)});
1579 1580
        foreach my $keyword (@keywordlist) {
            if ($keywordaction ne "makeexact") {
1581
                $sth_delete->execute($id, $keyword);
1582
                $changed = 1;
1583 1584
            }
            if ($keywordaction ne "delete") {
1585
                $sth_insert->execute($id, $keyword);
1586 1587 1588 1589
                $changed = 1;
            }
        }
        if ($changed) {
1590 1591 1592 1593 1594 1595 1596 1597
            my $list = $dbh->selectcol_arrayref(
                q{SELECT keyworddefs.name
                    FROM keyworddefs
              INNER JOIN keywords 
                      ON keyworddefs.id = keywords.keywordid
                   WHERE keywords.bug_id = ?
                ORDER BY keyworddefs.name},
                undef, $id);
1598
            $dbh->do("UPDATE bugs SET keywords = ? WHERE bug_id = ?",
1599
                     undef, join(', ', @$list), $id);
1600 1601
        }
    }
1602
    $query .= " WHERE bug_id = ?";
1603
    push(@bug_values, $id);
1604
    
1605
    if ($::comma ne "") {
1606
        $dbh->do($query, undef, @bug_values);
terry%netscape.com's avatar
terry%netscape.com committed
1607
    }
1608

1609
    # Check for duplicates if the bug is [re]open or its resolution is changed.
1610 1611
    my $resolution = $dbh->selectrow_array(
        q{SELECT resolution FROM bugs WHERE bug_id = ?}, undef, $id);
1612
    if ($resolution ne 'DUPLICATE') {
1613
        $dbh->do(q{DELETE FROM duplicates WHERE dupe = ?}, undef, $id);
1614
    }
1615

1616 1617
    my %groupsrequired = ();
    my %groupsforbidden = ();
1618 1619 1620 1621 1622 1623 1624
    my $group_controls =
        $dbh->selectall_arrayref(q{SELECT id, membercontrol
                                     FROM groups
                                LEFT JOIN group_control_map
                                       ON id = group_id
                                      AND product_id = ?
                                    WHERE isactive != 0},
1625
        undef, $oldhash{'product_id'});
1626 1627
    foreach my $group_control (@$group_controls) {
        my ($group, $control) = @$group_control;
1628
        $control ||= 0;
1629
        unless ($control > CONTROLMAPNA)  {
1630 1631
            $groupsforbidden{$group} = 1;
        }
1632
        if ($control == CONTROLMAPMANDATORY) {
1633 1634 1635 1636
            $groupsrequired{$group} = 1;
        }
    }

1637
    my @groupAddNames = ();
1638
    my @groupAddNamesAll = ();
1639 1640
    my $sth = $dbh->prepare(q{INSERT INTO bug_group_map (bug_id, group_id)
                                   VALUES (?, ?)});
1641 1642
    foreach my $grouptoadd (@groupAdd, keys %groupsrequired) {
        next if $groupsforbidden{$grouptoadd};
1643 1644
        my $group_obj = new Bugzilla::Group($grouptoadd);
        push(@groupAddNamesAll, $group_obj->name);
1645
        if (!BugInGroupId($id, $grouptoadd)) {
1646
            push(@groupAddNames, $group_obj->name);
1647
            $sth->execute($id, $grouptoadd);
1648 1649 1650
        }
    }
    my @groupDelNames = ();
1651
    my @groupDelNamesAll = ();
1652 1653
    $sth = $dbh->prepare(q{DELETE FROM bug_group_map
                                 WHERE bug_id = ? AND group_id = ?});
1654
    foreach my $grouptodel (@groupDel, keys %groupsforbidden) {
1655 1656
        my $group_obj = new Bugzilla::Group($grouptodel);
        push(@groupDelNamesAll, $group_obj->name);
1657
        next if $groupsrequired{$grouptodel};
1658
        if (BugInGroupId($id, $grouptodel)) {
1659
            push(@groupDelNames, $group_obj->name);
1660
        }
1661
        $sth->execute($id, $grouptodel);
1662 1663 1664 1665 1666
    }

    my $groupDelNames = join(',', @groupDelNames);
    my $groupAddNames = join(',', @groupAddNames);

1667 1668 1669 1670 1671
    if ($groupDelNames ne $groupAddNames) {
        LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames,
                         $whoid, $timestamp); 
        $bug_changed = 1;
    }
1672 1673

    my @ccRemoved = (); 
1674 1675 1676 1677
    if (defined $cgi->param('newcc')
        || defined $cgi->param('addselfcc')
        || defined $cgi->param('removecc')
        || defined $cgi->param('masscc')) {
1678 1679
        # Get the current CC list for this bug
        my %oncc;
1680 1681 1682 1683
        my $cc_list = $dbh->selectcol_arrayref(
            q{SELECT who FROM cc WHERE bug_id = ?}, undef, $id);
        foreach my $who (@$cc_list) {
            $oncc{$who} = 1;
1684 1685
        }

1686
        my (@added, @removed) = ();
1687 1688 1689

        my $sth_insert = $dbh->prepare(q{INSERT INTO cc (bug_id, who)
                                              VALUES (?, ?)});
1690 1691 1692
        foreach my $pid (keys %cc_add) {
            # If this person isn't already on the cc list, add them
            if (! $oncc{$pid}) {
1693
                $sth_insert->execute($id, $pid);
1694 1695
                push (@added, $cc_add{$pid});
                $oncc{$pid} = 1;
1696 1697
            }
        }
1698 1699
        my $sth_delete = $dbh->prepare(q{DELETE FROM cc
                                          WHERE bug_id = ? AND who = ?});
1700 1701 1702
        foreach my $pid (keys %cc_remove) {
            # If the person is on the cc list, remove them
            if ($oncc{$pid}) {
1703
                $sth_delete->execute($id, $pid);
1704 1705
                push (@removed, $cc_remove{$pid});
                $oncc{$pid} = 0;
1706 1707
            }
        }
1708

1709 1710
        # If any changes were found, record it in the activity log
        if (scalar(@removed) || scalar(@added)) {
1711 1712
            my $removed = join(", ", @removed);
            my $added = join(", ", @added);
1713 1714
            LogActivityEntry($id,"cc",$removed,$added,$whoid,$timestamp);
            $bug_changed = 1;
1715
        }
1716
        @ccRemoved = @removed;
1717
    }
1718

1719
    # We need to send mail for dependson/blocked bugs if the dependencies
1720 1721 1722
    # change or the status or resolution change. This var keeps track of that.
    my $check_dep_bugs = 0;

1723 1724 1725 1726 1727 1728 1729 1730
    foreach my $pair ("blocked/dependson", "dependson/blocked") {
        my ($me, $target) = split("/", $pair);

        my @oldlist = @{$dbh->selectcol_arrayref("SELECT $target FROM dependencies
                                                  WHERE $me = ? ORDER BY $target",
                                                  undef, $id)};
        @dependencychanged{@oldlist} = 1;

1731
        if (defined $cgi->param($target)) {
1732 1733
            my %snapshot;
            my @newlist = sort {$a <=> $b} @{$deps{$target}};
1734
            @dependencychanged{@newlist} = 1;
1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748

            while (0 < @oldlist || 0 < @newlist) {
                if (@oldlist == 0 || (@newlist > 0 &&
                                      $oldlist[0] > $newlist[0])) {
                    $snapshot{$newlist[0]} = SnapShotDeps($newlist[0], $me,
                                                          $target);
                    shift @newlist;
                } elsif (@newlist == 0 || (@oldlist > 0 &&
                                           $newlist[0] > $oldlist[0])) {
                    $snapshot{$oldlist[0]} = SnapShotDeps($oldlist[0], $me,
                                                          $target);
                    shift @oldlist;
                } else {
                    if ($oldlist[0] != $newlist[0]) {
1749
                        ThrowCodeError('list_comparison_error');
1750 1751 1752 1753 1754 1755 1756 1757
                    }
                    shift @oldlist;
                    shift @newlist;
                }
            }
            my @keys = keys(%snapshot);
            if (@keys) {
                my $oldsnap = SnapShotDeps($id, $target, $me);
1758 1759 1760 1761 1762
                $dbh->do(qq{DELETE FROM dependencies WHERE $me = ?},
                         undef, $id);
                my $sth =
                    $dbh->prepare(qq{INSERT INTO dependencies ($me, $target)
                                          VALUES (?, ?)});
1763
                foreach my $i (@{$deps{$target}}) {
1764
                    $sth->execute($id, $i);
1765 1766
                }
                foreach my $k (@keys) {
1767
                    LogDependencyActivity($k, $snapshot{$k}, $me, $target, $timestamp);
1768
                }
1769
                LogDependencyActivity($id, $oldsnap, $target, $me, $timestamp);
1770
                $check_dep_bugs = 1;
1771 1772 1773 1774
            }
        }
    }

1775 1776 1777 1778 1779
    # When a bug changes products and the old or new product is associated
    # with a bug group, it may be necessary to remove the bug from the old
    # group or add it to the new one.  There are a very specific series of
    # conditions under which these activities take place, more information
    # about which can be found in comments within the conditionals below.
1780
    # Check if the user has changed the product to which the bug belongs;
1781
    if ($cgi->param('product') ne $cgi->param('dontchange')
1782 1783
        && $cgi->param('product') ne $oldhash{'product'})
    {
1784 1785 1786 1787 1788 1789 1790 1791 1792
        # Depending on the "addtonewgroup" variable, groups with
        # defaults will change.
        #
        # For each group, determine
        # - The group id and if it is active
        # - The control map value for the old product and this group
        # - The control map value for the new product and this group
        # - Is the user in this group?
        # - Is the bug in this group?
1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809
        my $groups = $dbh->selectall_arrayref(
            qq{SELECT DISTINCT groups.id, isactive,
                               oldcontrolmap.membercontrol,
                               newcontrolmap.membercontrol,
                      CASE WHEN groups.id IN ($grouplist) THEN 1 ELSE 0 END,
                      CASE WHEN bug_group_map.group_id IS NOT NULL
                                THEN 1 ELSE 0 END
                 FROM groups
            LEFT JOIN group_control_map AS oldcontrolmap
                   ON oldcontrolmap.group_id = groups.id
                  AND oldcontrolmap.product_id = ?
            LEFT JOIN group_control_map AS newcontrolmap
                   ON newcontrolmap.group_id = groups.id
                  AND newcontrolmap.product_id = ?
            LEFT JOIN bug_group_map
                   ON bug_group_map.group_id = groups.id
                  AND bug_group_map.bug_id = ?},
1810
            undef, $oldhash{'product_id'}, $product->id, $id);
1811 1812 1813 1814 1815 1816 1817
        my @groupstoremove = ();
        my @groupstoadd = ();
        my @defaultstoremove = ();
        my @defaultstoadd = ();
        my @allgroups = ();
        my $buginanydefault = 0;
        my $buginanychangingdefault = 0;
1818 1819 1820
        foreach my $group (@$groups) {
            my ($groupid, $isactive, $oldcontrol, $newcontrol,
                   $useringroup, $bugingroup) = @$group;
1821 1822 1823 1824 1825 1826 1827 1828
            # An undefined newcontrol is none.
            $newcontrol = CONTROLMAPNA unless $newcontrol;
            $oldcontrol = CONTROLMAPNA unless $oldcontrol;
            push(@allgroups, $groupid);
            if (($bugingroup) && ($isactive)
                && ($oldcontrol == CONTROLMAPDEFAULT)) {
                # Bug was in a default group.
                $buginanydefault = 1;
1829 1830
                if (($newcontrol != CONTROLMAPDEFAULT)
                    && ($newcontrol != CONTROLMAPMANDATORY)) {
1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848
                    # Bug was in a default group that no longer is.
                    $buginanychangingdefault = 1;
                    push (@defaultstoremove, $groupid);
                }
            }
            if (($isactive) && (!$bugingroup)
                && ($newcontrol == CONTROLMAPDEFAULT)
                && ($useringroup)) {
                push (@defaultstoadd, $groupid);
            }
            if (($bugingroup) && ($isactive) && ($newcontrol == CONTROLMAPNA)) {
                # Group is no longer permitted.
                push(@groupstoremove, $groupid);
            }
            if ((!$bugingroup) && ($isactive) 
                && ($newcontrol == CONTROLMAPMANDATORY)) {
                # Group is now required.
                push(@groupstoadd, $groupid);
1849
            }
1850
        }
1851 1852 1853 1854 1855 1856 1857 1858 1859
        # If addtonewgroups = "yes", old default groups will be removed
        # and new default groups will be added.
        # If addtonewgroups = "yesifinold", old default groups will be removed
        # and new default groups will be added only if the bug was in ANY
        # of the old default groups.
        # If addtonewgroups = "no", old default groups will be removed and not
        # replaced.
        push(@groupstoremove, @defaultstoremove);
        if (AnyDefaultGroups()
1860 1861
            && (($cgi->param('addtonewgroup') eq 'yes')
            || (($cgi->param('addtonewgroup') eq 'yesifinold')
1862 1863
            && ($buginanydefault)))) {
            push(@groupstoadd, @defaultstoadd);
1864 1865
        }

1866 1867 1868
        # Now actually update the bug_group_map.
        my @DefGroupsAdded = ();
        my @DefGroupsRemoved = ();
1869 1870 1871 1872 1873 1874
        my $sth_insert =
            $dbh->prepare(q{INSERT INTO bug_group_map (bug_id, group_id)
                                 VALUES (?, ?)});
        my $sth_delete = $dbh->prepare(q{DELETE FROM bug_group_map
                                               WHERE bug_id = ?
                                                 AND group_id = ?});
1875 1876 1877 1878
        foreach my $groupid (@allgroups) {
            my $thisadd = grep( ($_ == $groupid), @groupstoadd);
            my $thisdel = grep( ($_ == $groupid), @groupstoremove);
            if ($thisadd) {
1879 1880
                my $group_obj = new Bugzilla::Group($groupid);
                push(@DefGroupsAdded, $group_obj->name);
1881
                $sth_insert->execute($id, $groupid);
1882
            } elsif ($thisdel) {
1883 1884
                my $group_obj = new Bugzilla::Group($groupid);
                push(@DefGroupsRemoved, $group_obj->name);
1885
                $sth_delete->execute($id, $groupid);
1886 1887 1888 1889 1890 1891 1892 1893
            }
        }
        if ((@DefGroupsAdded) || (@DefGroupsRemoved)) {
            LogActivityEntry($id, "bug_group",
                join(', ', @DefGroupsRemoved),
                join(', ', @DefGroupsAdded),
                     $whoid, $timestamp); 
        }
1894 1895
    }
  
1896 1897 1898 1899
    # get a snapshot of the newly set values out of the database, 
    # and then generate any necessary bug activity entries by seeing 
    # what has changed since before we wrote out the new values.
    #
1900
    my $new_bug_obj = new Bugzilla::Bug($id, $whoid);
1901
    my @newvalues = SnapShotBug($id);
1902 1903
    my %newhash;
    $i = 0;
1904
    foreach my $col (@editable_bug_fields) {
1905
        # Consider NULL db entries to be equivalent to the empty string
1906
        $newvalues[$i] = defined($newvalues[$i]) ? $newvalues[$i] : '';
1907 1908 1909 1910
        # Convert the deadline to the YYYY-MM-DD format.
        if ($col eq 'deadline') {
            $newvalues[$i] = format_time($newvalues[$i], "%Y-%m-%d");
        }
1911 1912 1913
        $newhash{$col} = $newvalues[$i];
        $i++;
    }
1914
    # for passing to Bugzilla::BugMail to ensure that when someone is removed
1915 1916 1917 1918
    # from one of these fields, they get notified of that fact (if desired)
    #
    my $origOwner = "";
    my $origQaContact = "";
1919 1920 1921 1922

    # $msgs will store emails which have to be sent to voters, if any.
    my $msgs;

1923
    foreach my $c (@editable_bug_fields) {
1924 1925
        my $col = $c;           # We modify it, don't want to modify array
                                # values in place.
1926 1927 1928
        my $old = shift @oldvalues;
        my $new = shift @newvalues;
        if ($old ne $new) {
1929

1930 1931 1932
            # Products and components are now stored in the DB using ID's
            # We need to translate this to English before logging it
            if ($col eq 'product_id') {
1933 1934
                $old = $old_bug_obj->product;
                $new = $new_bug_obj->product;
1935 1936 1937
                $col = 'product';
            }
            if ($col eq 'component_id') {
1938 1939
                $old = $old_bug_obj->component;
                $new = $new_bug_obj->component;
1940 1941 1942
                $col = 'component';
            }

1943
            # save off the old value for passing to Bugzilla::BugMail so
1944
            # the old assignee can be notified
1945 1946
            #
            if ($col eq 'assigned_to') {
1947 1948
                $old = ($old) ? user_id_to_login($old) : "";
                $new = ($new) ? user_id_to_login($new) : "";
1949 1950 1951 1952 1953 1954
                $origOwner = $old;
            }

            # ditto for the old qa contact
            #
            if ($col eq 'qa_contact') {
1955 1956
                $old = ($old) ? user_id_to_login($old) : "";
                $new = ($new) ? user_id_to_login($new) : "";
1957
                $origQaContact = $old;
terry%netscape.com's avatar
terry%netscape.com committed
1958
            }
1959

1960 1961
            # If this is the keyword field, only record the changes, not everything.
            if ($col eq 'keywords') {
1962
                ($old, $new) = diff_strings($old, $new);
1963 1964
            }

1965
            if ($col eq 'product') {
1966 1967 1968 1969 1970
                # If some votes have been removed, RemoveVotes() returns
                # a list of messages to send to voters.
                # We delay the sending of these messages till tables are unlocked.
                $msgs = RemoveVotes($id, 0,
                          "This bug has been moved to a different product");
1971
            }
1972

1973
            if ($col eq 'bug_status' 
1974
                && is_open_state($old) ne is_open_state($new))
1975 1976 1977
            {
                $check_dep_bugs = 1;
            }
1978

1979 1980
            LogActivityEntry($id,$col,$old,$new,$whoid,$timestamp);
            $bug_changed = 1;
terry%netscape.com's avatar
terry%netscape.com committed
1981 1982
        }
    }
1983
    # Set and update flags.
1984
    Bugzilla::Flag::process($new_bug_obj, undef, $timestamp, $cgi);
1985

1986
    if ($bug_changed) {
1987 1988
        $dbh->do(q{UPDATE bugs SET delta_ts = ? WHERE bug_id = ?},
                 undef, $timestamp, $id);
1989
    }
1990
    $dbh->bz_unlock_tables();
1991

1992 1993
    # Now is a good time to send email to voters.
    foreach my $msg (@$msgs) {
1994
        MessageToMTA($msg);
1995 1996
    }

1997
    if ($duplicate) {
1998 1999 2000 2001 2002
        # If the bug was already marked as a duplicate, remove
        # the existing entry.
        $dbh->do('DELETE FROM duplicates WHERE dupe = ?',
                  undef, $cgi->param('id'));

2003
        # Check to see if Reporter of this bug is reporter of Dupe 
2004 2005 2006 2007 2008 2009 2010 2011
        my $reporter = $dbh->selectrow_array(
            q{SELECT reporter FROM bugs WHERE bug_id = ?}, undef, $id);
        my $isreporter = $dbh->selectrow_array(
            q{SELECT reporter FROM bugs WHERE bug_id = ? AND reporter = ?},
            undef, $duplicate, $reporter);
        my $isoncc = $dbh->selectrow_array(q{SELECT who FROM cc
                                           WHERE bug_id = ? AND who = ?},
                                           undef, $duplicate, $reporter);
2012 2013
        unless ($isreporter || $isoncc
                || !$cgi->param('confirm_add_duplicate')) {
matty%chariot.net.au's avatar
matty%chariot.net.au committed
2014
            # The reporter is oblivious to the existence of the new bug and is permitted access
2015
            # ... add 'em to the cc (and record activity)
2016
            LogActivityEntry($duplicate,"cc","",user_id_to_login($reporter),
2017
                             $whoid,$timestamp);
2018 2019
            $dbh->do(q{INSERT INTO cc (who, bug_id) VALUES (?, ?)},
                     undef, $reporter, $duplicate);
2020
        }
2021
        # Bug 171639 - Duplicate notifications do not need to be private. 
2022
        AppendComment($duplicate, $whoid,
2023 2024
                      "*** Bug " . $cgi->param('id') .
                      " has been marked as a duplicate of this bug. ***",
2025 2026
                      0, $timestamp);

2027 2028
        $dbh->do(q{INSERT INTO duplicates VALUES (?, ?)}, undef,
                 $duplicate, $cgi->param('id'));
2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040
    }

    # Now all changes to the DB have been made. It's time to email
    # all concerned users, including the bug itself, but also the
    # duplicated bug and dependent bugs, if any.

    $vars->{'mailrecipients'} = { 'cc' => \@ccRemoved,
                                  'owner' => $origOwner,
                                  'qacontact' => $origQaContact,
                                  'changer' => Bugzilla->user->login };

    $vars->{'id'} = $id;
2041
    $vars->{'type'} = "bug";
2042 2043 2044 2045 2046 2047 2048 2049
    
    # Let the user know the bug was changed and who did and didn't
    # receive email about the change.
    $template->process("bug/process/results.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
    $vars->{'header_done'} = 1;
    
    if ($duplicate) {
2050
        $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; 
2051

2052 2053 2054 2055
        $vars->{'id'} = $duplicate;
        $vars->{'type'} = "dupe";
        
        # Let the user know a duplication notation was added to the original bug.
2056
        $template->process("bug/process/results.html.tmpl", $vars)
2057
          || ThrowTemplateError($template->error());
2058
        $vars->{'header_done'} = 1;
2059 2060
    }

2061 2062
    if ($check_dep_bugs) {
        foreach my $k (keys(%dependencychanged)) {
2063
            $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; 
2064 2065 2066
            $vars->{'id'} = $k;
            $vars->{'type'} = "dep";

2067 2068 2069
            # Let the user (if he is able to see the bug) know we checked to see 
            # if we should email notice of this change to users with a relationship
            # to the dependent bug and who did and didn't receive email about it.
2070 2071
            $template->process("bug/process/results.html.tmpl", $vars)
              || ThrowTemplateError($template->error());
2072
            $vars->{'header_done'} = 1;
2073
        }
2074
    }
terry%netscape.com's avatar
terry%netscape.com committed
2075 2076
}

2077 2078 2079 2080 2081 2082
# Determine if Patch Viewer is installed, for Diff link
# (NB: Duplicate code with show_bug.cgi.)
eval {
    require PatchReader;
    $vars->{'patchviewerinstalled'} = 1;
};
2083

2084 2085 2086 2087 2088 2089
if (defined $cgi->param('id')) {
    $action = Bugzilla->user->settings->{'post_bug_submit_action'}->{'value'};
} else {
    # param('id') is not defined when changing multiple bugs
    $action = 'nothing';
}
2090

2091 2092 2093 2094 2095 2096 2097 2098 2099 2100
if ($action eq 'next_bug') {
    my $next_bug;
    my $cur = lsearch(\@bug_list, $cgi->param("id"));
    if ($cur >= 0 && $cur < $#bug_list) {
        $next_bug = $bug_list[$cur + 1];
    }
    if ($next_bug) {
        if (detaint_natural($next_bug) && Bugzilla->user->can_see_bug($next_bug)) {
            my $bug = new Bugzilla::Bug($next_bug, $whoid);
            ThrowCodeError("bug_error", { bug => $bug }) if $bug->error;
2101

2102 2103
            $vars->{'bugs'} = [$bug];
            $vars->{'nextbug'} = $bug->bug_id;
2104

2105 2106 2107 2108 2109
            $template->process("bug/show.html.tmpl", $vars)
              || ThrowTemplateError($template->error());

            exit;
        }
2110
    }
2111
} elsif ($action eq 'same_bug') {
2112 2113 2114
    if (Bugzilla->user->can_see_bug($cgi->param('id'))) {
        my $bug = new Bugzilla::Bug($cgi->param('id'), $whoid);
        ThrowCodeError("bug_error", { bug => $bug }) if $bug->error;
2115

2116
        $vars->{'bugs'} = [$bug];
2117

2118 2119
        $template->process("bug/show.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
2120

2121 2122
        exit;
    }
2123 2124
} elsif ($action ne 'nothing') {
    ThrowCodeError("invalid_post_bug_submit_action");
terry%netscape.com's avatar
terry%netscape.com committed
2125
}
2126

2127
# End the response page.
2128
$template->process("bug/navigate.html.tmpl", $vars)
2129
  || ThrowTemplateError($template->error());
2130
$template->process("global/footer.html.tmpl", $vars)
2131
  || ThrowTemplateError($template->error());