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

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

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

38 39
use lib qw(.);

40
use Bugzilla;
41
use Bugzilla::Constants;
42
use Bugzilla::Error;
43 44
use Bugzilla::Flag; 
use Bugzilla::FlagType; 
45
use Bugzilla::User;
46
use Bugzilla::Util;
47
use Bugzilla::Bug;
48
use Bugzilla::Field;
49
use Bugzilla::Attachment;
50
use Bugzilla::Attachment::PatchReader;
51
use Bugzilla::Token;
52

53
Bugzilla->login();
54

55 56 57 58 59 60 61
# For most scripts we don't make $cgi and $template global variables. But
# when preparing Bugzilla for mod_perl, this script used these
# variables in so many subroutines that it was easier to just
# make them globals.
local our $cgi = Bugzilla->cgi;
local our $template = Bugzilla->template;
local our $vars = {};
62

63 64 65 66
################################################################################
# Main Body Execution
################################################################################

67 68 69 70
# All calls to this script should contain an "action" variable whose
# value determines what the user wants to do.  The code below checks
# the value of that variable and runs the appropriate code. If none is
# supplied, we default to 'view'.
71 72

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

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

exit;

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

124 125 126 127 128 129 130 131 132 133 134 135
# Validates an attachment ID. Optionally takes a parameter of a form
# variable name that contains the ID to be validated. If not specified,
# uses 'id'.
# 
# Will throw an error if 1) attachment ID is not a valid number,
# 2) attachment does not exist, or 3) user isn't allowed to access the
# attachment.
#
# Returns a list, where the first item is the validated, detainted
# attachment id, and the 2nd item is the bug id corresponding to the
# attachment.
# 
136 137
sub validateID
{
138
    my $param = @_ ? $_[0] : 'id';
139
    my $dbh = Bugzilla->dbh;
140 141
    my $user = Bugzilla->user;

142 143 144
    # If we're not doing interdiffs, check if id wasn't specified and
    # prompt them with a page that allows them to choose an attachment.
    # Happens when calling plain attachment.cgi from the urlbar directly
145 146
    if ($param eq 'id' && !$cgi->param('id')) {

147
        print $cgi->header();
148 149 150 151
        $template->process("attachment/choose.html.tmpl", $vars) ||
            ThrowTemplateError($template->error());
        exit;
    }
152
    
153 154 155 156 157 158 159
    my $attach_id = $cgi->param($param);

    # Validate the specified attachment id. detaint kills $attach_id if
    # non-natural, so use the original value from $cgi in our exception
    # message here.
    detaint_natural($attach_id)
     || ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) });
160
  
161
    # Make sure the attachment exists in the database.
162 163
    my ($bugid, $isprivate, $submitter_id) = $dbh->selectrow_array(
                                    "SELECT bug_id, isprivate, submitter_id
164 165 166 167 168
                                     FROM attachments 
                                     WHERE attach_id = ?",
                                     undef, $attach_id);
    ThrowUserError("invalid_attach_id", { attach_id => $attach_id }) 
        unless $bugid;
169

170 171
    # Make sure the user is authorized to access this attachment's bug.
    ValidateBugID($bugid);
172 173 174
    if ($isprivate && $user->id != $submitter_id && !$user->is_insider) {
        ThrowUserError('auth_failure', {action => 'access',
                                        object => 'attachment'});
175
    }
176

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

180 181 182
# Validates format of a diff/interdiff. Takes a list as an parameter, which
# defines the valid format values. Will throw an error if the format is not
# in the list. Returns either the user selected or default format.
183 184
sub validateFormat
{
185 186 187
  # receives a list of legal formats; first item is a default
  my $format = $cgi->param('format') || $_[0];
  if ( lsearch(\@_, $format) == -1)
188
  {
189
     ThrowUserError("invalid_format", { format  => $format, formats => \@_ });
190
  }
191

192
  return $format;
193 194
}

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

  return $context;
206 207
}

