122
# Validate the value of the "id" form field, which must contain an
123
# integer that is the ID of an existing attachment.
125
detaint_natural($::FORM{'id'})
126
|| DisplayError("You did not enter a valid attachment number.")
167
my $param = @_ ? $_[0] : 'id';
169
# If we're not doing interdiffs, check if id wasn't specified and
170
# prompt them with a page that allows them to choose an attachment.
171
# Happens when calling plain attachment.cgi from the urlbar directly
172
if ($param eq 'id' && !$cgi->param('id')) {
174
print Bugzilla->cgi->header();
175
$template->process("attachment/choose.html.tmpl", $vars) ||
176
ThrowTemplateError($template->error());
180
my $attach_id = $cgi->param($param);
182
# Validate the specified attachment id. detaint kills $attach_id if
183
# non-natural, so use the original value from $cgi in our exception
185
detaint_natural($attach_id)
186
|| ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) });
129
# Make sure the attachment exists in the database.
130
SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
132
|| DisplayError("Attachment #$::FORM{'id'} does not exist.")
135
# Make sure the user is authorized to access this attachment's bug.
136
my ($bugid) = FetchSQLData();
137
ValidateBugID($bugid);
188
# Make sure the attachment exists in the database.
189
SendSQL("SELECT bug_id, isprivate FROM attachments WHERE attach_id = $attach_id");
191
|| ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
193
# Make sure the user is authorized to access this attachment's bug.
194
($bugid, my $isprivate) = FetchSQLData();
195
ValidateBugID($bugid);
196
if (($isprivate > 0 ) && Param("insidergroup") &&
197
!(UserInGroup(Param("insidergroup")))) {
198
ThrowUserError("attachment_access_denied");
201
# XXX shim code, kill $::FORM
202
$::FORM{$param} = $attach_id;
207
# receives a list of legal formats; first item is a default
208
my $format = $cgi->param('format') || $_[0];
209
if ( lsearch(\@_, $format) == -1)
211
ThrowUserError("invalid_format", { format => $format, formats => \@_ });
214
# XXX shim code, kill $::FORM
215
$::FORM{'format'} = $format;
220
my $context = $cgi->param('context') || "patch";
221
if ($context ne "file" && $context ne "patch") {
222
detaint_natural($context)
223
|| ThrowUserError("invalid_context", { context => $cgi->param('context') });
225
# XXX shim code, kill $::FORM
226
$::FORM{'context'} = $context;
140
229
sub validateCanEdit
238
339
$::FORM{'isobsolete'} = $::FORM{'isobsolete'} ? 1 : 0;
243
# Get a list of attachment statuses that are valid for this attachment.
244
PushGlobalSQLState();
245
SendSQL("SELECT attachstatusdefs.id
246
FROM attachments, bugs, attachstatusdefs
247
WHERE attachments.attach_id = $::FORM{'id'}
248
AND attachments.bug_id = bugs.bug_id
249
AND attachstatusdefs.product = bugs.product");
251
push(@statusdefs, FetchSQLData()) while MoreSQLData();
254
foreach my $status (@{$::MFORM{'status'}})
256
grep($_ == $status, @statusdefs)
257
|| DisplayError("One of the statuses you entered is not a valid status
258
for this attachment.")
260
# We have tested that the status is valid, so it can be detainted
261
detaint_natural($status);
344
# Set the isprivate flag to zero if it is undefined, since the UI uses
345
# an HTML checkbox to represent this flag, and unchecked HTML checkboxes
346
# do not get sent in HTML requests.
347
$::FORM{'isprivate'} = $::FORM{'isprivate'} ? 1 : 0;
268
|| DisplayError("The file you are trying to attach is empty!")
271
my $len = length($::FORM{'data'});
273
my $maxpatchsize = Param('maxpatchsize');
274
my $maxattachmentsize = Param('maxattachmentsize');
276
# Makes sure the attachment does not exceed either the "maxpatchsize" or
277
# the "maxattachmentsize" parameter.
278
if ( $::FORM{'ispatch'} && $maxpatchsize && $len > $maxpatchsize*1024 )
352
my $maxsize = $::FORM{'ispatch'} ? Param('maxpatchsize') : Param('maxattachmentsize');
353
$maxsize *= 1024; # Convert from K
355
my $fh = $cgi->upload('data');
358
# We could get away with reading only as much as required, except that then
359
# we wouldn't have a size to print to the error handler below.
280
my $lenkb = sprintf("%.0f", $len/1024);
281
DisplayError("The file you are trying to attach is ${lenkb} kilobytes (KB) in size.
282
Patches cannot be more than ${maxpatchsize}KB in size.
283
Try breaking your patch into several pieces.");
285
} elsif ( !$::FORM{'ispatch'} && $maxattachmentsize && $len > $maxattachmentsize*1024 ) {
286
my $lenkb = sprintf("%.0f", $len/1024);
287
DisplayError("The file you are trying to attach is ${lenkb} kilobytes (KB) in size.
288
Non-patch attachments cannot be more than ${maxattachmentsize}KB.
289
If your attachment is an image, try converting it to a compressable
290
format like JPG or PNG, or put it elsewhere on the web and
291
link to it from the bug's URL field or in a comment on the bug.");
361
# enable 'slurp' mode
367
|| ThrowUserError("zero_length_file");
369
# Make sure the attachment does not exceed the maximum permitted size
370
my $len = length($data);
371
if ($maxsize && $len > $maxsize) {
372
my $vars = { filesize => sprintf("%.0f", $len/1024) };
373
if ( $::FORM{'ispatch'} ) {
374
ThrowUserError("patch_too_large", $vars);
376
ThrowUserError("file_too_large", $vars);
296
384
sub validateFilename
298
defined $::FILE{'data'}
299
|| DisplayError("You did not specify a file to attach.")
386
defined $cgi->upload('data')
387
|| ThrowUserError("file_not_specified");
389
$filename = $cgi->upload('data');
391
# Remove path info (if any) from the file name. The browser should do this
392
# for us, but some are buggy. This may not work on Mac file names and could
393
# mess up file names with slashes in them, but them's the breaks. We only
394
# use this as a hint to users downloading attachments anyway, so it's not
395
# a big deal if it munges incorrectly occasionally.
396
$filename =~ s/^.*[\/\\]//;
398
# Truncate the filename to 100 characters, counting from the end of the string
399
# to make sure we keep the filename extension.
400
$filename = substr($filename, -100, 100);
303
403
sub validateObsolete
305
405
# Make sure the attachment id is valid and the user has permissions to view
306
406
# the bug to which it is attached.
307
407
foreach my $attachid (@{$::MFORM{'obsolete'}}) {
409
$vars->{'attach_id'} = $attachid;
308
411
detaint_natural($attachid)
309
|| DisplayError("The attachment number of one of the attachments
310
you wanted to obsolete is invalid.")
412
|| ThrowCodeError("invalid_attach_id_to_obsolete", $vars);
313
414
SendSQL("SELECT bug_id, isobsolete, description
314
415
FROM attachments WHERE attach_id = $attachid");
316
417
# Make sure the attachment exists in the database.
318
|| DisplayError("Attachment #$attachid does not exist.")
419
|| ThrowUserError("invalid_attach_id", $vars);
321
421
my ($bugid, $isobsolete, $description) = FetchSQLData();
423
$vars->{'description'} = $description;
323
425
if ($bugid != $::FORM{'bugid'})
325
$description = html_quote($description);
326
DisplayError("Attachment #$attachid ($description) is attached
327
to bug #$bugid, but you tried to flag it as obsolete while
328
creating a new attachment to bug #$::FORM{'bugid'}.");
427
$vars->{'my_bug_id'} = $::FORM{'bugid'};
428
$vars->{'attach_bug_id'} = $bugid;
429
ThrowCodeError("mismatched_bug_ids_on_obsolete", $vars);
332
432
if ( $isobsolete )
334
$description = html_quote($description);
335
DisplayError("Attachment #$attachid ($description) is already obsolete.");
434
ThrowCodeError("attachment_already_obsolete", $vars);
339
437
# Check that the user can modify this attachment
340
438
validateCanEdit($attachid);
442
# Returns 1 if the parameter is a content-type viewable in this browser
443
# Note that we don't use $cgi->Accept()'s ability to check if a content-type
444
# matches, because this will return a value even if it's matched by the generic
445
# */* which most browsers add to the end of their Accept: headers.
448
my $contenttype = trim(shift);
450
# We assume we can view all text and image types
451
if ($contenttype =~ /^(text|image)\//) {
455
# Mozilla can view XUL. Note the trailing slash on the Gecko detection to
456
# avoid sending XUL to Safari.
457
if (($contenttype =~ /^application\/vnd\.mozilla\./) &&
458
($cgi->user_agent() =~ /Gecko\//))
463
# If it's not one of the above types, we check the Accept: header for any
464
# types mentioned explicitly.
465
my $accept = join(",", $cgi->Accept());
467
if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/) {
345
474
################################################################################
351
# Display an attachment.
353
# Retrieve the attachment content and its content type from the database.
354
SendSQL("SELECT mimetype, thedata FROM attachments WHERE attach_id = $::FORM{'id'}");
355
my ($contenttype, $thedata) = FetchSQLData();
357
# Return the appropriate HTTP response headers.
358
print "Content-Type: $contenttype\n\n";
480
# Display an attachment.
482
# Retrieve the attachment content and its content type from the database.
483
SendSQL("SELECT mimetype, filename, thedata FROM attachments WHERE attach_id = $::FORM{'id'}");
484
my ($contenttype, $filename, $thedata) = FetchSQLData();
486
# Bug 111522: allow overriding content-type manually in the posted $::FORM.
487
if ($::FORM{'content_type'})
489
$::FORM{'contenttypemethod'} = 'manual';
490
$::FORM{'contenttypeentry'} = $::FORM{'content_type'};
491
validateContentType();
492
$contenttype = $::FORM{'content_type'};
495
# Return the appropriate HTTP response headers.
496
$filename =~ s/^.*[\/\\]//;
497
my $filesize = length($thedata);
499
# escape quotes and backslashes in the filename, per RFCs 2045/822
500
$filename =~ s/\\/\\\\/g; # escape backslashes
501
$filename =~ s/"/\\"/g; # escape quotes
503
print Bugzilla->cgi->header(-type=>"$contenttype; name=\"$filename\"",
504
-content_disposition=> "inline; filename=\"$filename\"",
505
-content_length => $filesize);
513
my ($old_bugid, $old_description, $old_filename, $old_file_list) =
514
get_unified_diff($::FORM{'oldid'});
517
my ($new_bugid, $new_description, $new_filename, $new_file_list) =
518
get_unified_diff($::FORM{'newid'});
520
my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list);
523
# send through interdiff, send output directly to template
525
# Must hack path so that interdiff will work.
527
$ENV{'PATH'} = $::diffpath;
528
open my $interdiff_fh, "$::interdiffbin $old_filename $new_filename|";
529
binmode $interdiff_fh;
530
my ($reader, $last_reader) = setup_patch_readers("");
531
if ($::FORM{'format'} eq "raw")
533
require PatchReader::DiffPrinter::raw;
534
$last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
535
# Actually print out the patch
536
print $cgi->header(-type => 'text/plain',
541
$vars->{warning} = $warning if $warning;
542
$vars->{bugid} = $new_bugid;
543
$vars->{oldid} = $::FORM{'oldid'};
544
$vars->{old_desc} = $old_description;
545
$vars->{newid} = $::FORM{'newid'};
546
$vars->{new_desc} = $new_description;
547
delete $vars->{attachid};
548
delete $vars->{do_context};
549
delete $vars->{context};
550
setup_template_patch_reader($last_reader);
552
$reader->iterate_fh($interdiff_fh, "interdiff #$::FORM{'oldid'} #$::FORM{'newid'}");
557
# Delete temporary files
559
unlink($old_filename) or warn "Could not unlink $old_filename: $!";
560
unlink($new_filename) or warn "Could not unlink $new_filename: $!";
567
# Bring in the modules we need
568
require PatchReader::Raw;
569
require PatchReader::FixPatchRoot;
570
require PatchReader::DiffPrinter::raw;
571
require PatchReader::PatchInfoGrabber;
575
SendSQL("SELECT bug_id, description, ispatch, thedata FROM attachments WHERE attach_id = $id");
576
my ($bugid, $description, $ispatch, $thedata) = FetchSQLData();
578
$vars->{'attach_id'} = $id;
579
ThrowCodeError("must_be_patch");
582
# Reads in the patch, converting to unified diff in a temp file
583
my $reader = new PatchReader::Raw;
584
my $last_reader = $reader;
586
# fixes patch root (makes canonical if possible)
587
if (Param('cvsroot')) {
588
my $fix_patch_root = new PatchReader::FixPatchRoot(Param('cvsroot'));
589
$last_reader->sends_data_to($fix_patch_root);
590
$last_reader = $fix_patch_root;
593
# Grabs the patch file info
594
my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
595
$last_reader->sends_data_to($patch_info_grabber);
596
$last_reader = $patch_info_grabber;
598
# Prints out to temporary file
599
my ($fh, $filename) = File::Temp::tempfile();
600
my $raw_printer = new PatchReader::DiffPrinter::raw($fh);
601
$last_reader->sends_data_to($raw_printer);
602
$last_reader = $raw_printer;
605
$reader->iterate_string($id, $thedata);
607
return ($bugid, $description, $filename, $patch_info_grabber->patch_info()->{files});
610
sub warn_if_interdiff_might_fail {
611
my ($old_file_list, $new_file_list) = @_;
612
# Verify that the list of files diffed is the same
613
my @old_files = sort keys %{$old_file_list};
614
my @new_files = sort keys %{$new_file_list};
615
if (@old_files != @new_files ||
616
join(' ', @old_files) ne join(' ', @new_files)) {
620
# Verify that the revisions in the files are the same
621
foreach my $file (keys %{$old_file_list}) {
622
if ($old_file_list->{$file}{old_revision} ne
623
$new_file_list->{$file}{old_revision}) {
631
sub setup_patch_readers {
632
my ($diff_root) = @_;
637
# context=patch|file|0-n
642
# Define the patch readers
643
# The reader that reads the patch in (whatever its format)
644
require PatchReader::Raw;
645
my $reader = new PatchReader::Raw;
646
my $last_reader = $reader;
647
# Fix the patch root if we have a cvs root
648
if (Param('cvsroot'))
650
require PatchReader::FixPatchRoot;
651
$last_reader->sends_data_to(new PatchReader::FixPatchRoot(Param('cvsroot')));
652
$last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root);
653
$last_reader = $last_reader->sends_data_to;
655
# Add in cvs context if we have the necessary info to do it
656
if ($::FORM{'context'} ne "patch" && $::cvsbin && Param('cvsroot_get'))
658
require PatchReader::AddCVSContext;
659
$last_reader->sends_data_to(
660
new PatchReader::AddCVSContext($::FORM{'context'},
661
Param('cvsroot_get')));
662
$last_reader = $last_reader->sends_data_to;
664
return ($reader, $last_reader);
667
sub setup_template_patch_reader
669
my ($last_reader) = @_;
671
require PatchReader::DiffPrinter::template;
673
my $format = $::FORM{'format'};
675
# Define the vars for templates
676
if (defined($::FORM{'headers'})) {
677
$vars->{headers} = $::FORM{'headers'};
679
$vars->{headers} = 1 if !defined($::FORM{'headers'});
681
$vars->{collapsed} = $::FORM{'collapsed'};
682
$vars->{context} = $::FORM{'context'};
683
$vars->{do_context} = $::cvsbin && Param('cvsroot_get') && !$vars->{'newid'};
685
# Print everything out
686
print $cgi->header(-type => 'text/html',
688
$last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
689
"attachment/diff-header.$format.tmpl",
690
"attachment/diff-file.$format.tmpl",
691
"attachment/diff-footer.$format.tmpl",
693
bonsai_url => Param('bonsai_url'),
694
lxr_url => Param('lxr_url'),
695
lxr_root => Param('lxr_root'),
702
SendSQL("SELECT bug_id, description, ispatch, thedata FROM attachments WHERE attach_id = $::FORM{'id'}");
703
my ($bugid, $description, $ispatch, $thedata) = FetchSQLData();
705
# If it is not a patch, view normally
712
my ($reader, $last_reader) = setup_patch_readers();
714
if ($::FORM{'format'} eq "raw")
716
require PatchReader::DiffPrinter::raw;
717
$last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
718
# Actually print out the patch
720
print $cgi->header(-type => 'text/plain',
722
$reader->iterate_string("Attachment " . $::FORM{'id'}, $thedata);
726
$vars->{other_patches} = [];
727
if ($::interdiffbin && $::diffpath) {
728
# Get list of attachments on this bug.
729
# Ignore the current patch, but select the one right before it
731
SendSQL("SELECT attach_id, description FROM attachments WHERE bug_id = $bugid AND ispatch = 1 ORDER BY creation_ts DESC");
732
my $select_next_patch = 0;
733
while (my ($other_id, $other_desc) = FetchSQLData()) {
734
if ($other_id eq $::FORM{'id'}) {
735
$select_next_patch = 1;
737
push @{$vars->{other_patches}}, { id => $other_id, desc => $other_desc, selected => $select_next_patch };
738
if ($select_next_patch) {
739
$select_next_patch = 0;
745
$vars->{bugid} = $bugid;
746
$vars->{attachid} = $::FORM{'id'};
747
$vars->{description} = $description;
748
setup_template_patch_reader($last_reader);
749
# Actually print out the patch
750
$reader->iterate_string("Attachment " . $::FORM{'id'}, $thedata);
368
758
# Retrieve the attachments from the database and write them into an array
369
759
# of hashes where each hash represents one attachment.
370
SendSQL("SELECT attach_id, creation_ts, mimetype, description, ispatch, isobsolete
371
FROM attachments WHERE bug_id = $::FORM{'bugid'} ORDER BY attach_id");
761
if (Param("insidergroup") && !(UserInGroup(Param("insidergroup")))) {
762
$privacy = "AND isprivate < 1 ";
764
SendSQL("SELECT attach_id, DATE_FORMAT(creation_ts, '%Y.%m.%d %H:%i'),
765
mimetype, description, ispatch, isobsolete, isprivate,
767
FROM attachments WHERE bug_id = $::FORM{'bugid'} $privacy
768
ORDER BY attach_id");
372
769
my @attachments; # the attachments array
373
770
while (MoreSQLData())
375
772
my %a; # the attachment hash
376
773
($a{'attachid'}, $a{'date'}, $a{'contenttype'},
377
$a{'description'}, $a{'ispatch'}, $a{'isobsolete'}) = FetchSQLData();
379
# Format the attachment's creation/modification date into something readable.
380
if ($a{'date'} =~ /^(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/) {
381
$a{'date'} = "$3/$4/$2 $5:$6";
384
# Flag attachments as to whether or not they can be viewed (as opposed to
385
# being downloaded). Currently I decide they are viewable if their MIME type
386
# is either text/*, image/*, or application/vnd.mozilla.*.
387
# !!! Yuck, what an ugly hack. Fix it!
388
$a{'isviewable'} = ( $a{'contenttype'} =~ /^(text|image|application\/vnd\.mozilla\.)/ );
390
# Retrieve a list of status flags that have been set on the attachment.
391
PushGlobalSQLState();
393
FROM attachstatuses, attachstatusdefs
394
WHERE attach_id = $a{'attachid'}
395
AND attachstatuses.statusid = attachstatusdefs.id
398
push(@statuses, FetchSQLData()) while MoreSQLData();
399
$a{'statuses'} = \@statuses;
774
$a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, $a{'isprivate'},
775
$a{'datasize'}) = FetchSQLData();
776
$a{'isviewable'} = isViewable($a{'contenttype'});
777
$a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'},
402
780
# Add the hash representing the attachment to the array of attachments.
403
781
push @attachments, \%a;
406
# Retrieve the bug summary for displaying on screen.
407
SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $::FORM{'bugid'}");
408
my ($bugsummary) = FetchSQLData();
784
# Retrieve the bug summary (for displaying on screen) and assignee.
785
SendSQL("SELECT short_desc, assigned_to FROM bugs " .
786
"WHERE bug_id = $::FORM{'bugid'}");
787
my ($bugsummary, $assignee_id) = FetchSQLData();
410
789
# Define the variables and functions that will be passed to the UI template.
411
790
$vars->{'bugid'} = $::FORM{'bugid'};
791
$vars->{'attachments'} = \@attachments;
792
$vars->{'bugassignee_id'} = $assignee_id;
412
793
$vars->{'bugsummary'} = $bugsummary;
413
$vars->{'attachments'} = \@attachments;
794
$vars->{'GetBugLink'} = \&GetBugLink;
415
# Return the appropriate HTTP response headers.
416
print "Content-Type: text/html\n\n";
796
print Bugzilla->cgi->header();
418
798
# Generate and return the UI (HTML page) from the appropriate template.
419
799
$template->process("attachment/show-multiple.html.tmpl", $vars)
493
882
AppendComment($::FORM{'bugid'},
494
883
$::COOKIE{"Bugzilla_login"},
497
888
# Make existing attachments obsolete.
498
889
my $fieldid = GetFieldID('attachments.isobsolete');
499
foreach my $attachid (@{$::MFORM{'obsolete'}}) {
500
SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $attachid");
501
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
502
VALUES ($::FORM{'bugid'}, $attachid, $::userid, NOW(), $fieldid, '0', '1')");
890
foreach my $obsolete_id (@{$::MFORM{'obsolete'}}) {
891
SendSQL("UPDATE attachments SET isobsolete = 1 WHERE attach_id = $obsolete_id");
892
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
893
VALUES ($::FORM{'bugid'}, $obsolete_id, $::userid, $sql_timestamp, $fieldid, '0', '1')");
894
# If the obsolete attachment has pending flags, migrate them to the new attachment.
895
if (Bugzilla::Flag::count({ 'attach_id' => $obsolete_id ,
896
'status' => 'pending',
897
'is_active' => 1 })) {
898
Bugzilla::Flag::migrate($obsolete_id, $attachid, $timestamp);
505
# Send mail to let people know the attachment has been created. Uses a
506
# special syntax of the "open" and "exec" commands to capture the output of
507
# "processmail", which "system" doesn't allow, without running the command
508
# through a shell, which backticks (``) do.
509
#system ("./processmail", $bugid , $::userid);
510
#my $mailresults = `./processmail $bugid $::userid`;
511
my $mailresults = '';
512
open(PMAIL, "-|") or exec('/usr/share/bugzilla/lib/processmail', $::FORM{'bugid'}, $::COOKIE{'Bugzilla_login'});
513
$mailresults .= $_ while <PMAIL>;
902
# Assign the bug to the user, if they are allowed to take it
905
if ($::FORM{'takebug'} && UserInGroup("editbugs")) {
907
my @fields = ("assigned_to", "bug_status", "resolution", "login_name");
909
# Get the old values, for the bugs_activity table
910
SendSQL("SELECT " . join(", ", @fields) . " FROM bugs, profiles " .
911
"WHERE bugs.bug_id = $::FORM{'bugid'} " .
912
"AND profiles.userid = bugs.assigned_to");
914
my @oldvalues = FetchSQLData();
915
my @newvalues = ($::userid, "ASSIGNED", "", DBID_to_name($::userid));
917
# Make sure the person we are taking the bug from gets mail.
918
$owner = $oldvalues[3];
920
@oldvalues = map(SqlQuote($_), @oldvalues);
921
@newvalues = map(SqlQuote($_), @newvalues);
923
# Update the bug record. Note that this doesn't involve login_name.
924
SendSQL("UPDATE bugs SET " .
925
join(", ", map("$fields[$_] = $newvalues[$_]", (0..2))) .
926
" WHERE bug_id = $::FORM{'bugid'}");
928
# We store email addresses in the bugs_activity table rather than IDs.
929
$oldvalues[0] = $oldvalues[3];
930
$newvalues[0] = $newvalues[3];
932
# Add the changes to the bugs_activity table
933
for (my $i = 0; $i < 3; $i++) {
934
if ($oldvalues[$i] ne $newvalues[$i]) {
935
my $fieldid = GetFieldID($fields[$i]);
936
SendSQL("INSERT INTO bugs_activity " .
937
"(bug_id, who, bug_when, fieldid, removed, added) " .
938
" VALUES ($::FORM{'bugid'}, $::userid, " .
940
", $fieldid, $oldvalues[$i], $newvalues[$i])");
516
945
# Define the variables and functions that will be passed to the UI template.
517
$vars->{'bugid'} = $::FORM{'bugid'};
946
$vars->{'mailrecipients'} = { 'changer' => $::COOKIE{'Bugzilla_login'},
948
my $bugid = $::FORM{'bugid'};
949
detaint_natural($bugid); # don't bother with error condition, we know it'll work
950
# because of ValidateBugID above. This is only needed
951
# for Perl 5.6.0. If we ever require Perl 5.6.1 or
952
# newer, or detaint something other than $::FORM{'bugid'}
953
# in ValidateBugID above, then this can go away.
954
my $contenttypemethod = $::FORM{'contenttypemethod'} || '';
955
trick_taint($contenttypemethod); # Same Perl 5.6.0 hack as above
956
$contenttype = $::FORM{'contenttype'} || '';
957
trick_taint($contenttype); # Same Perl 5.6.0 hack as above
958
$vars->{'bugid'} = $bugid;
518
959
$vars->{'attachid'} = $attachid;
519
960
$vars->{'description'} = $description;
520
$vars->{'mailresults'} = $mailresults;
521
$vars->{'contenttypemethod'} = $::FORM{'contenttypemethod'};
522
$vars->{'contenttype'} = $::FORM{'contenttype'};
961
$vars->{'contenttypemethod'} = $contenttypemethod;
962
$vars->{'contenttype'} = $contenttype;
524
# Return the appropriate HTTP response headers.
525
print "Content-Type: text/html\n\n";
964
print Bugzilla->cgi->header();
527
966
# Generate and return the UI (HTML page) from the appropriate template.
528
967
$template->process("attachment/created.html.tmpl", $vars)
539
978
# Users cannot edit the content of the attachment itself.
541
980
# Retrieve the attachment from the database.
542
SendSQL("SELECT description, mimetype, bug_id, ispatch, isobsolete
981
SendSQL("SELECT description, mimetype, filename, bug_id, ispatch, isobsolete, isprivate, LENGTH(thedata)
543
982
FROM attachments WHERE attach_id = $::FORM{'id'}");
544
my ($description, $contenttype, $bugid, $ispatch, $isobsolete) = FetchSQLData();
546
# Flag attachment as to whether or not it can be viewed (as opposed to
547
# being downloaded). Currently I decide it is viewable if its content
548
# type is either text/.* or application/vnd.mozilla.*.
549
# !!! Yuck, what an ugly hack. Fix it!
550
my $isviewable = ( $contenttype =~ /^(text|image|application\/vnd\.mozilla\.)/ );
552
# Retrieve a list of status flags that have been set on the attachment.
554
SendSQL("SELECT id, name
555
FROM attachstatuses JOIN attachstatusdefs
556
WHERE attachstatuses.statusid = attachstatusdefs.id
557
AND attach_id = $::FORM{'id'}");
558
while ( my ($id, $name) = FetchSQLData() )
560
$statuses{$id} = $name;
563
# Retrieve a list of statuses for this bug's product, and build an array
564
# of hashes in which each hash is a status flag record.
565
# ???: Move this into versioncache or its own routine?
567
SendSQL("SELECT id, name
568
FROM attachstatusdefs, bugs
569
WHERE bug_id = $bugid
570
AND attachstatusdefs.product = bugs.product
572
while ( MoreSQLData() )
574
my ($id, $name) = FetchSQLData();
575
push @statusdefs, { 'id' => $id , 'name' => $name };
983
my ($description, $contenttype, $filename, $bugid, $ispatch, $isobsolete, $isprivate, $datasize) = FetchSQLData();
985
my $isviewable = isViewable($contenttype);
578
987
# Retrieve a list of attachments for this bug as well as a summary of the bug
579
988
# to use in a navigation bar across the top of the screen.
582
991
push(@bugattachments, FetchSQLData()) while (MoreSQLData());
583
992
SendSQL("SELECT short_desc FROM bugs WHERE bug_id = $bugid");
584
993
my ($bugsummary) = FetchSQLData();
995
# Get a list of flag types that can be set for this attachment.
996
SendSQL("SELECT product_id, component_id FROM bugs WHERE bug_id = $bugid");
997
my ($product_id, $component_id) = FetchSQLData();
998
my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' ,
999
'product_id' => $product_id ,
1000
'component_id' => $component_id });
1001
foreach my $flag_type (@$flag_types) {
1002
$flag_type->{'flags'} = Bugzilla::Flag::match({ 'type_id' => $flag_type->{'id'},
1003
'attach_id' => $::FORM{'id'},
1004
'is_active' => 1 });
1006
$vars->{'flag_types'} = $flag_types;
1007
$vars->{'any_flags_requesteeble'} = grep($_->{'is_requesteeble'}, @$flag_types);
586
1009
# Define the variables and functions that will be passed to the UI template.
587
1010
$vars->{'attachid'} = $::FORM{'id'};
588
1011
$vars->{'description'} = $description;
589
1012
$vars->{'contenttype'} = $contenttype;
1013
$vars->{'filename'} = $filename;
590
1014
$vars->{'bugid'} = $bugid;
591
1015
$vars->{'bugsummary'} = $bugsummary;
592
1016
$vars->{'ispatch'} = $ispatch;
593
1017
$vars->{'isobsolete'} = $isobsolete;
1018
$vars->{'isprivate'} = $isprivate;
1019
$vars->{'datasize'} = $datasize;
594
1020
$vars->{'isviewable'} = $isviewable;
595
$vars->{'statuses'} = \%statuses;
596
$vars->{'statusdefs'} = \@statusdefs;
597
1021
$vars->{'attachments'} = \@bugattachments;
1022
$vars->{'GetBugLink'} = \&GetBugLink;
599
# Return the appropriate HTTP response headers.
600
print "Content-Type: text/html\n\n";
1024
# Determine if PatchReader is installed
1026
require PatchReader;
1027
$vars->{'patchviewerinstalled'} = 1;
1029
print Bugzilla->cgi->header();
602
1031
# Generate and return the UI (HTML page) from the appropriate template.
603
1032
$template->process("attachment/edit.html.tmpl", $vars)
610
# Update an attachment record.
1039
# Updates an attachment record.
612
1041
# Get the bug ID for the bug to which this attachment is attached.
613
1042
SendSQL("SELECT bug_id FROM attachments WHERE attach_id = $::FORM{'id'}");
614
my $bugid = FetchSQLData()
615
|| DisplayError("Cannot figure out bug number.")
1043
my $bugid = FetchSQLData();
618
1045
# Lock database tables in preparation for updating the attachment.
619
SendSQL("LOCK TABLES attachments WRITE , attachstatuses WRITE ,
620
attachstatusdefs READ , fielddefs READ , bugs_activity WRITE");
1046
SendSQL("LOCK TABLES attachments WRITE , flags WRITE , " .
1047
"flagtypes READ , fielddefs READ , bugs_activity WRITE, " .
1048
"flaginclusions AS i READ, flagexclusions AS e READ, " .
1049
# cc, bug_group_map, user_group_map, and groups are in here so we
1050
# can check the permissions of flag requestees and email addresses
1051
# on the flag type cc: lists via the CanSeeBug
1052
# function call in Flag::notify. group_group_map is in here in case
1053
# Bugzilla::User needs to rederive groups. profiles and
1054
# user_group_map would be READ locks instead of WRITE locks if it
1055
# weren't for derive_groups, which needs to write to those tables.
1056
"bugs READ, profiles WRITE, " .
1057
"cc READ, bug_group_map READ, user_group_map WRITE, " .
1058
"group_group_map READ, groups READ");
622
1060
# Get a copy of the attachment record before we make changes
623
1061
# so we can record those changes in the activity table.
624
SendSQL("SELECT description, mimetype, ispatch, isobsolete
1062
SendSQL("SELECT description, mimetype, filename, ispatch, isobsolete, isprivate
625
1063
FROM attachments WHERE attach_id = $::FORM{'id'}");
626
my ($olddescription, $oldcontenttype, $oldispatch, $oldisobsolete) = FetchSQLData();
628
# Get the list of old status flags.
629
SendSQL("SELECT attachstatusdefs.name
630
FROM attachments, attachstatuses, attachstatusdefs
631
WHERE attachments.attach_id = $::FORM{'id'}
632
AND attachments.attach_id = attachstatuses.attach_id
633
AND attachstatuses.statusid = attachstatusdefs.id
634
ORDER BY attachstatusdefs.sortkey
637
while (MoreSQLData()) {
638
push(@oldstatuses, FetchSQLData());
640
my $oldstatuslist = join(', ', @oldstatuses);
642
# Update the database with the new status flags.
643
SendSQL("DELETE FROM attachstatuses WHERE attach_id = $::FORM{'id'}");
644
foreach my $statusid (@{$::MFORM{'status'}})
646
SendSQL("INSERT INTO attachstatuses (attach_id, statusid) VALUES ($::FORM{'id'}, $statusid)");
649
# Get the list of new status flags.
650
SendSQL("SELECT attachstatusdefs.name
651
FROM attachments, attachstatuses, attachstatusdefs
652
WHERE attachments.attach_id = $::FORM{'id'}
653
AND attachments.attach_id = attachstatuses.attach_id
654
AND attachstatuses.statusid = attachstatusdefs.id
655
ORDER BY attachstatusdefs.sortkey
658
while (MoreSQLData()) {
659
push(@newstatuses, FetchSQLData());
661
my $newstatuslist = join(', ', @newstatuses);
1064
my ($olddescription, $oldcontenttype, $oldfilename, $oldispatch,
1065
$oldisobsolete, $oldisprivate) = FetchSQLData();
663
1067
# Quote the description and content type for use in the SQL UPDATE statement.
664
1068
my $quoteddescription = SqlQuote($::FORM{'description'});
665
1069
my $quotedcontenttype = SqlQuote($::FORM{'contenttype'});
1070
my $quotedfilename = SqlQuote($::FORM{'filename'});
667
1072
# Update the attachment record in the database.
668
1073
# Sets the creation timestamp to itself to avoid it being updated automatically.
669
1074
SendSQL("UPDATE attachments
670
1075
SET description = $quoteddescription ,
671
1076
mimetype = $quotedcontenttype ,
1077
filename = $quotedfilename ,
672
1078
ispatch = $::FORM{'ispatch'} ,
673
isobsolete = $::FORM{'isobsolete'} ,
674
creation_ts = creation_ts
1079
isobsolete = $::FORM{'isobsolete'} ,
1080
isprivate = $::FORM{'isprivate'}
675
1081
WHERE attach_id = $::FORM{'id'}
1084
# Figure out when the changes were made.
1085
my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
1086
my $sql_timestamp = SqlQuote($timestamp);
678
1088
# Record changes in the activity table.
679
1089
if ($olddescription ne $::FORM{'description'}) {
680
1090
my $quotedolddescription = SqlQuote($olddescription);
681
1091
my $fieldid = GetFieldID('attachments.description');
682
1092
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
683
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedolddescription, $quoteddescription)");
1093
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedolddescription, $quoteddescription)");
685
1095
if ($oldcontenttype ne $::FORM{'contenttype'}) {
686
1096
my $quotedoldcontenttype = SqlQuote($oldcontenttype);
687
1097
my $fieldid = GetFieldID('attachments.mimetype');
688
1098
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
689
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
1099
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedoldcontenttype, $quotedcontenttype)");
1101
if ($oldfilename ne $::FORM{'filename'}) {
1102
my $quotedoldfilename = SqlQuote($oldfilename);
1103
my $fieldid = GetFieldID('attachments.filename');
1104
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
1105
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $quotedoldfilename, $quotedfilename)");
691
1107
if ($oldispatch ne $::FORM{'ispatch'}) {
692
1108
my $fieldid = GetFieldID('attachments.ispatch');
693
1109
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
694
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldispatch, $::FORM{'ispatch'})");
1110
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldispatch, $::FORM{'ispatch'})");
696
1112
if ($oldisobsolete ne $::FORM{'isobsolete'}) {
697
1113
my $fieldid = GetFieldID('attachments.isobsolete');
698
1114
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
699
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
1115
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldisobsolete, $::FORM{'isobsolete'})");
701
if ($oldstatuslist ne $newstatuslist) {
702
my ($removed, $added) = DiffStrings($oldstatuslist, $newstatuslist);
703
my $quotedremoved = SqlQuote($removed);
704
my $quotedadded = SqlQuote($added);
705
my $fieldid = GetFieldID('attachstatusdefs.name');
1117
if ($oldisprivate ne $::FORM{'isprivate'}) {
1118
my $fieldid = GetFieldID('attachments.isprivate');
706
1119
SendSQL("INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, fieldid, removed, added)
707
VALUES ($bugid, $::FORM{'id'}, $::userid, NOW(), $fieldid, $quotedremoved, $quotedadded)");
1120
VALUES ($bugid, $::FORM{'id'}, $::userid, $sql_timestamp, $fieldid, $oldisprivate, $::FORM{'isprivate'})");
1124
my $target = Bugzilla::Flag::GetTarget(undef, $::FORM{'id'});
1125
Bugzilla::Flag::process($target, $timestamp, \%::FORM);
710
1127
# Unlock all database tables now that we are finished updating the database.
711
1128
SendSQL("UNLOCK TABLES");
713
# If this installation has enabled the request manager, let the manager know
714
# an attachment was updated so it can check for requests on that attachment
715
# and fulfill them. The request manager allows users to request database
716
# changes of other users and tracks the fulfillment of those requests. When
717
# an attachment record is updated and the request manager is called, it will
718
# fulfill those requests that were requested of the user performing the update
719
# which are requests for the attachment being updated.
721
#if (Param('userequestmanager'))
724
# # Specify the fieldnames that have been updated.
725
# my @fieldnames = ('description', 'mimetype', 'status', 'ispatch', 'isobsolete');
726
# # Fulfill pending requests.
727
# $requests = Request::fulfillRequest('attachment', $::FORM{'id'}, @fieldnames);
728
# $vars->{'requests'} = $requests;
731
1130
# If the user submitted a comment while editing the attachment,
732
1131
# add the comment to the bug.
733
1132
if ( $::FORM{'comment'} )