attachment.cgi 38.9 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
#                 Myk Melez <myk@mozilla.org>
23 24
#                 Daniel Raichle <draichle@gmx.net>
#                 Dave Miller <justdave@syndicomm.com>
25
#                 Alexander J. Vincent <ajvincent@juno.com>
26 27 28 29 30 31 32 33

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

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

34 35
use lib qw(.);

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

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

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

50 51 52 53 54 55
# 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();

56 57 58 59 60 61 62 63 64 65 66
# 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;

67 68
my $cgi = Bugzilla->cgi;

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

exit;

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

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

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

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

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

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
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'};
  }
}

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
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()
232 233
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attach_id });
234 235
}

236 237 238 239 240 241 242 243 244
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)
245 246
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attachid });
247 248 249 250 251 252 253 254 255 256
}

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

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

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;
273 274 275 276 277 278 279 280 281

  # 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'})
  {
282
    ThrowUserError("missing_content_type_method");
283 284 285
  }
  elsif ($::FORM{'contenttypemethod'} eq 'autodetect')
  {
286
    my $contenttype = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
287 288
    # The user asked us to auto-detect the content type, so use the type
    # specified in the HTTP request headers.
289
    if ( !$contenttype )
290
    {
291
      ThrowUserError("missing_content_type");
292
    }
293
    $::FORM{'contenttype'} = $contenttype;
294 295 296 297 298 299 300 301 302 303 304 305 306
  }
  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
  {
307 308
    ThrowCodeError("illegal_content_type_method",
                   { contenttypemethod => $::FORM{'contenttypemethod'} });
309 310 311 312
  }

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

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

326 327 328 329 330 331 332 333
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;
}

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

339 340
  my $fh = $cgi->upload('data');
  my $data;
341

342 343
  # 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.
344
  {
345 346 347
      # enable 'slurp' mode
      local $/;
      $data = <$fh>;
348
  }
349 350 351 352 353 354 355

  $data
    || ThrowUserError("zero_length_file");

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

  return $data;
365 366
}

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

  $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);
385 386 387 388 389 390 391
}

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'}}) {
392
    my $vars = {};
393 394
    $vars->{'attach_id'} = $attachid;
    
395
    detaint_natural($attachid)
396
      || ThrowCodeError("invalid_attach_id_to_obsolete", $vars);
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
      $vars->{'my_bug_id'} = $::FORM{'bugid'};
      $vars->{'attach_bug_id'} = $bugid;
413
      ThrowCodeError("mismatched_bug_ids_on_obsolete", $vars);
414 415 416 417
    }

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

    # Check that the user can modify this attachment
    validateCanEdit($attachid);
423
  }
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
}

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

447 448 449 450 451 452 453 454 455
  # 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;
456 457
}

458 459 460 461 462 463
################################################################################
# Functions
################################################################################

sub view
{
464
    # Display an attachment.
465

466
    # Retrieve the attachment content and its content type from the database.
467 468
    SendSQL("SELECT mimetype, filename, thedata FROM attachments WHERE attach_id = $::FORM{'id'}");
    my ($contenttype, $filename, $thedata) = FetchSQLData();
469 470
   
    # Bug 111522: allow overriding content-type manually in the posted $::FORM.
471
    if ($::FORM{'content_type'})
472 473
    {
        $::FORM{'contenttypemethod'} = 'manual';
474
        $::FORM{'contenttypeentry'} = $::FORM{'content_type'};
475
        validateContentType();
476
        $contenttype = $::FORM{'content_type'};
477
    }
478

479
    # Return the appropriate HTTP response headers.
480 481
    $filename =~ s/^.*[\/\\]//;
    my $filesize = length($thedata);
482

483
    print Bugzilla->cgi->header(-type=>"$contenttype; name=\"$filename\"",
484
                                -content_disposition=> "inline; filename=$filename",
485 486 487
                                -content_length => $filesize);

    print $thedata;
488 489
}

490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509
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;
510
  my ($reader, $last_reader) = setup_patch_readers("");
511 512
  if ($::FORM{'format'} eq "raw")
  {
513 514
    require PatchReader::DiffPrinter::raw;
    $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
    # 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};
530
    setup_template_patch_reader($last_reader);
531
  }
