attachment.cgi 30.6 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::BugMail;
43
use Bugzilla::Constants;
44
use Bugzilla::Error;
45 46
use Bugzilla::Flag; 
use Bugzilla::FlagType; 
47
use Bugzilla::User;
48
use Bugzilla::Util;
49
use Bugzilla::Bug;
50
use Bugzilla::Field;
51
use Bugzilla::Attachment;
52
use Bugzilla::Attachment::PatchReader;
53
use Bugzilla::Token;
54
use Bugzilla::Keyword;
55

56
use Encode qw(encode find_encoding);
57

58 59 60 61 62 63 64
# 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 = {};
65

66 67 68 69
################################################################################
# Main Body Execution
################################################################################

70 71 72 73
# 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'.
74 75

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

79
# You must use the appropriate urlbase/sslbase param when doing anything
80 81 82 83
# but viewing an attachment, or a raw diff.
if ($action ne 'view'
    && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw'))
{
84 85
    do_ssl_redirect_if_required();
    if ($cgi->url_is_attachment_base) {
86 87 88 89 90 91 92 93
        $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")
94
{
95
    view();
96
}
97 98
elsif ($action eq "interdiff")
{
99
    interdiff();
100 101 102
}
elsif ($action eq "diff")
{
103
    diff();
104
}
105 106
elsif ($action eq "viewall") 
{ 
107
    viewall(); 
108
}
109 110
elsif ($action eq "enter") 
{ 
111 112
    Bugzilla->login(LOGIN_REQUIRED);
    enter(); 
113 114 115
}
elsif ($action eq "insert")
{
116 117
    Bugzilla->login(LOGIN_REQUIRED);
    insert();
118
}
119 120
elsif ($action eq "edit") 
{ 
121
    edit(); 
122 123 124
}
elsif ($action eq "update") 
{ 
125 126
    Bugzilla->login(LOGIN_REQUIRED);
    update();
127
}
128 129 130
elsif ($action eq "delete") {
    delete_attachment();
}
131 132
else 
{ 
133
  ThrowUserError('unknown_action', {action => $action});
134 135 136 137 138 139 140 141
}

exit;

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

142 143 144
# 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'.
145 146
# 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.
147 148 149 150
# 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.
#
151 152 153
# Returns an attachment object.

sub validateID {
154 155
    my($param, $dont_validate_access) = @_;
    $param ||= 'id';
156

157 158 159
    # 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
160
    if ($param eq 'id' && !$cgi->param('id')) {
161
        print $cgi->header();
162 163 164 165
        $template->process("attachment/choose.html.tmpl", $vars) ||
            ThrowTemplateError($template->error());
        exit;
    }
166
    
167 168 169 170 171 172
    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)
173 174
        || ThrowUserError("invalid_attach_id",
                          { attach_id => scalar $cgi->param($param) });
175
  
176
    # Make sure the attachment exists in the database.
177
    my $attachment = new Bugzilla::Attachment($attach_id)
178
        || ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
179

180 181 182 183 184 185 186 187
    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;

188
    # Make sure the user is authorized to access this attachment's bug.
189 190 191 192
    Bugzilla::Bug->check($attachment->bug_id);
    if ($attachment->isprivate && $user->id != $attachment->attacher->id 
        && !$user->is_insider) 
    {
193
        ThrowUserError('auth_failure', {action => 'access',
194 195
                                        object => 'attachment',
                                        attach_id => $attachment->id});
196
    }
197 198 199 200 201 202 203 204 205 206 207 208 209
    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);
210 211
}

212 213 214
# 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.
215
sub validateFormat {
216 217
  # receives a list of legal formats; first item is a default
  my $format = $cgi->param('format') || $_[0];
218
  if (not grep($_ eq $format, @_)) {
219
     ThrowUserError("invalid_format", { format  => $format, formats => \@_ });
220
  }
221

222
  return $format;
223 224
}

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

  return $context;
236 237
}

