process_bug.cgi 14.8 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>
29
#                 Akamai Technologies <bugzilla-dev@akamai.com>
30
#                 Max Kanat-Alexander <mkanat@bugzilla.org>
terry%netscape.com's avatar
terry%netscape.com committed
31

32 33 34 35 36 37 38 39 40 41 42 43
# 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.

44 45
use strict;

46
use lib qw(. lib);
47

48
use Bugzilla;
49
use Bugzilla::Constants;
50
use Bugzilla::Bug;
51
use Bugzilla::BugMail;
52
use Bugzilla::Mailer;
53
use Bugzilla::User;
54
use Bugzilla::Util;
55
use Bugzilla::Error;
56
use Bugzilla::Field;
57
use Bugzilla::Product;
58
use Bugzilla::Component;
59
use Bugzilla::Keyword;
60
use Bugzilla::Flag;
61
use Bugzilla::Status;
62
use Bugzilla::Token;
63

64
use List::MoreUtils qw(firstidx);
65 66
use Storable qw(dclone);

67
my $user = Bugzilla->login(LOGIN_REQUIRED);
68

69
my $cgi = Bugzilla->cgi;
70
my $dbh = Bugzilla->dbh;
71
my $template = Bugzilla->template;
72
my $vars = {};
73

74 75 76 77
######################################################################
# Subroutines
######################################################################

78
# Tells us whether or not a field should be changed by process_bug.
79
sub should_set {
80
    # check_defined is used for fields where there's another field
81 82 83 84
    # whose name starts with "defined_" and then the field name--it's used
    # to know when we did things like empty a multi-select or deselect
    # a checkbox.
    my ($field, $check_defined) = @_;
85
    my $cgi = Bugzilla->cgi;
86 87
    if ( defined $cgi->param($field) 
         || ($check_defined && defined $cgi->param("defined_$field")) )
88 89 90 91 92 93
    {
        return 1;
    }
    return 0;
}

94 95 96 97
######################################################################
# Begin Data/Security Validation
######################################################################

98 99
# Create a list of objects for all bugs being modified in this request.
my @bug_objects;
100
if (defined $cgi->param('id')) {
101 102 103
  my $bug = Bugzilla::Bug->check(scalar $cgi->param('id'));
  $cgi->param('id', $bug->id);
  push(@bug_objects, $bug);
104
} else {
105
    foreach my $i ($cgi->param()) {
106
        if ($i =~ /^id_([1-9][0-9]*)/) {
107
            my $id = $1;
108
            push(@bug_objects, Bugzilla::Bug->check($id));
109
        }
110 111 112
    }
}

113
# Make sure there are bugs to process.
114
scalar(@bug_objects) || ThrowUserError("no_bugs_chosen", {action => 'modify'});
115

116
my $first_bug = $bug_objects[0]; # Used when we're only updating a single bug.
117

118 119 120 121
# Delete any parameter set to 'dontchange'.
if (defined $cgi->param('dontchange')) {
    foreach my $name ($cgi->param) {
        next if $name eq 'dontchange'; # But don't delete dontchange itself!
122 123
        # Skip ones we've already deleted (such as "defined_$name").
        next if !defined $cgi->param($name);
124 125
        if ($cgi->param($name) eq $cgi->param('dontchange')) {
            $cgi->delete($name);
126
            $cgi->delete("defined_$name");
127 128
        }
    }
129 130
}

131
# do a match on the fields if applicable
132
Bugzilla::User::match_field({
133 134 135 136 137
    'qa_contact'                => { 'type' => 'single' },
    'newcc'                     => { 'type' => 'multi'  },
    'masscc'                    => { 'type' => 'multi'  },
    'assigned_to'               => { 'type' => 'single' },
});
138

139
print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL;
140