532
  $reader->iterate_fh($interdiff_fh, "interdiff #$::FORM{'oldid'} #$::FORM{'newid'}");
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
  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
548 549 550 551
  require PatchReader::Raw;
  require PatchReader::FixPatchRoot;
  require PatchReader::DiffPrinter::raw;
  require PatchReader::PatchInfoGrabber;
552 553 554 555 556 557 558 559 560 561 562
  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
563 564 565
  my $reader = new PatchReader::Raw;
  my $last_reader = $reader;

566
  # fixes patch root (makes canonical if possible)
567 568 569 570 571 572
  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;
  }

573
  # Grabs the patch file info
574 575 576 577
  my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
  $last_reader->sends_data_to($patch_info_grabber);
  $last_reader = $patch_info_grabber;

578 579
  # Prints out to temporary file
  my ($fh, $filename) = File::Temp::tempfile();
580 581 582 583
  my $raw_printer = new PatchReader::DiffPrinter::raw($fh);
  $last_reader->sends_data_to($raw_printer);
  $last_reader = $raw_printer;

584
  # Iterate!
585
  $reader->iterate_string($id, $thedata);
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610

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

611
sub setup_patch_readers {
612 613 614 615 616 617 618 619 620 621
  my ($diff_root) = @_;

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

622 623 624 625 626
  # 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;
627 628 629
  # Fix the patch root if we have a cvs root
  if (Param('cvsroot'))
  {
630 631 632 633
    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;
634 635 636 637
  }
  # Add in cvs context if we have the necessary info to do it
  if ($::FORM{'context'} ne "patch" && $::cvsbin && Param('cvsroot_get'))
  {
638 639 640
    require PatchReader::AddCVSContext;
    $last_reader->sends_data_to(
        new PatchReader::AddCVSContext($::FORM{'context'},
641
                                         Param('cvsroot_get')));
642
    $last_reader = $last_reader->sends_data_to;
643
  }
644
  return ($reader, $last_reader);
645 646
}

647
sub setup_template_patch_reader
648
{
649
  my ($last_reader) = @_;
650

651
  require PatchReader::DiffPrinter::template;
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667

  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');
668
  $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
                             "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;
  }

692
  my ($reader, $last_reader) = setup_patch_readers();
693 694 695

  if ($::FORM{'format'} eq "raw")
  {
696 697
    require PatchReader::DiffPrinter::raw;
    $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
698 699 700 701
    # Actually print out the patch
    use vars qw($cgi);
    print $cgi->header(-type => 'text/plain',
                       -expires => '+3M');
702
    $reader->iterate_string("Attachment " . $::FORM{'id'}, $thedata);
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
  }
  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;
728
    setup_template_patch_reader($last_reader);
729
    # Actually print out the patch
730
    $reader->iterate_string("Attachment " . $::FORM{'id'}, $thedata);
731 732
  }
}
733 734 735 736 737 738 739

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.
740 741 742 743
    my $privacy = "";
    if (Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
        $privacy = "AND isprivate < 1 ";
    }