238 239 240 241
# Gets the attachment object(s) generated by validateID, while ensuring
# attachbase and token authentication is used when required.
sub get_attachment {
    my @field_names = @_ ? @_ : qw(id);
242

243
    my %attachments;
244 245

    if (use_attachbase()) {
246 247 248 249 250 251 252 253 254 255
        # Load each attachment, and ensure they are all from the same bug
        my $bug_id = 0;
        foreach my $field_name (@field_names) {
            my $attachment = validateID($field_name, 1);
            if (!$bug_id) {
                $bug_id = $attachment->bug_id;
            } elsif ($attachment->bug_id != $bug_id) {
                ThrowUserError('attachment_bug_id_mismatch');
            }
            $attachments{$field_name} = $attachment;
256
        }
257 258 259 260 261
        my @args = map { $_ . '=' . $attachments{$_}->id } @field_names;
        my $cgi_params = $cgi->canonicalise_query(@field_names, 't',
            'Bugzilla_login', 'Bugzilla_password');
        push(@args, $cgi_params) if $cgi_params;
        my $path = 'attachment.cgi?' . join('&', @args);
262 263

        # Make sure the attachment is served from the correct server.
264 265 266
        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.
267
            if (!all_attachments_are_public(\%attachments)) {
268
                my $token = $cgi->param('t');
269 270 271 272 273 274 275 276 277 278 279 280 281 282
                my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token);
                my %token_data = unpack_token_data($token_data);
                my $valid_token = 1;
                foreach my $field_name (@field_names) {
                    my $token_id = $token_data{$field_name};
                    if (!$token_id
                        || !detaint_natural($token_id)
                        || $attachments{$field_name}->id != $token_id)
                    {
                        $valid_token = 0;
                        last;
                    }
                }
                unless ($userid && $valid_token) {
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
                    # 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
301 302
            # make sure we were not going to request credentials on the
            # alternate host.
303
            Bugzilla->login();
304 305 306 307
            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/;
308
            if (all_attachments_are_public(\%attachments)) {
309 310 311 312 313
                # 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.
314 315 316
                foreach my $field_name (@field_names) {
                    check_can_access($attachments{$field_name});
                }
317
                # Create a token and redirect.
318
                my $token = url_quote(issue_session_token(pack_token_data(\%attachments)));
319 320 321 322 323
                print $cgi->redirect(-location => $attachbase . "$path&t=$token");
                exit;
            }
        }
    } else {
324
        do_ssl_redirect_if_required();
325 326
        # No alternate host is used. Request credentials if required.
        Bugzilla->login();
327 328 329
        foreach my $field_name (@field_names) {
            $attachments{$field_name} = validateID($field_name);
        }
330 331
    }

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
    return wantarray
        ? map { $attachments{$_} } @field_names
        : $attachments{$field_names[0]};
}

sub all_attachments_are_public {
    my $attachments = shift;
    foreach my $field_name (keys %$attachments) {
        if (!attachmentIsPublic($attachments->{$field_name})) {
            return 0;
        }
    }
    return 1;
}

sub pack_token_data {
    my $attachments = shift;
    return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments);
}

sub unpack_token_data {
    my @token_data = split(/ /, shift || '');
    my %data;
    foreach my $token (@token_data) {
        my ($field_name, $attach_id) = split('=', $token);
        $data{$field_name} = $attach_id;
    }
    return %data;
}

################################################################################
# Functions
################################################################################

