attachment.cgi 41.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 31 32 33 34 35 36 37

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

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

38 39
use lib qw(.);

40
# Include the Bugzilla CGI and general utility library.
41
require "globals.pl";
42

43 44
use Bugzilla;
use Bugzilla::Config qw(:locations);
45
use Bugzilla::Constants;
46 47
use Bugzilla::Flag; 
use Bugzilla::FlagType; 
48
use Bugzilla::User;
49
use Bugzilla::Util;
50
use Bugzilla::Bug;
51
use Bugzilla::Field;
52
use Bugzilla::Attachment;
53
use Bugzilla::Token;
54

55
Bugzilla->login();
56

57
my $cgi = Bugzilla->cgi;
58 59
my $template = Bugzilla->template;
my $vars = {};
60

61 62 63 64
################################################################################
# Main Body Execution
################################################################################

65 66 67 68
# 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'.
69 70

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

if ($action eq "view")  
74
{
75
    view();
76
}
77 78
elsif ($action eq "interdiff")
{
79
    interdiff();
80 81 82
}
elsif ($action eq "diff")
{
83
    diff();
84
}
85 86
elsif ($action eq "viewall") 
{ 
87
    viewall(); 
88
}
89 90
elsif ($action eq "enter") 
{ 
91 92
    Bugzilla->login(LOGIN_REQUIRED);
    enter(); 
93 94 95
}
elsif ($action eq "insert")
{
96 97
    Bugzilla->login(LOGIN_REQUIRED);
    insert();
98
}
99 100
elsif ($action eq "edit") 
{ 
101
    edit(); 
102 103 104
}
elsif ($action eq "update") 
{ 
105 106
    Bugzilla->login(LOGIN_REQUIRED);
    update();
107
}
108 109 110
elsif ($action eq "delete") {
    delete_attachment();
}
111 112
else 
{ 
113
  ThrowCodeError("unknown_action", { action => $action });
114 115 116 117 118 119 120 121
}

exit;

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

122 123 124 125 126 127 128 129 130 131 132 133
# 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'.
# 
# 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.
#
# Returns a list, where the first item is the validated, detainted
# attachment id, and the 2nd item is the bug id corresponding to the
# attachment.
# 
134 135
sub validateID
{
136
    my $param = @_ ? $_[0] : 'id';
137 138
    my $dbh = Bugzilla->dbh;
    
139 140 141
    # 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
142 143
    if ($param eq 'id' && !$cgi->param('id')) {

144
        print $cgi->header();
145 146 147 148
        $template->process("attachment/choose.html.tmpl", $vars) ||
            ThrowTemplateError($template->error());
        exit;
    }
149
    
150 151 152 153 154 155 156
    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) });
157
  
158
    # Make sure the attachment exists in the database.
159 160 161 162 163 164 165
    my ($bugid, $isprivate) = $dbh->selectrow_array(
                                    "SELECT bug_id, isprivate 
                                     FROM attachments 
                                     WHERE attach_id = ?",
                                     undef, $attach_id);
    ThrowUserError("invalid_attach_id", { attach_id => $attach_id }) 
        unless $bugid;
166

167
    # Make sure the user is authorized to access this attachment's bug.
168

169
    ValidateBugID($bugid);
170 171 172 173
    if ($isprivate && Param("insidergroup")) {
        UserInGroup(Param("insidergroup"))
          || ThrowUserError("auth_failure", {action => "access",
                                             object => "attachment"});
174
    }
175

176
    return ($attach_id,$bugid);
177 178
}

179 180 181
# 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.
182 183
sub validateFormat
{
184 185 186
  # receives a list of legal formats; first item is a default
  my $format = $cgi->param('format') || $_[0];
  if ( lsearch(\@_, $format) == -1)
187
  {
188
     ThrowUserError("invalid_format", { format  => $format, formats => \@_ });
189
  }
190

191
  return $format;
192 193
}

194 195
# Validates context of a diff/interdiff. Will throw an error if the context
# is not number, "file" or "patch". Returns the validated, detainted context.
196 197
sub validateContext
{
198 199 200 201
  my $context = $cgi->param('context') || "patch";
  if ($context ne "file" && $context ne "patch") {
    detaint_natural($context)
      || ThrowUserError("invalid_context", { context => $cgi->param('context') });
202
  }
203 204

  return $context;
205 206
}

207 208 209
sub validateCanEdit
{
    my ($attach_id) = (@_);
210 211
    my $dbh = Bugzilla->dbh;
    
212 213 214 215
    # People in editbugs can edit all attachments
    return if UserInGroup("editbugs");

    # Bug 97729 - the submitter can edit their attachments
216 217 218 219
    my ($ref) = $dbh->selectrow_array("SELECT attach_id FROM attachments 
                                       WHERE attach_id = ? 
                                       AND submitter_id = ?",
                                       undef, ($attach_id, Bugzilla->user->id));
220

221 222

   $ref || ThrowUserError("illegal_attachment_edit",{ attach_id => $attach_id });
223 224
}

