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

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

42 43
use strict;

44 45
my $UserInEditGroupSet = -1;
my $UserInCanConfirmGroupSet = -1;
46
my $PrivilegesRequired = 0;
47
my $lastbugid = 0;
48

49 50
use lib qw(.);

51
require "globals.pl";
52
use Bugzilla;
53
use Bugzilla::Constants;
54
use Bugzilla::Bug;
55
use Bugzilla::Mailer;
56
use Bugzilla::User;
57
use Bugzilla::Util;
58
use Bugzilla::Field;
59
use Bugzilla::Product;
60
use Bugzilla::Component;
61
use Bugzilla::Keyword;
62

63 64
# Use the Flag module to modify flag data if the user set flags.
use Bugzilla::Flag;
65
use Bugzilla::FlagType;
66

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

69
use vars qw(%legal_opsys
70 71 72 73
          %legal_platform
          %legal_priority
          %settable_resolution
          %legal_severity
74
           );
75

76
my $user = Bugzilla->login(LOGIN_REQUIRED);
77
my $whoid = $user->id;
78
my $grouplist = $user->groups_as_string;
79

80
my $cgi = Bugzilla->cgi;
81
my $dbh = Bugzilla->dbh;
82 83
my $template = Bugzilla->template;
my $vars = {};
84
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
85

86 87
my @editable_bug_fields = editable_bug_fields();

88 89
my $requiremilestone = 0;

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
######################################################################
# Subroutines
######################################################################

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

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
# This function checks if there are any default groups defined.
# If so, then groups may have to be changed when bugs move from
# one bug to another.
sub AnyDefaultGroups {
    my $dbh = Bugzilla->dbh;
    my $any_default =
        $dbh->selectrow_array('SELECT 1
                                 FROM group_control_map
                           INNER JOIN groups
                                   ON groups.id = group_control_map.group_id
                                WHERE isactive != 0
                                  AND (membercontrol = ? OR othercontrol = ?) ' .
                                 $dbh->sql_limit(1),
                                 undef, (CONTROLMAPDEFAULT, CONTROLMAPDEFAULT));
    return $any_default;
}

122 123 124 125 126 127 128 129
######################################################################
# 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.
130 131
# 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.
132
my @idlist;
133 134 135 136 137 138 139 140
if (defined $cgi->param('id')) {
  my $id = $cgi->param('id');
  ValidateBugID($id);

  # Store the validated, and detainted id back in the cgi data, as
  # lots of later code will need it, and will obtain it from there
  $cgi->param('id', $id);
  push @idlist, $id;
141
} else {
142
    foreach my $i ($cgi->param()) {
143
        if ($i =~ /^id_([1-9][0-9]*)/) {
144 145 146
            my $id = $1;
            ValidateBugID($id);
            push @idlist, $id;
147
        }
148 149 150
    }
}

151
# Make sure there are bugs to process.
152
scalar(@idlist) || ThrowUserError("no_bugs_chosen");
153

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

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

160 161
# Validate all timetracking fields
foreach my $field ("estimated_time", "work_time", "remaining_time") {
162 163 164
    if (defined $cgi->param($field)) {
        my $er_time = trim($cgi->param($field));
        if ($er_time ne $cgi->param('dontchange')) {
165 166 167 168 169
            Bugzilla::Bug::ValidateTime($er_time, $field);
        }
    }
}

170
if (UserInGroup(Param('timetrackinggroup'))) {
171 172
    my $wk_time = $cgi->param('work_time');
    if ($cgi->param('comment') =~ /^\s*$/ && $wk_time && $wk_time != 0) {
173
        ThrowUserError('comment_required');
174
    }
175 176
}

177
ValidateComment(scalar $cgi->param('comment'));
178

179 180 181 182 183 184 185
# 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") {
186 187 188 189
    if ($cgi->param('id')) {
        my $bug = new Bugzilla::Bug($cgi->param('id'), $user->id);
        my @old = @{$bug->$field};
        my @new;
190
        foreach my $id (split(/[\s,]+/, $cgi->param($field))) {
191
            next unless $id;
192
            ValidateBugID($id, $field);
193
            push @new, $id;
194
        }
195 196 197 198 199 200
        $cgi->param($field, join(",", @new));
        my ($added, $removed) = Bugzilla::Util::diff_arrays(\@old, \@new);
        foreach my $id (@$added , @$removed) {
            # ValidateBugID is called without $field here so that it will
            # throw an error if any of the changed bugs are not visible.
            ValidateBugID($id);
201
            if (Param("strict_isolation")) {
202
                my $deltabug = new Bugzilla::Bug($id, $user->id);
203 204 205 206 207
                if (!$user->can_edit_product($deltabug->{'product_id'})) {
                    $vars->{'field'} = $field;
                    ThrowUserError("illegal_change_deps", $vars);
                }
            }
208
        }
209 210 211 212 213 214
        if ((@$added  || @$removed)
            && (!CheckCanChangeField($field, $bug->bug_id, 0, 1))) {
            $vars->{'privs'} = $PrivilegesRequired;
            $vars->{'field'} = $field;
            ThrowUserError("illegal_change", $vars);
        }
215 216 217 218 219
    } else {
        # Bugzilla does not support mass-change of dependencies so they
        # are not validated.  To prevent a URL-hacking risk, the dependencies
        # are deleted for mass-changes.
        $cgi->delete($field);
220 221 222
    }
}

223 224 225 226 227
# do a match on the fields if applicable

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

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

241 242 243 244
######################################################################
# End Data/Security Validation
######################################################################

245
print $cgi->header();
246
$vars->{'title_tag'} = "bug_processed";
247 248 249 250

# 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.
251
if (defined $cgi->param('id')) {
252 253 254
    my $delta_ts = $dbh->selectrow_array(
        q{SELECT delta_ts FROM bugs WHERE bug_id = ?},
        undef, $cgi->param('id'));
255
    
256
    if (defined $cgi->param('delta_ts') && $cgi->param('delta_ts') ne $delta_ts)
257
    {
258
        $vars->{'title_tag'} = "mid_air";
259 260
    }
}
261

262
# Set up the vars for nagiavtional <link> elements
263
my @bug_list;
264
if ($cgi->cookie("BUGLIST") && defined $cgi->param('id')) {
265
    @bug_list = split(/:/, $cgi->cookie("BUGLIST"));
266 267 268
    $vars->{'bug_list'} = \@bug_list;
}

269 270
GetVersionTable();

271 272 273 274
foreach my $field_name ('product', 'component', 'version') {
    defined($cgi->param($field_name))
      || ThrowCodeError('undefined_field', { field => $field_name });
}
275

276 277
# This function checks if there is a comment required for a specific
# function and tests, if the comment was given.
278
# If comments are required for functions is defined by params.
279
#
280
sub CheckonComment {
281 282 283 284 285 286 287 288 289
    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 ) {
290 291
        if (!defined $cgi->param('comment')
            || $cgi->param('comment') =~ /^\s*$/) {
292
            # No comment - sorry, action not allowed !
293
            ThrowUserError("comment_required");
294 295 296 297 298 299 300
        } else {
            $ret = 0;
        }
    }
    return( ! $ret ); # Return val has to be inverted
}

301 302 303 304 305
# 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.
306 307
my $oldproduct = '';
if (defined $cgi->param('id')) {
308 309 310 311
    $oldproduct = $dbh->selectrow_array(
        q{SELECT name FROM products INNER JOIN bugs
        ON products.id = bugs.product_id WHERE bug_id = ?},
        undef, $cgi->param('id'));
312
}
313 314 315 316

if (((defined $cgi->param('id') && $cgi->param('product') ne $oldproduct) 
     || (!$cgi->param('id')
         && $cgi->param('product') ne $cgi->param('dontchange')))
317 318
    && CheckonComment( "reassignbycomponent" ))
{
319
    # Check to make sure they actually have the right to change the product
320
    if (!CheckCanChangeField('product', scalar $cgi->param('id'), $oldproduct,
321 322 323
                              $cgi->param('product'))) {
        $vars->{'oldvalue'} = $oldproduct;
        $vars->{'newvalue'} = $cgi->param('product');
324
        $vars->{'field'} = 'product';
325
        $vars->{'privs'} = $PrivilegesRequired;
326
        ThrowUserError("illegal_change", $vars);
327
    }
328

329
    my $prod = $cgi->param('product');
330
    my $prod_obj = new Bugzilla::Product({name => $prod});
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
    trick_taint($prod);

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

348
    if ($check_can_enter) { $user->can_enter_product($prod, 1) }
349 350 351 352 353 354 355 356 357

    # 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.
    #
358
    my @version_names = map($_->name, @{$prod_obj->versions});
359
    my @component_names = map($_->name, @{$prod_obj->components});
360
    my $vok = lsearch(\@version_names, $cgi->param('version')) >= 0;
361
    my $cok = lsearch(\@component_names, $cgi->param('component')) >= 0;
362 363

    my $mok = 1;   # so it won't affect the 'if' statement if milestones aren't used
364
    my @milestone_names = ();
365
    if ( Param("usetargetmilestone") ) {
366 367 368
       defined($cgi->param('target_milestone'))
         || ThrowCodeError('undefined_field', { field => 'target_milestone' });

369 370
       @milestone_names = map($_->name, @{$prod_obj->milestones});
       $mok = lsearch(\@milestone_names, $cgi->param('target_milestone')) >= 0;
371 372
    }

373 374 375
    # 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.
376 377
    if (!$vok || !$cok || !$mok || (AnyDefaultGroups()
        && !defined $cgi->param('addtonewgroup'))) {
378
        
379
        if (!$vok || !$cok || !$mok) {
380
            $vars->{'verify_fields'} = 1;
381 382 383
            my %defaults;
            # We set the defaults to these fields to the old value,
            # if its a valid option, otherwise we use the default where
384
            # that's appropriate
385
            $vars->{'versions'} = \@version_names;
386
            if ($vok) {
387
                $defaults{'version'} = $cgi->param('version');
388
            }
389
            $vars->{'components'} = \@component_names;
390
            if ($cok) {
391
                $defaults{'component'} = $cgi->param('component');
392
            }
393 394
            if (Param("usetargetmilestone")) {
                $vars->{'use_target_milestone'} = 1;
395
                $vars->{'milestones'} = \@milestone_names;
396
                if ($mok) {
397
                    $defaults{'target_milestone'} = $cgi->param('target_milestone');
398
                } else {
399 400 401 402
                    $defaults{'target_milestone'} = $dbh->selectrow_array(
                        q{SELECT defaultmilestone FROM products 
                        WHERE name = ?}, undef, $prod);
;
403
                }
404
            }
405 406
            else {
                $vars->{'use_target_milestone'} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
407
            }
408
            $vars->{'defaults'} = \%defaults;
409
        }
410
        else {
411
            $vars->{'verify_fields'} = 0;
terry%netscape.com's avatar
terry%netscape.com committed
412
        }
413
        
414
        $vars->{'verify_bug_group'} = (AnyDefaultGroups() 
415
                                       && !defined $cgi->param('addtonewgroup'));
416
        
417
        $template->process("bug/process/verify-new-product.html.tmpl", $vars)
418
          || ThrowTemplateError($template->error());
419
        exit;
terry%netscape.com's avatar
terry%netscape.com committed
420 421 422 423
    }
}


