process_bug.cgi 66.2 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>
terry%netscape.com's avatar
terry%netscape.com committed
26

27 28
use strict;

29 30 31
my $UserInEditGroupSet = -1;
my $UserInCanConfirmGroupSet = -1;

32 33
use lib qw(.);

34
use Bugzilla;
35
use Bugzilla::Constants;
36
require "CGI.pl";
37

38
use Bugzilla::Bug;
39
use Bugzilla::User;
40
use Bugzilla::RelationSet;
41

42 43 44
# Use the Flag module to modify flag data if the user set flags.
use Bugzilla::Flag;

45 46
# Shut up misguided -w warnings about "used only once":

47 48 49 50 51 52 53 54
use vars qw(%versions
          %components
          %legal_opsys
          %legal_platform
          %legal_priority
          %settable_resolution
          %target_milestone
          %legal_severity
55
           );
56

57
my $user = Bugzilla->login(LOGIN_REQUIRED);
58
my $whoid = $user->id;
59

60 61
my $cgi = Bugzilla->cgi;

62 63
my $requiremilestone = 0;

64 65
use vars qw($template $vars);

66 67 68 69 70 71 72 73
######################################################################
# 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.
74 75
# 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.
76 77
my @idlist;
if (defined $::FORM{'id'}) {
78
    ValidateBugID($::FORM{'id'});
79
    push @idlist, $::FORM{'id'};
80
} else {
81 82
    foreach my $i (keys %::FORM) {
        if ($i =~ /^id_([1-9][0-9]*)/) {
83 84 85
            my $id = $1;
            ValidateBugID($id);
            push @idlist, $id;
86
        }
87 88 89
    }
}

90
# Make sure there are bugs to process.
91
scalar(@idlist) || ThrowUserError("no_bugs_chosen");
92

93 94 95 96 97 98 99 100 101 102
# Validate all timetracking fields
foreach my $field ("estimated_time", "work_time", "remaining_time") {
    if (defined $::FORM{$field}) {
        my $er_time = trim($::FORM{$field});
        if ($er_time ne $::FORM{'dontchange'}) {
            Bugzilla::Bug::ValidateTime($er_time, $field);
        }
    }
}

103 104
# do a match on the fields if applicable

105 106 107
# 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.
108
&Bugzilla::User::match_field({
109 110
    'qa_contact'                => { 'type' => 'single' },
    'newcc'                     => { 'type' => 'multi'  },
111
    'masscc'                    => { 'type' => 'multi'  },
112 113
    'assigned_to'               => { 'type' => 'single' },
    '^requestee(_type)?-(\d+)$' => { 'type' => 'single' },
114
});
115 116 117 118 119 120
# Validate flags, but only if the user is changing a single bug,
# since the multi-change form doesn't include flag changes.
if (defined $::FORM{'id'}) {
    Bugzilla::Flag::validate(\%::FORM, $::FORM{'id'});
    Bugzilla::FlagType::validate(\%::FORM, $::FORM{'id'});
}
121

122 123 124 125 126 127 128 129 130 131
# If we are duping bugs, let's also make sure that we can change 
# the original.  This takes care of issue A on bug 96085.
if (defined $::FORM{'dup_id'} && $::FORM{'knob'} eq "duplicate") {
    ValidateBugID($::FORM{'dup_id'});

    # 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();
}

132 133
ValidateComment($::FORM{'comment'});

134 135
$::FORM{'dontchange'} = '' unless exists $::FORM{'dontchange'};

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
# 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") {
    if (defined($::FORM{$field}) && $::FORM{$field} ne "") {
        my @validvalues;
        foreach my $id (split(/[\s,]+/, $::FORM{$field})) {
            next unless $id;
            ValidateBugID($id, 1);
            push(@validvalues, $id);
        }
        $::FORM{$field} = join(",", @validvalues);
    }
}

154 155 156 157
######################################################################
# End Data/Security Validation
######################################################################

158
print $cgi->header();
159
$vars->{'title_tag'} = "bug_processed";
160 161 162 163 164 165 166 167 168 169 170

# 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.
if (defined($::FORM{'id'})) {
    SendSQL("SELECT delta_ts FROM bugs WHERE bug_id = $::FORM{'id'}");
    my $delta_ts = FetchOneColumn();
    
    if (defined $::FORM{'delta_ts'} && $delta_ts && 
        $::FORM{'delta_ts'} ne $delta_ts) 
    {
171
        $vars->{'title_tag'} = "mid_air";
172 173
    }
}
174

175 176
# Set up the vars for nagiavtional <link> elements
my $next_bug;
177 178
if ($cgi->cookie("BUGLIST") && $::FORM{'id'}) {
    my @bug_list = split(/:/, $cgi->cookie("BUGLIST"));
179 180 181 182 183 184 185 186 187 188 189 190 191
    $vars->{'bug_list'} = \@bug_list;
    my $cur = lsearch(\@bug_list, $::FORM{"id"});
    if ($cur >= 0 && $cur < $#bug_list) {
        $next_bug = $bug_list[$cur + 1];

        # Note that we only bother with the bug_id here, and get
        # the full bug object at the end, before showing the edit
        # page. If you change this, remember that we have not
        # done the security checks on the next bug yet
        $vars->{'bug'} = { bug_id => $next_bug };
    }
}

192 193
GetVersionTable();

194 195 196
CheckFormFieldDefined(\%::FORM, 'product');
CheckFormFieldDefined(\%::FORM, 'version');
CheckFormFieldDefined(\%::FORM, 'component');
197

198 199 200
# check if target milestone is defined - matthew@zeroknowledge.com
if ( Param("usetargetmilestone") ) {
  CheckFormFieldDefined(\%::FORM, 'target_milestone');
201 202
}

203 204 205
#
# This function checks if there is a comment required for a specific
# function and tests, if the comment was given.
206
# If comments are required for functions is defined by params.
207 208 209 210 211 212 213 214 215 216 217 218 219
#
sub CheckonComment( $ ) {
    my ($function) = (@_);
    
    # Param is 1 if comment should be added !
    my $ret = Param( "commenton" . $function );

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

    if( $ret ) {
        if (!defined $::FORM{'comment'} || $::FORM{'comment'} =~ /^\s*$/) {
            # No comment - sorry, action not allowed !
220
            ThrowUserError("comment_required");
221 222 223 224 225 226 227
        } else {
            $ret = 0;
        }
    }
    return( ! $ret ); # Return val has to be inverted
}