225 226 227
sub validateCanChangeAttachment 
{
    my ($attachid) = @_;
228 229 230
    my $dbh = Bugzilla->dbh;
    my ($productid) = $dbh->selectrow_array(
            "SELECT product_id
231 232 233
             FROM attachments
             INNER JOIN bugs
             ON bugs.bug_id = attachments.bug_id
234 235
             WHERE attach_id = ?", undef, $attachid);

236
    Bugzilla->user->can_edit_product($productid)
237 238
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attachid });
239 240 241 242 243
}

sub validateCanChangeBug
{
    my ($bugid) = @_;
244 245 246
    my $dbh = Bugzilla->dbh;
    my ($productid) = $dbh->selectrow_array(
            "SELECT product_id
247
             FROM bugs 
248 249
             WHERE bug_id = ?", undef, $bugid);

250
    Bugzilla->user->can_edit_product($productid)
251 252
      || ThrowUserError("illegal_attachment_edit_bug",
                        { bug_id => $bugid });
253 254
}

255 256
sub validateIsObsolete
{
257 258 259 260
    # Set the isobsolete flag to zero if it is undefined, since the UI uses
    # an HTML checkbox to represent this flag, and unchecked HTML checkboxes
    # do not get sent in HTML requests.
    $cgi->param('isobsolete', $cgi->param('isobsolete') ? 1 : 0);
261 262
}

263 264 265 266 267
sub validatePrivate
{
    # Set the isprivate flag to zero if it is undefined, since the UI uses
    # an HTML checkbox to represent this flag, and unchecked HTML checkboxes
    # do not get sent in HTML requests.
268
    $cgi->param('isprivate', $cgi->param('isprivate') ? 1 : 0);
269 270
}

271
sub validateObsolete {
272 273
  # Make sure the attachment id is valid and the user has permissions to view
  # the bug to which it is attached.
274
  my @obsolete_attachments;
275
  foreach my $attachid ($cgi->param('obsolete')) {
276
    my $vars = {};
277
    $vars->{'attach_id'} = $attachid;
278

279
    detaint_natural($attachid)
280
      || ThrowCodeError("invalid_attach_id_to_obsolete", $vars);
281

282
    my $attachment = Bugzilla::Attachment->get($attachid);
283

284 285
    # Make sure the attachment exists in the database.
    ThrowUserError("invalid_attach_id", $vars) unless $attachment;
286

287
    $vars->{'description'} = $attachment->description;
288

289
    if ($attachment->bug_id != $cgi->param('bugid')) {
290
      $vars->{'my_bug_id'} = $cgi->param('bugid');
291
      $vars->{'attach_bug_id'} = $attachment->bug_id;
292
      ThrowCodeError("mismatched_bug_ids_on_obsolete", $vars);
293 294
    }

295
    if ($attachment->isobsolete) {
296
      ThrowCodeError("attachment_already_obsolete", $vars);
297
    }
298 299 300

    # Check that the user can modify this attachment
    validateCanEdit($attachid);
301
    push(@obsolete_attachments, $attachment);
302
  }
303
  return @obsolete_attachments;
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
}

# Returns 1 if the parameter is a content-type viewable in this browser
# Note that we don't use $cgi->Accept()'s ability to check if a content-type
# matches, because this will return a value even if it's matched by the generic
# */* which most browsers add to the end of their Accept: headers.
sub isViewable
{
  my $contenttype = trim(shift);
    
  # We assume we can view all text and image types  
  if ($contenttype =~ /^(text|image)\//) {
    return 1;
  }
  
  # Mozilla can view XUL. Note the trailing slash on the Gecko detection to
  # avoid sending XUL to Safari.
  if (($contenttype =~ /^application\/vnd\.mozilla\./) &&
      ($cgi->user_agent() =~ /Gecko\//))
  {
    return 1;
  }
326

327 328 329 330 331 332 333 334 335
  # If it's not one of the above types, we check the Accept: header for any 
  # types mentioned explicitly.
  my $accept = join(",", $cgi->Accept());
  
  if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/) {
    return 1;
  }
  
  return 0;
336 337
}

338 339 340 341
################################################################################
# Functions
################################################################################

342
# Display an attachment.
343 344
sub view
{
345 346
    # Retrieve and validate parameters
    my ($attach_id) = validateID();
347 348
    my $dbh = Bugzilla->dbh;
    
349
    # Retrieve the attachment content and its content type from the database.
350 351
    my ($contenttype, $filename, $thedata) = $dbh->selectrow_array(
            "SELECT mimetype, filename, thedata FROM attachments " .
352
            "INNER JOIN attach_data ON id = attach_id " .
353
            "WHERE attach_id = ?", undef, $attach_id);
354
   
355 356 357
    # Bug 111522: allow overriding content-type manually in the posted form
    # params.
    if (defined $cgi->param('content_type'))
358
    {
359 360
        $cgi->param('contenttypemethod', 'manual');
        $cgi->param('contenttypeentry', $cgi->param('content_type'));
361
        Bugzilla::Attachment->validate_content_type(THROW_ERROR);
362
        $contenttype = $cgi->param('content_type');
363
    }
364

365
    # Return the appropriate HTTP response headers.
366 367
    $filename =~ s/^.*[\/\\]//;
    my $filesize = length($thedata);
368 369 370 371
    # A zero length attachment in the database means the attachment is 
    # stored in a local file
    if ($filesize == 0)
    {
372
        my $hash = ($attach_id % 100) + 100;
373
        $hash =~ s/.*(\d\d)$/group.$1/;
374
        if (open(AH, "$attachdir/$hash/attachment.$attach_id")) {
375 376 377 378 379 380 381 382 383
            binmode AH;
            $filesize = (stat(AH))[7];
        }
    }
    if ($filesize == 0)
    {
        ThrowUserError("attachment_removed");
    }

384

385 386 387 388
    # escape quotes and backslashes in the filename, per RFCs 2045/822
    $filename =~ s/\\/\\\\/g; # escape backslashes
    $filename =~ s/"/\\"/g; # escape quotes

389 390 391
    print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
                       -content_disposition=> "inline; filename=\"$filename\"",
                       -content_length => $filesize);
392

393 394 395 396 397 398 399 400 401
    if ($thedata) {
        print $thedata;
    } else {
        while (<AH>) {
            print $_;
        }
        close(AH);
    }

402 403
}

404 405
sub interdiff
{
406 407 408 409 410 411
  # Retrieve and validate parameters
  my ($old_id) = validateID('oldid');
  my ($new_id) = validateID('newid');
  my $format = validateFormat('html', 'raw');
  my $context = validateContext();

412 413
  # Get old patch data
  my ($old_bugid, $old_description, $old_filename, $old_file_list) =
414
      get_unified_diff($old_id);
415 416 417

  # Get new patch data
  my ($new_bugid, $new_description, $new_filename, $new_file_list) =
418
      get_unified_diff($new_id);
419 420 421 422 423 424 425 426 427 428 429

  my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list);

  #
  # send through interdiff, send output directly to template
  #
  # Must hack path so that interdiff will work.
  #
  $ENV{'PATH'} = $::diffpath;
  open my $interdiff_fh, "$::interdiffbin $old_filename $new_filename|";
  binmode $interdiff_fh;
430 431
    my ($reader, $last_reader) = setup_patch_readers("", $context);
    if ($format eq 'raw')
432
  {
433 434
    require PatchReader::DiffPrinter::raw;
    $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
435 436 437 438 439 440 441 442
    # Actually print out the patch
    print $cgi->header(-type => 'text/plain',
                       -expires => '+3M');
  }
  else
  {
    $vars->{warning} = $warning if $warning;
    $vars->{bugid} = $new_bugid;
443
    $vars->{oldid} = $old_id;
444
    $vars->{old_desc} = $old_description;
445
    $vars->{newid} = $new_id;
446 447 448 449
    $vars->{new_desc} = $new_description;
    delete $vars->{attachid};
    delete $vars->{do_context};
    delete $vars->{context};
450
    setup_template_patch_reader($last_reader, $format, $context);
451
  }
452
  $reader->iterate_fh($interdiff_fh, "interdiff #$old_id #$new_id");
453 454 455 456 457 458 459 460 461 462 463 464 465
  close $interdiff_fh;
  $ENV{'PATH'} = '';

  #
  # Delete temporary files
  #
  unlink($old_filename) or warn "Could not unlink $old_filename: $!";
  unlink($new_filename) or warn "Could not unlink $new_filename: $!";
}