424 425 426 427 428 429 430 431
# 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 $ownerid;
my $reporterid;
my $qacontactid;

432 433 434
################################################################################
# CheckCanChangeField() defines what users are allowed to change what bugs. You
# can add code here for site-specific policy changes, according to the 
435
# instructions given in the Bugzilla Guide and below. Note that you may also
436 437
# have to update the Bugzilla::Bug::user() function to give people access to the
# options that they are permitted to change.
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
#
# 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.
################################################################################
453
sub CheckCanChangeField {
454 455 456
    # START DO_NOT_CHANGE
    my ($field, $bugid, $oldvalue, $newvalue) = (@_);

457 458 459
    $oldvalue = defined($oldvalue) ? $oldvalue : '';
    $newvalue = defined($newvalue) ? $newvalue : '';

460
    # Return true if they haven't changed this field at all.
461 462
    if ($oldvalue eq $newvalue) {
        return 1;
463
    } elsif (trim($oldvalue) eq trim($newvalue)) {
464
        return 1;
465
    # numeric fields need to be compared using == 
466
    } elsif (($field eq "estimated_time" || $field eq "remaining_time")
467
             && $newvalue ne $cgi->param('dontchange')
468 469
             && $oldvalue == $newvalue)
    {
470
        return 1;
471
    }
472 473 474 475 476
    # END DO_NOT_CHANGE

    # Allow anyone to change comments.
    if ($field =~ /^longdesc/) {
        return 1;
477
    }
478 479 480

    # Ignore the assigned_to field if the bug is not being reassigned
    if ($field eq "assigned_to"
481 482
        && $cgi->param('knob') ne "reassignbycomponent"
        && $cgi->param('knob') ne "reassign")
483 484 485 486
    {
        return 1;
    }

487 488 489 490
    # 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.
491 492 493
    if ($UserInEditGroupSet < 0) {
        $UserInEditGroupSet = UserInGroup("editbugs");
    }
494 495 496 497 498
    
    if ($UserInCanConfirmGroupSet < 0) {
        $UserInCanConfirmGroupSet = UserInGroup("canconfirm");
    }
    # END DO_NOT_CHANGE
499 500 501 502 503 504

    # If the user isn't allowed to change a field, we must tell him who can.
    # We store the required permission set into the $PrivilegesRequired
    # variable which gets passed to the error template.
    #
    # $PrivilegesRequired = 0 : no privileges required;
505 506
    # $PrivilegesRequired = 1 : the reporter, assignee or an empowered user;
    # $PrivilegesRequired = 2 : the assignee or an empowered user;
507 508 509
    # $PrivilegesRequired = 3 : an empowered user.

    # Allow anyone with "editbugs" privs to change anything.
510 511 512
    if ($UserInEditGroupSet) {
        return 1;
    }
513 514 515 516

    # *Only* users with "canconfirm" privs can confirm bugs.
    if ($field eq "canconfirm"
        || ($field eq "bug_status"
517
            && $oldvalue eq 'UNCONFIRMED'
518
            && is_open_state($newvalue)))
519 520 521
    {
        $PrivilegesRequired = 3;
        return $UserInCanConfirmGroupSet;
522
    }
523

524 525
    # START DO_NOT_CHANGE
    # $reporterid, $ownerid and $qacontactid are caches of the results of
526
    # the call to find out the assignee, reporter and qacontact of the current bug.
527
    if ($lastbugid != $bugid) {
528 529 530
        ($reporterid, $ownerid, $qacontactid) = $dbh->selectrow_array(
            q{SELECT reporter, assigned_to, qa_contact FROM bugs
            WHERE bug_id = ? }, undef, $bugid);
531
        $lastbugid = $bugid;
532
    }
533 534
    # END DO_NOT_CHANGE

535
    # Allow the assignee to change anything else.
536
    if ($ownerid == $whoid) {
537
        return 1;
538
    }
539
    
540
    # Allow the QA contact to change anything else.
541
    if (Param('useqacontact') && $qacontactid && ($qacontactid == $whoid)) {
542 543
        return 1;
    }
544
    
545 546
    # At this point, the user is either the reporter or an
    # unprivileged user. We first check for fields the reporter
547
    # is not allowed to change.
548 549 550 551

    # The reporter may not:
    # - reassign bugs, unless the bugs are assigned to him;
    #   in that case we will have already returned 1 above
552
    #   when checking for the assignee of the bug.
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
    if ($field eq "assigned_to") {
        $PrivilegesRequired = 2;
        return 0;
    }
    # - change the QA contact
    if ($field eq "qa_contact") {
        $PrivilegesRequired = 2;
        return 0;
    }
    # - change the target milestone
    if ($field eq "target_milestone") {
        $PrivilegesRequired = 2;
        return 0;
    }
    # - change the priority (unless he could have set it originally)
    if ($field eq "priority"
        && !Param('letsubmitterchoosepriority'))
    {
571
        $PrivilegesRequired = 2;
572 573
        return 0;
    }
574

575 576
    # The reporter is allowed to change anything else.
    if ($reporterid == $whoid) {
577
        return 1;
578
    }
579

580 581
    # If we haven't returned by this point, then the user doesn't
    # have the necessary permissions to change this field.
582
    $PrivilegesRequired = 1;
583
    return 0;
584 585
}

586 587
# Confirm that the reporter of the current bug can access the bug we are duping to.
sub DuplicateUserConfirm {
588
    # if we've already been through here, then exit
589
    if (defined $cgi->param('confirm_add_duplicate')) {
590 591 592
        return;
    }

593 594 595 596
    # Remember that we validated both these ids earlier, so we know
    # they are both valid bug ids
    my $dupe = $cgi->param('id');
    my $original = $cgi->param('dup_id');
597
    
598 599
    my $reporter = $dbh->selectrow_array(
        q{SELECT reporter FROM bugs WHERE bug_id = ?}, undef, $dupe);
600
    my $rep_user = Bugzilla::User->new($reporter);
601

602
    if ($rep_user->can_see_bug($original)) {
603
        $cgi->param('confirm_add_duplicate', '1');
604 605
        return;
    }
606

607 608 609
    $vars->{'cclist_accessible'} = $dbh->selectrow_array(
        q{SELECT cclist_accessible FROM bugs WHERE bug_id = ?},
        undef, $original);
610
    
611 612 613
    # 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.
614
    
615 616 617 618 619
    $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).
620
    print Bugzilla->cgi->header();
621
    $template->process("bug/process/confirm-duplicate.html.tmpl", $vars)
622
      || ThrowTemplateError($template->error());
623
    exit;
624
}
625

626
if (defined $cgi->param('id')) {
627 628 629 630 631 632
    # 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...)
633
    my $prod_obj = Bugzilla::Product::check_product($cgi->param('product'));
634
    check_field('component', scalar $cgi->param('component'), 
635
                [map($_->name, @{$prod_obj->components})]);
636
    check_field('version', scalar $cgi->param('version'),
637
                [map($_->name, @{$prod_obj->versions})]);
638
    if ( Param("usetargetmilestone") ) {
639
        check_field('target_milestone', scalar $cgi->param('target_milestone'), 
640
                    [map($_->name, @{$prod_obj->milestones})]);
641 642 643 644 645 646 647 648 649 650 651
    }
    check_field('rep_platform', scalar $cgi->param('rep_platform'), \@::legal_platform);
    check_field('op_sys',       scalar $cgi->param('op_sys'),       \@::legal_opsys);
    check_field('priority',     scalar $cgi->param('priority'),     \@::legal_priority);
    check_field('bug_severity', scalar $cgi->param('bug_severity'), \@::legal_severity);

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

654
    if (trim($cgi->param('short_desc')) eq "") {
655
        ThrowUserError("require_summary");
656
    }
terry%netscape.com's avatar
terry%netscape.com committed
657 658
}

659 660 661 662 663
my $action = trim($cgi->param('action') || '');

