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