208 209 210
sub validateCanChangeAttachment 
{
    my ($attachid) = @_;
211 212 213
    my $dbh = Bugzilla->dbh;
    my ($productid) = $dbh->selectrow_array(
            "SELECT product_id
214 215 216
             FROM attachments
             INNER JOIN bugs
             ON bugs.bug_id = attachments.bug_id
217 218
             WHERE attach_id = ?", undef, $attachid);

219
    Bugzilla->user->can_edit_product($productid)
220 221
      || ThrowUserError("illegal_attachment_edit",
                        { attach_id => $attachid });
222 223 224 225 226
}

sub validateCanChangeBug
{
    my ($bugid) = @_;
227 228 229
    my $dbh = Bugzilla->dbh;
    my ($productid) = $dbh->selectrow_array(
            "SELECT product_id
230
             FROM bugs 
231 232
             WHERE bug_id = ?", undef, $bugid);

233
    Bugzilla->user->can_edit_product($productid)
234 235
      || ThrowUserError("illegal_attachment_edit_bug",
                        { bug_id => $bugid });
236 237
}

238 239
sub validateIsObsolete
{
240 241 242 243
    # Set the isobsolete flag to zero if it is undefined, since the UI uses
    # an HTML checkbox to represent this flag, and unchecked HTML checkboxes
    # do not get sent in HTML requests.
    $cgi->param('isobsolete', $cgi->param('isobsolete') ? 1 : 0);
244 245
}

246 247 248 249 250
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.
251
    $cgi->param('isprivate', $cgi->param('isprivate') ? 1 : 0);
252 253
}

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
# 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;
  }
274

275 276 277 278 279 280 281 282 283
  # 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;
284 285
}

286 287 288 289
################################################################################
# Functions
################################################################################

290
# Display an attachment.
291 292
sub view
{
293 294
    # Retrieve and validate parameters
    my ($attach_id) = validateID();
295 296
    my $dbh = Bugzilla->dbh;
    
297
    # Retrieve the attachment content and its content type from the database.
298 299
    my ($contenttype, $filename, $thedata) = $dbh->selectrow_array(
            "SELECT mimetype, filename, thedata FROM attachments " .
300
            "INNER JOIN attach_data ON id = attach_id " .
301
            "WHERE attach_id = ?", undef, $attach_id);
302
   
303 304 305
    # Bug 111522: allow overriding content-type manually in the posted form
    # params.
    if (defined $cgi->param('content_type'))
306
    {
307 308
        $cgi->param('contenttypemethod', 'manual');
        $cgi->param('contenttypeentry', $cgi->param('content_type'));
309
        Bugzilla::Attachment->validate_content_type(THROW_ERROR);
310
        $contenttype = $cgi->param('content_type');
311
    }
312

313
    # Return the appropriate HTTP response headers.
314 315
    $filename =~ s/^.*[\/\\]//;
    my $filesize = length($thedata);
316 317 318 319
    # A zero length attachment in the database means the attachment is 
    # stored in a local file
    if ($filesize == 0)
    {
320
        my $hash = ($attach_id % 100) + 100;
321
        $hash =~ s/.*(\d\d)$/group.$1/;
322
        if (open(AH, bz_locations()->{'attachdir'} . "/$hash/attachment.$attach_id")) {
323 324 325 326 327 328 329 330 331
            binmode AH;
            $filesize = (stat(AH))[7];
        }
    }
    if ($filesize == 0)
    {
        ThrowUserError("attachment_removed");
    }

332

333 334 335 336
    # escape quotes and backslashes in the filename, per RFCs 2045/822
    $filename =~ s/\\/\\\\/g; # escape backslashes
    $filename =~ s/"/\\"/g; # escape quotes

337 338 339
    print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
                       -content_disposition=> "inline; filename=\"$filename\"",
                       -content_length => $filesize);
340

341 342 343 344 345 346 347 348 349
    if ($thedata) {
        print $thedata;
    } else {
        while (<AH>) {
            print $_;
        }
        close(AH);
    }

350 351
}

352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
sub interdiff {
    # Retrieve and validate parameters
    my ($old_id) = validateID('oldid');
    my ($new_id) = validateID('newid');
    my $format = validateFormat('html', 'raw');
    my $context = validateContext();

    # XXX - validateID should be replaced by Attachment::check_attachment()
    # and should return an attachment object. This would save us a lot of
    # trouble.
    my $old_attachment = Bugzilla::Attachment->get($old_id);
    my $new_attachment = Bugzilla::Attachment->get($new_id);

    Bugzilla::Attachment::PatchReader::process_interdiff(
        $old_attachment, $new_attachment, $format, $context);
367 368
}

