attachment.cgi 26.9 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# 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.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
#                 Myk Melez <myk@mozilla.org>
23 24
#                 Daniel Raichle <draichle@gmx.net>
#                 Dave Miller <justdave@syndicomm.com>
25
#                 Alexander J. Vincent <ajvincent@juno.com>
26
#                 Max Kanat-Alexander <mkanat@bugzilla.org>
27
#                 Greg Hendricks <ghendricks@novell.com>
28
#                 Frédéric Buclin <LpSolit@gmail.com>
29
#                 Marc Schumann <wurblzap@gmail.com>
30
#                 Byron Jones <bugzilla@glob.com.au>
31 32 33 34 35 36 37 38

################################################################################
# Script Initialization
################################################################################

# Make it harder for us to do dangerous things in Perl.
use strict;

39
use lib qw(. lib);
40

41
use Bugzilla;
42
use Bugzilla::Constants;
43
use Bugzilla::Error;
44 45
use Bugzilla::Flag; 
use Bugzilla::FlagType; 
46
use Bugzilla::User;
47
use Bugzilla::Util;
48
use Bugzilla::Bug;
49
use Bugzilla::Field;
50
use Bugzilla::Attachment;
51
use Bugzilla::Attachment::PatchReader;
52
use Bugzilla::Token;
53
use Bugzilla::Keyword;
54

55 56
use Encode qw(encode);

57 58 59 60 61 62 63
# For most scripts we don't make $cgi and $template global variables. But
# when preparing Bugzilla for mod_perl, this script used these
# variables in so many subroutines that it was easier to just
# make them globals.
local our $cgi = Bugzilla->cgi;
local our $template = Bugzilla->template;
local our $vars = {};
64

65 66 67 68
################################################################################
# Main Body Execution
################################################################################

69 70 71 72
# All calls to this script should contain an "action" variable whose
# value determines what the user wants to do.  The code below checks
# the value of that variable and runs the appropriate code. If none is
# supplied, we default to 'view'.
73 74

# Determine whether to use the action specified by the user or the default.
75
my $action = $cgi->param('action') || 'view';
76

77 78 79
# You must use the appropriate urlbase/sslbase param when doing anything
# but viewing an attachment.
if ($action ne 'view') {
80 81
    do_ssl_redirect_if_required();
    if ($cgi->url_is_attachment_base) {
82 83 84 85 86 87 88 89
        $cgi->redirect_to_urlbase;
    }
    Bugzilla->login();
}

# When viewing an attachment, do not request credentials if we are on
# the alternate host. Let view() decide when to call Bugzilla->login.
if ($action eq "view")
90
{
91
    view();
92
}
93 94
elsif ($action eq "interdiff")
{
95
    interdiff();
96 97 98
}
elsif ($action eq "diff")
{
99
    diff();
100
}
101 102
elsif ($action eq "viewall") 
{ 
103
    viewall(); 
104
}
105 106
elsif ($action eq "enter") 
{ 
107 108
    Bugzilla->login(LOGIN_REQUIRED);
    enter(); 
109 110 111
}
elsif ($action eq "insert")
{
112 113
    Bugzilla->login(LOGIN_REQUIRED);
    insert();
114
}
115 116
elsif ($action eq "edit") 
{ 
117
    edit(); 
118 119 120
}
elsif ($action eq "update") 
{ 
121 122
    Bugzilla->login(LOGIN_REQUIRED);
    update();
123
}
124 125 126
elsif ($action eq "delete") {
    delete_attachment();
}
127 128
else 
{ 
129
  ThrowCodeError("unknown_action", { action => $action });
130 131 132 133 134 135 136 137
}

exit;

################################################################################
# Data Validation / Security Authorization
################################################################################

138 139 140
# Validates an attachment ID. Optionally takes a parameter of a form
# variable name that contains the ID to be validated. If not specified,
# uses 'id'.
141 142
# If the second parameter is true, the attachment ID will be validated,
# however the current user's access to the attachment will not be checked.
143 144 145 146
# Will throw an error if 1) attachment ID is not a valid number,
# 2) attachment does not exist, or 3) user isn't allowed to access the
# attachment.
#
147 148 149
# Returns an attachment object.