# Display an attachment.
sub view {
    my $attachment = get_attachment();

370
    # At this point, Bugzilla->login has been called if it had to.
371 372 373
    my $contenttype = $attachment->contenttype;
    my $filename = $attachment->filename;

374 375
    # Bug 111522: allow overriding content-type manually in the posted form
    # params.
376 377
    if (defined $cgi->param('content_type')) {
        $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
378
    }
379

380
    # Return the appropriate HTTP response headers.
381
    $attachment->datasize || ThrowUserError("attachment_removed");
382

383
    $filename =~ s/^.*[\/\\]//;
384 385 386 387
    # escape quotes and backslashes in the filename, per RFCs 2045/822
    $filename =~ s/\\/\\\\/g; # escape backslashes
    $filename =~ s/"/\\"/g; # escape quotes

388 389 390 391 392
    # 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);

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

395 396 397 398 399 400 401
    # Don't send a charset header with attachments--they might not be UTF-8.
    # However, we do allow people to explicitly specify a charset if they
    # want.
    if ($contenttype !~ /\bcharset=/i) {
        # In order to prevent Apache from adding a charset, we have to send a
        # charset that's a single space.
        $cgi->charset(' ');
402 403 404 405 406 407
        if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) {
            my $encoding = detect_encoding($attachment->data);
            if ($encoding) {
                $cgi->charset(find_encoding($encoding)->mime_name);
            }
        }
408
    }
409
    print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
410
                       -content_disposition=> "$disposition; filename=\"$filename\"",
411 412
                       -content_length => $attachment->datasize,
                       -x_content_type_options => "nosniff");
413
    disable_utf8();
414
    print $attachment->data;
415 416
}

417 418 419
sub interdiff {
    # Retrieve and validate parameters
    my $format = validateFormat('html', 'raw');
420 421 422 423 424 425 426
    my($old_attachment, $new_attachment);
    if ($format eq 'raw') {
        ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid');
    } else {
        $old_attachment = validateID('oldid');
        $new_attachment = validateID('newid');
    }
427 428 429 430
    my $context = validateContext();

    Bugzilla::Attachment::PatchReader::process_interdiff(
        $old_attachment, $new_attachment, $format, $context);
431 432
}

433 434 435
sub diff {
    # Retrieve and validate parameters
    my $format = validateFormat('html', 'raw');
436
    my $attachment = $format eq 'raw' ? get_attachment() : validateID();
437
    my $context = validateContext();
438

439 440 441 442
    # If it is not a patch, view normally.
    if (!$attachment->ispatch) {
        view();
        return;
443 444
    }

445
    Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
446
}
447

448 449
# Display all attachments for a given bug in a series of IFRAMEs within one
# HTML page.
450
sub viewall {
451
    # Retrieve and validate parameters
452 453
    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
    my $bugid = $bug->id;
454

455
    my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
456 457
    # Ignore deleted attachments.
    @$attachments = grep { $_->datasize } @$attachments;
458

459 460 461 462 463
    if ($cgi->param('hide_obsolete')) {
        @$attachments = grep { !$_->isobsolete } @$attachments;
        $vars->{'hide_obsolete'} = 1;
    }

464 465 466
    # Define the variables and functions that will be passed to the UI template.
    $vars->{'bug'} = $bug;
    $vars->{'attachments'} = $attachments;
467

468
    print $cgi->header();
469

470 471 472
    # Generate and return the UI (HTML page) from the appropriate template.
    $template->process("attachment/show-multiple.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
473 474
}

475
# Display a form for entering a new attachment.
476
sub enter {
477
  # Retrieve and validate parameters
478 479
  my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
  my $bugid = $bug->id;
480
  Bugzilla::Attachment->_check_bug($bug);
481
  my $dbh = Bugzilla->dbh;
482 483
  my $user = Bugzilla->user;

484 485 486
  # 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 = "";
487 488
  if (!$user->in_group('editbugs', $bug->product_id)) {
      $canEdit = "AND submitter_id = " . $user->id;
489
  }
490 491 492
  my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
                                             WHERE bug_id = ? AND isobsolete = 0 $canEdit
                                             ORDER BY attach_id", undef, $bugid);
493 494

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

498
  my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
499 500
                                              'product_id'   => $bug->product_id,
                                              'component_id' => $bug->component_id});
501
  $vars->{'flag_types'} = $flag_types;
502 503
  $vars->{'any_flags_requesteeble'} =
    grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
