process_bug.cgi 49.4 KB
Newer Older
1
#!/usr/bonsaitools/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 29
use diagnostics;
use strict;

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

33 34
use lib qw(.);

35
require "CGI.pl";
36 37
require "bug_form.pl";

38
use RelationSet;
39 40 41

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

42 43 44 45 46 47 48 49 50 51 52 53 54
use vars qw(%versions
          %components
          %COOKIE
          %MFORM
          %legal_keywords
          %legal_opsys
          %legal_platform
          %legal_priority
          %settable_resolution
          %target_milestone
          %legal_severity
          %superusergroupset
          $next_bug);
55

56
ConnectToDatabase();
57
my $whoid = confirm_login();
58

59 60
my $requiremilestone = 0;

61 62
use vars qw($template $vars);

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

87 88 89 90 91
# Make sure there are bugs to process.
scalar(@idlist)
  || DisplayError("You did not select any bugs to modify.")
  && exit;

92 93 94 95 96 97 98 99 100 101
# 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();
}

102 103
ValidateComment($::FORM{'comment'});

104 105 106 107
######################################################################
# End Data/Security Validation
######################################################################

108 109
print "Content-type: text/html\n\n";

110 111
# Start displaying the response page.
$vars->{'title'} = "Bug processed";
112
$template->process("global/header.html.tmpl", $vars)
113 114 115
  || ThrowTemplateError($template->error());

$vars->{'header_done'} = 1;
116

117 118
GetVersionTable();

119 120 121
CheckFormFieldDefined(\%::FORM, 'product');
CheckFormFieldDefined(\%::FORM, 'version');
CheckFormFieldDefined(\%::FORM, 'component');
122

123 124 125
# check if target milestone is defined - matthew@zeroknowledge.com
if ( Param("usetargetmilestone") ) {
  CheckFormFieldDefined(\%::FORM, 'target_milestone');
126 127
}

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
#
# This function checks if there is a comment required for a specific
# function and tests, if the comment was given.
# If comments are required for functions  is defined by params.
#
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 !
145 146
            ThrowUserError("You have to specify a <b>comment</b> on this change.  
                            Please give some words on the reason for your change.");
147 148 149 150 151 152 153
        } else {
            $ret = 0;
        }
    }
    return( ! $ret ); # Return val has to be inverted
}

154 155 156 157 158 159 160 161 162
# 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'} ) {
    SendSQL("SELECT product FROM bugs WHERE bug_id = $::FORM{'id'}");
    $::oldproduct = FetchSQLData();
}
163 164 165 166
if ((($::FORM{'id'} && $::FORM{'product'} ne $::oldproduct) 
     || (!$::FORM{'id'} && $::FORM{'product'} ne $::dontchange))
    && CheckonComment( "reassignbycomponent" ))
{
167
    CheckFormField(\%::FORM, 'product', \@::legal_product);
168
    my $prod = $::FORM{'product'};
169 170 171 172 173 174 175 176 177

    # 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.
    #
178 179
    my $vok = lsearch($::versions{$prod}, $::FORM{'version'}) >= 0;
    my $cok = lsearch($::components{$prod}, $::FORM{'component'}) >= 0;
180 181 182 183 184 185

    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;
    }

186 187 188
    # 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.
189
    if (!$vok || !$cok || !$mok || (Param('usebuggroups') && !defined($::FORM{'addtonewgroup'}))) {
190 191
        $vars->{'form'} = \%::FORM;
        
192
        if (!$vok || !$cok || !$mok) {
193 194 195 196 197 198 199
            $vars->{'verify_fields'} = 1;
            $vars->{'versions'} = $::versions{$prod};
            $vars->{'components'} = $::components{$prod};
        
            if (Param("usetargetmilestone")) {
                $vars->{'use_target_milestone'} = 1;
                $vars->{'milestones'} = $::target_milestone{$prod};
200
            }
201 202
            else {
                $vars->{'use_target_milestone'} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
203
            }
204
        }
205 206
        else {
            $vars->{"verify_fields"} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
207
        }
208 209 210 211
        
        $vars->{'verify_bug_group'} = (Param('usebuggroups') 
                                       && !defined($::FORM{'addtonewgroup'}));
        
212
        $template->process("bug/process/verify-new-product.html.tmpl", $vars)
213
          || ThrowTemplateError($template->error());
214
        exit;
terry%netscape.com's avatar
terry%netscape.com committed
215 216 217 218
    }
}


219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
# 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;