sub validateID {
150 151
    my($param, $dont_validate_access) = @_;
    $param ||= 'id';
152

153 154 155
    # If we're not doing interdiffs, check if id wasn't specified and
    # prompt them with a page that allows them to choose an attachment.
    # Happens when calling plain attachment.cgi from the urlbar directly
156
    if ($param eq 'id' && !$cgi->param('id')) {
157
        print $cgi->header();
158 159 160 161
        $template->process("attachment/choose.html.tmpl", $vars) ||
            ThrowTemplateError($template->error());
        exit;
    }
162
    
163 164 165 166 167 168 169
    my $attach_id = $cgi->param($param);

    # Validate the specified attachment id. detaint kills $attach_id if
    # non-natural, so use the original value from $cgi in our exception
    # message here.
    detaint_natural($attach_id)
     || ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) });
170
  
171
    # Make sure the attachment exists in the database.
172
    my $attachment = new Bugzilla::Attachment($attach_id)
173
      || ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
174

175 176 177 178 179 180 181 182
    return $attachment if ($dont_validate_access || check_can_access($attachment));
}

# Make sure the current user has access to the specified attachment.
sub check_can_access {
    my $attachment = shift;
    my $user = Bugzilla->user;

183
    # Make sure the user is authorized to access this attachment's bug.
184 185 186 187
    Bugzilla::Bug->check($attachment->bug_id);
    if ($attachment->isprivate && $user->id != $attachment->attacher->id 
        && !$user->is_insider) 
    {
188 189
        ThrowUserError('auth_failure', {action => 'access',
                                        object => 'attachment'});
190
    }
191 192 193 194 195 196 197 198 199 200 201 202 203
    return 1;
}

# Determines if the attachment is public -- that is, if users who are
# not logged in have access to the attachment
sub attachmentIsPublic {
    my $attachment = shift;

    return 0 if Bugzilla->params->{'requirelogin'};
    return 0 if $attachment->isprivate;

    my $anon_user = new Bugzilla::User;
    return $anon_user->can_see_bug($attachment->bug_id);
204 205
}

206 207 208
# Validates format of a diff/interdiff. Takes a list as an parameter, which
# defines the valid format values. Will throw an error if the format is not
# in the list. Returns either the user selected or default format.
209 210
sub validateFormat
{
211 212 213
  # receives a list of legal formats; first item is a default
  my $format = $cgi->param('format') || $_[0];
  if ( lsearch(\@_, $format) == -1)
214
  {
215
     ThrowUserError("invalid_format", { format  => $format, formats => \@_ });
216
  }
217

218
  return $format;
219 220
}

221 222
# Validates context of a diff/interdiff. Will throw an error if the context
# is not number, "file" or "patch". Returns the validated, detainted context.
223 224
sub validateContext
{
225 226 227 228
  my $context = $cgi->param('context') || "patch";
  if ($context ne "file" && $context ne "patch") {
    detaint_natural($context)
      || ThrowUserError("invalid_context", { context => $cgi->param('context') });
229
  }
230 231

  return $context;
232 233
}

234 235 236 237
################################################################################
# Functions
################################################################################