228 229 230 231 232 233
# 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.
if ( $::FORM{'id'} ) {
234 235
    SendSQL("SELECT name FROM products, bugs " .
            "WHERE products.id = bugs.product_id AND bug_id = $::FORM{'id'}");
236 237
    $::oldproduct = FetchSQLData();
}
238
if ((($::FORM{'id'} && $::FORM{'product'} ne $::oldproduct) 
239
     || (!$::FORM{'id'} && $::FORM{'product'} ne $::FORM{'dontchange'}))
240 241
    && CheckonComment( "reassignbycomponent" ))
{
242 243 244 245 246
    # Check to make sure they actually have the right to change the product
    if (!CheckCanChangeField('product', $::FORM{'id'}, $::oldproduct, $::FORM{'product'})) {
        $vars->{'oldvalue'} = $::oldproduct;
        $vars->{'newvalue'} = $::FORM{'product'};
        $vars->{'field'} = 'product';
247 248 249 250 251 252
        ThrowUserError("illegal_change",
                       { oldvalue => $::oldproduct,
                         newvalue => $::FORM{'product'},
                         field => 'product',
                       },
                       "abort");
253 254
    }
 
255
    CheckFormField(\%::FORM, 'product', \@::legal_product);
256
    my $prod = $::FORM{'product'};
257 258 259 260 261 262 263 264 265

    # 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.
    #
266 267
    my $vok = lsearch($::versions{$prod}, $::FORM{'version'}) >= 0;
    my $cok = lsearch($::components{$prod}, $::FORM{'component'}) >= 0;
268 269 270 271 272 273

    my $mok = 1;   # so it won't affect the 'if' statement if milestones aren't used
    if ( Param("usetargetmilestone") ) {
       $mok = lsearch($::target_milestone{$prod}, $::FORM{'target_milestone'}) >= 0;
    }

274 275 276
    # 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.
277
    if (!$vok || !$cok || !$mok || (AnyDefaultGroups() && !defined($::FORM{'addtonewgroup'}))) {
278
        $vars->{'form'} = \%::FORM;
279
        $vars->{'mform'} = \%::MFORM;
280
        
281
        if (!$vok || !$cok || !$mok) {
282
            $vars->{'verify_fields'} = 1;
283 284 285 286
            my %defaults;
            # We set the defaults to these fields to the old value,
            # if its a valid option, otherwise we use the default where
            # thats appropriate
287
            $vars->{'versions'} = $::versions{$prod};
288 289 290
            if (lsearch($::versions{$prod}, $::FORM{'version'}) != -1) {
                $defaults{'version'} = $::FORM{'version'};
            }
291
            $vars->{'components'} = $::components{$prod};
292 293 294 295
            if (lsearch($::components{$prod}, $::FORM{'component'}) != -1) {
                $defaults{'component'} = $::FORM{'component'};
            }

296 297 298
            if (Param("usetargetmilestone")) {
                $vars->{'use_target_milestone'} = 1;
                $vars->{'milestones'} = $::target_milestone{$prod};
299 300 301 302 303
                if (lsearch($::target_milestone{$prod},
                            $::FORM{'target_milestone'}) != -1) {
                    $defaults{'target_milestone'} = $::FORM{'target_milestone'};
                } else {
                    SendSQL("SELECT defaultmilestone FROM products WHERE " .
304
                            "name = " . SqlQuote($prod));
305 306
                    $defaults{'target_milestone'} = FetchOneColumn();
                }
307
            }
308 309
            else {
                $vars->{'use_target_milestone'} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
310
            }
311
            $vars->{'defaults'} = \%defaults;
312
        }
313 314
        else {
            $vars->{"verify_fields"} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
315
        }
316
        
317
        $vars->{'verify_bug_group'} = (AnyDefaultGroups() 
318 319
                                       && !defined($::FORM{'addtonewgroup'}));
        
320
        $template->process("bug/process/verify-new-product.html.tmpl", $vars)
321
          || ThrowTemplateError($template->error());
322
        exit;
terry%netscape.com's avatar
terry%netscape.com committed
323 324 325 326
    }
}


327 328 329 330 331 332 333 334 335
# Checks that the user is allowed to change the given field.  Actually, right
# now, the rules are pretty simple, and don't look at the field itself very
# much, but that could be enhanced.

my $lastbugid = 0;
my $ownerid;
my $reporterid;
my $qacontactid;

336 337 338
################################################################################
# CheckCanChangeField() defines what users are allowed to change what bugs. You
# can add code here for site-specific policy changes, according to the 
339
# instructions given in the Bugzilla Guide and below. Note that you may also
340 341
# have to update the Bugzilla::Bug::user() function to give people access to the
# options that they are permitted to change.
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
#
# CheckCanChangeField() should return true if the user is allowed to change this
# field, and false if they are not.
#
# The parameters to this function are as follows:
# $field    - name of the field in the bugs table the user is trying to change
# $bugid    - the ID of the bug they are changing
# $oldvalue - what they are changing it from
# $newvalue - what they are changing it to
#
# Note that this function is currently not called for dependency changes 
# (bug 141593) or CC changes, which means anyone can change those fields.
#
# Do not change the sections between START DO_NOT_CHANGE and END DO_NOT_CHANGE.
################################################################################
357
sub CheckCanChangeField {
358 359 360 361 362 363 364 365
    # START DO_NOT_CHANGE
    my ($field, $bugid, $oldvalue, $newvalue) = (@_);

    # Convert email IDs into addresses for $oldvalue
    if (($field eq "assigned_to") || 
        ($field eq "reporter") || 
        ($field eq "qa_contact")) 
    {
366 367 368 369 370 371 372 373
        if ($oldvalue =~ /^\d+$/) {
            if ($oldvalue == 0) {
                $oldvalue = "";
            } else {
                $oldvalue = DBID_to_name($oldvalue);
            }
        }
    }
374

375
    # Return true if they haven't changed this field at all.
376 377
    if ($oldvalue eq $newvalue) {
        return 1;
378 379
    }    
    elsif (trim($oldvalue) eq trim($newvalue)) {
380
        return 1;
381 382 383 384
    # numeric fields need to be compared using == 
    } elsif (($field eq "estimated_time" || $field eq "remaining_time") &&
             $oldvalue == $newvalue) {
        return 1;
385
    }
386 387
        
    
388 389 390 391 392 393
    
    # A resolution change is always accompanied by a status change. So, we 
    # always OK resolution changes; if they really can't do this, we will 
    # notice it when status is checked. 
    if ($field eq "resolution") { 
        return 1;             
394
    }
395 396 397 398 399
    # END DO_NOT_CHANGE

    # Allow anyone to change comments.
    if ($field =~ /^longdesc/) {
        return 1;
400
    }
401 402 403 404 405
    
    # START DO_NOT_CHANGE
    # Find out whether the user is a member of the "editbugs" and/or
    # "canconfirm" groups. $UserIn*GroupSet are caches of the return value of 
    # the UserInGroup calls.
406 407 408
    if ($UserInEditGroupSet < 0) {
        $UserInEditGroupSet = UserInGroup("editbugs");
    }
409 410 411 412 413 414 415
    
    if ($UserInCanConfirmGroupSet < 0) {
        $UserInCanConfirmGroupSet = UserInGroup("canconfirm");
    }
    # END DO_NOT_CHANGE
    
    # Allow anyone with "editbugs" to change anything.
416 417 418
    if ($UserInEditGroupSet) {
        return 1;
    }
419 420
    
    # Allow anyone with "canconfirm" to confirm bugs.
421 422 423 424 425 426 427 428
    if ($UserInCanConfirmGroupSet) {
        if (($field eq "canconfirm") ||
            (($field eq "bug_status") && 
             ($oldvalue eq $::unconfirmedstate) &&
             IsOpenedState($newvalue)))
        {
            return 1;
        }
429 430 431 432 433
    }
    
    # START DO_NOT_CHANGE
    # $reporterid, $ownerid and $qacontactid are caches of the results of
    # the call to find out the owner, reporter and qacontact of the current bug.
434
    if ($lastbugid != $bugid) {
435 436
        SendSQL("SELECT reporter, assigned_to, qa_contact FROM bugs
                 WHERE bug_id = $bugid");
437
        ($reporterid, $ownerid, $qacontactid) = (FetchSQLData());
438 439 440 441 442 443
    }    
    # END DO_NOT_CHANGE

    # Allow the owner to change anything.
    if ($ownerid eq $whoid) {
        return 1;
444
    }
445 446
    
    # Allow the QA contact to change anything.
447
    if (Param('useqacontact') && ($qacontactid eq $whoid)) {
448 449
        return 1;
    }
450 451 452 453 454 455 456 457 458 459 460
    
    # The reporter's a more complicated case...
    if ($reporterid eq $whoid) {
        # Reporter may not:
        # - confirm his own bugs (this assumes he doesn't have canconfirm, or we
        #   would have returned "1" earlier)
        if (($field eq "bug_status") && 
            ($oldvalue eq $::unconfirmedstate) &&
             IsOpenedState($newvalue))
        {
            return 0;
461
        }
462 463 464 465 466 467 468 469 470 471 472
        
        # - change the target milestone            
        if  ($field eq "target_milestone")  {
            return 0;
        }       
        
        # - change the priority (unless he could have set it originally)
        if (($field eq "priority") &&
            !Param('letsubmitterchoosepriority'))
        {
            return 0;
473
        }
474 475
        
        # Allow reporter to change anything else.
476
        return 1;
477
    }
478 479 480 481
  
    # If we haven't returned by this point, then the user doesn't have the
    # necessary permissions to change this field.
    return 0;
482 483
}

484 485
# Confirm that the reporter of the current bug can access the bug we are duping to.
sub DuplicateUserConfirm {
486 487 488 489 490
    # if we've already been through here, then exit
    if (defined $::FORM{'confirm_add_duplicate'}) {
        return;
    }

491 492 493 494 495
    my $dupe = trim($::FORM{'id'});
    my $original = trim($::FORM{'dup_id'});
    
    SendSQL("SELECT reporter FROM bugs WHERE bug_id = " . SqlQuote($dupe));
    my $reporter = FetchOneColumn();
496
    my $rep_user = Bugzilla::User->new($reporter);
497

498
    if ($rep_user->can_see_bug($original)) {
499 500 501
        $::FORM{'confirm_add_duplicate'} = "1";
        return;
    }
502 503

    SendSQL("SELECT cclist_accessible FROM bugs WHERE bug_id = $original");
504
    $vars->{'cclist_accessible'} = FetchOneColumn();
505
    
506 507 508
    # 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.
509
    
510
    $vars->{'form'} = \%::FORM;
511
    $vars->{'mform'} = \%::MFORM;
512 513 514 515 516
    $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).
517
    print Bugzilla->cgi->header();
518
    $template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
519
      || ThrowTemplateError($template->error());
520 521
    exit;
} # end DuplicateUserConfirm()
522

