diff options
Diffstat (limited to 'Bugzilla/Comment.pm')
-rw-r--r-- | Bugzilla/Comment.pm | 419 |
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 |