if ($action eq Param('move-button-text')) {
    Param('move-enabled') || ThrowUserError("move_bugs_disabled");

664 665
    $user->is_mover || ThrowUserError("auth_failure", {action => 'move',
                                                       object => 'bugs'});
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680

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

    my $comment = "";
    if (defined $cgi->param('comment') && $cgi->param('comment') !~ /^\s*$/) {
        $comment = $cgi->param('comment') . "\n\n";
    }
    $comment .= "Bug moved to " . Param('move-to-url') . ".\n\n";
681
    $comment .= "If the move succeeded, " . $user->login . " will receive a mail\n";
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
    $comment .= "containing the number of the new bug in the other database.\n";
    $comment .= "If all went well,  please mark this bug verified, and paste\n";
    $comment .= "in a link to the new bug. Otherwise, reopen this bug.\n";

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

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

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

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

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

    # Now send emails.
    foreach my $id (@idlist) {
718
        $vars->{'mailrecipients'} = { 'changer' => $user->login };
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
        $vars->{'id'} = $id;
        $vars->{'type'} = "move";

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

735
    my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc',
736
                     'attachment', 'attachmentdata');
737 738 739 740 741 742 743 744 745 746 747
    my %displayfields;
    foreach (@fieldlist) {
        $displayfields{$_} = 1;
    }

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

    $msg .= "\n";
748
    MessageToMTA($msg);
749 750 751 752 753 754 755

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


759
$::query = "UPDATE bugs SET";
760
$::comma = "";
761
my @values;
762 763
umask(0);

764 765
sub _remove_remaining_time {
    if (UserInGroup(Param('timetrackinggroup'))) {
766 767
        if ( defined $cgi->param('remaining_time') 
             && $cgi->param('remaining_time') > 0 )
768
        {
769
            $cgi->param('remaining_time', 0);
770 771 772 773 774 775 776 777 778
            $vars->{'message'} = "remaining_time_zeroed";
        }
    }
    else {
        DoComma();
        $::query .= "remaining_time = 0";
    }
}

779 780 781
sub DoComma {
    $::query .= "$::comma\n    ";
    $::comma = ",";
terry%netscape.com's avatar
terry%netscape.com committed
782 783
}

784 785 786
# $everconfirmed is used by ChangeStatus() to determine whether we are
# confirming the bug or not.
my $everconfirmed;
787
sub DoConfirm {
788
    if (CheckCanChangeField("canconfirm", scalar $cgi->param('id'), 0, 1)) {
789 790
        DoComma();
        $::query .= "everconfirmed = 1";
791
        $everconfirmed = 1;
792 793 794
    }
}

795 796
sub ChangeStatus {
    my ($str) = (@_);
797 798
    if (!$cgi->param('dontchange')
        || $str ne $cgi->param('dontchange')) {
799
        DoComma();
800
        if ($cgi->param('knob') eq 'reopen') {
801 802
            # When reopening, we need to check whether the bug was ever
            # confirmed or not
803
            $::query .= "bug_status = CASE WHEN everconfirmed = 1 THEN " .
804
                        $dbh->quote($str) . " ELSE 'UNCONFIRMED' END";
805
        } elsif (is_open_state($str)) {
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
            # 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

831
            my @open_state = map($dbh->quote($_), BUG_STATE_OPEN);
832
            my $open_state = join(", ", @open_state);
833 834 835

            # If we are changing everconfirmed to 1, we have to take this change
            # into account and the new bug status is given by $str.
836
            my $cond = $dbh->quote($str);
837 838 839 840 841 842
            # If we are not setting everconfirmed, the new bug status depends on
            # the actual value of everconfirmed, which is bug-specific.
            unless ($everconfirmed) {
                $cond = "(CASE WHEN everconfirmed = 1 THEN " . $cond .
                        " ELSE 'UNCONFIRMED' END)";
            }
843
            $::query .= "bug_status = CASE WHEN bug_status IN($open_state) THEN " .
844
                                      $cond . " ELSE bug_status END";
845
        } else {
846 847
            $::query .= "bug_status = ?";
            push(@values, $str);
848
        }
849 850 851
        # If bugs are reassigned and their status is "UNCONFIRMED", they
        # should keep this status instead of "NEW" as suggested here.
        # This point is checked for each bug later in the code.
852
        $cgi->param('bug_status', $str);
terry%netscape.com's avatar
terry%netscape.com committed
853 854 855
    }
}

856 857
sub ChangeResolution {
    my ($str) = (@_);
858 859
    if (!$cgi->param('dontchange')
        || $str ne $cgi->param('dontchange'))
860
    {
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
        # Make sure the user is allowed to change the resolution.
        # If the user is changing several bugs at once using the UI,
        # then he has enough privs to do so. In the case he is hacking
        # the URL, we don't care if he reads --UNKNOWN-- as a resolution
        # in the error message.
        my $old_resolution = '-- UNKNOWN --';
        my $bug_id = $cgi->param('id');
        if ($bug_id) {
            $old_resolution =
                $dbh->selectrow_array('SELECT resolution FROM bugs WHERE bug_id = ?',
                                       undef, $bug_id);
        }
        unless (CheckCanChangeField('resolution', $bug_id, $old_resolution, $str)) {
            $vars->{'oldvalue'} = $old_resolution;
            $vars->{'newvalue'} = $str;
            $vars->{'field'} = 'resolution';
            $vars->{'privs'} = $PrivilegesRequired;
            ThrowUserError("illegal_change", $vars);
        }

881
        DoComma();
882 883 884
        $::query .= "resolution = ?";
        trick_taint($str);
        push(@values, $str);
885 886
        # We define this variable here so that customized installations
        # may set rules based on the resolution in CheckCanChangeField.
887
        $cgi->param('resolution', $str);
terry%netscape.com's avatar
terry%netscape.com committed
888 889 890
    }
}

891 892 893 894
# 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.
895 896
# If the form element isn't present, or the user isn't in the group, leave
# it as-is
897

898 899 900
my @groupAdd = ();
my @groupDel = ();

901 902 903 904 905
my $groups = $dbh->selectall_arrayref(
    qq{SELECT groups.id, isactive FROM groups
        WHERE id IN($grouplist) AND isbuggroup = 1});
foreach my $group (@$groups) {
    my ($b, $isactive) = @$group;
906 907 908 909 910 911
    # 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
912 913
    if (defined $cgi->param('id') || defined $cgi->param("bit-$b")) {
        if (!$cgi->param("bit-$b")) {
914
            push(@groupDel, $b);
915
        } elsif ($cgi->param("bit-$b") == 1 && $isactive) {
916
            push(@groupAdd, $b);
917 918
        }
    }
919 920
}

921 922
foreach my $field ("rep_platform", "priority", "bug_severity",
                   "bug_file_loc", "short_desc", "version", "op_sys",
923
                   "target_milestone", "status_whiteboard") {
924 925 926
    if (defined $cgi->param($field)) {
        if (!$cgi->param('dontchange')
            || $cgi->param($field) ne $cgi->param('dontchange')) {
927
            DoComma();
928 929 930 931
            $::query .= "$field = ?";
            my $value = trim($cgi->param($field));
            trick_taint($value);
            push(@values, $value);
terry%netscape.com's avatar
terry%netscape.com committed
932 933 934 935
        }
    }
}

936 937 938 939 940 941 942
# Add custom fields data to the query that will update the database.
foreach my $field (Bugzilla->custom_field_names) {
    if (defined $cgi->param($field)
        && (!$cgi->param('dontchange')
            || $cgi->param($field) ne $cgi->param('dontchange')))
    {
        DoComma();
943 944 945 946
        $::query .= "$field = ?";
        my $value = $cgi->param($field);
        trick_taint($value);
        push(@values, $value);
947 948 949
    }
}

950
my $product;
951 952
my $prod_changed;
my @newprod_ids;
953
if ($cgi->param('product') ne $cgi->param('dontchange')) {
954 955
    $product = Bugzilla::Product::check_product(scalar $cgi->param('product'));

956
    DoComma();
957
    $::query .= "product_id = ?";
958 959
    push(@values, $product->id);
    @newprod_ids = ($product->id);
960
    $prod_changed = 1;
961
} else {
962 963 964 965 966 967
    @newprod_ids = @{$dbh->selectcol_arrayref("SELECT DISTINCT product_id
                                               FROM bugs 
                                               WHERE bug_id IN (" .
                                                   join(',', @idlist) . 
                                               ")")};
    if (scalar(@newprod_ids) == 1) {
968
        $product = new Bugzilla::Product($newprod_ids[0]);
969
    }
970 971
}

972
my $component;
973
if ($cgi->param('component') ne $cgi->param('dontchange')) {
974
    if (scalar(@newprod_ids) > 1) {
975
        ThrowUserError("no_component_change_for_multiple_products");
976
    }
977 978 979 980 981
    $component =
        Bugzilla::Component::check_component($product, scalar $cgi->param('component'));

    # This parameter is required later when checking fields the user can change.
    $cgi->param('component_id', $component->id);
982
    DoComma();
983
    $::query .= "component_id = ?";
984
    push(@values, $component->id);
985 986
}

987 988
# If this installation uses bug aliases, and the user is changing the alias,
# add this change to the query.
989 990
if (Param("usebugaliases") && defined $cgi->param('alias')) {
    my $alias = trim($cgi->param('alias'));
991 992 993 994 995 996 997 998 999 1000
    
    # Since aliases are unique (like bug numbers), they can only be changed
    # for one bug at a time, so ignore the alias change unless only a single
    # bug is being changed.
    if (scalar(@idlist) == 1) {
        # Add the alias change to the query.  If the field contains the blank 
        # value, make the field be NULL to indicate that the bug has no alias.
        # Otherwise, if the field contains a value, update the record 
        # with that value.
        DoComma();
1001 1002
        if ($alias ne "") {
            ValidateBugAlias($alias, $idlist[0]);
1003 1004
            $::query .= "alias = ?";
            push(@values, $alias);
1005
        } else {
1006
            $::query .= "alias = NULL";
1007
        }
1008 1009
    }
}
1010

1011 1012
# 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
1013
# allowed the user to set whether or not the reporter
1014 1015
# and cc list can see the bug even if they are not members of all groups 
# to which the bug is restricted.
1016
if (defined $cgi->param('id')) {
1017 1018 1019
    my ($havegroup) = $dbh->selectrow_array(
        q{SELECT group_id FROM bug_group_map WHERE bug_id = ?},
        undef, $cgi->param('id'));
1020
    if ( $havegroup ) {
1021
        DoComma();
1022 1023
        $cgi->param('reporter_accessible',
                    $cgi->param('reporter_accessible') ? '1' : '0');
1024 1025
        $::query .= "reporter_accessible = ?";
        push(@values, $cgi->param('reporter_accessible'));
1026 1027

        DoComma();
1028 1029
        $cgi->param('cclist_accessible',
                    $cgi->param('cclist_accessible') ? '1' : '0');
1030 1031
        $::query .= "cclist_accessible = ?";
        push(@values, $cgi->param('cclist_accessible'));
1032 1033 1034
    }
}

1035
if (defined $cgi->param('id') &&
1036
    (Param("insidergroup") && UserInGroup(Param("insidergroup")))) {
1037

1038 1039 1040
    my $sth = $dbh->prepare('UPDATE longdescs SET isprivate = ?
                             WHERE bug_id = ? AND bug_when = ?');

1041
    foreach my $field ($cgi->param()) {
1042 1043
        if ($field =~ /when-([0-9]+)/) {
            my $sequence = $1;
1044 1045 1046
            my $private = $cgi->param("isprivate-$sequence") ? 1 : 0 ;
            if ($private != $cgi->param("oisprivate-$sequence")) {
                my $field_data = $cgi->param("$field");
1047 1048 1049
                # Make sure a valid date is given.
                $field_data = format_time($field_data, '%Y-%m-%d %T');
                $sth->execute($private, $cgi->param('id'), $field_data);
1050 1051 1052 1053 1054
            }
        }

    }
}
1055

1056
my $duplicate = 0;
1057

1058 1059 1060 1061
# 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);
1062 1063 1064 1065
if (defined $cgi->param('newcc')
    || defined $cgi->param('addselfcc')
    || defined $cgi->param('removecc')
    || defined $cgi->param('masscc')) {
1066 1067 1068
    # 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) = "";
1069 1070 1071 1072 1073
    if (defined $cgi->param('masscc')) {
        if ($cgi->param('ccaction') eq 'add') {
            $cc_add = join(' ',$cgi->param('masscc'));
        } elsif ($cgi->param('ccaction') eq 'remove') {
            $cc_remove = join(' ',$cgi->param('masscc'));
1074 1075
        }
    } else {
1076
        $cc_add = join(' ',$cgi->param('newcc'));
1077 1078
        # We came from bug_form which uses a select box to determine what cc's
        # need to be removed...
1079 1080
        if (defined $cgi->param('removecc') && $cgi->param('cc')) {
            $cc_remove = join (",", $cgi->param('cc'));
1081 1082 1083 1084
        }
    }

    if ($cc_add) {
1085 1086
        $cc_add =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_add) ) {
1087
            my $pid = login_to_id($person, THROW_ERROR);
1088 1089 1090
            $cc_add{$pid} = $person;
        }
    }
1091
    if ($cgi->param('addselfcc')) {
1092 1093
        $cc_add{$whoid} = $user->login;
    }
1094
    if ($cc_remove) {
1095 1096
        $cc_remove =~ s/[\s,]+/ /g; # Change all delimiters to a single space
        foreach my $person ( split(" ", $cc_remove) ) {
1097
            my $pid = login_to_id($person, THROW_ERROR);
1098 1099 1100 1101 1102
            $cc_remove{$pid} = $person;
        }
    }
}

1103 1104
# Store the new assignee and QA contact IDs (if any). This is the
# only way to keep these informations when bugs are reassigned by
1105
# component as $cgi->param('assigned_to') and $cgi->param('qa_contact')
1106
# are not the right fields to look at.
1107 1108 1109
# If the assignee or qacontact is changed, the new one is checked when
# changed information is validated.  If not, then the unchanged assignee
# or qacontact may have to be validated later.
1110 1111 1112

my $assignee;
my $qacontact;
1113 1114 1115 1116
my $qacontact_checked = 0;
my $assignee_checked = 0;

my %usercache = ();
1117

1118 1119
if (defined $cgi->param('qa_contact')
    && $cgi->param('knob') ne "reassignbycomponent")
1120
{
1121
    my $name = trim($cgi->param('qa_contact'));
1122
    # The QA contact cannot be deleted from show_bug.cgi for a single bug!
1123
    if ($name ne $cgi->param('dontchange')) {
1124
        $qacontact = login_to_id($name, THROW_ERROR) if ($name ne "");
1125 1126 1127 1128 1129
        if ($qacontact && Param("strict_isolation")) {
                $usercache{$qacontact} ||= Bugzilla::User->new($qacontact);
                my $qa_user = $usercache{$qacontact};
                foreach my $product_id (@newprod_ids) {
                    if (!$qa_user->can_edit_product($product_id)) {
1130
                        my $product_name = Bugzilla::Product->new($product_id)->name;
1131 1132 1133 1134 1135 1136 1137 1138
                        ThrowUserError('invalid_user_group',
                                          {'users'   => $qa_user->login,
                                           'product' => $product_name,
                                           'bug_id' => (scalar(@idlist) > 1)
                                                         ? undef : $idlist[0]
                                          });
                    }
                }
1139
        }
1140
        $qacontact_checked = 1;
1141
        DoComma();
1142
        if($qacontact) {
1143 1144
            $::query .= "qa_contact = ?";
            push(@values, $qacontact);
1145 1146 1147 1148
        }
        else {
            $::query .= "qa_contact = NULL";
        }
1149 1150
    }
}
1151

1152
SWITCH: for ($cgi->param('knob')) {
1153 1154 1155
    /^none$/ && do {
        last SWITCH;
    };
1156 1157 1158 1159 1160
    /^confirm$/ && CheckonComment( "confirm" ) && do {
        DoConfirm();
        ChangeStatus('NEW');
        last SWITCH;
    };
1161
    /^accept$/ && CheckonComment( "accept" ) && do {
1162
        DoConfirm();
1163
        ChangeStatus('ASSIGNED');
1164 1165
        if (Param("usetargetmilestone") && Param("musthavemilestoneonaccept")) {
            $requiremilestone = 1;
1166
        }
1167 1168
        last SWITCH;
    };
1169
    /^clearresolution$/ && CheckonComment( "clearresolution" ) && do {
1170 1171 1172
        ChangeResolution('');
        last SWITCH;
    };
1173
    /^(resolve|change_resolution)$/ && CheckonComment( "resolve" ) && do {
1174
        # Check here, because its the only place we require the resolution
1175 1176
        check_field('resolution', scalar $cgi->param('resolution'),
                    \@::settable_resolution);
1177

1178
        # don't resolve as fixed while still unresolved blocking bugs
1179
        if (Param("noresolveonopenblockers")
1180
            && $cgi->param('resolution') eq 'FIXED')
1181
        {
1182
            my @dependencies = Bugzilla::Bug::CountOpenDependencies(@idlist);
1183 1184
            if (scalar @dependencies > 0) {
                ThrowUserError("still_unresolved_bugs",
1185 1186
                               { dependencies     => \@dependencies,
                                 dependency_count => scalar @dependencies });
1187
            }
1188
        }
1189

1190 1191 1192 1193 1194 1195 1196
        if ($cgi->param('knob') eq 'resolve') {
            # RESOLVED bugs should have no time remaining;
            # more time can be added for the VERIFY step, if needed.
            _remove_remaining_time();

            ChangeStatus('RESOLVED');
        }
1197

1198
        ChangeResolution($cgi->param('resolution'));
1199 1200
        last SWITCH;
    };
1201
    /^reassign$/ && CheckonComment( "reassign" ) && do {
1202
        if ($cgi->param('andconfirm')) {
1203 1204
            DoConfirm();
        }
1205 1206
        ChangeStatus('NEW');
        DoComma();
1207 1208
        if (defined $cgi->param('assigned_to')
            && trim($cgi->param('assigned_to')) ne "") { 
1209
            $assignee = login_to_id(trim($cgi->param('assigned_to')), THROW_ERROR);
1210
            if (Param("strict_isolation")) {
1211 1212 1213 1214
                $usercache{$assignee} ||= Bugzilla::User->new($assignee);
                my $assign_user = $usercache{$assignee};
                foreach my $product_id (@newprod_ids) {
                    if (!$assign_user->can_edit_product($product_id)) {
1215
                        my $product_name = Bugzilla::Product->new($product_id)->name;
1216 1217 1218 1219 1220 1221 1222 1223
                        ThrowUserError('invalid_user_group',
                                          {'users'   => $assign_user->login,
                                           'product' => $product_name,
                                           'bug_id' => (scalar(@idlist) > 1)
                                                         ? undef : $idlist[0]
                                          });
                    }
                }
1224
            }
1225
        } else {
1226
            ThrowUserError("reassign_to_empty");
1227
        }
1228 1229
        $::query .= "assigned_to = ?";
        push(@values, $assignee);
1230
        $assignee_checked = 1;
1231 1232
        last SWITCH;
    };
1233
    /^reassignbycomponent$/  && CheckonComment( "reassignbycomponent" ) && do {
1234
        if ($cgi->param('compconfirm')) {
1235 1236
            DoConfirm();
        }
1237 1238
        ChangeStatus('NEW');
        last SWITCH;
1239
    };
1240
    /^reopen$/  && CheckonComment( "reopen" ) && do {
1241
        ChangeStatus('REOPENED');
1242
        ChangeResolution('');
1243 1244
        last SWITCH;
    };
1245
    /^verify$/ && CheckonComment( "verify" ) && do {
1246 1247 1248
        ChangeStatus('VERIFIED');
        last SWITCH;
    };
1249
    /^close$/ && CheckonComment( "close" ) && do {
1250 1251 1252
        # CLOSED bugs should have no time remaining.
        _remove_remaining_time();

1253 1254 1255
        ChangeStatus('CLOSED');
        last SWITCH;
    };
1256
    /^duplicate$/ && CheckonComment( "duplicate" ) && do {
1257 1258 1259 1260 1261 1262
        # You cannot mark bugs as duplicates when changing
        # several bugs at once.
        unless (defined $cgi->param('id')) {
            ThrowUserError('dupe_not_allowed');
        }

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

1267 1268 1269
        $duplicate = $cgi->param('dup_id');
        ValidateBugID($duplicate, 'dup_id');
        $cgi->param('dup_id', $duplicate);
1270

1271 1272 1273
        # Make sure a loop isn't created when marking this bug
        # as duplicate.
        my %dupes;
1274
        my $dupe_of = $duplicate;
1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296
        my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates
                                 WHERE dupe = ?');

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

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

1297 1298 1299
        # DUPLICATE bugs should have no time remaining.
        _remove_remaining_time();

1300 1301
        ChangeStatus('RESOLVED');
        ChangeResolution('DUPLICATE');
1302 1303
        my $comment = $cgi->param('comment');
        $comment .= "\n\n*** This bug has been marked " .
1304
                    "as a duplicate of bug $duplicate ***";
1305
        $cgi->param('comment', $comment);
1306 1307
        last SWITCH;
    };
1308

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

1312 1313 1314
my @keywordlist;
my %keywordseen;

1315 1316
if (defined $cgi->param('keywords')) {
    foreach my $keyword (split(/[\s,]+/, $cgi->param('keywords'))) {
1317 1318 1319
        if ($keyword eq '') {
            next;
        }
1320 1321
        my $keyword_obj = new Bugzilla::Keyword({name => $keyword});
        if (!$keyword_obj) {
1322 1323
            ThrowUserError("unknown_keyword",
                           { keyword => $keyword });
1324
        }
1325 1326 1327
        if (!$keywordseen{$keyword_obj->id}) {
            push(@keywordlist, $keyword_obj->id);
            $keywordseen{$keyword_obj->id} = 1;
1328 1329 1330 1331
        }
    }
}

1332
my $keywordaction = $cgi->param('keywordaction') || "makeexact";
1333 1334 1335
if (!grep($keywordaction eq $_, qw(add delete makeexact))) {
    $keywordaction = "makeexact";
}
1336

1337
if ($::comma eq ""
1338
    && (! @groupAdd) && (! @groupDel)
1339 1340
    && (!Bugzilla::Keyword::keyword_count() 
        || (0 == @keywordlist && $keywordaction ne "makeexact"))
1341
    && defined $cgi->param('masscc') && ! $cgi->param('masscc')
1342
    ) {
1343
    if (!defined $cgi->param('comment') || $cgi->param('comment') =~ /^\s*$/) {
1344
        ThrowUserError("bugs_not_changed");
terry%netscape.com's avatar
terry%netscape.com committed
1345 1346 1347
    }
}

1348 1349 1350
# Process data for Time Tracking fields
if (UserInGroup(Param('timetrackinggroup'))) {
    foreach my $field ("estimated_time", "remaining_time") {
1351 1352 1353
        if (defined $cgi->param($field)) {
            my $er_time = trim($cgi->param($field));
            if ($er_time ne $cgi->param('dontchange')) {
1354
                DoComma();
1355 1356 1357
                $::query .= "$field = ?";
                trick_taint($er_time);
                push(@values, $er_time);
1358 1359 1360 1361
            }
        }
    }

1362
    if (defined $cgi->param('deadline')) {
1363
        DoComma();
1364
        if ($cgi->param('deadline')) {
1365 1366 1367
            validate_date($cgi->param('deadline'))
              || ThrowUserError('illegal_date', {date => $cgi->param('deadline'),
                                                 format => 'YYYY-MM-DD'});
1368 1369 1370 1371
            $::query .= "deadline = ?";
            my $deadline = $cgi->param('deadline');
            trick_taint($deadline);
            push(@values, $deadline);
1372
        } else {
1373
            $::query .= "deadline = NULL";
1374 1375 1376 1377
        }
    }
}

1378
my $basequery = $::query;
1379
my $delta_ts;
terry%netscape.com's avatar
terry%netscape.com committed
1380

1381

1382 1383
sub SnapShotBug {
    my ($id) = (@_);
1384
    my @row = $dbh->selectrow_array(q{SELECT delta_ts, } .
1385
                join(',', @editable_bug_fields).q{ FROM bugs WHERE bug_id = ?},
1386
                undef, $id);
1387
    $delta_ts = shift @row;
1388

1389
    return @row;
terry%netscape.com's avatar
terry%netscape.com committed
1390 1391 1392
}


1393 1394
sub SnapShotDeps {
    my ($i, $target, $me) = (@_);
1395 1396 1397 1398
    my $list = $dbh->selectcol_arrayref(qq{SELECT $target FROM dependencies
                                        WHERE $me = ? ORDER BY $target},
                                        undef, $i);
    return join(',', @$list);
1399 1400 1401 1402
}


my $timestamp;
1403
my $bug_changed;
1404 1405

sub LogDependencyActivity {
1406
    my ($i, $oldstr, $target, $me, $timestamp) = (@_);
1407 1408
    my $newstr = SnapShotDeps($i, $target, $me);
    if ($oldstr ne $newstr) {
1409
        # Figure out what's really different...
1410
        my ($removed, $added) = diff_strings($oldstr, $newstr);
1411
        LogActivityEntry($i,$target,$removed,$added,$whoid,$timestamp);
1412
        # update timestamp on target bug so midairs will be triggered
1413 1414
        $dbh->do(q{UPDATE bugs SET delta_ts = ? WHERE bug_id = ?},
                 undef, $timestamp, $i);
1415
        $bug_changed = 1;
1416 1417 1418 1419 1420
        return 1;
    }
    return 0;
}

1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446
if (Param("strict_isolation")) {
    my @blocked_cc = ();
    foreach my $pid (keys %cc_add) {
        $usercache{$pid} ||= Bugzilla::User->new($pid);
        my $cc_user = $usercache{$pid};
        foreach my $product_id (@newprod_ids) {
            if (!$cc_user->can_edit_product($product_id)) {
                push (@blocked_cc, $cc_user->login);
                last;
            }
        }
    }
    if (scalar(@blocked_cc)) {
        ThrowUserError("invalid_user_group", 
            {'users' => \@blocked_cc,
             'bug_id' => (scalar(@idlist) > 1) ? undef : $idlist[0]});
    }
}

if ($prod_changed && Param("strict_isolation")) {
    my $sth_cc = $dbh->prepare("SELECT who
                                FROM cc
                                WHERE bug_id = ?");
    my $sth_bug = $dbh->prepare("SELECT assigned_to, qa_contact
                                 FROM bugs
                                 WHERE bug_id = ?");
1447

1448 1449 1450 1451 1452 1453
    foreach my $id (@idlist) {
        $sth_cc->execute($id);
        my @blocked_cc = ();
        while (my ($pid) = $sth_cc->fetchrow_array) {
            $usercache{$pid} ||= Bugzilla::User->new($pid);
            my $cc_user = $usercache{$pid};
1454
            if (!$cc_user->can_edit_product($product->id)) {
1455 1456 1457 1458 1459 1460 1461
                push (@blocked_cc, $cc_user->login);
            }
        }
        if (scalar(@blocked_cc)) {
            ThrowUserError('invalid_user_group',
                              {'users'   => \@blocked_cc,
                               'bug_id' => $id,
1462
                               'product' => $product->name});
1463 1464 1465 1466 1467 1468
        }
        $sth_bug->execute($id);
        my ($assignee, $qacontact) = $sth_bug->fetchrow_array;
        if (!$assignee_checked) {
            $usercache{$assignee} ||= Bugzilla::User->new($assignee);
            my $assign_user = $usercache{$assignee};
1469
            if (!$assign_user->can_edit_product($product->id)) {
1470 1471 1472
                    ThrowUserError('invalid_user_group',
                                      {'users'   => $assign_user->login,
                                       'bug_id' => $id,
1473
                                       'product' => $product->name});
1474 1475 1476 1477 1478
            }
        }
        if (!$qacontact_checked && $qacontact) {
            $usercache{$qacontact} ||= Bugzilla::User->new($qacontact);
            my $qa_user = $usercache{$qacontact};
1479
            if (!$qa_user->can_edit_product($product->id)) {
1480 1481 1482
                    ThrowUserError('invalid_user_group',
                                      {'users'   => $qa_user->login,
                                       'bug_id' => $id,
1483
                                       'product' => $product->name});
1484 1485 1486 1487 1488 1489
            }
        }
    }
}


1490 1491 1492
# This loop iterates once for each bug to be processed (i.e. all the
# bugs selected when this script is called with multiple bugs selected
# from buglist.cgi, or just the one bug when called from
1493 1494
# show_bug.cgi).
#
1495
foreach my $id (@idlist) {
1496
    my $query = $basequery;
1497
    my @bug_values = @values;
1498
    my $old_bug_obj = new Bugzilla::Bug($id, $whoid);
1499 1500 1501

    if ($cgi->param('knob') eq 'reassignbycomponent') {
        # We have to check whether the bug is moved to another product
1502
        # and/or component before reassigning. If $component is defined,
1503
        # use it; else use the product/component the bug is already in.
1504
        my $new_comp_id = $component ? $component->id : $old_bug_obj->{'component_id'};
1505 1506 1507 1508
        $assignee = $dbh->selectrow_array('SELECT initialowner
                                           FROM components
                                           WHERE components.id = ?',
                                           undef, $new_comp_id);
1509
        $query .= ", assigned_to = ?";
1510
        push(@bug_values, $assignee);
1511 1512 1513 1514 1515 1516
        if (Param("useqacontact")) {
            $qacontact = $dbh->selectrow_array('SELECT initialqacontact
                                                FROM components
                                                WHERE components.id = ?',
                                                undef, $new_comp_id);
            if ($qacontact) {
1517
                $query .= ", qa_contact = ?";
1518
                push(@bug_values, $qacontact);
1519 1520 1521 1522 1523 1524 1525
            }
            else {
                $query .= ", qa_contact = NULL";
            }
        }
    }

1526
    my %dependencychanged;
1527
    $bug_changed = 0;
1528 1529
    my $write = "WRITE";        # Might want to make a param to control
                                # whether we do LOW_PRIORITY ...
1530
    $dbh->bz_lock_tables("bugs $write", "bugs_activity $write", "cc $write",
1531
            "profiles READ", "dependencies $write", "votes $write",
1532
            "products READ", "components READ", "milestones READ",
1533
            "keywords $write", "longdescs $write", "fielddefs READ",
1534
            "bug_group_map $write", "flags $write", "duplicates $write",
1535
            "user_group_map READ", "group_group_map READ", "flagtypes READ",
1536 1537 1538 1539
            "flaginclusions AS i READ", "flagexclusions AS e READ",
            "keyworddefs READ", "groups READ", "attachments READ",
            "group_control_map AS oldcontrolmap READ",
            "group_control_map AS newcontrolmap READ",
1540
            "group_control_map READ", "email_setting READ", "classifications READ");
1541

1542
    # It may sound crazy to set %formhash for each bug as $cgi->param()
1543 1544
    # will not change, but %formhash is modified below and we prefer
    # to set it again.
1545
    my $i = 0;
1546 1547 1548
    my @oldvalues = SnapShotBug($id);
    my %oldhash;
    my %formhash;
1549
    foreach my $col (@editable_bug_fields) {
1550
        # Consider NULL db entries to be equivalent to the empty string
1551 1552 1553 1554 1555 1556 1557
        $oldvalues[$i] = defined($oldvalues[$i]) ? $oldvalues[$i] : '';
        # Convert the deadline taken from the DB into the YYYY-MM-DD format
        # for consistency with the deadline provided by the user, if any.
        # Else CheckCanChangeField() would see them as different in all cases.
        if ($col eq 'deadline') {
            $oldvalues[$i] = format_time($oldvalues[$i], "%Y-%m-%d");
        }
1558
        $oldhash{$col} = $oldvalues[$i];
1559
        $formhash{$col} = $cgi->param($col) if defined $cgi->param($col);
1560 1561 1562 1563 1564 1565 1566 1567
        $i++;
    }
    # If the user is reassigning bugs, we need to:
    # - convert $newhash{'assigned_to'} and $newhash{'qa_contact'}
    #   email addresses into their corresponding IDs;
    # - update $newhash{'bug_status'} to its real state if the bug
    #   is in the unconfirmed state.
    $formhash{'qa_contact'} = $qacontact if Param('useqacontact');
1568 1569
    if ($cgi->param('knob') eq 'reassignbycomponent'
        || $cgi->param('knob') eq 'reassign') {
1570
        $formhash{'assigned_to'} = $assignee;
1571
        if ($oldhash{'bug_status'} eq 'UNCONFIRMED') {
1572 1573 1574
            $formhash{'bug_status'} = $oldhash{'bug_status'};
        }
    }
1575
    foreach my $col (@editable_bug_fields) {
1576 1577 1578
        # The 'resolution' field is checked by ChangeResolution(),
        # i.e. only if we effectively use it.
        next if ($col eq 'resolution');
1579 1580 1581 1582 1583 1584
        if (exists $formhash{$col}
            && !CheckCanChangeField($col, $id, $oldhash{$col}, $formhash{$col}))
        {
            my $vars;
            if ($col eq 'component_id') {
                # Display the component name
1585
                $vars->{'oldvalue'} = $old_bug_obj->component;
1586
                $vars->{'newvalue'} = $cgi->param('component');
1587 1588 1589 1590 1591 1592 1593 1594 1595 1596
                $vars->{'field'} = 'component';
            } elsif ($col eq 'assigned_to' || $col eq 'qa_contact') {
                # Display the assignee or QA contact email address
                $vars->{'oldvalue'} = DBID_to_name($oldhash{$col});
                $vars->{'newvalue'} = DBID_to_name($formhash{$col});
                $vars->{'field'} = $col;
            } else {
                $vars->{'oldvalue'} = $oldhash{$col};
                $vars->{'newvalue'} = $formhash{$col};
                $vars->{'field'} = $col;
1597
            }
1598
            $vars->{'privs'} = $PrivilegesRequired;
1599
            ThrowUserError("illegal_change", $vars);
1600 1601
        }
    }
1602
    
1603 1604 1605 1606 1607 1608
    # When editing multiple bugs, users can specify a list of keywords to delete
    # from bugs.  If the list matches the current set of keywords on those bugs,
    # CheckCanChangeField above will fail to check permissions because it thinks
    # the list hasn't changed.  To fix that, we have to call CheckCanChangeField
    # again with old!=new if the keyword action is "delete" and old=new.
    if ($keywordaction eq "delete"
1609
        && defined $cgi->param('keywords')
1610
        && length(@keywordlist) > 0
1611
        && $cgi->param('keywords') eq $oldhash{keywords}
1612 1613 1614 1615 1616
        && !CheckCanChangeField("keywords", $id, "old is not", "equal to new"))
    {
        $vars->{'oldvalue'} = $oldhash{keywords};
        $vars->{'newvalue'} = "no keywords";
        $vars->{'field'} = "keywords";
1617
        $vars->{'privs'} = $PrivilegesRequired;
1618
        ThrowUserError("illegal_change", $vars);
1619 1620
    }

1621
    $oldhash{'product'} = $old_bug_obj->product;
1622
    if (!Bugzilla->user->can_edit_product($oldhash{'product_id'})) {
1623
        ThrowUserError("product_edit_denied",
1624
                      { product => $oldhash{'product'} });
1625 1626
    }

1627
    if ($requiremilestone) {
1628 1629
        # musthavemilestoneonaccept applies only if at least two
        # target milestones are defined for the current product.
1630 1631
        my $prod_obj = new Bugzilla::Product({'name' => $oldhash{'product'}});
        my $nb_milestones = scalar(@{$prod_obj->milestones});
1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643
        if ($nb_milestones > 1) {
            my $value = $cgi->param('target_milestone');
            if (!defined $value || $value eq $cgi->param('dontchange')) {
                $value = $oldhash{'target_milestone'};
            }
            my $defaultmilestone =
                $dbh->selectrow_array("SELECT defaultmilestone
                                       FROM products WHERE id = ?",
                                       undef, $oldhash{'product_id'});
            # if musthavemilestoneonaccept == 1, then the target
            # milestone must be different from the default one.
            if ($value eq $defaultmilestone) {
1644
                ThrowUserError("milestone_required", { bug_id => $id });
1645
            }
1646 1647
        }
    }   
1648 1649
    if (defined $cgi->param('delta_ts') && $cgi->param('delta_ts') ne $delta_ts)
    {
1650 1651
        ($vars->{'operations'}) =
            Bugzilla::Bug::GetBugActivity($id, $cgi->param('delta_ts'));
1652

1653
        $vars->{'start_at'} = $cgi->param('longdesclength');
1654 1655 1656 1657

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

1659
        $cgi->param('delta_ts', $delta_ts);
1660 1661 1662
        
        $vars->{'bug_id'} = $id;
        
1663
        $dbh->bz_unlock_tables(UNLOCK_ABORT);
1664 1665
        
        # Warn the user about the mid-air collision and ask them what to do.
1666
        $template->process("bug/process/midair.html.tmpl", $vars)
1667
          || ThrowTemplateError($template->error());
1668 1669 1670
        exit;
    }

1671
    # Gather the dependency list, and make sure there are no circular refs
1672 1673
    my %deps = Bugzilla::Bug::ValidateDependencies(scalar($cgi->param('dependson')),
                                                   scalar($cgi->param('blocked')),
1674
                                                   $id);
1675

1676 1677 1678 1679
    #
    # Start updating the relevant database entries
    #

1680
    $timestamp = $dbh->selectrow_array(q{SELECT NOW()});
1681

1682 1683
    my $work_time;
    if (UserInGroup(Param('timetrackinggroup'))) {
1684
        $work_time = $cgi->param('work_time');
1685 1686 1687 1688
        if ($work_time) {
            # AppendComment (called below) can in theory raise an error,
            # but because we've already validated work_time here it's
            # safe to log the entry before adding the comment.
1689
            LogActivityEntry($id, "work_time", "", $work_time,
1690
                             $whoid, $timestamp);
1691 1692 1693
        }
    }

1694
    if ($cgi->param('comment') || $work_time) {
1695 1696
        AppendComment($id, $whoid, scalar($cgi->param('comment')),
                      scalar($cgi->param('commentprivacy')), $timestamp, $work_time);
1697 1698 1699
        $bug_changed = 1;
    }

1700 1701 1702
    if (Bugzilla::Keyword::keyword_count() 
        && defined $cgi->param('keywords')) 
    {
1703 1704 1705 1706 1707
        # 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.
1708
        my $changed = 0;
1709
        if ($keywordaction eq "makeexact") {
1710 1711
            $dbh->do(q{DELETE FROM keywords WHERE bug_id = ?},
                     undef, $id);
1712
            $changed = 1;
1713
        }
1714 1715 1716 1717 1718 1719
        my $sth_delete = $dbh->prepare(q{DELETE FROM keywords
                                               WHERE bug_id = ?
                                                 AND keywordid = ?});
        my $sth_insert =
            $dbh->prepare(q{INSERT INTO keywords (bug_id, keywordid)
                                 VALUES (?, ?)});
1720 1721
        foreach my $keyword (@keywordlist) {
            if ($keywordaction ne "makeexact") {
1722
                $sth_delete->execute($id, $keyword);
1723
                $changed = 1;
1724 1725
            }
            if ($keywordaction ne "delete") {
1726
                $sth_insert->execute($id, $keyword);
1727 1728 1729 1730
                $changed = 1;
            }
        }
        if ($changed) {
1731 1732 1733 1734 1735 1736 1737 1738
            my $list = $dbh->selectcol_arrayref(
                q{SELECT keyworddefs.name
                    FROM keyworddefs
              INNER JOIN keywords 
                      ON keyworddefs.id = keywords.keywordid
                   WHERE keywords.bug_id = ?
                ORDER BY keyworddefs.name},
                undef, $id);
1739
            $dbh->do("UPDATE bugs SET keywords = ? WHERE bug_id = ?",
1740
                     undef, join(', ', @$list), $id);
1741 1742
        }
    }
1743
    $query .= " WHERE bug_id = ?";
1744
    push(@bug_values, $id);
1745
    
1746
    if ($::comma ne "") {
1747
        $dbh->do($query, undef, @bug_values);
terry%netscape.com's avatar
terry%netscape.com committed
1748
    }
1749

1750
    # Check for duplicates if the bug is [re]open or its resolution is changed.
1751 1752
    my $resolution = $dbh->selectrow_array(
        q{SELECT resolution FROM bugs WHERE bug_id = ?}, undef, $id);
1753
    if ($resolution ne 'DUPLICATE') {
1754
        $dbh->do(q{DELETE FROM duplicates WHERE dupe = ?}, undef, $id);
1755
    }
1756

1757 1758
    my %groupsrequired = ();
    my %groupsforbidden = ();
1759 1760 1761 1762 1763 1764 1765
    my $group_controls =
        $dbh->selectall_arrayref(q{SELECT id, membercontrol
                                     FROM groups
                                LEFT JOIN group_control_map
                                       ON id = group_id
                                      AND product_id = ?
                                    WHERE isactive != 0},
1766
        undef, $oldhash{'product_id'});
1767 1768
    foreach my $group_control (@$group_controls) {
        my ($group, $control) = @$group_control;
1769 1770 1771 1772 1773 1774 1775 1776 1777
        $control ||= 0;
        unless ($control > &::CONTROLMAPNA)  {
            $groupsforbidden{$group} = 1;
        }
        if ($control == &::CONTROLMAPMANDATORY) {
            $groupsrequired{$group} = 1;
        }
    }

1778
    my @groupAddNames = ();
1779
    my @groupAddNamesAll = ();
1780 1781
    my $sth = $dbh->prepare(q{INSERT INTO bug_group_map (bug_id, group_id)
                                   VALUES (?, ?)});
1782 1783
    foreach my $grouptoadd (@groupAdd, keys %groupsrequired) {
        next if $groupsforbidden{$grouptoadd};
1784 1785
        my $group_obj = new Bugzilla::Group($grouptoadd);
        push(@groupAddNamesAll, $group_obj->name);
1786
        if (!BugInGroupId($id, $grouptoadd)) {
1787
            push(@groupAddNames, $group_obj->name);
1788
            $sth->execute($id, $grouptoadd);
1789 1790 1791
        }
    }
    my @groupDelNames = ();
1792
    my @groupDelNamesAll = ();
1793 1794
    $sth = $dbh->prepare(q{DELETE FROM bug_group_map
                                 WHERE bug_id = ? AND group_id = ?});
1795
    foreach my $grouptodel (@groupDel, keys %groupsforbidden) {
1796 1797
        my $group_obj = new Bugzilla::Group($grouptodel);
        push(@groupDelNamesAll, $group_obj->name);
1798
        next if $groupsrequired{$grouptodel};
1799
        if (BugInGroupId($id, $grouptodel)) {
1800
            push(@groupDelNames, $group_obj->name);
1801
        }
1802
        $sth->execute($id, $grouptodel);
1803 1804 1805 1806 1807
    }

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

1808 1809 1810 1811 1812
    if ($groupDelNames ne $groupAddNames) {
        LogActivityEntry($id, "bug_group", $groupDelNames, $groupAddNames,
                         $whoid, $timestamp); 
        $bug_changed = 1;
    }
1813 1814

    my @ccRemoved = (); 
1815 1816 1817 1818
    if (defined $cgi->param('newcc')
        || defined $cgi->param('addselfcc')
        || defined $cgi->param('removecc')
        || defined $cgi->param('masscc')) {
1819 1820
        # Get the current CC list for this bug
        my %oncc;
1821 1822 1823 1824
        my $cc_list = $dbh->selectcol_arrayref(
            q{SELECT who FROM cc WHERE bug_id = ?}, undef, $id);
        foreach my $who (@$cc_list) {
            $oncc{$who} = 1;
1825 1826
        }

1827
        my (@added, @removed) = ();
1828 1829 1830

        my $sth_insert = $dbh->prepare(q{INSERT INTO cc (bug_id, who)
                                              VALUES (?, ?)});
1831 1832 1833
        foreach my $pid (keys %cc_add) {
            # If this person isn't already on the cc list, add them
            if (! $oncc{$pid}) {
1834
                $sth_insert->execute($id, $pid);
1835 1836
                push (@added, $cc_add{$pid});
                $oncc{$pid} = 1;
1837 1838
            }
        }
1839 1840
        my $sth_delete = $dbh->prepare(q{DELETE FROM cc
                                          WHERE bug_id = ? AND who = ?});
1841 1842 1843
        foreach my $pid (keys %cc_remove) {
            # If the person is on the cc list, remove them
            if ($oncc{$pid}) {
1844
                $sth_delete->execute($id, $pid);
1845 1846
                push (@removed, $cc_remove{$pid});
                $oncc{$pid} = 0;
1847 1848
            }
        }
1849

1850 1851
        # If any changes were found, record it in the activity log
        if (scalar(@removed) || scalar(@added)) {
1852 1853
            my $removed = join(", ", @removed);
            my $added = join(", ", @added);
1854 1855
            LogActivityEntry($id,"cc",$removed,$added,$whoid,$timestamp);
            $bug_changed = 1;
1856
        }
1857
        @ccRemoved = @removed;
1858
    }
1859

1860
    # We need to send mail for dependson/blocked bugs if the dependencies
1861 1862 1863
    # change or the status or resolution change. This var keeps track of that.
    my $check_dep_bugs = 0;

1864 1865 1866 1867 1868 1869 1870 1871
    foreach my $pair ("blocked/dependson", "dependson/blocked") {
        my ($me, $target) = split("/", $pair);

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

1872
        if (defined $cgi->param($target)) {
1873 1874
            my %snapshot;
            my @newlist = sort {$a <=> $b} @{$deps{$target}};
1875
            @dependencychanged{@newlist} = 1;
1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889

            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]) {
1890
                        ThrowCodeError('list_comparison_error');
1891 1892 1893 1894 1895 1896 1897 1898
                    }
                    shift @oldlist;
                    shift @newlist;
                }
            }
            my @keys = keys(%snapshot);
            if (@keys) {
                my $oldsnap = SnapShotDeps($id, $target, $me);
1899 1900 1901 1902 1903
                $dbh->do(qq{DELETE FROM dependencies WHERE $me = ?},
                         undef, $id);
                my $sth =
                    $dbh->prepare(qq{INSERT INTO dependencies ($me, $target)
                                          VALUES (?, ?)});
1904
                foreach my $i (@{$deps{$target}}) {
1905
                    $sth->execute($id, $i);
1906 1907
                }
                foreach my $k (@keys) {
1908
                    LogDependencyActivity($k, $snapshot{$k}, $me, $target, $timestamp);
1909
                }
1910
                LogDependencyActivity($id, $oldsnap, $target, $me, $timestamp);
1911
                $check_dep_bugs = 1;
1912 1913 1914 1915
            }
        }
    }

1916 1917 1918 1919 1920
    # 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.
1921
    # Check if the user has changed the product to which the bug belongs;
1922
    if ($cgi->param('product') ne $cgi->param('dontchange')
1923 1924
        && $cgi->param('product') ne $oldhash{'product'})
    {
1925 1926 1927 1928 1929 1930 1931 1932 1933
        # 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?
1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950
        my $groups = $dbh->selectall_arrayref(
            qq{SELECT DISTINCT groups.id, isactive,
                               oldcontrolmap.membercontrol,
                               newcontrolmap.membercontrol,
                      CASE WHEN groups.id IN ($grouplist) THEN 1 ELSE 0 END,
                      CASE WHEN bug_group_map.group_id IS NOT NULL
                                THEN 1 ELSE 0 END
                 FROM groups
            LEFT JOIN group_control_map AS oldcontrolmap
                   ON oldcontrolmap.group_id = groups.id
                  AND oldcontrolmap.product_id = ?
            LEFT JOIN group_control_map AS newcontrolmap
                   ON newcontrolmap.group_id = groups.id
                  AND newcontrolmap.product_id = ?
            LEFT JOIN bug_group_map
                   ON bug_group_map.group_id = groups.id
                  AND bug_group_map.bug_id = ?},
1951
            undef, $oldhash{'product_id'}, $product->id, $id);
1952 1953 1954 1955 1956 1957 1958
        my @groupstoremove = ();
        my @groupstoadd = ();
        my @defaultstoremove = ();
        my @defaultstoadd = ();
        my @allgroups = ();
        my $buginanydefault = 0;
        my $buginanychangingdefault = 0;
1959 1960 1961
        foreach my $group (@$groups) {
            my ($groupid, $isactive, $oldcontrol, $newcontrol,
                   $useringroup, $bugingroup) = @$group;
1962 1963 1964 1965 1966 1967 1968 1969
            # 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;
1970 1971
                if (($newcontrol != CONTROLMAPDEFAULT)
                    && ($newcontrol != CONTROLMAPMANDATORY)) {
1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989
                    # 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);
1990
            }
1991
        }
1992 1993 1994 1995 1996 1997 1998 1999 2000
        # 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()
2001 2002
            && (($cgi->param('addtonewgroup') eq 'yes')
            || (($cgi->param('addtonewgroup') eq 'yesifinold')
2003 2004
            && ($buginanydefault)))) {
            push(@groupstoadd, @defaultstoadd);
2005 2006
        }

2007 2008 2009
        # Now actually update the bug_group_map.
        my @DefGroupsAdded = ();
        my @DefGroupsRemoved = ();
2010 2011 2012 2013 2014 2015
        my $sth_insert =
            $dbh->prepare(q{INSERT INTO bug_group_map (bug_id, group_id)
                                 VALUES (?, ?)});
        my $sth_delete = $dbh->prepare(q{DELETE FROM bug_group_map
                                               WHERE bug_id = ?
                                                 AND group_id = ?});
2016 2017 2018 2019
        foreach my $groupid (@allgroups) {
            my $thisadd = grep( ($_ == $groupid), @groupstoadd);
            my $thisdel = grep( ($_ == $groupid), @groupstoremove);
            if ($thisadd) {
2020 2021
                my $group_obj = new Bugzilla::Group($groupid);
                push(@DefGroupsAdded, $group_obj->name);
2022
                $sth_insert->execute($id, $groupid);
2023
            } elsif ($thisdel) {
2024 2025
                my $group_obj = new Bugzilla::Group($groupid);
                push(@DefGroupsRemoved, $group_obj->name);
2026
                $sth_delete->execute($id, $groupid);
2027 2028 2029 2030 2031 2032 2033 2034
            }
        }
        if ((@DefGroupsAdded) || (@DefGroupsRemoved)) {
            LogActivityEntry($id, "bug_group",
                join(', ', @DefGroupsRemoved),
                join(', ', @DefGroupsAdded),
                     $whoid, $timestamp); 
        }
2035 2036
    }
  