369 370 371 372 373
sub diff {
    # Retrieve and validate parameters
    my ($attach_id) = validateID();
    my $format = validateFormat('html', 'raw');
    my $context = validateContext();
374

375
    my $attachment = Bugzilla::Attachment->get($attach_id);
376

377 378 379 380
    # If it is not a patch, view normally.
    if (!$attachment->ispatch) {
        view();
        return;
381 382
    }

383
    Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
384
}
385

386 387
# Display all attachments for a given bug in a series of IFRAMEs within one
# HTML page.
388
sub viewall {
389 390 391
    # Retrieve and validate parameters
    my $bugid = $cgi->param('bugid');
    ValidateBugID($bugid);
392
    my $bug = new Bugzilla::Bug($bugid);
393

394
    my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
395

396 397
    foreach my $a (@$attachments) {
        $a->{'isviewable'} = isViewable($a->contenttype);
398
    }
399

400 401 402
    # Define the variables and functions that will be passed to the UI template.
    $vars->{'bug'} = $bug;
    $vars->{'attachments'} = $attachments;
403

404
    print $cgi->header();
405

406 407 408
    # Generate and return the UI (HTML page) from the appropriate template.
    $template->process("attachment/show-multiple.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
409 410
}

411
# Display a form for entering a new attachment.
412 413
sub enter
{
414 415 416 417
  # Retrieve and validate parameters
  my $bugid = $cgi->param('bugid');
  ValidateBugID($bugid);
  validateCanChangeBug($bugid);
418
  my $dbh = Bugzilla->dbh;
419 420 421
  my $user = Bugzilla->user;

  my $bug = new Bugzilla::Bug($bugid, $user->id);
422 423 424
  # 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 = "";
425 426
  if (!$user->in_group('editbugs', $bug->product_id)) {
      $canEdit = "AND submitter_id = " . $user->id;
427
  }
428 429
  my $attachments = $dbh->selectall_arrayref(
          "SELECT attach_id AS id, description, isprivate
430
           FROM attachments
431
           WHERE bug_id = ? 
432
           AND isobsolete = 0 $canEdit
433
           ORDER BY attach_id",{'Slice' =>{}}, $bugid);
434 435

  # Define the variables and functions that will be passed to the UI template.
436
  $vars->{'bug'} = $bug;
437
  $vars->{'attachments'} = $attachments;
438

439
  my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
440 441
                                              'product_id'   => $bug->product_id,
                                              'component_id' => $bug->component_id});
442
  $vars->{'flag_types'} = $flag_types;
443
  $vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
444

445
  print $cgi->header();
446 447

  # Generate and return the UI (HTML page) from the appropriate template.
448 449
  $template->process("attachment/create.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
450 451
}

452
# Insert a new attachment into the database.
453 454
sub insert
{
455 456
    my $dbh = Bugzilla->dbh;
    my $user = Bugzilla->user;
457

458 459 460 461 462
    # Retrieve and validate parameters
    my $bugid = $cgi->param('bugid');
    ValidateBugID($bugid);
    validateCanChangeBug($bugid);
    ValidateComment(scalar $cgi->param('comment'));
463
    my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); 
464

465
    my $bug = new Bugzilla::Bug($bugid);
466
    my $attachid =
467
        Bugzilla::Attachment->insert_attachment_for_bug(THROW_ERROR, $bug, $user,
468
                                                        $timestamp, \$vars);
469

470
  # Insert a comment about the new attachment into the database.
471 472 473
  my $comment = "Created an attachment (id=$attachid)\n" .
                $cgi->param('description') . "\n";
  $comment .= ("\n" . $cgi->param('comment')) if defined $cgi->param('comment');
474

475
  my $isprivate = $cgi->param('isprivate') ? 1 : 0;
476
  AppendComment($bugid, $user->id, $comment, $isprivate, $timestamp);
477

478
  # Assign the bug to the user, if they are allowed to take it
479
  my $owner = "";
480
  
481
  if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
482
      
483 484
      my @fields = ("assigned_to", "bug_status", "resolution", "everconfirmed",
                    "login_name");
485 486
      
      # Get the old values, for the bugs_activity table
487 488
      my @oldvalues = $dbh->selectrow_array(
              "SELECT " . join(", ", @fields) . " " .
489 490 491
              "FROM bugs " .
              "INNER JOIN profiles " .
              "ON profiles.userid = bugs.assigned_to " .
492
              "WHERE bugs.bug_id = ?", undef, $bugid);
493
      
494
      my @newvalues = ($user->id, "ASSIGNED", "", 1, $user->login);
495 496
      
      # Make sure the person we are taking the bug from gets mail.
497
      $owner = $oldvalues[4];  
498

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

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

508
      # We store email addresses in the bugs_activity table rather than IDs.
509 510
      $oldvalues[0] = $oldvalues[4];
      $newvalues[0] = $newvalues[4];
511

512
      for (my $i = 0; $i < 4; $i++) {
513
          if ($oldvalues[$i] ne $newvalues[$i]) {
514
              LogActivityEntry($bugid, $fields[$i], $oldvalues[$i],
515
                               $newvalues[$i], $user->id, $timestamp);
516 517 518
          }
      }      
  }   