sub get_unified_diff
{
  my ($id) = @_;
466 467
  my $dbh = Bugzilla->dbh;
  
468
  # Bring in the modules we need
469 470 471 472
  require PatchReader::Raw;
  require PatchReader::FixPatchRoot;
  require PatchReader::DiffPrinter::raw;
  require PatchReader::PatchInfoGrabber;
473 474 475
  require File::Temp;

  # Get the patch
476 477
  my ($bugid, $description, $ispatch, $thedata) = $dbh->selectrow_array(
          "SELECT bug_id, description, ispatch, thedata " . 
478 479 480
          "FROM attachments " .
          "INNER JOIN attach_data " .
          "ON id = attach_id " .
481
          "WHERE attach_id = ?", undef, $id);
482 483 484 485 486 487
  if (!$ispatch) {
    $vars->{'attach_id'} = $id;
    ThrowCodeError("must_be_patch");
  }

  # Reads in the patch, converting to unified diff in a temp file
488 489 490
  my $reader = new PatchReader::Raw;
  my $last_reader = $reader;

491
  # fixes patch root (makes canonical if possible)
492 493 494 495 496 497
  if (Param('cvsroot')) {
    my $fix_patch_root = new PatchReader::FixPatchRoot(Param('cvsroot'));
    $last_reader->sends_data_to($fix_patch_root);
    $last_reader = $fix_patch_root;
  }

498
  # Grabs the patch file info
499 500 501 502
  my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
  $last_reader->sends_data_to($patch_info_grabber);
  $last_reader = $patch_info_grabber;

503 504
  # Prints out to temporary file
  my ($fh, $filename) = File::Temp::tempfile();
505 506 507 508
  my $raw_printer = new PatchReader::DiffPrinter::raw($fh);
  $last_reader->sends_data_to($raw_printer);
  $last_reader = $raw_printer;

509
  # Iterate!
510
  $reader->iterate_string($id, $thedata);
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535

  return ($bugid, $description, $filename, $patch_info_grabber->patch_info()->{files});
}

