attachment.cgi 38.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 26 27 28 29 30 31 32

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

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

33 34
use lib qw(.);

35 36 37 38 39
use vars qw(
  $template
  $vars
);

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

43 44 45
# Use these modules to handle flags.
use Bugzilla::Flag; 
use Bugzilla::FlagType; 
46
use Bugzilla::User;
47

48 49 50 51 52 53
# Establish a connection to the database backend.
ConnectToDatabase();

# Check whether or not the user is logged in and, if so, set the $::userid 
quietly_check_login();

54 55 56 57 58 59 60 61 62 63 64
# The ID of the bug to which the attachment is attached.  Gets set
# by validateID() (which validates the attachment ID, not the bug ID, but has
# to check if the user is authorized to access this attachment) and is used 
# by Flag:: and FlagType::validate() to ensure the requestee (if any) for a 
# requested flag is authorized to see the bug in question.  Note: This should 
# really be defined just above validateID() itself, but it's used in the main 
# body of the script before that function is defined, so we define it up here 
# instead.  We should move the validation into each function and then move this
# to just above validateID().
my $bugid;

65 66
my $cgi = Bugzilla->cgi;

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
################################################################################
# Main Body Execution
################################################################################

# 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.

# Determine whether to use the action specified by the user or the default.
my $action = $::FORM{'action'} || 'view';

if ($action eq "view")  
{ 
  validateID();
  view(); 
}
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
elsif ($action eq "interdiff")
{
  validateID('oldid');
  validateID('newid');
  validateFormat("html", "raw");
  validateContext();
  interdiff();
}
elsif ($action eq "diff")
{
  validateID();
  validateFormat("html", "raw");
  validateContext();
  diff();
}
98 99 100 101 102
elsif ($action eq "viewall") 
{ 
  ValidateBugID($::FORM{'bugid'});
  viewall(); 
}
103 104
elsif ($action eq "enter") 
{ 
105
  confirm_login();
106
  ValidateBugID($::FORM{'bugid'});
107
  validateCanChangeBug($::FORM{'bugid'});
108 109 110 111
  enter(); 
}
elsif ($action eq "insert")
{
112
  confirm_login();
113
  ValidateBugID($::FORM{'bugid'});
114
  validateCanChangeBug($::FORM{'bugid'});
115
  ValidateComment($::FORM{'comment'});
116 117
  validateFilename();
  validateIsPatch();
118 119
  my $data = validateData();
  validateDescription();
120 121
  validateContentType() unless $::FORM{'ispatch'};
  validateObsolete() if $::FORM{'obsolete'};
122
  insert($data);
123
}
124 125
elsif ($action eq "edit") 
{ 
126
  quietly_check_login();
127
  validateID();
128
  validateCanEdit($::FORM{'id'});
129 130 131 132 133
  edit(); 
}
elsif ($action eq "update") 
{ 
  confirm_login();
134
  ValidateComment($::FORM{'comment'});
135
  validateID();
136
  validateCanEdit($::FORM{'id'});
137
  validateCanChangeAttachment($::FORM{'id'});
138 139
  validateDescription();
  validateIsPatch();
140
  validateContentType() unless $::FORM{'ispatch'};
141
  validateIsObsolete();
142
  validatePrivate();
143 144 145 146
  
  # 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.
147 148
  Bugzilla::User::match_field({ '^requestee(_type)?-(\d+)$' => 
                                    { 'type' => 'single' } });
149 150 151
  Bugzilla::Flag::validate(\%::FORM, $bugid);
  Bugzilla::FlagType::validate(\%::FORM, $bugid, $::FORM{'id'});
  
152 153 154 155
  update();
}
else 
{ 
156
  ThrowCodeError("unknown_action");
157 158 159 160 161 162 163 164 165 166
}

exit;

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

sub validateID
{
167 168
    my $param = @_ ? $_[0] : 'id';

169 170
    # Validate the value of the "id" form field, which must contain an
    # integer that is the ID of an existing attachment.
171

172
    $vars->{'attach_id'} = $::FORM{$param};
173
    
174
    detaint_natural($::FORM{$param}) 
175
     || ThrowUserError("invalid_attach_id");
176
  
177
    # Make sure the attachment exists in the database.
178
    SendSQL("SELECT bug_id, isprivate FROM attachments WHERE attach_id = $::FORM{$param}");
179
    MoreSQLData()
180
      || ThrowUserError("invalid_attach_id");
181

182
    # Make sure the user is authorized to access this attachment's bug.
183
    ($bugid, my $isprivate) = FetchSQLData();
184 185
    ValidateBugID($bugid);
    if (($isprivate > 0 ) && Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
186
        ThrowUserError("attachment_access_denied");
187
    }
188 189
}

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
sub validateFormat
{
  $::FORM{'format'} ||= $_[0];
  if (! grep { $_ eq $::FORM{'format'} } @_)
  {
     $vars->{'format'} = $::FORM{'format'};
     $vars->{'formats'} = \@_;
     ThrowUserError("invalid_format");
  }
}