238
# Display an attachment.
239
sub view {
240 241 242 243 244
    my $attachment;

    if (use_attachbase()) {
        $attachment = validateID(undef, 1);
        my $path = 'attachment.cgi?id=' . $attachment->id;
245 246 247 248
        # The user is allowed to override the content type of the attachment.
        if (defined $cgi->param('content_type')) {
            $path .= '&content_type=' . url_quote($cgi->param('content_type'));
        }
249 250

        # Make sure the attachment is served from the correct server.
251 252 253 254 255
        my $bug_id = $attachment->bug_id;
        if (!$cgi->url_is_attachment_base($bug_id)) {
            # We couldn't call Bugzilla->login earlier as we first had to 
            # make sure we were not going to request credentials on the
            # alternate host.
256
            Bugzilla->login();
257 258 259 260
            my $attachbase = Bugzilla->params->{'attachment_base'};
            # Replace %bugid% by the ID of the bug the attachment 
            # belongs to, if present.
            $attachbase =~ s/\%bugid\%/$bug_id/;
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
            if (attachmentIsPublic($attachment)) {
                # No need for a token; redirect to attachment base.
                print $cgi->redirect(-location => $attachbase . $path);
                exit;
            } else {
                # Make sure the user can view the attachment.
                check_can_access($attachment);
                # Create a token and redirect.
                my $token = url_quote(issue_session_token($attachment->id));
                print $cgi->redirect(-location => $attachbase . "$path&t=$token");
                exit;
            }
        } else {
            # No need to validate the token for public attachments. We cannot request
            # credentials as we are on the alternate host.
            if (!attachmentIsPublic($attachment)) {
                my $token = $cgi->param('t');
                my ($userid, undef, $token_attach_id) = Bugzilla::Token::GetTokenData($token);
                unless ($userid
                        && detaint_natural($token_attach_id)
                        && ($token_attach_id == $attachment->id))
                {
                    # Not a valid token.
                    print $cgi->redirect('-location' => correct_urlbase() . $path);
                    exit;
                }
                # Change current user without creating cookies.
                Bugzilla->set_user(new Bugzilla::User($userid));
                # Tokens are single use only, delete it.
                delete_token($token);
            }
        }
    } else {
294
        do_ssl_redirect_if_required();
295 296 297 298 299 300
        # No alternate host is used. Request credentials if required.
        Bugzilla->login();
        $attachment = validateID();
    }

    # At this point, Bugzilla->login has been called if it had to.
301 302 303
    my $contenttype = $attachment->contenttype;
    my $filename = $attachment->filename;

304 305
    # Bug 111522: allow overriding content-type manually in the posted form
    # params.
306 307
    if (defined $cgi->param('content_type')) {
        $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
308
    }
309

310
    # Return the appropriate HTTP response headers.
311
    $attachment->datasize || ThrowUserError("attachment_removed");
312

313
    $filename =~ s/^.*[\/\\]//;
314 315 316 317
    # escape quotes and backslashes in the filename, per RFCs 2045/822
    $filename =~ s/\\/\\\\/g; # escape backslashes
    $filename =~ s/"/\\"/g; # escape quotes

318 319 320 321 322
    # Avoid line wrapping done by Encode, which we don't need for HTTP
    # headers. See discussion in bug 328628 for details.
    local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 10000;
    $filename = encode('MIME-Q', $filename);

323 324
    my $disposition = Bugzilla->params->{'allow_attachment_display'} ? 'inline' : 'attachment';

325
    print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
326
                       -content_disposition=> "$disposition; filename=\"$filename\"",
327
                       -content_length => $attachment->datasize);
328
    disable_utf8();
329
    print $attachment->data;
330 331
}

332 333
sub interdiff {
    # Retrieve and validate parameters
334 335
    my $old_attachment = validateID('oldid');
    my $new_attachment = validateID('newid');
336 337 338 339 340
    my $format = validateFormat('html', 'raw');
    my $context = validateContext();

    Bugzilla::Attachment::PatchReader::process_interdiff(
        $old_attachment, $new_attachment, $format, $context);
341 342
}

343 344
sub diff {
    # Retrieve and validate parameters
345
    my $attachment = validateID();
346 347
    my $format = validateFormat('html', 'raw');
    my $context = validateContext();
348

349 350 351 352
    # If it is not a patch, view normally.
    if (!$attachment->ispatch) {
        view();
        return;
353 354
    }

355
    Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
356
}
357

358 359
# Display all attachments for a given bug in a series of IFRAMEs within one
# HTML page.
360
sub viewall {
361
    # Retrieve and validate parameters
362 363
    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
    my $bugid = $bug->id;
364

365
    my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
366

367 368 369
    # Define the variables and functions that will be passed to the UI template.
    $vars->{'bug'} = $bug;
    $vars->{'attachments'} = $attachments;
370

371
    print $cgi->header();
372

373 374 375
    # Generate and return the UI (HTML page) from the appropriate template.
    $template->process("attachment/show-multiple.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
376 377
}

378
# Display a form for entering a new attachment.
379
sub enter {
380
  # Retrieve and validate parameters
381 382
  my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
  my $bugid = $bug->id;
383
  Bugzilla::Attachment->_check_bug($bug);
384
  my $dbh = Bugzilla->dbh;
385 386
  my $user = Bugzilla->user;

387 388 389
  # Retrieve the attachments the user can edit from the database and write
  # them into an array of hashes where each hash represents one attachment.
  my $canEdit = "";
390 391
  if (!$user->in_group('editbugs', $bug->product_id)) {
      $canEdit = "AND submitter_id = " . $user->id;
392
  }
393 394 395
  my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
                                             WHERE bug_id = ? AND isobsolete = 0 $canEdit
                                             ORDER BY attach_id", undef, $bugid);
396 397

  # Define the variables and functions that will be passed to the UI template.
398
  $vars->{'bug'} = $bug;
399
  $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
400

401
  my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
402 403
                                              'product_id'   => $bug->product_id,
                                              'component_id' => $bug->component_id});