sub warn_if_interdiff_might_fail {
  my ($old_file_list, $new_file_list) = @_;
  # Verify that the list of files diffed is the same
  my @old_files = sort keys %{$old_file_list};
  my @new_files = sort keys %{$new_file_list};
  if (@old_files != @new_files ||
      join(' ', @old_files) ne join(' ', @new_files)) {
    return "interdiff1";
  }

  # Verify that the revisions in the files are the same
  foreach my $file (keys %{$old_file_list}) {
    if ($old_file_list->{$file}{old_revision} ne
        $new_file_list->{$file}{old_revision}) {
      return "interdiff2";
    }
  }

  return undef;
}

536
sub setup_patch_readers {
537
  my ($diff_root, $context) = @_;
538 539 540 541 542 543 544 545 546

  #
  # Parameters:
  # format=raw|html
  # context=patch|file|0-n
  # collapsed=0|1
  # headers=0|1
  #

547 548 549 550 551
  # Define the patch readers
  # The reader that reads the patch in (whatever its format)
  require PatchReader::Raw;
  my $reader = new PatchReader::Raw;
  my $last_reader = $reader;
552 553 554
  # Fix the patch root if we have a cvs root
  if (Param('cvsroot'))
  {
555 556 557 558
    require PatchReader::FixPatchRoot;
    $last_reader->sends_data_to(new PatchReader::FixPatchRoot(Param('cvsroot')));
    $last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root);
    $last_reader = $last_reader->sends_data_to;
559 560
  }
  # Add in cvs context if we have the necessary info to do it
561
  if ($context ne "patch" && $::cvsbin && Param('cvsroot_get'))
562
  {
563 564
    require PatchReader::AddCVSContext;
    $last_reader->sends_data_to(
565
          new PatchReader::AddCVSContext($context,
566
                                         Param('cvsroot_get')));
567
    $last_reader = $last_reader->sends_data_to;
568
  }
569
  return ($reader, $last_reader);
570 571
}

572
sub setup_template_patch_reader
573
{
574
  my ($last_reader, $format, $context) = @_;
575

576
  require PatchReader::DiffPrinter::template;
577 578

  # Define the vars for templates
579 580
  if (defined $cgi->param('headers')) {
    $vars->{headers} = $cgi->param('headers');
581
  } else {
582
    $vars->{headers} = 1 if !defined $cgi->param('headers');
583
  }
584 585
  $vars->{collapsed} = $cgi->param('collapsed');
  $vars->{context} = $context;
586 587 588 589 590
  $vars->{do_context} = $::cvsbin && Param('cvsroot_get') && !$vars->{'newid'};

  # Print everything out
  print $cgi->header(-type => 'text/html',
                     -expires => '+3M');
591
  $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
592 593 594 595 596 597 598 599 600 601 602 603
                             "attachment/diff-header.$format.tmpl",
                             "attachment/diff-file.$format.tmpl",
                             "attachment/diff-footer.$format.tmpl",
                             { %{$vars},
                               bonsai_url => Param('bonsai_url'),
                               lxr_url => Param('lxr_url'),
                               lxr_root => Param('lxr_root'),
                             }));
}