sub validateContext
{
  $::FORM{'context'} ||= "patch";
  if ($::FORM{'context'} ne "file" && $::FORM{'context'} ne "patch") {
    $vars->{'context'} = $::FORM{'context'};
    detaint_natural($::FORM{'context'})
      || ThrowUserError("invalid_context");
    delete $vars->{'context'};
  }
}

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
sub validateCanEdit
{
    my ($attach_id) = (@_);

    # If the user is not logged in, claim that they can edit. This allows
    # the edit scrren to be displayed to people who aren't logged in.
    # People not logged in can't actually commit changes, because that code
    # calls confirm_login, not quietly_check_login, before calling this sub
    return if $::userid == 0;

    # People in editbugs can edit all attachments
    return if UserInGroup("editbugs");

    # Bug 97729 - the submitter can edit their attachments
    SendSQL("SELECT attach_id FROM attachments WHERE " .
            "attach_id = $attach_id AND submitter_id = $::userid");

    FetchSQLData()
230 231
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attach_id });
232 233
}

234 235 236 237 238 239 240 241 242
sub validateCanChangeAttachment 
{
    my ($attachid) = @_;
    SendSQL("SELECT product_id
             FROM attachments, bugs 
             WHERE attach_id = $attachid
             AND bugs.bug_id = attachments.bug_id");
    my $productid = FetchOneColumn();
    CanEditProductId($productid)
243 244
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attachid });
245 246 247 248 249 250 251 252 253 254
}

sub validateCanChangeBug
{
    my ($bugid) = @_;
    SendSQL("SELECT product_id
             FROM bugs 
             WHERE bug_id = $bugid");
    my $productid = FetchOneColumn();
    CanEditProductId($productid)
255 256
      || ThrowUserError("illegal_attachment_edit_bug",
                        { bug_id => $bugid });
257 258
}

259 260 261
sub validateDescription
{
  $::FORM{'description'}
262
    || ThrowUserError("missing_attachment_description");
263 264 265 266 267 268 269 270
}

sub validateIsPatch
{
  # Set the ispatch 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.
  $::FORM{'ispatch'} = $::FORM{'ispatch'} ? 1 : 0;
271 272 273 274 275 276 277 278 279

  # Set the content type to text/plain if the attachment is a patch.
  $::FORM{'contenttype'} = "text/plain" if $::FORM{'ispatch'};
}

sub validateContentType
{
  if (!$::FORM{'contenttypemethod'})
  {
280
    ThrowUserError("missing_content_type_method");
281 282 283
  }
  elsif ($::FORM{'contenttypemethod'} eq 'autodetect')
  {
284
    my $contenttype = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
285 286
    # The user asked us to auto-detect the content type, so use the type
    # specified in the HTTP request headers.
287
    if ( !$contenttype )
288
    {
289
      ThrowUserError("missing_content_type");
290
    }
291
    $::FORM{'contenttype'} = $contenttype;
292 293 294 295 296 297 298 299 300 301 302 303 304
  }
  elsif ($::FORM{'contenttypemethod'} eq 'list')
  {
    # The user selected a content type from the list, so use their selection.
    $::FORM{'contenttype'} = $::FORM{'contenttypeselection'};
  }
  elsif ($::FORM{'contenttypemethod'} eq 'manual')
  {
    # The user entered a content type manually, so use their entry.
    $::FORM{'contenttype'} = $::FORM{'contenttypeentry'};
  }
  else
  {
305 306
    $vars->{'contenttypemethod'} = $::FORM{'contenttypemethod'};
    ThrowCodeError("illegal_content_type_method");
307 308 309 310
  }

  if ( $::FORM{'contenttype'} !~ /^(application|audio|image|message|model|multipart|text|video)\/.+$/ )
  {
311 312
    ThrowUserError("invalid_content_type",
                   { contenttype => $::FORM{'contenttype'} });
313
  }
314 315 316 317 318 319 320 321 322 323
}

sub validateIsObsolete
{
  # 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.
  $::FORM{'isobsolete'} = $::FORM{'isobsolete'} ? 1 : 0;
}

324 325 326 327 328 329 330 331
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.
    $::FORM{'isprivate'} = $::FORM{'isprivate'} ? 1 : 0;
}