404
  $vars->{'flag_types'} = $flag_types;
405 406
  $vars->{'any_flags_requesteeble'} =
    grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
407
  $vars->{'token'} = issue_session_token('create_attachment:');
408

409
  print $cgi->header();
410 411

  # Generate and return the UI (HTML page) from the appropriate template.
412 413
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
414 415
}

416
# Insert a new attachment into the database.
417
sub insert {
418 419
    my $dbh = Bugzilla->dbh;
    my $user = Bugzilla->user;
420

421 422
    $dbh->bz_start_transaction;

423
    # Retrieve and validate parameters
424 425
    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
    my $bugid = $bug->id;
426
    my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
427

428 429 430 431 432 433
    # Detect if the user already used the same form to submit an attachment
    my $token = trim($cgi->param('token'));
    if ($token) {
        my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token);
        unless ($creator_id 
            && ($creator_id == $user->id) 
434
                && ($old_attach_id =~ "^create_attachment:")) 
435 436 437 438 439
        {
            # The token is invalid.
            ThrowUserError('token_does_not_exist');
        }
    
440
        $old_attach_id =~ s/^create_attachment://;
441 442 443 444 445 446 447 448 449 450 451
   
        if ($old_attach_id) {
            $vars->{'bugid'} = $bugid;
            $vars->{'attachid'} = $old_attach_id;
            print $cgi->header();
            $template->process("attachment/cancel-create-dupe.html.tmpl",  $vars)
                || ThrowTemplateError($template->error());
            exit;
        }
    }

452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
    # Check attachments the user tries to mark as obsolete.
    my @obsolete_attachments;
    if ($cgi->param('obsolete')) {
        my @obsolete = $cgi->param('obsolete');
        @obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
    }

    # Must be called before create() as it may alter $cgi->param('ispatch').
    my $content_type = Bugzilla::Attachment::get_content_type();

    my $attachment = Bugzilla::Attachment->create(
        {bug           => $bug,
         creation_ts   => $timestamp,
         data          => scalar $cgi->param('attachurl') || $cgi->upload('data'),
         description   => scalar $cgi->param('description'),
         filename      => $cgi->param('attachurl') ? '' : scalar $cgi->upload('data'),
         ispatch       => scalar $cgi->param('ispatch'),
         isprivate     => scalar $cgi->param('isprivate'),
         isurl         => scalar $cgi->param('attachurl'),
         mimetype      => $content_type,
         store_in_file => scalar $cgi->param('bigfile'),
         });

    foreach my $obsolete_attachment (@obsolete_attachments) {
476 477
        $obsolete_attachment->set_is_obsolete(1);
        $obsolete_attachment->update($timestamp);
478
    }
479

480 481 482 483 484
    my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
                                  $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
    $attachment->set_flags($flags, $new_flags);
    $attachment->update($timestamp);

485 486 487 488
    # Insert a comment about the new attachment into the database.
    my $comment = "Created an attachment (id=" . $attachment->id . ")\n" .
                  $attachment->description . "\n";
    $comment .= ("\n" . $cgi->param('comment')) if defined $cgi->param('comment');
489

490
    $bug->add_comment($comment, { isprivate => $attachment->isprivate });
491

492
  # Assign the bug to the user, if they are allowed to take it
493
  my $owner = "";
494
  if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
495 496 497 498 499 500 501 502 503 504
      # When taking a bug, we have to follow the workflow.
      my $bug_status = $cgi->param('bug_status') || '';
      ($bug_status) = grep {$_->name eq $bug_status} @{$bug->status->can_change_to};

      if ($bug_status && $bug_status->is_open
          && ($bug_status->name ne 'UNCONFIRMED' || $bug->product_obj->votes_to_confirm))
      {
          $bug->set_status($bug_status->name);
          $bug->clear_resolution();
      }
505
      # Make sure the person we are taking the bug from gets mail.
506
      $owner = $bug->assigned_to->login;
507
      $bug->set_assigned_to($user);
