attachment.cgi 29.6 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
#                 Myk Melez <myk@mozilla.org>
23 24
#                 Daniel Raichle <draichle@gmx.net>
#                 Dave Miller <justdave@syndicomm.com>
25 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 83 84 85 86 87
################################################################################
# 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(); 
}
elsif ($action eq "viewall") 
{ 
  ValidateBugID($::FORM{'bugid'});
  viewall(); 
}
88 89
elsif ($action eq "enter") 
{ 
90
  confirm_login();
91
  ValidateBugID($::FORM{'bugid'});
92
  validateCanChangeBug($::FORM{'bugid'});
93 94 95 96
  enter(); 
}
elsif ($action eq "insert")
{
97
  confirm_login();
98
  ValidateBugID($::FORM{'bugid'});
99
  validateCanChangeBug($::FORM{'bugid'});
100
  ValidateComment($::FORM{'comment'});
101 102
  validateFilename();
  validateIsPatch();
103 104
  my $data = validateData();
  validateDescription();
105 106
  validateContentType() unless $::FORM{'ispatch'};
  validateObsolete() if $::FORM{'obsolete'};
107
  insert($data);
108
}
109 110
elsif ($action eq "edit") 
{ 
111
  quietly_check_login();
112
  validateID();
113
  validateCanEdit($::FORM{'id'});
114 115 116 117 118
  edit(); 
}
elsif ($action eq "update") 
{ 
  confirm_login();
119
  ValidateComment($::FORM{'comment'});
120
  validateID();
121
  validateCanEdit($::FORM{'id'});
122
  validateCanChangeAttachment($::FORM{'id'});
123 124
  validateDescription();
  validateIsPatch();
125
  validateContentType() unless $::FORM{'ispatch'};
126
  validateIsObsolete();
127
  validatePrivate();
128 129 130 131
  
  # 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.
132 133
  Bugzilla::User::match_field({ '^requestee(_type)?-(\d+)$' => 
                                    { 'type' => 'single' } });
134 135 136
  Bugzilla::Flag::validate(\%::FORM, $bugid);
  Bugzilla::FlagType::validate(\%::FORM, $bugid, $::FORM{'id'});
  
137 138 139 140
  update();
}
else 
{ 
141
  ThrowCodeError("unknown_action");
142 143 144 145 146 147 148 149 150 151
}

exit;

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

sub validateID
{
152 153
    # Validate the value of the "id" form field, which must contain an
    # integer that is the ID of an existing attachment.
154

155 156 157 158
    $vars->{'attach_id'} = $::FORM{'id'};
    
    detaint_natural($::FORM{'id'}) 
     || ThrowUserError("invalid_attach_id");
159
  
160 161 162
    # Make sure the attachment exists in the database.
    SendSQL("SELECT bug_id, isprivate FROM attachments WHERE attach_id = $::FORM{'id'}");
    MoreSQLData()
163
      || ThrowUserError("invalid_attach_id");
164

165
    # Make sure the user is authorized to access this attachment's bug.
166
    ($bugid, my $isprivate) = FetchSQLData();
167 168
    ValidateBugID($bugid);
    if (($isprivate > 0 ) && Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
169
        ThrowUserError("attachment_access_denied");
170
    }
171 172
}

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
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()
191 192
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attach_id });
193 194
}

195 196 197 198 199 200 201 202 203
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)
204 205
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attachid });
206 207 208 209 210 211 212 213 214 215
}

sub validateCanChangeBug
{
    my ($bugid) = @_;
    SendSQL("SELECT product_id
             FROM bugs 
             WHERE bug_id = $bugid");
    my $productid = FetchOneColumn();
    CanEditProductId($productid)
216 217
      || ThrowUserError("illegal_attachment_edit_bug",
                        { bug_id => $bugid });
218 219
}

220 221 222
sub validateDescription
{
  $::FORM{'description'}
223
    || ThrowUserError("missing_attachment_description");
224 225 226 227 228 229 230 231
}

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;
232 233 234 235 236 237 238 239 240

  # 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'})
  {
241
    ThrowUserError("missing_content_type_method");
242 243 244
  }
  elsif ($::FORM{'contenttypemethod'} eq 'autodetect')
  {
245
    my $contenttype = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
246 247
    # The user asked us to auto-detect the content type, so use the type
    # specified in the HTTP request headers.
248
    if ( !$contenttype )
249
    {
250
      ThrowUserError("missing_content_type");
251
    }
252
    $::FORM{'contenttype'} = $contenttype;
253 254 255 256 257 258 259 260 261 262 263 264 265
  }
  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
  {
266 267
    $vars->{'contenttypemethod'} = $::FORM{'contenttypemethod'};
    ThrowCodeError("illegal_content_type_method");
268 269 270 271
  }

  if ( $::FORM{'contenttype'} !~ /^(application|audio|image|message|model|multipart|text|video)\/.+$/ )
  {
272 273
    ThrowUserError("invalid_content_type",
                   { contenttype => $::FORM{'contenttype'} });
274
  }
275 276 277 278 279 280 281 282 283 284
}

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