2037 2038 2039 2040
    # 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.
    #
2041
    my $new_bug_obj = new Bugzilla::Bug($id, $whoid);
2042
    my @newvalues = SnapShotBug($id);
2043 2044
    my %newhash;
    $i = 0;
2045
    foreach my $col (@editable_bug_fields) {
2046
        # Consider NULL db entries to be equivalent to the empty string
2047
        $newvalues[$i] = defined($newvalues[$i]) ? $newvalues[$i] : '';
2048 2049 2050 2051
        # Convert the deadline to the YYYY-MM-DD format.
        if ($col eq 'deadline') {
            $newvalues[$i] = format_time($newvalues[$i], "%Y-%m-%d");
        }
2052 2053 2054
        $newhash{$col} = $newvalues[$i];
        $i++;
    }
2055
    # for passing to Bugzilla::BugMail to ensure that when someone is removed
2056 2057 2058 2059
    # from one of these fields, they get notified of that fact (if desired)
    #
    my $origOwner = "";
    my $origQaContact = "";
2060 2061 2062 2063

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

2064
    foreach my $c (@editable_bug_fields) {
2065 2066
        my $col = $c;           # We modify it, don't want to modify array
                                # values in place.
2067 2068 2069
        my $old = shift @oldvalues;
        my $new = shift @newvalues;
        if ($old ne $new) {
2070

2071 2072 2073
            # 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') {
2074 2075
                $old = $old_bug_obj->product;
                $new = $new_bug_obj->product;
2076 2077 2078
                $col = 'product';
            }
            if ($col eq 'component_id') {
2079 2080
                $old = $old_bug_obj->component;
                $new = $new_bug_obj->component;
2081 2082 2083
                $col = 'component';
            }

2084
            # save off the old value for passing to Bugzilla::BugMail so
2085
            # the old assignee can be notified
2086 2087 2088 2089 2090 2091 2092 2093 2094 2095
            #
            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') {
2096 2097
                $old = ($old) ? DBID_to_name($old) : "";
                $new = ($new) ? DBID_to_name($new) : "";
2098
                $origQaContact = $old;
terry%netscape.com's avatar
terry%netscape.com committed
2099
            }
