Commit a3cbf561 authored by myk%mozilla.org's avatar myk%mozilla.org

Partial fix for bug 302669: rewrites Attachment.pm to provide a real Attachment class; r=lpsolit

parent a094f0eb
...@@ -20,96 +20,384 @@ ...@@ -20,96 +20,384 @@
# Contributor(s): Terry Weissman <terry@mozilla.org> # Contributor(s): Terry Weissman <terry@mozilla.org>
# Myk Melez <myk@mozilla.org> # Myk Melez <myk@mozilla.org>
############################################################################
# Module Initialization
############################################################################
use strict; use strict;
package Bugzilla::Attachment; package Bugzilla::Attachment;
# This module requires that its caller have said "require globals.pl" to import =head1 NAME
# relevant functions from that script.
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
# This module requires that its caller have said "require globals.pl"
# to import relevant functions from that script.
# Use the Flag module to handle flags.
use Bugzilla::Flag; use Bugzilla::Flag;
use Bugzilla::Config qw(:locations); use Bugzilla::Config qw(:locations);
use Bugzilla::User; use Bugzilla::User;
############################################################################ sub get {
# Functions my $invocant = shift;
############################################################################ my $id = shift;
sub new {
# Returns a hash of information about the attachment with the given ID.
my ($invocant, $id) = @_; my $attachments = _retrieve([$id]);
return undef if !$id; my $self = $attachments->[0];
my $self = { 'id' => $id }; bless($self, ref($invocant) || $invocant) if $self;
my $class = ref($invocant) || $invocant;
bless($self, $class);
&::PushGlobalSQLState();
&::SendSQL("SELECT 1, description, bug_id, isprivate FROM attachments " .
"WHERE attach_id = $id");
($self->{'exists'},
$self->{'summary'},
$self->{'bug_id'},
$self->{'isprivate'}) = &::FetchSQLData();
&::PopGlobalSQLState();
return $self; return $self;
} }
sub query sub get_list {
{ my $invocant = shift;
# Retrieves and returns an array of attachment records for a given bug. my $ids = shift;
# This data should be given to attachment/list.html.tmpl in an
# "attachments" variable. my $attachments = _retrieve($ids);
my ($bugid) = @_; foreach my $attachment (@$attachments) {
bless($attachment, ref($invocant) || $invocant);
my $dbh = Bugzilla->dbh; }
# Retrieve a list of attachments for this bug and write them into an array return $attachments;
# of hashes in which each hash represents a single attachment. }
my $list = $dbh->selectall_arrayref("SELECT attach_id, " .
$dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . sub _retrieve {
", mimetype, description, ispatch, my ($ids) = @_;
isobsolete, isprivate, LENGTH(thedata),
submitter_id return [] if scalar(@$ids) == 0;
FROM attachments
INNER JOIN attach_data my @columns = (
ON id = attach_id 'attachments.attach_id AS id',
WHERE bug_id = ? ORDER BY attach_id", 'attachments.bug_id AS bug_id',
undef, $bugid); 'attachments.description AS description',
'attachments.mimetype AS contenttype',
my @attachments = (); 'attachments.submitter_id AS _attacher_id',
foreach my $row (@$list) { Bugzilla->dbh->sql_date_format('attachments.creation_ts',
my %a; '%Y.%m.%d %H:%i') . " AS attached",
($a{'attachid'}, $a{'date'}, $a{'contenttype'}, 'attachments.filename AS filename',
$a{'description'}, $a{'ispatch'}, $a{'isobsolete'}, 'attachments.ispatch AS ispatch',
$a{'isprivate'}, $a{'datasize'}, $a{'submitter_id'}) = @$row; 'attachments.isobsolete AS isobsolete',
'attachments.isprivate AS isprivate'
$a{'submitter'} = new Bugzilla::User($a{'submitter_id'}); );
my $columns = join(", ", @columns);
# Retrieve a list of flags for this attachment.
$a{'flags'} = Bugzilla::Flag::match({ 'attach_id' => $a{'attachid'}, my $records = Bugzilla->dbh->selectall_arrayref("SELECT $columns
'is_active' => 1 }); FROM attachments
WHERE attach_id IN (" .
# A zero size indicates that the attachment is stored locally. join(",", @$ids) . ")",
if ($a{'datasize'} == 0) { { Slice => {} });
my $attachid = $a{'attachid'}; return $records;
my $hash = ($attachid % 100) + 100; }
$hash =~ s/.*(\d\d)$/group.$1/;
if (open(AH, "$attachdir/$hash/attachment.$attachid")) { =pod
$a{'datasize'} = (stat(AH))[7];
=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<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<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<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())) {
binmode AH;
$self->{data} = <AH>;
close(AH); close(AH);
} }
} }
push @attachments, \%a;
} 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});
# If there's no attachment data in the database, the attachment
# is stored in a local file, so retrieve its size from the file.
if ($self->{datasize} == 0) {
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,
is_active => 1 });
return $self->{flags};
}
# Instance methods; no POD documentation here yet because the only one so far
# is private.
sub _get_local_filename {
my $self = shift;
my $hash = ($self->id % 100) + 100;
$hash =~ s/.*(\d\d)$/group.$1/;
return "$attachdir/$hash/attachment.$self->id";
}
=pod
=head2 Class Methods
=over
=item C<get_attachments_by_bug($bug_id)>
Description: retrieves and returns the attachments 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.
=back
=cut
return \@attachments; sub get_attachments_by_bug {
my ($class, $bug_id) = @_;
my $attach_ids = Bugzilla->dbh->selectcol_arrayref("SELECT attach_id
FROM attachments
WHERE bug_id = ?
ORDER BY attach_id",
undef, $bug_id);
my $attachments = Bugzilla::Attachment->get_list($attach_ids);
return $attachments;
} }
1; 1;
...@@ -354,7 +354,9 @@ sub attachments { ...@@ -354,7 +354,9 @@ sub attachments {
my ($self) = @_; my ($self) = @_;
return $self->{'attachments'} if exists $self->{'attachments'}; return $self->{'attachments'} if exists $self->{'attachments'};
return [] if $self->{'error'}; return [] if $self->{'error'};
$self->{'attachments'} = Bugzilla::Attachment::query($self->{bug_id});
$self->{'attachments'} =
Bugzilla::Attachment->get_attachments_by_bug($self->bug_id);
return $self->{'attachments'}; return $self->{'attachments'};
} }
......
...@@ -347,26 +347,26 @@ sub validate { ...@@ -347,26 +347,26 @@ sub validate {
# can_see_bug() will refer to old settings. # can_see_bug() will refer to old settings.
if (!$requestee->can_see_bug($bug_id)) { if (!$requestee->can_see_bug($bug_id)) {
ThrowUserError("flag_requestee_unauthorized", ThrowUserError("flag_requestee_unauthorized",
{ flag_type => $flag->{'type'}, { flag_type => $flag->{'type'},
requestee => $requestee, requestee => $requestee,
bug_id => $bug_id, bug_id => $bug_id,
attach_id => attachment => $flag->{target}->{attachment}
$flag->{target}->{attachment}->{id} }); });
} }
# Throw an error if the target is a private attachment and # Throw an error if the target is a private attachment and
# the requestee isn't in the group of insiders who can see it. # the requestee isn't in the group of insiders who can see it.
if ($flag->{target}->{attachment}->{exists} if ($flag->{target}->{attachment}
&& $cgi->param('isprivate') && $cgi->param('isprivate')
&& Param("insidergroup") && Param("insidergroup")
&& !$requestee->in_group(Param("insidergroup"))) && !$requestee->in_group(Param("insidergroup")))
{ {
ThrowUserError("flag_requestee_unauthorized_attachment", ThrowUserError("flag_requestee_unauthorized_attachment",
{ flag_type => $flag->{'type'}, { flag_type => $flag->{'type'},
requestee => $requestee, requestee => $requestee,
bug_id => $bug_id, bug_id => $bug_id,
attach_id => attachment => $flag->{target}->{attachment}
$flag->{target}->{attachment}->{id} }); });
} }
} }
} }
...@@ -532,7 +532,9 @@ sub create { ...@@ -532,7 +532,9 @@ sub create {
$flag->{'id'} = (&::FetchOneColumn() || 0) + 1; $flag->{'id'} = (&::FetchOneColumn() || 0) + 1;
# Insert a record for the flag into the flags table. # Insert a record for the flag into the flags table.
my $attach_id = $flag->{'target'}->{'attachment'}->{'id'} || "NULL"; my $attach_id =
$flag->{target}->{attachment} ? $flag->{target}->{attachment}->{id}
: "NULL";
my $requestee_id = $flag->{'requestee'} ? $flag->{'requestee'}->id : "NULL"; my $requestee_id = $flag->{'requestee'} ? $flag->{'requestee'}->id : "NULL";
&::SendSQL("INSERT INTO flags (id, type_id, &::SendSQL("INSERT INTO flags (id, type_id,
bug_id, attach_id, bug_id, attach_id,
...@@ -807,7 +809,8 @@ sub FormToNewFlags { ...@@ -807,7 +809,8 @@ sub FormToNewFlags {
{ 'type_id' => $type_id, { 'type_id' => $type_id,
'target_type' => $target->{'type'}, 'target_type' => $target->{'type'},
'bug_id' => $target->{'bug'}->{'id'}, 'bug_id' => $target->{'bug'}->{'id'},
'attach_id' => $target->{'attachment'}->{'id'}, 'attach_id' => $target->{'attachment'} ?
$target->{'attachment'}->{'id'} : undef,
'is_active' => 1 }); 'is_active' => 1 });
# Do not create a new flag of this type if this flag type is # Do not create a new flag of this type if this flag type is
...@@ -902,15 +905,18 @@ sub get_target { ...@@ -902,15 +905,18 @@ sub get_target {
my $target = { 'exists' => 0 }; my $target = { 'exists' => 0 };
if ($attach_id) { if ($attach_id) {
$target->{'attachment'} = new Bugzilla::Attachment($attach_id); $target->{'attachment'} = Bugzilla::Attachment->get($attach_id);
if ($bug_id) { if ($bug_id) {
# Make sure the bug and attachment IDs correspond to each other # Make sure the bug and attachment IDs correspond to each other
# (i.e. this is the bug to which this attachment is attached). # (i.e. this is the bug to which this attachment is attached).
$bug_id == $target->{'attachment'}->{'bug_id'} if (!$target->{'attachment'}
|| return { 'exists' => 0 }; || $target->{'attachment'}->{'bug_id'} != $bug_id)
{
return { 'exists' => 0 };
}
} }
$target->{'bug'} = GetBug($target->{'attachment'}->{'bug_id'}); $target->{'bug'} = GetBug($bug_id);
$target->{'exists'} = $target->{'attachment'}->{'exists'}; $target->{'exists'} = 1;
$target->{'type'} = "attachment"; $target->{'type'} = "attachment";
} }
elsif ($bug_id) { elsif ($bug_id) {
...@@ -937,20 +943,21 @@ Sends an email notification about a flag being created or fulfilled. ...@@ -937,20 +943,21 @@ Sends an email notification about a flag being created or fulfilled.
sub notify { sub notify {
my ($flag, $template_file) = @_; my ($flag, $template_file) = @_;
my $attachment_is_private = $flag->{'target'}->{'attachment'} ?
$flag->{'target'}->{'attachment'}->{'isprivate'} : undef;
# If the target bug is restricted to one or more groups, then we need # If the target bug is restricted to one or more groups, then we need
# to make sure we don't send email about it to unauthorized users # to make sure we don't send email about it to unauthorized users
# on the request type's CC: list, so we have to trawl the list for users # on the request type's CC: list, so we have to trawl the list for users
# not in those groups or email addresses that don't have an account. # not in those groups or email addresses that don't have an account.
if ($flag->{'target'}->{'bug'}->{'restricted'} if ($flag->{'target'}->{'bug'}->{'restricted'} || $attachment_is_private) {
|| $flag->{'target'}->{'attachment'}->{'isprivate'})
{
my @new_cc_list; my @new_cc_list;
foreach my $cc (split(/[, ]+/, $flag->{'type'}->{'cc_list'})) { foreach my $cc (split(/[, ]+/, $flag->{'type'}->{'cc_list'})) {
my $ccuser = Bugzilla::User->new_from_login($cc) || next; my $ccuser = Bugzilla::User->new_from_login($cc) || next;
next if $flag->{'target'}->{'bug'}->{'restricted'} next if $flag->{'target'}->{'bug'}->{'restricted'}
&& !$ccuser->can_see_bug($flag->{'target'}->{'bug'}->{'id'}); && !$ccuser->can_see_bug($flag->{'target'}->{'bug'}->{'id'});
next if $flag->{'target'}->{'attachment'}->{'isprivate'} next if $attachment_is_private
&& Param("insidergroup") && Param("insidergroup")
&& !$ccuser->in_group(Param("insidergroup")); && !$ccuser->in_group(Param("insidergroup"));
push(@new_cc_list, $cc); push(@new_cc_list, $cc);
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
[% IF !attachment.isprivate || canseeprivate %] [% IF !attachment.isprivate || canseeprivate %]
<tr [% "class=\"bz_private\"" IF attachment.isprivate %]> <tr [% "class=\"bz_private\"" IF attachment.isprivate %]>
<td valign="top"> <td valign="top">
<a href="attachment.cgi?id=[% attachment.attachid %]">[% attachment.description FILTER html FILTER obsolete(attachment.isobsolete) %]</a> <a href="attachment.cgi?id=[% attachment.id %]">[% attachment.description FILTER html FILTER obsolete(attachment.isobsolete) %]</a>
</td> </td>
<td valign="top"> <td valign="top">
...@@ -49,11 +49,11 @@ ...@@ -49,11 +49,11 @@
</td> </td>
<td valign="top"> <td valign="top">
<a href="mailto:[% attachment.submitter.email FILTER html %]"> <a href="mailto:[% attachment.attacher.email FILTER html %]">
[% attachment.submitter.name || attachment.submitter.login FILTER html %] [% attachment.attacher.name || attachment.attacher.login FILTER html %]
</a> </a>
</td> </td>
<td valign="top">[% attachment.date FILTER time %]</td> <td valign="top">[% attachment.attached FILTER time %]</td>
<td valign="top">[% attachment.datasize FILTER unitconvert %]</td> <td valign="top">[% attachment.datasize FILTER unitconvert %]</td>
[% IF show_attachment_flags %] [% IF show_attachment_flags %]
...@@ -75,9 +75,9 @@ ...@@ -75,9 +75,9 @@
[% END %] [% END %]
<td valign="top"> <td valign="top">
<a href="attachment.cgi?id=[% attachment.attachid %]&amp;action=edit">Edit</a> <a href="attachment.cgi?id=[% attachment.id %]&amp;action=edit">Edit</a>
[% IF attachment.ispatch && patchviewerinstalled %] [% IF attachment.ispatch && patchviewerinstalled %]
| <a href="attachment.cgi?id=[% attachment.attachid %]&amp;action=diff">Diff</a> | <a href="attachment.cgi?id=[% attachment.id %]&amp;action=diff">Diff</a>
[% END %] [% END %]
[% Hook.process("action") %] [% Hook.process("action") %]
</td> </td>
......
...@@ -73,8 +73,8 @@ ...@@ -73,8 +73,8 @@
ispatch="1" ispatch="1"
[% END %] [% END %]
> >
<attachid>[% a.attachid %]</attachid> <attachid>[% a.id %]</attachid>
<date>[% a.date FILTER time FILTER xml %]</date> <date>[% a.attached FILTER time FILTER xml %]</date>
<desc>[% a.description FILTER xml %]</desc> <desc>[% a.description FILTER xml %]</desc>
<ctype>[% a.contenttype FILTER xml %]</ctype> <ctype>[% a.contenttype FILTER xml %]</ctype>
[% FOREACH flag = a.flags %] [% FOREACH flag = a.flags %]
......
...@@ -368,7 +368,7 @@ ...@@ -368,7 +368,7 @@
'bug/show.xml.tmpl' => [ 'bug/show.xml.tmpl' => [
'VERSION', 'VERSION',
'a.attachid', 'a.id',
'field', 'field',
], ],
...@@ -458,7 +458,7 @@ ...@@ -458,7 +458,7 @@
], ],
'attachment/list.html.tmpl' => [ 'attachment/list.html.tmpl' => [
'attachment.attachid', 'attachment.id',
'flag.status', 'flag.status',
'bugid', 'bugid',
], ],
......
...@@ -442,7 +442,7 @@ ...@@ -442,7 +442,7 @@
You asked [% requestee.identity FILTER html %] You asked [% requestee.identity FILTER html %]
for <code>[% flag_type.name FILTER html %]</code> on [% terms.bug %] for <code>[% flag_type.name FILTER html %]</code> on [% terms.bug %]
[% bug_id FILTER html -%] [% bug_id FILTER html -%]
[% IF attach_id %], attachment [% attach_id FILTER html %][% END %], [% IF attachment %], attachment [% attachment.id FILTER html %][% END %],
but that [% terms.bug %] has been restricted to users in certain groups, but that [% terms.bug %] has been restricted to users in certain groups,
and the user you asked isn't in all the groups to which and the user you asked isn't in all the groups to which
the [% terms.bug %] has been restricted. the [% terms.bug %] has been restricted.
...@@ -455,11 +455,10 @@ ...@@ -455,11 +455,10 @@
You asked [% requestee.identity FILTER html %] You asked [% requestee.identity FILTER html %]
for <code>[% flag_type.name FILTER html %]</code> on for <code>[% flag_type.name FILTER html %]</code> on
[%+ terms.bug %] [%+ bug_id FILTER html %], [%+ terms.bug %] [%+ bug_id FILTER html %],
attachment [% attach_id FILTER html %], but that attachment is restricted attachment [% attachment.id FILTER html %], but that attachment
to users is restricted to users in the [% Param("insidergroup") FILTER html %] group,
in the [% Param("insidergroup") FILTER html %] group, and the user and the user you asked isn't in that group. Please choose someone else
you asked isn't in that group. Please choose someone else to ask, to ask, or ask an administrator to add the user to the group.
or ask an administrator to add the user to the group.
[% ELSIF error == "flag_type_cc_list_invalid" %] [% ELSIF error == "flag_type_cc_list_invalid" %]
[% title = "Flag Type CC List Invalid" %] [% title = "Flag Type CC List Invalid" %]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment