summaryrefslogtreecommitdiffstats
path: root/extensions/EditComments
diff options
context:
space:
mode:
authorDave Lawrence <dlawrence@mozilla.com>2014-01-11 18:12:42 +0100
committerDave Lawrence <dlawrence@mozilla.com>2014-01-11 18:12:42 +0100
commit01db31261bcaa73dfeb6c287c1a22ece47d119eb (patch)
tree46c51ec369b81d2b13dc5a58dee9400ad7d8234e /extensions/EditComments
parent86f6ac8e6f57e2355691f95935330e5b1040f096 (diff)
downloadbugzilla-01db31261bcaa73dfeb6c287c1a22ece47d119eb.tar.gz
bugzilla-01db31261bcaa73dfeb6c287c1a22ece47d119eb.tar.xz
Bug 936165 - Allow for privileged users to edit past comments and preserve comment history (BMO extension)
r=glob
Diffstat (limited to 'extensions/EditComments')
-rw-r--r--extensions/EditComments/Config.pm19
-rw-r--r--extensions/EditComments/Extension.pm262
-rw-r--r--extensions/EditComments/lib/WebService.pm170
-rw-r--r--extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl13
-rw-r--r--extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl45
-rw-r--r--extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl12
-rw-r--r--extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl11
-rw-r--r--extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl12
-rw-r--r--extensions/EditComments/template/en/default/pages/editcomments.html.tmpl122
-rw-r--r--extensions/EditComments/web/js/editcomments.js90
-rw-r--r--extensions/EditComments/web/styles/editcomments.css10
11 files changed, 766 insertions, 0 deletions
diff --git a/extensions/EditComments/Config.pm b/extensions/EditComments/Config.pm
new file mode 100644
index 000000000..dae675001
--- /dev/null
+++ b/extensions/EditComments/Config.pm
@@ -0,0 +1,19 @@
+# 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::Extension::EditComments;
+
+use 5.10.1;
+use strict;
+
+use constant NAME => 'EditComments';
+
+use constant REQUIRED_MODULES => [];
+
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditComments/Extension.pm b/extensions/EditComments/Extension.pm
new file mode 100644
index 000000000..7396c6f28
--- /dev/null
+++ b/extensions/EditComments/Extension.pm
@@ -0,0 +1,262 @@
+# 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::Extension::EditComments;
+
+use 5.10.1;
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::Config::Common;
+use Bugzilla::Config::GroupSecurity;
+use Bugzilla::WebService::Bug;
+use Bugzilla::WebService::Util qw(filter_wants);
+
+our $VERSION = '0.01';
+
+################
+# Installation #
+################
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ my $schema = $args->{schema};
+
+ $schema->{'longdescs_activity'} = {
+ FIELDS => [
+ comment_id => {TYPE => 'INT', NOTNULL => 1,
+ REFERENCES => {TABLE => 'longdescs',
+ COLUMN => 'comment_id',
+ DELETE => 'CASCADE'}},
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ change_when => {TYPE => 'DATETIME', NOTNULL => 1},
+ old_comment => {TYPE => 'LONGTEXT', NOTNULL => 1},
+ ],
+ INDEXES => [
+ longdescs_activity_comment_id_idx => ['comment_id'],
+ longdescs_activity_change_when_idx => ['change_when'],
+ longdescs_activity_comment_id_change_when_idx => [qw(comment_id change_when)],
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column('longdescs', 'edit_count', { TYPE => 'INT3', DEFAULT => 0 });
+}
+
+####################
+# Template Methods #
+####################
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ return if $args->{'page_id'} ne 'editcomments.html';
+
+ my $vars = $args->{'vars'};
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+
+ # validate group membership
+ my $edit_comments_group = Bugzilla->params->{edit_comments_group};
+ if (!$edit_comments_group || !$user->in_group($edit_comments_group)) {
+ ThrowUserError('auth_failure', { group => $edit_comments_group,
+ action => 'view',
+ object => 'editcomments' });
+ }
+
+ my $bug_id = $params->{bug_id};
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ my $comment_id = $params->{comment_id};
+
+ my ($comment) = grep($_->id == $comment_id, @{ $bug->comments });
+ if (!$comment
+ || ($comment->is_private && !$user->is_insider))
+ {
+ ThrowUserError("edit_comment_invalid_comment_id", { comment_id => $comment_id });
+ }
+
+ $vars->{'bug'} = $bug;
+ $vars->{'comment'} = $comment;
+}
+
+##################
+# Object Methods #
+##################
+
+BEGIN {
+ no warnings 'redefine';
+ *Bugzilla::Comment::activity = \&_get_activity;
+ *Bugzilla::Comment::edit_count = \&_edit_count;
+ *Bugzilla::WebService::Bug::_super_translate_comment = \&Bugzilla::WebService::Bug::_translate_comment;
+ *Bugzilla::WebService::Bug::_translate_comment = \&_new_translate_comment;
+}
+
+sub _new_translate_comment {
+ my ($self, $comment, $filters) = @_;
+
+ my $comment_hash = $self->_super_translate_comment($comment, $filters);
+
+ if (filter_wants $filters, 'raw_text') {
+ $comment_hash->{raw_text} = $self->type('string', $comment->body);
+ }
+
+ return $comment_hash;
+}
+
+sub _edit_count { return $_[0]->{'edit_count'}; }
+
+sub _get_activity {
+ my ($self, $activity_sort_order) = @_;
+
+ return $self->{'activity'} if $self->{'activity'};
+
+ my $dbh = Bugzilla->dbh;
+ my $query = 'SELECT longdescs_activity.comment_id AS id, profiles.userid, ' .
+ $dbh->sql_date_format('longdescs_activity.change_when', '%Y.%m.%d %H:%i:%s') . '
+ AS time, longdescs_activity.old_comment AS old
+ FROM longdescs_activity
+ INNER JOIN profiles
+ ON profiles.userid = longdescs_activity.who
+ WHERE longdescs_activity.comment_id = ?';
+ $query .= " ORDER BY longdescs_activity.change_when DESC";
+ my $sth = $dbh->prepare($query);
+ $sth->execute($self->id);
+
+ # We are shifting each comment activity body 1 back. The reason this
+ # has to be done is that the longdescs_activity table stores the comment
+ # body that the comment was before the edit, not the actual new version
+ # of the comment.
+ my @activity;
+ my $new_comment;
+ my $last_old_comment;
+ my $count = 0;
+ while (my $change_ref = $sth->fetchrow_hashref()) {
+ my %change = %$change_ref;
+ $change{'author'} = Bugzilla::User->new({ id => $change{'userid'}, cache => 1 });
+ if ($count == 0) {
+ $change{new} = $self->body;
+ }
+ else {
+ $change{new} = $new_comment;
+ }
+ $new_comment = $change{old};
+ $last_old_comment = $change{old};
+ push (@activity, \%change);
+ $count++;
+ }
+
+ return [] if !@activity;
+
+ # Store the original comment as the first or last entry
+ # depending on sort order
+ push(@activity, {
+ author => $self->author,
+ body => $last_old_comment,
+ time => $self->creation_ts,
+ original => 1
+ });
+
+ $activity_sort_order
+ ||= Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
+
+ if ($activity_sort_order eq "oldest_to_newest") {
+ @activity = reverse @activity;
+ }
+
+ $self->{'activity'} = \@activity;
+
+ return $self->{'activity'};
+}
+
+#########
+# Hooks #
+#########
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Comment')) {
+ push(@$columns, 'edit_count');
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+
+ # Silently return if not in the proper group
+ # or if editing comments is disabled
+ my $user = Bugzilla->user;
+ my $edit_comments_group = Bugzilla->params->{edit_comments_group};
+ return if (!$edit_comments_group || !$user->in_group($edit_comments_group));
+
+ my $bug = $args->{bug};
+ my $timestamp = $args->{timestamp};
+ my $params = Bugzilla->input_params;
+ my $dbh = Bugzilla->dbh;
+
+ my $updated = 0;
+ foreach my $param (grep(/^edit_comment_textarea_/, keys %$params)) {
+ my ($comment_id) = $param =~ /edit_comment_textarea_(\d+)$/;
+ next if !detaint_natural($comment_id);
+
+ # The comment ID must belong to this bug.
+ my ($comment_obj) = grep($_->id == $comment_id, @{ $bug->comments});
+ next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider));
+
+ my $new_comment = $comment_obj->_check_thetext($params->{$param});
+
+ my $old_comment = $comment_obj->body;
+ next if $old_comment eq $new_comment;
+
+ trick_taint($new_comment);
+ $dbh->do("UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1
+ WHERE comment_id = ?",
+ undef, $new_comment, $comment_id);
+
+ # Log old comment to the longdescs activity table
+ $timestamp ||= $dbh->selectrow_array("SELECT NOW()");
+ $dbh->do("INSERT INTO longdescs_activity " .
+ "(comment_id, who, change_when, old_comment) " .
+ "VALUES (?, ?, ?, ?)",
+ undef, ($comment_id, $user->id, $timestamp, $old_comment));
+
+ $comment_obj->{thetext} = $new_comment;
+
+ $updated = 1;
+ }
+
+ $bug->_sync_fulltext( update_comments => 1 ) if $updated;
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ push @{ $args->{panels}->{groupsecurity}->{params} }, {
+ name => 'edit_comments_group',
+ type => 's',
+ choices => \&Bugzilla::Config::GroupSecurity::_get_all_group_names,
+ default => 'admin',
+ checker => \&check_group
+ };
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{EditComments} = "Bugzilla::Extension::EditComments::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditComments/lib/WebService.pm b/extensions/EditComments/lib/WebService.pm
new file mode 100644
index 000000000..9213f0407
--- /dev/null
+++ b/extensions/EditComments/lib/WebService.pm
@@ -0,0 +1,170 @@
+# 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::Extension::EditComments::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trim);
+use Bugzilla::WebService::Util qw(validate);
+
+sub comments {
+ my ($self, $params) = validate(@_, 'comment_ids');
+ my $dbh = Bugzilla->switch_to_shadow_db();
+ my $user = Bugzilla->user;
+
+ if (!defined $params->{comment_ids}) {
+ ThrowCodeError('param_required',
+ { function => 'Bug.comments',
+ param => 'comment_ids' });
+ }
+
+ my @ids = map { trim($_) } @{ $params->{comment_ids} || [] };
+ my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
+
+ # See if we were passed any invalid comment ids.
+ my %got_ids = map { $_->id => 1 } @$comment_data;
+ foreach my $comment_id (@ids) {
+ if (!$got_ids{$comment_id}) {
+ ThrowUserError('comment_id_invalid', { id => $comment_id });
+ }
+ }
+
+ # Now make sure that we can see all the associated bugs.
+ my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
+ $user->visible_bugs([ keys %got_bug_ids ]); # preload cache for visibility check
+ Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
+
+ my %comments;
+ foreach my $comment (@$comment_data) {
+ if ($comment->is_private && !$user->is_insider) {
+ ThrowUserError('comment_is_private', { id => $comment->id });
+ }
+ $comments{$comment->id} = $comment->body;
+ }
+
+ return { comments => \%comments };
+}
+
+sub rest_resources {
+ return [
+ qr{^/editcomments/comment/(\d+)$}, {
+ GET => {
+ method => 'comments',
+ params => sub {
+ return { comment_ids => $_[0] };
+ },
+ },
+ },
+ qr{^/editcomments/comment$}, {
+ GET => {
+ method => 'comments',
+ },
+ },
+ ];
+};
+
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::EditComments::Webservice - The EditComments WebServices API
+
+=head1 DESCRIPTION
+
+This module contains API methods that are useful to user's of bugzilla.mozilla.org.
+
+=head1 METHODS
+
+=head2 comments
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+This allows you to get the raw comment text about comments, given a list of comment ids.
+
+=item B<REST>
+
+To get all comment text for a list of comment ids:
+
+GET /bug/editcomments/comment?comment_ids=1234&comment_ids=5678...
+
+To get comment text for a specific comment based on the comment ID:
+
+GET /bug/editcomments/comment/<comment_id>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<comment_ids> (required)
+
+C<array> An array of integer comment_ids. These comments will be
+returned individually, separate from any other comments in their
+respective bugs.
+
+=item B<Returns>
+
+1 item is returned:
+
+=over
+
+=item C<comments>
+
+Each individual comment requested in C<comment_ids> is returned here,
+in a hash where the numeric comment id is the key, and the value
+is the comment's raw text.
+
+=back
+
+=item B<Errors>
+
+In addition to standard Bug.get type errors, this method can throw the
+following additional errors:
+
+=over
+
+=item 110 (Comment Is Private)
+
+You specified the id of a private comment in the C<comment_ids>
+argument, and you are not in the "insider group" that can see
+private comments.
+
+=item 111 (Invalid Comment ID)
+
+You specified an id in the C<comment_ids> argument that is invalid--either
+you specified something that wasn't a number, or there is no comment with
+that id.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.2>.
+
+=back
+
+=back
+
+=back
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
diff --git a/extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
new file mode 100644
index 000000000..01ca7bbb7
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
@@ -0,0 +1,13 @@
+[%# 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.
+ #%]
+
+[% IF panel.name == "groupsecurity" %]
+ [% panel.param_descs.edit_comments_group =
+ 'The name of the group of users who can edit comments. Leave blank to disable comment editing.'
+ %]
+[% END -%]
diff --git a/extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl b/extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
new file mode 100644
index 000000000..28482c6c3
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
@@ -0,0 +1,45 @@
+[%# 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.
+ #%]
+
+[% IF Param('edit_comments_group') && user.in_group(Param('edit_comments_group')) %]
+ <span id="edit_comment_link_[% comment.count FILTER html %]">
+ [<a href="javascript:void(0);" id="edit_comment_edit_link_[% comment.count FILTER html %]"
+ onclick="editComment('[% comment.count FILTER js %]','[% comment.id FILTER js %]');">edit</a>
+ [% IF comment.edit_count %]
+ | <a href="page.cgi?id=editcomments.html&bug_id=[% bug.id FILTER uri %]&comment_id=[% comment.id FILTER uri %]">history</a>
+ ([% comment.edit_count FILTER html %])
+ [% END %]]
+ </span>
+ <div id="edit_comment_[% comment.count FILTER html %]">
+ <div class="bz_comment_text bz_default_hidden" id="edit_comment_loading_[% comment.count FILTER html %]">Loading...</div>
+ [% INCLUDE global/textarea.html.tmpl
+ name = "edit_comment_textarea_${comment.id}"
+ id = "edit_comment_textarea_${comment.count}"
+ minrows = 10
+ maxrows = 25
+ classes = "edit_comment_textarea bz_default_hidden"
+ cols = constants.COMMENT_COLS
+ disabled = 1
+ %]
+ </div>
+ <script>
+ YAHOO.util.Event.onDOMReady(function() {
+ // Insert edit links near other comment actions such as reply
+ var comment_div = YAHOO.util.Dom.get('c[% comment.count FILTER js %]');
+ var bz_comment_actions = YAHOO.util.Dom.getElementsByClassName('bz_comment_actions', 'span', comment_div)[0];
+ var edit_comment_link = YAHOO.util.Dom.get('edit_comment_link_[% comment.count FILTER js %]');
+ bz_comment_actions.insertBefore(edit_comment_link, bz_comment_actions.firstChild);
+
+ // Insert blank textarea right below formatted comment
+ var comment_div = YAHOO.util.Dom.get('c[% comment.count FILTER js %]');
+ var comment_pre = YAHOO.util.Dom.get('comment_text_[% comment.count FILTER js %]');
+ var edit_comment_div = YAHOO.util.Dom.get('edit_comment_[% comment.count FILTER js %]');
+ comment_div.insertBefore(edit_comment_div, comment_pre);
+ });
+ </script>
+[% END %]
diff --git a/extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..331d7e6df
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,12 @@
+[%# 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.
+ #%]
+
+[% IF Param('edit_comments_group') && user.in_group(Param('edit_comments_group')) %]
+ [% style_urls.push('extensions/EditComments/web/styles/editcomments.css') %]
+ [% javascript_urls.push('extensions/EditComments/web/js/editcomments.js') %]
+[% END %]
diff --git a/extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..4325aab30
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,11 @@
+[%# 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.
+ #%]
+
+[% IF object == 'editcomments' %]
+ edit comments
+[% END %]
diff --git a/extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..bc02b52f0
--- /dev/null
+++ b/extensions/EditComments/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,12 @@
+[%# 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.
+ #%]
+
+[% IF error == "edit_comment_invalid_comment_id" %]
+ [% title = "Invalid Comment ID" %]
+ The comment id '[% comment_id FILTER html %]' is invalid.
+[% END %]
diff --git a/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl
new file mode 100644
index 000000000..8b3b90c9e
--- /dev/null
+++ b/extensions/EditComments/template/en/default/pages/editcomments.html.tmpl
@@ -0,0 +1,122 @@
+[%# 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.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Comment changes made to $terms.bug $bug.id, comment $comment.id"
+ header = "Activity log for $terms.bug $bug.id, comment $comment.id"
+ %]
+
+<script type="text/javascript">
+/* The functions below expand and collapse comments */
+function toggle_comment_display(link, comment_id) {
+ if (YAHOO.util.Dom.hasClass('comment_text_' + comment_id, 'collapsed')) {
+ expand_comment(link, comment);
+ }
+ else {
+ collapse_comment(link, comment);
+ }
+}
+
+function toggle_all_comments(action) {
+ var num_comments = [% comment.activity.size FILTER html %];
+
+ // If for some given ID the comment doesn't exist, this doesn't mean
+ // there are no more comments, but that the comment is private and
+ // the user is not allowed to view it.
+
+ for (var id = 0; id < num_comments; id++) {
+ var comment = document.getElementById('comment_text_' + id);
+ if (!comment) {
+ continue;
+ }
+
+ var link = document.getElementById('comment_link_' + id);
+ if (action == 'collapse') {
+ collapse_comment(link, comment);
+ }
+ else {
+ expand_comment(link, comment);
+ }
+ }
+}
+
+function collapse_comment(link, comment) {
+ link.innerHTML = "[+]";
+ link.title = "Expand the comment.";
+ YAHOO.util.Dom.addClass(comment, 'collapsed');
+}
+
+function expand_comment(link, comment) {
+ link.innerHTML = "[-]";
+ link.title = "Collapse the comment";
+ YAHOO.util.Dom.removeClass(comment, 'collapsed');
+}
+</script>
+
+<p>
+ [% "Back to $terms.bug $bug.id" FILTER bug_link(bug.id) FILTER none %]
+</p>
+
+<p>
+ <strong>Note</strong>: The actual edited comment in the [% terms.bug %] view page will always show the original commentor's name and original timestamp.
+</p>
+
+<p>
+ <a href="#" onclick="toggle_all_comments('collapse'); return false;">Collapse All Changes</a> -
+ <a href="#" onclick="toggle_all_comments('expand'); return false;">Expand All Changes</a>
+</p>
+
+[% count = 0 %]
+[% FOREACH a = comment.activity %]
+ <div class="bz_comment">
+ <div class="bz_comment_head">
+ <i>
+ [% IF a.original %]
+ Original comment by [% (a.author.name || "Need Real Name") FILTER html %]
+ <span class="vcard">
+ (<a class="fn email" href="mailto:[% a.author.email FILTER html %]">
+ [%- a.author.email FILTER html -%]</a>)
+ </span>
+ on [%+ a.time FILTER time %]
+ [% ELSE %]
+ Revision by [% (a.author.name || "Need Real Name") FILTER html %]
+ <span class="vcard">
+ (<a class="fn email" href="mailto:[% a.author.email FILTER html %]">
+ [%- a.author.email FILTER html -%]</a>)
+ </span>
+ on [%+ a.time FILTER time %]
+ [% END %]
+ </i>
+ <a href="#" id="comment_link_[% count FILTER html %]"
+ onclick="toggle_comment_display(this, '[% count FILTER html FILTER js %]'); return false;"
+ title="Collapse the comment.">[-]</a>
+ </div>
+ [% IF a.original %]
+ [% wrapped_comment = a.body FILTER wrap_comment %]
+ [% ELSE %]
+ [% wrapped_comment = a.new FILTER wrap_comment %]
+ [% END %]
+[%# Don't indent the <pre> block, since then the spaces are displayed in the
+ # generated HTML %]
+<pre class="bz_comment_text" id="comment_text_[% count FILTER html %]">
+ [%- wrapped_comment FILTER quoteUrls(bug) -%]
+</pre>
+ </div>
+ [% count = count + 1 %]
+[% END %]
+
+[% IF comment.activity.size > 0 %]
+ <p>
+ [% "Back to $terms.bug $bug.id" FILTER bug_link(bug.id) FILTER none %]
+ </p>
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
+
diff --git a/extensions/EditComments/web/js/editcomments.js b/extensions/EditComments/web/js/editcomments.js
new file mode 100644
index 000000000..91763fa62
--- /dev/null
+++ b/extensions/EditComments/web/js/editcomments.js
@@ -0,0 +1,90 @@
+/* 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.
+ */
+
+function editComment(comment_count, comment_id) {
+ if (!comment_count || !comment_id) return;
+
+ var edit_comment_textarea = YAHOO.util.Dom.get('edit_comment_textarea_' + comment_count);
+ if (!YAHOO.util.Dom.hasClass(edit_comment_textarea, 'bz_default_hidden')) {
+ hideEditCommentField(comment_count);
+ return;
+ }
+
+ // Show the loading indicator
+ toggleCommentLoading(comment_count);
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ // Hide the loading indicator
+ toggleCommentLoading(comment_count);
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ alert("Get [% comment failed: " + data.error.message);
+ }
+ else if (data.result.comments[comment_id]) {
+ var comment_text = data.result.comments[comment_id];
+ showEditCommentField(comment_count, comment_text);
+ }
+ },
+ failure: function(res) {
+ // Hide the loading indicator
+ toggleCommentLoading(comment_count);
+ if (res.responseText) {
+ alert("Get comment failed: " + res.responseText);
+ }
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "EditComments.comments",
+ id: comment_id,
+ params: { comment_ids: [ comment_id ] }
+ })
+ );
+}
+
+function hideEditCommentField(comment_count) {
+ var comment_text_pre = YAHOO.util.Dom.get('comment_text_' + comment_count);
+ YAHOO.util.Dom.removeClass(comment_text_pre, 'bz_default_hidden');
+
+ var edit_comment_textarea = YAHOO.util.Dom.get('edit_comment_textarea_' + comment_count);
+ YAHOO.util.Dom.addClass(edit_comment_textarea, 'bz_default_hidden');
+ edit_comment_textarea.disabled = true;
+
+ YAHOO.util.Dom.get("edit_comment_edit_link_" + comment_count).innerHTML = "edit";
+}
+
+function showEditCommentField(comment_count, comment_text) {
+ var comment_text_pre = YAHOO.util.Dom.get('comment_text_' + comment_count);
+ YAHOO.util.Dom.addClass(comment_text_pre, 'bz_default_hidden');
+
+ var edit_comment_textarea = YAHOO.util.Dom.get('edit_comment_textarea_' + comment_count);
+ YAHOO.util.Dom.removeClass(edit_comment_textarea, 'bz_default_hidden');
+ edit_comment_textarea.disabled = false;
+ edit_comment_textarea.value = comment_text;
+
+ YAHOO.util.Dom.get("edit_comment_edit_link_" + comment_count).innerHTML = "unedit";
+}
+
+function toggleCommentLoading(comment_count, hide) {
+ var comment_div = 'comment_text_' + comment_count;
+ var loading_div = 'edit_comment_loading_' + comment_count;
+ if (YAHOO.util.Dom.hasClass(loading_div, 'bz_default_hidden')) {
+ YAHOO.util.Dom.addClass(comment_div, 'bz_default_hidden');
+ YAHOO.util.Dom.removeClass(loading_div, 'bz_default_hidden');
+ }
+ else {
+ YAHOO.util.Dom.removeClass(comment_div, 'bz_default_hidden');
+ YAHOO.util.Dom.addClass(loading_div, 'bz_default_hidden');
+ }
+}
+
diff --git a/extensions/EditComments/web/styles/editcomments.css b/extensions/EditComments/web/styles/editcomments.css
new file mode 100644
index 000000000..911896ac8
--- /dev/null
+++ b/extensions/EditComments/web/styles/editcomments.css
@@ -0,0 +1,10 @@
+/* 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. */
+
+.edit_comment_textarea {
+ width: 845px;
+}