332 333
sub validateData
{
334 335
  my $maxsize = $::FORM{'ispatch'} ? Param('maxpatchsize') : Param('maxattachmentsize');
  $maxsize *= 1024; # Convert from K
336

337 338
  my $fh = $cgi->upload('data');
  my $data;
339

340 341
  # We could get away with reading only as much as required, except that then
  # we wouldn't have a size to print to the error handler below.
342
  {
343 344 345
      # enable 'slurp' mode
      local $/;
      $data = <$fh>;
346
  }
347 348 349 350 351 352 353

  $data
    || ThrowUserError("zero_length_file");

  # Make sure the attachment does not exceed the maximum permitted size
  my $len = length($data);
  if ($maxsize && $len > $maxsize) {
354
      my $vars = { filesize => sprintf("%.0f", $len/1024) };
355
      if ( $::FORM{'ispatch'} ) {
356
          ThrowUserError("patch_too_large", $vars);
357
      } else {
358
          ThrowUserError("file_too_large", $vars);
359 360 361 362
      }
  }

  return $data;
363 364
}

365
my $filename;
366 367
sub validateFilename
{
368
  defined $cgi->upload('data')
369
    || ThrowUserError("file_not_specified");
370 371 372 373 374 375 376 377 378 379 380 381 382

  $filename = $cgi->upload('data');
  
  # Remove path info (if any) from the file name.  The browser should do this
  # for us, but some are buggy.  This may not work on Mac file names and could
  # mess up file names with slashes in them, but them's the breaks.  We only
  # use this as a hint to users downloading attachments anyway, so it's not 
  # a big deal if it munges incorrectly occasionally.
  $filename =~ s/^.*[\/\\]//;

  # Truncate the filename to 100 characters, counting from the end of the string
  # to make sure we keep the filename extension.
  $filename = substr($filename, -100, 100);
383 384 385 386 387 388 389
}

sub validateObsolete
{
  # Make sure the attachment id is valid and the user has permissions to view
  # the bug to which it is attached.
  foreach my $attachid (@{$::MFORM{'obsolete'}}) {
390 391 392
    # my $vars after ThrowCodeError is updated to not use the global
    # vars hash

393 394
    $vars->{'attach_id'} = $attachid;
    
395
    detaint_natural($attachid)
396
      || ThrowCodeError("invalid_attach_id_to_obsolete");
397 398 399 400 401 402
  
    SendSQL("SELECT bug_id, isobsolete, description 
             FROM attachments WHERE attach_id = $attachid");

    # Make sure the attachment exists in the database.
    MoreSQLData()
403
      || ThrowUserError("invalid_attach_id", $vars);
404 405 406

    my ($bugid, $isobsolete, $description) = FetchSQLData();

407 408
    $vars->{'description'} = $description;
    
409 410
    if ($bugid != $::FORM{'bugid'})
    {
411 412 413
      $vars->{'my_bug_id'} = $::FORM{'bugid'};
      $vars->{'attach_bug_id'} = $bugid;
      ThrowCodeError("mismatched_bug_ids_on_obsolete");
414 415 416 417
    }

    if ( $isobsolete )
    {
418
      ThrowCodeError("attachment_already_obsolete");
419
    }
420 421 422

    # Check that the user can modify this attachment
    validateCanEdit($attachid);
423 424 425 426
  }

}

427 428 429 430 431 432
################################################################################
# Functions
################################################################################

sub view
{
433
    # Display an attachment.
434

435
    # Retrieve the attachment content and its content type from the database.
436 437 438
    SendSQL("SELECT mimetype, filename, thedata FROM attachments WHERE attach_id = $::FORM{'id'}");
    my ($contenttype, $filename, $thedata) = FetchSQLData();

439
    # Return the appropriate HTTP response headers.
440 441
    $filename =~ s/^.*[\/\\]//;
    my $filesize = length($thedata);
442

443
    print Bugzilla->cgi->header(-type=>"$contenttype; name=\"$filename\"",
444
                                -content_disposition=> "inline; filename=$filename",
445 446 447
                                -content_length => $filesize);

    print $thedata;
448 449
}

450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
sub interdiff
{
  # Get old patch data
  my ($old_bugid, $old_description, $old_filename, $old_file_list) =
      get_unified_diff($::FORM{'oldid'});

  # Get new patch data
  my ($new_bugid, $new_description, $new_filename, $new_file_list) =
      get_unified_diff($::FORM{'newid'});

  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;
470
  my ($reader, $last_reader) = setup_patch_readers("");
471 472
  if ($::FORM{'format'} eq "raw")
  {
473 474
    require PatchReader::DiffPrinter::raw;
    $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489
    # Actually print out the patch
    print $cgi->header(-type => 'text/plain',
                       -expires => '+3M');
  }
  else
  {
    $vars->{warning} = $warning if $warning;
    $vars->{bugid} = $new_bugid;
    $vars->{oldid} = $::FORM{'oldid'};
    $vars->{old_desc} = $old_description;
    $vars->{newid} = $::FORM{'newid'};
    $vars->{new_desc} = $new_description;
    delete $vars->{attachid};
    delete $vars->{do_context};
    delete $vars->{context};
490
    setup_template_patch_reader($last_reader);
491
  }
492
  $reader->iterate_fh($interdiff_fh, "interdiff #$::FORM{'oldid'} #$::FORM{'newid'}");
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
  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) = @_;

  # Bring in the modules we need
508 509 510 511
  require PatchReader::Raw;
  require PatchReader::FixPatchRoot;
  require PatchReader::DiffPrinter::raw;
  require PatchReader::PatchInfoGrabber;
512 513 514 515 516 517 518 519 520 521 522
  require File::Temp;

  # Get the patch
  SendSQL("SELECT bug_id, description, ispatch, thedata FROM attachments WHERE attach_id = $id");
  my ($bugid, $description, $ispatch, $thedata) = FetchSQLData();
  if (!$ispatch) {
    $vars->{'attach_id'} = $id;
    ThrowCodeError("must_be_patch");
  }

  # Reads in the patch, converting to unified diff in a temp file
523 524 525
  my $reader = new PatchReader::Raw;
  my $last_reader = $reader;

526
  # fixes patch root (makes canonical if possible)
527 528 529 530 531 532
  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;
  }