523
if (defined $::FORM{'id'}) {
524 525 526 527 528 529 530
    # 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...)
    #
531 532 533 534 535 536 537 538 539 540 541 542
    CheckFormField(\%::FORM, 'rep_platform', \@::legal_platform);
    CheckFormField(\%::FORM, 'priority', \@::legal_priority);
    CheckFormField(\%::FORM, 'bug_severity', \@::legal_severity);
    CheckFormField(\%::FORM, 'component', 
                   \@{$::components{$::FORM{'product'}}});
    CheckFormFieldDefined(\%::FORM, 'bug_file_loc');
    CheckFormFieldDefined(\%::FORM, 'short_desc');
    CheckFormField(\%::FORM, 'product', \@::legal_product);
    CheckFormField(\%::FORM, 'version', 
                   \@{$::versions{$::FORM{'product'}}});
    CheckFormField(\%::FORM, 'op_sys', \@::legal_opsys);
    CheckFormFieldDefined(\%::FORM, 'longdesclength');
543 544
    
    if (trim($::FORM{'short_desc'}) eq "") {
545
        ThrowUserError("require_summary");
546
    }
terry%netscape.com's avatar
terry%netscape.com committed
547 548
}

549
my $action = '';
550
if (defined $::FORM{action}) {
551
  $action = trim($::FORM{action});
552
}
553
if (Param("move-enabled") && $action eq Param("move-button-text")) {
554 555 556 557 558 559 560
  $::FORM{'buglist'} = join (":", @idlist);
  do "move.pl" || die "Error executing move.cgi: $!";
  PutFooter();
  exit;
}


561 562 563 564 565 566 567
$::query = "update bugs\nset";
$::comma = "";
umask(0);

sub DoComma {
    $::query .= "$::comma\n    ";
    $::comma = ",";
terry%netscape.com's avatar
terry%netscape.com committed
568 569
}

570
sub DoConfirm {
571
    if (CheckCanChangeField("canconfirm", $::FORM{'id'}, 0, 1)) {
572 573 574 575 576 577
        DoComma();
        $::query .= "everconfirmed = 1";
    }
}


578 579
sub ChangeStatus {
    my ($str) = (@_);
580 581
    if (!$::FORM{'dontchange'} ||
       ($str ne $::FORM{'dontchange'})) {
582
        DoComma();
583 584 585
        if ($::FORM{knob} eq 'reopen') {
            # When reopening, we need to check whether the bug was ever
            # confirmed or not
586 587 588
            $::query .= "bug_status = CASE WHEN everconfirmed = 1 THEN " .
                         SqlQuote($str) . " ELSE " .
                         SqlQuote($::unconfirmedstate) . " END";
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
        } elsif (IsOpenedState($str)) {
            # 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
            # from closed to open, and its handled above
            # This also relies on the fact that confirming and accepting have
            # already called DoConfirm before this is called

615 616
            my @open_state = map(SqlQuote($_), OpenStates());
            my $open_state = join(", ", @open_state);
617 618 619 620 621
            $::query .= "bug_status = CASE WHEN bug_status IN($open_state) THEN " .
                                        "(CASE WHEN everconfirmed = 1 THEN " .
                                            SqlQuote($str) . " ELSE " .
                                            SqlQuote($::unconfirmedstate) . " END) ELSE " .
                                        "bug_status END";
622
        } else {
623
            $::query .= "bug_status = " . SqlQuote($str);
624
        }
625 626 627
        $::FORM{'bug_status'} = $str; # Used later for call to
                                      # CheckCanChangeField to make sure this
                                      # is really kosher.
terry%netscape.com's avatar
terry%netscape.com committed
628 629 630
    }
}

631 632
sub ChangeResolution {
    my ($str) = (@_);
633 634
    if (!$::FORM{'dontchange'} ||
       ($str ne $::FORM{'dontchange'})) {
635
        DoComma();
636
        $::query .= "resolution = " . SqlQuote($str);
637
        $::FORM{'resolution'} = $str; # Used later by CheckCanChangeField
terry%netscape.com's avatar
terry%netscape.com committed
638 639 640
    }
}

641 642 643 644
# 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.
645 646
# If the form element isn't present, or the user isn't in the group, leave
# it as-is
647

648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666
my @groupAdd = ();
my @groupDel = ();

SendSQL("SELECT groups.id, isactive FROM groups, user_group_map WHERE " .
        "groups.id = user_group_map.group_id AND " .
        "user_group_map.user_id = $::userid AND " .
        "isbless = 0 AND isbuggroup = 1");
while (my ($b, $isactive) = FetchSQLData()) {
    # 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.
    # All the checkboxes should be shown in that case, though, so its not
    # an issue there
    if ($::FORM{'id'} || exists $::FORM{"bit-$b"}) {
        if (!$::FORM{"bit-$b"}) {
            push(@groupDel, $b);
        } elsif ($::FORM{"bit-$b"} == 1 && $isactive) {
            push(@groupAdd, $b);
667 668
        }
    }
669 670
}

671
foreach my $field ("rep_platform", "priority", "bug_severity",          
672 673
                   "summary", "bug_file_loc", "short_desc",
                   "version", "op_sys",
674
                   "target_milestone", "status_whiteboard") {
675
    if (defined $::FORM{$field}) {
676 677
        if (!$::FORM{'dontchange'}
            || $::FORM{$field} ne $::FORM{'dontchange'}) {
678
            DoComma();
679
            $::query .= "$field = " . SqlQuote(trim($::FORM{$field}));
terry%netscape.com's avatar
terry%netscape.com committed
680 681 682 683
        }
    }
}

684
my $prod_id; # Remember, can't use this for mass changes
685
if ($::FORM{'product'} ne $::FORM{'dontchange'}) {
686
    $prod_id = get_product_id($::FORM{'product'});
687
    $prod_id ||
688
      ThrowUserError("invalid_product_name", {product => $::FORM{'product'}});
689
      
690 691 692 693 694 695 696 697 698 699
    DoComma();
    $::query .= "product_id = $prod_id";
} else {
    SendSQL("SELECT DISTINCT product_id FROM bugs WHERE bug_id IN (" .
            join(',', @idlist) . ") LIMIT 2");
    $prod_id = FetchOneColumn();
    $prod_id = undef if (FetchOneColumn());
}

my $comp_id; # Remember, can't use this for mass changes
700
if ($::FORM{'component'} ne $::FORM{'dontchange'}) {
701
    if (!defined $prod_id) {
702
        ThrowUserError("no_component_change_for_multiple_products");
703 704 705
    }
    $comp_id = get_component_id($prod_id,
                                $::FORM{'component'});
706 707 708 709
    $comp_id || ThrowCodeError("invalid_component", 
                               {component => $::FORM{'component'},
                                product => $::FORM{'product'}});
    
710 711 712 713
    DoComma();
    $::query .= "component_id = $comp_id";
}

714 715 716 717 718 719 720 721 722 723 724 725 726
# If this installation uses bug aliases, and the user is changing the alias,
# add this change to the query.
if (Param("usebugaliases") && defined($::FORM{'alias'})) {
    my $alias = trim($::FORM{'alias'});
    
    # 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) {
        # Validate the alias if the user entered one.
        if ($alias ne "") {
            # Make sure the alias isn't too long.
            if (length($alias) > 20) {
727
                ThrowUserError("alias_too_long");
728 729 730 731
            }

            # Make sure the alias is unique.
            my $escaped_alias = SqlQuote($alias);
732
            my $vars = { alias => $alias };
733
            
734 735 736
            SendSQL("SELECT bug_id FROM bugs WHERE alias = $escaped_alias " . 
                    "AND bug_id != $idlist[0]");
            my $id = FetchOneColumn();
737
            
738
            if ($id) {
739
                $vars->{'bug_link'} = GetBugLink($id, "Bug $id");
740
                ThrowUserError("alias_in_use", $vars);
741 742 743 744
            }

            # Make sure the alias isn't just a number.
            if ($alias =~ /^\d+$/) {
745
                ThrowUserError("alias_is_numeric", $vars);
746 747 748 749
            }

            # Make sure the alias has no commas or spaces.
            if ($alias =~ /[, ]/) {
750
                ThrowUserError("alias_has_comma_or_space", $vars);
751 752 753 754 755 756 757 758 759
            }
        }
        
        # 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();
        $::query .= "alias = ";
760 761 762 763 764
        if ($alias eq "") {
            $::query .= "NULL";
        } else {
            $::query .= SqlQuote($alias);
        }
765 766
    }
}
767