sub diff
{
604 605 606 607
  # Retrieve and validate parameters
  my ($attach_id) = validateID();
  my $format = validateFormat('html', 'raw');
  my $context = validateContext();
608 609
  my $dbh = Bugzilla->dbh;
  
610
  # Get patch data
611 612
  my ($bugid, $description, $ispatch, $thedata) = $dbh->selectrow_array(
          "SELECT bug_id, description, ispatch, thedata FROM attachments " .
613
          "INNER JOIN attach_data ON id = attach_id " .
614
          "WHERE attach_id = ?", undef, $attach_id);
615 616 617 618 619 620 621 622

  # If it is not a patch, view normally
  if (!$ispatch)
  {
    view();
    return;
  }

623
  my ($reader, $last_reader) = setup_patch_readers(undef,$context);
624

625
  if ($format eq 'raw')
626
  {
627 628
    require PatchReader::DiffPrinter::raw;
    $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
629 630 631
    # Actually print out the patch
    print $cgi->header(-type => 'text/plain',
                       -expires => '+3M');
632
    $reader->iterate_string("Attachment $attach_id", $thedata);
633 634 635 636 637 638 639 640
  }
  else
  {
    $vars->{other_patches} = [];
    if ($::interdiffbin && $::diffpath) {
      # Get list of attachments on this bug.
      # Ignore the current patch, but select the one right before it
      # chronologically.
641 642 643 644 645 646
      my $sth = $dbh->prepare("SELECT attach_id, description 
                               FROM attachments 
                               WHERE bug_id = ? 
                               AND ispatch = 1 
                               ORDER BY creation_ts DESC");
      $sth->execute($bugid);
647
      my $select_next_patch = 0;
648
      while (my ($other_id, $other_desc) = $sth->fetchrow_array) {
649
        if ($other_id eq $attach_id) {
650 651 652 653 654 655 656 657 658 659 660
          $select_next_patch = 1;
        } else {
          push @{$vars->{other_patches}}, { id => $other_id, desc => $other_desc, selected => $select_next_patch };
          if ($select_next_patch) {
            $select_next_patch = 0;
          }
        }
      }
    }

    $vars->{bugid} = $bugid;
661
    $vars->{attachid} = $attach_id;
662
    $vars->{description} = $description;
663
    setup_template_patch_reader($last_reader, $format, $context);
664
    # Actually print out the patch
665
    $reader->iterate_string("Attachment $attach_id", $thedata);
666 667
  }
}
668

669 670
# Display all attachments for a given bug in a series of IFRAMEs within one
# HTML page.
671 672
sub viewall
{
673 674 675
    # Retrieve and validate parameters
    my $bugid = $cgi->param('bugid');
    ValidateBugID($bugid);
676

677 678
    # Retrieve the attachments from the database and write them into an array
    # of hashes where each hash represents one attachment.
679
    my $privacy = "";
680 681
    my $dbh = Bugzilla->dbh;

682 683 684
    if (Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
        $privacy = "AND isprivate < 1 ";
    }
685 686 687 688 689
  my $attachments = $dbh->selectall_arrayref(
           "SELECT attach_id AS attachid, " .
            $dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . " AS date,
            mimetype AS contenttype, description, ispatch, isobsolete, isprivate, 
            LENGTH(thedata) AS datasize
690 691 692
            FROM attachments 
            INNER JOIN attach_data
            ON attach_id = id
693 694 695 696
            WHERE bug_id = ? $privacy 
            ORDER BY attach_id", {'Slice'=>{}}, $bugid);

  foreach my $a (@{$attachments})
697
  {
698 699
    
    $a->{'isviewable'} = isViewable($a->{'contenttype'});
700
    $a->{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a->{'attachid'} });
701 702
  }

703
  # Retrieve the bug summary (for displaying on screen) and assignee.
704 705 706
  my ($bugsummary, $assignee_id) = $dbh->selectrow_array(
          "SELECT short_desc, assigned_to FROM bugs " .
          "WHERE bug_id = ?", undef, $bugid);
707 708

  # Define the variables and functions that will be passed to the UI template.
709
  $vars->{'bugid'} = $bugid;
710
  $vars->{'attachments'} = $attachments;
711 712
  $vars->{'bugassignee_id'} = $assignee_id;
  $vars->{'bugsummary'} = $bugsummary;
713

714
  print $cgi->header();
715 716

  # Generate and return the UI (HTML page) from the appropriate template.
717 718
  $template->process("attachment/show-multiple.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
719 720
}

721
# Display a form for entering a new attachment.
722 723
sub enter
{
724 725 726 727
  # Retrieve and validate parameters
  my $bugid = $cgi->param('bugid');
  ValidateBugID($bugid);
  validateCanChangeBug($bugid);
728 729
  my $dbh = Bugzilla->dbh;
  
730 731 732 733
  # 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 = "";
  if (!UserInGroup("editbugs")) {
734
      $canEdit = "AND submitter_id = " . Bugzilla->user->id;
735
  }
736 737
  my $attachments = $dbh->selectall_arrayref(
          "SELECT attach_id AS id, description, isprivate
738
           FROM attachments
739
           WHERE bug_id = ? 
740
           AND isobsolete = 0 $canEdit
741
           ORDER BY attach_id",{'Slice' =>{}}, $bugid);
742

743
  # Retrieve the bug summary (for displaying on screen) and assignee.
744 745 746
  my ($bugsummary, $assignee_id) = $dbh->selectrow_array(
          "SELECT short_desc, assigned_to FROM bugs 
           WHERE bug_id = ?", undef, $bugid);
747 748

  # Define the variables and functions that will be passed to the UI template.
749
  $vars->{'bugid'} = $bugid;
750
  $vars->{'attachments'} = $attachments;
751 752
  $vars->{'bugassignee_id'} = $assignee_id;
  $vars->{'bugsummary'} = $bugsummary;
753

754 755 756 757
  my ($product_id, $component_id)= $dbh->selectrow_array(
          "SELECT product_id, component_id FROM bugs
           WHERE bug_id = ?", undef, $bugid);
           
758 759 760 761 762 763 764
  my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
                                              'product_id'   => $product_id,
                                              'component_id' => $component_id});
  $vars->{'flag_types'} = $flag_types;
  $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'},
                                           @$flag_types);

765
  print $cgi->header();
766 767

  # Generate and return the UI (HTML page) from the appropriate template.
768 769
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
770 771
}

772
# Insert a new attachment into the database.
773 774
sub insert
{
775 776
    my $dbh = Bugzilla->dbh;
    my $user = Bugzilla->user;
777

778 779 780 781 782
    # Retrieve and validate parameters
    my $bugid = $cgi->param('bugid');
    ValidateBugID($bugid);
    validateCanChangeBug($bugid);
    ValidateComment(scalar $cgi->param('comment'));
783
    my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); 
784

785 786 787 788 789
    my $attachid =
        Bugzilla::Attachment->insert_attachment_for_bug(THROW_ERROR,
                                                        $bugid, $user,
                                                        $timestamp, \$vars);
    my $isprivate = $cgi->param('isprivate') ? 1 : 0;
790 791
    my @obsolete_attachments;
    @obsolete_attachments = validateObsolete() if $cgi->param('obsolete');
792

793
  # Insert a comment about the new attachment into the database.
794 795 796
  my $comment = "Created an attachment (id=$attachid)\n" .
                $cgi->param('description') . "\n";
  $comment .= ("\n" . $cgi->param('comment')) if defined $cgi->param('comment');
797

798
  AppendComment($bugid, $user->id, $comment, $isprivate, $timestamp);
799 800

  # Make existing attachments obsolete.
801
  my $fieldid = get_field_id('attachments.isobsolete');
802 803 804
  my $bug = new Bugzilla::Bug($bugid, $user->id);

  foreach my $obsolete_attachment (@obsolete_attachments) {
805 806
      # If the obsolete attachment has request flags, cancel them.
      # This call must be done before updating the 'attachments' table.
807
      Bugzilla::Flag::CancelRequests($bug, $obsolete_attachment, $timestamp);
808

809
      $dbh->do("UPDATE attachments SET isobsolete = 1 " . 
810
              "WHERE attach_id = ?", undef, $obsolete_attachment->id);
811
      $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
812
                                          fieldid, removed, added) 
813
              VALUES (?,?,?,?,?,?,?)", undef, 
814
              $bugid, $obsolete_attachment->id, $user->id, $timestamp, $fieldid, 0, 1);
815 816
  }