508
  }
509 510
  $bug->update($timestamp);

511 512 513
  if ($token) {
      trick_taint($token);
      $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
514
               ("create_attachment:" . $attachment->id, $token));
515 516
  }

517
  $dbh->bz_commit_transaction;
518

519
  # Define the variables and functions that will be passed to the UI template.
520
  $vars->{'mailrecipients'} =  { 'changer' => $user->login,
521
                                 'owner'   => $owner };
522
  $vars->{'attachment'} = $attachment;
523 524 525 526
  # We cannot reuse the $bug object as delta_ts has eventually been updated
  # since the object was created.
  $vars->{'bugs'} = [new Bugzilla::Bug($bugid)];
  $vars->{'header_done'} = 1;
527
  $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
528

529
  print $cgi->header();
530
  # Generate and return the UI (HTML page) from the appropriate template.
531 532
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
533 534
}

535 536 537 538
# Displays a form for editing attachment properties.
# Any user is allowed to access this page, unless the attachment
# is private and the user does not belong to the insider group.
# Validations are done later when the user submits changes.
539
sub edit {
540
  my $attachment = validateID();
541

542
  my $bugattachments =
543 544 545
      Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
  # We only want attachment IDs.
  @$bugattachments = map { $_->id } @$bugattachments;
546

547 548 549 550 551 552
  my $any_flags_requesteeble =
    grep { $_->is_requestable && $_->is_requesteeble } @{$attachment->flag_types};
  # Useful in case a flagtype is no longer requestable but a requestee
  # has been set before we turned off that bit.
  $any_flags_requesteeble ||= grep { $_->requestee_id } @{$attachment->flags};
  $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble;
553
  $vars->{'attachment'} = $attachment;
554
  $vars->{'attachments'} = $bugattachments;
555

556
  print $cgi->header();
557 558

  # Generate and return the UI (HTML page) from the appropriate template.
559 560
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
561 562
}