533
  # Grabs the patch file info
534 535 536 537
  my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
  $last_reader->sends_data_to($patch_info_grabber);
  $last_reader = $patch_info_grabber;

538 539
  # Prints out to temporary file
  my ($fh, $filename) = File::Temp::tempfile();
540 541 542 543
  my $raw_printer = new PatchReader::DiffPrinter::raw($fh);
  $last_reader->sends_data_to($raw_printer);
  $last_reader = $raw_printer;

544
  # Iterate!
545
  $reader->iterate_string($id, $thedata);
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570

  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;
}

571
sub setup_patch_readers {
572 573 574 575 576 577 578 579 580 581
  my ($diff_root) = @_;

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

582 583 584 585 586
  # 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;
587 588 589
  # Fix the patch root if we have a cvs root
  if (Param('cvsroot'))
  {
590 591 592 593
    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;
594 595 596 597
  }
  # Add in cvs context if we have the necessary info to do it
  if ($::FORM{'context'} ne "patch" && $::cvsbin && Param('cvsroot_get'))
  {
598 599 600
    require PatchReader::AddCVSContext;
    $last_reader->sends_data_to(
        new PatchReader::AddCVSContext($::FORM{'context'},
601
                                         Param('cvsroot_get')));
602
    $last_reader = $last_reader->sends_data_to;
603
  }
604
  return ($reader, $last_reader);
605 606
}