768 769
if (defined $::FORM{'qa_contact'}) {
    my $name = trim($::FORM{'qa_contact'});
770
    if ($name ne $::FORM{'dontchange'}) {
771 772 773 774 775 776 777 778 779
        my $id = 0;
        if ($name ne "") {
            $id = DBNameToIdAndCheck($name);
        }
        DoComma();
        $::query .= "qa_contact = $id";
    }
}

780
# jeff.hedlund@matrixsi.com time tracking data processing:
781 782 783 784 785
if (UserInGroup(Param('timetrackinggroup'))) {
    foreach my $field ("estimated_time", "remaining_time") {
        if (defined $::FORM{$field}) {
            my $er_time = trim($::FORM{$field});
            if ($er_time ne $::FORM{'dontchange'}) {
786 787
                DoComma();
                $::query .= "$field = " . SqlQuote($er_time);
788 789 790 791
            }
        }
    }
}
792

793 794
# 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
795
# allowed the user to set whether or not the reporter
796 797
# and cc list can see the bug even if they are not members of all groups 
# to which the bug is restricted.
798
if ( $::FORM{'id'} ) {
799 800 801
    SendSQL("SELECT group_id FROM bug_group_map WHERE bug_id = $::FORM{'id'}");
    my ($havegroup) = FetchSQLData();
    if ( $havegroup ) {
802 803 804 805 806 807 808 809 810 811
        DoComma();
        $::FORM{'reporter_accessible'} = $::FORM{'reporter_accessible'} ? '1' : '0';
        $::query .= "reporter_accessible = $::FORM{'reporter_accessible'}";

        DoComma();
        $::FORM{'cclist_accessible'} = $::FORM{'cclist_accessible'} ? '1' : '0';
        $::query .= "cclist_accessible = $::FORM{'cclist_accessible'}";
    }
}

812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
if ($::FORM{'id'} && 
    (Param("insidergroup") && UserInGroup(Param("insidergroup")))) {
    detaint_natural($::FORM{'id'});
    foreach my $field (keys %::FORM) {
        if ($field =~ /when-([0-9]+)/) {
            my $sequence = $1;
            my $private = $::FORM{"isprivate-$sequence"} ? 1 : 0 ;
            if ($private != $::FORM{"oisprivate-$sequence"}) {
                detaint_natural($::FORM{"$field"});
                SendSQL("UPDATE longdescs SET isprivate = $private 
                    WHERE bug_id = $::FORM{'id'} AND bug_when = " . $::FORM{"$field"});
            }
        }

    }
}
828

829
my $duplicate = 0;
830

831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848
# 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);
if (defined $::FORM{newcc} || defined $::FORM{removecc} || defined $::FORM{masscc}) {
    # 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) = "";
    if (defined $::FORM{masscc}) {
        if ($::FORM{ccaction} eq 'add') {
            $cc_add = $::FORM{masscc};
        } elsif ($::FORM{ccaction} eq 'remove') {
            $cc_remove = $::FORM{masscc};
        }
    } else {
        $cc_add = $::FORM{newcc};
        # We came from bug_form which uses a select box to determine what cc's
        # need to be removed...
849
        if (defined $::FORM{removecc} && $::FORM{cc}) {
850 851 852 853 854
            $cc_remove = join (",", @{$::MFORM{cc}});
        }
    }

    if ($cc_add) {
855 856
        $cc_add =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_add) ) {
857 858 859 860 861
            my $pid = DBNameToIdAndCheck($person);
            $cc_add{$pid} = $person;
        }
    }
    if ($cc_remove) {
862 863
        $cc_remove =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_remove) ) {
864 865 866 867 868 869 870
            my $pid = DBNameToIdAndCheck($person);
            $cc_remove{$pid} = $person;
        }
    }
}


871
CheckFormFieldDefined(\%::FORM, 'knob');
872 873 874 875
SWITCH: for ($::FORM{'knob'}) {
    /^none$/ && do {
        last SWITCH;
    };
876 877 878 879 880
    /^confirm$/ && CheckonComment( "confirm" ) && do {
        DoConfirm();
        ChangeStatus('NEW');
        last SWITCH;
    };
881
    /^accept$/ && CheckonComment( "accept" ) && do {
882
        DoConfirm();
883
        ChangeStatus('ASSIGNED');
884 885
        if (Param("musthavemilestoneonaccept") &&
                scalar(@{$::target_milestone{$::FORM{'product'}}}) > 1) {
886 887 888 889
            if (Param("usetargetmilestone")) {
                $requiremilestone = 1;
            }
        }
890 891
        last SWITCH;
    };
892
    /^clearresolution$/ && CheckonComment( "clearresolution" ) && do {
893 894 895
        ChangeResolution('');
        last SWITCH;
    };
896
    /^resolve$/ && CheckonComment( "resolve" ) && do {
897 898 899 900 901 902
        if (UserInGroup(Param('timetrackinggroup'))) {
            if (defined $::FORM{'remaining_time'} &&
                $::FORM{'remaining_time'} > 0) {
                ThrowUserError("resolving_remaining_time");
            }
        }
903 904 905 906 907 908 909 910 911 912 913
        
        # don't resolve as fixed while still unresolved blocking bugs
        if (Param("noresolveonopenblockers") && ($::FORM{'resolution'} eq 'FIXED')) {
           my @dependencies = CountOpenDependencies(@idlist);
           if (scalar @dependencies > 0) {
               ThrowUserError("still_unresolved_bugs", 
                               { dependencies     => \@dependencies,
                                 dependency_count => scalar @dependencies });
            }          
        }

914 915
        # Check here, because its the only place we require the resolution
        CheckFormField(\%::FORM, 'resolution', \@::settable_resolution);
916 917 918 919
        ChangeStatus('RESOLVED');
        ChangeResolution($::FORM{'resolution'});
        last SWITCH;
    };
920
    /^reassign$/ && CheckonComment( "reassign" ) && do {
921 922 923
        if ($::FORM{'andconfirm'}) {
            DoConfirm();
        }
924 925
        ChangeStatus('NEW');
        DoComma();
926 927 928
        if (!defined$::FORM{'assigned_to'} ||
            trim($::FORM{'assigned_to'}) eq "") {
            ThrowUserError("reassign_to_empty");
929
        }
930
        my $newid = DBNameToIdAndCheck(trim($::FORM{'assigned_to'}));
931 932 933
        $::query .= "assigned_to = $newid";
        last SWITCH;
    };
934
    /^reassignbycomponent$/  && CheckonComment( "reassignbycomponent" ) && do {
935
        if ($::FORM{'product'} eq $::FORM{'dontchange'}) {
936
            ThrowUserError("need_product");
937
        }
938
        if ($::FORM{'component'} eq $::FORM{'dontchange'}) {
939
            ThrowUserError("need_component");
terry%netscape.com's avatar
terry%netscape.com committed
940
        }
941 942 943
        if ($::FORM{'compconfirm'}) {
            DoConfirm();
        }
944
        ChangeStatus('NEW');
945 946
        SendSQL("SELECT initialowner FROM components " .
                "WHERE components.id = $comp_id");
947 948
        my $newid = FetchOneColumn();
        my $newname = DBID_to_name($newid);
949 950
        DoComma();
        $::query .= "assigned_to = $newid";
951
        if (Param("useqacontact")) {
952 953
            SendSQL("SELECT initialqacontact FROM components " .
                    "WHERE components.id = $comp_id");
954
            my $qacontact = FetchOneColumn();
955 956 957
            $qacontact = 0 unless (defined $qacontact && $qacontact != 0);
            DoComma();
            $::query .= "qa_contact = $qacontact";
958
        }
959 960
        last SWITCH;
    };   
961
    /^reopen$/  && CheckonComment( "reopen" ) && do {
962
        ChangeStatus('REOPENED');
963
        ChangeResolution('');
964 965
        last SWITCH;
    };
966
    /^verify$/ && CheckonComment( "verify" ) && do {
967 968 969
        ChangeStatus('VERIFIED');
        last SWITCH;
    };
970
    /^close$/ && CheckonComment( "close" ) && do {
971 972 973
        ChangeStatus('CLOSED');
        last SWITCH;
    };
974
    /^duplicate$/ && CheckonComment( "duplicate" ) && do {
975 976
        ChangeStatus('RESOLVED');
        ChangeResolution('DUPLICATE');
977
        CheckFormFieldDefined(\%::FORM,'dup_id');
978
        my $num = trim($::FORM{'dup_id'});
979 980 981
        SendSQL("SELECT bug_id FROM bugs WHERE bug_id = " . SqlQuote($num));
        $num = FetchOneColumn();
        if (!$num) {
982
            ThrowUserError("dupe_invalid_bug_id")
terry%netscape.com's avatar
terry%netscape.com committed
983
        }
984
        if (!defined($::FORM{'id'}) || $num == $::FORM{'id'}) {
985
            ThrowUserError("dupe_of_self_disallowed");
terry%netscape.com's avatar
terry%netscape.com committed
986
        }
987 988 989 990
        my $checkid = trim($::FORM{'id'});
        SendSQL("SELECT bug_id FROM bugs where bug_id = " .  SqlQuote($checkid));
        $checkid = FetchOneColumn();
        if (!$checkid) {
991 992
            ThrowUserError("invalid_bug_id",
                           { bug_id => $checkid });
993
        }
994
        $::FORM{'comment'} .= "\n\n*** This bug has been marked as a duplicate of $num ***";
995
        $duplicate = $num;
996

997 998
        last SWITCH;
    };
999 1000

    ThrowCodeError("unknown_action", { action => $::FORM{'knob'} });
terry%netscape.com's avatar
terry%netscape.com committed
1001 1002 1003
}


