| # -*- Mode: perl; indent-tabs-mode: nil -*- |
| # |
| # The contents of this file are subject to the Mozilla Public |
| # License Version 1.1 (the "License"); you may not use this file |
| # except in compliance with the License. You may obtain a copy of |
| # the License at http://www.mozilla.org/MPL/ |
| # |
| # Software distributed under the License is distributed on an "AS |
| # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or |
| # implied. See the License for the specific language governing |
| # rights and limitations under the License. |
| # |
| # The Original Code is the Bugzilla Bug Tracking System. |
| # |
| # The Initial Developer of the Original Code is Netscape Communications |
| # Corporation. Portions created by Netscape are |
| # Copyright (C) 1998 Netscape Communications Corporation. All |
| # Rights Reserved. |
| # |
| # Contributor(s): Terry Weissman <terry@mozilla.org> |
| # Myk Melez <myk@mozilla.org> |
| # Marc Schumann <wurblzap@gmail.com> |
| # Frédéric Buclin <LpSolit@gmail.com> |
| |
| use strict; |
| |
| package Bugzilla::Attachment; |
| |
| =head1 NAME |
| |
| Bugzilla::Attachment - a file related to a bug that a user has uploaded |
| to the Bugzilla server |
| |
| =head1 SYNOPSIS |
| |
| use Bugzilla::Attachment; |
| |
| # Get the attachment with the given ID. |
| my $attachment = Bugzilla::Attachment->get($attach_id); |
| |
| # Get the attachments with the given IDs. |
| my $attachments = Bugzilla::Attachment->get_list($attach_ids); |
| |
| =head1 DESCRIPTION |
| |
| This module defines attachment objects, which represent files related to bugs |
| that users upload to the Bugzilla server. |
| |
| =cut |
| |
| use Bugzilla::Constants; |
| use Bugzilla::Error; |
| use Bugzilla::Flag; |
| use Bugzilla::User; |
| use Bugzilla::Util; |
| use Bugzilla::Field; |
| |
| sub get { |
| my $invocant = shift; |
| my $id = shift; |
| |
| my $attachments = _retrieve([$id]); |
| my $self = $attachments->[0]; |
| bless($self, ref($invocant) || $invocant) if $self; |
| |
| return $self; |
| } |
| |
| sub get_list { |
| my $invocant = shift; |
| my $ids = shift; |
| |
| my $attachments = _retrieve($ids); |
| foreach my $attachment (@$attachments) { |
| bless($attachment, ref($invocant) || $invocant); |
| } |
| |
| return $attachments; |
| } |
| |
| sub _retrieve { |
| my ($ids) = @_; |
| |
| return [] if scalar(@$ids) == 0; |
| |
| my @columns = ( |
| 'attachments.attach_id AS id', |
| 'attachments.bug_id AS bug_id', |
| 'attachments.description AS description', |
| 'attachments.mimetype AS contenttype', |
| 'attachments.submitter_id AS attacher_id', |
| Bugzilla->dbh->sql_date_format('attachments.creation_ts', |
| '%Y.%m.%d %H:%i') . " AS attached", |
| 'attachments.modification_time', |
| 'attachments.filename AS filename', |
| 'attachments.ispatch AS ispatch', |
| 'attachments.isurl AS isurl', |
| 'attachments.isobsolete AS isobsolete', |
| 'attachments.isprivate AS isprivate' |
| ); |
| my $columns = join(", ", @columns); |
| my $dbh = Bugzilla->dbh; |
| my $records = $dbh->selectall_arrayref( |
| "SELECT $columns |
| FROM attachments |
| WHERE " |
| . Bugzilla->dbh->sql_in('attach_id', $ids) |
| . " ORDER BY attach_id", |
| { Slice => {} }); |
| return $records; |
| } |
| |
| =pod |
| |
| =head2 Instance Properties |
| |
| =over |
| |
| =item C<id> |
| |
| the unique identifier for the attachment |
| |
| =back |
| |
| =cut |
| |
| sub id { |
| my $self = shift; |
| return $self->{id}; |
| } |
| |
| =over |
| |
| =item C<bug_id> |
| |
| the ID of the bug to which the attachment is attached |
| |
| =back |
| |
| =cut |
| |
| # XXX Once Bug.pm slims down sufficiently this should become a reference |
| # to a bug object. |
| sub bug_id { |
| my $self = shift; |
| return $self->{bug_id}; |
| } |
| |
| =over |
| |
| =item C<description> |
| |
| user-provided text describing the attachment |
| |
| =back |
| |
| =cut |
| |
| sub description { |
| my $self = shift; |
| return $self->{description}; |
| } |
| |
| =over |
| |
| =item C<contenttype> |
| |
| the attachment's MIME media type |
| |
| =back |
| |
| =cut |
| |
| sub contenttype { |
| my $self = shift; |
| return $self->{contenttype}; |
| } |
| |
| =over |
| |
| =item C<attacher> |
| |
| the user who attached the attachment |
| |
| =back |
| |
| =cut |
| |
| sub attacher { |
| my $self = shift; |
| return $self->{attacher} if exists $self->{attacher}; |
| $self->{attacher} = new Bugzilla::User($self->{attacher_id}); |
| return $self->{attacher}; |
| } |
| |
| =over |
| |
| =item C<attached> |
| |
| the date and time on which the attacher attached the attachment |
| |
| =back |
| |
| =cut |
| |
| sub attached { |
| my $self = shift; |
| return $self->{attached}; |
| } |
| |
| =over |
| |
| =item C<modification_time> |
| |
| the date and time on which the attachment was last modified. |
| |
| =back |
| |
| =cut |
| |
| sub modification_time { |
| my $self = shift; |
| return $self->{modification_time}; |
| } |
| |
| =over |
| |
| =item C<filename> |
| |
| the name of the file the attacher attached |
| |
| =back |
| |
| =cut |
| |
| sub filename { |
| my $self = shift; |
| return $self->{filename}; |
| } |
| |
| =over |
| |
| =item C<ispatch> |
| |
| whether or not the attachment is a patch |
| |
| =back |
| |
| =cut |
| |
| sub ispatch { |
| my $self = shift; |
| return $self->{ispatch}; |
| } |
| |
| =over |
| |
| =item C<isurl> |
| |
| whether or not the attachment is a URL |
| |
| =back |
| |
| =cut |
| |
| sub isurl { |
| my $self = shift; |
| return $self->{isurl}; |
| } |
| |
| =over |
| |
| =item C<isobsolete> |
| |
| whether or not the attachment is obsolete |
| |
| =back |
| |
| =cut |
| |
| sub isobsolete { |
| my $self = shift; |
| return $self->{isobsolete}; |
| } |
| |
| =over |
| |
| =item C<isprivate> |
| |
| whether or not the attachment is private |
| |
| =back |
| |
| =cut |
| |
| sub isprivate { |
| my $self = shift; |
| return $self->{isprivate}; |
| } |
| |
| =over |
| |
| =item C<is_viewable> |
| |
| Returns 1 if the attachment has a content-type viewable in this browser. |
| Note that we don't use $cgi->Accept()'s ability to check if a content-type |
| matches, because this will return a value even if it's matched by the generic |
| */* which most browsers add to the end of their Accept: headers. |
| |
| =back |
| |
| =cut |
| |
| sub is_viewable { |
| my $self = shift; |
| my $contenttype = $self->contenttype; |
| my $cgi = Bugzilla->cgi; |
| |
| # We assume we can view all text and image types. |
| return 1 if ($contenttype =~ /^(text|image)\//); |
| |
| # Mozilla can view XUL. Note the trailing slash on the Gecko detection to |
| # avoid sending XUL to Safari. |
| return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./) |
| && ($cgi->user_agent() =~ /Gecko\//)); |
| |
| # If it's not one of the above types, we check the Accept: header for any |
| # types mentioned explicitly. |
| my $accept = join(",", $cgi->Accept()); |
| return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); |
| |
| return 0; |
| } |
| |
| =over |
| |
| =item C<data> |
| |
| the content of the attachment |
| |
| =back |
| |
| =cut |
| |
| sub data { |
| my $self = shift; |
| return $self->{data} if exists $self->{data}; |
| |
| # First try to get the attachment data from the database. |
| ($self->{data}) = Bugzilla->dbh->selectrow_array("SELECT thedata |
| FROM attach_data |
| WHERE id = ?", |
| undef, |
| $self->{id}); |
| |
| # If there's no attachment data in the database, the attachment is stored |
| # in a local file, so retrieve it from there. |
| if (length($self->{data}) == 0) { |
| if (open(AH, $self->_get_local_filename())) { |
| local $/; |
| binmode AH; |
| $self->{data} = <AH>; |
| close(AH); |
| } |
| } |
| |
| return $self->{data}; |
| } |
| |
| =over |
| |
| =item C<datasize> |
| |
| the length (in characters) of the attachment content |
| |
| =back |
| |
| =cut |
| |
| # datasize is a property of the data itself, and it's unclear whether we should |
| # expose it at all, since you can easily derive it from the data itself: in TT, |
| # attachment.data.size; in Perl, length($attachment->{data}). But perhaps |
| # it makes sense for performance reasons, since accessing the data forces it |
| # to get retrieved from the database/filesystem and loaded into memory, |
| # while datasize avoids loading the attachment into memory, calling SQL's |
| # LENGTH() function or stat()ing the file instead. I've left it in for now. |
| |
| sub datasize { |
| my $self = shift; |
| return $self->{datasize} if exists $self->{datasize}; |
| |
| # If we have already retrieved the data, return its size. |
| return length($self->{data}) if exists $self->{data}; |
| |
| $self->{datasize} = |
| Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata) |
| FROM attach_data |
| WHERE id = ?", |
| undef, $self->{id}) || 0; |
| |
| # If there's no attachment data in the database, either the attachment |
| # is stored in a local file, and so retrieve its size from the file, |
| # or the attachment has been deleted. |
| unless ($self->{datasize}) { |
| if (open(AH, $self->_get_local_filename())) { |
| binmode AH; |
| $self->{datasize} = (stat(AH))[7]; |
| close(AH); |
| } |
| } |
| |
| return $self->{datasize}; |
| } |
| |
| =over |
| |
| =item C<flags> |
| |
| flags that have been set on the attachment |
| |
| =back |
| |
| =cut |
| |
| sub flags { |
| my $self = shift; |
| return $self->{flags} if exists $self->{flags}; |
| |
| $self->{flags} = Bugzilla::Flag->match({ 'attach_id' => $self->id }); |
| return $self->{flags}; |
| } |
| |
| # Instance methods; no POD documentation here yet because the only ones so far |
| # are private. |
| |
| sub _get_local_filename { |
| my $self = shift; |
| my $hash = ($self->id % 100) + 100; |
| $hash =~ s/.*(\d\d)$/group.$1/; |
| return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; |
| } |
| |
| sub _validate_filename { |
| my ($throw_error) = @_; |
| my $cgi = Bugzilla->cgi; |
| defined $cgi->upload('data') |
| || ($throw_error ? ThrowUserError("file_not_specified") : return 0); |
| |
| my $filename = $cgi->upload('data'); |
| |
| # Remove path info (if any) from the file name. The browser should do this |
| # for us, but some are buggy. This may not work on Mac file names and could |
| # mess up file names with slashes in them, but them's the breaks. We only |
| # use this as a hint to users downloading attachments anyway, so it's not |
| # a big deal if it munges incorrectly occasionally. |
| $filename =~ s/^.*[\/\\]//; |
| |
| # Truncate the filename to 100 characters, counting from the end of the |
| # string to make sure we keep the filename extension. |
| $filename = substr($filename, -100, 100); |
| |
| return $filename; |
| } |
| |
| sub _validate_data { |
| my ($throw_error, $hr_vars) = @_; |
| my $cgi = Bugzilla->cgi; |
| my $maxsize = $cgi->param('ispatch') ? Bugzilla->params->{'maxpatchsize'} |
| : Bugzilla->params->{'maxattachmentsize'}; |
| $maxsize *= 1024; # Convert from K |
| my $fh; |
| # Skip uploading into a local variable if the user wants to upload huge |
| # attachments into local files. |
| if (!$cgi->param('bigfile')) { |
| $fh = $cgi->upload('data'); |
| } |
| my $data; |
| |
| # We could get away with reading only as much as required, except that then |
| # we wouldn't have a size to print to the error handler below. |
| if (!$cgi->param('bigfile')) { |
| # enable 'slurp' mode |
| local $/; |
| $data = <$fh>; |
| } |
| |
| $data |
| || ($cgi->param('bigfile')) |
| || ($throw_error ? ThrowUserError("zero_length_file") : return 0); |
| |
| # Windows screenshots are usually uncompressed BMP files which |
| # makes for a quick way to eat up disk space. Let's compress them. |
| # We do this before we check the size since the uncompressed version |
| # could easily be greater than maxattachmentsize. |
| if (Bugzilla->params->{'convert_uncompressed_images'} |
| && $cgi->param('contenttype') eq 'image/bmp') { |
| require Image::Magick; |
| my $img = Image::Magick->new(magick=>'bmp'); |
| $img->BlobToImage($data); |
| $img->set(magick=>'png'); |
| my $imgdata = $img->ImageToBlob(); |
| $data = $imgdata; |
| $cgi->param('contenttype', 'image/png'); |
| $hr_vars->{'convertedbmp'} = 1; |
| } |
| |
| # Make sure the attachment does not exceed the maximum permitted size |
| my $len = $data ? length($data) : 0; |
| if ($maxsize && $len > $maxsize) { |
| my $vars = { filesize => sprintf("%.0f", $len/1024) }; |
| if ($cgi->param('ispatch')) { |
| $throw_error ? ThrowUserError("patch_too_large", $vars) : return 0; |
| } |
| else { |
| $throw_error ? ThrowUserError("file_too_large", $vars) : return 0; |
| } |
| } |
| |
| return $data || ''; |
| } |
| |
| =pod |
| |
| =head2 Class Methods |
| |
| =over |
| |
| =item C<get_attachments_by_bug($bug_id)> |
| |
| Description: retrieves and returns the attachments the currently logged in |
| user can view for the given bug. |
| |
| Params: C<$bug_id> - integer - the ID of the bug for which |
| to retrieve and return attachments. |
| |
| Returns: a reference to an array of attachment objects. |
| |
| =cut |
| |
| sub get_attachments_by_bug { |
| my ($class, $bug_id) = @_; |
| my $user = Bugzilla->user; |
| my $dbh = Bugzilla->dbh; |
| |
| # By default, private attachments are not accessible, unless the user |
| # is in the insider group or submitted the attachment. |
| my $and_restriction = ''; |
| my @values = ($bug_id); |
| |
| unless ($user->is_insider) { |
| $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; |
| push(@values, $user->id); |
| } |
| |
| my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments |
| WHERE bug_id = ? $and_restriction", |
| undef, @values); |
| my $attachments = Bugzilla::Attachment->get_list($attach_ids); |
| return $attachments; |
| } |
| |
| =pod |
| |
| =item C<validate_is_patch()> |
| |
| Description: validates the "patch" flag passed in by CGI. |
| |
| Returns: 1 on success. |
| |
| =cut |
| |
| sub validate_is_patch { |
| my ($class, $throw_error) = @_; |
| my $cgi = Bugzilla->cgi; |
| |
| # Set the ispatch flag to zero if it is undefined, since the UI uses |
| # an HTML checkbox to represent this flag, and unchecked HTML checkboxes |
| # do not get sent in HTML requests. |
| $cgi->param('ispatch', $cgi->param('ispatch') ? 1 : 0); |
| |
| # Set the content type to text/plain if the attachment is a patch. |
| $cgi->param('contenttype', 'text/plain') if $cgi->param('ispatch'); |
| |
| return 1; |
| } |
| |
| =pod |
| |
| =item C<validate_description()> |
| |
| Description: validates the description passed in by CGI. |
| |
| Returns: 1 on success. |
| |
| =cut |
| |
| sub validate_description { |
| my ($class, $throw_error) = @_; |
| my $cgi = Bugzilla->cgi; |
| |
| $cgi->param('description') |
| || ($throw_error ? ThrowUserError("missing_attachment_description") : return 0); |
| |
| return 1; |
| } |
| |
| =pod |
| |
| =item C<validate_content_type()> |
| |
| Description: validates the content type passed in by CGI. |
| |
| Returns: 1 on success. |
| |
| =cut |
| |
| sub validate_content_type { |
| my ($class, $throw_error) = @_; |
| my $cgi = Bugzilla->cgi; |
| |
| if (!defined $cgi->param('contenttypemethod')) { |
| $throw_error ? ThrowUserError("missing_content_type_method") : return 0; |
| } |
| elsif ($cgi->param('contenttypemethod') eq 'autodetect') { |
| my $contenttype = |
| $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; |
| # The user asked us to auto-detect the content type, so use the type |
| # specified in the HTTP request headers. |
| if ( !$contenttype ) { |
| $throw_error ? ThrowUserError("missing_content_type") : return 0; |
| } |
| $cgi->param('contenttype', $contenttype); |
| } |
| elsif ($cgi->param('contenttypemethod') eq 'list') { |
| # The user selected a content type from the list, so use their |
| # selection. |
| $cgi->param('contenttype', $cgi->param('contenttypeselection')); |
| } |
| elsif ($cgi->param('contenttypemethod') eq 'manual') { |
| # The user entered a content type manually, so use their entry. |
| $cgi->param('contenttype', $cgi->param('contenttypeentry')); |
| } |
| else { |
| $throw_error ? |
| ThrowCodeError("illegal_content_type_method", |
| { contenttypemethod => $cgi->param('contenttypemethod') }) : |
| return 0; |
| } |
| |
| if ( $cgi->param('contenttype') !~ |
| /^(application|audio|image|message|model|multipart|text|video)\/.+$/ ) { |
| $throw_error ? |
| ThrowUserError("invalid_content_type", |
| { contenttype => $cgi->param('contenttype') }) : |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| =pod |
| |
| =item C<validate_can_edit($attachment, $product_id)> |
| |
| Description: validates if the user is allowed to view and edit the attachment. |
| Only the submitter or someone with editbugs privs can edit it. |
| Only the submitter and users in the insider group can view |
| private attachments. |
| |
| Params: $attachment - the attachment object being edited. |
| $product_id - the product ID the attachment belongs to. |
| |
| Returns: 1 on success. Else an error is thrown. |
| |
| =cut |
| |
| sub validate_can_edit { |
| my ($attachment, $product_id) = @_; |
| my $user = Bugzilla->user; |
| |
| # The submitter can edit their attachments. |
| return 1 if ($attachment->attacher->id == $user->id |
| || ((!$attachment->isprivate || $user->is_insider) |
| && $user->in_group('editbugs', $product_id))); |
| |
| # If we come here, then this attachment cannot be seen by the user. |
| ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id }); |
| } |
| |
| =item C<validate_obsolete($bug)> |
| |
| Description: validates if attachments the user wants to mark as obsolete |
| really belong to the given bug and are not already obsolete. |
| Moreover, a user cannot mark an attachment as obsolete if |
| he cannot view it (due to restrictions on it). |
| |
| Params: $bug - The bug object obsolete attachments should belong to. |
| |
| Returns: 1 on success. Else an error is thrown. |
| |
| =cut |
| |
| sub validate_obsolete { |
| my ($class, $bug) = @_; |
| my $cgi = Bugzilla->cgi; |
| |
| # Make sure the attachment id is valid and the user has permissions to view |
| # the bug to which it is attached. Make sure also that the user can view |
| # the attachment itself. |
| my @obsolete_attachments; |
| foreach my $attachid ($cgi->param('obsolete')) { |
| my $vars = {}; |
| $vars->{'attach_id'} = $attachid; |
| |
| detaint_natural($attachid) |
| || ThrowCodeError('invalid_attach_id_to_obsolete', $vars); |
| |
| my $attachment = Bugzilla::Attachment->get($attachid); |
| |
| # Make sure the attachment exists in the database. |
| ThrowUserError('invalid_attach_id', $vars) unless $attachment; |
| |
| # Check that the user can view and edit this attachment. |
| $attachment->validate_can_edit($bug->product_id); |
| |
| $vars->{'description'} = $attachment->description; |
| |
| if ($attachment->bug_id != $bug->bug_id) { |
| $vars->{'my_bug_id'} = $bug->bug_id; |
| $vars->{'attach_bug_id'} = $attachment->bug_id; |
| ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars); |
| } |
| |
| if ($attachment->isobsolete) { |
| ThrowCodeError('attachment_already_obsolete', $vars); |
| } |
| |
| push(@obsolete_attachments, $attachment); |
| } |
| return @obsolete_attachments; |
| } |
| |
| |
| =pod |
| |
| =item C<insert_attachment_for_bug($throw_error, $bug, $user, $timestamp, $hr_vars)> |
| |
| Description: inserts an attachment from CGI input for the given bug. |
| |
| Params: C<$bug> - Bugzilla::Bug object - the bug for which to insert |
| the attachment. |
| C<$user> - Bugzilla::User object - the user we're inserting an |
| attachment for. |
| C<$timestamp> - scalar - timestamp of the insert as returned |
| by SELECT NOW(). |
| C<$hr_vars> - hash reference - reference to a hash of template |
| variables. |
| |
| Returns: the ID of the new attachment. |
| |
| =cut |
| |
| sub insert_attachment_for_bug { |
| my ($class, $throw_error, $bug, $user, $timestamp, $hr_vars) = @_; |
| |
| my $cgi = Bugzilla->cgi; |
| my $dbh = Bugzilla->dbh; |
| my $attachurl = $cgi->param('attachurl') || ''; |
| my $data; |
| my $filename; |
| my $contenttype; |
| my $isurl; |
| $class->validate_is_patch($throw_error) || return; |
| $class->validate_description($throw_error) || return; |
| |
| if (Bugzilla->params->{'allow_attach_url'} |
| && ($attachurl =~ /^(http|https|ftp):\/\/\S+/) |
| && !defined $cgi->upload('data')) |
| { |
| $filename = ''; |
| $data = $attachurl; |
| $isurl = 1; |
| $contenttype = 'text/plain'; |
| $cgi->param('ispatch', 0); |
| $cgi->delete('bigfile'); |
| } |
| else { |
| $filename = _validate_filename($throw_error) || return; |
| # need to validate content type before data as |
| # we now check the content type for image/bmp in _validate_data() |
| unless ($cgi->param('ispatch')) { |
| $class->validate_content_type($throw_error) || return; |
| |
| # Set the ispatch flag to 1 if we're set to autodetect |
| # and the content type is text/x-diff or text/x-patch |
| if ($cgi->param('contenttypemethod') eq 'autodetect' |
| && $cgi->param('contenttype') =~ m{text/x-(?:diff|patch)}) |
| { |
| $cgi->param('ispatch', 1); |
| $cgi->param('contenttype', 'text/plain'); |
| } |
| } |
| $data = _validate_data($throw_error, $hr_vars); |
| # If the attachment is stored locally, $data eq ''. |
| # If an error is thrown, $data eq '0'. |
| ($data ne '0') || return; |
| $contenttype = $cgi->param('contenttype'); |
| |
| # These are inserted using placeholders so no need to panic |
| trick_taint($filename); |
| trick_taint($contenttype); |
| $isurl = 0; |
| } |
| |
| # Check attachments the user tries to mark as obsolete. |
| my @obsolete_attachments; |
| if ($cgi->param('obsolete')) { |
| @obsolete_attachments = $class->validate_obsolete($bug); |
| } |
| |
| # The order of these function calls is important, as Flag::validate |
| # assumes User::match_field has ensured that the |
| # values in the requestee fields are legitimate user email addresses. |
| my $match_status = Bugzilla::User::match_field($cgi, { |
| '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, |
| }, MATCH_SKIP_CONFIRM); |
| |
| $hr_vars->{'match_field'} = 'requestee'; |
| if ($match_status == USER_MATCH_FAILED) { |
| $hr_vars->{'message'} = 'user_match_failed'; |
| } |
| elsif ($match_status == USER_MATCH_MULTIPLE) { |
| $hr_vars->{'message'} = 'user_match_multiple'; |
| } |
| |
| # Escape characters in strings that will be used in SQL statements. |
| my $description = $cgi->param('description'); |
| trick_taint($description); |
| my $isprivate = $cgi->param('isprivate') ? 1 : 0; |
| |
| # Insert the attachment into the database. |
| my $sth = $dbh->do( |
| "INSERT INTO attachments |
| (bug_id, creation_ts, modification_time, filename, description, |
| mimetype, ispatch, isurl, isprivate, submitter_id) |
| VALUES (?,?,?,?,?,?,?,?,?,?)", undef, ($bug->bug_id, $timestamp, $timestamp, |
| $filename, $description, $contenttype, $cgi->param('ispatch'), |
| $isurl, $isprivate, $user->id)); |
| # Retrieve the ID of the newly created attachment record. |
| my $attachid = $dbh->bz_last_key('attachments', 'attach_id'); |
| |
| # We only use $data here in this INSERT with a placeholder, |
| # so it's safe. |
| $sth = $dbh->prepare("INSERT INTO attach_data |
| (id, thedata) VALUES ($attachid, ?)"); |
| trick_taint($data); |
| $sth->bind_param(1, $data, $dbh->BLOB_TYPE); |
| $sth->execute(); |
| |
| # If the file is to be stored locally, stream the file from the web server |
| # to the local file without reading it into a local variable. |
| if ($cgi->param('bigfile')) { |
| my $attachdir = bz_locations()->{'attachdir'}; |
| my $fh = $cgi->upload('data'); |
| my $hash = ($attachid % 100) + 100; |
| $hash =~ s/.*(\d\d)$/group.$1/; |
| mkdir "$attachdir/$hash", 0770; |
| chmod 0770, "$attachdir/$hash"; |
| open(AH, ">$attachdir/$hash/attachment.$attachid"); |
| binmode AH; |
| my $sizecount = 0; |
| my $limit = (Bugzilla->params->{"maxlocalattachment"} * 1048576); |
| while (<$fh>) { |
| print AH $_; |
| $sizecount += length($_); |
| if ($sizecount > $limit) { |
| close AH; |
| close $fh; |
| unlink "$attachdir/$hash/attachment.$attachid"; |
| $throw_error ? ThrowUserError("local_file_too_large") : return; |
| } |
| } |
| close AH; |
| close $fh; |
| } |
| |
| # Make existing attachments obsolete. |
| my $fieldid = get_field_id('attachments.isobsolete'); |
| |
| foreach my $obsolete_attachment (@obsolete_attachments) { |
| # If the obsolete attachment has request flags, cancel them. |
| # This call must be done before updating the 'attachments' table. |
| Bugzilla::Flag->CancelRequests($bug, $obsolete_attachment, $timestamp); |
| |
| $dbh->do('UPDATE attachments SET isobsolete = 1, modification_time = ? |
| WHERE attach_id = ?', |
| undef, ($timestamp, $obsolete_attachment->id)); |
| |
| $dbh->do('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when, |
| fieldid, removed, added) |
| VALUES (?,?,?,?,?,?,?)', |
| undef, ($bug->bug_id, $obsolete_attachment->id, $user->id, |
| $timestamp, $fieldid, 0, 1)); |
| } |
| |
| my $attachment = Bugzilla::Attachment->get($attachid); |
| |
| # 1. Add flags, if any. To avoid dying if something goes wrong |
| # while processing flags, we will eval() flag validation. |
| # This requires errors to die(). |
| # XXX: this can go away as soon as flag validation is able to |
| # fail without dying. |
| # |
| # 2. Flag::validate() should not detect any reference to existing flags |
| # when creating a new attachment. Setting the third param to -1 will |
| # force this function to check this point. |
| my $error_mode_cache = Bugzilla->error_mode; |
| Bugzilla->error_mode(ERROR_MODE_DIE); |
| eval { |
| Bugzilla::Flag::validate($bug->bug_id, -1, SKIP_REQUESTEE_ON_ERROR); |
| Bugzilla::Flag->process($bug, $attachment, $timestamp, $hr_vars); |
| }; |
| Bugzilla->error_mode($error_mode_cache); |
| if ($@) { |
| $hr_vars->{'message'} = 'flag_creation_failed'; |
| $hr_vars->{'flag_creation_error'} = $@; |
| } |
| |
| # Return the new attachment object. |
| return $attachment; |
| } |
| |
| =pod |
| |
| =item C<remove_from_db()> |
| |
| Description: removes an attachment from the DB. |
| |
| Params: none |
| |
| Returns: nothing |
| |
| =back |
| |
| =cut |
| |
| sub remove_from_db { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| $dbh->bz_start_transaction(); |
| $dbh->do('DELETE FROM flags WHERE attach_id = ?', undef, $self->id); |
| $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); |
| $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isurl = ?, isobsolete = ? |
| WHERE attach_id = ?', undef, ('text/plain', 0, 0, 1, $self->id)); |
| $dbh->bz_commit_transaction(); |
| } |
| |
| 1; |