blob: ee342fb2d29d9e29bcf4403d2f3d32f0c21eb6dd [file] [log] [blame]
# -*- 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 James Robson.
# Portions created by James Robson are Copyright (c) 2009 James Robson.
# All rights reserved.
#
# Contributor(s): James Robson <arbingersys@gmail.com>
# Christian Legnitto <clegnitto@mozilla.com>
use strict;
package Bugzilla::Comment;
use base qw(Bugzilla::Object);
use Bugzilla::Attachment;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
use Scalar::Util qw(blessed);
###############################
#### Initialization ####
###############################
# Creation and updating of comments are audited in longdescs
# and bugs_activity respectively instead of audit_log.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant DB_COLUMNS => qw(
comment_id
bug_id
who
bug_when
work_time
thetext
isprivate
already_wrapped
type
extra_data
);
use constant UPDATE_COLUMNS => qw(
isprivate
type
extra_data
);
use constant DB_TABLE => 'longdescs';
use constant ID_FIELD => 'comment_id';
# In some rare cases, two comments can have identical timestamps. If
# this happens, we want to be sure that the comment added later shows up
# later in the sequence.
use constant LIST_ORDER => 'bug_when, comment_id';
use constant VALIDATORS => {
bug_id => \&_check_bug_id,
who => \&_check_who,
bug_when => \&_check_bug_when,
work_time => \&_check_work_time,
thetext => \&_check_thetext,
isprivate => \&_check_isprivate,
extra_data => \&_check_extra_data,
type => \&_check_type,
};
use constant VALIDATOR_DEPENDENCIES => {
extra_data => ['type'],
bug_id => ['who'],
work_time => ['who', 'bug_id'],
isprivate => ['who'],
};
#########################
# Database Manipulation #
#########################
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
$self->bug->_sync_fulltext();
return $changes;
}
# Speeds up displays of comment lists by loading all ->author objects
# at once for a whole list.
sub preload {
my ($class, $comments) = @_;
my %user_ids = map { $_->{who} => 1 } @$comments;
my $users = Bugzilla::User->new_from_list([keys %user_ids]);
my %user_map = map { $_->id => $_ } @$users;
foreach my $comment (@$comments) {
$comment->{author} = $user_map{$comment->{who}};
}
}
###############################
#### Accessors ######
###############################
sub already_wrapped { return $_[0]->{'already_wrapped'}; }
sub body { return $_[0]->{'thetext'}; }
sub bug_id { return $_[0]->{'bug_id'}; }
sub creation_ts { return $_[0]->{'bug_when'}; }
sub is_private { return $_[0]->{'isprivate'}; }
sub work_time {
# Work time is returned as a string (see bug 607909)
return 0 if $_[0]->{'work_time'} + 0 == 0;
return $_[0]->{'work_time'};
}
sub type { return $_[0]->{'type'}; }
sub extra_data { return $_[0]->{'extra_data'} }
sub bug {
my $self = shift;
require Bugzilla::Bug;
$self->{bug} ||= new Bugzilla::Bug($self->bug_id);
return $self->{bug};
}
sub is_about_attachment {
my ($self) = @_;
return 1 if ($self->type == CMT_ATTACHMENT_CREATED
or $self->type == CMT_ATTACHMENT_UPDATED);
return 0;
}
sub attachment {
my ($self) = @_;
return undef if not $self->is_about_attachment;
$self->{attachment} ||= new Bugzilla::Attachment($self->extra_data);
return $self->{attachment};
}
sub author {
my $self = shift;
$self->{'author'} ||= new Bugzilla::User($self->{'who'});
return $self->{'author'};
}
sub body_full {
my ($self, $params) = @_;
$params ||= {};
my $template = Bugzilla->template_inner;
my $body;
if ($self->type) {
$template->process("bug/format_comment.txt.tmpl",
{ comment => $self, %$params }, \$body)
|| ThrowTemplateError($template->error());
$body =~ s/^X//;
}
else {
$body = $self->body;
}
if ($params->{wrap} and !$self->already_wrapped) {
$body = wrap_comment($body);
}
return $body;
}
############
# Mutators #
############
sub set_is_private { $_[0]->set('isprivate', $_[1]); }
sub set_type { $_[0]->set('type', $_[1]); }
sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
##############
# Validators #
##############
sub run_create_validators {
my $self = shift;
my $params = $self->SUPER::run_create_validators(@_);
# Sometimes this run_create_validators is called with parameters that
# skip bug_id validation, so it might not exist in the resulting hash.
if (defined $params->{bug_id}) {
$params->{bug_id} = $params->{bug_id}->id;
}
return $params;
}
sub _check_extra_data {
my ($invocant, $extra_data, undef, $params) = @_;
my $type = blessed($invocant) ? $invocant->type : $params->{type};
if ($type == CMT_NORMAL) {
if (defined $extra_data) {
ThrowCodeError('comment_extra_data_not_allowed',
{ type => $type, extra_data => $extra_data });
}
}
else {
if (!defined $extra_data) {
ThrowCodeError('comment_extra_data_required', { type => $type });
}
elsif ($type == CMT_ATTACHMENT_CREATED
or $type == CMT_ATTACHMENT_UPDATED)
{
my $attachment = Bugzilla::Attachment->check({
id => $extra_data });
$extra_data = $attachment->id;
}
else {
my $original = $extra_data;
detaint_natural($extra_data)
or ThrowCodeError('comment_extra_data_not_numeric',
{ type => $type, extra_data => $original });
}
}
return $extra_data;
}
sub _check_type {
my ($invocant, $type) = @_;
$type ||= CMT_NORMAL;
my $original = $type;
detaint_natural($type)
or ThrowCodeError('comment_type_invalid', { type => $original });
return $type;
}
sub _check_bug_id {
my ($invocant, $bug_id) = @_;
ThrowCodeError('param_required', {function => 'Bugzilla::Comment->create',
param => 'bug_id'}) unless $bug_id;
my $bug;
if (blessed $bug_id) {
# We got a bug object passed in, use it
$bug = $bug_id;
$bug->check_is_visible;
}
else {
# We got a bug id passed in, check it and get the bug object
$bug = Bugzilla::Bug->check({ id => $bug_id });
}
# Make sure the user can edit the product
Bugzilla->user->can_edit_product($bug->{product_id});
# Make sure the user can comment
my $privs;
$bug->check_can_change_field('longdesc', 0, 1, \$privs)
|| ThrowUserError('illegal_change',
{ field => 'longdesc', privs => $privs });
return $bug;
}
sub _check_who {
my ($invocant, $who) = @_;
Bugzilla->login(LOGIN_REQUIRED);
return Bugzilla->user->id;
}
sub _check_bug_when {
my ($invocant, $when) = @_;
# Make sure the timestamp is defined, default to a timestamp from the db
if (!defined $when) {
$when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
}
# Make sure the timestamp parses
if (!datetime_from($when)) {
ThrowCodeError('invalid_timestamp', { timestamp => $when });
}
return $when;
}
sub _check_work_time {
my ($invocant, $value_in, $field, $params) = @_;
# Call down to Bugzilla::Object, letting it know negative
# values are ok
my $time = $invocant->check_time($value_in, $field, $params, 1);
my $privs;
$params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs)
|| ThrowUserError('illegal_change',
{ field => 'work_time', privs => $privs });
return $time;
}
sub _check_thetext {
my ($invocant, $thetext) = @_;
ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create',
param => 'thetext'}) unless defined $thetext;
# Remove any trailing whitespace. Leading whitespace could be
# a valid part of the comment.
$thetext =~ s/\s*$//s;
$thetext =~ s/\r\n?/\n/g; # Get rid of \r.
ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
return $thetext;
}
sub _check_isprivate {
my ($invocant, $isprivate) = @_;
if ($isprivate && !Bugzilla->user->is_insider) {
ThrowUserError('user_not_insider');
}
return $isprivate ? 1 : 0;
}
sub count {
my ($self) = @_;
return $self->{'count'} if defined $self->{'count'};
my $dbh = Bugzilla->dbh;
($self->{'count'}) = $dbh->selectrow_array(
"SELECT COUNT(*)
FROM longdescs
WHERE bug_id = ?
AND bug_when <= ?",
undef, $self->bug_id, $self->creation_ts);
return --$self->{'count'};
}
1;
__END__
=head1 NAME
Bugzilla::Comment - A Comment for a given bug
=head1 SYNOPSIS
use Bugzilla::Comment;
my $comment = Bugzilla::Comment->new($comment_id);
my $comments = Bugzilla::Comment->new_from_list($comment_ids);
=head1 DESCRIPTION
Bugzilla::Comment represents a comment attached to a bug.
This implements all standard C<Bugzilla::Object> methods. See
L<Bugzilla::Object> for more details.
=head2 Accessors
=over
=item C<bug_id>
C<int> The ID of the bug to which the comment belongs.
=item C<creation_ts>
C<string> The comment creation timestamp.
=item C<body>
C<string> The body without any special additional text.
=item C<work_time>
C<string> Time spent as related to this comment.
=item C<is_private>
C<boolean> Comment is marked as private
=item C<already_wrapped>
If this comment is stored in the database word-wrapped, this will be C<1>.
C<0> otherwise.
=item C<author>
L<Bugzilla::User> who created the comment.
=item C<count>
C<int> The position this comment is located in the full list of comments for a bug starting from 0.
=item C<body_full>
=over
=item B<Description>
C<string> Body of the comment, including any special text (such as
"this bug was marked as a duplicate of...").
=item B<Params>
=over
=item C<is_bugmail>
C<boolean>. C<1> if this comment should be formatted specifically for
bugmail.
=item C<wrap>
C<boolean>. C<1> if the comment should be returned word-wrapped.
=back
=item B<Returns>
A string, the full text of the comment as it would be displayed to an end-user.
=back
=back
=cut