aboutsummaryrefslogtreecommitdiff
path: root/Bugzilla/Comment.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/Comment.pm')
-rw-r--r--Bugzilla/Comment.pm419
1 files changed, 419 insertions, 0 deletions
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
new file mode 100644
index 0000000..30ec1cb
--- /dev/null
+++ b/Bugzilla/Comment.pm
@@ -0,0 +1,419 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+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( update_comments => 1);
+ 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