1004
if ($#idlist < 0) {
1005
    ThrowUserError("no_bugs_chosen");
terry%netscape.com's avatar
terry%netscape.com committed
1006 1007
}

1008 1009 1010 1011 1012

my @keywordlist;
my %keywordseen;

if ($::FORM{'keywords'}) {
1013 1014 1015 1016
    foreach my $keyword (split(/[\s,]+/, $::FORM{'keywords'})) {
        if ($keyword eq '') {
            next;
        }
1017
        my $i = GetKeywordIdFromName($keyword);
1018
        if (!$i) {
1019 1020
            ThrowUserError("unknown_keyword",
                           { keyword => $keyword });
1021 1022 1023 1024 1025 1026 1027 1028
        }
        if (!$keywordseen{$i}) {
            push(@keywordlist, $i);
            $keywordseen{$i} = 1;
        }
    }
}

1029 1030
my $keywordaction = $::FORM{'keywordaction'} || "makeexact";

1031
if ($::comma eq ""
1032
    && (! @groupAdd) && (! @groupDel)
1033
    && (! @::legal_keywords || (0 == @keywordlist && $keywordaction ne "makeexact"))
1034 1035
    && defined $::FORM{'masscc'} && ! $::FORM{'masscc'}
    ) {
1036
    if (!defined $::FORM{'comment'} || $::FORM{'comment'} =~ /^\s*$/) {
1037
        ThrowUserError("bugs_not_changed");
terry%netscape.com's avatar
terry%netscape.com committed
1038 1039 1040
    }
}

1041
my $basequery = $::query;
1042
my $delta_ts;
terry%netscape.com's avatar
terry%netscape.com committed
1043

1044

1045 1046
sub SnapShotBug {
    my ($id) = (@_);
1047
    SendSQL("select delta_ts, " . join(',', @::log_columns) .
1048
            " from bugs where bug_id = $id");
1049 1050
    my @row = FetchSQLData();
    $delta_ts = shift @row;
1051

1052
    return @row;
terry%netscape.com's avatar
terry%netscape.com committed
1053 1054 1055
}


1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
sub SnapShotDeps {
    my ($i, $target, $me) = (@_);
    SendSQL("select $target from dependencies where $me = $i order by $target");
    my @list;
    while (MoreSQLData()) {
        push(@list, FetchOneColumn());
    }
    return join(',', @list);
}


my $timestamp;
1068
my $bug_changed;
1069

1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
sub FindWrapPoint {
    my ($string, $startpos) = @_;
    if (!$string) { return 0 }
    if (length($string) < $startpos) { return length($string) }
    my $wrappoint = rindex($string, ",", $startpos); # look for comma
    if ($wrappoint < 0) {  # can't find comma
        $wrappoint = rindex($string, " ", $startpos); # look for space
        if ($wrappoint < 0) {  # can't find space
            $wrappoint = rindex($string, "-", $startpos); # look for hyphen
            if ($wrappoint < 0) {  # can't find hyphen
                $wrappoint = $startpos;  # just truncate it
            } else {
                $wrappoint++; # leave hyphen on the left side
            }
        }
    }
    return $wrappoint;
}

1089 1090 1091 1092
sub LogDependencyActivity {
    my ($i, $oldstr, $target, $me) = (@_);
    my $newstr = SnapShotDeps($i, $target, $me);
    if ($oldstr ne $newstr) {
1093 1094
        # Figure out what's really different...
        my ($removed, $added) = DiffStrings($oldstr, $newstr);
1095
        LogActivityEntry($i,$target,$removed,$added,$whoid,$timestamp);
1096 1097
        # update timestamp on target bug so midairs will be triggered
        SendSQL("UPDATE bugs SET delta_ts=NOW() WHERE bug_id=$i");
1098
        $bug_changed = 1;
1099 1100 1101 1102 1103
        return 1;
    }
    return 0;
}