141 142
# Check for a mid-air collision. Currently this only works when updating
# an individual bug.
143
if (defined $cgi->param('delta_ts'))
144
{
145 146 147 148 149 150 151 152
    my $delta_ts_z = datetime_from($cgi->param('delta_ts'));
    my $first_delta_tz_z =  datetime_from($first_bug->delta_ts);
    if ($first_delta_tz_z ne $delta_ts_z) {
        ($vars->{'operations'}) =
            Bugzilla::Bug::GetBugActivity($first_bug->id, undef,
                                          scalar $cgi->param('delta_ts'));

        $vars->{'title_tag'} = "mid_air";
153
    
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
        ThrowCodeError('undefined_field', { field => 'longdesclength' })
            if !defined $cgi->param('longdesclength');

        $vars->{'start_at'} = $cgi->param('longdesclength');
        # Always sort midair collision comments oldest to newest,
        # regardless of the user's personal preference.
        $vars->{'comments'} = $first_bug->comments({ order => "oldest_to_newest" });
        $vars->{'bug'} = $first_bug;

        # The token contains the old delta_ts. We need a new one.
        $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
        # Warn the user about the mid-air collision and ask them what to do.
        $template->process("bug/process/midair.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
        exit;
    }
170
}
171

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
# We couldn't do this check earlier as we first had to validate bug IDs
# and display the mid-air collision page if delta_ts changed.
# If we do a mass-change, we use session tokens.
my $token = $cgi->param('token');

if ($cgi->param('id')) {
    check_hash_token($token, [$first_bug->id, $first_bug->delta_ts]);
}
else {
    check_token_data($token, 'buglist_mass_change', 'query.cgi');
}

######################################################################
# End Data/Security Validation
######################################################################

188 189
$vars->{'title_tag'} = "bug_processed";

190
my $action;
191 192 193 194
if (defined $cgi->param('id')) {
    $action = Bugzilla->user->settings->{'post_bug_submit_action'}->{'value'};

    if ($action eq 'next_bug') {
195 196 197 198
        my @bug_list;
        if ($cgi->cookie("BUGLIST")) {
            @bug_list = split(/:/, $cgi->cookie("BUGLIST"));
        }
199
        my $cur = firstidx { $_ eq $cgi->param('id') } @bug_list;
200
        if ($cur >= 0 && $cur < $#bug_list) {
201 202 203
            my $next_bug_id = $bug_list[$cur + 1];
            detaint_natural($next_bug_id);
            if ($next_bug_id and $user->can_see_bug($next_bug_id)) {
204
                # We create an object here so that $bug->send_changes can use it
205 206 207
                # when displaying the header.
                $vars->{'bug'} = new Bugzilla::Bug($next_bug_id);
            }
208 209 210 211
        }
    }
    # Include both action = 'same_bug' and 'nothing'.
    else {
212
        $vars->{'bug'} = $first_bug;
213 214 215 216 217 218 219
    }
}
else {
    # param('id') is not defined when changing multiple bugs at once.
    $action = 'nothing';
}

220 221 222 223 224 225
# For each bug, we have to check if the user can edit the bug the product
# is currently in, before we allow them to change anything.
foreach my $bug (@bug_objects) {
    if (!Bugzilla->user->can_edit_product($bug->product_obj->id) ) {
        ThrowUserError("product_edit_denied",
                      { product => $bug->product });
226
    }
227
}
228

229 230 231
# Component, target_milestone, and version are in here just in case
# the 'product' field wasn't defined in the CGI. It doesn't hurt to set
# them twice.
232 233 234
my @set_fields = qw(op_sys rep_platform priority bug_severity
                    component target_milestone version
                    bug_file_loc status_whiteboard short_desc
235
                    deadline remaining_time estimated_time
236
                    work_time set_default_assignee set_default_qa_contact
237
                    cclist_accessible reporter_accessible 
238 239
                    product confirm_product_change
                    bug_status resolution dup_id);
240 241
push(@set_fields, 'assigned_to') if !$cgi->param('set_default_assignee');
push(@set_fields, 'qa_contact')  if !$cgi->param('set_default_qa_contact');
242 243 244 245 246
my %field_translation = (
    bug_severity => 'severity',
    rep_platform => 'platform',
    short_desc   => 'summary',
    bug_file_loc => 'url',
247 248
    set_default_assignee   => 'reset_assigned_to',
    set_default_qa_contact => 'reset_qa_contact',
249
    confirm_product_change => 'product_change_confirmed',
250 251
);

252
my %set_all_fields = ( other_bugs => \@bug_objects );
253
foreach my $field_name (@set_fields) {
254
    if (should_set($field_name, 1)) {
255 256 257 258 259
        my $param_name = $field_translation{$field_name} || $field_name;
        $set_all_fields{$param_name} = $cgi->param($field_name);
    }
}

260
if (should_set('keywords')) {
261 262
    my $action = $cgi->param('keywordaction') || '';
    # Backward-compatibility for Bugzilla 3.x and older.
263 264 265 266
    $action = 'remove' if $action eq 'delete';
    $action = 'set'    if $action eq 'makeexact';
    $set_all_fields{keywords}->{$action} = $cgi->param('keywords');
}
267 268 269 270 271 272
if (should_set('comment')) {
    $set_all_fields{comment} = {
        body       => scalar $cgi->param('comment'),
        is_private => scalar $cgi->param('commentprivacy'),
    };
}
273 274 275 276 277 278 279
if (should_set('see_also')) {
    $set_all_fields{'see_also'}->{add} = 
        [split(/[\s,]+/, $cgi->param('see_also'))];
}
if (should_set('remove_see_also')) {
    $set_all_fields{'see_also'}->{remove} = [$cgi->param('remove_see_also')];
}
280 281 282 283 284 285 286 287 288 289 290
foreach my $dep_field (qw(dependson blocked)) {
    if (should_set($dep_field)) {
        if (my $dep_action = $cgi->param("${dep_field}_action")) {
            $set_all_fields{$dep_field}->{$dep_action} =
                [split(/\s,/, $cgi->param($dep_field))];
        }
        else {
            $set_all_fields{$dep_field}->{set} = $cgi->param($dep_field);
        }
    }
}
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
# Formulate the CC data into two arrays of users involved in this CC change.
my (@cc_add, @cc_remove);
if (defined $cgi->param('newcc')
    or defined $cgi->param('addselfcc')
    or defined $cgi->param('removecc')
    or defined $cgi->param('masscc')) 
{

    # If masscc is defined, then we came from buglist and need to either add or
    # remove cc's... otherwise, we came from show_bug and may need to do both.
    my ($cc_add, $cc_remove) = "";
    if (defined $cgi->param('masscc')) {
        if ($cgi->param('ccaction') eq 'add') {
            $cc_add = $cgi->param('masscc');
        } elsif ($cgi->param('ccaction') eq 'remove') {
            $cc_remove = $cgi->param('masscc');
        }
    } else {
        $cc_add = $cgi->param('newcc');
        # We came from bug_form which uses a select box to determine what cc's
        # need to be removed...
        if ($cgi->param('removecc') && $cgi->param('cc')) {
            $cc_remove = join(",", $cgi->param('cc'));
        }
    }

    push(@cc_add, split(/[\s,]+/, $cc_add)) if $cc_add;
    push(@cc_add, Bugzilla->user) if $cgi->param('addselfcc');

    push(@cc_remove, split(/[\s,]+/, $cc_remove)) if $cc_remove;
}
$set_all_fields{cc} = { add => \@cc_add, remove => \@cc_remove };
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340

# Fields that can only be set on one bug at a time.
if (defined $cgi->param('id')) {
    # Since aliases are unique (like bug numbers), they can only be changed
    # for one bug at a time.
    if (Bugzilla->params->{"usebugaliases"} && defined $cgi->param('alias')) {
        $set_all_fields{alias} = $cgi->param('alias');
    }
}

my %is_private;
foreach my $field (grep(/^defined_isprivate/, $cgi->param())) {
    $field =~ /(\d+)$/;
    my $comment_id = $1;
    $is_private{$comment_id} = $cgi->param("isprivate_$comment_id");
}
$set_all_fields{comment_is_private} = \%is_private;

341 342 343 344
my @check_groups = $cgi->param('defined_groups');
my @set_groups = $cgi->param('groups');
my ($removed_groups) = diff_arrays(\@check_groups, \@set_groups);
$set_all_fields{groups} = { add => \@set_groups, remove => $removed_groups };
345

346
my @custom_fields = Bugzilla->active_custom_fields;
347 348 349 350 351 352
foreach my $field (@custom_fields) {
    my $fname = $field->name;
    if (should_set($fname, 1)) {
        $set_all_fields{$fname} = [$cgi->param($fname)];
    }
}
353

354
foreach my $b (@bug_objects) {
355
    $b->set_all(\%set_all_fields);
356 357
}

358
if (defined $cgi->param('id')) {
359
    # Flags should be set AFTER the bug has been moved into another
360 361
    # product/component. The structure of flags code doesn't currently
    # allow them to be set using set_all.
362 363 364
    my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
        $first_bug, undef, $vars);
    $first_bug->set_flags($flags, $new_flags);
365
}
366

