# 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. package Bugzilla::Comment; use 5.10.1; use strict; 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 methods. See L for more details. =head2 Accessors =over =item C C The ID of the bug to which the comment belongs. =item C C The comment creation timestamp. =item C C The body without any special additional text. =item C C Time spent as related to this comment. =item C C Comment is marked as private =item C If this comment is stored in the database word-wrapped, this will be C<1>. C<0> otherwise. =item C L who created the comment. =item C C The position this comment is located in the full list of comments for a bug starting from 0. =item C =over =item B C Body of the comment, including any special text (such as "this bug was marked as a duplicate of..."). =item B =over =item C C. C<1> if this comment should be formatted specifically for bugmail. =item C C. C<1> if the comment should be returned word-wrapped. =back =item B A string, the full text of the comment as it would be displayed to an end-user. =back =back =cut