504
  $vars->{'token'} = issue_session_token('create_attachment:');
505

506
  print $cgi->header();
507 508

  # Generate and return the UI (HTML page) from the appropriate template.
509 510
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
511 512
}

513
# Insert a new attachment into the database.
514
sub insert {
515 516
    my $dbh = Bugzilla->dbh;
    my $user = Bugzilla->user;
517

518 519
    $dbh->bz_start_transaction;

520
    # Retrieve and validate parameters
521 522
    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
    my $bugid = $bug->id;
523
    my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
524

525 526 527 528 529 530
    # 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) 
531
                && ($old_attach_id =~ "^create_attachment:")) 
532 533 534 535 536
        {
            # The token is invalid.
            ThrowUserError('token_does_not_exist');
        }
    
537
        $old_attach_id =~ s/^create_attachment://;
538 539 540 541 542 543 544 545 546 547 548
   
        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;
        }
    }

549 550 551 552 553 554 555 556 557 558
    # 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();

559 560 561
    # Get the filehandle of the attachment.
    my $data_fh = $cgi->upload('data');

562 563 564
    my $attachment = Bugzilla::Attachment->create(
        {bug           => $bug,
         creation_ts   => $timestamp,
565
         data          => scalar $cgi->param('attach_text') || $data_fh,
566
         description   => scalar $cgi->param('description'),
567
         filename      => $cgi->param('attach_text') ? "file_$bugid.txt" : scalar $cgi->upload('data'),
568 569 570 571 572 573
         ispatch       => scalar $cgi->param('ispatch'),
         isprivate     => scalar $cgi->param('isprivate'),
         mimetype      => $content_type,
         });

    foreach my $obsolete_attachment (@obsolete_attachments) {
574 575
        $obsolete_attachment->set_is_obsolete(1);
        $obsolete_attachment->update($timestamp);
576
    }
577

578 579 580 581 582
    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);

583
    # Insert a comment about the new attachment into the database.
584
    my $comment = $cgi->param('comment');
585
    $comment = '' unless defined $comment;
586 587 588
    $bug->add_comment($comment, { isprivate => $attachment->isprivate,
                                  type => CMT_ATTACHMENT_CREATED,
                                  extra_data => $attachment->id });
589

590
  # Assign the bug to the user, if they are allowed to take it
591
  my $owner = "";
592
  if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
593 594 595 596 597
      # 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
598 599
          && ($bug_status->name ne 'UNCONFIRMED' 
              || $bug->product_obj->allows_unconfirmed))
600
      {
601
          $bug->set_bug_status($bug_status->name);
602 603
          $bug->clear_resolution();
      }
604
      # Make sure the person we are taking the bug from gets mail.
605
      $owner = $bug->assigned_to->login;
606
      $bug->set_assigned_to($user);
607
  }
608 609
  $bug->update($timestamp);

610 611 612
  if ($token) {
      trick_taint($token);
      $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
613
               ("create_attachment:" . $attachment->id, $token));
614 615
  }

616
  $dbh->bz_commit_transaction;
617

618
  # Define the variables and functions that will be passed to the UI template.
619
  $vars->{'attachment'} = $attachment;
620 621 622 623
  # 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;
624
  $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
625

626
  my $recipients =  { 'changer' => $user, 'owner' => $owner };
627 628
  $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients);

629
  print $cgi->header();
630
  # Generate and return the UI (HTML page) from the appropriate template.
631 632
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
633 634
}

635 636 637 638
# 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.
639
sub edit {
640
  my $attachment = validateID();
641

642
  my $bugattachments =
643 644 645
      Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
  # We only want attachment IDs.
  @$bugattachments = map { $_->id } @$bugattachments;
646

647 648 649 650 651 652
  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;
653
  $vars->{'attachment'} = $attachment;
654
  $vars->{'attachments'} = $bugattachments;
655

656
  print $cgi->header();
657 658

  # Generate and return the UI (HTML page) from the appropriate template.
659 660
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
661 662
}