sub CheckCanChangeField {
    my ($f, $bugid, $oldvalue, $newvalue) = (@_);
    if ($f eq "assigned_to" || $f eq "reporter" || $f eq "qa_contact") {
        if ($oldvalue =~ /^\d+$/) {
            if ($oldvalue == 0) {
                $oldvalue = "";
            } else {
                $oldvalue = DBID_to_name($oldvalue);
            }
        }
    }
    if ($oldvalue eq $newvalue) {
        return 1;
    }
242 243 244
    if (trim($oldvalue) eq trim($newvalue)) {
        return 1;
    }
245 246 247
    if ($f =~ /^longdesc/) {
        return 1;
    }
248 249
    if ($f eq "resolution") { # always OK this.  if they really can't,
        return 1;             # it'll flag it when "status" is checked.
250
    }
251 252 253 254 255 256 257 258 259 260 261
    if ($UserInEditGroupSet < 0) {
        $UserInEditGroupSet = UserInGroup("editbugs");
    }
    if ($UserInEditGroupSet) {
        return 1;
    }
    if ($lastbugid != $bugid) {
        SendSQL("SELECT reporter, assigned_to, qa_contact FROM bugs " .
                "WHERE bug_id = $bugid");
        ($reporterid, $ownerid, $qacontactid) = (FetchSQLData());
    }
262 263 264 265 266 267
    # Let reporter change bug status, even if they can't edit bugs.
    # If reporter can't re-open their bug they will just file a duplicate.
    # While we're at it, let them close their own bugs as well.
    if ( ($f eq "bug_status") && ($whoid eq $reporterid) ) {
        return 1;
    }
268 269
    if ($f eq "bug_status" && $newvalue ne $::unconfirmedstate &&
        IsOpenedState($newvalue)) {
270 271 272 273 274 275 276 277 278 279 280 281

        # Hmm.  They are trying to set this bug to some opened state
        # that isn't the UNCONFIRMED state.  Are they in the right
        # group?  Or, has it ever been confirmed?  If not, then this
        # isn't legal.

        if ($UserInCanConfirmGroupSet < 0) {
            $UserInCanConfirmGroupSet = UserInGroup("canconfirm");
        }
        if ($UserInCanConfirmGroupSet) {
            return 1;
        }
282 283 284 285
        SendSQL("SELECT everconfirmed FROM bugs WHERE bug_id = $bugid");
        my $everconfirmed = FetchOneColumn();
        if ($everconfirmed) {
            return 1;
286
        }
287 288 289
    } elsif ($reporterid eq $whoid || $ownerid eq $whoid ||
             $qacontactid eq $whoid) {
        return 1;
290 291
    }
    SendSQL("UNLOCK TABLES");
292 293 294 295 296 297
    $oldvalue = html_quote($oldvalue);
    $newvalue = html_quote($newvalue);
    ThrowUserError("You tried to change the <strong>$f</strong> field 
                    from <em>$oldvalue</em> to <em>$newvalue</em>, 
                    but only the owner or submitter of the bug, or a 
                    sufficiently empowered user, may change that field.");
298 299
}

300 301
# Confirm that the reporter of the current bug can access the bug we are duping to.
sub DuplicateUserConfirm {
302 303 304 305 306
    # if we've already been through here, then exit
    if (defined $::FORM{'confirm_add_duplicate'}) {
        return;
    }

307 308 309 310 311 312 313 314
    my $dupe = trim($::FORM{'id'});
    my $original = trim($::FORM{'dup_id'});
    
    SendSQL("SELECT reporter FROM bugs WHERE bug_id = " . SqlQuote($dupe));
    my $reporter = FetchOneColumn();
    SendSQL("SELECT profiles.groupset FROM profiles WHERE profiles.userid =".SqlQuote($reporter));
    my $reportergroupset = FetchOneColumn();

315
    if (CanSeeBug($original, $reporter, $reportergroupset)) {
316 317 318
        $::FORM{'confirm_add_duplicate'} = "1";
        return;
    }
319 320

    SendSQL("SELECT cclist_accessible FROM bugs WHERE bug_id = $original");
321
    $vars->{'cclist_accessible'} = FetchOneColumn();
322
    
323 324 325
    # 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.
326
    
327 328 329 330 331 332
    $vars->{'form'} = \%::FORM;
    $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).
333
    print "Content-type: text/html\n\n";
334
    $template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
335
      || ThrowTemplateError($template->error());
336 337
    exit;
} # end DuplicateUserConfirm()
338

339
if (defined $::FORM{'id'}) {
340 341 342 343 344 345 346
    # 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...)
    #
347 348 349 350 351 352 353 354 355 356 357 358
    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');
terry%netscape.com's avatar
terry%netscape.com committed
359 360
}

361 362 363 364
my $action  = '';
if (defined $::FORM{action}) {
  $action  = trim($::FORM{action});
}
365
if (Param("move-enabled") && $action eq Param("move-button-text")) {
366 367 368 369 370 371 372
  $::FORM{'buglist'} = join (":", @idlist);
  do "move.pl" || die "Error executing move.cgi: $!";
  PutFooter();
  exit;
}


373 374 375 376 377 378 379
$::query = "update bugs\nset";
$::comma = "";
umask(0);

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

382 383 384 385 386 387 388 389 390 391 392 393 394 395
sub DoConfirm {
    if ($UserInEditGroupSet < 0) {
        $UserInEditGroupSet = UserInGroup("editbugs");
    }
    if ($UserInCanConfirmGroupSet < 0) {
        $UserInCanConfirmGroupSet = UserInGroup("canconfirm");
    }
    if ($UserInEditGroupSet || $UserInCanConfirmGroupSet) {
        DoComma();
        $::query .= "everconfirmed = 1";
    }
}


396 397 398 399
sub ChangeStatus {
    my ($str) = (@_);
    if ($str ne $::dontchange) {
        DoComma();
400 401 402 403 404 405 406 407 408 409 410 411 412
        # Ugly, but functional.  We don't want to change Status if we are
        # reasigning non-open bugs via the mass change form.
        if ( ($::FORM{knob} eq 'reassign' || $::FORM{knob} eq 'reassignbycomponent') &&
             ! defined $::FORM{id} && $str eq 'NEW' ) {
            # If we got to here, we're dealing with a reassign from the mass
            # change page.  We don't know (and can't easily figure out) if this
            # bug is open or closed.  If it's closed, we don't want to change
            # its status to NEW.  We have to put some logic into the SQL itself
            # to handle that.
            my @open_state = map(SqlQuote($_), OpenStates());
            my $open_state = join(", ", @open_state);
            $::query .= "bug_status = IF(bug_status IN($open_state), '$str', bug_status)";
        } elsif (IsOpenedState($str)) {
413 414 415 416
            $::query .= "bug_status = IF(everconfirmed = 1, '$str', '$::unconfirmedstate')";
        } else {
            $::query .= "bug_status = '$str'";
        }
417 418 419
        $::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
420 421 422
    }
}

