1020 lines
28 KiB
Perl
1020 lines
28 KiB
Perl
# -*- 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 - Bugzilla attachment class.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use Bugzilla::Attachment;
|
|
|
|
# Get the attachment with the given ID.
|
|
my $attachment = new Bugzilla::Attachment($attach_id);
|
|
|
|
# Get the attachments with the given IDs.
|
|
my $attachments = Bugzilla::Attachment->new_from_list($attach_ids);
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Attachment.pm represents an attachment object. It is an implementation
|
|
of L<Bugzilla::Object>, and thus provides all methods that
|
|
L<Bugzilla::Object> provides.
|
|
|
|
The methods that are specific to C<Bugzilla::Attachment> are listed
|
|
below.
|
|
|
|
=cut
|
|
|
|
use Bugzilla::Constants;
|
|
use Bugzilla::Error;
|
|
use Bugzilla::Flag;
|
|
use Bugzilla::User;
|
|
use Bugzilla::Util;
|
|
use Bugzilla::Field;
|
|
|
|
use base qw(Bugzilla::Object);
|
|
|
|
###############################
|
|
#### Initialization ####
|
|
###############################
|
|
|
|
use constant DB_TABLE => 'attachments';
|
|
use constant ID_FIELD => 'attach_id';
|
|
use constant LIST_ORDER => ID_FIELD;
|
|
|
|
sub DB_COLUMNS {
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
return qw(
|
|
attach_id
|
|
bug_id
|
|
description
|
|
filename
|
|
isobsolete
|
|
ispatch
|
|
isprivate
|
|
isurl
|
|
mimetype
|
|
modification_time
|
|
submitter_id),
|
|
$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
|
|
}
|
|
|
|
###############################
|
|
#### Accessors ######
|
|
###############################
|
|
|
|
=pod
|
|
|
|
=head2 Instance Properties
|
|
|
|
=over
|
|
|
|
=item C<bug_id>
|
|
|
|
the ID of the bug to which the attachment is attached
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub bug_id {
|
|
my $self = shift;
|
|
return $self->{bug_id};
|
|
}
|
|
|
|
=over
|
|
|
|
=item C<bug>
|
|
|
|
the bug object to which the attachment is attached
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub bug {
|
|
my $self = shift;
|
|
|
|
require Bugzilla::Bug;
|
|
$self->{bug} = Bugzilla::Bug->new($self->bug_id);
|
|
return $self->{bug};
|
|
}
|
|
|
|
=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->{mimetype};
|
|
}
|
|
|
|
=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->{submitter_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->{creation_ts};
|
|
}
|
|
|
|
=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};
|
|
}
|
|
|
|
=over
|
|
|
|
=item C<flag_types>
|
|
|
|
Return all flag types available for this attachment as well as flags
|
|
already set, grouped by flag type.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub 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 };
|
|
|
|
$self->{flag_types} = Bugzilla::Flag::_flag_types($vars);
|
|
return $self->{flag_types};
|
|
}
|
|
|
|
###############################
|
|
#### Validators ######
|
|
###############################
|
|
|
|
# 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')
|
|
|| ($cgi->param('text_attachment') !~ /^\s*$/so)
|
|
|| ($throw_error ? ThrowUserError("file_not_specified") : return 0);
|
|
|
|
my $filename = $cgi->upload('data') || $cgi->param('filename');
|
|
$filename = $cgi->param('description')
|
|
if !$filename && $cgi->param('text_attachment') !~ /^\s*$/so;
|
|
|
|
# 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 $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'))
|
|
|| ($cgi->param('text_attachment') !~ /^\s*$/so)
|
|
|| ($throw_error ? ThrowUserError("zero_length_file") : return 0);
|
|
|
|
if (!$data && $cgi->param('text_attachment') !~ /^\s*$/so)
|
|
{
|
|
$data = $cgi->param('text_attachment');
|
|
}
|
|
|
|
# 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 $maxsize = Bugzilla->params->{'maxattachmentsize'} * 1024; # Convert from K
|
|
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, $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);
|
|
|
|
# To avoid $attachment->flags to run SQL queries itself for each
|
|
# attachment listed here, we collect all the data at once and
|
|
# populate $attachment->{flags} ourselves.
|
|
if ($vars->{preload}) {
|
|
$_->{flags} = [] foreach @$attachments;
|
|
my %att = map { $_->id => $_ } @$attachments;
|
|
|
|
my $flags = Bugzilla::Flag->match({ bug_id => $bug_id,
|
|
target_type => 'attachment' });
|
|
|
|
# Exclude flags for private attachments you cannot see.
|
|
@$flags = grep {exists $att{$_->attach_id}} @$flags;
|
|
|
|
push(@{$att{$_->attach_id}->{flags}}, $_) foreach @$flags;
|
|
$attachments = [sort {$a->id <=> $b->id} values %att];
|
|
}
|
|
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;
|
|
if ($cgi->param('data'))
|
|
{
|
|
$contenttype = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
|
|
}
|
|
else
|
|
{
|
|
$contenttype = 'text/plain';
|
|
}
|
|
# 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);
|
|
|
|
# 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($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;
|
|
}
|
|
|
|
###############################
|
|
#### Constructors #####
|
|
###############################
|
|
|
|
=pod
|
|
|
|
=item C<create($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
|
|
|
|
# FIXME: needs to follow the way Object->create() works.
|
|
sub create {
|
|
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->{force_attach_bigfile})
|
|
{
|
|
# Force uploading into files instead of DB
|
|
$cgi->param('bigfile', 1);
|
|
}
|
|
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, ?)");
|
|
if (!$cgi->param('bigfile') && $data)
|
|
{
|
|
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 $hash = ($attachid % 100) + 100;
|
|
$hash =~ s/.*(\d\d)$/group.$1/;
|
|
mkdir "$attachdir/$hash", 0770;
|
|
chmod 0770, "$attachdir/$hash";
|
|
open AH, ">$attachdir/$hash/attachment.$attachid" or die "Could not write into $attachdir/$hash/attachment.$attachid: $!";
|
|
binmode AH;
|
|
if (my $fh = $cgi->upload('data'))
|
|
{
|
|
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 $fh;
|
|
}
|
|
elsif ($data)
|
|
{
|
|
print AH $data;
|
|
}
|
|
close AH;
|
|
}
|
|
|
|
# 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 = new Bugzilla::Attachment($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;
|