285 286 287 288 289 290 291 292
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;
}

293 294
sub validateData
{
295 296
  my $maxsize = $::FORM{'ispatch'} ? Param('maxpatchsize') : Param('maxattachmentsize');
  $maxsize *= 1024; # Convert from K
297

298 299
  my $fh = $cgi->upload('data');
  my $data;
300

301 302
  # 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.
303
  {
304 305 306
      # enable 'slurp' mode
      local $/;
      $data = <$fh>;
307
  }
308 309 310 311 312 313 314

  $data
    || ThrowUserError("zero_length_file");

  # Make sure the attachment does not exceed the maximum permitted size
  my $len = length($data);
  if ($maxsize && $len > $maxsize) {
315
      my $vars = { filesize => sprintf("%.0f", $len/1024) };
316
      if ( $::FORM{'ispatch'} ) {
317
          ThrowUserError("patch_too_large", $vars);
318
      } else {
319
          ThrowUserError("file_too_large", $vars);
320 321 322 323
      }
  }

  return $data;
324 325
}

326
my $filename;
327 328
sub validateFilename
{
329
  defined $cgi->upload('data')
330
    || ThrowUserError("file_not_specified");
331 332 333 334 335 336 337 338 339 340 341 342 343

  $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);
344 345 346 347 348 349 350
}

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'}}) {
351 352 353
    # my $vars after ThrowCodeError is updated to not use the global
    # vars hash

354 355
    $vars->{'attach_id'} = $attachid;
    
356
    detaint_natural($attachid)
357
      || ThrowCodeError("invalid_attach_id_to_obsolete");
358 359 360 361 362 363
  
    SendSQL("SELECT bug_id, isobsolete, description 
             FROM attachments WHERE attach_id = $attachid");

    # Make sure the attachment exists in the database.
    MoreSQLData()
364
      || ThrowUserError("invalid_attach_id", $vars);
365 366 367

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

368 369
    $vars->{'description'} = $description;
    
370 371
    if ($bugid != $::FORM{'bugid'})
    {
372 373 374
      $vars->{'my_bug_id'} = $::FORM{'bugid'};
      $vars->{'attach_bug_id'} = $bugid;
      ThrowCodeError("mismatched_bug_ids_on_obsolete");
375 376 377 378
    }

    if ( $isobsolete )
    {
379
      ThrowCodeError("attachment_already_obsolete");
380
    }
381 382 383

    # Check that the user can modify this attachment
    validateCanEdit($attachid);
384 385 386 387
  }

}

388 389 390 391 392 393
################################################################################
# Functions
################################################################################

sub view
{
394
    # Display an attachment.
395

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

400
    # Return the appropriate HTTP response headers.
401 402
    $filename =~ s/^.*[\/\\]//;
    my $filesize = length($thedata);
403

404
    print Bugzilla->cgi->header(-type=>"$contenttype; name=\"$filename\"",
405
                                -content_disposition=> "inline; filename=$filename",
406 407 408
                                -content_length => $filesize);

    print $thedata;
409 410 411 412 413 414 415 416 417
}


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.
418 419 420 421
    my $privacy = "";
    if (Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
        $privacy = "AND isprivate < 1 ";
    }