423 424 425 426
sub ChangeResolution {
    my ($str) = (@_);
    if ($str ne $::dontchange) {
        DoComma();
427
        $::query .= "resolution = " . SqlQuote($str);
terry%netscape.com's avatar
terry%netscape.com committed
428 429 430
    }
}

431 432 433 434
# 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.
435 436 437 438 439
# In order to make mass changes work correctly, keep a sum of bits for groups
# added, and another one for groups removed, and then let mysql do the bit
# operations
# If the form element isn't present, or the user isn't in the group, leave
# it as-is
440
if($::usergroupset ne '0') {
441 442 443 444 445 446
    my $groupAdd = "0";
    my $groupDel = "0";

    SendSQL("SELECT bit, isactive FROM groups WHERE " .
            "isbuggroup != 0 AND bit & $::usergroupset != 0 ORDER BY bit");
    while (my ($b, $isactive) = FetchSQLData()) {
447 448 449 450 451 452 453 454 455 456 457 458
        # 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"}) {
                $groupDel .= "+$b";
            } elsif ($::FORM{"bit-$b"} == 1 && $isactive) {
                $groupAdd .= "+$b";
            }
459 460 461 462
        }
    }
    if ($groupAdd ne "0" || $groupDel ne "0") {
        DoComma();
463 464 465
        # mysql < 3.23.5 doesn't support the ~ operator, even though
        # the docs say that it does
        $::query .= "groupset = ((groupset & ($::superusergroupset - ($groupDel))) | ($groupAdd))";
466
    }
467 468
}

469
foreach my $field ("rep_platform", "priority", "bug_severity",          
470
                   "summary", "component", "bug_file_loc", "short_desc",
471
                   "product", "version", "op_sys",
472
                   "target_milestone", "status_whiteboard") {
473 474 475
    if (defined $::FORM{$field}) {
        if ($::FORM{$field} ne $::dontchange) {
            DoComma();
476
            $::query .= "$field = " . SqlQuote(trim($::FORM{$field}));
terry%netscape.com's avatar
terry%netscape.com committed
477 478 479 480
        }
    }
}

481

482 483
if (defined $::FORM{'qa_contact'}) {
    my $name = trim($::FORM{'qa_contact'});
terry%netscape.com's avatar
terry%netscape.com committed
484
    if ($name ne $::dontchange) {
485 486 487 488 489 490 491 492 493
        my $id = 0;
        if ($name ne "") {
            $id = DBNameToIdAndCheck($name);
        }
        DoComma();
        $::query .= "qa_contact = $id";
    }
}

494

495 496
# 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
497
# allowed the user to set whether or not the reporter
498 499
# and cc list can see the bug even if they are not members of all groups 
# to which the bug is restricted.
500
if ( $::FORM{'id'} ) {
501 502 503
    SendSQL("SELECT groupset FROM bugs WHERE bug_id = $::FORM{'id'}");
    my ($groupset) = FetchSQLData();
    if ( $groupset ) {
504 505 506 507 508 509 510 511 512 513 514
        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'}";
    }
}


515
my $duplicate = 0;
516

517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
# 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...
535
        if (defined $::FORM{removecc} && $::FORM{cc}) {
536 537 538 539 540
            $cc_remove = join (",", @{$::MFORM{cc}});
        }
    }

    if ($cc_add) {
541 542
        $cc_add =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_add) ) {
543 544 545 546 547
            my $pid = DBNameToIdAndCheck($person);
            $cc_add{$pid} = $person;
        }
    }
    if ($cc_remove) {
548 549
        $cc_remove =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_remove) ) {
550 551 552 553 554 555 556
            my $pid = DBNameToIdAndCheck($person);
            $cc_remove{$pid} = $person;
        }
    }
}