817
  # Assign the bug to the user, if they are allowed to take it
818
  my $owner = "";
819
  
820
  if ($cgi->param('takebug') && UserInGroup("editbugs")) {
821
      
822 823
      my @fields = ("assigned_to", "bug_status", "resolution", "everconfirmed",
                    "login_name");
824 825
      
      # Get the old values, for the bugs_activity table
826 827
      my @oldvalues = $dbh->selectrow_array(
              "SELECT " . join(", ", @fields) . " " .
828 829 830
              "FROM bugs " .
              "INNER JOIN profiles " .
              "ON profiles.userid = bugs.assigned_to " .
831
              "WHERE bugs.bug_id = ?", undef, $bugid);
832
      
833
      my @newvalues = ($user->id, "ASSIGNED", "", 1, $user->login);
834 835
      
      # Make sure the person we are taking the bug from gets mail.
836
      $owner = $oldvalues[4];  
837

838
      # Update the bug record. Note that this doesn't involve login_name.
839 840 841 842
      $dbh->do('UPDATE bugs SET delta_ts = ?, ' .
               join(', ', map("$fields[$_] = ?", (0..3))) . ' WHERE bug_id = ?',
               undef, ($timestamp, map($newvalues[$_], (0..3)) , $bugid));

843 844 845 846
      # If the bug was a dupe, we have to remove its entry from the
      # 'duplicates' table.
      $dbh->do('DELETE FROM duplicates WHERE dupe = ?', undef, $bugid);

847
      # We store email addresses in the bugs_activity table rather than IDs.
848 849
      $oldvalues[0] = $oldvalues[4];
      $newvalues[0] = $newvalues[4];
850

851
      for (my $i = 0; $i < 4; $i++) {
852
          if ($oldvalues[$i] ne $newvalues[$i]) {
853
              LogActivityEntry($bugid, $fields[$i], $oldvalues[$i],
854
                               $newvalues[$i], $user->id, $timestamp);
855 856 857 858
          }
      }      
  }   
  
859
  # Create flags.
860 861 862 863
  # Update the bug object with updated data.
  $bug = new Bugzilla::Bug($bugid, $user->id);
  my $attachment = Bugzilla::Attachment->get($attachid);
  Bugzilla::Flag::process($bug, $attachment, $timestamp, $cgi);
864
   
865
  # Define the variables and functions that will be passed to the UI template.
866
  $vars->{'mailrecipients'} =  { 'changer' => $user->login,
867
                                 'owner'   => $owner };
868
  $vars->{'bugid'} = $bugid;
869
  $vars->{'attachid'} = $attachid;
870
  $vars->{'description'} = $cgi->param('description');
871 872
  $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
  $vars->{'contenttype'} = $cgi->param('contenttype');
873

874
  print $cgi->header();
875 876

  # Generate and return the UI (HTML page) from the appropriate template.
877 878
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
879 880
}