422 423
    SendSQL("SELECT attach_id, DATE_FORMAT(creation_ts, '%Y.%m.%d %H:%i'),
            mimetype, description, ispatch, isobsolete, isprivate 
424 425
            FROM attachments WHERE bug_id = $::FORM{'bugid'} $privacy 
            ORDER BY attach_id");
426
  my @attachments; # the attachments array
427 428
  while (MoreSQLData())
  {
429
    my %a; # the attachment hash
430
    ($a{'attachid'}, $a{'date'}, $a{'contenttype'}, 
431
     $a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, $a{'isprivate'}) = FetchSQLData();
432 433 434 435 436

    # 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!
437
    $a{'isviewable'} = ( $a{'contenttype'} =~ /^(text|image|application\/vnd\.mozilla\.)/ );
438 439 440 441 442

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

443 444 445 446
  # 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();
447 448 449 450

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

455
  print Bugzilla->cgi->header();
456 457

  # Generate and return the UI (HTML page) from the appropriate template.
458 459
  $template->process("attachment/show-multiple.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
460 461 462
}


463 464 465 466
sub enter
{
  # Display a form for entering a new attachment.

467 468 469 470 471 472
  # 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";
  }
473
  SendSQL("SELECT attach_id, description, isprivate
474 475
           FROM attachments
           WHERE bug_id = $::FORM{'bugid'}
476
           AND isobsolete = 0 $canEdit
477 478 479 480
           ORDER BY attach_id");
  my @attachments; # the attachments array
  while ( MoreSQLData() ) {
    my %a; # the attachment hash
481
    ($a{'id'}, $a{'description'}, $a{'isprivate'}) = FetchSQLData();
482 483 484 485 486

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

487 488 489 490
  # 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();
491 492 493 494

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

499
  print Bugzilla->cgi->header();
500 501

  # Generate and return the UI (HTML page) from the appropriate template.
502 503
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
504 505 506 507 508
}


sub insert
{
509 510
  my ($data) = @_;

511 512 513
  # Insert a new attachment into the database.

  # Escape characters in strings that will be used in SQL statements.
514
  $filename = SqlQuote($filename);
515 516
  my $description = SqlQuote($::FORM{'description'});
  my $contenttype = SqlQuote($::FORM{'contenttype'});
517
  my $thedata = SqlQuote($data);
518
  my $isprivate = $::FORM{'isprivate'} ? 1 : 0;
519 520

  # Insert the attachment into the database.
521 522
  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)");
523 524 525 526 527 528

  # 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.
529
  my $comment = "Created an attachment (id=$attachid)\n$::FORM{'description'}\n";
530 531 532 533 534 535 536 537 538
  $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"},
539 540
                $comment,
                $isprivate);
541 542 543

  # Make existing attachments obsolete.
  my $fieldid = GetFieldID('attachments.isobsolete');
544 545
  foreach my $obsolete_id (@{$::MFORM{'obsolete'}}) {
      SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $obsolete_id");
546
      SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
547 548 549 550 551
               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);
      }
552 553
  }

554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
  # 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])");
          }
      }      
  }   
  
599
  # Define the variables and functions that will be passed to the UI template.
600
  $vars->{'mailrecipients'} =  { 'changer' => $::COOKIE{'Bugzilla_login'} };
601 602 603 604 605 606
  $vars->{'bugid'} = $::FORM{'bugid'};
  $vars->{'attachid'} = $attachid;
  $vars->{'description'} = $description;
  $vars->{'contenttypemethod'} = $::FORM{'contenttypemethod'};
  $vars->{'contenttype'} = $::FORM{'contenttype'};

607
  print Bugzilla->cgi->header();
608 609

  # Generate and return the UI (HTML page) from the appropriate template.
610 611
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
612 613 614
}


615 616
sub edit
{
617 618 619 620
  # 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.
621 622 623
  # Users cannot edit the content of the attachment itself.

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

  # Flag attachment as to whether or not it can be viewed (as opposed to
629 630
  # being downloaded).  Currently I decide it is viewable if its content
  # type is either text/.* or application/vnd.mozilla.*.
631
  # !!! Yuck, what an ugly hack.  Fix it!
632
  my $isviewable = ( $contenttype =~ /^(text|image|application\/vnd\.mozilla\.)/ );
633 634 635 636 637 638 639 640

  # 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();
641 642 643 644 645
  
  # 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' , 
646 647
                                               'product_id'   => $product_id , 
                                               'component_id' => $component_id });
648 649
  foreach my $flag_type (@$flag_types) {
    $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id'   => $flag_type->{'id'}, 
650
                                                    'attach_id' => $::FORM{'id'} });
651 652
  }
  $vars->{'flag_types'} = $flag_types;
653
  $vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types);
654
  
655 656 657
  # Define the variables and functions that will be passed to the UI template.
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'description'} = $description; 
658
  $vars->{'contenttype'} = $contenttype; 
659
  $vars->{'filename'} = $filename;
660 661 662 663
  $vars->{'bugid'} = $bugid; 
  $vars->{'bugsummary'} = $bugsummary; 
  $vars->{'ispatch'} = $ispatch; 
  $vars->{'isobsolete'} = $isobsolete; 