607
sub setup_template_patch_reader
608
{
609
  my ($last_reader) = @_;
610

611
  require PatchReader::DiffPrinter::template;
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627

  my $format = $::FORM{'format'};

  # Define the vars for templates
  if (defined($::FORM{'headers'})) {
    $vars->{headers} = $::FORM{'headers'};
  } else {
    $vars->{headers} = 1 if !defined($::FORM{'headers'});
  }
  $vars->{collapsed} = $::FORM{'collapsed'};
  $vars->{context} = $::FORM{'context'};
  $vars->{do_context} = $::cvsbin && Param('cvsroot_get') && !$vars->{'newid'};

  # Print everything out
  print $cgi->header(-type => 'text/html',
                     -expires => '+3M');
628
  $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
                             "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
{
  # Get patch data
  SendSQL("SELECT bug_id, description, ispatch, thedata FROM attachments WHERE attach_id = $::FORM{'id'}");
  my ($bugid, $description, $ispatch, $thedata) = FetchSQLData();

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

652
  my ($reader, $last_reader) = setup_patch_readers();
653 654 655

  if ($::FORM{'format'} eq "raw")
  {
656 657
    require PatchReader::DiffPrinter::raw;
    $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
658 659 660 661
    # Actually print out the patch
    use vars qw($cgi);
    print $cgi->header(-type => 'text/plain',
                       -expires => '+3M');
662
    $reader->iterate_string("Attachment " . $::FORM{'id'}, $thedata);
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687
  }
  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.
      SendSQL("SELECT attach_id, description FROM attachments WHERE bug_id = $bugid AND ispatch = 1 ORDER BY creation_ts DESC");
      my $select_next_patch = 0;
      while (my ($other_id, $other_desc) = FetchSQLData()) {
        if ($other_id eq $::FORM{'id'}) {
          $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;
    $vars->{attachid} = $::FORM{'id'};
    $vars->{description} = $description;
688
    setup_template_patch_reader($last_reader);
689
    # Actually print out the patch
690
    $reader->iterate_string("Attachment " . $::FORM{'id'}, $thedata);
691 692
  }
}
693 694 695 696 697 698 699

sub viewall
{
  # Display all attachments for a given bug in a series of IFRAMEs within one HTML page.

  # Retrieve the attachments from the database and write them into an array
  # of hashes where each hash represents one attachment.
700 701 702 703
    my $privacy = "";
    if (Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
        $privacy = "AND isprivate < 1 ";
    }
704 705
    SendSQL("SELECT attach_id, DATE_FORMAT(creation_ts, '%Y.%m.%d %H:%i'),
            mimetype, description, ispatch, isobsolete, isprivate 
706 707
            FROM attachments WHERE bug_id = $::FORM{'bugid'} $privacy 
            ORDER BY attach_id");
708
  my @attachments; # the attachments array
709 710
  while (MoreSQLData())
  {
711
    my %a; # the attachment hash
712
    ($a{'attachid'}, $a{'date'}, $a{'contenttype'}, 
713
     $a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, $a{'isprivate'}) = FetchSQLData();
714 715 716 717 718

    # Flag attachments as to whether or not they can be viewed (as opposed to
    # being downloaded).  Currently I decide they are viewable if their MIME type 
    # is either text/*, image/*, or application/vnd.mozilla.*.
    # !!! Yuck, what an ugly hack.  Fix it!
719
    $a{'isviewable'} = ( $a{'contenttype'} =~ /^(text|image|application\/vnd\.mozilla\.)/ );
720 721 722 723 724

    # Add the hash representing the attachment to the array of attachments.
    push @attachments, \%a;
  }

725 726 727 728
  # Retrieve the bug summary (for displaying on screen) and assignee.
  SendSQL("SELECT short_desc, assigned_to FROM bugs " .
          "WHERE bug_id = $::FORM{'bugid'}");
  my ($bugsummary, $assignee_id) = FetchSQLData();
729 730 731 732

  # Define the variables and functions that will be passed to the UI template.
  $vars->{'bugid'} = $::FORM{'bugid'};
  $vars->{'attachments'} = \@attachments;
733 734
  $vars->{'bugassignee_id'} = $assignee_id;
  $vars->{'bugsummary'} = $bugsummary;
735
  $vars->{'GetBugLink'} = \&GetBugLink;
736

737
  print Bugzilla->cgi->header();
738 739

  # Generate and return the UI (HTML page) from the appropriate template.
740 741
  $template->process("attachment/show-multiple.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
742 743 744
}


745 746 747 748
sub enter
{
  # Display a form for entering a new attachment.

749 750 751 752 753 754
  # 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")) {
      $canEdit = "AND submitter_id = $::userid";
  }
755
  SendSQL("SELECT attach_id, description, isprivate
756 757
           FROM attachments
           WHERE bug_id = $::FORM{'bugid'}
758
           AND isobsolete = 0 $canEdit
759 760 761 762
           ORDER BY attach_id");
  my @attachments; # the attachments array
  while ( MoreSQLData() ) {
    my %a; # the attachment hash
763
    ($a{'id'}, $a{'description'}, $a{'isprivate'}) = FetchSQLData();
764 765 766 767 768

    # Add the hash representing the attachment to the array of attachments.
    push @attachments, \%a;
  }

769 770 771 772
  # Retrieve the bug summary (for displaying on screen) and assignee.
  SendSQL("SELECT short_desc, assigned_to FROM bugs 
           WHERE bug_id = $::FORM{'bugid'}");
  my ($bugsummary, $assignee_id) = FetchSQLData();
773 774 775 776

  # Define the variables and functions that will be passed to the UI template.
  $vars->{'bugid'} = $::FORM{'bugid'};
  $vars->{'attachments'} = \@attachments;
777 778
  $vars->{'bugassignee_id'} = $assignee_id;
  $vars->{'bugsummary'} = $bugsummary;
779
  $vars->{'GetBugLink'} = \&GetBugLink;
780

781
  print Bugzilla->cgi->header();
782 783

  # Generate and return the UI (HTML page) from the appropriate template.
784 785
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
786 787 788 789 790
}


sub insert
{
791 792
  my ($data) = @_;

793 794 795
  # Insert a new attachment into the database.

  # Escape characters in strings that will be used in SQL statements.
796
  $filename = SqlQuote($filename);
797 798
  my $description = SqlQuote($::FORM{'description'});
  my $contenttype = SqlQuote($::FORM{'contenttype'});
799
  my $thedata = SqlQuote($data);
800
  my $isprivate = $::FORM{'isprivate'} ? 1 : 0;
801 802

  # Insert the attachment into the database.
803 804
  SendSQL("INSERT INTO attachments (bug_id, creation_ts, filename, description, mimetype, ispatch, isprivate, submitter_id, thedata) 
           VALUES ($::FORM{'bugid'}, now(), $filename, $description, $contenttype, $::FORM{'ispatch'}, $isprivate, $::userid, $thedata)");
805 806 807 808 809 810

  # Retrieve the ID of the newly created attachment record.
  SendSQL("SELECT LAST_INSERT_ID()");
  my $attachid = FetchOneColumn();

  # Insert a comment about the new attachment into the database.
811
  my $comment = "Created an attachment (id=$attachid)\n$::FORM{'description'}\n";
812 813 814 815 816 817 818 819 820
  $comment .= ("\n" . $::FORM{'comment'}) if $::FORM{'comment'};

  use Text::Wrap;
  $Text::Wrap::columns = 80;
  $Text::Wrap::huge = 'overflow';
  $comment = Text::Wrap::wrap('', '', $comment);

  AppendComment($::FORM{'bugid'}, 
                $::COOKIE{"Bugzilla_login"},
821 822
                $comment,
                $isprivate);
823 824 825

  # Make existing attachments obsolete.
  my $fieldid = GetFieldID('attachments.isobsolete');
826 827
  foreach my $obsolete_id (@{$::MFORM{'obsolete'}}) {
      SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $obsolete_id");
828
      SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
829 830 831 832 833
               VALUES ($::FORM{'bugid'}, $obsolete_id, $::userid, NOW(), $fieldid, '0', '1')");
      # If the obsolete attachment has pending flags, migrate them to the new attachment.
      if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id , 'status' => 'pending' })) {
        Bugzilla::Flag::migrate($obsolete_id, $attachid);
      }
834 835
  }

836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
  # Assign the bug to the user, if they are allowed to take it
  my $forcecc = "";
  
  if ($::FORM{'takebug'} && UserInGroup("editbugs")) {
      SendSQL("select NOW()");
      my $timestamp = FetchOneColumn();
      
      my @fields = ("assigned_to", "bug_status", "resolution", "login_name");
      
      # Get the old values, for the bugs_activity table
      SendSQL("SELECT " . join(", ", @fields) . " FROM bugs, profiles " .
              "WHERE bugs.bug_id = $::FORM{'bugid'} " .
              "AND   profiles.userid = bugs.assigned_to");
      
      my @oldvalues = FetchSQLData();
      my @newvalues = ($::userid, "ASSIGNED", "", DBID_to_name($::userid));
      
      # Make sure the person we are taking the bug from gets mail.
      $forcecc = $oldvalues[3];  
                  
      @oldvalues = map(SqlQuote($_), @oldvalues);
      @newvalues = map(SqlQuote($_), @newvalues);
               
      # Update the bug record. Note that this doesn't involve login_name.
      SendSQL("UPDATE bugs SET " . 
              join(", ", map("$fields[$_] = $newvalues[$_]", (0..2))) . 
              " WHERE bug_id = $::FORM{'bugid'}");
      
      # We store email addresses in the bugs_activity table rather than IDs.
      $oldvalues[0] = $oldvalues[3];
      $newvalues[0] = $newvalues[3];
      
      # Add the changes to the bugs_activity table
      for (my $i = 0; $i < 3; $i++) {
          if ($oldvalues[$i] ne $newvalues[$i]) {
              my $fieldid = GetFieldID($fields[$i]);
              SendSQL("INSERT INTO bugs_activity " .
                      "(bug_id, who, bug_when, fieldid, removed, added) " .
                      " VALUES ($::FORM{'bugid'}, $::userid, " . 
                      SqlQuote($timestamp) . 
                      ", $fieldid, $oldvalues[$i], $newvalues[$i])");
          }
      }      
  }   
  
881
  # Define the variables and functions that will be passed to the UI template.
882
  $vars->{'mailrecipients'} =  { 'changer' => $::COOKIE{'Bugzilla_login'} };
883 884 885 886 887 888
  $vars->{'bugid'} = $::FORM{'bugid'};
  $vars->{'attachid'} = $attachid;
  $vars->{'description'} = $description;
  $vars->{'contenttypemethod'} = $::FORM{'contenttypemethod'};
  $vars->{'contenttype'} = $::FORM{'contenttype'};

889
  print Bugzilla->cgi->header();
890 891

  # Generate and return the UI (HTML page) from the appropriate template.
892 893
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
894 895 896
}


897 898
sub edit
{
899 900 901 902
  # Edit 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.
903 904 905
  # Users cannot edit the content of the attachment itself.

  # Retrieve the attachment from the database.
906
  SendSQL("SELECT description, mimetype, filename, bug_id, ispatch, isobsolete, isprivate 
907
           FROM attachments WHERE attach_id = $::FORM{'id'}");
908
  my ($description, $contenttype, $filename, $bugid, $ispatch, $isobsolete, $isprivate) = FetchSQLData();
909 910

  # Flag attachment as to whether or not it can be viewed (as opposed to
911 912
  # being downloaded).  Currently I decide it is viewable if its content
  # type is either text/.* or application/vnd.mozilla.*.
913
  # !!! Yuck, what an ugly hack.  Fix it!
914
  my $isviewable = ( $contenttype =~ /^(text|image|application\/vnd\.mozilla\.)/ );
915 916 917 918 919 920 921 922

  # 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.
  SendSQL("SELECT attach_id FROM attachments WHERE bug_id = $bugid ORDER BY attach_id");
  my @bugattachments;
  push(@bugattachments, FetchSQLData()) while (MoreSQLData());
  SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid");
  my ($bugsummary) = FetchSQLData();
923 924 925 926 927
  
  # Get a list of flag types that can be set for this attachment.
  SendSQL("SELECT product_id, component_id FROM bugs WHERE bug_id = $bugid");
  my ($product_id, $component_id) = FetchSQLData();
  my $flag_types = Bugzilla::FlagType::match({ 'target_type'  => 'attachment' , 
928 929
                                               'product_id'   => $product_id , 
                                               'component_id' => $component_id });
930 931
  foreach my $flag_type (@$flag_types) {
    $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id'   => $flag_type->{'id'}, 
932
                                                    'attach_id' => $::FORM{'id'} });
933 934
  }
  $vars->{'flag_types'} = $flag_types;
935
  $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types);
936
  
937 938 939
  # Define the variables and functions that will be passed to the UI template.
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'description'} = $description; 
940
  $vars->{'contenttype'} = $contenttype; 
941
  $vars->{'filename'} = $filename;
942 943 944 945
  $vars->{'bugid'} = $bugid; 
  $vars->{'bugsummary'} = $bugsummary; 
  $vars->{'ispatch'} = $ispatch; 
  $vars->{'isobsolete'} = $isobsolete; 
946
  $vars->{'isprivate'} = $isprivate; 
947 948
  $vars->{'isviewable'} = $isviewable; 
  $vars->{'attachments'} = \@bugattachments; 
949
  $vars->{'GetBugLink'} = \&GetBugLink;
950

951 952 953 954 955
  # Determine if PatchReader is installed
  eval {
    require PatchReader;
    $vars->{'patchviewerinstalled'} = 1;
  };
956
  print Bugzilla->cgi->header();
957 958

  # Generate and return the UI (HTML page) from the appropriate template.
959 960
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
961 962 963 964 965
}