519

520
  # Define the variables and functions that will be passed to the UI template.
521
  $vars->{'mailrecipients'} =  { 'changer' => $user->login,
522
                                 'owner'   => $owner };
523
  $vars->{'bugid'} = $bugid;
524
  $vars->{'attachid'} = $attachid;
525
  $vars->{'description'} = $cgi->param('description');
526 527
  $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
  $vars->{'contenttype'} = $cgi->param('contenttype');
528

529
  print $cgi->header();
530 531

  # Generate and return the UI (HTML page) from the appropriate template.
532 533
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
534 535
}

536 537 538 539
# Displays a form for editing attachment properties.
# Any user is allowed to access this page, unless the attachment
# is private and the user does not belong to the insider group.
# Validations are done later when the user submits changes.
540
sub edit {
541
  my ($attach_id) = validateID();
542
  my $dbh = Bugzilla->dbh;
543

544 545
  my $attachment = Bugzilla::Attachment->get($attach_id);
  my $isviewable = !$attachment->isurl && isViewable($attachment->contenttype);
546 547 548

  # 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.
549
  my $bugattachments =
550 551 552
      Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
  # We only want attachment IDs.
  @$bugattachments = map { $_->id } @$bugattachments;
553 554 555 556 557 558

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

559
  # Get a list of flag types that can be set for this attachment.
560 561
  my $flag_types = Bugzilla::FlagType::match({ 'target_type'  => 'attachment' ,
                                               'product_id'   => $product_id ,
562
                                               'component_id' => $component_id });
563
  foreach my $flag_type (@$flag_types) {
564
    $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id'   => $flag_type->id,
565
                                                    'attach_id' => $attachment->id });
566 567
  }
  $vars->{'flag_types'} = $flag_types;
568
  $vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
569
  $vars->{'attachment'} = $attachment;
570 571
  $vars->{'bugsummary'} = $bugsummary; 
  $vars->{'isviewable'} = $isviewable; 
572
  $vars->{'attachments'} = $bugattachments; 
573

574 575 576 577 578
  # Determine if PatchReader is installed
  eval {
    require PatchReader;
    $vars->{'patchviewerinstalled'} = 1;
  };
579
  print $cgi->header();
580 581

  # Generate and return the UI (HTML page) from the appropriate template.
582 583
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
584 585
}

