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