sub update
{
966
  # Updates an attachment record.
967 968 969

  # Get the bug ID for the bug to which this attachment is attached.
  SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
970 971 972
  my $bugid = FetchSQLData();
  unless ($bugid) 
  {
973 974
    ThrowUserError("invalid_bug_id",
                   { bug_id => $bugid });
975 976
  }
  
977
  # Lock database tables in preparation for updating the attachment.
978 979 980
  SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " . 
          "flagtypes READ , fielddefs READ , bugs_activity WRITE, " . 
          "flaginclusions AS i READ, flagexclusions AS e READ, " . 
981 982
          # cc, bug_group_map, user_group_map, and groups are in here so we
          # can check the permissions of flag requestees and email addresses
983 984 985 986 987
          # on the flag type cc: lists via the CanSeeBug
          # function call in Flag::notify. group_group_map is in here in case
          # Bugzilla::User needs to rederive groups. profiles and 
          # user_group_map would be READ locks instead of WRITE locks if it
          # weren't for derive_groups, which needs to write to those tables.
988 989 990
          "bugs READ, profiles WRITE, " . 
          "cc READ, bug_group_map READ, user_group_map WRITE, " . 
          "group_group_map READ, groups READ");
991
  
992 993
  # Get a copy of the attachment record before we make changes
  # so we can record those changes in the activity table.