881 882 883 884
# 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.
885
sub edit {
886
  my ($attach_id) = validateID();
887
  my $dbh = Bugzilla->dbh;
888

889 890
  my $attachment = Bugzilla::Attachment->get($attach_id);
  my $isviewable = !$attachment->isurl && isViewable($attachment->contenttype);
891 892 893

  # Retrieve a list of attachments for this bug as well as a summary of the bug
  # to use in a navigation bar across the top of the screen.
894 895 896 897 898 899 900 901 902 903
  my $bugattachments =
      $dbh->selectcol_arrayref('SELECT attach_id FROM attachments
                                WHERE bug_id = ? ORDER BY attach_id',
                                undef, $attachment->bug_id);

  my ($bugsummary, $product_id, $component_id) =
      $dbh->selectrow_array('SELECT short_desc, product_id, component_id
                               FROM bugs
                              WHERE bug_id = ?', undef, $attachment->bug_id);

904
  # Get a list of flag types that can be set for this attachment.
905 906
  my $flag_types = Bugzilla::FlagType::match({ 'target_type'  => 'attachment' ,
                                               'product_id'   => $product_id ,
907
                                               'component_id' => $component_id });
908
  foreach my $flag_type (@$flag_types) {
909
    $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id'   => $flag_type->{'id'},
910
                                                    'attach_id' => $attachment->id });
911 912
  }
  $vars->{'flag_types'} = $flag_types;
913
  $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types);
914
  $vars->{'attachment'} = $attachment;
915 916
  $vars->{'bugsummary'} = $bugsummary; 
  $vars->{'isviewable'} = $isviewable; 
917
  $vars->{'attachments'} = $bugattachments; 
918

919 920 921 922 923
  # Determine if PatchReader is installed
  eval {
    require PatchReader;
    $vars->{'patchviewerinstalled'} = 1;
  };
924
  print $cgi->header();
925 926

  # Generate and return the UI (HTML page) from the appropriate template.
927 928
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
929 930
}

931 932 933 934 935
# Updates an attachment record. Users with "editbugs" privileges, (or the
# original attachment's submitter) can edit the attachment's description,
# content type, ispatch and isobsolete flags, and statuses, and they can
# also submit a comment that appears in the bug.
# Users cannot edit the content of the attachment itself.
936 937
sub update
{
938 939 940 941 942 943 944
  my $userid = Bugzilla->user->id;

    # Retrieve and validate parameters
    ValidateComment(scalar $cgi->param('comment'));
    my ($attach_id, $bugid) = validateID();
    validateCanEdit($attach_id);
    validateCanChangeAttachment($attach_id);
945 946 947
    Bugzilla::Attachment->validate_description(THROW_ERROR);
    Bugzilla::Attachment->validate_is_patch(THROW_ERROR);
    Bugzilla::Attachment->validate_content_type(THROW_ERROR) unless $cgi->param('ispatch');
948 949
    validateIsObsolete();
    validatePrivate();
950
    my $dbh = Bugzilla->dbh;
951 952 953 954 955

    # The order of these function calls is important, as both Flag::validate
    # and FlagType::validate assume User::match_field has ensured that the
    # values in the requestee fields are legitimate user email addresses.
    Bugzilla::User::match_field($cgi, {
956
        '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }
957
    });
958
    Bugzilla::Flag::validate($cgi, $bugid, $attach_id);
959
    Bugzilla::FlagType::validate($cgi, $bugid, $attach_id);
960

961 962 963
    my $bug = new Bugzilla::Bug($bugid, $userid);
    # Lock database tables in preparation for updating the attachment.
    $dbh->bz_lock_tables('attachments WRITE', 'flags WRITE' ,
964 965
          'flagtypes READ', 'fielddefs READ', 'bugs_activity WRITE',
          'flaginclusions AS i READ', 'flagexclusions AS e READ',
966 967
          # cc, bug_group_map, user_group_map, and groups are in here so we
          # can check the permissions of flag requestees and email addresses
968
          # on the flag type cc: lists via the CanSeeBug
969 970 971 972
          # function call in Flag::notify. group_group_map is in here si
          # Bugzilla::User can flatten groups.
          'bugs WRITE', 'profiles READ', 'email_setting READ',
          'cc READ', 'bug_group_map READ', 'user_group_map READ',
973
          'group_group_map READ', 'groups READ', 'group_control_map READ');
974

975 976
  # Get a copy of the attachment record before we make changes
  # so we can record those changes in the activity table.
977
  my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
978 979 980
      $oldisobsolete, $oldisprivate) = $dbh->selectrow_array(
      "SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
       FROM attachments WHERE attach_id = ?", undef, $attach_id);
981

982
  # Quote the description and content type for use in the SQL UPDATE statement.
983 984 985 986 987 988 989
  my $description = $cgi->param('description');
  my $contenttype = $cgi->param('contenttype');
  my $filename = $cgi->param('filename');
  # we can detaint this way thanks to placeholders
  trick_taint($description);
  trick_taint($contenttype);
  trick_taint($filename);
990

991
  # Figure out when the changes were made.
992
  my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
993
    
994 995 996 997
  # Update flags.  We have to do this before committing changes
  # to attachments so that we can delete pending requests if the user
  # is obsoleting this attachment without deleting any requests
  # the user submits at the same time.
998 999
  my $attachment = Bugzilla::Attachment->get($attach_id);
  Bugzilla::Flag::process($bug, $attachment, $timestamp, $cgi);
1000

1001
  # Update the attachment record in the database.
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
  $dbh->do("UPDATE  attachments 
            SET     description = ?,
                    mimetype    = ?,
                    filename    = ?,
                    ispatch     = ?,
                    isobsolete  = ?,
                    isprivate   = ?
            WHERE   attach_id   = ?",
            undef, ($description, $contenttype, $filename,
            $cgi->param('ispatch'), $cgi->param('isobsolete'), 
            $cgi->param('isprivate'), $attach_id));
1013 1014

  # Record changes in the activity table.
1015
  if ($olddescription ne $cgi->param('description')) {
1016
    my $fieldid = get_field_id('attachments.description');
1017
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
1018
                                        fieldid, removed, added)
1019 1020 1021
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $olddescription, $description));
1022
  }