744 745
    SendSQL("SELECT attach_id, DATE_FORMAT(creation_ts, '%Y.%m.%d %H:%i'),
            mimetype, description, ispatch, isobsolete, isprivate 
746 747
            FROM attachments WHERE bug_id = $::FORM{'bugid'} $privacy 
            ORDER BY attach_id");
748
  my @attachments; # the attachments array
749 750
  while (MoreSQLData())
  {
751
    my %a; # the attachment hash
752
    ($a{'attachid'}, $a{'date'}, $a{'contenttype'}, 
753 754 755
     $a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, $a{'isprivate'}) 
                                                               = FetchSQLData();
    $a{'isviewable'} = isViewable($a{'contenttype'});
756 757 758 759 760

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

761 762 763 764
  # 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();
765 766 767 768

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

773
  print Bugzilla->cgi->header();
774 775

  # Generate and return the UI (HTML page) from the appropriate template.
776 777
  $template->process("attachment/show-multiple.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
778 779 780
}


781 782 783 784
sub enter
{
  # Display a form for entering a new attachment.

785 786 787 788 789 790
  # 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";
  }
791
  SendSQL("SELECT attach_id, description, isprivate
792 793
           FROM attachments
           WHERE bug_id = $::FORM{'bugid'}
794
           AND isobsolete = 0 $canEdit
795 796 797 798
           ORDER BY attach_id");
  my @attachments; # the attachments array
  while ( MoreSQLData() ) {
    my %a; # the attachment hash
799
    ($a{'id'}, $a{'description'}, $a{'isprivate'}) = FetchSQLData();
800 801 802 803 804

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

805 806 807 808
  # 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();
809 810 811 812

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

817
  print Bugzilla->cgi->header();
818 819

  # Generate and return the UI (HTML page) from the appropriate template.
820 821
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
822 823 824 825 826
}


sub insert
{
827 828
  my ($data) = @_;

829 830 831
  # Insert a new attachment into the database.

  # Escape characters in strings that will be used in SQL statements.
832
  $filename = SqlQuote($filename);
833 834
  my $description = SqlQuote($::FORM{'description'});
  my $contenttype = SqlQuote($::FORM{'contenttype'});
835
  my $thedata = SqlQuote($data);
836
  my $isprivate = $::FORM{'isprivate'} ? 1 : 0;
837 838

  # Insert the attachment into the database.
839 840
  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)");
841 842 843 844 845 846

  # 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.
847
  my $comment = "Created an attachment (id=$attachid)\n$::FORM{'description'}\n";
848 849 850 851 852 853 854 855 856
  $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"},
857 858
                $comment,
                $isprivate);
859 860 861

  # Make existing attachments obsolete.
  my $fieldid = GetFieldID('attachments.isobsolete');
862 863
  foreach my $obsolete_id (@{$::MFORM{'obsolete'}}) {
      SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $obsolete_id");
864
      SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
865 866 867 868 869
               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);
      }
870 871
  }

872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916
  # 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])");
          }
      }      
  }   
  
917
  # Define the variables and functions that will be passed to the UI template.
918
  $vars->{'mailrecipients'} =  { 'changer' => $::COOKIE{'Bugzilla_login'} };
919 920 921 922 923 924
  $vars->{'bugid'} = $::FORM{'bugid'};
  $vars->{'attachid'} = $attachid;
  $vars->{'description'} = $description;
  $vars->{'contenttypemethod'} = $::FORM{'contenttypemethod'};
  $vars->{'contenttype'} = $::FORM{'contenttype'};

925
  print Bugzilla->cgi->header();
926 927

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


933 934
sub edit
{
935 936 937 938
  # 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.
939 940 941
  # Users cannot edit the content of the attachment itself.

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

946
  my $isviewable = isViewable($contenttype);
947 948 949 950 951 952 953 954

  # 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();
955 956 957 958 959
  
  # 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' , 
960 961
                                               'product_id'   => $product_id , 
                                               'component_id' => $component_id });
962 963
  foreach my $flag_type (@$flag_types) {
    $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id'   => $flag_type->{'id'}, 
964
                                                    'attach_id' => $::FORM{'id'} });
965 966
  }
  $vars->{'flag_types'} = $flag_types;
967
  $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types);
968
  
969 970 971
  # Define the variables and functions that will be passed to the UI template.
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'description'} = $description; 
972
  $vars->{'contenttype'} = $contenttype; 
973
  $vars->{'filename'} = $filename;
974 975 976 977
  $vars->{'bugid'} = $bugid; 
  $vars->{'bugsummary'} = $bugsummary; 
  $vars->{'ispatch'} = $ispatch; 
  $vars->{'isobsolete'} = $isobsolete; 
978
  $vars->{'isprivate'} = $isprivate; 
979 980
  $vars->{'isviewable'} = $isviewable; 
  $vars->{'attachments'} = \@bugattachments; 
981
  $vars->{'GetBugLink'} = \&GetBugLink;
982

983 984 985 986 987
  # Determine if PatchReader is installed
  eval {
    require PatchReader;
    $vars->{'patchviewerinstalled'} = 1;
  };
988
  print Bugzilla->cgi->header();
989 990

  # Generate and return the UI (HTML page) from the appropriate template.
991 992
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
993 994 995 996 997
}