994
  SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
995
           FROM attachments WHERE attach_id = $::FORM{'id'}");
996 997
  my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
      $oldisobsolete, $oldisprivate) = FetchSQLData();
998

999
  # Quote the description and content type for use in the SQL UPDATE statement.
1000
  my $quoteddescription = SqlQuote($::FORM{'description'});
1001
  my $quotedcontenttype = SqlQuote($::FORM{'contenttype'});
1002
  my $quotedfilename = SqlQuote($::FORM{'filename'});
1003 1004 1005 1006 1007

  # Update the attachment record in the database.
  # Sets the creation timestamp to itself to avoid it being updated automatically.
  SendSQL("UPDATE  attachments 
           SET     description = $quoteddescription , 
1008
                   mimetype = $quotedcontenttype , 
1009
                   filename = $quotedfilename ,
1010
                   ispatch = $::FORM{'ispatch'} , 
1011 1012
                   isobsolete = $::FORM{'isobsolete'} ,
                   isprivate = $::FORM{'isprivate'} 
1013 1014 1015
           WHERE   attach_id = $::FORM{'id'}
         ");

1016 1017 1018 1019
  # Figure out when the changes were made.
  SendSQL("SELECT NOW()");
  my $timestamp = FetchOneColumn();
    
1020
  # Record changes in the activity table.
1021
  my $sql_timestamp = SqlQuote($timestamp);
1022 1023 1024 1025
  if ($olddescription ne $::FORM{'description'}) {
    my $quotedolddescription = SqlQuote($olddescription);
    my $fieldid = GetFieldID('attachments.description');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
1026
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedolddescription, $quoteddescription)");
1027
  }
