attachment.cgi 27.1 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
        my $bug_id = $attachment->bug_id;
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
        if ($cgi->url_is_attachment_base($bug_id)) {
            # 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);
            }
        }
        elsif ($cgi->url_is_attachment_base) {
            # If we come here, this means that each bug has its own host
            # for attachments, and that we are trying to view one attachment
            # using another bug's host. That's not desired.
            $cgi->redirect_to_urlbase;
        }
        else {
            # We couldn't call Bugzilla->login earlier as we first had to
280 281
            # make sure we were not going to request credentials on the
            # alternate host.
282
            Bugzilla->login();
283 284 285 286
            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/;
287 288 289 290 291 292 293 294 295 296 297 298 299 300
            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 {
301
        do_ssl_redirect_if_required();
302 303 304 305 306 307
        # 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.
308 309 310
    my $contenttype = $attachment->contenttype;
    my $filename = $attachment->filename;

311 312
    # Bug 111522: allow overriding content-type manually in the posted form
    # params.
313 314
    if (defined $cgi->param('content_type')) {
        $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
315
    }
316

317
    # Return the appropriate HTTP response headers.
318
    $attachment->datasize || ThrowUserError("attachment_removed");
319

320
    $filename =~ s/^.*[\/\\]//;
321 322 323 324
    # escape quotes and backslashes in the filename, per RFCs 2045/822
    $filename =~ s/\\/\\\\/g; # escape backslashes
    $filename =~ s/"/\\"/g; # escape quotes

325 326 327 328 329
    # 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);

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

332
    print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
333
                       -content_disposition=> "$disposition; filename=\"$filename\"",
334
                       -content_length => $attachment->datasize);
335
    disable_utf8();
336
    print $attachment->data;
337 338
}

339 340
sub interdiff {
    # Retrieve and validate parameters
341 342
    my $old_attachment = validateID('oldid');
    my $new_attachment = validateID('newid');
343 344 345 346 347
    my $format = validateFormat('html', 'raw');
    my $context = validateContext();

    Bugzilla::Attachment::PatchReader::process_interdiff(
        $old_attachment, $new_attachment, $format, $context);
348 349
}

350 351
sub diff {
    # Retrieve and validate parameters
352
    my $attachment = validateID();
353 354
    my $format = validateFormat('html', 'raw');
    my $context = validateContext();
355

356 357 358 359
    # If it is not a patch, view normally.
    if (!$attachment->ispatch) {
        view();
        return;
360 361
    }

362
    Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
363
}
364

365 366
# Display all attachments for a given bug in a series of IFRAMEs within one
# HTML page.
367
sub viewall {
368
    # Retrieve and validate parameters
369 370
    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
    my $bugid = $bug->id;
371

372
    my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
373

374 375 376
    # Define the variables and functions that will be passed to the UI template.
    $vars->{'bug'} = $bug;
    $vars->{'attachments'} = $attachments;
377

378
    print $cgi->header();
379

380 381 382
    # Generate and return the UI (HTML page) from the appropriate template.
    $template->process("attachment/show-multiple.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
383 384
}

385
# Display a form for entering a new attachment.
386
sub enter {
387
  # Retrieve and validate parameters
388 389
  my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
  my $bugid = $bug->id;
390
  Bugzilla::Attachment->_check_bug($bug);
391
  my $dbh = Bugzilla->dbh;
392 393
  my $user = Bugzilla->user;

394 395 396
  # 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 = "";
397 398
  if (!$user->in_group('editbugs', $bug->product_id)) {
      $canEdit = "AND submitter_id = " . $user->id;
399
  }
400 401 402
  my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
                                             WHERE bug_id = ? AND isobsolete = 0 $canEdit
                                             ORDER BY attach_id", undef, $bugid);
403 404

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

408
  my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
409 410
                                              'product_id'   => $bug->product_id,
                                              'component_id' => $bug->component_id});
411
  $vars->{'flag_types'} = $flag_types;
412 413
  $vars->{'any_flags_requesteeble'} =
    grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
414
  $vars->{'token'} = issue_session_token('create_attachment:');
415

416
  print $cgi->header();
417 418

  # Generate and return the UI (HTML page) from the appropriate template.
419 420
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
421 422
}

423
# Insert a new attachment into the database.
424
sub insert {
425 426
    my $dbh = Bugzilla->dbh;
    my $user = Bugzilla->user;
427

428 429
    $dbh->bz_start_transaction;

430
    # Retrieve and validate parameters
431 432
    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
    my $bugid = $bug->id;
433
    my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
434

435 436 437 438 439 440
    # 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) 
441
                && ($old_attach_id =~ "^create_attachment:")) 
442 443 444 445 446
        {
            # The token is invalid.
            ThrowUserError('token_does_not_exist');
        }
    
447
        $old_attach_id =~ s/^create_attachment://;
448 449 450 451 452 453 454 455 456 457 458
   
        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;
        }
    }

459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
    # 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) {
483 484
        $obsolete_attachment->set_is_obsolete(1);
        $obsolete_attachment->update($timestamp);
485
    }
486

487 488 489 490 491
    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);