1104
# this loop iterates once for each bug to be processed (eg when this script
1105
# is called with multiple bugs selected from buglist.cgi instead of
1106 1107
# show_bug.cgi).
#
1108
foreach my $id (@idlist) {
1109
    my %dependencychanged;
1110
    $bug_changed = 0;
1111 1112
    my $write = "WRITE";        # Might want to make a param to control
                                # whether we do LOW_PRIORITY ...
1113
    SendSQL("LOCK TABLES bugs $write, bugs_activity $write, cc $write, " .
1114
            "cc AS selectVisible_cc $write, " .
1115
            "profiles $write, dependencies $write, votes $write, " .
1116
            "products READ, components READ, " .
1117
            "keywords $write, longdescs $write, fielddefs $write, " .
1118
            "bug_group_map $write, flags $write, duplicates $write," .
1119
            # user_group_map would be a READ lock except that Flag::process
1120 1121 1122 1123
            # may call Flag::notify, which creates a new user object,
            # which might call derive_groups, which wants a WRITE lock on that
            # table. group_group_map is in here at all because derive_groups
            # needs it.
1124
            "user_group_map $write, group_group_map READ, flagtypes READ, " . 
1125
            "flaginclusions AS i READ, flagexclusions AS e READ, " .
1126 1127 1128 1129
            "keyworddefs READ, groups READ, attachments READ, " .
            "group_control_map AS oldcontrolmap READ, " .
            "group_control_map AS newcontrolmap READ, " .
            "group_control_map READ");
1130
    my @oldvalues = SnapShotBug($id);
1131
    my %oldhash;
1132 1133 1134 1135 1136 1137
    # Fun hack.  @::log_columns only contains the component_id,
    # not the name (since bug 43600 got fixed).  So, we need to have
    # this id ready for the loop below, otherwise anybody can
    # change the component of a bug (we checked product above).
    # http://bugzilla.mozilla.org/show_bug.cgi?id=180545
    my $product_id = get_product_id($::FORM{'product'});
1138 1139 1140 1141 1142 1143
    
    if ($::FORM{'component'} ne $::FORM{'dontchange'}) {
        $::FORM{'component_id'} = 
                            get_component_id($product_id, $::FORM{'component'});
    }
    
1144 1145
    my $i = 0;
    foreach my $col (@::log_columns) {
1146
        # Consider NULL db entries to be equivalent to the empty string
1147
        $oldvalues[$i] = '' unless defined($oldvalues[$i]);
1148
        $oldhash{$col} = $oldvalues[$i];
1149
        if (exists $::FORM{$col}) {
1150
            if (!CheckCanChangeField($col, $id, $oldvalues[$i], $::FORM{$col})) {
1151
                # More fun hacking... don't display component_id
1152
                my $vars;
1153
                if ($col eq 'component_id') {
1154 1155
                    $vars->{'oldvalue'} = 
                                   get_component_name($oldhash{'component_id'});
1156 1157 1158 1159 1160 1161 1162 1163
                    $vars->{'newvalue'} = $::FORM{'component'};
                    $vars->{'field'} = 'component';
                }
                else {
                    $vars->{'oldvalue'} = $oldvalues[$i];
                    $vars->{'newvalue'} = $::FORM{$col};
                    $vars->{'field'} = $col;
                }
1164
                ThrowUserError("illegal_change", $vars, "abort");
1165
            }
1166 1167 1168
        }
        $i++;
    }
1169

1170
    $oldhash{'product'} = get_product_name($oldhash{'product_id'});
1171
    if (!CanEditProductId($oldhash{'product_id'})) {
1172
        ThrowUserError("product_edit_denied",
1173
                      { product => $oldhash{'product'} }, "abort");
1174 1175 1176 1177 1178 1179
    }

    if (defined $::FORM{'product'} 
        && $::FORM{'product'} ne $::FORM{'dontchange'} 
        && $::FORM{'product'} ne $oldhash{'product'}
        && !CanEnterProduct($::FORM{'product'})) {
1180
        ThrowUserError("entry_access_denied",
1181
                       { product => $::FORM{'product'} }, "abort");
1182
    }
1183 1184
    if ($requiremilestone) {
        my $value = $::FORM{'target_milestone'};
1185
        if (!defined $value || $value eq $::FORM{'dontchange'}) {
1186 1187
            $value = $oldhash{'target_milestone'};
        }
1188
        SendSQL("SELECT defaultmilestone FROM products WHERE name = " .
1189 1190
                SqlQuote($oldhash{'product'}));
        if ($value eq FetchOneColumn()) {
1191 1192 1193
            ThrowUserError("milestone_required",
                           { bug_id => $id },
                           "abort");
1194 1195
        }
    }   
1196
    if (defined $::FORM{'delta_ts'} && $::FORM{'delta_ts'} ne $delta_ts) {
1197 1198 1199 1200
        ($vars->{'operations'}) = GetBugActivity($::FORM{'id'}, $::FORM{'delta_ts'});

        $vars->{'start_at'} = $::FORM{'longdesclength'};
        $vars->{'comments'} = GetComments($id);
1201

1202
        $::FORM{'delta_ts'} = $delta_ts;
1203
        $vars->{'form'} = \%::FORM;
1204
        $vars->{'mform'} = \%::MFORM;
1205 1206 1207 1208 1209 1210
        
        $vars->{'bug_id'} = $id;
        
        SendSQL("UNLOCK TABLES");
        
        # Warn the user about the mid-air collision and ask them what to do.
1211
        $template->process("bug/process/midair.html.tmpl", $vars)
1212
          || ThrowTemplateError($template->error());
1213 1214 1215
        exit;
    }
        
1216 1217 1218 1219
    my %deps;
    if (defined $::FORM{'dependson'}) {
        my $me = "blocked";
        my $target = "dependson";
1220
        my %deptree;
1221
        for (1..2) {
1222
            $deptree{$target} = [];
1223 1224
            my %seen;
            foreach my $i (split('[\s,]+', $::FORM{$target})) {
1225 1226
                next if $i eq "";
                
1227
                if ($id eq $i) {
1228
                    ThrowUserError("dependency_loop_single", undef, "abort");
1229
                }
1230
                if (!exists $seen{$i}) {
1231
                    push(@{$deptree{$target}}, $i);
1232 1233 1234
                    $seen{$i} = 1;
                }
            }
1235 1236 1237
            # populate $deps{$target} as first-level deps only.
            # and find remainder of dependency tree in $deptree{$target}
            @{$deps{$target}} = @{$deptree{$target}};
1238 1239 1240
            my @stack = @{$deps{$target}};
            while (@stack) {
                my $i = shift @stack;
1241 1242
                SendSQL("select $target from dependencies where $me = " .
                        SqlQuote($i));
1243 1244
                while (MoreSQLData()) {
                    my $t = FetchOneColumn();
1245 1246 1247 1248
                    # ignore any _current_ dependencies involving this bug,
                    # as they will be overwritten with data from the form.
                    if ($t != $id && !exists $seen{$t}) {
                        push(@{$deptree{$target}}, $t);
1249 1250 1251 1252 1253
                        push @stack, $t;
                        $seen{$t} = 1;
                    }
                }
            }
1254

1255
            if ($me eq 'dependson') {
1256 1257
                my @deps   =  @{$deptree{'dependson'}};
                my @blocks =  @{$deptree{'blocked'}};
1258 1259 1260 1261 1262 1263 1264
                my @union = ();
                my @isect = ();
                my %union = ();
                my %isect = ();
                foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ }
                @union = keys %union;
                @isect = keys %isect;
1265
                if (@isect > 0) {
1266 1267
                    my $both;
                    foreach my $i (@isect) {
1268
                       $both = $both . GetBugLink($i, "#" . $i) . " ";
1269
                    }
1270 1271 1272 1273

                    ThrowUserError("dependency_loop_multi",
                                   { both => $both },
                                   "abort");
1274 1275
                }
            }
1276 1277 1278 1279 1280
            my $tmp = $me;
            $me = $target;
            $target = $tmp;
        }
    }
1281

1282 1283 1284 1285
    #
    # Start updating the relevant database entries
    #

1286 1287 1288
    SendSQL("select now()");
    $timestamp = FetchOneColumn();

1289 1290 1291 1292 1293 1294
    my $work_time;
    if (UserInGroup(Param('timetrackinggroup'))) {
        $work_time = $::FORM{'work_time'};
        if ($work_time) {
            if (!defined $::FORM{'comment'} || $::FORM{'comment'} =~ /^\s*$/) {
                ThrowUserError('comment_required', undef, "abort");
1295
            }
1296 1297 1298 1299 1300
            # 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.
            LogActivityEntry($id, "work_time", "", $::FORM{'work_time'},
                             $whoid, $timestamp);
1301 1302 1303
        }
    }

1304 1305 1306 1307 1308 1309
    if ($::FORM{'comment'} || $work_time) {
        AppendComment($id, Bugzilla->user->login, $::FORM{'comment'},
                      $::FORM{'commentprivacy'}, $timestamp, $work_time);
        $bug_changed = 1;
    }

