diff options
Diffstat (limited to 'Bugzilla/Attachment.pm')
-rw-r--r-- | Bugzilla/Attachment.pm | 997 |
1 files changed, 514 insertions, 483 deletions
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm index 33183797b..26f768c2f 100644 --- a/Bugzilla/Attachment.pm +++ b/Bugzilla/Attachment.pm @@ -57,55 +57,51 @@ use parent qw(Bugzilla::Object); use constant DB_TABLE => 'attachments'; use constant ID_FIELD => 'attach_id'; use constant LIST_ORDER => ID_FIELD; + # Attachments are tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant DB_COLUMNS => qw( - attach_id - bug_id - creation_ts - description - filename - isobsolete - ispatch - isprivate - mimetype - modification_time - submitter_id + attach_id + bug_id + creation_ts + description + filename + isobsolete + ispatch + isprivate + mimetype + modification_time + submitter_id ); -use constant REQUIRED_FIELD_MAP => { - bug_id => 'bug', -}; +use constant REQUIRED_FIELD_MAP => {bug_id => 'bug',}; use constant EXTRA_REQUIRED_FIELDS => qw(data); use constant UPDATE_COLUMNS => qw( - description - filename - isobsolete - ispatch - isprivate - mimetype + description + filename + isobsolete + ispatch + isprivate + mimetype ); use constant VALIDATORS => { - bug => \&_check_bug, - description => \&_check_description, - filename => \&_check_filename, - ispatch => \&Bugzilla::Object::check_boolean, - isprivate => \&_check_is_private, - mimetype => \&_check_content_type, + bug => \&_check_bug, + description => \&_check_description, + filename => \&_check_filename, + ispatch => \&Bugzilla::Object::check_boolean, + isprivate => \&_check_is_private, + mimetype => \&_check_content_type, }; -use constant VALIDATOR_DEPENDENCIES => { - content_type => ['ispatch'], - mimetype => ['ispatch'], -}; +use constant VALIDATOR_DEPENDENCIES => + {content_type => ['ispatch'], mimetype => ['ispatch'],}; -use constant UPDATE_VALIDATORS => { - isobsolete => \&Bugzilla::Object::check_boolean, -}; +use constant UPDATE_VALIDATORS => + {isobsolete => \&Bugzilla::Object::check_boolean,}; ############################### #### Accessors ###### @@ -126,7 +122,7 @@ the ID of the bug to which the attachment is attached =cut sub bug_id { - return $_[0]->{bug_id}; + return $_[0]->{bug_id}; } =over @@ -140,8 +136,8 @@ the bug object to which the attachment is attached =cut sub bug { - require Bugzilla::Bug; - return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 }); + require Bugzilla::Bug; + return $_[0]->{bug} //= Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1}); } =over @@ -155,7 +151,7 @@ user-provided text describing the attachment =cut sub description { - return $_[0]->{description}; + return $_[0]->{description}; } =over @@ -169,7 +165,7 @@ the attachment's MIME media type =cut sub contenttype { - return $_[0]->{mimetype}; + return $_[0]->{mimetype}; } =over @@ -183,8 +179,8 @@ the user who attached the attachment =cut sub attacher { - return $_[0]->{attacher} - //= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 }); + return $_[0]->{attacher} + //= new Bugzilla::User({id => $_[0]->{submitter_id}, cache => 1}); } =over @@ -198,7 +194,7 @@ the date and time on which the attacher attached the attachment =cut sub attached { - return $_[0]->{creation_ts}; + return $_[0]->{creation_ts}; } =over @@ -212,7 +208,7 @@ the date and time on which the attachment was last modified. =cut sub modification_time { - return $_[0]->{modification_time}; + return $_[0]->{modification_time}; } =over @@ -226,7 +222,7 @@ the name of the file the attacher attached =cut sub filename { - return $_[0]->{filename}; + return $_[0]->{filename}; } =over @@ -240,7 +236,7 @@ whether or not the attachment is a patch =cut sub ispatch { - return $_[0]->{ispatch}; + return $_[0]->{ispatch}; } =over @@ -254,7 +250,7 @@ whether or not the attachment is obsolete =cut sub isobsolete { - return $_[0]->{isobsolete}; + return $_[0]->{isobsolete}; } =over @@ -268,7 +264,7 @@ whether or not the attachment is private =cut sub isprivate { - return $_[0]->{isprivate}; + return $_[0]->{isprivate}; } =over @@ -285,23 +281,24 @@ matches, because this will return a value even if it's matched by the generic =cut sub is_viewable { - my $contenttype = $_[0]->contenttype; - my $cgi = Bugzilla->cgi; + my $contenttype = $_[0]->contenttype; + my $cgi = Bugzilla->cgi; - # We assume we can view all text and image types. - return 1 if ($contenttype =~ /^(text|image)\//); + # 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\//)); + # 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(,.*)?$/); + # 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; + return 0; } =over @@ -315,28 +312,29 @@ the content of the attachment =cut sub data { - my $self = shift; - return $self->{data} if exists $self->{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 + # 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); - } + 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}; + return $self->{data}; } =over @@ -358,37 +356,37 @@ the length (in bytes) of the attachment content # LENGTH() function or stat()ing the file instead. I've left it in for now. sub datasize { - my $self = shift; - return $self->{datasize} if defined $self->{datasize}; + my $self = shift; + return $self->{datasize} if defined $self->{datasize}; - # If we have already retrieved the data, return its size. - return length($self->{data}) if exists $self->{data}; + # 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) + $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); - } + 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}; + return $self->{datasize}; } 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; + my $self = shift; + my $hash = ($self->id % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; } =over @@ -402,8 +400,9 @@ flags that have been set on the attachment =cut sub flags { - # Don't cache it as it must be in sync with ->flag_types. - return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; + + # Don't cache it as it must be in sync with ->flag_types. + return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; } =over @@ -418,202 +417,216 @@ already set, grouped by flag type. =cut sub flag_types { - my $self = shift; - return $self->{flag_types} if exists $self->{flag_types}; + my $self = shift; + return $self->{flag_types} if exists $self->{flag_types}; - my $vars = { target_type => 'attachment', - product_id => $self->bug->product_id, - component_id => $self->bug->component_id, - attach_id => $self->id }; + my $vars = { + target_type => 'attachment', + product_id => $self->bug->product_id, + component_id => $self->bug->component_id, + attach_id => $self->id + }; - return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); + return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); } ############################### #### Validators ###### ############################### -sub set_content_type { $_[0]->set('mimetype', $_[1]); } +sub set_content_type { $_[0]->set('mimetype', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_filename { $_[0]->set('filename', $_[1]); } -sub set_is_patch { $_[0]->set('ispatch', $_[1]); } -sub set_is_private { $_[0]->set('isprivate', $_[1]); } - -sub set_is_obsolete { - my ($self, $obsolete) = @_; - - my $old = $self->isobsolete; - $self->set('isobsolete', $obsolete); - my $new = $self->isobsolete; - - # If the attachment is being marked as obsolete, cancel pending requests. - if ($new && $old != $new) { - my @requests = grep { $_->status eq '?' } @{$self->flags}; - return unless scalar @requests; - - my %flag_ids = map { $_->id => 1 } @requests; - foreach my $flagtype (@{$self->flag_types}) { - @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}}; - } +sub set_filename { $_[0]->set('filename', $_[1]); } +sub set_is_patch { $_[0]->set('ispatch', $_[1]); } +sub set_is_private { $_[0]->set('isprivate', $_[1]); } + +sub set_is_obsolete { + my ($self, $obsolete) = @_; + + my $old = $self->isobsolete; + $self->set('isobsolete', $obsolete); + my $new = $self->isobsolete; + + # If the attachment is being marked as obsolete, cancel pending requests. + if ($new && $old != $new) { + my @requests = grep { $_->status eq '?' } @{$self->flags}; + return unless scalar @requests; + + my %flag_ids = map { $_->id => 1 } @requests; + foreach my $flagtype (@{$self->flag_types}) { + @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}}; } + } } sub set_flags { - my ($self, $flags, $new_flags) = @_; + my ($self, $flags, $new_flags) = @_; - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); } sub _check_bug { - my ($invocant, $bug) = @_; - my $user = Bugzilla->user; + my ($invocant, $bug) = @_; + my $user = Bugzilla->user; - $bug = ref $invocant ? $invocant->bug : $bug; + $bug = ref $invocant ? $invocant->bug : $bug; - $bug || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'bug' }); + $bug + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'bug'}); - ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) - || ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bug->id }); + ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) + || ThrowUserError("illegal_attachment_edit_bug", {bug_id => $bug->id}); - return $bug; + return $bug; } sub _check_content_type { - my ($invocant, $content_type, undef, $params) = @_; - - my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; - $content_type = 'text/plain' if $is_patch; - $content_type = clean_text($content_type); - # The subsets below cover all existing MIME types and charsets registered by IANA. - # (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) - my $legal_types = join('|', LEGAL_CONTENT_TYPES); - if (!$content_type - || $content_type !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) - { - ThrowUserError("invalid_content_type", { contenttype => $content_type }); + my ($invocant, $content_type, undef, $params) = @_; + + my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; + $content_type = 'text/plain' if $is_patch; + $content_type = clean_text($content_type); + +# The subsets below cover all existing MIME types and charsets registered by IANA. +# (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) + my $legal_types = join('|', LEGAL_CONTENT_TYPES); + if (!$content_type + || $content_type + !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) + { + ThrowUserError("invalid_content_type", {contenttype => $content_type}); + } + trick_taint($content_type); + + # $ENV{HOME} must be defined when using File::MimeInfo::Magic, + # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. + local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); + + # If we have autodetected application/octet-stream from the Content-Type + # header, let's have a better go using a sniffer if available. + if ( defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' + && $content_type eq 'application/octet-stream' + && Bugzilla->feature('typesniffer')) + { + import File::MimeInfo::Magic qw(mimetype); + require IO::Scalar; + + # data is either a filehandle, or the data itself. + my $fh = $params->{data}; + if (!ref($fh)) { + $fh = new IO::Scalar \$fh; } - trick_taint($content_type); - - # $ENV{HOME} must be defined when using File::MimeInfo::Magic, - # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. - local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); - - # If we have autodetected application/octet-stream from the Content-Type - # header, let's have a better go using a sniffer if available. - if (defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type eq 'application/octet-stream' - && Bugzilla->feature('typesniffer')) - { - import File::MimeInfo::Magic qw(mimetype); - require IO::Scalar; - - # data is either a filehandle, or the data itself. - my $fh = $params->{data}; - if (!ref($fh)) { - $fh = new IO::Scalar \$fh; - } - elsif (!$fh->isa('IO::Handle')) { - # CGI.pm sends us an Fh that isn't actually an IO::Handle, but - # has a method for getting an actual handle out of it. - $fh = $fh->handle; - # ->handle returns an literal IO::Handle, even though the - # underlying object is a file. So we rebless it to be a proper - # IO::File object so that we can call ->seek on it and so on. - # Just in case CGI.pm fixes this some day, we check ->isa first. - if (!$fh->isa('IO::File')) { - bless $fh, 'IO::File'; - } - } - - my $mimetype = mimetype($fh); - $fh->seek(0, 0); - $content_type = $mimetype if $mimetype; + elsif (!$fh->isa('IO::Handle')) { + + # CGI.pm sends us an Fh that isn't actually an IO::Handle, but + # has a method for getting an actual handle out of it. + $fh = $fh->handle; + + # ->handle returns an literal IO::Handle, even though the + # underlying object is a file. So we rebless it to be a proper + # IO::File object so that we can call ->seek on it and so on. + # Just in case CGI.pm fixes this some day, we check ->isa first. + if (!$fh->isa('IO::File')) { + bless $fh, 'IO::File'; + } } - # Make sure patches are viewable in the browser - if (!ref($invocant) - && defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type =~ m{text/x-(?:diff|patch)}) - { - $params->{ispatch} = 1; - $content_type = 'text/plain'; - } - - return $content_type; + my $mimetype = mimetype($fh); + $fh->seek(0, 0); + $content_type = $mimetype if $mimetype; + } + + # Make sure patches are viewable in the browser + if (!ref($invocant) + && defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' + && $content_type =~ m{text/x-(?:diff|patch)}) + { + $params->{ispatch} = 1; + $content_type = 'text/plain'; + } + + return $content_type; } sub _check_data { - my ($invocant, $params) = @_; + my ($invocant, $params) = @_; - my $data = $params->{data}; - $params->{filesize} = ref $data ? -s $data : length($data); + my $data = $params->{data}; + $params->{filesize} = ref $data ? -s $data : length($data); - Bugzilla::Hook::process('attachment_process_data', { data => \$data, - attributes => $params }); + Bugzilla::Hook::process('attachment_process_data', + {data => \$data, attributes => $params}); - $params->{filesize} || ThrowUserError('zero_length_file'); - # Make sure the attachment does not exceed the maximum permitted size. - my $max_size = max(Bugzilla->params->{'maxlocalattachment'} * 1048576, - Bugzilla->params->{'maxattachmentsize'} * 1024); + $params->{filesize} || ThrowUserError('zero_length_file'); - if ($params->{filesize} > $max_size) { - my $vars = { filesize => sprintf("%.0f", $params->{filesize}/1024) }; - ThrowUserError('file_too_large', $vars); - } - return $data; + # Make sure the attachment does not exceed the maximum permitted size. + my $max_size = max( + Bugzilla->params->{'maxlocalattachment'} * 1048576, + Bugzilla->params->{'maxattachmentsize'} * 1024 + ); + + if ($params->{filesize} > $max_size) { + my $vars = {filesize => sprintf("%.0f", $params->{filesize} / 1024)}; + ThrowUserError('file_too_large', $vars); + } + return $data; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('missing_attachment_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('missing_attachment_description'); + return $description; } sub _check_filename { - my ($invocant, $filename) = @_; - - $filename = clean_text($filename); - if (!$filename) { - if (ref $invocant) { - ThrowUserError('filename_not_specified'); - } - else { - ThrowUserError('file_not_specified'); - } - } + my ($invocant, $filename) = @_; - # 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 MAX_ATTACH_FILENAME_LENGTH characters, counting - # from the end of the string to make sure we keep the filename extension. - $filename = substr($filename, - -&MAX_ATTACH_FILENAME_LENGTH, - MAX_ATTACH_FILENAME_LENGTH); - trick_taint($filename); - - return $filename; + $filename = clean_text($filename); + if (!$filename) { + if (ref $invocant) { + ThrowUserError('filename_not_specified'); + } + else { + ThrowUserError('file_not_specified'); + } + } + + # 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 MAX_ATTACH_FILENAME_LENGTH characters, counting + # from the end of the string to make sure we keep the filename extension. + $filename + = substr($filename, -&MAX_ATTACH_FILENAME_LENGTH, MAX_ATTACH_FILENAME_LENGTH); + trick_taint($filename); + + return $filename; } sub _check_is_private { - my ($invocant, $is_private) = @_; - - $is_private = $is_private ? 1 : 0; - if (((!ref $invocant && $is_private) - || (ref $invocant && $invocant->isprivate != $is_private)) - && !Bugzilla->user->is_insider) { - ThrowUserError('user_not_insider'); - } - return $is_private; + my ($invocant, $is_private) = @_; + + $is_private = $is_private ? 1 : 0; + if ( + ( + (!ref $invocant && $is_private) + || (ref $invocant && $invocant->isprivate != $is_private) + ) + && !Bugzilla->user->is_insider + ) + { + ThrowUserError('user_not_insider'); + } + return $is_private; } =pod @@ -635,69 +648,74 @@ Returns: a reference to an array of attachment objects. =cut sub get_attachments_by_bug { - my ($class, $bug, $vars) = @_; - 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 ($class, $bug, $vars) = @_; + 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->new_from_list($attach_ids); + $_->{bug} = $bug foreach @$attachments; + + # To avoid $attachment->flags and $attachment->flag_types running SQL queries + # themselves for each attachment listed here, we collect all the data at once and + # populate $attachment->{flag_types} ourselves. We also load all attachers and + # datasizes at once for the same reason. + if ($vars->{preload}) { + + # Preload flag types and flags + my $vars = { + target_type => 'attachment', + product_id => $bug->product_id, + component_id => $bug->component_id, + attach_id => $attach_ids + }; + my $flag_types = Bugzilla::Flag->_flag_types($vars); + + foreach my $attachment (@$attachments) { + $attachment->{flag_types} = []; + my $new_types = dclone($flag_types); + foreach my $new_type (@$new_types) { + $new_type->{flags} + = [grep($_->attach_id == $attachment->id, @{$new_type->{flags}})]; + push(@{$attachment->{flag_types}}, $new_type); + } } - my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments - WHERE bug_id = ? $and_restriction", - undef, @values); - - my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); - $_->{bug} = $bug foreach @$attachments; - - # To avoid $attachment->flags and $attachment->flag_types running SQL queries - # themselves for each attachment listed here, we collect all the data at once and - # populate $attachment->{flag_types} ourselves. We also load all attachers and - # datasizes at once for the same reason. - if ($vars->{preload}) { - # Preload flag types and flags - my $vars = { target_type => 'attachment', - product_id => $bug->product_id, - component_id => $bug->component_id, - attach_id => $attach_ids }; - my $flag_types = Bugzilla::Flag->_flag_types($vars); - - foreach my $attachment (@$attachments) { - $attachment->{flag_types} = []; - my $new_types = dclone($flag_types); - foreach my $new_type (@$new_types) { - $new_type->{flags} = [ grep($_->attach_id == $attachment->id, - @{ $new_type->{flags} }) ]; - push(@{ $attachment->{flag_types} }, $new_type); - } - } - - # Preload attachers. - my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; - my $users = Bugzilla::User->new_from_list([keys %user_ids]); - my %user_map = map { $_->id => $_ } @$users; - foreach my $attachment (@$attachments) { - $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; - } - - # Preload datasizes. - my $sizes = - $dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize + # Preload attachers. + my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; + my $users = Bugzilla::User->new_from_list([keys %user_ids]); + my %user_map = map { $_->id => $_ } @$users; + foreach my $attachment (@$attachments) { + $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; + } + + # Preload datasizes. + my $sizes = $dbh->selectall_hashref( + 'SELECT attach_id, LENGTH(thedata) AS datasize FROM attachments LEFT JOIN attach_data ON attach_id = id - WHERE bug_id = ?', - 'attach_id', undef, $bug->id); + WHERE bug_id = ?', 'attach_id', undef, $bug->id + ); - # Force the size of attachments not in the DB to be recalculated. - $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; - } + # Force the size of attachments not in the DB to be recalculated. + $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; + } - return $attachments; + return $attachments; } =pod @@ -716,13 +734,15 @@ Returns: 1 on success, 0 otherwise. =cut sub validate_can_edit { - my $attachment = shift; - my $user = Bugzilla->user; - - # The submitter can edit their attachments. - return ($attachment->attacher->id == $user->id - || ((!$attachment->isprivate || $user->is_insider) - && $user->in_group('editbugs', $attachment->bug->product_id))) ? 1 : 0; + my $attachment = shift; + my $user = Bugzilla->user; + + # The submitter can edit their attachments. + return ( + $attachment->attacher->id == $user->id + || ((!$attachment->isprivate || $user->is_insider) + && $user->in_group('editbugs', $attachment->bug->product_id)) + ) ? 1 : 0; } =item C<validate_obsolete($bug, $attach_ids)> @@ -741,37 +761,36 @@ Returns: The list of attachment objects to mark as obsolete. =cut sub validate_obsolete { - my ($class, $bug, $list) = @_; + my ($class, $bug, $list) = @_; - # 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 (@$list) { - my $vars = {}; - $vars->{'attach_id'} = $attachid; + # 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 (@$list) { + my $vars = {}; + $vars->{'attach_id'} = $attachid; - detaint_natural($attachid) - || ThrowUserError('invalid_attach_id', $vars); + detaint_natural($attachid) || ThrowUserError('invalid_attach_id', $vars); - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment($attachid) - || ThrowUserError('invalid_attach_id', $vars); + # Make sure the attachment exists in the database. + my $attachment = new Bugzilla::Attachment($attachid) + || ThrowUserError('invalid_attach_id', $vars); - # Check that the user can view and edit this attachment. - $attachment->validate_can_edit - || ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id }); + # Check that the user can view and edit this attachment. + $attachment->validate_can_edit + || ThrowUserError('illegal_attachment_edit', {attach_id => $attachment->id}); - if ($attachment->bug_id != $bug->bug_id) { - $vars->{'my_bug_id'} = $bug->bug_id; - ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); - } + if ($attachment->bug_id != $bug->bug_id) { + $vars->{'my_bug_id'} = $bug->bug_id; + ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); + } - next if $attachment->isobsolete; + next if $attachment->isobsolete; - push(@obsolete_attachments, $attachment); - } - return @obsolete_attachments; + push(@obsolete_attachments, $attachment); + } + return @obsolete_attachments; } ############################### @@ -806,112 +825,119 @@ Returns: The new attachment object. =cut sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - - # Extract everything which is not a valid column name. - my $bug = delete $params->{bug}; - $params->{bug_id} = $bug->id; - my $data = delete $params->{data}; - my $size = delete $params->{filesize}; - - my $attachment = $class->insert_create_data($params); - my $attachid = $attachment->id; - - # The file is too large to be stored in the DB, so we store it locally. - if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { - my $attachdir = bz_locations()->{'attachdir'}; - my $hash = ($attachid % 100) + 100; - $hash =~ s/.*(\d\d)$/group.$1/; - mkdir "$attachdir/$hash", 0770; - chmod 0770, "$attachdir/$hash"; - if (ref $data) { - copy($data, "$attachdir/$hash/attachment.$attachid"); - close $data; - } - else { - open(AH, '>', "$attachdir/$hash/attachment.$attachid"); - binmode AH; - print AH $data; - close AH; - } - $data = ''; # Will be stored in the DB. - } - # If we have a filehandle, we need its content to store it in the DB. - elsif (ref $data) { - local $/; - # Store the content in a temp variable while we close the FH. - my $tmp = <$data>; - close $data; - $data = $tmp; - } + my $class = shift; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("INSERT INTO attach_data - (id, thedata) VALUES ($attachid, ?)"); + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); - trick_taint($data); - $sth->bind_param(1, $data, $dbh->BLOB_TYPE); - $sth->execute(); + # Extract everything which is not a valid column name. + my $bug = delete $params->{bug}; + $params->{bug_id} = $bug->id; + my $data = delete $params->{data}; + my $size = delete $params->{filesize}; - $attachment->{bug} = $bug; + my $attachment = $class->insert_create_data($params); + my $attachid = $attachment->id; - # Return the new attachment object. - return $attachment; -} + # The file is too large to be stored in the DB, so we store it locally. + if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { + my $attachdir = bz_locations()->{'attachdir'}; + my $hash = ($attachid % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + mkdir "$attachdir/$hash", 0770; + chmod 0770, "$attachdir/$hash"; + if (ref $data) { + copy($data, "$attachdir/$hash/attachment.$attachid"); + close $data; + } + else { + open(AH, '>', "$attachdir/$hash/attachment.$attachid"); + binmode AH; + print AH $data; + close AH; + } + $data = ''; # Will be stored in the DB. + } -sub run_create_validators { - my ($class, $params) = @_; + # If we have a filehandle, we need its content to store it in the DB. + elsif (ref $data) { + local $/; + + # Store the content in a temp variable while we close the FH. + my $tmp = <$data>; + close $data; + $data = $tmp; + } - $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); + my $sth = $dbh->prepare( + "INSERT INTO attach_data + (id, thedata) VALUES ($attachid, ?)" + ); - # Let's validate the attachment content first as it may - # alter some other attachment attributes. - $params->{data} = $class->_check_data($params); - $params = $class->SUPER::run_create_validators($params); + trick_taint($data); + $sth->bind_param(1, $data, $dbh->BLOB_TYPE); + $sth->execute(); - $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $params->{modification_time} = $params->{creation_ts}; + $attachment->{bug} = $bug; - return $params; + # Return the new attachment object. + return $attachment; } -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +sub run_create_validators { + my ($class, $params) = @_; - my ($changes, $old_self) = $self->SUPER::update(@_); + $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } + # Let's validate the attachment content first as it may + # alter some other attachment attributes. + $params->{data} = $class->_check_data($params); + $params = $class->SUPER::run_create_validators($params); - # Record changes in the activity table. - require Bugzilla::Bug; - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; - $field = "attachments.$field" unless $field eq "flagtypes.name"; - Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], - $change->[1], $user->id, $timestamp, undef, $self->id); - } + $params->{creation_ts} + ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $params->{modification_time} = $params->{creation_ts}; - if (scalar(keys %$changes)) { - $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?', - undef, ($timestamp, $self->id)); - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, ($timestamp, $self->bug_id)); - $self->{modification_time} = $timestamp; - # because we updated the attachments table after SUPER::update(), we - # need to ensure the cache is flushed. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - } + return $params; +} - return $changes; +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my ($changes, $old_self) = $self->SUPER::update(@_); + + my ($removed, $added) + = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); + if ($removed || $added) { + $changes->{'flagtypes.name'} = [$removed, $added]; + } + + # Record changes in the activity table. + require Bugzilla::Bug; + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + $field = "attachments.$field" unless $field eq "flagtypes.name"; + Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], + $change->[1], $user->id, $timestamp, undef, $self->id); + } + + if (scalar(keys %$changes)) { + $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?', + undef, ($timestamp, $self->id)); + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, ($timestamp, $self->bug_id)); + $self->{modification_time} = $timestamp; + + # because we updated the attachments table after SUPER::update(), we + # need to ensure the cache is flushed. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + } + + return $changes; } =pod @@ -929,30 +955,33 @@ Returns: nothing =cut sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my $flag_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM flags WHERE attach_id = ?', undef, $self->id); - $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) - if @$flag_ids; - $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); - $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? - WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id)); - $dbh->bz_commit_transaction(); - - my $filename = $self->_get_local_filename; - if (-e $filename) { - unlink $filename or warn "Couldn't unlink $filename: $!"; - } - - # As we don't call SUPER->remove_from_db we need to manually clear - # memcached here. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - foreach my $flag_id (@$flag_ids) { - Bugzilla->memcached->clear({ table => 'flags', id => $flag_id }); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $flag_ids + = $dbh->selectcol_arrayref('SELECT id FROM flags WHERE attach_id = ?', + undef, $self->id); + $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) + if @$flag_ids; + $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); + $dbh->do( + 'UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? + WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id) + ); + $dbh->bz_commit_transaction(); + + my $filename = $self->_get_local_filename; + if (-e $filename) { + unlink $filename or warn "Couldn't unlink $filename: $!"; + } + + # As we don't call SUPER->remove_from_db we need to manually clear + # memcached here. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + foreach my $flag_id (@$flag_ids) { + Bugzilla->memcached->clear({table => 'flags', id => $flag_id}); + } } ############################### @@ -961,37 +990,39 @@ sub remove_from_db { # Extract the content type from the attachment form. sub get_content_type { - my $cgi = Bugzilla->cgi; + my $cgi = Bugzilla->cgi; - return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); + return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); - my $content_type; - my $method = $cgi->param('contenttypemethod') || ''; + my $content_type; + my $method = $cgi->param('contenttypemethod') || ''; - if ($method eq 'list') { - # The user selected a content type from the list, so use their - # selection. - $content_type = $cgi->param('contenttypeselection'); - } - elsif ($method eq 'manual') { - # The user entered a content type manually, so use their entry. - $content_type = $cgi->param('contenttypeentry'); - } - else { - defined $cgi->upload('data') || ThrowUserError('file_not_specified'); - # The user asked us to auto-detect the content type, so use the type - # specified in the HTTP request headers. - $content_type = - $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; - $content_type || ThrowUserError("missing_content_type"); - - # Internet Explorer sends image/x-png for PNG images, - # so convert that to image/png to match other browsers. - if ($content_type eq 'image/x-png') { - $content_type = 'image/png'; - } + if ($method eq 'list') { + + # The user selected a content type from the list, so use their + # selection. + $content_type = $cgi->param('contenttypeselection'); + } + elsif ($method eq 'manual') { + + # The user entered a content type manually, so use their entry. + $content_type = $cgi->param('contenttypeentry'); + } + else { + defined $cgi->upload('data') || ThrowUserError('file_not_specified'); + + # The user asked us to auto-detect the content type, so use the type + # specified in the HTTP request headers. + $content_type = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; + $content_type || ThrowUserError("missing_content_type"); + + # Internet Explorer sends image/x-png for PNG images, + # so convert that to image/png to match other browsers. + if ($content_type eq 'image/x-png') { + $content_type = 'image/png'; } - return $content_type; + } + return $content_type; } |