367 368 369 370
##############################
# Do Actual Database Updates #
##############################
foreach my $bug (@bug_objects) {
371
    my $changes = $bug->update();
372 373

    if ($changes->{'bug_status'}) {
374
        my $new_status = $changes->{'bug_status'}->[1];
375 376 377 378
        # We may have zeroed the remaining time, if we moved into a closed
        # status, so we should inform the user about that.
        if (!is_open_state($new_status) && $changes->{'remaining_time'}) {
            $vars->{'message'} = "remaining_time_zeroed"
379
              if Bugzilla->user->is_timetracker;
380 381
        }
    }
382

383
    $bug->send_changes($changes, $vars);
terry%netscape.com's avatar
terry%netscape.com committed
384 385
}

386 387 388
if (Bugzilla->usage_mode == USAGE_MODE_EMAIL) {
    # Do nothing.
}
389 390 391 392 393 394 395 396 397 398
elsif ($action eq 'next_bug' or $action eq 'same_bug') {
    my $bug = $vars->{'bug'};
    if ($bug and $user->can_see_bug($bug)) {
        if ($action eq 'same_bug') {
            # $bug->update() does not update the internal structure of
            # the bug sufficiently to display the bug with the new values.
            # (That is, if we just passed in the old Bug object, we'd get
            # a lot of old values displayed.)
            $bug = new Bugzilla::Bug($bug->id);
            $vars->{'bug'} = $bug;
399
        }
400
        $vars->{'bugs'} = [$bug];
401 402 403
        if ($action eq 'next_bug') {
            $vars->{'nextbug'} = $bug->id;
        }
404 405 406 407
        $template->process("bug/show.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
        exit;
    }
408 409
} elsif ($action ne 'nothing') {
    ThrowCodeError("invalid_post_bug_submit_action");
terry%netscape.com's avatar
terry%netscape.com committed
410
}
411

412
# End the response page.
413 414 415 416 417 418 419 420
unless (Bugzilla->usage_mode == USAGE_MODE_EMAIL) {
    $template->process("bug/navigate.html.tmpl", $vars)
        || ThrowTemplateError($template->error());
    $template->process("global/footer.html.tmpl", $vars)
        || ThrowTemplateError($template->error());
}

1;