1310 1311 1312 1313 1314 1315
    if (@::legal_keywords) {
        # 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.
1316
        my $changed = 0;
1317 1318
        if ($keywordaction eq "makeexact") {
            SendSQL("DELETE FROM keywords WHERE bug_id = $id");
1319
            $changed = 1;
1320 1321 1322 1323 1324
        }
        foreach my $keyword (@keywordlist) {
            if ($keywordaction ne "makeexact") {
                SendSQL("DELETE FROM keywords
                         WHERE bug_id = $id AND keywordid = $keyword");
1325
                $changed = 1;
1326 1327 1328 1329
            }
            if ($keywordaction ne "delete") {
                SendSQL("INSERT INTO keywords 
                         (bug_id, keywordid) VALUES ($id, $keyword)");
1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341
                $changed = 1;
            }
        }
        if ($changed) {
            SendSQL("SELECT keyworddefs.name 
                     FROM keyworddefs, keywords
                     WHERE keywords.bug_id = $id
                         AND keyworddefs.id = keywords.keywordid
                     ORDER BY keyworddefs.name");
            my @list;
            while (MoreSQLData()) {
                push(@list, FetchOneColumn());
1342
            }
1343 1344 1345
            SendSQL("UPDATE bugs SET keywords = " .
                    SqlQuote(join(', ', @list)) .
                    " WHERE bug_id = $id");
1346 1347
        }
    }
1348
    my $query = "$basequery\nwhere bug_id = $id";
terry%netscape.com's avatar
terry%netscape.com committed
1349
    
1350 1351
    if ($::comma ne "") {
        SendSQL($query);
terry%netscape.com's avatar
terry%netscape.com committed
1352
    }
1353

1354 1355 1356 1357 1358 1359 1360
    # Check for duplicates if the bug is [re]open
    SendSQL("SELECT resolution FROM bugs WHERE bug_id = $id");
    my $resolution = FetchOneColumn();
    if ($resolution eq '') {
        SendSQL("DELETE FROM duplicates WHERE dupe = $id");
    }
    
1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383
    my $newproduct_id = $oldhash{'product_id'};
    if ((defined $::FORM{'product'})
        && ($::FORM{'product'} ne $::FORM{'dontchange'})) {
        my $newproduct_id = get_product_id($::FORM{'product'});
    }

    my %groupsrequired = ();
    my %groupsforbidden = ();
    SendSQL("SELECT id, membercontrol 
             FROM groups LEFT JOIN group_control_map
             ON id = group_id
             AND product_id = $newproduct_id WHERE isactive != 0");
    while (MoreSQLData()) {
        my ($group, $control) = FetchSQLData();
        $control ||= 0;
        unless ($control > &::CONTROLMAPNA)  {
            $groupsforbidden{$group} = 1;
        }
        if ($control == &::CONTROLMAPMANDATORY) {
            $groupsrequired{$group} = 1;
        }
    }

1384
    my @groupAddNames = ();
1385 1386 1387 1388
    my @groupAddNamesAll = ();
    foreach my $grouptoadd (@groupAdd, keys %groupsrequired) {
        next if $groupsforbidden{$grouptoadd};
        push(@groupAddNamesAll, GroupIdToName($grouptoadd));
1389 1390 1391 1392 1393 1394 1395
        if (!BugInGroupId($id, $grouptoadd)) {
            push(@groupAddNames, GroupIdToName($grouptoadd));
            SendSQL("INSERT INTO bug_group_map (bug_id, group_id) 
                     VALUES ($id, $grouptoadd)");
        }
    }
    my @groupDelNames = ();
1396 1397 1398 1399
    my @groupDelNamesAll = ();
    foreach my $grouptodel (@groupDel, keys %groupsforbidden) {
        push(@groupDelNamesAll, GroupIdToName($grouptodel));
        next if $groupsrequired{$grouptodel};
1400 1401 1402 1403 1404 1405 1406 1407 1408 1409
        if (BugInGroupId($id, $grouptodel)) {
            push(@groupDelNames, GroupIdToName($grouptodel));
        }
        SendSQL("DELETE FROM bug_group_map 
                 WHERE bug_id = $id AND group_id = $grouptodel");
    }

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

1410 1411 1412 1413 1414
    if ($groupDelNames ne $groupAddNames) {
        LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames,
                         $whoid, $timestamp); 
        $bug_changed = 1;
    }
1415 1416

    my @ccRemoved = (); 
1417 1418 1419 1420 1421 1422 1423 1424 1425
    if (defined $::FORM{newcc} || defined $::FORM{removecc} || defined $::FORM{masscc}) {
        # Get the current CC list for this bug
        my %oncc;
        SendSQL("SELECT who FROM cc WHERE bug_id = $id");
        while (MoreSQLData()) {
            $oncc{FetchOneColumn()} = 1;
        }

        my (@added, @removed) = ();
1426 1427 1428 1429 1430 1431
        foreach my $pid (keys %cc_add) {
            # If this person isn't already on the cc list, add them
            if (! $oncc{$pid}) {
                SendSQL("INSERT INTO cc (bug_id, who) VALUES ($id, $pid)");
                push (@added, $cc_add{$pid});
                $oncc{$pid} = 1;
1432 1433
            }
        }
1434 1435 1436 1437 1438 1439
        foreach my $pid (keys %cc_remove) {
            # If the person is on the cc list, remove them
            if ($oncc{$pid}) {
                SendSQL("DELETE FROM cc WHERE bug_id = $id AND who = $pid");
                push (@removed, $cc_remove{$pid});
                $oncc{$pid} = 0;
1440 1441
            }
        }
1442

1443 1444
        # If any changes were found, record it in the activity log
        if (scalar(@removed) || scalar(@added)) {
1445 1446
            my $removed = join(", ", @removed);
            my $added = join(", ", @added);
1447 1448
            LogActivityEntry($id,"cc",$removed,$added,$whoid,$timestamp);
            $bug_changed = 1;
1449
        }
1450
        @ccRemoved = @removed;
1451
    }
1452

1453
    # We need to send mail for dependson/blocked bugs if the dependencies
1454 1455 1456
    # change or the status or resolution change. This var keeps track of that.
    my $check_dep_bugs = 0;

1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467
    if (defined $::FORM{'dependson'}) {
        my $me = "blocked";
        my $target = "dependson";
        for (1..2) {
            SendSQL("select $target from dependencies where $me = $id order by $target");
            my %snapshot;
            my @oldlist;
            while (MoreSQLData()) {
                push(@oldlist, FetchOneColumn());
            }
            my @newlist = sort {$a <=> $b} @{$deps{$target}};
1468 1469
            @dependencychanged{@oldlist} = 1;
            @dependencychanged{@newlist} = 1;
1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497

            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]) {
                        die "Error in list comparing code";
                    }
                    shift @oldlist;
                    shift @newlist;
                }
            }
            my @keys = keys(%snapshot);
            if (@keys) {
                my $oldsnap = SnapShotDeps($id, $target, $me);
                SendSQL("delete from dependencies where $me = $id");
                foreach my $i (@{$deps{$target}}) {
                    SendSQL("insert into dependencies ($me, $target) values ($id, $i)");
                }
                foreach my $k (@keys) {
1498
                    LogDependencyActivity($k, $snapshot{$k}, $me, $target);
1499 1500
                }
                LogDependencyActivity($id, $oldsnap, $target, $me);
1501
                $check_dep_bugs = 1;
1502 1503 1504 1505 1506 1507 1508 1509
            }

            my $tmp = $me;
            $me = $target;
            $target = $tmp;
        }
    }

1510 1511 1512 1513 1514
    # 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.
1515
    # Check if the user has changed the product to which the bug belongs;
1516
    if ( 
1517
      defined $::FORM{'product'} 
1518
        && $::FORM{'product'} ne $::FORM{'dontchange'} 
1519 1520
          && $::FORM{'product'} ne $oldhash{'product'} 
    ) {
1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567
        my $newproduct_id = get_product_id($::FORM{'product'});
        # 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?
        SendSQL("SELECT DISTINCT groups.id, isactive, " .
                "oldcontrolmap.membercontrol, newcontrolmap.membercontrol, " .
                "user_group_map.user_id IS NOT NULL, " .
                "bug_group_map.group_id IS NOT NULL " .
                "FROM groups " .
                "LEFT JOIN group_control_map AS oldcontrolmap " .
                "ON oldcontrolmap.group_id = groups.id " .
                "AND oldcontrolmap.product_id = " . $oldhash{'product_id'} .
                " LEFT JOIN group_control_map AS newcontrolmap " .
                "ON newcontrolmap.group_id = groups.id " .
                "AND newcontrolmap.product_id = $newproduct_id " .
                "LEFT JOIN user_group_map " .
                "ON user_group_map.group_id = groups.id " .
                "AND user_group_map.user_id = $::userid " .
                "AND user_group_map.isbless = 0 " .
                "LEFT JOIN bug_group_map " .
                "ON bug_group_map.group_id = groups.id " .
                "AND bug_group_map.bug_id = $id "
            );
        my @groupstoremove = ();
        my @groupstoadd = ();
        my @defaultstoremove = ();
        my @defaultstoadd = ();
        my @allgroups = ();
        my $buginanydefault = 0;
        my $buginanychangingdefault = 0;
        while (MoreSQLData()) {
            my ($groupid, $isactive, $oldcontrol, $newcontrol, 
            $useringroup, $bugingroup) = FetchSQLData();
            # 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;
1568 1569
                if (($newcontrol != CONTROLMAPDEFAULT)
                    && ($newcontrol != CONTROLMAPMANDATORY)) {
1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587
                    # 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);
1588
            }
1589
        }
1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602
        # 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()
            && (($::FORM{'addtonewgroup'} eq 'yes')
            || (($::FORM{'addtonewgroup'} eq 'yesifinold') 
            && ($buginanydefault)))) {
            push(@groupstoadd, @defaultstoadd);
1603 1604
        }

1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626
        # Now actually update the bug_group_map.
        my @DefGroupsAdded = ();
        my @DefGroupsRemoved = ();
        foreach my $groupid (@allgroups) {
            my $thisadd = grep( ($_ == $groupid), @groupstoadd);
            my $thisdel = grep( ($_ == $groupid), @groupstoremove);
            if ($thisadd) {
                push(@DefGroupsAdded, GroupIdToName($groupid));
                SendSQL("INSERT INTO bug_group_map (bug_id, group_id) VALUES " .
                        "($id, $groupid)");
            } elsif ($thisdel) {
                push(@DefGroupsRemoved, GroupIdToName($groupid));
                SendSQL("DELETE FROM bug_group_map WHERE bug_id = $id " .
                        "AND group_id = $groupid");
            }
        }
        if ((@DefGroupsAdded) || (@DefGroupsRemoved)) {
            LogActivityEntry($id, "bug_group",
                join(', ', @DefGroupsRemoved),
                join(', ', @DefGroupsAdded),
                     $whoid, $timestamp); 
        }
1627 1628
    }
  