586 587 588 589 590
# Updates an attachment record. Users with "editbugs" privileges, (or the
# original attachment's submitter) can edit the attachment's description,
# content type, ispatch and isobsolete flags, and statuses, and they can
# also submit a comment that appears in the bug.
# Users cannot edit the content of the attachment itself.
591 592
sub update
{
593 594 595
    my $user = Bugzilla->user;
    my $userid = $user->id;
    my $dbh = Bugzilla->dbh;
596 597 598 599

    # Retrieve and validate parameters
    ValidateComment(scalar $cgi->param('comment'));
    my ($attach_id, $bugid) = validateID();
600
    my $bug = new Bugzilla::Bug($bugid);
601
    my $attachment = Bugzilla::Attachment->get($attach_id);
602
    $attachment->validate_can_edit($bug->product_id);
603
    validateCanChangeAttachment($attach_id);
604 605 606
    Bugzilla::Attachment->validate_description(THROW_ERROR);
    Bugzilla::Attachment->validate_is_patch(THROW_ERROR);
    Bugzilla::Attachment->validate_content_type(THROW_ERROR) unless $cgi->param('ispatch');
607 608
    validateIsObsolete();
    validatePrivate();
609 610 611 612 613 614 615 616 617 618 619 620 621

    # If the submitter of the attachment is not in the insidergroup,
    # be sure that he cannot overwrite the private bit.
    # This check must be done before calling Bugzilla::Flag*::validate(),
    # because they will look at the private bit when checking permissions.
    # XXX - This is a ugly hack. Ideally, we shouldn't have to look at the
    # old private bit twice (first here, and then below again), but this is
    # the less risky change.
    unless ($user->is_insider) {
        my $oldisprivate = $dbh->selectrow_array('SELECT isprivate FROM attachments
                                                  WHERE attach_id = ?', undef, $attach_id);
        $cgi->param('isprivate', $oldisprivate);
    }
622

623 624 625
    # The order of these function calls is important, as Flag::validate
    # assumes User::match_field has ensured that the values in the
    # requestee fields are legitimate user email addresses.
626
    Bugzilla::User::match_field($cgi, {
627
        '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }
628
    });
629
    Bugzilla::Flag::validate($cgi, $bugid, $attach_id);
630

631 632
    # Lock database tables in preparation for updating the attachment.
    $dbh->bz_lock_tables('attachments WRITE', 'flags WRITE' ,
633 634
          'flagtypes READ', 'fielddefs READ', 'bugs_activity WRITE',
          'flaginclusions AS i READ', 'flagexclusions AS e READ',
635 636
          # cc, bug_group_map, user_group_map, and groups are in here so we
          # can check the permissions of flag requestees and email addresses
637
          # on the flag type cc: lists via the CanSeeBug
638 639 640 641
          # function call in Flag::notify. group_group_map is in here si
          # Bugzilla::User can flatten groups.
          'bugs WRITE', 'profiles READ', 'email_setting READ',
          'cc READ', 'bug_group_map READ', 'user_group_map READ',
642
          'group_group_map READ', 'groups READ', 'group_control_map READ');
643

644 645
  # Get a copy of the attachment record before we make changes
  # so we can record those changes in the activity table.
646
  my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
647 648 649
      $oldisobsolete, $oldisprivate) = $dbh->selectrow_array(
      "SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
       FROM attachments WHERE attach_id = ?", undef, $attach_id);
650

651
  # Quote the description and content type for use in the SQL UPDATE statement.
652 653 654 655 656 657 658
  my $description = $cgi->param('description');
  my $contenttype = $cgi->param('contenttype');
  my $filename = $cgi->param('filename');
  # we can detaint this way thanks to placeholders
  trick_taint($description);
  trick_taint($contenttype);
  trick_taint($filename);
659

660
  # Figure out when the changes were made.
661
  my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
662
    
663 664 665 666
  # Update flags.  We have to do this before committing changes
  # to attachments so that we can delete pending requests if the user
  # is obsoleting this attachment without deleting any requests
  # the user submits at the same time.
667
  Bugzilla::Flag::process($bug, $attachment, $timestamp, $cgi);
668

669
  # Update the attachment record in the database.
670 671 672 673 674 675 676 677 678 679 680
  $dbh->do("UPDATE  attachments 
            SET     description = ?,
                    mimetype    = ?,
                    filename    = ?,
                    ispatch     = ?,
                    isobsolete  = ?,
                    isprivate   = ?
            WHERE   attach_id   = ?",
            undef, ($description, $contenttype, $filename,
            $cgi->param('ispatch'), $cgi->param('isobsolete'), 
            $cgi->param('isprivate'), $attach_id));
681 682

  # Record changes in the activity table.
683
  if ($olddescription ne $cgi->param('description')) {
684
    my $fieldid = get_field_id('attachments.description');
685
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
686
                                        fieldid, removed, added)
687 688 689
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $olddescription, $description));
690
  }
691
  if ($oldcontenttype ne $cgi->param('contenttype')) {
692
    my $fieldid = get_field_id('attachments.mimetype');
693
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
694
                                        fieldid, removed, added)