2100

2101 2102
            # If this is the keyword field, only record the changes, not everything.
            if ($col eq 'keywords') {
2103
                ($old, $new) = diff_strings($old, $new);
2104 2105
            }

2106
            if ($col eq 'product') {
2107 2108 2109 2110 2111
                # If some votes have been removed, RemoveVotes() returns
                # a list of messages to send to voters.
                # We delay the sending of these messages till tables are unlocked.
                $msgs = RemoveVotes($id, 0,
                          "This bug has been moved to a different product");
2112
            }
2113

2114
            if ($col eq 'bug_status' 
2115
                && is_open_state($old) ne is_open_state($new))
2116 2117 2118
            {
                $check_dep_bugs = 1;
            }
2119

2120 2121
            LogActivityEntry($id,$col,$old,$new,$whoid,$timestamp);
            $bug_changed = 1;
terry%netscape.com's avatar
terry%netscape.com committed
2122 2123
        }
    }
2124
    # Set and update flags.
2125
    Bugzilla::Flag::process($new_bug_obj, undef, $timestamp, $cgi);
2126

2127
    if ($bug_changed) {
2128 2129
        $dbh->do(q{UPDATE bugs SET delta_ts = ? WHERE bug_id = ?},
                 undef, $timestamp, $id);
2130
    }