557
CheckFormFieldDefined(\%::FORM, 'knob');
558 559 560 561
SWITCH: for ($::FORM{'knob'}) {
    /^none$/ && do {
        last SWITCH;
    };
562 563 564 565 566
    /^confirm$/ && CheckonComment( "confirm" ) && do {
        DoConfirm();
        ChangeStatus('NEW');
        last SWITCH;
    };
567
    /^accept$/ && CheckonComment( "accept" ) && do {
568
        DoConfirm();
569
        ChangeStatus('ASSIGNED');
570 571
        if (Param("musthavemilestoneonaccept") &&
                scalar(@{$::target_milestone{$::FORM{'product'}}}) > 1) {
572 573 574 575
            if (Param("usetargetmilestone")) {
                $requiremilestone = 1;
            }
        }
576 577
        last SWITCH;
    };
578
    /^clearresolution$/ && CheckonComment( "clearresolution" ) && do {
579 580 581
        ChangeResolution('');
        last SWITCH;
    };
582
    /^resolve$/ && CheckonComment( "resolve" ) && do {
583 584
        # Check here, because its the only place we require the resolution
        CheckFormField(\%::FORM, 'resolution', \@::settable_resolution);
585 586 587 588
        ChangeStatus('RESOLVED');
        ChangeResolution($::FORM{'resolution'});
        last SWITCH;
    };
589
    /^reassign$/ && CheckonComment( "reassign" ) && do {
590 591 592
        if ($::FORM{'andconfirm'}) {
            DoConfirm();
        }
593 594
        ChangeStatus('NEW');
        DoComma();
595 596
        if ( !defined$::FORM{'assigned_to'} ||
             trim($::FORM{'assigned_to'}) eq "") {
597 598 599
          ThrowUserError("You cannot reassign to a bug to nobody.  Unless you
                          intentionally cleared out the \"Reassign bug to\" 
                          field, " . Param("browserbugmessage"));
600
        }
601
        my $newid = DBNameToIdAndCheck(trim($::FORM{'assigned_to'}));
602 603 604
        $::query .= "assigned_to = $newid";
        last SWITCH;
    };
605
    /^reassignbycomponent$/  && CheckonComment( "reassignbycomponent" ) && do {
606
        if ($::FORM{'product'} eq $::dontchange) {
607 608
            ThrowUserError("You must specify a product to help determine 
                            the new owner of these bugs.");
609
        }
610
        if ($::FORM{'component'} eq $::dontchange) {
611 612
            ThrowUserError("You must specify a component whose owner 
                            should get assigned these bugs.");
terry%netscape.com's avatar
terry%netscape.com committed
613
        }
614 615 616
        if ($::FORM{'compconfirm'}) {
            DoConfirm();
        }
617 618
        ChangeStatus('NEW');
        SendSQL("select initialowner from components where program=" .
619 620
                SqlQuote($::FORM{'product'}) . " and value=" .
                SqlQuote($::FORM{'component'}));
621 622
        my $newid = FetchOneColumn();
        my $newname = DBID_to_name($newid);
623 624
        DoComma();
        $::query .= "assigned_to = $newid";
625 626 627 628 629
        if (Param("useqacontact")) {
            SendSQL("select initialqacontact from components where program=" .
                    SqlQuote($::FORM{'product'}) .
                    " and value=" . SqlQuote($::FORM{'component'}));
            my $qacontact = FetchOneColumn();
630
            if (defined $qacontact && $qacontact != 0) {
631
                DoComma();
632
                $::query .= "qa_contact = $qacontact";
633 634
            }
        }
635 636
        last SWITCH;
    };   
637
    /^reopen$/  && CheckonComment( "reopen" ) && do {
638
                SendSQL("SELECT resolution FROM bugs WHERE bug_id = $::FORM{'id'}");
639
        ChangeStatus('REOPENED');
640
        ChangeResolution('');
641 642 643
                if (FetchOneColumn() eq 'DUPLICATE') {
                        SendSQL("DELETE FROM duplicates WHERE dupe = $::FORM{'id'}");
                }
644 645
        last SWITCH;
    };
646
    /^verify$/ && CheckonComment( "verify" ) && do {
647 648 649
        ChangeStatus('VERIFIED');
        last SWITCH;
    };
650
    /^close$/ && CheckonComment( "close" ) && do {
651 652 653
        ChangeStatus('CLOSED');
        last SWITCH;
    };
654
    /^duplicate$/ && CheckonComment( "duplicate" ) && do {
655 656
        ChangeStatus('RESOLVED');
        ChangeResolution('DUPLICATE');
657
        CheckFormFieldDefined(\%::FORM,'dup_id');
658
        my $num = trim($::FORM{'dup_id'});
659 660 661
        SendSQL("SELECT bug_id FROM bugs WHERE bug_id = " . SqlQuote($num));
        $num = FetchOneColumn();
        if (!$num) {
662 663
            ThrowUserError("You must specify a valid bug number of which this bug
                            is a duplicate.  The bug has not been changed.")
terry%netscape.com's avatar
terry%netscape.com committed
664
        }
665
        if (!defined($::FORM{'id'}) || $num == $::FORM{'id'}) {
666 667 668
            ThrowUserError("Nice try, $::COOKIE{'Bugzilla_login'}, but it doesn't 
                            really make sense to mark a bug as a duplicate of itself, 
                            does it?");
terry%netscape.com's avatar
terry%netscape.com committed
669
        }
670 671 672 673
        my $checkid = trim($::FORM{'id'});
        SendSQL("SELECT bug_id FROM bugs where bug_id = " .  SqlQuote($checkid));
        $checkid = FetchOneColumn();
        if (!$checkid) {
674
            ThrowUserError("The bug id $::FORM{'id'} is invalid.");
675
        }
676
        $::FORM{'comment'} .= "\n\n*** This bug has been marked as a duplicate of $num ***";
677
        $duplicate = $num;
678

679 680 681
        last SWITCH;
    };
    # default
682 683
    my $escaped_knob = html_quote($::FORM{'knob'});
    ThrowCodeError("Unknown action $escaped_knob!\n");
terry%netscape.com's avatar
terry%netscape.com committed
684 685 686
}


687
if ($#idlist < 0) {
688
    ThrowUserError("You apparently didn't choose any bugs to modify.");
terry%netscape.com's avatar
terry%netscape.com committed
689 690
}

691 692 693 694 695

my @keywordlist;
my %keywordseen;

if ($::FORM{'keywords'}) {
696 697 698 699
    foreach my $keyword (split(/[\s,]+/, $::FORM{'keywords'})) {
        if ($keyword eq '') {
            next;
        }
700
        my $i = GetKeywordIdFromName($keyword);
701
        if (!$i) {
702 703 704
            ThrowUserError("Unknown keyword named <code>" . html_quote($keyword) . 
                           "</code>. <p>The legal keyword names are 
                            <a href=\"describekeywords.cgi\">listed here</a></p>.");
705 706 707 708 709 710 711 712
        }
        if (!$keywordseen{$i}) {
            push(@keywordlist, $i);
            $keywordseen{$i} = 1;
        }
    }
}

713 714
my $keywordaction = $::FORM{'keywordaction'} || "makeexact";

715 716 717 718
if ($::comma eq ""
    && 0 == @keywordlist && $keywordaction ne "makeexact"
    && defined $::FORM{'masscc'} && ! $::FORM{'masscc'}
    ) {
719
    if (!defined $::FORM{'comment'} || $::FORM{'comment'} =~ /^\s*$/) {
720 721
        ThrowUserError("Um, you apparently did not change anything 
                        on the selected bugs.");
terry%netscape.com's avatar
terry%netscape.com committed
722 723 724
    }
}

725
my $basequery = $::query;
726
my $delta_ts;
terry%netscape.com's avatar
terry%netscape.com committed
727

728

729 730
sub SnapShotBug {
    my ($id) = (@_);
731
    SendSQL("select delta_ts, " . join(',', @::log_columns) .
732
            " from bugs where bug_id = $id");
733 734
    my @row = FetchSQLData();
    $delta_ts = shift @row;
735

736
    return @row;
terry%netscape.com's avatar
terry%netscape.com committed
737 738 739
}


740 741 742 743 744 745 746 747 748 749 750 751
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;
752
my $bug_changed;
753

754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
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;
}

sub LogActivityEntry {
    my ($i,$col,$removed,$added) = @_;
    # in the case of CCs, deps, and keywords, there's a possibility that someone
    # might try to add or remove a lot of them at once, which might take more
    # space than the activity table allows.  We'll solve this by splitting it
    # into multiple entries if it's too long.
    while ($removed || $added) {
        my ($removestr, $addstr) = ($removed, $added);
        if (length($removestr) > 254) {
            my $commaposition = FindWrapPoint($removed, 254);
            $removestr = substr($removed,0,$commaposition);
            $removed = substr($removed,$commaposition);
            $removed =~ s/^[,\s]+//; # remove any comma or space
        } else {
            $removed = ""; # no more entries
        }
        if (length($addstr) > 254) {
            my $commaposition = FindWrapPoint($added, 254);
            $addstr = substr($added,0,$commaposition);
            $added = substr($added,$commaposition);
            $added =~ s/^[,\s]+//; # remove any comma or space
        } else {
            $added = ""; # no more entries
        }
        $addstr = SqlQuote($addstr);
        $removestr = SqlQuote($removestr);
        my $fieldid = GetFieldID($col);
        SendSQL("INSERT INTO bugs_activity " .
                "(bug_id,who,bug_when,fieldid,removed,added) VALUES " .
802 803
                "($i,$whoid," . SqlQuote($timestamp) . ",$fieldid,$removestr,$addstr)");
        $bug_changed = 1;
804 805 806
    }
}

807 808 809 810
sub LogDependencyActivity {
    my ($i, $oldstr, $target, $me) = (@_);
    my $newstr = SnapShotDeps($i, $target, $me);
    if ($oldstr ne $newstr) {
811 812
        # Figure out what's really different...
        my ($removed, $added) = DiffStrings($oldstr, $newstr);
813
        LogActivityEntry($i,$target,$removed,$added);
814 815
        # update timestamp on target bug so midairs will be triggered
        SendSQL("UPDATE bugs SET delta_ts=NOW() WHERE bug_id=$i");
816 817 818 819 820
        return 1;
    }
    return 0;
}

821
# this loop iterates once for each bug to be processed (eg when this script
822
# is called with multiple bugs selected from buglist.cgi instead of
823 824
# show_bug.cgi).
#
825
foreach my $id (@idlist) {
826
    my %dependencychanged;
827
    $bug_changed = 0;
828 829
    my $write = "WRITE";        # Might want to make a param to control
                                # whether we do LOW_PRIORITY ...
830
    SendSQL("LOCK TABLES bugs $write, bugs_activity $write, cc $write, " .
831
            "cc AS selectVisible_cc $write, " .
832 833
            "profiles $write, dependencies $write, votes $write, " .
            "keywords $write, longdescs $write, fielddefs $write, " .
834
            "keyworddefs READ, groups READ, attachments READ, products READ");
835
    my @oldvalues = SnapShotBug($id);
836
    my %oldhash;
837 838
    my $i = 0;
    foreach my $col (@::log_columns) {
839
        $oldhash{$col} = $oldvalues[$i];
840 841 842 843 844
        if (exists $::FORM{$col}) {
            CheckCanChangeField($col, $id, $oldvalues[$i], $::FORM{$col});
        }
        $i++;
    }
845 846 847 848 849 850 851 852 853
    if ($requiremilestone) {
        my $value = $::FORM{'target_milestone'};
        if (!defined $value || $value eq $::dontchange) {
            $value = $oldhash{'target_milestone'};
        }
        SendSQL("SELECT defaultmilestone FROM products WHERE product = " .
                SqlQuote($oldhash{'product'}));
        if ($value eq FetchOneColumn()) {
            SendSQL("UNLOCK TABLES");
854 855 856 857 858
            ThrowUserError("You must determine a target milestone for bug $id
                            if you are going to accept it.  Part of accepting 
                            a bug is giving an estimate of when it will be fixed.", 
                           undef, 
                           "abort");
859 860
        }
    }   
861
    if (defined $::FORM{'delta_ts'} && $::FORM{'delta_ts'} ne $delta_ts) {
862 863 864 865
        ($vars->{'operations'}) = GetBugActivity($::FORM{'id'}, $::FORM{'delta_ts'});

        $vars->{'start_at'} = $::FORM{'longdesclength'};
        $vars->{'comments'} = GetComments($id);
866
        
867 868 869
        $::FORM{'comment'} =~ s/\r\n/\n/g;     # Get rid of windows-style line endings.
        $::FORM{'comment'} =~ s/\r/\n/g;       # Get rid of mac-style line endings.

870
        $::FORM{'delta_ts'} = $delta_ts;
871 872 873 874 875 876 877 878
        $vars->{'form'} = \%::FORM;
        
        $vars->{'bug_id'} = $id;
        $vars->{'quoteUrls'} = \&quoteUrls;
        
        SendSQL("UNLOCK TABLES");
        
        # Warn the user about the mid-air collision and ask them what to do.
879
        $template->process("bug/process/midair.html.tmpl", $vars)
880
          || ThrowTemplateError($template->error());
881 882 883
        exit;
    }
        
884 885 886 887
    my %deps;
    if (defined $::FORM{'dependson'}) {
        my $me = "blocked";
        my $target = "dependson";
888
        my %deptree;
889
        for (1..2) {
890
            $deptree{$target} = [];
891 892 893 894
            my %seen;
            foreach my $i (split('[\s,]+', $::FORM{$target})) {
                if ($i eq "") {
                    next;
895
                }
896

897 898
                my $orig = $i;
                if (!detaint_natural($i)) {
899
                    ThrowUserError("$orig is not a legal bug number", undef, "abort");
900
                }
901 902 903

                # Don't use CanSeeBug, since we want to keep deps to bugs a
                # user can't see
904 905 906 907
                SendSQL("select bug_id from bugs where bug_id = " .
                        SqlQuote($i));
                my $comp = FetchOneColumn();
                if ($comp ne $i) {
908
                    ThrowUserError("$i is not a legal bug number", undef, "abort");
909
                }
910
                if ($id eq $i) {
911 912 913
                    ThrowUserError("You can't make a bug blocked or dependent on itself.",
                                   undef,
                                   "abort");
914
                }
915
                if (!exists $seen{$i}) {
916
                    push(@{$deptree{$target}}, $i);
917 918 919
                    $seen{$i} = 1;
                }
            }
920 921 922
            # populate $deps{$target} as first-level deps only.
            # and find remainder of dependency tree in $deptree{$target}
            @{$deps{$target}} = @{$deptree{$target}};
923 924 925
            my @stack = @{$deps{$target}};
            while (@stack) {
                my $i = shift @stack;
926 927
                SendSQL("select $target from dependencies where $me = " .
                        SqlQuote($i));
928 929
                while (MoreSQLData()) {
                    my $t = FetchOneColumn();
930 931 932 933
                    # 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);
934 935 936 937 938
                        push @stack, $t;
                        $seen{$t} = 1;
                    }
                }
            }
939

940
            if ($me eq 'dependson') {
941 942
                my @deps   =  @{$deptree{'dependson'}};
                my @blocks =  @{$deptree{'blocked'}};
943 944 945 946 947 948 949
                my @union = ();
                my @isect = ();
                my %union = ();
                my %isect = ();
                foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ }
                @union = keys %union;
                @isect = keys %isect;
950
                if (@isect > 0) {
951 952
                    my $both;
                    foreach my $i (@isect) {
953
                       $both = $both . GetBugLink($i, "#" . $i) . " ";
954
                    }
955 956 957 958 959 960 961
                    ThrowUserError(qq|Dependency loop detected!<p>
                      The following bug(s) would appear on both the "depends on"
                      and "blocks" parts of the dependency tree if these changes
                      are committed: $both<br>This would create a circular 
                      dependency, which is not allowed.</p>|,
                      undef,
                      "abort");
962 963
                }
            }
964 965 966 967 968
            my $tmp = $me;
            $me = $target;
            $target = $tmp;
        }
    }
969

970 971 972 973 974 975
    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.
976
        my $changed = 0;
977 978
        if ($keywordaction eq "makeexact") {
            SendSQL("DELETE FROM keywords WHERE bug_id = $id");
979
            $changed = 1;
980 981 982 983 984
        }
        foreach my $keyword (@keywordlist) {
            if ($keywordaction ne "makeexact") {
                SendSQL("DELETE FROM keywords
                         WHERE bug_id = $id AND keywordid = $keyword");
985
                $changed = 1;
986 987 988 989
            }
            if ($keywordaction ne "delete") {
                SendSQL("INSERT INTO keywords 
                         (bug_id, keywordid) VALUES ($id, $keyword)");
990 991 992 993 994 995 996 997 998 999 1000 1001
                $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());
1002
            }
1003 1004 1005
            SendSQL("UPDATE bugs SET keywords = " .
                    SqlQuote(join(', ', @list)) .
                    " WHERE bug_id = $id");
1006 1007
        }
    }
1008
    my $query = "$basequery\nwhere bug_id = $id";
terry%netscape.com's avatar
terry%netscape.com committed
1009
    
1010 1011
    if ($::comma ne "") {
        SendSQL($query);
terry%netscape.com's avatar
terry%netscape.com committed
1012
    }
1013
    SendSQL("select now()");
1014
    $timestamp = FetchOneColumn();
terry%netscape.com's avatar
terry%netscape.com committed
1015
    
1016
    if (defined $::FORM{'comment'}) {
1017
        AppendComment($id, $::COOKIE{'Bugzilla_login'}, $::FORM{'comment'});
terry%netscape.com's avatar
terry%netscape.com committed
1018 1019
    }
    
1020
    my $removedCcString = "";
1021 1022 1023 1024 1025 1026 1027 1028 1029
    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) = ();
1030 1031 1032 1033 1034 1035
        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;
1036 1037
            }
        }
1038 1039 1040 1041 1042 1043
        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;
1044 1045
            }
        }