492
    # Insert a comment about the new attachment into the database.
493 494 495 496
    my $comment = $cgi->param('comment');
    $bug->add_comment($comment, { isprivate => $attachment->isprivate,
                                  type => CMT_ATTACHMENT_CREATED,
                                  extra_data => $attachment->id });
497

498
  # Assign the bug to the user, if they are allowed to take it
499
  my $owner = "";
500
  if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
501 502 503 504 505
      # 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
506 507
          && ($bug_status->name ne 'UNCONFIRMED' 
              || $bug->product_obj->allows_unconfirmed))
508 509 510 511
      {
          $bug->set_status($bug_status->name);
          $bug->clear_resolution();
      }
512
      # Make sure the person we are taking the bug from gets mail.
513
      $owner = $bug->assigned_to->login;
514
      $bug->set_assigned_to($user);
515
  }
516 517
  $bug->update($timestamp);

518 519 520
  if ($token) {
      trick_taint($token);
      $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
521
               ("create_attachment:" . $attachment->id, $token));
522 523
  }

524
  $dbh->bz_commit_transaction;
525

526
  # Define the variables and functions that will be passed to the UI template.
527
  $vars->{'mailrecipients'} =  { 'changer' => $user->login,
528
                                 'owner'   => $owner };
529
  $vars->{'attachment'} = $attachment;
530 531 532 533
  # 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;
534
  $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
535

536
  print $cgi->header();
537
  # Generate and return the UI (HTML page) from the appropriate template.
538 539
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
540 541
}

542 543 544 545
# 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.
546
sub edit {
547
  my $attachment = validateID();
548

549
  my $bugattachments =
550 551 552
      Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
  # We only want attachment IDs.
  @$bugattachments = map { $_->id } @$bugattachments;
553

554 555 556 557 558 559
  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;
560
  $vars->{'attachment'} = $attachment;
561
  $vars->{'attachments'} = $bugattachments;
562

563
  print $cgi->header();
564 565

  # Generate and return the UI (HTML page) from the appropriate template.
566 567
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
568 569
}

570 571
# Updates an attachment record. Only users with "editbugs" privileges,
# (or the original attachment's submitter) can edit the attachment.
572
# Users cannot edit the content of the attachment itself.
573
sub update {
574 575
    my $user = Bugzilla->user;
    my $dbh = Bugzilla->dbh;
576

577 578 579
    # Start a transaction in preparation for updating the attachment.
    $dbh->bz_start_transaction();

580
    # Retrieve and validate parameters
581
    my $attachment = validateID();
582 583
    my $bug = $attachment->bug;
    $attachment->_check_bug;
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 610 611 612 613 614 615 616
    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;
            }
617 618
        }
    }
619 620 621 622 623 624

    # 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]);

625 626
    # If the user submitted a comment while editing the attachment,
    # add the comment to the bug. Do this after having validated isprivate!
627 628 629 630 631
    my $comment = $cgi->param('comment');
    if (trim($comment)) {
        $bug->add_comment($comment, { isprivate => $attachment->isprivate,
                                      type => CMT_ATTACHMENT_UPDATED,
                                      extra_data => $attachment->id });
632 633
    }

634 635 636 637 638
    if ($can_edit) {
        my ($flags, $new_flags) =
          Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
        $attachment->set_flags($flags, $new_flags);
    }
639

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

643 644 645 646 647 648 649
    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);
    }

650
    # Commit the comment, if any.
651
    $bug->update($timestamp);
652

653 654
    # Commit the transaction now that we are finished updating the database.
    $dbh->bz_commit_transaction();
655

656 657 658 659 660
    # 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;
661

662
    print $cgi->header();
663

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

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

681
    Bugzilla->params->{'allow_attachment_deletion'}
682 683 684
      || ThrowUserError('attachment_deletion_disabled');

    # Make sure the administrator is allowed to edit this attachment.
685
    my $attachment = validateID();
686
    Bugzilla::Attachment->_check_bug($attachment->bug);
687 688 689 690 691 692 693 694 695

    $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)
696
                  && ($event eq 'delete_attachment' . $attachment->id))
697 698
        {
            # The token is invalid.
699
            ThrowUserError('token_does_not_exist');
700 701
        }

702 703
        my $bug = new Bugzilla::Bug($attachment->bug_id);

704 705
        # The token is valid. Delete the content of the attachment.
        my $msg;
706
        $vars->{'attachment'} = $attachment;
707 708 709 710 711 712 713
        $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());

714 715 716
        # Paste the reason provided by the admin into a comment.
        $bug->add_comment($msg);

717 718 719 720
        # If the attachment is stored locally, remove it.
        if (-e $attachment->_get_local_filename) {
            unlink $attachment->_get_local_filename;
        }
721
        $attachment->remove_from_db();
722 723

        # Now delete the token.
724
        delete_token($token);
725

726 727
        # Insert the comment.
        $bug->update();
728

729
        # Required to display the bug the deleted attachment belongs to.
730
        $vars->{'bugs'} = [$bug];
731 732
        $vars->{'header_done'} = 1;

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

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

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