summaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla')
-rw-r--r--Bugzilla/Bug.pm33
-rw-r--r--Bugzilla/Comment.pm195
-rw-r--r--Bugzilla/Comment/TagWeights.pm74
-rw-r--r--Bugzilla/Config/BugFields.pm8
-rw-r--r--Bugzilla/Config/Common.pm14
-rw-r--r--Bugzilla/Config/GroupSecurity.pm11
-rw-r--r--Bugzilla/Constants.pm7
-rw-r--r--Bugzilla/DB/Schema.pm48
-rw-r--r--Bugzilla/Field.pm1
-rw-r--r--Bugzilla/User.pm17
-rw-r--r--Bugzilla/WebService/Bug.pm211
-rw-r--r--Bugzilla/WebService/Constants.pm7
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bug.pm16
13 files changed, 626 insertions, 16 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 1e82da30d..9669352cd 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -3822,7 +3822,7 @@ sub _bugs_in_order {
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes Bugzilla::Bug->check has been previously called.
sub get_activity {
- my ($self, $attach_id, $starttime) = @_;
+ my ($self, $attach_id, $starttime, $include_comment_tags) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
@@ -3834,7 +3834,7 @@ sub get_activity {
if (defined $starttime) {
trick_taint($starttime);
push (@args, $starttime);
- $datepart = "AND bugs_activity.bug_when > ?";
+ $datepart = "AND bug_when > ?";
}
my $attachpart = "";
@@ -3854,7 +3854,7 @@ sub get_activity {
my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " .
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
- ", bugs_activity.removed, bugs_activity.added, profiles.login_name,
+ " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name,
bugs_activity.comment_id
FROM bugs_activity
$suppjoins
@@ -3865,8 +3865,31 @@ sub get_activity {
WHERE bugs_activity.bug_id = ?
$datepart
$attachpart
- $suppwhere
- ORDER BY bugs_activity.bug_when, bugs_activity.id";
+ $suppwhere ";
+
+ if (Bugzilla->params->{'comment_taggers_group'}
+ && $include_comment_tags
+ && !$attach_id)
+ {
+ $query .= "
+ UNION ALL
+ SELECT 'comment_tag' AS name,
+ NULL AS attach_id," .
+ $dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when,
+ longdescs_tags_activity.removed,
+ longdescs_tags_activity.added,
+ profiles.login_name,
+ longdescs_tags_activity.comment_id as comment_id
+ FROM longdescs_tags_activity
+ INNER JOIN profiles ON profiles.userid = longdescs_tags_activity.who
+ WHERE longdescs_tags_activity.bug_id = ?
+ $datepart
+ ";
+ push @args, $self->id;
+ push @args, $starttime if defined $starttime;
+ }
+
+ $query .= "ORDER BY bug_when, comment_id";
my $list = $dbh->selectall_arrayref($query, undef, @args);
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index d4e3383ea..d1e1e2530 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -13,11 +13,13 @@ use strict;
use parent qw(Bugzilla::Object);
use Bugzilla::Attachment;
+use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
+use List::Util qw(first);
use Scalar::Util qw(blessed);
###############################
@@ -79,21 +81,90 @@ use constant VALIDATOR_DEPENDENCIES => {
sub update {
my $self = shift;
- my $changes = $self->SUPER::update(@_);
- $self->bug->_sync_fulltext( update_comments => 1);
+ my ($changes, $old_comment) = $self->SUPER::update(@_);
+
+ if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) {
+ $self->bug->_sync_fulltext( update_comments => 1);
+ }
+
+ my @old_tags = @{ $old_comment->tags };
+ my @new_tags = @{ $self->tags };
+ my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags);
+
+ if (@$removed_tags || @$added_tags) {
+ my $dbh = Bugzilla->dbh;
+ my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+ my $sth_delete = $dbh->prepare(
+ "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"
+ );
+ my $sth_insert = $dbh->prepare(
+ "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"
+ );
+ my $sth_activity = $dbh->prepare(
+ "INSERT INTO longdescs_tags_activity
+ (bug_id, comment_id, who, bug_when, added, removed)
+ VALUES (?, ?, ?, ?, ?, ?)"
+ );
+
+ foreach my $tag (@$removed_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
+ if ($weighted) {
+ if ($weighted->weight == 1) {
+ $weighted->remove_from_db();
+ } else {
+ $weighted->set_weight($weighted->weight - 1);
+ $weighted->update();
+ }
+ }
+ trick_taint($tag);
+ $sth_delete->execute($self->id, $tag);
+ $sth_activity->execute(
+ $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag);
+ }
+
+ foreach my $tag (@$added_tags) {
+ my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag });
+ if ($weighted) {
+ $weighted->set_weight($weighted->weight + 1);
+ $weighted->update();
+ } else {
+ Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 });
+ }
+ trick_taint($tag);
+ $sth_insert->execute($self->id, $tag);
+ $sth_activity->execute(
+ $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, '');
+ }
+ }
+
return $changes;
}
-# Speeds up displays of comment lists by loading all ->author objects
-# at once for a whole list.
+# Speeds up displays of comment lists by loading all author objects and tags at
+# once for a whole list.
sub preload {
my ($class, $comments) = @_;
+ # Author
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}};
}
+ # Tags
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ my $dbh = Bugzilla->dbh;
+ my @comment_ids = map { $_->id } @$comments;
+ my %comment_map = map { $_->id => $_ } @$comments;
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
+ FROM longdescs_tags
+ WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . "
+ GROUP BY comment_id");
+ foreach my $row (@$rows) {
+ $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
+ }
+ }
}
###############################
@@ -113,6 +184,39 @@ sub work_time {
sub type { return $_[0]->{'type'}; }
sub extra_data { return $_[0]->{'extra_data'} }
+sub tags {
+ my ($self) = @_;
+ return [] unless Bugzilla->params->{'comment_taggers_group'};
+ $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT tag
+ FROM longdescs_tags
+ WHERE comment_id = ?
+ ORDER BY tag",
+ undef, $self->id);
+ return $self->{'tags'};
+}
+
+sub collapsed {
+ my ($self) = @_;
+ return 0 unless Bugzilla->params->{'comment_taggers_group'};
+ return $self->{collapsed} if exists $self->{collapsed};
+ $self->{collapsed} = 0;
+ Bugzilla->request_cache->{comment_tags_collapsed}
+ ||= [ split(/\s*,\s*/, Bugzilla->params->{'collapsed_comment_tags'}) ];
+ my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
+ foreach my $my_tag (@{ $self->tags }) {
+ $my_tag = lc($my_tag);
+ foreach my $collapsed_tag (@collapsed_tags) {
+ if ($my_tag eq lc($collapsed_tag)) {
+ $self->{collapsed} = 1;
+ last;
+ }
+ }
+ last if $self->{collapsed};
+ }
+ return $self->{collapsed};
+}
+
sub bug {
my $self = shift;
require Bugzilla::Bug;
@@ -170,6 +274,26 @@ sub set_is_private { $_[0]->set('isprivate', $_[1]); }
sub set_type { $_[0]->set('type', $_[1]); }
sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
+sub add_tag {
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
+
+ my $tags = $self->tags;
+ return if grep { lc($tag) eq lc($_) } @$tags;
+ push @$tags, $tag;
+ $self->{'tags'} = [ sort @$tags ];
+}
+
+sub remove_tag {
+ my ($self, $tag) = @_;
+ $tag = $self->_check_tag($tag);
+
+ my $tags = $self->tags;
+ my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1;
+ return unless defined $index;
+ splice(@$tags, $index, 1);
+}
+
##############
# Validators #
##############
@@ -312,6 +436,17 @@ sub _check_isprivate {
return $isprivate ? 1 : 0;
}
+sub _check_tag {
+ my ($invocant, $tag) = @_;
+ length($tag) < MIN_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_short', { tag => $tag });
+ length($tag) > MAX_COMMENT_TAG_LENGTH
+ and ThrowUserError('comment_tag_too_long', { tag => $tag });
+ $tag =~ /^[\w\d\._-]+$/
+ or ThrowUserError('comment_tag_invalid', { tag => $tag });
+ return $tag;
+}
+
sub count {
my ($self) = @_;
@@ -326,7 +461,7 @@ sub count {
undef, $self->bug_id, $self->creation_ts);
return --$self->{'count'};
-}
+}
1;
@@ -372,7 +507,7 @@ C<string> Time spent as related to this comment.
=item C<is_private>
-C<boolean> Comment is marked as private
+C<boolean> Comment is marked as private.
=item C<already_wrapped>
@@ -387,6 +522,54 @@ L<Bugzilla::User> who created the comment.
C<int> The position this comment is located in the full list of comments for a bug starting from 0.
+=item C<collapsed>
+
+C<boolean> Comment should be displayed as collapsed by default.
+
+=item C<tags>
+
+C<array of strings> The tags attached to the comment.
+
+=item C<add_tag>
+
+=over
+
+=item B<Description>
+
+Attaches the specified tag to the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to attach.
+
+=back
+
+=back
+
+=item C<remove_tag>
+
+=over
+
+=item B<Description>
+
+Detaches the specified tag from the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to detach.
+
+=back
+
+=back
+
=item C<body_full>
=over
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
new file mode 100644
index 000000000..5835efbc4
--- /dev/null
+++ b/Bugzilla/Comment/TagWeights.pm
@@ -0,0 +1,74 @@
+# 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::TagWeights;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::Object);
+
+use Bugzilla::Constants;
+
+# No auditing required
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+
+use constant DB_COLUMNS => qw(
+ id
+ tag
+ weight
+);
+
+use constant UPDATE_COLUMNS => qw(
+ weight
+);
+
+use constant DB_TABLE => 'longdescs_tags_weights';
+use constant ID_FIELD => 'id';
+use constant NAME_FIELD => 'tag';
+use constant LIST_ORDER => 'weight DESC';
+use constant VALIDATORS => { };
+
+sub tag { return $_[0]->{'tag'} }
+sub weight { return $_[0]->{'weight'} }
+
+sub set_weight { $_[0]->set('weight', $_[1]); }
+
+1;
+
+=head1 NAME
+
+Comment::TagWeights - Bugzilla comment weighting class.
+
+=head1 DESCRIPTION
+
+TagWeights.pm represents a Comment::TagWeight object. It is an implementation
+of L<Bugzilla::Object>, and thus provides all methods that L<Bugzilla::Object>
+provides.
+
+TagWeights is used to quickly find tags and order by their usage count.
+
+=head1 PROPERTIES
+
+=over
+
+=item C<tag>
+
+C<getter string> The tag
+
+=item C<weight>
+
+C<getter int> The tag's weight. The value returned corresponds to the number of
+comments with this tag attached.
+
+=item C<set_weight>
+
+C<setter int> Set the tag's weight.
+
+=back
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index e24f75661..39c43cb92 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -84,7 +84,13 @@ sub get_param_list {
choices => ['', @legal_OS],
default => '',
checker => \&check_opsys
- } );
+ },
+
+ {
+ name => 'collapsed_comment_tags',
+ type => 't',
+ default => 'obsolete, spam',
+ });
return @param_list;
}
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index 5872ae1e1..96d7c1c89 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -28,6 +28,7 @@ use parent qw(Exporter);
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize check_email check_smtp_ssl
+ check_comment_taggers_group
);
# Checking functions for the various values
@@ -367,6 +368,14 @@ sub check_theschwartz_available {
return "";
}
+sub check_comment_taggers_group {
+ my $group_name = shift;
+ if ($group_name && !Bugzilla->feature('jsonrpc')) {
+ return "Comment tagging requires installation of the JSONRPC feature";
+ }
+ return check_group($group_name);
+}
+
# OK, here are the parameter definitions themselves.
#
# Each definition is a hash with keys:
@@ -465,6 +474,11 @@ Checks that the value is a valid number
Checks that the value is a valid regexp
+=item C<check_comment_taggers_group>
+
+Checks that the required modules for comment tagging are installed, and that a
+valid group is provided.
+
=back
=head1 B<Methods in need of POD>
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index d57573de3..1d3878415 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -56,7 +56,15 @@ sub get_param_list {
default => 'editbugs',
checker => \&check_group
},
-
+
+ {
+ name => 'comment_taggers_group',
+ type => 's',
+ choices => \&_get_all_group_names,
+ default => 'editbugs',
+ checker => \&check_comment_taggers_group
+ },
+
{
name => 'debug_group',
type => 's',
@@ -84,4 +92,5 @@ sub _get_all_group_names {
unshift(@group_names, '');
return \@group_names;
}
+
1;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 60c7a34e0..de045273b 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -69,6 +69,9 @@ use Memoize;
COMMENT_COLS
MAX_COMMENT_LENGTH
+ MIN_COMMENT_TAG_LENGTH
+ MAX_COMMENT_TAG_LENGTH
+
CMT_NORMAL
CMT_DUPE_OF
CMT_HAS_DUPE
@@ -291,6 +294,10 @@ use constant COMMENT_COLS => 80;
# Used in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535;
+# The minimum and maximum length of comment tags.
+use constant MIN_COMMENT_TAG_LENGTH => 3;
+use constant MAX_COMMENT_TAG_LENGTH => 24;
+
# The type of bug comments.
use constant CMT_NORMAL => 0;
use constant CMT_DUPE_OF => 1;
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index c619a5780..30ed55b9a 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -406,6 +406,54 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ longdescs_tags => {
+ FIELDS => [
+ id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
+ comment_id => { TYPE => 'INT4',
+ REFERENCES => { TABLE => 'longdescs',
+ COLUMN => 'comment_id',
+ DELETE => 'CASCADE' }},
+ tag => { TYPE => 'varchar(24)', NOTNULL => 1 },
+ ],
+ INDEXES => [
+ longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' },
+ ],
+ },
+
+ longdescs_tags_weights => {
+ FIELDS => [
+ id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
+ tag => { TYPE => 'varchar(24)', NOTNULL => 1 },
+ weight => { TYPE => 'INT3', NOTNULL => 1 },
+ ],
+ INDEXES => [
+ longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' },
+ ],
+ },
+
+ longdescs_tags_activity => {
+ FIELDS => [
+ id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 },
+ bug_id => { TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => { TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE' }},
+ comment_id => { TYPE => 'INT4',
+ REFERENCES => { TABLE => 'longdescs',
+ COLUMN => 'comment_id',
+ DELETE => 'CASCADE' }},
+ who => { TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => { TABLE => 'profiles',
+ COLUMN => 'userid' }},
+ bug_when => { TYPE => 'DATETIME', NOTNULL => 1 },
+ added => { TYPE => 'varchar(24)' },
+ removed => { TYPE => 'varchar(24)' },
+ ],
+ INDEXES => [
+ longdescs_tags_activity_bug_id_idx => ['bug_id'],
+ ],
+ },
+
dependencies => {
FIELDS => [
blocked => {TYPE => 'INT3', NOTNULL => 1,
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index cda252542..82c82161a 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -255,6 +255,7 @@ use constant DEFAULT_FIELDS => (
type => FIELD_TYPE_BUG_URLS},
{name => 'tag', desc => 'Tags', buglist => 1,
type => FIELD_TYPE_KEYWORDS},
+ {name => 'comment_tag', desc => 'Comment Tag'},
);
################
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 84487dbbe..7a067fce0 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -1852,6 +1852,17 @@ sub is_timetracker {
return $self->{'is_timetracker'};
}
+sub can_tag_comments {
+ my $self = shift;
+
+ if (!defined $self->{'can_tag_comments'}) {
+ my $group = Bugzilla->params->{'comment_taggers_group'};
+ $self->{'can_tag_comments'} =
+ ($group && $self->in_group($group)) ? 1 : 0;
+ }
+ return $self->{'can_tag_comments'};
+}
+
sub get_userlist {
my $self = shift;
@@ -2648,6 +2659,12 @@ i.e. if the 'insidergroup' parameter is set and the user belongs to this group.
Returns true if the user is a global watcher,
i.e. if the 'globalwatchers' parameter contains the user.
+=item C<can_tag_comments>
+
+Returns true if the user can attach tags to comments.
+i.e. if the 'comment_taggers_group' parameter is set and the user belongs to
+this group.
+
=back
=head1 CLASS FUNCTIONS
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 598316c59..7f012160c 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -13,6 +13,7 @@ use strict;
use parent qw(Bugzilla::WebService);
use Bugzilla::Comment;
+use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
@@ -20,7 +21,7 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(filter filter_wants validate translate);
use Bugzilla::Bug;
use Bugzilla::BugMail;
-use Bugzilla::Util qw(trick_taint trim diff_arrays);
+use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural);
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Status;
@@ -320,7 +321,8 @@ sub _translate_comment {
my ($self, $comment, $filters) = @_;
my $attach_id = $comment->is_about_attachment ? $comment->extra_data
: undef;
- return filter $filters, {
+
+ my $comment_hash = {
id => $self->type('int', $comment->id),
bug_id => $self->type('int', $comment->bug_id),
creator => $self->type('email', $comment->author->login),
@@ -332,6 +334,16 @@ sub _translate_comment {
attachment_id => $self->type('int', $attach_id),
count => $self->type('int', $comment->count),
};
+
+ # Don't load comment tags unless enabled
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ $comment_hash->{tags} = [
+ map { $self->type('string', $_) }
+ @{ $comment->tags }
+ ];
+ }
+
+ return filter $filters, $comment_hash;
}
sub get {
@@ -999,6 +1011,70 @@ sub update_tags {
return { changes => \%changes };
}
+sub update_comment_tags {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ $user->can_tag_comments
+ || ThrowUserError("auth_failure",
+ { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "update",
+ object => "comment_tags" });
+
+ my $comment_id = $params->{comment_id}
+ // ThrowCodeError('param_required',
+ { function => 'Bug.update_comment_tags',
+ param => 'comment_id' });
+
+ my $comment = Bugzilla::Comment->new($comment_id)
+ || return [];
+ $comment->bug->check_is_visible();
+ if ($comment->is_private && !$user->is_insider) {
+ ThrowUserError('comment_is_private', { id => $comment_id });
+ }
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ foreach my $tag (@{ $params->{add} || [] }) {
+ $comment->add_tag($tag) if defined $tag;
+ }
+ foreach my $tag (@{ $params->{remove} || [] }) {
+ $comment->remove_tag($tag) if defined $tag;
+ }
+ $comment->update();
+ $dbh->bz_commit_transaction();
+
+ return $comment->tags;
+}
+
+sub search_comment_tags {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ Bugzilla->user->can_tag_comments
+ || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "search",
+ object => "comment_tags"});
+
+ my $query = $params->{query};
+ $query
+ // ThrowCodeError('param_required', { param => 'query' });
+ my $limit = detaint_natural($params->{limit}) || 7;
+
+ my $tags = Bugzilla::Comment::TagWeights->match({
+ WHERE => {
+ 'tag LIKE ?' => "\%$query\%",
+ },
+ LIMIT => $limit,
+ });
+ return [ map { $_->tag } @$tags ];
+}
+
+
##############################
# Private Helper Subroutines #
##############################
@@ -4032,6 +4108,137 @@ This method can throw the same errors as L</get>.
=back
+=head2 search_comment_tags
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+Searches for tags which contain the provided substring.
+
+=item B<REST>
+
+To search for comment tags:
+
+GET /bug/comment/tags/<query>
+
+=item B<Params>
+
+=over
+
+=item C<query>
+
+B<Required> C<string> Only tags containg this substring will be returned.
+
+=item C<limit>
+
+C<int> If provided will return no more than C<limit> tags. Defaults to C<10>.
+
+=back
+
+=item B<Returns>
+
+An C<array of strings> of matching tags.
+
+=item B<Errors>
+
+This method can throw all of the errors that L</get> throws, plus:
+
+=over
+
+=item 125 (Comment Tagging Disabled)
+
+Comment tagging support is not available or enabled.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 update_comment_tags
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+Adds or removes tags from a comment.
+
+=item B<REST>
+
+To update the tags comments attached to a comment:
+
+PUT /bug/comment/tags
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<comment_id>
+
+B<Required> C<int> The ID of the comment to update.
+
+=item C<add>
+
+C<array of strings> The tags to attach to the comment.
+
+=item C<remove>
+
+C<array of strings> The tags to detach from the comment.
+
+=back
+
+=item B<Returns>
+
+An C<array of strings> containing the comment's updated tags.
+
+=item B<Errors>
+
+This method can throw all of the errors that L</get> throws, plus:
+
+=over
+
+=item 125 (Comment Tagging Disabled)
+
+Comment tagging support is not available or enabled.
+
+=item 126 (Invalid Comment Tag)
+
+The comment tag provided was not valid (eg. contains invalid characters).
+
+=item 127 (Comment Tag Too Short)
+
+The comment tag provided is shorter than the minimum length.
+
+=item 128 (Comment Tag Too Long)
+
+The comment tag provided is longer than the maximum length.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
=head1 B<Methods in need of POD>
=over
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 1c3929e53..2c5bc31dd 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -98,7 +98,12 @@ use constant WS_ERROR_CODE => {
comment_is_private => 110,
comment_id_invalid => 111,
comment_too_long => 114,
- comment_invalid_isprivate => 117,
+ comment_invalid_isprivate => 117,
+ # Comment tagging
+ comment_tag_disabled => 125,
+ comment_tag_invalid => 126,
+ comment_tag_too_long => 127,
+ comment_tag_too_short => 128,
# See Also errors
bug_url_invalid => 112,
bug_url_too_long => 112,
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
index 98ae6049c..ea420b4ed 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -65,6 +65,22 @@ sub _rest_resources {
}
}
},
+ qr{^/bug/comment/tags/([^/]+)$}, {
+ GET => {
+ method => 'search_comment_tags',
+ params => sub {
+ return { query => $_[0] };
+ },
+ },
+ },
+ qr{^/bug/comment/([^/]+)/tags$}, {
+ PUT => {
+ method => 'update_comment_tags',
+ params => sub {
+ return { comment_id => $_[0] };
+ },
+ },
+ },
qr{^/bug/([^/]+)/history$}, {
GET => {
method => 'history',