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