2131
    $dbh->bz_unlock_tables();
2132

2133 2134
    # Now is a good time to send email to voters.
    foreach my $msg (@$msgs) {
2135
        MessageToMTA($msg);
2136 2137
    }

2138
    if ($duplicate) {
2139 2140 2141 2142 2143
        # If the bug was already marked as a duplicate, remove
        # the existing entry.
        $dbh->do('DELETE FROM duplicates WHERE dupe = ?',
                  undef, $cgi->param('id'));

2144
        # Check to see if Reporter of this bug is reporter of Dupe 
2145 2146 2147 2148 2149 2150 2151 2152
        my $reporter = $dbh->selectrow_array(
            q{SELECT reporter FROM bugs WHERE bug_id = ?}, undef, $id);
        my $isreporter = $dbh->selectrow_array(
            q{SELECT reporter FROM bugs WHERE bug_id = ? AND reporter = ?},
            undef, $duplicate, $reporter);
        my $isoncc = $dbh->selectrow_array(q{SELECT who FROM cc
                                           WHERE bug_id = ? AND who = ?},
                                           undef, $duplicate, $reporter);
2153 2154
        unless ($isreporter || $isoncc
                || !$cgi->param('confirm_add_duplicate')) {
matty%chariot.net.au's avatar
matty%chariot.net.au committed
2155
            # The reporter is oblivious to the existence of the new bug and is permitted access
2156
            # ... add 'em to the cc (and record activity)
2157 2158
            LogActivityEntry($duplicate,"cc","",DBID_to_name($reporter),
                             $whoid,$timestamp);
2159 2160
            $dbh->do(q{INSERT INTO cc (who, bug_id) VALUES (?, ?)},
                     undef, $reporter, $duplicate);
2161
        }
2162
        # Bug 171639 - Duplicate notifications do not need to be private. 
2163
        AppendComment($duplicate, $whoid,
2164 2165
                      "*** Bug " . $cgi->param('id') .
                      " has been marked as a duplicate of this bug. ***",
2166 2167
                      0, $timestamp);

2168 2169
        $dbh->do(q{INSERT INTO duplicates VALUES (?, ?)}, undef,
                 $duplicate, $cgi->param('id'));
2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181
    }

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

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

    $vars->{'id'} = $id;