664
  $vars->{'isprivate'} = $isprivate; 
665 666
  $vars->{'isviewable'} = $isviewable; 
  $vars->{'attachments'} = \@bugattachments; 
667
  $vars->{'GetBugLink'} = \&GetBugLink;
668

669
  print Bugzilla->cgi->header();
670 671

  # Generate and return the UI (HTML page) from the appropriate template.
672 673
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
674 675 676 677 678
}


sub update
{
679
  # Updates an attachment record.
680 681 682

  # Get the bug ID for the bug to which this attachment is attached.
  SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
683 684 685
  my $bugid = FetchSQLData();
  unless ($bugid) 
  {
686 687
    ThrowUserError("invalid_bug_id",
                   { bug_id => $bugid });
688 689
  }
  
690
  # Lock database tables in preparation for updating the attachment.
691 692 693
  SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " . 
          "flagtypes READ , fielddefs READ , bugs_activity WRITE, " . 
          "flaginclusions AS i READ, flagexclusions AS e READ, " . 
694 695
          # cc, bug_group_map, user_group_map, and groups are in here so we
          # can check the permissions of flag requestees and email addresses
696 697 698 699 700
          # 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.
701 702 703
          "bugs READ, profiles WRITE, " . 
          "cc READ, bug_group_map READ, user_group_map WRITE, " . 
          "group_group_map READ, groups READ");
704
  
705 706
  # Get a copy of the attachment record before we make changes
  # so we can record those changes in the activity table.
707
  SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
708
           FROM attachments WHERE attach_id = $::FORM{'id'}");
709 710
  my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
      $oldisobsolete, $oldisprivate) = FetchSQLData();
711

712
  # Quote the description and content type for use in the SQL UPDATE statement.
713
  my $quoteddescription = SqlQuote($::FORM{'description'});
714
  my $quotedcontenttype = SqlQuote($::FORM{'contenttype'});
715
  my $quotedfilename = SqlQuote($::FORM{'filename'});
716 717 718 719 720

  # 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 , 
721
                   mimetype = $quotedcontenttype , 
722
                   filename = $quotedfilename ,
723
                   ispatch = $::FORM{'ispatch'} , 
724 725
                   isobsolete = $::FORM{'isobsolete'} ,
                   isprivate = $::FORM{'isprivate'} 
726 727 728
           WHERE   attach_id = $::FORM{'id'}
         ");

729 730 731 732
  # Figure out when the changes were made.
  SendSQL("SELECT NOW()");
  my $timestamp = FetchOneColumn();
    
733
  # Record changes in the activity table.
734
  my $sql_timestamp = SqlQuote($timestamp);
735 736 737 738
  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) 
739
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedolddescription, $quoteddescription)");
740
  }
741 742
  if ($oldcontenttype ne $::FORM{'contenttype'}) {
    my $quotedoldcontenttype = SqlQuote($oldcontenttype);
743 744
    my $fieldid = GetFieldID('attachments.mimetype');
    SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added) 
745
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
746
  }
747 748 749 750 751 752
  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)");
  }
753 754 755
  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) 
756
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldispatch, $::FORM{'ispatch'})");
757 758 759 760
  }
  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) 
761
             VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
762
  }
763 764 765 766 767
  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'})");
  }
768 769 770 771 772
  
  # Update flags.
  my $target = Bugzilla::Flag::GetTarget(undef, $::FORM{'id'});
  Bugzilla::Flag::process($target, $timestamp, \%::FORM);
  
773 774 775 776 777 778 779
  # 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'} )
  {
780 781 782 783
    use Text::Wrap;
    $Text::Wrap::columns = 80;
    $Text::Wrap::huge = 'wrap';

784 785 786 787
    # 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'};

788 789 790 791 792 793 794 795 796 797 798 799 800
    my $wrappedcomment = "";
    foreach my $line (split(/\r\n|\r|\n/, $comment))
    {
      if ( $line =~ /^>/ )
      {
        $wrappedcomment .= $line . "\n";
      }
      else
      {
        $wrappedcomment .= wrap('', '', $line) . "\n";
      }
    }

801 802 803 804 805 806
    # 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.
807
    AppendComment($bugid, $who, $wrappedcomment, $::FORM{'isprivate'}, $timestamp);
808 809

  }
810
  
811
  # Define the variables and functions that will be passed to the UI template.
812
  $vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'} };
813 814 815
  $vars->{'attachid'} = $::FORM{'id'}; 
  $vars->{'bugid'} = $bugid; 

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

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