563 564
# Updates an attachment record. Only users with "editbugs" privileges,
# (or the original attachment's submitter) can edit the attachment.
565
# Users cannot edit the content of the attachment itself.
566
sub update {
567 568
    my $user = Bugzilla->user;
    my $dbh = Bugzilla->dbh;
569

570 571 572
    # Start a transaction in preparation for updating the attachment.
    $dbh->bz_start_transaction();

573
    # Retrieve and validate parameters
574
    my $attachment = validateID();
575 576
    my $bug = $attachment->bug;
    $attachment->_check_bug;
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609
    my $can_edit = $attachment->validate_can_edit($bug->product_id);

    if ($can_edit) {
        $attachment->set_description(scalar $cgi->param('description'));
        $attachment->set_is_patch(scalar $cgi->param('ispatch'));
        $attachment->set_content_type(scalar $cgi->param('contenttypeentry'));
        $attachment->set_is_obsolete(scalar $cgi->param('isobsolete'));
        $attachment->set_is_private(scalar $cgi->param('isprivate'));
        $attachment->set_filename(scalar $cgi->param('filename'));

        # Now make sure the attachment has not been edited since we loaded the page.
        if (defined $cgi->param('delta_ts')
            && $cgi->param('delta_ts') ne $attachment->modification_time)
        {
            ($vars->{'operations'}) =
                Bugzilla::Bug::GetBugActivity($bug->id, $attachment->id, $cgi->param('delta_ts'));

            # The token contains the old modification_time. We need a new one.
            $cgi->param('token', issue_hash_token([$attachment->id, $attachment->modification_time]));

            # If the modification date changed but there is no entry in
            # the activity table, this means someone commented only.
            # In this case, there is no reason to midair.
            if (scalar(@{$vars->{'operations'}})) {
                $cgi->param('delta_ts', $attachment->modification_time);
                $vars->{'attachment'} = $attachment;

                print $cgi->header();
                # Warn the user about the mid-air collision and ask them what to do.
                $template->process("attachment/midair.html.tmpl", $vars)
                  || ThrowTemplateError($template->error());
                exit;
            }
610 611
        }
    }
612 613 614 615 616 617

    # We couldn't do this check earlier as we first had to validate attachment ID
    # and display the mid-air collision page if modification_time changed.
    my $token = $cgi->param('token');
    check_hash_token($token, [$attachment->id, $attachment->modification_time]);

618 619 620 621 622 623 624 625
    # If the user submitted a comment while editing the attachment,
    # add the comment to the bug. Do this after having validated isprivate!
    if ($cgi->param('comment')) {
        # Prepend a string to the comment to let users know that the comment came
        # from the "edit attachment" screen.
        my $comment = "(From update of attachment " . $attachment->id . ")\n" .
                      $cgi->param('comment');

626
        $bug->add_comment($comment, { isprivate => $attachment->isprivate });
627 628
    }

629 630 631 632 633
    if ($can_edit) {
        my ($flags, $new_flags) =
          Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
        $attachment->set_flags($flags, $new_flags);
    }
634

635
    # Figure out when the changes were made.
636
    my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
637

638 639 640 641 642 643 644
    if ($can_edit) {
        my $changes = $attachment->update($timestamp);
        # If there are changes, we updated delta_ts in the DB. We have to
        # reflect this change in the bug object.
        $bug->{delta_ts} = $timestamp if scalar(keys %$changes);
    }

645
    # Commit the comment, if any.
646
    $bug->update($timestamp);
647

648 649
    # Commit the transaction now that we are finished updating the database.
    $dbh->bz_commit_transaction();
650

651 652 653 654 655
    # Define the variables and functions that will be passed to the UI template.
    $vars->{'mailrecipients'} = { 'changer' => $user->login };
    $vars->{'attachment'} = $attachment;
    $vars->{'bugs'} = [$bug];
    $vars->{'header_done'} = 1;
656

657
    print $cgi->header();
658

659 660 661
    # Generate and return the UI (HTML page) from the appropriate template.
    $template->process("attachment/updated.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
662
}
663 664 665 666 667 668 669 670 671 672 673 674 675

# Only administrators can delete attachments.
sub delete_attachment {
    my $user = Bugzilla->login(LOGIN_REQUIRED);
    my $dbh = Bugzilla->dbh;

    print $cgi->header();

    $user->in_group('admin')
      || ThrowUserError('auth_failure', {group  => 'admin',
                                         action => 'delete',
                                         object => 'attachment'});

676
    Bugzilla->params->{'allow_attachment_deletion'}
677 678 679
      || ThrowUserError('attachment_deletion_disabled');

    # Make sure the administrator is allowed to edit this attachment.
680
    my $attachment = validateID();
681
    Bugzilla::Attachment->_check_bug($attachment->bug);
682 683 684 685 686 687 688 689 690

    $attachment->datasize || ThrowUserError('attachment_removed');

    # We don't want to let a malicious URL accidentally delete an attachment.
    my $token = trim($cgi->param('token'));
    if ($token) {
        my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
        unless ($creator_id
                  && ($creator_id == $user->id)
691
                  && ($event eq 'delete_attachment' . $attachment->id))
692 693
        {
            # The token is invalid.
694
            ThrowUserError('token_does_not_exist');
695 696
        }

697 698
        my $bug = new Bugzilla::Bug($attachment->bug_id);

699 700
        # The token is valid. Delete the content of the attachment.
        my $msg;
701
        $vars->{'attachment'} = $attachment;
702 703 704 705 706 707 708
        $vars->{'date'} = $date;
        $vars->{'reason'} = clean_text($cgi->param('reason') || '');
        $vars->{'mailrecipients'} = { 'changer' => $user->login };

        $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
          || ThrowTemplateError($template->error());

709 710 711
        # Paste the reason provided by the admin into a comment.
        $bug->add_comment($msg);

712 713 714 715
        # If the attachment is stored locally, remove it.
        if (-e $attachment->_get_local_filename) {
            unlink $attachment->_get_local_filename;
        }
716
        $attachment->remove_from_db();
717 718

        # Now delete the token.
719
        delete_token($token);
720

721 722
        # Insert the comment.
        $bug->update();
723

724
        # Required to display the bug the deleted attachment belongs to.
725
        $vars->{'bugs'} = [$bug];
726 727
        $vars->{'header_done'} = 1;

728 729 730 731 732
        $template->process("attachment/updated.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
    }
    else {
        # Create a token.
733
        $token = issue_session_token('delete_attachment' . $attachment->id);
734 735 736 737 738 739 740 741

        $vars->{'a'} = $attachment;
        $vars->{'token'} = $token;

        $template->process("attachment/confirm-delete.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
    }
}