663 664
# Updates an attachment record. Only users with "editbugs" privileges,
# (or the original attachment's submitter) can edit the attachment.
665
# Users cannot edit the content of the attachment itself.
666
sub update {
667 668
    my $user = Bugzilla->user;
    my $dbh = Bugzilla->dbh;
669

670 671 672
    # Start a transaction in preparation for updating the attachment.
    $dbh->bz_start_transaction();

673
    # Retrieve and validate parameters
674
    my $attachment = validateID();
675 676
    my $bug = $attachment->bug;
    $attachment->_check_bug;
677 678 679 680 681 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
    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;
            }
710 711
        }
    }
712 713 714 715 716 717

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

718 719
    # If the user submitted a comment while editing the attachment,
    # add the comment to the bug. Do this after having validated isprivate!
720
    my $comment = $cgi->param('comment');
721
    if (defined $comment && trim($comment) ne '') {
722 723 724
        $bug->add_comment($comment, { isprivate => $attachment->isprivate,
                                      type => CMT_ATTACHMENT_UPDATED,
                                      extra_data => $attachment->id });
725 726
    }

727 728 729 730 731
    if ($can_edit) {
        my ($flags, $new_flags) =
          Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
        $attachment->set_flags($flags, $new_flags);
    }
732

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

736 737 738 739 740 741 742
    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);
    }

743
    # Commit the comment, if any.
744
    $bug->update($timestamp);
745

746 747
    # Commit the transaction now that we are finished updating the database.
    $dbh->bz_commit_transaction();
748

749 750 751 752
    # Define the variables and functions that will be passed to the UI template.
    $vars->{'attachment'} = $attachment;
    $vars->{'bugs'} = [$bug];
    $vars->{'header_done'} = 1;
753
    $vars->{'sent_bugmail'} = 
754
        Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
755

756
    print $cgi->header();
757

758 759 760
    # Generate and return the UI (HTML page) from the appropriate template.
    $template->process("attachment/updated.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
761
}
762 763 764 765 766 767 768 769 770 771 772 773 774

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

775
    Bugzilla->params->{'allow_attachment_deletion'}
776 777 778
      || ThrowUserError('attachment_deletion_disabled');

    # Make sure the administrator is allowed to edit this attachment.
779
    my $attachment = validateID();
780
    Bugzilla::Attachment->_check_bug($attachment->bug);
781 782 783 784 785 786 787 788 789

    $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)
790
                  && ($event eq 'delete_attachment' . $attachment->id))
791 792
        {
            # The token is invalid.
793
            ThrowUserError('token_does_not_exist');
794 795
        }

796 797
        my $bug = new Bugzilla::Bug($attachment->bug_id);

798 799
        # The token is valid. Delete the content of the attachment.
        my $msg;
800
        $vars->{'attachment'} = $attachment;
801 802 803 804 805 806
        $vars->{'date'} = $date;
        $vars->{'reason'} = clean_text($cgi->param('reason') || '');

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

807 808 809
        # Paste the reason provided by the admin into a comment.
        $bug->add_comment($msg);

810 811 812 813
        # If the attachment is stored locally, remove it.
        if (-e $attachment->_get_local_filename) {
            unlink $attachment->_get_local_filename;
        }
814
        $attachment->remove_from_db();
815 816

        # Now delete the token.
817
        delete_token($token);
818

819 820
        # Insert the comment.
        $bug->update();
821

822
        # Required to display the bug the deleted attachment belongs to.
823
        $vars->{'bugs'} = [$bug];
824 825
        $vars->{'header_done'} = 1;

826
        $vars->{'sent_bugmail'} =
827
            Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
828

829 830 831 832 833
        $template->process("attachment/updated.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
    }
    else {
        # Create a token.
834
        $token = issue_session_token('delete_attachment' . $attachment->id);
835 836 837 838 839 840 841 842

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

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