1046 1047 1048
        # Save off the removedCcString so it can be fed to processmail
        $removedCcString = join (",", @removed);

1049 1050
        # If any changes were found, record it in the activity log
        if (scalar(@removed) || scalar(@added)) {
1051 1052 1053
            my $removed = join(", ", @removed);
            my $added = join(", ", @added);
            LogActivityEntry($id,"cc",$removed,$added);
1054
        }
1055
    }
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067

    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}};
1068 1069
            @dependencychanged{@oldlist} = 1;
            @dependencychanged{@newlist} = 1;
1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097

            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) {
1098
                    LogDependencyActivity($k, $snapshot{$k}, $me, $target);
1099 1100 1101 1102 1103 1104 1105 1106 1107 1108
                }
                LogDependencyActivity($id, $oldsnap, $target, $me);
            }

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

1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
    # 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.
    if ( 
      # the "usebuggroups" parameter is on, indicating that products
      # are associated with groups of the same name;
      Param('usebuggroups')

      # the user has changed the product to which the bug belongs;
      && defined $::FORM{'product'} 
        && $::FORM{'product'} ne $::dontchange 
          && $::FORM{'product'} ne $oldhash{'product'} 
    ) {
        if (
          # the user wants to add the bug to the new product's group;
          ($::FORM{'addtonewgroup'} eq 'yes' 
            || ($::FORM{'addtonewgroup'} eq 'yesifinold' 
                  && GroupNameToBit($oldhash{'product'}) & $oldhash{'groupset'})) 

          # the new product is associated with a group;
          && GroupExists($::FORM{'product'})

          # the bug is not already in the group; (This can happen when the user
          # goes to the "edit multiple bugs" form with a list of bugs at least
          # one of which is in the new group.  In this situation, the user can
          # simultaneously change the bugs to a new product and move the bugs
          # into that product's group, which happens earlier in this script
          # and thus is already done.  If we didn't check for this, then this
          # situation would cause us to add the bug to the group twice, which
          # would result in the bug being added to a totally different group.)
          && !BugInGroup($id, $::FORM{'product'})

          # the user is a member of the associated group, indicating they
          # are authorized to add bugs to that group, *or* the "usebuggroupsentry"
          # parameter is off, indicating that users can add bugs to a product 
          # regardless of whether or not they belong to its associated group;
          && (UserInGroup($::FORM{'product'}) || !Param('usebuggroupsentry'))

          # the associated group is active, indicating it can accept new bugs;
          && GroupIsActive(GroupNameToBit($::FORM{'product'}))
        ) { 
            # Add the bug to the group associated with its new product.
            my $groupbit = GroupNameToBit($::FORM{'product'});
            SendSQL("UPDATE bugs SET groupset = groupset + $groupbit WHERE bug_id = $id");
        }

        if ( 
          # the old product is associated with a group;
          GroupExists($oldhash{'product'})

          # the bug is a member of that group;
          && BugInGroup($id, $oldhash{'product'}) 
        ) { 
            # Remove the bug from the group associated with its old product.
            my $groupbit = GroupNameToBit($oldhash{'product'});
            SendSQL("UPDATE bugs SET groupset = groupset - $groupbit WHERE bug_id = $id");
        }

    }
  