sub update
{
998
  # Updates an attachment record.
999 1000 1001

  # Get the bug ID for the bug to which this attachment is attached.
  SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
1002 1003 1004
  my $bugid = FetchSQLData();
  unless ($bugid) 
  {
1005 1006
    ThrowUserError("invalid_bug_id",
                   { bug_id => $bugid });
1007 1008
  }
  
1009
  # Lock database tables in preparation for updating the attachment.
1010 1011 1012
  SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " . 
          "flagtypes READ , fielddefs READ , bugs_activity WRITE, " . 
          "flaginclusions AS i READ, flagexclusions AS e READ, " . 
1013 1014
          # cc, bug_group_map, user_group_map, and groups are in here so we
          # can check the permissions of flag requestees and email addresses
1015 1016 1017 1018 1019
          # 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.
1020 1021 1022
          "bugs READ, profiles WRITE, " . 
          "cc READ, bug_group_map READ, user_group_map WRITE, " . 
          "group_group_map READ, groups READ");
1023
  
1024 1025
  # Get a copy of the attachment record before we make changes
  # so we can record those changes in the activity table.
1026
  SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
1027
           FROM attachments WHERE attach_id = $::FORM{'id'}");
1028 1029
  my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
      $oldisobsolete, $oldisprivate) = FetchSQLData();
1030

1031
  # Quote the description and content type for use in the SQL UPDATE statement.
1032
  my $quoteddescription = SqlQuote($::FORM{'description'});
1033
  my $quotedcontenttype = SqlQuote($::FORM{'contenttype'});
1034
  my $quotedfilename = SqlQuote($::FORM{'filename'});
1035 1036 1037 1038 1039

  # 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 , 
1040
                   mimetype = $quotedcontenttype , 
1041
                   filename = $quotedfilename ,
1042
                   ispatch = $::FORM{'ispatch'} , 
1043 1044
                   isobsolete = $::FORM{'isobsolete'} ,
                   isprivate = $::FORM{'isprivate'} 
1045 1046 1047
           WHERE   attach_id = $::FORM{'id'}
         ");

1048 1049 1050 1051
  # Figure out when the changes were made.
  SendSQL("SELECT NOW()");
  my $timestamp = FetchOneColumn();
    
1052
  # Record changes in the activity table.
1053
  my $sql_timestamp = SqlQuote($timestamp);
1054 1055 1056 1057
  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) 
1058
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedolddescription, $quoteddescription)");
1059
  }
1060 1061
  if ($oldcontenttype ne $::FORM{'contenttype'}) {
    my $quotedoldcontenttype = SqlQuote($oldcontenttype);
1062 1063
    my $fieldid = GetFieldID('attachments.mimetype');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
1064
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
1065
  }
1066 1067 1068 1069 1070 1071
  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)");
  }
1072 1073 1074
  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) 
1075
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldispatch, $::FORM{'ispatch'})");
1076 1077 1078 1079
  }
  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) 
1080
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
1081
  }
1082 1083 1084 1085 1086
  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'})");
  }
1087 1088 1089 1090 1091
  
  # Update flags.
  my $target = Bugzilla::Flag::GetTarget(undef, $::FORM{'id'});
  Bugzilla::Flag::process($target, $timestamp, \%::FORM);
  
1092 1093 1094 1095 1096 1097 1098
  # 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'} )
  {
1099 1100 1101 1102
    use Text::Wrap;
    $Text::Wrap::columns = 80;
    $Text::Wrap::huge = 'wrap';

1103 1104 1105 1106
    # 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'};

1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119
    my $wrappedcomment = "";
    foreach my $line (split(/\r\n|\r|\n/, $comment))
    {
      if ( $line =~ /^>/ )
      {
        $wrappedcomment .= $line . "\n";
      }
      else
      {
        $wrappedcomment .= wrap('', '', $line) . "\n";
      }
    }

1120 1121 1122 1123 1124 1125
    # 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.
1126
    AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'}, $timestamp);
1127 1128

  }
1129
  
1130
  # Define the variables and functions that will be passed to the UI template.
1131
  $vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'} };
1132 1133 1134
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'bugid'} = $bugid; 

1135
  print Bugzilla->cgi->header();
1136 1137

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