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

433
  # Retrieve the bug summary (for displaying on screen) and assignee.
434 435 436
  my ($bugsummary, $assignee_id) = $dbh->selectrow_array(
          "SELECT short_desc, assigned_to FROM bugs 
           WHERE bug_id = ?", undef, $bugid);
437 438

  # Define the variables and functions that will be passed to the UI template.
439
  $vars->{'bugid'} = $bugid;
440
  $vars->{'attachments'} = $attachments;
441 442
  $vars->{'bugassignee_id'} = $assignee_id;
  $vars->{'bugsummary'} = $bugsummary;
443

444 445 446 447
  my ($product_id, $component_id)= $dbh->selectrow_array(
          "SELECT product_id, component_id FROM bugs
           WHERE bug_id = ?", undef, $bugid);
           
448 449 450 451
  my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
                                              'product_id'   => $product_id,
                                              'component_id' => $component_id});
  $vars->{'flag_types'} = $flag_types;
452
  $vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
453

454
  print $cgi->header();
455 456

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

461
# Insert a new attachment into the database.
462 463
sub insert
{
464 465
    my $dbh = Bugzilla->dbh;
    my $user = Bugzilla->user;
466

467 468 469 470 471
    # Retrieve and validate parameters
    my $bugid = $cgi->param('bugid');
    ValidateBugID($bugid);
    validateCanChangeBug($bugid);
    ValidateComment(scalar $cgi->param('comment'));
472
    my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); 
473

474
    my $bug = new Bugzilla::Bug($bugid);
475
    my $attachid =
476
        Bugzilla::Attachment->insert_attachment_for_bug(THROW_ERROR, $bug, $user,
477
                                                        $timestamp, \$vars);
478

479
  # Insert a comment about the new attachment into the database.
480 481 482
  my $comment = "Created an attachment (id=$attachid)\n" .
                $cgi->param('description') . "\n";
  $comment .= ("\n" . $cgi->param('comment')) if defined $cgi->param('comment');
483

484
  my $isprivate = $cgi->param('isprivate') ? 1 : 0;
485
  AppendComment($bugid, $user->id, $comment, $isprivate, $timestamp);
486

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

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

513 514 515 516
      # 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);

517
      # We store email addresses in the bugs_activity table rather than IDs.
518 519
      $oldvalues[0] = $oldvalues[4];
      $newvalues[0] = $newvalues[4];
520

521
      for (my $i = 0; $i < 4; $i++) {
522
          if ($oldvalues[$i] ne $newvalues[$i]) {
523
              LogActivityEntry($bugid, $fields[$i], $oldvalues[$i],
524
                               $newvalues[$i], $user->id, $timestamp);
525 526 527
          }
      }      
  }   
528

529
  # Define the variables and functions that will be passed to the UI template.
530
  $vars->{'mailrecipients'} =  { 'changer' => $user->login,
531
                                 'owner'   => $owner };
532
  $vars->{'bugid'} = $bugid;
533
  $vars->{'attachid'} = $attachid;
534
  $vars->{'description'} = $cgi->param('description');
535 536
  $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
  $vars->{'contenttype'} = $cgi->param('contenttype');
537

538
  print $cgi->header();
539 540

  # Generate and return the UI (HTML page) from the appropriate template.
541 542
  $template->process("attachment/created.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
543 544
}

545 546 547 548
# 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.
549
sub edit {
550
  my ($attach_id) = validateID();
551
  my $dbh = Bugzilla->dbh;
552

553 554
  my $attachment = Bugzilla::Attachment->get($attach_id);
  my $isviewable = !$attachment->isurl && isViewable($attachment->contenttype);
555 556 557

  # 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.
558
  my $bugattachments =
559 560 561
      Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
  # We only want attachment IDs.
  @$bugattachments = map { $_->id } @$bugattachments;
562 563 564 565 566 567

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

568
  # Get a list of flag types that can be set for this attachment.
569 570
  my $flag_types = Bugzilla::FlagType::match({ 'target_type'  => 'attachment' ,
                                               'product_id'   => $product_id ,
571
                                               'component_id' => $component_id });
572
  foreach my $flag_type (@$flag_types) {
573
    $flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id'   => $flag_type->id,
574
                                                    'attach_id' => $attachment->id });
575 576
  }
  $vars->{'flag_types'} = $flag_types;
577
  $vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
578
  $vars->{'attachment'} = $attachment;