695 696 697
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldcontenttype, $contenttype));
698
  }
699
  if ($oldfilename ne $cgi->param('filename')) {
700
    my $fieldid = get_field_id('attachments.filename');
701
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
702
                                        fieldid, removed, added)
703 704 705
              VALUES (?,?,?,?,?,?,?)", 
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldfilename, $filename));
706
  }
707
  if ($oldispatch ne $cgi->param('ispatch')) {
708
    my $fieldid = get_field_id('attachments.ispatch');
709
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
710
                                        fieldid, removed, added)
711 712 713
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldispatch, $cgi->param('ispatch')));
714
  }
715
  if ($oldisobsolete ne $cgi->param('isobsolete')) {
716
    my $fieldid = get_field_id('attachments.isobsolete');
717
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
718
                                        fieldid, removed, added)
719 720 721
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldisobsolete, $cgi->param('isobsolete')));
722
  }
723
  if ($oldisprivate ne $cgi->param('isprivate')) {
724
    my $fieldid = get_field_id('attachments.isprivate');
725
    $dbh->do("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
726
                                        fieldid, removed, added)
727 728 729
              VALUES (?,?,?,?,?,?,?)",
              undef, ($bugid, $attach_id, $userid, $timestamp, $fieldid,
                     $oldisprivate, $cgi->param('isprivate')));
730
  }
731
  
732
  # Unlock all database tables now that we are finished updating the database.
733
  $dbh->bz_unlock_tables();
734

735
  # If the user submitted a comment while editing the attachment,
736
  # add the comment to the bug.
737
  if ($cgi->param('comment'))
738
  {
739 740 741 742
    # Prepend a string to the comment to let users know that the comment came
    # from the "edit attachment" screen.
    my $comment = qq|(From update of attachment $attach_id)\n| .
                  $cgi->param('comment');
743 744

    # Append the comment to the list of comments in the database.
745
    AppendComment($bugid, $userid, $comment, $cgi->param('isprivate'), $timestamp);
746
  }
747
  
748
  # Define the variables and functions that will be passed to the UI template.
749
  $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login };
750
  $vars->{'attachid'} = $attach_id; 
751 752
  $vars->{'bugid'} = $bugid; 

753
  print $cgi->header();
754 755

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

# Only administrators can delete attachments.
sub delete_attachment {
    my $user = Bugzilla->login(LOGIN_REQUIRED);
    my $dbh = Bugzilla->dbh;

    print $cgi->header();

    $user->in_group('admin')
      || ThrowUserError('auth_failure', {group  => 'admin',
                                         action => 'delete',
                                         object => 'attachment'});

772
    Bugzilla->params->{'allow_attachment_deletion'}
773 774 775 776
      || ThrowUserError('attachment_deletion_disabled');

    # Make sure the administrator is allowed to edit this attachment.
    my ($attach_id, $bug_id) = validateID();
777
    my $attachment = Bugzilla::Attachment->get($attach_id);
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817
    validateCanChangeAttachment($attach_id);

    $attachment->datasize || ThrowUserError('attachment_removed');

    # We don't want to let a malicious URL accidentally delete an attachment.
    my $token = trim($cgi->param('token'));
    if ($token) {
        my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
        unless ($creator_id
                  && ($creator_id == $user->id)
                  && ($event eq "attachment$attach_id"))
        {
            # The token is invalid.
            ThrowUserError('token_inexistent');
        }

        # The token is valid. Delete the content of the attachment.
        my $msg;
        $vars->{'attachid'} = $attach_id;
        $vars->{'bugid'} = $bug_id;
        $vars->{'date'} = $date;
        $vars->{'reason'} = clean_text($cgi->param('reason') || '');
        $vars->{'mailrecipients'} = { 'changer' => $user->login };

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

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

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

        # Now delete the token.
818
        delete_token($token);
819 820 821 822 823 824 825 826 827

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

        $template->process("attachment/updated.html.tmpl", $vars)
          || ThrowTemplateError($template->error());
    }
    else {
        # Create a token.
828
        $token = issue_session_token('attachment' . $attach_id);
829 830 831 832 833 834 835 836

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

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