1171 1172 1173 1174
    # 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.
    #
1175
    my @newvalues = SnapShotBug($id);
1176

1177 1178 1179 1180 1181 1182
    # for passing to processmail to ensure that when someone is removed
    # from one of these fields, they get notified of that fact (if desired)
    #
    my $origOwner = "";
    my $origQaContact = "";

1183
    foreach my $c (@::log_columns) {
1184 1185
        my $col = $c;           # We modify it, don't want to modify array
                                # values in place.
1186 1187
        my $old = shift @oldvalues;
        my $new = shift @newvalues;
1188 1189 1190 1191 1192 1193
        if (!defined $old) {
            $old = "";
        }
        if (!defined $new) {
            $new = "";
        }
1194
        if ($old ne $new) {
1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207

            # save off the old value for passing to processmail so the old
            # owner can be notified
            #
            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') {
1208 1209
                $old = ($old) ? DBID_to_name($old) : "";
                $new = ($new) ? DBID_to_name($new) : "";
1210
                $origQaContact = $old;
terry%netscape.com's avatar
terry%netscape.com committed
1211
            }
1212

1213 1214 1215 1216 1217
            # If this is the keyword field, only record the changes, not everything.
            if ($col eq 'keywords') {
                ($old, $new) = DiffStrings($old, $new);
            }

1218
            if ($col eq 'product') {
1219
                RemoveVotes($id, 0,
1220 1221
                            "This bug has been moved to a different product");
            }
1222
            LogActivityEntry($id,$col,$old,$new);
terry%netscape.com's avatar
terry%netscape.com committed
1223 1224
        }
    }