579 580
  $vars->{'bugsummary'} = $bugsummary; 
  $vars->{'isviewable'} = $isviewable; 
581
  $vars->{'attachments'} = $bugattachments; 
582

583 584 585 586 587
  # Determine if PatchReader is installed
  eval {
    require PatchReader;
    $vars->{'patchviewerinstalled'} = 1;
  };
588
  print $cgi->header();
589 590

  # Generate and return the UI (HTML page) from the appropriate template.
591 592
  $template->process("attachment/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
593 594
}

595 596 597 598 599
# 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.
600 601
sub update
{
602 603 604
    my $user = Bugzilla->user;
    my $userid = $user->id;
    my $dbh = Bugzilla->dbh;
605 606 607 608

    # Retrieve and validate parameters
    ValidateComment(scalar $cgi->param('comment'));
    my ($attach_id, $bugid) = validateID();
609 610
    my $attachment = Bugzilla::Attachment->get($attach_id);
    $attachment->validate_can_edit;
611
    validateCanChangeAttachment($attach_id);
612 613 614
    Bugzilla::Attachment->validate_description(THROW_ERROR);
    Bugzilla::Attachment->validate_is_patch(THROW_ERROR);
    Bugzilla::Attachment->validate_content_type(THROW_ERROR) unless $cgi->param('ispatch');
615 616
    validateIsObsolete();
    validatePrivate();
617 618 619 620 621 622 623 624 625 626 627 628 629

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

631 632 633
    # 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.
634
    Bugzilla::User::match_field($cgi, {
635
        '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }
636
    });
637
    Bugzilla::Flag::validate($cgi, $bugid, $attach_id);
638

639
    my $bug = new Bugzilla::Bug($bugid);
640 641
    # Lock database tables in preparation for updating the attachment.
    $dbh->bz_lock_tables('attachments WRITE', 'flags WRITE' ,
642 643
          'flagtypes READ', 'fielddefs READ', 'bugs_activity WRITE',
          'flaginclusions AS i READ', 'flagexclusions AS e READ',
644 645
          # cc, bug_group_map, user_group_map, and groups are in here so we
          # can check the permissions of flag requestees and email addresses
646
          # on the flag type cc: lists via the CanSeeBug
647 648 649 650
          # 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',
651
          'group_group_map READ', 'groups READ', 'group_control_map READ');
652

653 654
  # Get a copy of the attachment record before we make changes
  # so we can record those changes in the activity table.
655
  my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
656 657 658
      $oldisobsolete, $oldisprivate) = $dbh->selectrow_array(
      "SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
       FROM attachments WHERE attach_id = ?", undef, $attach_id);
659

660
  # Quote the description and content type for use in the SQL UPDATE statement.
661 662 663 664 665 666 667
  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);
668

669
  # Figure out when the changes were made.
670
  my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
671
    
672 673 674 675
  # 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.
676
  Bugzilla::Flag::process($bug, $attachment, $timestamp, $cgi);
677

678
  # Update the attachment record in the database.
679 680 681 682 683 684 685 686 687 688 689
  $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));
690 691

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

744
  # If the user submitted a comment while editing the attachment,
745
  # add the comment to the bug.
746
  if ($cgi->param('comment'))
747
  {
748 749 750 751
    # 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');
752 753

    # Append the comment to the list of comments in the database.
754
    AppendComment($bugid, $userid, $comment, $cgi->param('isprivate'), $timestamp);
755
  }
756
  
757
  # Define the variables and functions that will be passed to the UI template.
758
  $vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login };
759
  $vars->{'attachid'} = $attach_id; 
760 761
  $vars->{'bugid'} = $bugid; 

762
  print $cgi->header();
763 764

  # Generate and return the UI (HTML page) from the appropriate template.
765 766
  $template->process("attachment/updated.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
767
}
768 769 770 771 772 773 774 775 776 777 778 779 780

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

781
    Bugzilla->params->{'allow_attachment_deletion'}
782 783 784 785
      || ThrowUserError('attachment_deletion_disabled');

    # Make sure the administrator is allowed to edit this attachment.
    my ($attach_id, $bug_id) = validateID();
786 787
    my $attachment = Bugzilla::Attachment->get($attach_id);
    $attachment->validate_can_edit;
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 818 819 820 821 822 823 824 825 826 827
    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.
828
        delete_token($token);
829 830 831 832 833 834 835 836 837

        # 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.
838
        $token = issue_session_token('attachment' . $attach_id);
839 840 841 842 843 844 845 846

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

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