1023
  if ($oldcontenttype ne $cgi->param('contenttype')) {
1024
    my $fieldid = get_field_id('attachments.mimetype');
1025
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
1026
                                        fieldid, removed, added)
1027 1028 1029
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldcontenttype, $contenttype));
1030
  }
1031
  if ($oldfilename ne $cgi->param('filename')) {
1032
    my $fieldid = get_field_id('attachments.filename');
1033
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
1034
                                        fieldid, removed, added)
1035 1036 1037
              VALUES (?,?,?,?,?,?,?)", 
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldfilename, $filename));
1038
  }
1039
  if ($oldispatch ne $cgi->param('ispatch')) {
1040
    my $fieldid = get_field_id('attachments.ispatch');
1041
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
1042
                                        fieldid, removed, added)
1043 1044 1045
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldispatch, $cgi->param('ispatch')));
1046
  }
1047
  if ($oldisobsolete ne $cgi->param('isobsolete')) {
1048
    my $fieldid = get_field_id('attachments.isobsolete');
1049
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
1050
                                        fieldid, removed, added)
1051 1052 1053
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldisobsolete, $cgi->param('isobsolete')));
1054
  }
1055
  if ($oldisprivate ne $cgi->param('isprivate')) {
1056
    my $fieldid = get_field_id('attachments.isprivate');
1057
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
1058
                                        fieldid, removed, added)
1059 1060 1061
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldisprivate, $cgi->param('isprivate')));
1062
  }
1063
  
1064
  # Unlock all database tables now that we are finished updating the database.
1065
  $dbh->bz_unlock_tables();
1066

1067
  # If the user submitted a comment while editing the attachment,
1068
  # add the comment to the bug.
1069
  if ($cgi->param('comment'))
1070
  {
1071 1072 1073 1074
    # Prepend a string to the comment to let users know that the comment came
    # from the "edit attachment" screen.
    my $comment = qq|(From update of attachment $attach_id)\n| .
                  $cgi->param('comment');
1075 1076

    # Append the comment to the list of comments in the database.
1077
    AppendComment($bugid, $userid, $comment, $cgi->param('isprivate'), $timestamp);
1078
  }
1079
  
1080
  # Define the variables and functions that will be passed to the UI template.
1081
  $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login };
1082
  $vars->{'attachid'} = $attach_id; 
1083 1084
  $vars->{'bugid'} = $bugid; 

1085
  print $cgi->header();
1086 1087

  # Generate and return the UI (HTML page) from the appropriate template.
1088 1089
  $template->process("attachment/updated.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
1090
}
1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169

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

    Param('allow_attachment_deletion')
      || ThrowUserError('attachment_deletion_disabled');

    # Make sure the administrator is allowed to edit this attachment.
    my ($attach_id, $bug_id) = validateID();
    validateCanEdit($attach_id);
    validateCanChangeAttachment($attach_id);

    my $attachment = Bugzilla::Attachment->get($attach_id);
    $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)
                  && ($event eq "attachment$attach_id"))
        {
            # The token is invalid.
            ThrowUserError('token_inexistent');
        }

        # The token is valid. Delete the content of the attachment.
        my $msg;
        $vars->{'attachid'} = $attach_id;
        $vars->{'bugid'} = $bug_id;
        $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());

        $dbh->bz_lock_tables('attachments WRITE', 'attach_data WRITE', 'flags WRITE');
        $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $attach_id);
        $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isurl = ?
                  WHERE attach_id = ?', undef, ('text/plain', 0, 0, $attach_id));
        $dbh->do('DELETE FROM flags WHERE attach_id = ?', undef, $attach_id);
        $dbh->bz_unlock_tables;

        # If the attachment is stored locally, remove it.
        if (-e $attachment->_get_local_filename) {
            unlink $attachment->_get_local_filename;
        }

        # Now delete the token.
        Bugzilla::Token::DeleteToken($token);

        # Paste the reason provided by the admin into a comment.
        AppendComment($bug_id, $user->id, $msg);

        $template->process("attachment/updated.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
    }
    else {
        # Create a token.
        $token = Bugzilla::Token::IssueSessionToken('attachment' . $attach_id);

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

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