1225 1226 1227
    if ($bug_changed) {
        SendSQL("UPDATE bugs SET delta_ts = " . SqlQuote($timestamp) . " WHERE bug_id = $id");
    }
1228
    SendSQL("UNLOCK TABLES");
1229

1230
    my @ARGLIST = ();
1231 1232 1233 1234 1235 1236 1237 1238 1239
    if ( $removedCcString ne "" ) {
        push @ARGLIST, ("-forcecc", $removedCcString);
    }
    if ( $origOwner ne "" ) {
        push @ARGLIST, ("-forceowner", $origOwner);
    }
    if ( $origQaContact ne "") { 
        push @ARGLIST, ( "-forceqacontact", $origQaContact);
    }
1240
    push @ARGLIST, ($id, $::COOKIE{'Bugzilla_login'});
1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256
  
    # Send mail to let people know the bug has been changed.  Uses 
    # a special syntax of the "open" and "exec" commands to capture 
    # the output "processmail", which "system" doesn't allow 
    # (i.e. "system ('./processmail', $bugid , $::userid);"), without 
    # the insecurity of running the command through a shell via backticks
    # (i.e. "my $mailresults = `./processmail $bugid $::userid`;").
    $vars->{'mail'} = "";
    open(PMAIL, "-|") or exec('./processmail', @ARGLIST);
    $vars->{'mail'} .= $_ while <PMAIL>;
    close(PMAIL);

    $vars->{'id'} = $id;
    
    # Let the user know the bug was changed and who did and didn't
    # receive email about the change.