1629 1630 1631 1632
    # 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.
    #
1633
    my @newvalues = SnapShotBug($id);
1634 1635 1636 1637 1638 1639 1640 1641
    my %newhash;
    $i = 0;
    foreach my $col (@::log_columns) {
        # Consider NULL db entries to be equivalent to the empty string
        $newvalues[$i] ||= '';
        $newhash{$col} = $newvalues[$i];
        $i++;
    }
1642
    # for passing to Bugzilla::BugMail to ensure that when someone is removed
1643 1644 1645 1646
    # from one of these fields, they get notified of that fact (if desired)
    #
    my $origOwner = "";
    my $origQaContact = "";
1647
    
1648
    foreach my $c (@::log_columns) {
1649 1650
        my $col = $c;           # We modify it, don't want to modify array
                                # values in place.
1651 1652 1653
        my $old = shift @oldvalues;
        my $new = shift @newvalues;
        if ($old ne $new) {
1654

1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667
            # 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') {
                $old = get_product_name($old);
                $new = get_product_name($new);
                $col = 'product';
            }
            if ($col eq 'component_id') {
                $old = get_component_name($old);
                $new = get_component_name($new);
                $col = 'component';
            }

1668 1669
            # save off the old value for passing to Bugzilla::BugMail so
            # the old owner can be notified
1670 1671 1672 1673 1674 1675 1676 1677 1678 1679
            #
            if ($col eq 'assigned_to') {
                $old = ($old) ? DBID_to_name($old) : "";
                $new = ($new) ? DBID_to_name($new) : "";
                $origOwner = $old;
            }

            # ditto for the old qa contact
            #
            if ($col eq 'qa_contact') {
1680 1681
                $old = ($old) ? DBID_to_name($old) : "";
                $new = ($new) ? DBID_to_name($new) : "";
1682
                $origQaContact = $old;
terry%netscape.com's avatar
terry%netscape.com committed
1683
            }
1684

1685 1686 1687 1688 1689
            # If this is the keyword field, only record the changes, not everything.
            if ($col eq 'keywords') {
                ($old, $new) = DiffStrings($old, $new);
            }

1690
            if ($col eq 'product') {
1691
                RemoveVotes($id, 0,
1692 1693
                            "This bug has been moved to a different product");
            }
1694 1695 1696 1697 1698 1699 1700
            
            if ($col eq 'bug_status' 
                && IsOpenedState($old) ne IsOpenedState($new))
            {
                $check_dep_bugs = 1;
            }
            
1701 1702
            LogActivityEntry($id,$col,$old,$new,$whoid,$timestamp);
            $bug_changed = 1;
terry%netscape.com's avatar
terry%netscape.com committed
1703 1704
        }
    }
1705 1706 1707 1708 1709
    # Set and update flags.
    if ($UserInEditGroupSet) {
        my $target = Bugzilla::Flag::GetTarget($id);
        Bugzilla::Flag::process($target, $timestamp, \%::FORM);
    }
1710 1711 1712
    if ($bug_changed) {
        SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) . " WHERE bug_id = $id");
    }
1713
    SendSQL("UNLOCK TABLES");
1714

1715 1716 1717
    $vars->{'mailrecipients'} = { 'cc' => \@ccRemoved,
                                  'owner' => $origOwner,
                                  'qa' => $origQaContact,
1718
                                  'changer' => Bugzilla->user->login };
1719 1720 1721 1722 1723

    $vars->{'id'} = $id;
    
    # Let the user know the bug was changed and who did and didn't
    # receive email about the change.
1724
    $template->process("bug/process/results.html.tmpl", $vars)
1725
      || ThrowTemplateError($template->error());
1726
    $vars->{'header_done'} = 1;
1727
    
1728 1729 1730 1731 1732 1733 1734 1735
    if ($duplicate) {
        # Check to see if Reporter of this bug is reporter of Dupe 
        SendSQL("SELECT reporter FROM bugs WHERE bug_id = " . SqlQuote($::FORM{'id'}));
        my $reporter = FetchOneColumn();
        SendSQL("SELECT reporter FROM bugs WHERE bug_id = " . SqlQuote($duplicate) . " and reporter = $reporter");
        my $isreporter = FetchOneColumn();
        SendSQL("SELECT who FROM cc WHERE bug_id = " . SqlQuote($duplicate) . " and who = $reporter");
        my $isoncc = FetchOneColumn();
1736
        unless ($isreporter || $isoncc || ! $::FORM{'confirm_add_duplicate'}) {
matty%chariot.net.au's avatar
matty%chariot.net.au committed
1737
            # The reporter is oblivious to the existence of the new bug and is permitted access
1738
            # ... add 'em to the cc (and record activity)
1739 1740
            LogActivityEntry($duplicate,"cc","",DBID_to_name($reporter),
                             $whoid,$timestamp);
1741 1742
            SendSQL("INSERT INTO cc (who, bug_id) VALUES ($reporter, " . SqlQuote($duplicate) . ")");
        }
1743
        # Bug 171639 - Duplicate notifications do not need to be private. 
1744
        AppendComment($duplicate, Bugzilla->user->login, "*** Bug $::FORM{'id'} has been marked as a duplicate of this bug. ***", 0);
1745
        CheckFormFieldDefined(\%::FORM,'comment');
1746
        SendSQL("INSERT INTO duplicates VALUES ($duplicate, $::FORM{'id'})");
1747
        
1748
        $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; 
1749

1750 1751 1752 1753
        $vars->{'id'} = $duplicate;
        $vars->{'type'} = "dupe";
        
        # Let the user know a duplication notation was added to the original bug.
1754
        $template->process("bug/process/results.html.tmpl", $vars)
1755
          || ThrowTemplateError($template->error());
1756
        $vars->{'header_done'} = 1;
1757 1758
    }

1759 1760
    if ($check_dep_bugs) {
        foreach my $k (keys(%dependencychanged)) {
1761
            $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; 
1762 1763 1764 1765 1766 1767 1768 1769
            $vars->{'id'} = $k;
            $vars->{'type'} = "dep";

            # Let the user 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.
            $template->process("bug/process/results.html.tmpl", $vars)
              || ThrowTemplateError($template->error());
1770
            $vars->{'header_done'} = 1;
1771
        }
1772
    }
terry%netscape.com's avatar
terry%netscape.com committed
1773 1774
}

1775 1776
# now show the next bug
if ($next_bug) {
1777
    if (detaint_natural($next_bug) && Bugzilla->user->can_see_bug($next_bug)) {
1778
        my $bug = new Bugzilla::Bug($next_bug, $::userid);
1779
        ThrowCodeError("bug_error", { bug => $bug }) if $bug->error;
1780

1781 1782 1783
        # next.html.tmpl includes edit.html.tmpl, and therefore we
        # need $bug defined in $vars.
        $vars->{'bug'} = $bug;
1784 1785 1786 1787 1788 1789 1790 1791

        # And we need to determine if Patch Viewer is installed, for
        # Diff link (NB: Duplicate code with show_bug.cgi.)
        eval {
            require PatchReader;
            $vars->{'patchviewerinstalled'} = 1;
        };

1792 1793
        $template->process("bug/process/next.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
1794

1795
        exit;
1796
    }
terry%netscape.com's avatar
terry%netscape.com committed
1797
}
1798

1799
# End the response page.
1800
$template->process("bug/navigate.html.tmpl", $vars)
1801
  || ThrowTemplateError($template->error());
1802
$template->process("global/footer.html.tmpl", $vars)
1803
  || ThrowTemplateError($template->error());