1028 1029
  if ($oldcontenttype ne $::FORM{'contenttype'}) {
    my $quotedoldcontenttype = SqlQuote($oldcontenttype);
1030 1031
    my $fieldid = GetFieldID('attachments.mimetype');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
1032
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
1033
  }
1034 1035 1036 1037 1038 1039
  if ($oldfilename ne $::FORM{'filename'}) {
    my $quotedoldfilename = SqlQuote($oldfilename);
    my $fieldid = GetFieldID('attachments.filename');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
             VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedoldfilename, $quotedfilename)");
  }
1040 1041 1042
  if ($oldispatch ne $::FORM{'ispatch'}) {
    my $fieldid = GetFieldID('attachments.ispatch');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
1043
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldispatch, $::FORM{'ispatch'})");
1044 1045 1046 1047
  }
  if ($oldisobsolete ne $::FORM{'isobsolete'}) {
    my $fieldid = GetFieldID('attachments.isobsolete');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
1048
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
1049
  }
1050 1051 1052 1053 1054
  if ($oldisprivate ne $::FORM{'isprivate'}) {
    my $fieldid = GetFieldID('attachments.isprivate');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
             VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisprivate, $::FORM{'isprivate'})");
  }
1055 1056 1057 1058 1059
  
  # Update flags.
  my $target = Bugzilla::Flag::GetTarget(undef, $::FORM{'id'});
  Bugzilla::Flag::process($target, $timestamp, \%::FORM);
  
1060 1061 1062 1063 1064 1065 1066
  # Unlock all database tables now that we are finished updating the database.
  SendSQL("UNLOCK TABLES");

  # If the user submitted a comment while editing the attachment, 
  # add the comment to the bug.
  if ( $::FORM{'comment'} )
  {
1067 1068 1069 1070
    use Text::Wrap;
    $Text::Wrap::columns = 80;
    $Text::Wrap::huge = 'wrap';

1071 1072 1073 1074
    # Append 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 $::FORM{'id'})\n| . $::FORM{'comment'};

1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087
    my $wrappedcomment = "";
    foreach my $line (split(/\r\n|\r|\n/, $comment))
    {
      if ( $line =~ /^>/ )
      {
        $wrappedcomment .= $line . "\n";
      }
      else
      {
        $wrappedcomment .= wrap('', '', $line) . "\n";
      }
    }

1088 1089 1090 1091 1092 1093
    # Get the user's login name since the AppendComment function needs it.
    my $who = DBID_to_name($::userid);
    # Mention $::userid again so Perl doesn't give me a warning about it.
    my $neverused = $::userid;

    # Append the comment to the list of comments in the database.
1094
    AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'}, $timestamp);
1095 1096

  }
1097
  
1098
  # Define the variables and functions that will be passed to the UI template.
1099
  $vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'} };
1100 1101 1102
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'bugid'} = $bugid; 

1103
  print Bugzilla->cgi->header();
1104 1105

  # Generate and return the UI (HTML page) from the appropriate template.
1106 1107
  $template->process("attachment/updated.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
1108
}