2182
    $vars->{'type'} = "bug";
2183 2184 2185 2186 2187 2188 2189 2190
    
    # Let the user know the bug was changed and who did and didn't
    # receive email about the change.
    $template->process("bug/process/results.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
    $vars->{'header_done'} = 1;
    
    if ($duplicate) {
2191
        $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; 
2192

2193 2194 2195 2196
        $vars->{'id'} = $duplicate;
        $vars->{'type'} = "dupe";
        
        # Let the user know a duplication notation was added to the original bug.
2197
        $template->process("bug/process/results.html.tmpl", $vars)
2198
          || ThrowTemplateError($template->error());
2199
        $vars->{'header_done'} = 1;
2200 2201
    }

2202 2203
    if ($check_dep_bugs) {
        foreach my $k (keys(%dependencychanged)) {
2204
            $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login }; 
2205 2206 2207
            $vars->{'id'} = $k;
            $vars->{'type'} = "dep";

2208 2209 2210
            # Let the user (if he is able to see the bug) know we checked to see 
            # if we should email notice of this change to users with a relationship
            # to the dependent bug and who did and didn't receive email about it.
2211 2212
            $template->process("bug/process/results.html.tmpl", $vars)
              || ThrowTemplateError($template->error());
2213
            $vars->{'header_done'} = 1;
2214
        }
2215
    }