1257
    $template->process("bug/process/results.html.tmpl", $vars)
1258 1259
      || ThrowTemplateError($template->error());
    
1260 1261 1262 1263 1264 1265 1266 1267
    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();
1268 1269 1270
        unless ($isreporter || $isoncc || ! $::FORM{'confirm_add_duplicate'}) {
            # The reporter is oblivious to the existance of the new bug and is permitted access
            # ... add 'em to the cc (and record activity)
1271
            LogActivityEntry($duplicate,"cc","",DBID_to_name($reporter));
1272 1273
            SendSQL("INSERT INTO cc (who, bug_id) VALUES ($reporter, " . SqlQuote($duplicate) . ")");
        }
1274
        AppendComment($duplicate, $::COOKIE{'Bugzilla_login'}, "*** Bug $::FORM{'id'} has been marked as a duplicate of this bug. ***");
1275
        CheckFormFieldDefined(\%::FORM,'comment');
1276
        SendSQL("INSERT INTO duplicates VALUES ($duplicate, $::FORM{'id'})");
1277 1278 1279 1280 1281 1282 1283 1284 1285 1286
        
        $vars->{'mail'} = "";
        open(PMAIL, "-|") or exec('./processmail', $duplicate, $::COOKIE{'Bugzilla_login'});
        $vars->{'mail'} .= $_ while <PMAIL>;
        close(PMAIL);
        
        $vars->{'id'} = $duplicate;
        $vars->{'type'} = "dupe";
        
        # Let the user know a duplication notation was added to the original bug.
1287
        $template->process("bug/process/results.html.tmpl", $vars)
1288
          || ThrowTemplateError($template->error());
1289 1290
    }

1291
    foreach my $k (keys(%dependencychanged)) {
1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302
        $vars->{'mail'} = "";
        open(PMAIL, "-|") or exec('./processmail', $k, $::COOKIE{'Bugzilla_login'});
        $vars->{'mail'} .= $_ while <PMAIL>;
        close(PMAIL);
        
        $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.
1303
        $template->process("bug/process/results.html.tmpl", $vars)
1304
          || ThrowTemplateError($template->error());
1305 1306
    }

terry%netscape.com's avatar
terry%netscape.com committed
1307 1308
}

1309 1310 1311
# Show next bug, if it exists.
if ($::COOKIE{"BUGLIST"} && $::FORM{'id'}) {
    my @bugs = split(/:/, $::COOKIE{"BUGLIST"});
1312
    $vars->{'bug_list'} = \@bugs;
1313 1314 1315 1316 1317
    my $cur = lsearch(\@bugs, $::FORM{"id"});
    if ($cur >= 0 && $cur < $#bugs) {
        my $next_bug = $bugs[$cur + 1];
        if (detaint_natural($next_bug) && CanSeeBug($next_bug)) {
            $::FORM{'id'} = $next_bug;
1318 1319 1320 1321
            
            $vars->{'next_id'} = $next_bug;
            
            # Let the user know we are about to display the next bug in their list.
1322
            $template->process("bug/process/next.html.tmpl", $vars)
1323
              || ThrowTemplateError($template->error());
1324 1325 1326 1327 1328 1329

            show_bug("header is already done");

            exit;
        }
    }
terry%netscape.com's avatar
terry%netscape.com committed
1330
}
1331

1332
# End the response page.
1333
$template->process("bug/navigate.html.tmpl", $vars)
1334
  || ThrowTemplateError($template->error());
1335
$template->process("global/footer.html.tmpl", $vars)
1336
  || ThrowTemplateError($template->error());