1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4# 5# This Source Code Form is "Incompatible With Secondary Licenses", as 6# defined by the Mozilla Public License, v. 2.0. 7 8package Bugzilla::Attachment; 9 10use 5.10.1; 11use strict; 12use warnings; 13 14=head1 NAME 15 16Bugzilla::Attachment - Bugzilla attachment class. 17 18=head1 SYNOPSIS 19 20 use Bugzilla::Attachment; 21 22 # Get the attachment with the given ID. 23 my $attachment = new Bugzilla::Attachment($attach_id); 24 25 # Get the attachments with the given IDs. 26 my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); 27 28=head1 DESCRIPTION 29 30Attachment.pm represents an attachment object. It is an implementation 31of L<Bugzilla::Object>, and thus provides all methods that 32L<Bugzilla::Object> provides. 33 34The methods that are specific to C<Bugzilla::Attachment> are listed 35below. 36 37=cut 38 39use Bugzilla::Constants; 40use Bugzilla::Error; 41use Bugzilla::Flag; 42use Bugzilla::User; 43use Bugzilla::Util; 44use Bugzilla::Field; 45use Bugzilla::Hook; 46 47use File::Copy; 48use List::Util qw(max); 49use Storable qw(dclone); 50 51use parent qw(Bugzilla::Object); 52 53############################### 54#### Initialization #### 55############################### 56 57use constant DB_TABLE => 'attachments'; 58use constant ID_FIELD => 'attach_id'; 59use constant LIST_ORDER => ID_FIELD; 60# Attachments are tracked in bugs_activity. 61use constant AUDIT_CREATES => 0; 62use constant AUDIT_UPDATES => 0; 63 64use constant DB_COLUMNS => qw( 65 attach_id 66 bug_id 67 creation_ts 68 description 69 filename 70 isobsolete 71 ispatch 72 isprivate 73 mimetype 74 modification_time 75 submitter_id 76); 77 78use constant REQUIRED_FIELD_MAP => { 79 bug_id => 'bug', 80}; 81use constant EXTRA_REQUIRED_FIELDS => qw(data); 82 83use constant UPDATE_COLUMNS => qw( 84 description 85 filename 86 isobsolete 87 ispatch 88 isprivate 89 mimetype 90); 91 92use constant VALIDATORS => { 93 bug => \&_check_bug, 94 description => \&_check_description, 95 filename => \&_check_filename, 96 ispatch => \&Bugzilla::Object::check_boolean, 97 isprivate => \&_check_is_private, 98 mimetype => \&_check_content_type, 99}; 100 101use constant VALIDATOR_DEPENDENCIES => { 102 content_type => ['ispatch'], 103 mimetype => ['ispatch'], 104}; 105 106use constant UPDATE_VALIDATORS => { 107 isobsolete => \&Bugzilla::Object::check_boolean, 108}; 109 110############################### 111#### Accessors ###### 112############################### 113 114=pod 115 116=head2 Instance Properties 117 118=over 119 120=item C<bug_id> 121 122the ID of the bug to which the attachment is attached 123 124=back 125 126=cut 127 128sub bug_id { 129 return $_[0]->{bug_id}; 130} 131 132=over 133 134=item C<bug> 135 136the bug object to which the attachment is attached 137 138=back 139 140=cut 141 142sub bug { 143 require Bugzilla::Bug; 144 return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 }); 145} 146 147=over 148 149=item C<description> 150 151user-provided text describing the attachment 152 153=back 154 155=cut 156 157sub description { 158 return $_[0]->{description}; 159} 160 161=over 162 163=item C<contenttype> 164 165the attachment's MIME media type 166 167=back 168 169=cut 170 171sub contenttype { 172 return $_[0]->{mimetype}; 173} 174 175=over 176 177=item C<attacher> 178 179the user who attached the attachment 180 181=back 182 183=cut 184 185sub attacher { 186 return $_[0]->{attacher} 187 //= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 }); 188} 189 190=over 191 192=item C<attached> 193 194the date and time on which the attacher attached the attachment 195 196=back 197 198=cut 199 200sub attached { 201 return $_[0]->{creation_ts}; 202} 203 204=over 205 206=item C<modification_time> 207 208the date and time on which the attachment was last modified. 209 210=back 211 212=cut 213 214sub modification_time { 215 return $_[0]->{modification_time}; 216} 217 218=over 219 220=item C<filename> 221 222the name of the file the attacher attached 223 224=back 225 226=cut 227 228sub filename { 229 return $_[0]->{filename}; 230} 231 232=over 233 234=item C<ispatch> 235 236whether or not the attachment is a patch 237 238=back 239 240=cut 241 242sub ispatch { 243 return $_[0]->{ispatch}; 244} 245 246=over 247 248=item C<isobsolete> 249 250whether or not the attachment is obsolete 251 252=back 253 254=cut 255 256sub isobsolete { 257 return $_[0]->{isobsolete}; 258} 259 260=over 261 262=item C<isprivate> 263 264whether or not the attachment is private 265 266=back 267 268=cut 269 270sub isprivate { 271 return $_[0]->{isprivate}; 272} 273 274=over 275 276=item C<is_viewable> 277 278Returns 1 if the attachment has a content-type viewable in this browser. 279Note that we don't use $cgi->Accept()'s ability to check if a content-type 280matches, because this will return a value even if it's matched by the generic 281*/* which most browsers add to the end of their Accept: headers. 282 283=back 284 285=cut 286 287sub is_viewable { 288 my $contenttype = $_[0]->contenttype; 289 my $cgi = Bugzilla->cgi; 290 291 # We assume we can view all text and image types. 292 return 1 if ($contenttype =~ /^(text|image)\//); 293 294 # Mozilla can view XUL. Note the trailing slash on the Gecko detection to 295 # avoid sending XUL to Safari. 296 return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./) 297 && ($cgi->user_agent() =~ /Gecko\//)); 298 299 # If it's not one of the above types, we check the Accept: header for any 300 # types mentioned explicitly. 301 my $accept = join(",", $cgi->Accept()); 302 return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); 303 304 return 0; 305} 306 307=over 308 309=item C<data> 310 311the content of the attachment 312 313=back 314 315=cut 316 317sub data { 318 my $self = shift; 319 return $self->{data} if exists $self->{data}; 320 321 # First try to get the attachment data from the database. 322 ($self->{data}) = Bugzilla->dbh->selectrow_array("SELECT thedata 323 FROM attach_data 324 WHERE id = ?", 325 undef, 326 $self->id); 327 328 # If there's no attachment data in the database, the attachment is stored 329 # in a local file, so retrieve it from there. 330 if (length($self->{data}) == 0) { 331 if (open(AH, '<', $self->_get_local_filename())) { 332 local $/; 333 binmode AH; 334 $self->{data} = <AH>; 335 close(AH); 336 } 337 } 338 339 return $self->{data}; 340} 341 342=over 343 344=item C<datasize> 345 346the length (in bytes) of the attachment content 347 348=back 349 350=cut 351 352# datasize is a property of the data itself, and it's unclear whether we should 353# expose it at all, since you can easily derive it from the data itself: in TT, 354# attachment.data.size; in Perl, length($attachment->{data}). But perhaps 355# it makes sense for performance reasons, since accessing the data forces it 356# to get retrieved from the database/filesystem and loaded into memory, 357# while datasize avoids loading the attachment into memory, calling SQL's 358# LENGTH() function or stat()ing the file instead. I've left it in for now. 359 360sub datasize { 361 my $self = shift; 362 return $self->{datasize} if defined $self->{datasize}; 363 364 # If we have already retrieved the data, return its size. 365 return length($self->{data}) if exists $self->{data}; 366 367 $self->{datasize} = 368 Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata) 369 FROM attach_data 370 WHERE id = ?", 371 undef, $self->id) || 0; 372 373 # If there's no attachment data in the database, either the attachment 374 # is stored in a local file, and so retrieve its size from the file, 375 # or the attachment has been deleted. 376 unless ($self->{datasize}) { 377 if (open(AH, '<', $self->_get_local_filename())) { 378 binmode AH; 379 $self->{datasize} = (stat(AH))[7]; 380 close(AH); 381 } 382 } 383 384 return $self->{datasize}; 385} 386 387sub _get_local_filename { 388 my $self = shift; 389 my $hash = ($self->id % 100) + 100; 390 $hash =~ s/.*(\d\d)$/group.$1/; 391 return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; 392} 393 394=over 395 396=item C<flags> 397 398flags that have been set on the attachment 399 400=back 401 402=cut 403 404sub flags { 405 # Don't cache it as it must be in sync with ->flag_types. 406 return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; 407} 408 409=over 410 411=item C<flag_types> 412 413Return all flag types available for this attachment as well as flags 414already set, grouped by flag type. 415 416=back 417 418=cut 419 420sub flag_types { 421 my $self = shift; 422 return $self->{flag_types} if exists $self->{flag_types}; 423 424 my $vars = { target_type => 'attachment', 425 product_id => $self->bug->product_id, 426 component_id => $self->bug->component_id, 427 attach_id => $self->id }; 428 429 return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); 430} 431 432############################### 433#### Validators ###### 434############################### 435 436sub set_content_type { $_[0]->set('mimetype', $_[1]); } 437sub set_description { $_[0]->set('description', $_[1]); } 438sub set_filename { $_[0]->set('filename', $_[1]); } 439sub set_is_patch { $_[0]->set('ispatch', $_[1]); } 440sub set_is_private { $_[0]->set('isprivate', $_[1]); } 441 442sub set_is_obsolete { 443 my ($self, $obsolete) = @_; 444 445 my $old = $self->isobsolete; 446 $self->set('isobsolete', $obsolete); 447 my $new = $self->isobsolete; 448 449 # If the attachment is being marked as obsolete, cancel pending requests. 450 if ($new && $old != $new) { 451 my @requests = grep { $_->status eq '?' } @{$self->flags}; 452 return unless scalar @requests; 453 454 my %flag_ids = map { $_->id => 1 } @requests; 455 foreach my $flagtype (@{$self->flag_types}) { 456 @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}}; 457 } 458 } 459} 460 461sub set_flags { 462 my ($self, $flags, $new_flags) = @_; 463 464 Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); 465} 466 467sub _check_bug { 468 my ($invocant, $bug) = @_; 469 my $user = Bugzilla->user; 470 471 $bug = ref $invocant ? $invocant->bug : $bug; 472 473 $bug || ThrowCodeError('param_required', 474 { function => "$invocant->create", param => 'bug' }); 475 476 ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) 477 || ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bug->id }); 478 479 return $bug; 480} 481 482sub _check_content_type { 483 my ($invocant, $content_type, undef, $params) = @_; 484 485 my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; 486 $content_type = 'text/plain' if $is_patch; 487 $content_type = clean_text($content_type); 488 # The subsets below cover all existing MIME types and charsets registered by IANA. 489 # (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) 490 my $legal_types = join('|', LEGAL_CONTENT_TYPES); 491 if (!$content_type 492 || $content_type !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) 493 { 494 ThrowUserError("invalid_content_type", { contenttype => $content_type }); 495 } 496 trick_taint($content_type); 497 498 # $ENV{HOME} must be defined when using File::MimeInfo::Magic, 499 # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. 500 local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); 501 502 # If we have autodetected application/octet-stream from the Content-Type 503 # header, let's have a better go using a sniffer if available. 504 if (defined Bugzilla->input_params->{contenttypemethod} 505 && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' 506 && $content_type eq 'application/octet-stream' 507 && Bugzilla->feature('typesniffer')) 508 { 509 import File::MimeInfo::Magic qw(mimetype); 510 require IO::Scalar; 511 512 # data is either a filehandle, or the data itself. 513 my $fh = $params->{data}; 514 if (!ref($fh)) { 515 $fh = new IO::Scalar \$fh; 516 } 517 elsif (!$fh->isa('IO::Handle')) { 518 # CGI.pm sends us an Fh that isn't actually an IO::Handle, but 519 # has a method for getting an actual handle out of it. 520 $fh = $fh->handle; 521 # ->handle returns an literal IO::Handle, even though the 522 # underlying object is a file. So we rebless it to be a proper 523 # IO::File object so that we can call ->seek on it and so on. 524 # Just in case CGI.pm fixes this some day, we check ->isa first. 525 if (!$fh->isa('IO::File')) { 526 bless $fh, 'IO::File'; 527 } 528 } 529 530 my $mimetype = mimetype($fh); 531 $fh->seek(0, 0); 532 $content_type = $mimetype if $mimetype; 533 } 534 535 # Make sure patches are viewable in the browser 536 if (!ref($invocant) 537 && defined Bugzilla->input_params->{contenttypemethod} 538 && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' 539 && $content_type =~ m{text/x-(?:diff|patch)}) 540 { 541 $params->{ispatch} = 1; 542 $content_type = 'text/plain'; 543 } 544 545 return $content_type; 546} 547 548sub _check_data { 549 my ($invocant, $params) = @_; 550 551 my $data = $params->{data}; 552 $params->{filesize} = ref $data ? -s $data : length($data); 553 554 Bugzilla::Hook::process('attachment_process_data', { data => \$data, 555 attributes => $params }); 556 557 $params->{filesize} || ThrowUserError('zero_length_file'); 558 # Make sure the attachment does not exceed the maximum permitted size. 559 my $max_size = max(Bugzilla->params->{'maxlocalattachment'} * 1048576, 560 Bugzilla->params->{'maxattachmentsize'} * 1024); 561 562 if ($params->{filesize} > $max_size) { 563 my $vars = { filesize => sprintf("%.0f", $params->{filesize}/1024) }; 564 ThrowUserError('file_too_large', $vars); 565 } 566 return $data; 567} 568 569sub _check_description { 570 my ($invocant, $description) = @_; 571 572 $description = trim($description); 573 $description || ThrowUserError('missing_attachment_description'); 574 return $description; 575} 576 577sub _check_filename { 578 my ($invocant, $filename) = @_; 579 580 $filename = clean_text($filename); 581 if (!$filename) { 582 if (ref $invocant) { 583 ThrowUserError('filename_not_specified'); 584 } 585 else { 586 ThrowUserError('file_not_specified'); 587 } 588 } 589 590 # Remove path info (if any) from the file name. The browser should do this 591 # for us, but some are buggy. This may not work on Mac file names and could 592 # mess up file names with slashes in them, but them's the breaks. We only 593 # use this as a hint to users downloading attachments anyway, so it's not 594 # a big deal if it munges incorrectly occasionally. 595 $filename =~ s/^.*[\/\\]//; 596 597 # Truncate the filename to MAX_ATTACH_FILENAME_LENGTH characters, counting 598 # from the end of the string to make sure we keep the filename extension. 599 $filename = substr($filename, 600 -&MAX_ATTACH_FILENAME_LENGTH, 601 MAX_ATTACH_FILENAME_LENGTH); 602 trick_taint($filename); 603 604 return $filename; 605} 606 607sub _check_is_private { 608 my ($invocant, $is_private) = @_; 609 610 $is_private = $is_private ? 1 : 0; 611 if (((!ref $invocant && $is_private) 612 || (ref $invocant && $invocant->isprivate != $is_private)) 613 && !Bugzilla->user->is_insider) { 614 ThrowUserError('user_not_insider'); 615 } 616 return $is_private; 617} 618 619=pod 620 621=head2 Class Methods 622 623=over 624 625=item C<get_attachments_by_bug($bug)> 626 627Description: retrieves and returns the attachments the currently logged in 628 user can view for the given bug. 629 630Params: C<$bug> - Bugzilla::Bug object - the bug for which 631 to retrieve and return attachments. 632 633Returns: a reference to an array of attachment objects. 634 635=cut 636 637sub get_attachments_by_bug { 638 my ($class, $bug, $vars) = @_; 639 my $user = Bugzilla->user; 640 my $dbh = Bugzilla->dbh; 641 642 # By default, private attachments are not accessible, unless the user 643 # is in the insider group or submitted the attachment. 644 my $and_restriction = ''; 645 my @values = ($bug->id); 646 647 unless ($user->is_insider) { 648 $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; 649 push(@values, $user->id); 650 } 651 652 my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments 653 WHERE bug_id = ? $and_restriction", 654 undef, @values); 655 656 my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); 657 $_->{bug} = $bug foreach @$attachments; 658 659 # To avoid $attachment->flags and $attachment->flag_types running SQL queries 660 # themselves for each attachment listed here, we collect all the data at once and 661 # populate $attachment->{flag_types} ourselves. We also load all attachers and 662 # datasizes at once for the same reason. 663 if ($vars->{preload}) { 664 # Preload flag types and flags 665 my $vars = { target_type => 'attachment', 666 product_id => $bug->product_id, 667 component_id => $bug->component_id, 668 attach_id => $attach_ids }; 669 my $flag_types = Bugzilla::Flag->_flag_types($vars); 670 671 foreach my $attachment (@$attachments) { 672 $attachment->{flag_types} = []; 673 my $new_types = dclone($flag_types); 674 foreach my $new_type (@$new_types) { 675 $new_type->{flags} = [ grep($_->attach_id == $attachment->id, 676 @{ $new_type->{flags} }) ]; 677 push(@{ $attachment->{flag_types} }, $new_type); 678 } 679 } 680 681 # Preload attachers. 682 my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; 683 my $users = Bugzilla::User->new_from_list([keys %user_ids]); 684 my %user_map = map { $_->id => $_ } @$users; 685 foreach my $attachment (@$attachments) { 686 $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; 687 } 688 689 # Preload datasizes. 690 my $sizes = 691 $dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize 692 FROM attachments LEFT JOIN attach_data ON attach_id = id 693 WHERE bug_id = ?', 694 'attach_id', undef, $bug->id); 695 696 # Force the size of attachments not in the DB to be recalculated. 697 $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; 698 } 699 700 return $attachments; 701} 702 703=pod 704 705=item C<validate_can_edit> 706 707Description: validates if the user is allowed to view and edit the attachment. 708 Only the submitter or someone with editbugs privs can edit it. 709 Only the submitter and users in the insider group can view 710 private attachments. 711 712Params: none 713 714Returns: 1 on success, 0 otherwise. 715 716=cut 717 718sub validate_can_edit { 719 my $attachment = shift; 720 my $user = Bugzilla->user; 721 722 # The submitter can edit their attachments. 723 return ($attachment->attacher->id == $user->id 724 || ((!$attachment->isprivate || $user->is_insider) 725 && $user->in_group('editbugs', $attachment->bug->product_id))) ? 1 : 0; 726} 727 728=item C<validate_obsolete($bug, $attach_ids)> 729 730Description: validates if attachments the user wants to mark as obsolete 731 really belong to the given bug and are not already obsolete. 732 Moreover, a user cannot mark an attachment as obsolete if 733 they cannot view it (due to restrictions on it). 734 735Params: $bug - The bug object obsolete attachments should belong to. 736 $attach_ids - The list of attachments to mark as obsolete. 737 738Returns: The list of attachment objects to mark as obsolete. 739 Else an error is thrown. 740 741=cut 742 743sub validate_obsolete { 744 my ($class, $bug, $list) = @_; 745 746 # Make sure the attachment id is valid and the user has permissions to view 747 # the bug to which it is attached. Make sure also that the user can view 748 # the attachment itself. 749 my @obsolete_attachments; 750 foreach my $attachid (@$list) { 751 my $vars = {}; 752 $vars->{'attach_id'} = $attachid; 753 754 detaint_natural($attachid) 755 || ThrowUserError('invalid_attach_id', $vars); 756 757 # Make sure the attachment exists in the database. 758 my $attachment = new Bugzilla::Attachment($attachid) 759 || ThrowUserError('invalid_attach_id', $vars); 760 761 # Check that the user can view and edit this attachment. 762 $attachment->validate_can_edit 763 || ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id }); 764 765 if ($attachment->bug_id != $bug->bug_id) { 766 $vars->{'my_bug_id'} = $bug->bug_id; 767 ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); 768 } 769 770 next if $attachment->isobsolete; 771 772 push(@obsolete_attachments, $attachment); 773 } 774 return @obsolete_attachments; 775} 776 777############################### 778#### Constructors ##### 779############################### 780 781=pod 782 783=item C<create> 784 785Description: inserts an attachment into the given bug. 786 787Params: takes a hashref with the following keys: 788 C<bug> - Bugzilla::Bug object - the bug for which to insert 789 the attachment. 790 C<data> - Either a filehandle pointing to the content of the 791 attachment, or the content of the attachment itself. 792 C<description> - string - describe what the attachment is about. 793 C<filename> - string - the name of the attachment (used by the 794 browser when downloading it). If the attachment is a URL, this 795 parameter has no effect. 796 C<mimetype> - string - a valid MIME type. 797 C<creation_ts> - string (optional) - timestamp of the insert 798 as returned by SELECT LOCALTIMESTAMP(0). 799 C<ispatch> - boolean (optional, default false) - true if the 800 attachment is a patch. 801 C<isprivate> - boolean (optional, default false) - true if 802 the attachment is private. 803 804Returns: The new attachment object. 805 806=cut 807 808sub create { 809 my $class = shift; 810 my $dbh = Bugzilla->dbh; 811 812 $class->check_required_create_fields(@_); 813 my $params = $class->run_create_validators(@_); 814 815 # Extract everything which is not a valid column name. 816 my $bug = delete $params->{bug}; 817 $params->{bug_id} = $bug->id; 818 my $data = delete $params->{data}; 819 my $size = delete $params->{filesize}; 820 821 my $attachment = $class->insert_create_data($params); 822 my $attachid = $attachment->id; 823 824 # The file is too large to be stored in the DB, so we store it locally. 825 if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { 826 my $attachdir = bz_locations()->{'attachdir'}; 827 my $hash = ($attachid % 100) + 100; 828 $hash =~ s/.*(\d\d)$/group.$1/; 829 mkdir "$attachdir/$hash", 0770; 830 chmod 0770, "$attachdir/$hash"; 831 if (ref $data) { 832 copy($data, "$attachdir/$hash/attachment.$attachid"); 833 close $data; 834 } 835 else { 836 open(AH, '>', "$attachdir/$hash/attachment.$attachid"); 837 binmode AH; 838 print AH $data; 839 close AH; 840 } 841 $data = ''; # Will be stored in the DB. 842 } 843 # If we have a filehandle, we need its content to store it in the DB. 844 elsif (ref $data) { 845 local $/; 846 # Store the content in a temp variable while we close the FH. 847 my $tmp = <$data>; 848 close $data; 849 $data = $tmp; 850 } 851 852 my $sth = $dbh->prepare("INSERT INTO attach_data 853 (id, thedata) VALUES ($attachid, ?)"); 854 855 trick_taint($data); 856 $sth->bind_param(1, $data, $dbh->BLOB_TYPE); 857 $sth->execute(); 858 859 $attachment->{bug} = $bug; 860 861 # Return the new attachment object. 862 return $attachment; 863} 864 865sub run_create_validators { 866 my ($class, $params) = @_; 867 868 $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); 869 870 # Let's validate the attachment content first as it may 871 # alter some other attachment attributes. 872 $params->{data} = $class->_check_data($params); 873 $params = $class->SUPER::run_create_validators($params); 874 875 $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); 876 $params->{modification_time} = $params->{creation_ts}; 877 878 return $params; 879} 880 881sub update { 882 my $self = shift; 883 my $dbh = Bugzilla->dbh; 884 my $user = Bugzilla->user; 885 my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); 886 887 my ($changes, $old_self) = $self->SUPER::update(@_); 888 889 my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); 890 if ($removed || $added) { 891 $changes->{'flagtypes.name'} = [$removed, $added]; 892 } 893 894 # Record changes in the activity table. 895 require Bugzilla::Bug; 896 foreach my $field (keys %$changes) { 897 my $change = $changes->{$field}; 898 $field = "attachments.$field" unless $field eq "flagtypes.name"; 899 Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], 900 $change->[1], $user->id, $timestamp, undef, $self->id); 901 } 902 903 if (scalar(keys %$changes)) { 904 $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?', 905 undef, ($timestamp, $self->id)); 906 $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', 907 undef, ($timestamp, $self->bug_id)); 908 $self->{modification_time} = $timestamp; 909 # because we updated the attachments table after SUPER::update(), we 910 # need to ensure the cache is flushed. 911 Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); 912 } 913 914 return $changes; 915} 916 917=pod 918 919=item C<remove_from_db()> 920 921Description: removes an attachment from the DB. 922 923Params: none 924 925Returns: nothing 926 927=back 928 929=cut 930 931sub remove_from_db { 932 my $self = shift; 933 my $dbh = Bugzilla->dbh; 934 935 $dbh->bz_start_transaction(); 936 my $flag_ids = $dbh->selectcol_arrayref( 937 'SELECT id FROM flags WHERE attach_id = ?', undef, $self->id); 938 $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) 939 if @$flag_ids; 940 $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); 941 $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? 942 WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id)); 943 $dbh->bz_commit_transaction(); 944 945 my $filename = $self->_get_local_filename; 946 if (-e $filename) { 947 unlink $filename or warn "Couldn't unlink $filename: $!"; 948 } 949 950 # As we don't call SUPER->remove_from_db we need to manually clear 951 # memcached here. 952 Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); 953 foreach my $flag_id (@$flag_ids) { 954 Bugzilla->memcached->clear({ table => 'flags', id => $flag_id }); 955 } 956} 957 958############################### 959#### Helpers ##### 960############################### 961 962# Extract the content type from the attachment form. 963sub get_content_type { 964 my $cgi = Bugzilla->cgi; 965 966 return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); 967 968 my $content_type; 969 my $method = $cgi->param('contenttypemethod') || ''; 970 971 if ($method eq 'list') { 972 # The user selected a content type from the list, so use their 973 # selection. 974 $content_type = $cgi->param('contenttypeselection'); 975 } 976 elsif ($method eq 'manual') { 977 # The user entered a content type manually, so use their entry. 978 $content_type = $cgi->param('contenttypeentry'); 979 } 980 else { 981 defined $cgi->upload('data') || ThrowUserError('file_not_specified'); 982 # The user asked us to auto-detect the content type, so use the type 983 # specified in the HTTP request headers. 984 $content_type = 985 $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; 986 $content_type || ThrowUserError("missing_content_type"); 987 988 # Internet Explorer sends image/x-png for PNG images, 989 # so convert that to image/png to match other browsers. 990 if ($content_type eq 'image/x-png') { 991 $content_type = 'image/png'; 992 } 993 } 994 return $content_type; 995} 996 997 9981; 999 1000=head1 B<Methods in need of POD> 1001 1002=over 1003 1004=item set_filename 1005 1006=item set_is_obsolete 1007 1008=item DB_COLUMNS 1009 1010=item set_is_private 1011 1012=item set_content_type 1013 1014=item set_description 1015 1016=item get_content_type 1017 1018=item set_flags 1019 1020=item set_is_patch 1021 1022=item update 1023 1024=back 1025