terry%netscape.com's avatar
terry%netscape.com committed
2216 2217
}

2218 2219 2220 2221 2222 2223
# Determine if Patch Viewer is installed, for Diff link
# (NB: Duplicate code with show_bug.cgi.)
eval {
    require PatchReader;
    $vars->{'patchviewerinstalled'} = 1;
};
2224

2225 2226 2227 2228 2229 2230
if (defined $cgi->param('id')) {
    $action = Bugzilla->user->settings->{'post_bug_submit_action'}->{'value'};
} else {
    # param('id') is not defined when changing multiple bugs
    $action = 'nothing';
}
2231

2232 2233 2234 2235 2236 2237 2238 2239 2240 2241
if ($action eq 'next_bug') {
    my $next_bug;
    my $cur = lsearch(\@bug_list, $cgi->param("id"));
    if ($cur >= 0 && $cur < $#bug_list) {
        $next_bug = $bug_list[$cur + 1];
    }
    if ($next_bug) {
        if (detaint_natural($next_bug) && Bugzilla->user->can_see_bug($next_bug)) {
            my $bug = new Bugzilla::Bug($next_bug, $whoid);
            ThrowCodeError("bug_error", { bug => $bug }) if $bug->error;
2242

2243 2244
            $vars->{'bugs'} = [$bug];
            $vars->{'nextbug'} = $bug->bug_id;
2245

2246 2247 2248 2249 2250
            $template->process("bug/show.html.tmpl", $vars)
              || ThrowTemplateError($template->error());

            exit;
        }
2251
    }
2252
} elsif ($action eq 'same_bug') {
2253 2254 2255
    if (Bugzilla->user->can_see_bug($cgi->param('id'))) {
        my $bug = new Bugzilla::Bug($cgi->param('id'), $whoid);
        ThrowCodeError("bug_error", { bug => $bug }) if $bug->error;
2256

2257
        $vars->{'bugs'} = [$bug];
2258

2259 2260
        $template->process("bug/show.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
2261

2262 2263
        exit;
    }
2264 2265
} elsif ($action ne 'nothing') {
    ThrowCodeError("invalid_post_bug_submit_action");
terry%netscape.com's avatar
terry%netscape.com committed
2266
}
2267

2268
# End the response page.
2269
$template->process("bug/navigate.html.tmpl", $vars)
2270
  || ThrowTemplateError($template->error());
2271
$template->process("global/footer.html.tmpl", $vars)
2272
  || ThrowTemplateError($template->error());