aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'extensions')
-rw-r--r--extensions/BmpConvert/Config.pm19
-rw-r--r--extensions/BmpConvert/Extension.pm42
-rw-r--r--extensions/BmpConvert/disabled0
-rw-r--r--extensions/Example/Config.pm28
-rw-r--r--extensions/Example/Extension.pm944
-rw-r--r--extensions/Example/disabled0
-rw-r--r--extensions/Example/lib/Auth/Login.pm19
-rw-r--r--extensions/Example/lib/Auth/Verify.pm18
-rw-r--r--extensions/Example/lib/Config.pm29
-rw-r--r--extensions/Example/lib/Util.pm15
-rw-r--r--extensions/Example/lib/WebService.pm19
-rw-r--r--extensions/Example/template/en/default/account/prefs/my_tab.html.tmpl18
-rw-r--r--extensions/Example/template/en/default/admin/params/example.html.tmpl16
-rw-r--r--extensions/Example/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl11
-rw-r--r--extensions/Example/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl11
-rw-r--r--extensions/Example/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl23
-rw-r--r--extensions/Example/template/en/default/hook/global/footer-end.html.tmpl16
-rw-r--r--extensions/Example/template/en/default/hook/global/setting-descs-settings.none.tmpl14
-rw-r--r--extensions/Example/template/en/default/hook/global/user-error-errors.html.tmpl14
-rw-r--r--extensions/Example/template/en/default/pages/example.html.tmpl19
-rw-r--r--extensions/Example/template/en/default/setup/strings.txt.pl12
-rw-r--r--extensions/MoreBugUrl/Config.pm19
-rw-r--r--extensions/MoreBugUrl/Extension.pm46
-rw-r--r--extensions/MoreBugUrl/disabled0
-rw-r--r--extensions/MoreBugUrl/lib/GetSatisfaction.pm37
-rw-r--r--extensions/MoreBugUrl/lib/PHP.pm40
-rw-r--r--extensions/MoreBugUrl/lib/RT.pm37
-rw-r--r--extensions/MoreBugUrl/lib/ReviewBoard.pm42
-rw-r--r--extensions/MoreBugUrl/lib/Rietveld.pm45
-rw-r--r--extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl13
-rw-r--r--extensions/OldBugMove/Config.pm11
-rw-r--r--extensions/OldBugMove/Extension.pm196
-rw-r--r--extensions/OldBugMove/disabled0
-rw-r--r--extensions/OldBugMove/lib/Params.pm36
-rw-r--r--extensions/OldBugMove/template/en/default/admin/params/oldbugmove.html.tmpl27
-rw-r--r--extensions/OldBugMove/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl15
-rw-r--r--extensions/OldBugMove/template/en/default/hook/bug/format_comment-type.txt.tmpl17
-rw-r--r--extensions/OldBugMove/template/en/default/hook/global/user-error-auth_failure_action.html.tmpl11
-rw-r--r--extensions/OldBugMove/template/en/default/hook/global/user-error-errors.html.tmpl17
-rw-r--r--extensions/OldBugMove/template/en/default/hook/list/edit-multiple-after_groups.html.tmpl15
-rw-r--r--extensions/Voting/Config.pm19
-rw-r--r--extensions/Voting/Extension.pm867
-rw-r--r--extensions/Voting/disabled0
-rw-r--r--extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl10
-rw-r--r--extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl47
-rw-r--r--extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl90
-rw-r--r--extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl29
-rw-r--r--extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl26
-rw-r--r--extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl25
-rw-r--r--extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.tmpl11
-rw-r--r--extensions/Voting/template/en/default/hook/bug/process/header-title.html.tmpl11
-rw-r--r--extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl9
-rw-r--r--extensions/Voting/template/en/default/hook/bug/show-header-end.html.tmpl9
-rw-r--r--extensions/Voting/template/en/default/hook/global/code-error-errors.html.tmpl13
-rw-r--r--extensions/Voting/template/en/default/hook/global/field-descs-end.none.tmpl11
-rw-r--r--extensions/Voting/template/en/default/hook/global/reason-descs-end.none.tmpl11
-rw-r--r--extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl43
-rw-r--r--extensions/Voting/template/en/default/hook/search/form-after_freetext_fields.html.tmpl16
-rw-r--r--extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl9
-rw-r--r--extensions/Voting/template/en/default/pages/voting.html.tmpl55
-rw-r--r--extensions/Voting/template/en/default/pages/voting/bug.html.tmpl50
-rw-r--r--extensions/Voting/template/en/default/pages/voting/user.html.tmpl178
-rw-r--r--extensions/Voting/template/en/default/voting/delete-all.html.tmpl38
-rw-r--r--extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl40
-rw-r--r--extensions/Voting/web/style.css21
-rwxr-xr-xextensions/create.pl71
66 files changed, 3620 insertions, 0 deletions
diff --git a/extensions/BmpConvert/Config.pm b/extensions/BmpConvert/Config.pm
new file mode 100644
index 0000000..42fb3ce
--- /dev/null
+++ b/extensions/BmpConvert/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::BmpConvert;
+use strict;
+use constant NAME => 'BmpConvert';
+use constant REQUIRED_MODULES => [
+ {
+ package => 'PerlMagick',
+ module => 'Image::Magick',
+ version => 0,
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BmpConvert/Extension.pm b/extensions/BmpConvert/Extension.pm
new file mode 100644
index 0000000..87ec5f5
--- /dev/null
+++ b/extensions/BmpConvert/Extension.pm
@@ -0,0 +1,42 @@
+# 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::BmpConvert;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Image::Magick;
+
+our $VERSION = '1.0';
+
+sub attachment_process_data {
+ my ($self, $args) = @_;
+ return unless $args->{attributes}->{mimetype} eq 'image/bmp';
+
+ my $data = ${$args->{data}};
+ my $img = Image::Magick->new(magick => 'bmp');
+
+ # $data is a filehandle.
+ if (ref $data) {
+ $img->Read(file => \*$data);
+ $img->set(magick => 'png');
+ $img->Write(file => \*$data);
+ }
+ # $data is a blob.
+ else {
+ $img->BlobToImage($data);
+ $img->set(magick => 'png');
+ $data = $img->ImageToBlob();
+ }
+ undef $img;
+
+ ${$args->{data}} = $data;
+ $args->{attributes}->{mimetype} = 'image/png';
+ $args->{attributes}->{filename} =~ s/^(.+)\.bmp$/$1.png/i;
+}
+
+ __PACKAGE__->NAME;
diff --git a/extensions/BmpConvert/disabled b/extensions/BmpConvert/disabled
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/BmpConvert/disabled
diff --git a/extensions/Example/Config.pm b/extensions/Example/Config.pm
new file mode 100644
index 0000000..b40ed99
--- /dev/null
+++ b/extensions/Example/Config.pm
@@ -0,0 +1,28 @@
+# 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::Example;
+use strict;
+use constant NAME => 'Example';
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Data-Dumper',
+ module => 'Data::Dumper',
+ version => 0,
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+ {
+ package => 'Acme',
+ module => 'Acme',
+ version => 1.11,
+ feature => ['example_acme'],
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm
new file mode 100644
index 0000000..08a5144
--- /dev/null
+++ b/extensions/Example/Extension.pm
@@ -0,0 +1,944 @@
+# 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::Example;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::User;
+use Bugzilla::User::Setting;
+use Bugzilla::Util qw(diff_arrays html_quote);
+use Bugzilla::Status qw(is_open_state);
+use Bugzilla::Install::Filesystem;
+
+# This is extensions/Example/lib/Util.pm. I can load this here in my
+# Extension.pm only because I have a Config.pm.
+use Bugzilla::Extension::Example::Util;
+
+use Data::Dumper;
+
+# See bugmail_relationships.
+use constant REL_EXAMPLE => -127;
+
+our $VERSION = '1.0';
+
+sub admin_editusers_action {
+ my ($self, $args) = @_;
+ my ($vars, $action, $user) = @$args{qw(vars action user)};
+ my $template = Bugzilla->template;
+
+ if ($action eq 'my_action') {
+ # Allow to restrict the search to any group the user is allowed to bless.
+ $vars->{'restrictablegroups'} = $user->bless_groups();
+ $template->process('admin/users/search.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+}
+
+sub attachment_process_data {
+ my ($self, $args) = @_;
+ my $type = $args->{attributes}->{mimetype};
+ my $filename = $args->{attributes}->{filename};
+
+ # Make sure images have the correct extension.
+ # Uncomment the two lines below to make this check effective.
+ if ($type =~ /^image\/(\w+)$/) {
+ my $format = $1;
+ if ($filename =~ /^(.+)(:?\.[^\.]+)$/) {
+ my $name = $1;
+ #$args->{attributes}->{filename} = "${name}.$format";
+ }
+ else {
+ # The file has no extension. We append it.
+ #$args->{attributes}->{filename} .= ".$format";
+ }
+ }
+}
+
+sub auth_login_methods {
+ my ($self, $args) = @_;
+ my $modules = $args->{modules};
+ if (exists $modules->{Example}) {
+ $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Login.pm';
+ }
+}
+
+sub auth_verify_methods {
+ my ($self, $args) = @_;
+ my $modules = $args->{modules};
+ if (exists $modules->{Example}) {
+ $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Verify.pm';
+ }
+}
+
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+
+ my ($bug, $field, $new_value, $old_value, $priv_results)
+ = @$args{qw(bug field new_value old_value priv_results)};
+
+ my $user = Bugzilla->user;
+
+ # Disallow a bug from being reopened if currently closed unless user
+ # is in 'admin' group
+ if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') {
+ if (!is_open_state($old_value) && is_open_state($new_value)
+ && !$user->in_group('admin'))
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ return;
+ }
+ }
+
+ # Disallow a bug's keywords from being edited unless user is the
+ # reporter of the bug
+ if ($field eq 'keywords' && $bug->product_obj->name eq 'Example'
+ && $user->login ne $bug->reporter->login)
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER);
+ return;
+ }
+
+ # Allow updating of priority even if user cannot normally edit the bug
+ # and they are in group 'engineering'
+ if ($field eq 'priority' && $bug->product_obj->name eq 'Example'
+ && $user->in_group('engineering'))
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ return;
+ }
+}
+
+sub bug_columns {
+ my ($self, $args) = @_;
+ my $columns = $args->{'columns'};
+ push (@$columns, "delta_ts AS example")
+}
+
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $bug = $args->{'bug'};
+ my $timestamp = $args->{'timestamp'};
+
+ my $bug_id = $bug->id;
+ # Uncomment this line to see a line in your webserver's error log whenever
+ # you file a bug.
+ # warn "Bug $bug_id has been filed!";
+}
+
+sub bug_end_of_create_validators {
+ my ($self, $args) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $bug_params = $args->{'params'};
+
+ # Uncomment this line below to see a line in your webserver's error log
+ # containing all validated bug field values every time you file a bug.
+ # warn Dumper($bug_params);
+
+ # This would remove all ccs from the bug, preventing ANY ccs from being
+ # added on bug creation.
+ # $bug_params->{cc} = [];
+}
+
+sub bug_start_of_update {
+ my ($self, $args) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my ($bug, $old_bug, $timestamp, $changes) =
+ @$args{qw(bug old_bug timestamp changes)};
+
+ foreach my $field (keys %$changes) {
+ my $used_to_be = $changes->{$field}->[0];
+ my $now_it_is = $changes->{$field}->[1];
+ }
+
+ my $old_summary = $old_bug->short_desc;
+
+ my $status_message;
+ if (my $status_change = $changes->{'bug_status'}) {
+ my $old_status = new Bugzilla::Status({ name => $status_change->[0] });
+ my $new_status = new Bugzilla::Status({ name => $status_change->[1] });
+ if ($new_status->is_open && !$old_status->is_open) {
+ $status_message = "Bug re-opened!";
+ }
+ if (!$new_status->is_open && $old_status->is_open) {
+ $status_message = "Bug closed!";
+ }
+ }
+
+ my $bug_id = $bug->id;
+ my $num_changes = scalar keys %$changes;
+ my $result = "There were $num_changes changes to fields on bug $bug_id"
+ . " at $timestamp.";
+ # Uncomment this line to see $result in your webserver's error log whenever
+ # you update a bug.
+ # warn $result;
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my ($bug, $old_bug, $timestamp, $changes) =
+ @$args{qw(bug old_bug timestamp changes)};
+
+ foreach my $field (keys %$changes) {
+ my $used_to_be = $changes->{$field}->[0];
+ my $now_it_is = $changes->{$field}->[1];
+ }
+
+ my $old_summary = $old_bug->short_desc;
+
+ my $status_message;
+ if (my $status_change = $changes->{'bug_status'}) {
+ my $old_status = new Bugzilla::Status({ name => $status_change->[0] });
+ my $new_status = new Bugzilla::Status({ name => $status_change->[1] });
+ if ($new_status->is_open && !$old_status->is_open) {
+ $status_message = "Bug re-opened!";
+ }
+ if (!$new_status->is_open && $old_status->is_open) {
+ $status_message = "Bug closed!";
+ }
+ }
+
+ my $bug_id = $bug->id;
+ my $num_changes = scalar keys %$changes;
+ my $result = "There were $num_changes changes to fields on bug $bug_id"
+ . " at $timestamp.";
+ # Uncomment this line to see $result in your webserver's error log whenever
+ # you update a bug.
+ # warn $result;
+}
+
+sub bug_fields {
+ my ($self, $args) = @_;
+
+ my $fields = $args->{'fields'};
+ push (@$fields, "example")
+}
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+
+ # This replaces every occurrence of the word "foo" with the word
+ # "bar"
+
+ my $regexes = $args->{'regexes'};
+ push(@$regexes, { match => qr/\bfoo\b/, replace => 'bar' });
+
+ # And this links every occurrence of the word "bar" to example.com,
+ # but it won't affect "foo"s that have already been turned into "bar"
+ # above (because each regex is run in order, and later regexes don't modify
+ # earlier matches, due to some cleverness in Bugzilla's internals).
+ #
+ # For example, the phrase "foo bar" would become:
+ # bar <a href="http://example.com/bar">bar</a>
+ my $bar_match = qr/\b(bar)\b/;
+ push(@$regexes, { match => $bar_match, replace => \&_replace_bar });
+}
+
+# Used by bug_format_comment--see its code for an explanation.
+sub _replace_bar {
+ my $args = shift;
+ # $match is the first parentheses match in the $bar_match regex
+ # in bug-format_comment.pl. We get up to 10 regex matches as
+ # arguments to this function.
+ my $match = $args->{matches}->[0];
+ # Remember, you have to HTML-escape any data that you are returning!
+ $match = html_quote($match);
+ return qq{<a href="http://example.com/">$match</a>};
+};
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+
+ my $columns = $args->{'columns'};
+ $columns->{'example'} = { 'name' => 'bugs.delta_ts' , 'title' => 'Example' };
+ $columns->{'product_desc'} = { 'name' => 'prod_desc.description',
+ 'title' => 'Product Description' };
+}
+
+sub buglist_column_joins {
+ my ($self, $args) = @_;
+ my $joins = $args->{'column_joins'};
+
+ # This column is added using the "buglist_columns" hook
+ $joins->{'product_desc'} = {
+ from => 'product_id',
+ to => 'id',
+ table => 'products',
+ as => 'prod_desc',
+ join => 'INNER',
+ };
+}
+
+sub search_operator_field_override {
+ my ($self, $args) = @_;
+
+ my $operators = $args->{'operators'};
+
+ my $original = $operators->{component}->{_non_changed};
+ $operators->{component} = {
+ _non_changed => sub { _component_nonchanged($original, @_) }
+ };
+}
+
+sub _component_nonchanged {
+ my $original = shift;
+ my ($invocant, $args) = @_;
+
+ $invocant->$original($args);
+ # Actually, it does not change anything in the result,
+ # just an example.
+ $args->{term} = $args->{term} . " OR 1=2";
+}
+
+sub bugmail_recipients {
+ my ($self, $args) = @_;
+ my $recipients = $args->{recipients};
+ my $bug = $args->{bug};
+
+ my $user =
+ new Bugzilla::User({ name => Bugzilla->params->{'maintainer'} });
+
+ if ($bug->id == 1) {
+ # Uncomment the line below to add the maintainer to the recipients
+ # list of every bugmail from bug 1 as though that the maintainer
+ # were on the CC list.
+ #$recipients->{$user->id}->{+REL_CC} = 1;
+
+ # And this line adds the maintainer as though he had the "REL_EXAMPLE"
+ # relationship from the bugmail_relationships hook below.
+ #$recipients->{$user->id}->{+REL_EXAMPLE} = 1;
+ }
+}
+
+sub bugmail_relationships {
+ my ($self, $args) = @_;
+ my $relationships = $args->{relationships};
+ $relationships->{+REL_EXAMPLE} = 'Example';
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+
+ my $modules = $args->{panel_modules};
+ $modules->{Example} = "Bugzilla::Extension::Example::Config";
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+
+ my $panels = $args->{panels};
+
+ # Add the "Example" auth methods.
+ my $auth_params = $panels->{'auth'}->{params};
+ my ($info_class) = grep($_->{name} eq 'user_info_class', @$auth_params);
+ my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params);
+
+ push(@{ $info_class->{choices} }, 'CGI,Example');
+ push(@{ $verify_class->{choices} }, 'Example');
+
+ push(@$auth_params, { name => 'param_example',
+ type => 't',
+ default => 0,
+ checker => \&check_numeric });
+}
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+# $args->{'schema'}->{'example_table'} = {
+# FIELDS => [
+# id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
+# PRIMARYKEY => 1},
+# for_key => {TYPE => 'INT3', NOTNULL => 1,
+# REFERENCES => {TABLE => 'example_table2',
+# COLUMN => 'id',
+# DELETE => 'CASCADE'}},
+# col_3 => {TYPE => 'varchar(64)', NOTNULL => 1},
+# ],
+# INDEXES => [
+# id_index_idx => {FIELDS => ['col_3'], TYPE => 'UNIQUE'},
+# for_id_idx => ['for_key'],
+# ],
+# };
+}
+
+sub email_in_before_parse {
+ my ($self, $args) = @_;
+
+ my $subject = $args->{mail}->header('Subject');
+ # Correctly extract the bug ID from email subjects of the form [Bug comp/NNN].
+ if ($subject =~ /\[.*(\d+)\].*/) {
+ $args->{fields}->{bug_id} = $1;
+ }
+}
+
+sub email_in_after_parse {
+ my ($self, $args) = @_;
+ my $reporter = $args->{fields}->{reporter};
+ my $dbh = Bugzilla->dbh;
+
+ # No other check needed if this is a valid regular user.
+ return if login_to_id($reporter);
+
+ # The reporter is not a regular user. We create an account for him,
+ # but he can only comment on existing bugs.
+ # This is useful for people who reply by email to bugmails received
+ # in mailing-lists.
+ if ($args->{fields}->{bug_id}) {
+ # WARNING: we return now to skip the remaining code below.
+ # You must understand that removing this line would make the code
+ # below effective! Do it only if you are OK with the behavior
+ # described here.
+ return;
+
+ Bugzilla::User->create({ login_name => $reporter, cryptpassword => '*' });
+
+ # For security reasons, delete all fields unrelated to comments.
+ foreach my $field (keys %{$args->{fields}}) {
+ next if $field =~ /^(?:bug_id|comment|reporter)$/;
+ delete $args->{fields}->{$field};
+ }
+ }
+ else {
+ ThrowUserError('invalid_username', { name => $reporter });
+ }
+}
+
+sub enter_bug_entrydefaultvars {
+ my ($self, $args) = @_;
+
+ my $vars = $args->{vars};
+ $vars->{'example'} = 1;
+}
+
+sub error_catch {
+ my ($self, $args) = @_;
+ # Customize the error message displayed when someone tries to access
+ # page.cgi with an invalid page ID, and keep track of this attempt
+ # in the web server log.
+ return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE;
+ return unless $args->{error} eq 'bad_page_cgi_id';
+
+ my $page_id = $args->{vars}->{page_id};
+ my $login = Bugzilla->user->identity || "Someone";
+ warn "$login attempted to access page.cgi with id = $page_id";
+
+ my $page = $args->{message};
+ my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!";
+ $new_error_msg = html_quote($new_error_msg);
+ # There are better tools to parse an HTML page, but it's just an example.
+ # Since Perl 5.16, we can no longer write "class" inside look-behind
+ # assertions, because "ss" is also seen as the german ß character, which
+ # makes Perl 5.16 complain. The right fix is to use the /aa modifier,
+ # but it's only understood since Perl 5.14. So the workaround is to write
+ # "clas[s]" instead of "class". Stupid and ugly hack, but it works with
+ # all Perl versions.
+ $$page =~ s/(?<=<td id="error_msg" clas[s]="throw_error">).*(?=<\/td>)/$new_error_msg/si;
+}
+
+sub flag_end_of_update {
+ my ($self, $args) = @_;
+
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $flag_params = $args;
+ my ($object, $timestamp, $old_flags, $new_flags) =
+ @$flag_params{qw(object timestamp old_flags new_flags)};
+ my ($removed, $added) = diff_arrays($old_flags, $new_flags);
+ my ($granted, $denied) = (0, 0);
+ foreach my $new_flag (@$added) {
+ $granted++ if $new_flag =~ /\+$/;
+ $denied++ if $new_flag =~ /-$/;
+ }
+ my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id
+ : $object->bug_id;
+ my $result = "$granted flags were granted and $denied flags were denied"
+ . " on bug $bug_id at $timestamp.";
+ # Uncomment this line to see $result in your webserver's error log whenever
+ # you update flags.
+ # warn $result;
+}
+
+sub group_before_delete {
+ my ($self, $args) = @_;
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+
+ my $group = $args->{'group'};
+ my $group_id = $group->id;
+ # Uncomment this line to see a line in your webserver's error log whenever
+ # you file a bug.
+ # warn "Group $group_id is about to be deleted!";
+}
+
+sub group_end_of_create {
+ my ($self, $args) = @_;
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+ my $group = $args->{'group'};
+
+ my $group_id = $group->id;
+ # Uncomment this line to see a line in your webserver's error log whenever
+ # you create a new group.
+ #warn "Group $group_id has been created!";
+}
+
+sub group_end_of_update {
+ my ($self, $args) = @_;
+ # This code doesn't actually *do* anything, it's just here to show you
+ # how to use this hook.
+
+ my ($group, $changes) = @$args{qw(group changes)};
+
+ foreach my $field (keys %$changes) {
+ my $used_to_be = $changes->{$field}->[0];
+ my $now_it_is = $changes->{$field}->[1];
+ }
+
+ my $group_id = $group->id;
+ my $num_changes = scalar keys %$changes;
+ my $result =
+ "There were $num_changes changes to fields on group $group_id.";
+ # Uncomment this line to see $result in your webserver's error log whenever
+ # you update a group.
+ #warn $result;
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ print "Install-before_final_checks hook\n" unless $args->{silent};
+
+ # Add a new user setting like this:
+ #
+ # add_setting('product_chooser', # setting name
+ # ['pretty', 'full', 'small'], # options
+ # 'pretty'); # default
+ #
+ # To add descriptions for the setting and choices, add extra values to
+ # the hash defined in global/setting-descs.none.tmpl. Do this in a hook:
+ # hook/global/setting-descs-settings.none.tmpl .
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $create_dirs = $args->{'create_dirs'};
+ my $recurse_dirs = $args->{'recurse_dirs'};
+ my $htaccess = $args->{'htaccess'};
+
+ # Create a new directory in datadir specifically for this extension.
+ # The directory will need to allow files to be created by the extension
+ # code as well as allow the webserver to server content from it.
+ # my $data_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME;
+ # $create_dirs->{$data_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE;
+
+ # Update the permissions of any files and directories that currently reside
+ # in the extension's directory.
+ # $recurse_dirs->{$data_path} = {
+ # files => Bugzilla::Install::Filesystem::CGI_READ,
+ # dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE
+ # };
+
+ # Create a htaccess file that allows specific content to be served from the
+ # extension's directory.
+ # $htaccess->{"$data_path/.htaccess"} = {
+ # perms => Bugzilla::Install::Filesystem::WS_SERVE,
+ # contents => Bugzilla::Install::Filesystem::HT_DEFAULT_DENY
+ # };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+# $dbh->bz_add_column('example', 'new_column',
+# {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
+# $dbh->bz_add_index('example', 'example_new_column_idx', [qw(value)]);
+}
+
+sub install_update_db_fielddefs {
+ my $dbh = Bugzilla->dbh;
+# $dbh->bz_add_column('fielddefs', 'example_column',
+# {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => ''});
+}
+
+sub job_map {
+ my ($self, $args) = @_;
+
+ my $job_map = $args->{job_map};
+
+ # This adds the named class (an instance of TheSchwartz::Worker) as a
+ # handler for when a job is added with the name "some_task".
+ $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass';
+
+ # Schedule a job like this:
+ # my $queue = Bugzilla->job_queue();
+ # $queue->insert('some_task', { some_parameter => $some_variable });
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+
+ my $email = $args->{email};
+ # If you add a header to an email, it's best to start it with
+ # 'X-Bugzilla-<Extension>' so that you don't conflict with
+ # other extensions.
+ $email->header_set('X-Bugzilla-Example-Header', 'Example');
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+
+ my $class = $args->{'class'};
+ my $object_params = $args->{'params'};
+
+ # Note that this is a made-up class, for this example.
+ if ($class->isa('Bugzilla::ExampleObject')) {
+ warn "About to create an ExampleObject!";
+ warn "Got the following parameters: "
+ . join(', ', keys(%$object_params));
+ }
+}
+
+sub object_before_delete {
+ my ($self, $args) = @_;
+
+ my $object = $args->{'object'};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ my $id = $object->id;
+ warn "An object with id $id is about to be deleted!";
+ }
+}
+
+sub object_before_set {
+ my ($self, $args) = @_;
+
+ my ($object, $field, $value) = @$args{qw(object field value)};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ warn "The field $field is changing from " . $object->{$field}
+ . " to $value!";
+ }
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+
+ if ($class->isa('Bugzilla::ExampleObject')) {
+ push(@$columns, 'example');
+ }
+}
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+
+ my $class = $args->{'class'};
+ my $object = $args->{'object'};
+
+ warn "Created a new $class object!";
+}
+
+sub object_end_of_create_validators {
+ my ($self, $args) = @_;
+
+ my $class = $args->{'class'};
+ my $object_params = $args->{'params'};
+
+ # Note that this is a made-up class, for this example.
+ if ($class->isa('Bugzilla::ExampleObject')) {
+ # Always set example_field to 1, even if the validators said otherwise.
+ $object_params->{example_field} = 1;
+ }
+
+}
+
+sub object_end_of_set {
+ my ($self, $args) = @_;
+
+ my ($object, $field) = @$args{qw(object field)};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ warn "The field $field has changed to " . $object->{$field};
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+
+ my $object = $args->{'object'};
+ my $object_params = $args->{'params'};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ if ($object_params->{example_field} == 1) {
+ $object->{example_field} = 1;
+ }
+ }
+
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+
+ my ($object, $old_object, $changes) =
+ @$args{qw(object old_object changes)};
+
+ # Note that this is a made-up class, for this example.
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ if (defined $changes->{'name'}) {
+ my ($old, $new) = @{ $changes->{'name'} };
+ print "The name field changed from $old to $new!";
+ }
+ }
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+
+ if ($object->isa('Bugzilla::ExampleObject')) {
+ push(@$columns, 'example');
+ }
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ my ($class, $validators) = @$args{qw(class validators)};
+
+ if ($class->isa('Bugzilla::Bug')) {
+ # This is an example of adding a new validator.
+ # See the _check_example subroutine below.
+ $validators->{example} = \&_check_example;
+
+ # This is an example of overriding an existing validator.
+ # See the check_short_desc validator below.
+ my $original = $validators->{short_desc};
+ $validators->{short_desc} = sub { _check_short_desc($original, @_) };
+ }
+}
+
+sub _check_example {
+ my ($invocant, $value, $field) = @_;
+ warn "I was called to validate the value of $field.";
+ warn "The value of $field that I was passed in is: $value";
+
+ # Make the value always be 1.
+ my $fixed_value = 1;
+ return $fixed_value;
+}
+
+sub _check_short_desc {
+ my $original = shift;
+ my $invocant = shift;
+ my $value = $invocant->$original(@_);
+ if ($value !~ /example/i) {
+ # Use this line to make Bugzilla throw an error every time
+ # you try to file a bug or update a bug without the word "example"
+ # in the summary.
+ if (0) {
+ ThrowUserError('example_short_desc_invalid');
+ }
+ }
+ return $value;
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ my ($vars, $page) = @$args{qw(vars page_id)};
+
+ # You can see this hook in action by loading page.cgi?id=example.html
+ if ($page eq 'example.html') {
+ $vars->{cgi_variables} = { Bugzilla->cgi->Vars };
+ }
+}
+
+sub path_info_whitelist {
+ my ($self, $args) = @_;
+ my $whitelist = $args->{whitelist};
+ push(@$whitelist, "page.cgi");
+}
+
+sub post_bug_after_creation {
+ my ($self, $args) = @_;
+
+ my $vars = $args->{vars};
+ $vars->{'example'} = 1;
+}
+
+sub product_confirm_delete {
+ my ($self, $args) = @_;
+
+ my $vars = $args->{vars};
+ $vars->{'example'} = 1;
+}
+
+
+sub product_end_of_create {
+ my ($self, $args) = @_;
+
+ my $product = $args->{product};
+
+ # For this example, any lines of code that actually make changes to your
+ # database have been commented out.
+
+ # This section will take a group that exists in your installation
+ # (possible called test_group) and automatically makes the new
+ # product hidden to only members of the group. Just remove
+ # the restriction if you want the new product to be public.
+
+ my $example_group = new Bugzilla::Group({ name => 'example_group' });
+
+ if ($example_group) {
+ $product->set_group_controls($example_group,
+ { entry => 1,
+ membercontrol => CONTROLMAPMANDATORY,
+ othercontrol => CONTROLMAPMANDATORY });
+# $product->update();
+ }
+
+ # This section will automatically add a default component
+ # to the new product called 'No Component'.
+
+ my $default_assignee = new Bugzilla::User(
+ { name => Bugzilla->params->{maintainer} });
+
+ if ($default_assignee) {
+# Bugzilla::Component->create(
+# { name => 'No Component',
+# product => $product,
+# description => 'Select this component if one does not ' .
+# 'exist in the current list of components',
+# initialowner => $default_assignee });
+ }
+}
+
+sub quicksearch_map {
+ my ($self, $args) = @_;
+ my $map = $args->{'map'};
+
+ # This demonstrates adding a shorter alias for a long custom field name.
+ $map->{'impact'} = $map->{'cf_long_field_name_for_impact_field'};
+}
+
+sub sanitycheck_check {
+ my ($self, $args) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $sth;
+
+ my $status = $args->{'status'};
+
+ # Check that all users are Australian
+ $status->('example_check_au_user');
+
+ $sth = $dbh->prepare("SELECT userid, login_name
+ FROM profiles
+ WHERE login_name NOT LIKE '%.au'");
+ $sth->execute;
+
+ my $seen_nonau = 0;
+ while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) {
+ $status->('example_check_au_user_alert',
+ { userid => $userid, login => $login },
+ 'alert');
+ $seen_nonau = 1;
+ }
+
+ $status->('example_check_au_user_prompt') if $seen_nonau;
+}
+
+sub sanitycheck_repair {
+ my ($self, $args) = @_;
+
+ my $cgi = Bugzilla->cgi;
+ my $dbh = Bugzilla->dbh;
+
+ my $status = $args->{'status'};
+
+ if ($cgi->param('example_repair_au_user')) {
+ $status->('example_repair_au_user_start');
+
+ #$dbh->do("UPDATE profiles
+ # SET login_name = CONCAT(login_name, '.au')
+ # WHERE login_name NOT LIKE '%.au'");
+
+ $status->('example_repair_au_user_end');
+ }
+}
+
+sub template_before_create {
+ my ($self, $args) = @_;
+
+ my $config = $args->{'config'};
+ # This will be accessible as "example_global_variable" in every
+ # template in Bugzilla. See Bugzilla/Template.pm's create() function
+ # for more things that you can set.
+ $config->{VARIABLES}->{example_global_variable} = sub { return 'value' };
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+
+ my ($vars, $file, $context) = @$args{qw(vars file context)};
+
+ if ($file eq 'bug/edit.html.tmpl') {
+ $vars->{'viewing_the_bug_form'} = 1;
+ }
+}
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{current_tab};
+ my $save = $args->{save_changes};
+ my $handled = $args->{handled};
+
+ return unless $tab eq 'my_tab';
+
+ my $value = Bugzilla->input_params->{'example_pref'};
+ if ($save) {
+ # Validate your data and update the DB accordingly.
+ $value =~ s/\s+/:/g;
+ }
+ $args->{'vars'}->{example_pref} = $value;
+
+ # Set the 'handled' scalar reference to true so that the caller
+ # knows the panel name is valid and that an extension took care of it.
+ $$handled = 1;
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+
+ my $dispatch = $args->{dispatch};
+ $dispatch->{Example} = "Bugzilla::Extension::Example::WebService";
+}
+
+sub webservice_error_codes {
+ my ($self, $args) = @_;
+
+ my $error_map = $args->{error_map};
+ $error_map->{'example_my_error'} = 10001;
+}
+
+# This must be the last line of your extension.
+__PACKAGE__->NAME;
diff --git a/extensions/Example/disabled b/extensions/Example/disabled
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/Example/disabled
diff --git a/extensions/Example/lib/Auth/Login.pm b/extensions/Example/lib/Auth/Login.pm
new file mode 100644
index 0000000..f878311
--- /dev/null
+++ b/extensions/Example/lib/Auth/Login.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::Example::Auth::Login;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+use constant user_can_create_account => 0;
+use Bugzilla::Constants;
+
+# Always returns no data.
+sub get_login_info {
+ return { failure => AUTH_NODATA };
+}
+
+1;
diff --git a/extensions/Example/lib/Auth/Verify.pm b/extensions/Example/lib/Auth/Verify.pm
new file mode 100644
index 0000000..0d068b2
--- /dev/null
+++ b/extensions/Example/lib/Auth/Verify.pm
@@ -0,0 +1,18 @@
+# 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::Example::Auth::Verify;
+use strict;
+use base qw(Bugzilla::Auth::Verify);
+use Bugzilla::Constants;
+
+# A verifier that always fails.
+sub check_credentials {
+ return { failure => AUTH_NO_SUCH_USER };
+}
+
+1;
diff --git a/extensions/Example/lib/Config.pm b/extensions/Example/lib/Config.pm
new file mode 100644
index 0000000..b0497a7
--- /dev/null
+++ b/extensions/Example/lib/Config.pm
@@ -0,0 +1,29 @@
+# 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::Example::Config;
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 5000;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {
+ name => 'example_string',
+ type => 't',
+ default => 'EXAMPLE',
+ },
+ );
+ return @param_list;
+}
+
+1;
diff --git a/extensions/Example/lib/Util.pm b/extensions/Example/lib/Util.pm
new file mode 100644
index 0000000..b4ed3e0
--- /dev/null
+++ b/extensions/Example/lib/Util.pm
@@ -0,0 +1,15 @@
+# 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::Example::Util;
+use strict;
+use warnings;
+
+# This file exists only to demonstrate how to use and name your
+# modules in an extension.
+
+1;
diff --git a/extensions/Example/lib/WebService.pm b/extensions/Example/lib/WebService.pm
new file mode 100644
index 0000000..659189d
--- /dev/null
+++ b/extensions/Example/lib/WebService.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::Example::WebService;
+use strict;
+use warnings;
+use base qw(Bugzilla::WebService);
+use Bugzilla::Error;
+
+# This can be called as Example.hello() from the WebService.
+sub hello { return 'Hello!'; }
+
+sub throw_an_error { ThrowUserError('example_my_error') }
+
+1;
diff --git a/extensions/Example/template/en/default/account/prefs/my_tab.html.tmpl b/extensions/Example/template/en/default/account/prefs/my_tab.html.tmpl
new file mode 100644
index 0000000..d2f1dcd
--- /dev/null
+++ b/extensions/Example/template/en/default/account/prefs/my_tab.html.tmpl
@@ -0,0 +1,18 @@
+[%# 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.
+ #%]
+
+<p>
+ Type some short text in the field below. Whitespaces will be replaced
+ by colons.
+</p>
+
+<p>
+ <label for="example_pref">Short text:</label>
+ <input type="text" id="example_pref" name="example_pref" size="30"
+ maxlength="50" value="[% example_pref FILTER html %]">
+</p>
diff --git a/extensions/Example/template/en/default/admin/params/example.html.tmpl b/extensions/Example/template/en/default/admin/params/example.html.tmpl
new file mode 100644
index 0000000..4814394
--- /dev/null
+++ b/extensions/Example/template/en/default/admin/params/example.html.tmpl
@@ -0,0 +1,16 @@
+[%# 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.
+ #%]
+[%
+ title = "Example Extension"
+ desc = "Configure example extension"
+%]
+
+[% param_descs = {
+ example_string => "Example string",
+}
+%]
diff --git a/extensions/Example/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/Example/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 0000000..202dd1e
--- /dev/null
+++ b/extensions/Example/template/en/default/hook/account/prefs/prefs-tabs.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.
+ #%]
+
+[% tabs = tabs.import([{ name => "my_tab", label => "Example Custom Preferences",
+ link => "userprefs.cgi?tab=my_tab", saveable => 1 }
+ ]) %]
diff --git a/extensions/Example/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/Example/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl
new file mode 100644
index 0000000..42826e9
--- /dev/null
+++ b/extensions/Example/template/en/default/hook/admin/params/editparams-current_panel.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 panel.name == "auth" %]
+ [% panel.param_descs.param_example = 'Example new parameter' %]
+[% END -%]
diff --git a/extensions/Example/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl b/extensions/Example/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
new file mode 100644
index 0000000..9cc4551
--- /dev/null
+++ b/extensions/Example/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
@@ -0,0 +1,23 @@
+[%# 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 san_tag == "example_check_au_user" %]
+ <em>EXAMPLE PLUGIN</em> - Checking for non-Australian users.
+[% ELSIF san_tag == "example_check_au_user_alert" %]
+ User &lt;[% login FILTER html %]&gt; isn't Australian.
+ [% IF user.in_group('editusers') %]
+ <a href="editusers.cgi?id=[% userid FILTER none %]">Edit this user</a>.
+ [% END %]
+[% ELSIF san_tag == "example_check_au_user_prompt" %]
+ <a href="sanitycheck.cgi?example_repair_au_user=1&amp;token=
+ [%- issue_hash_token(['sanitycheck']) FILTER uri %]">Fix these users</a>.
+[% ELSIF san_tag == "example_repair_au_user_start" %]
+ <em>EXAMPLE PLUGIN</em> - OK, would now make users Australian.
+[% ELSIF san_tag == "example_repair_au_user_end" %]
+ <em>EXAMPLE PLUGIN</em> - Users would now be Australian.
+[% END %]
diff --git a/extensions/Example/template/en/default/hook/global/footer-end.html.tmpl b/extensions/Example/template/en/default/hook/global/footer-end.html.tmpl
new file mode 100644
index 0000000..f4cb1e0
--- /dev/null
+++ b/extensions/Example/template/en/default/hook/global/footer-end.html.tmpl
@@ -0,0 +1,16 @@
+[%# 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 date %]
+
+<p align="center">
+ <em>[% component.callers.first FILTER html %]</em> processed
+ on [% date.format(date.now, '%b %d, %Y at %H:%M:%S') FILTER html %].
+ <br>
+ (provided by the Example extension).
+</p>
diff --git a/extensions/Example/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/Example/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 0000000..7077f26
--- /dev/null
+++ b/extensions/Example/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,14 @@
+[%# 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.
+ #%]
+
+[%
+ setting_descs.product_chooser = "Product chooser to use when entering $terms.bugs",
+ setting_descs.pretty = "Pretty chooser with common products and icons",
+ setting_descs.full = "Full chooser with all products",
+ setting_descs.small = "Product chooser for mobile devices",
+%]
diff --git a/extensions/Example/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Example/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 0000000..33c77e4
--- /dev/null
+++ b/extensions/Example/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,14 @@
+[%# 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 == "example_my_error" %]
+ [% title = "Example Error Title" %]
+ This is the error message! It contains <em>some html</em>.
+[% ELSIF error == "example_short_desc_invalid" %]
+ [% title = "Bad Summary" %]
+ The Summary must contain the word "example".
+[% END %]
diff --git a/extensions/Example/template/en/default/pages/example.html.tmpl b/extensions/Example/template/en/default/pages/example.html.tmpl
new file mode 100644
index 0000000..fda74bf
--- /dev/null
+++ b/extensions/Example/template/en/default/pages/example.html.tmpl
@@ -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.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Example Page"
+%]
+
+<p>Here's what you passed me:</p>
+[% USE Dumper %]
+<pre>
+ [% Dumper.dump_html(cgi_variables) FILTER none %]
+</pre>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Example/template/en/default/setup/strings.txt.pl b/extensions/Example/template/en/default/setup/strings.txt.pl
new file mode 100644
index 0000000..9855054
--- /dev/null
+++ b/extensions/Example/template/en/default/setup/strings.txt.pl
@@ -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.
+
+%strings = (
+ feature_example_acme => 'Example Extension: Acme Feature',
+);
+
+1;
diff --git a/extensions/MoreBugUrl/Config.pm b/extensions/MoreBugUrl/Config.pm
new file mode 100644
index 0000000..b5af9c0
--- /dev/null
+++ b/extensions/MoreBugUrl/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::MoreBugUrl;
+use strict;
+
+use constant NAME => 'MoreBugUrl';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/MoreBugUrl/Extension.pm b/extensions/MoreBugUrl/Extension.pm
new file mode 100644
index 0000000..52b6e61
--- /dev/null
+++ b/extensions/MoreBugUrl/Extension.pm
@@ -0,0 +1,46 @@
+# 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::MoreBugUrl;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use constant MORE_SUB_CLASSES => qw(
+ Bugzilla::Extension::MoreBugUrl::ReviewBoard
+ Bugzilla::Extension::MoreBugUrl::Rietveld
+ Bugzilla::Extension::MoreBugUrl::RT
+ Bugzilla::Extension::MoreBugUrl::GetSatisfaction
+ Bugzilla::Extension::MoreBugUrl::PHP
+);
+
+# We need to update bug_see_also table because both
+# Rietveld and ReviewBoard were originally under Bugzilla/BugUrl/.
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+
+ my $should_rename = $dbh->selectrow_array(
+ q{SELECT 1 FROM bug_see_also
+ WHERE class IN ('Bugzilla::BugUrl::Rietveld',
+ 'Bugzilla::BugUrl::ReviewBoard')});
+
+ if ($should_rename) {
+ my $sth = $dbh->prepare('UPDATE bug_see_also SET class = ?
+ WHERE class = ?');
+ $sth->execute('Bugzilla::Extension::MoreBugUrl::ReviewBoard',
+ 'Bugzilla::BugUrl::ReviewBoard');
+
+ $sth->execute('Bugzilla::Extension::MoreBugUrl::Rietveld',
+ 'Bugzilla::BugUrl::Rietveld');
+ }
+}
+
+sub bug_url_sub_classes {
+ my ($self, $args) = @_;
+ push @{ $args->{sub_classes} }, MORE_SUB_CLASSES;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/MoreBugUrl/disabled b/extensions/MoreBugUrl/disabled
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/MoreBugUrl/disabled
diff --git a/extensions/MoreBugUrl/lib/GetSatisfaction.pm b/extensions/MoreBugUrl/lib/GetSatisfaction.pm
new file mode 100644
index 0000000..e454856
--- /dev/null
+++ b/extensions/MoreBugUrl/lib/GetSatisfaction.pm
@@ -0,0 +1,37 @@
+# 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::MoreBugUrl::GetSatisfaction;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+
+ # GetSatisfaction URLs only have one form:
+ # http(s)://getsatisfaction.com/PROJECT_NAME/topics/TOPIC_NAME
+ return (lc($uri->authority) eq 'getsatisfaction.com'
+ and $uri->path =~ m|^/[^/]+/topics/[^/]+$|) ? 1 : 0;
+}
+
+sub _check_value {
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ # GetSatisfaction HTTP URLs redirect to HTTPS, so just use the HTTPS
+ # scheme.
+ $uri->scheme('https');
+
+ return $uri;
+}
+
+1;
diff --git a/extensions/MoreBugUrl/lib/PHP.pm b/extensions/MoreBugUrl/lib/PHP.pm
new file mode 100644
index 0000000..c17a499
--- /dev/null
+++ b/extensions/MoreBugUrl/lib/PHP.pm
@@ -0,0 +1,40 @@
+# 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::MoreBugUrl::PHP;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+
+ # PHP Bug URLs have only one form:
+ # https://bugs.php.net/bug.php?id=1234
+ return (lc($uri->authority) eq 'bugs.php.net'
+ and $uri->path =~ m|/bug\.php$|
+ and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0;
+}
+
+sub _check_value {
+ my $class = shift;
+
+ my $uri = $class->SUPER::_check_value(@_);
+
+ # PHP Bug URLs redirect to HTTPS, so just use the HTTPS scheme.
+ $uri->scheme('https');
+
+ # And remove any # part if there is one.
+ $uri->fragment(undef);
+
+ return $uri;
+}
+
+1;
diff --git a/extensions/MoreBugUrl/lib/RT.pm b/extensions/MoreBugUrl/lib/RT.pm
new file mode 100644
index 0000000..724c773
--- /dev/null
+++ b/extensions/MoreBugUrl/lib/RT.pm
@@ -0,0 +1,37 @@
+# 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::MoreBugUrl::RT;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+
+ # RT URLs can look like various things:
+ # http://example.com/rt/Ticket/Display.html?id=1234
+ # https://example.com/Public/Bug/Display.html?id=1234
+ return ($uri->path =~ m|/Display\.html$|
+ and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0;
+}
+
+sub _check_value {
+ my $class = shift;
+
+ my $uri = $class->SUPER::_check_value(@_);
+
+ # And remove any # part if there is one.
+ $uri->fragment(undef);
+
+ return $uri;
+}
+
+1;
diff --git a/extensions/MoreBugUrl/lib/ReviewBoard.pm b/extensions/MoreBugUrl/lib/ReviewBoard.pm
new file mode 100644
index 0000000..7628dd3
--- /dev/null
+++ b/extensions/MoreBugUrl/lib/ReviewBoard.pm
@@ -0,0 +1,42 @@
+# 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::MoreBugUrl::ReviewBoard;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+ return ($uri->path =~ m|/r/\d+/?$|) ? 1 : 0;
+}
+
+sub _check_value {
+ my $class = shift;
+
+ my $uri = $class->SUPER::_check_value(@_);
+
+ # Review Board URLs have only one form (the trailing slash is optional):
+ # http://reviews.reviewboard.org/r/111/
+
+ # Make sure there are no query parameters.
+ $uri->query(undef);
+ # And remove any # part if there is one.
+ $uri->fragment(undef);
+
+ # make sure the trailing slash is present
+ if ($uri->path !~ m|/$|) {
+ $uri->path($uri->path . '/');
+ }
+
+ return $uri;
+}
+
+1;
diff --git a/extensions/MoreBugUrl/lib/Rietveld.pm b/extensions/MoreBugUrl/lib/Rietveld.pm
new file mode 100644
index 0000000..0c52892
--- /dev/null
+++ b/extensions/MoreBugUrl/lib/Rietveld.pm
@@ -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.
+
+package Bugzilla::Extension::MoreBugUrl::Rietveld;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+ return ($uri->authority =~ /\.appspot\.com$/i
+ and $uri->path =~ m#^/\d+(?:/|/show)?$#) ? 1 : 0;
+}
+
+sub _check_value {
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ # Rietveld URLs have three forms:
+ # http(s)://example.appspot.com/1234
+ # http(s)://example.appspot.com/1234/
+ # http(s)://example.appspot.com/1234/show
+ if ($uri->path =~ m#^/(\d+)(?:/|/show)$#) {
+ # This is the shortest standard URL form for Rietveld issues,
+ # and so we reduce all URLs to this.
+ $uri->path('/' . $1);
+ }
+
+ # Make sure there are no query parameters.
+ $uri->query(undef);
+ # And remove any # part if there is one.
+ $uri->fragment(undef);
+
+ return $uri;
+}
+
+1;
diff --git a/extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl b/extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl
new file mode 100644
index 0000000..7683e42
--- /dev/null
+++ b/extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.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.
+ #%]
+
+<li>A Review Board review request.</li>
+<li>An issue in a Rietveld installation.</li>
+<li>A ticket in an RT installation.</li>
+<li>A topic on getsatisfaction.com.</li>
+<li>A b[% %]ug on b[% %]ugs.php.net.</li>
diff --git a/extensions/OldBugMove/Config.pm b/extensions/OldBugMove/Config.pm
new file mode 100644
index 0000000..9564861
--- /dev/null
+++ b/extensions/OldBugMove/Config.pm
@@ -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.
+
+package Bugzilla::Extension::OldBugMove;
+use strict;
+use constant NAME => 'OldBugMove';
+__PACKAGE__->NAME;
diff --git a/extensions/OldBugMove/Extension.pm b/extensions/OldBugMove/Extension.pm
new file mode 100644
index 0000000..9a499d5
--- /dev/null
+++ b/extensions/OldBugMove/Extension.pm
@@ -0,0 +1,196 @@
+# 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::OldBugMove;
+use strict;
+use base qw(Bugzilla::Extension);
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field::Choice;
+use Bugzilla::Mailer;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim);
+
+use Scalar::Util qw(blessed);
+use Storable qw(dclone);
+
+use constant VERSION => BUGZILLA_VERSION;
+
+# This is 4 because that's what it originally was when this code was
+# a part of Bugzilla.
+use constant CMT_MOVED_TO => 4;
+
+sub install_update_db {
+ my $reso_type = Bugzilla::Field::Choice->type('resolution');
+ my $moved_reso = $reso_type->new({ name => 'MOVED' });
+ # We make the MOVED resolution inactive, so that it doesn't show up
+ # as a valid drop-down option.
+ if ($moved_reso) {
+ $moved_reso->set_is_active(0);
+ $moved_reso->update();
+ }
+ else {
+ print "Creating the MOVED resolution...\n";
+ $reso_type->create(
+ { value => 'MOVED', sortkey => '30000', isactive => 0 });
+ }
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+ my $modules = $args->{'panel_modules'};
+ $modules->{'OldBugMove'} = 'Bugzilla::Extension::OldBugMove::Params';
+}
+
+sub template_before_create {
+ my ($self, $args) = @_;
+ my $config = $args->{config};
+
+ my $constants = $config->{CONSTANTS};
+ $constants->{CMT_MOVED_TO} = CMT_MOVED_TO;
+
+ my $vars = $config->{VARIABLES};
+ $vars->{oldbugmove_user_is_mover} = \&_user_is_mover;
+}
+
+sub object_before_delete {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+ if ($object->isa('Bugzilla::Field::Choice::resolution')) {
+ if ($object->name eq 'MOVED') {
+ ThrowUserError('oldbugmove_no_delete_moved');
+ }
+ }
+}
+
+sub object_before_set {
+ my ($self, $args) = @_;
+ my ($object, $field) = @$args{qw(object field)};
+ if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) {
+ # Store the old value so that end_of_set can check it.
+ $object->{'_oldbugmove_old_resolution'} = $object->resolution;
+ }
+}
+
+sub object_end_of_set {
+ my ($self, $args) = @_;
+ my ($object, $field) = @$args{qw(object field)};
+ if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) {
+ my $old_value = delete $object->{'_oldbugmove_old_resolution'};
+ return if $old_value eq $object->resolution;
+ if ($object->resolution eq 'MOVED') {
+ $object->add_comment('', { type => CMT_MOVED_TO,
+ extra_data => Bugzilla->user->login });
+ }
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+
+ if ($object->isa('Bugzilla::Bug') and Bugzilla->input_params->{'oldbugmove'}) {
+ my $new_status = Bugzilla->params->{'duplicate_or_move_bug_status'};
+ $object->set_bug_status($new_status, { resolution => 'MOVED' });
+ }
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ my ($class, $validators) = @$args{qw(class validators)};
+ if ($class->isa('Bugzilla::Comment')) {
+ my $extra_data_validator = $validators->{extra_data};
+ $validators->{extra_data} =
+ sub { _check_comment_extra_data($extra_data_validator, @_) };
+ }
+ elsif ($class->isa('Bugzilla::Bug')) {
+ my $reso_validator = $validators->{resolution};
+ $validators->{resolution} =
+ sub { _check_bug_resolution($reso_validator, @_) };
+ }
+}
+
+sub _check_bug_resolution {
+ my $original_validator = shift;
+ my ($invocant, $resolution) = @_;
+
+ if ($resolution eq 'MOVED' && $invocant->resolution ne 'MOVED'
+ && !Bugzilla->input_params->{'oldbugmove'})
+ {
+ # MOVED has a special meaning and can only be used when
+ # really moving bugs to another installation.
+ ThrowUserError('oldbugmove_no_manual_move');
+ }
+
+ return $original_validator->(@_);
+}
+
+sub _check_comment_extra_data {
+ my $original_validator = shift;
+ my ($invocant, $extra_data, undef, $params) = @_;
+ my $type = blessed($invocant) ? $invocant->type : $params->{type};
+
+ if ($type == CMT_MOVED_TO) {
+ return Bugzilla::User->check($extra_data)->login;
+ }
+ return $original_validator->(@_);
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ my ($bug, $old_bug, $changes) = @$args{qw(bug old_bug changes)};
+ if (defined $changes->{'resolution'}
+ and $changes->{'resolution'}->[1] eq 'MOVED')
+ {
+ $self->_move_bug($bug, $old_bug);
+ }
+}
+
+sub _move_bug {
+ my ($self, $bug, $old_bug) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $template = Bugzilla->template;
+
+ _user_is_mover(Bugzilla->user)
+ or ThrowUserError("auth_failure", { action => 'move',
+ object => 'bugs' });
+
+ # Don't export the new status and resolution. We want the current
+ # ones.
+ local $Storable::forgive_me = 1;
+ my $export_me = dclone($bug);
+ $export_me->{bug_status} = $old_bug->bug_status;
+ delete $export_me->{status};
+ $export_me->{resolution} = $old_bug->resolution;
+
+ # Prepare and send all data about these bugs to the new database
+ my $to = Bugzilla->params->{'move-to-address'};
+ $to =~ s/@/\@/;
+ my $from = Bugzilla->params->{'mailfrom'};
+ $from =~ s/@/\@/;
+ my $msg = "To: $to\n";
+ $msg .= "From: Bugzilla <" . $from . ">\n";
+ $msg .= "Subject: Moving bug " . $bug->id . "\n\n";
+ my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc',
+ 'attachment', 'attachmentdata');
+ my %displayfields = map { $_ => 1 } @fieldlist;
+ my $vars = { bugs => [$export_me], displayfields => \%displayfields };
+ $template->process("bug/show.xml.tmpl", $vars, \$msg)
+ || ThrowTemplateError($template->error());
+ $msg .= "\n";
+ MessageToMTA($msg);
+}
+
+sub _user_is_mover {
+ my $user = shift;
+
+ my @movers = map { trim($_) } split(',', Bugzilla->params->{'movers'});
+ return ($user->id and grep($_ eq $user->login, @movers)) ? 1 : 0;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/OldBugMove/disabled b/extensions/OldBugMove/disabled
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/OldBugMove/disabled
diff --git a/extensions/OldBugMove/lib/Params.pm b/extensions/OldBugMove/lib/Params.pm
new file mode 100644
index 0000000..dbb1eb2
--- /dev/null
+++ b/extensions/OldBugMove/lib/Params.pm
@@ -0,0 +1,36 @@
+# 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::OldBugMove::Params;
+
+use strict;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 700;
+
+use constant get_param_list => (
+ {
+ name => 'move-to-url',
+ type => 't',
+ default => ''
+ },
+
+ {
+ name => 'move-to-address',
+ type => 't',
+ default => 'bugzilla-import'
+ },
+
+ {
+ name => 'movers',
+ type => 't',
+ default => ''
+ },
+);
+
+1;
diff --git a/extensions/OldBugMove/template/en/default/admin/params/oldbugmove.html.tmpl b/extensions/OldBugMove/template/en/default/admin/params/oldbugmove.html.tmpl
new file mode 100644
index 0000000..0a54c46
--- /dev/null
+++ b/extensions/OldBugMove/template/en/default/admin/params/oldbugmove.html.tmpl
@@ -0,0 +1,27 @@
+[%# 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.
+ #%]
+[%
+ title = "$terms.Bug Moving"
+ desc = "Set up parameters to move $terms.bugs to/from another installation"
+%]
+
+[% param_descs = {
+
+ "move-to-url" =>
+ "The URL of the database we allow some of our $terms.bugs to"
+ _ " be moved to.",
+
+ "move-to-address" =>
+ "To move ${terms.bugs}, an email is sent to the target database."
+ _ " This is the email address that that database uses to listen"
+ _ " for incoming ${terms.bugs}.",
+
+ movers =>
+ "A list of people with permission to move $terms.bugs ",
+
+} %]
diff --git a/extensions/OldBugMove/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl b/extensions/OldBugMove/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl
new file mode 100644
index 0000000..1490f08
--- /dev/null
+++ b/extensions/OldBugMove/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl
@@ -0,0 +1,15 @@
+[%# 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 oldbugmove_user_is_mover(user) AND bug.resolution != 'MOVED' %]
+ <p>
+ <input type="submit" id="oldbugmove" name="oldbugmove"
+ value="Move [% terms.Bug FILTER html %] to
+ [%= Param('move-to-url') FILTER html %]">
+ </p>
+[% END %]
diff --git a/extensions/OldBugMove/template/en/default/hook/bug/format_comment-type.txt.tmpl b/extensions/OldBugMove/template/en/default/hook/bug/format_comment-type.txt.tmpl
new file mode 100644
index 0000000..afe03af
--- /dev/null
+++ b/extensions/OldBugMove/template/en/default/hook/bug/format_comment-type.txt.tmpl
@@ -0,0 +1,17 @@
+[%# 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 comment.type == constants.CMT_MOVED_TO %]
+[% comment.body %]
+
+[%+ terms.Bug %] moved to [% Param("move-to-url") %].
+If the move succeeded, [% comment.extra_data FILTER email %] will receive a mail
+containing the number of the new [% terms.bug %] in the other database.
+If all went well, please paste in a link to the new [% terms.bug %].
+Otherwise, reopen this [% terms.bug %].
+[% END %]
diff --git a/extensions/OldBugMove/template/en/default/hook/global/user-error-auth_failure_action.html.tmpl b/extensions/OldBugMove/template/en/default/hook/global/user-error-auth_failure_action.html.tmpl
new file mode 100644
index 0000000..1966a1a
--- /dev/null
+++ b/extensions/OldBugMove/template/en/default/hook/global/user-error-auth_failure_action.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 action == "move" %]
+ move
+[% END %]
diff --git a/extensions/OldBugMove/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/OldBugMove/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 0000000..d45026b
--- /dev/null
+++ b/extensions/OldBugMove/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,17 @@
+[%# 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 == "oldbugmove_no_delete_moved" %]
+ As long as the OldBugMove extension is active, you cannot
+ delete the [%+ display_value("resolution", "MOVED") FILTER html %]
+ resolution.
+[% ELSIF error == "oldbugmove_no_manual_move" %]
+ You cannot set the resolution of [% terms.abug %] to
+ [%+ display_value("resolution", "MOVED") FILTER html %] without
+ moving the [% terms.bug %].
+[% END %]
diff --git a/extensions/OldBugMove/template/en/default/hook/list/edit-multiple-after_groups.html.tmpl b/extensions/OldBugMove/template/en/default/hook/list/edit-multiple-after_groups.html.tmpl
new file mode 100644
index 0000000..a5380da
--- /dev/null
+++ b/extensions/OldBugMove/template/en/default/hook/list/edit-multiple-after_groups.html.tmpl
@@ -0,0 +1,15 @@
+[%# 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 oldbugmove_user_is_mover(user) %]
+ <p>
+ <input type="submit" id="oldbugmove" name="oldbugmove"
+ value="Move [% terms.Bugs FILTER html %] to
+ [%= Param('move-to-url') FILTER html %]">
+ </p>
+[% END %]
diff --git a/extensions/Voting/Config.pm b/extensions/Voting/Config.pm
new file mode 100644
index 0000000..4fefe95
--- /dev/null
+++ b/extensions/Voting/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::Voting;
+use strict;
+
+use constant NAME => 'Voting';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
new file mode 100644
index 0000000..d186e44
--- /dev/null
+++ b/extensions/Voting/Extension.pm
@@ -0,0 +1,867 @@
+# 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::Voting;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::BugMail;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Mailer;
+use Bugzilla::User;
+use Bugzilla::Util qw(detaint_natural);
+use Bugzilla::Token;
+
+use List::Util qw(min sum);
+
+use constant VERSION => BUGZILLA_VERSION;
+use constant DEFAULT_VOTES_PER_BUG => 1;
+# These came from Bugzilla itself, so they maintain the old numbers
+# they had before.
+use constant CMT_POPULAR_VOTES => 3;
+use constant REL_VOTER => 4;
+
+################
+# Installation #
+################
+
+BEGIN {
+ *Bugzilla::Bug::votes = \&votes;
+}
+
+sub votes {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $self->{votes} if exists $self->{votes};
+
+ $self->{votes} = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?',
+ undef, $self->id);
+ return $self->{votes};
+}
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'votes'} = {
+ FIELDS => [
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'}},
+ vote_count => {TYPE => 'INT2', NOTNULL => 1},
+ ],
+ INDEXES => [
+ votes_who_idx => ['who'],
+ votes_bug_id_idx => ['bug_id'],
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla,
+ # so updates to the columns for old versions of Bugzilla happen in
+ # Bugzilla::Install::DB, and can't safely be moved to this extension.
+
+ my $field = new Bugzilla::Field({ name => 'votes' });
+ if (!$field) {
+ Bugzilla::Field->create(
+ { name => 'votes', description => 'Votes', buglist => 1 });
+ }
+
+ $dbh->bz_add_column('products', 'votesperuser',
+ {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
+ $dbh->bz_add_column('products', 'maxvotesperbug',
+ {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG});
+ $dbh->bz_add_column('products', 'votestoconfirm',
+ {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
+
+ $dbh->bz_add_column('bugs', 'votes',
+ {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0});
+ $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']);
+
+ # maxvotesperbug used to default to 10,000, which isn't very sensible.
+ my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug');
+ if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) {
+ $dbh->bz_alter_column('products', 'maxvotesperbug',
+ {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG});
+ }
+}
+
+###########
+# Objects #
+###########
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Bug')) {
+ push(@$columns, 'votes');
+ }
+ elsif ($class->isa('Bugzilla::Product')) {
+ push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm));
+ }
+}
+
+sub bug_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{fields};
+ push(@$fields, 'votes');
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my ($object, $columns) = @$args{qw(object columns)};
+ if ($object->isa('Bugzilla::Product')) {
+ push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm));
+ }
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ my ($class, $validators) = @$args{qw(class validators)};
+ if ($class->isa('Bugzilla::Product')) {
+ $validators->{'votesperuser'} = \&_check_votesperuser;
+ $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug;
+ $validators->{'votestoconfirm'} = \&_check_votestoconfirm;
+ }
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my ($class, $params) = @$args{qw(class params)};
+ if ($class->isa('Bugzilla::Bug')) {
+ # Don't ever allow people to directly specify "votes" into the bugs
+ # table.
+ delete $params->{votes};
+ }
+ elsif ($class->isa('Bugzilla::Product')) {
+ my $input = Bugzilla->input_params;
+ $params->{votesperuser} = $input->{'votesperuser'};
+ $params->{maxvotesperbug} = $input->{'maxvotesperbug'};
+ $params->{votestoconfirm} = $input->{'votestoconfirm'};
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my ($object) = $args->{object};
+ if ($object->isa('Bugzilla::Product')) {
+ my $input = Bugzilla->input_params;
+ $object->set('votesperuser', $input->{'votesperuser'});
+ $object->set('maxvotesperbug', $input->{'maxvotesperbug'});
+ $object->set('votestoconfirm', $input->{'votestoconfirm'});
+ }
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my ($object, $changes) = @$args{qw(object changes)};
+ if ( $object->isa('Bugzilla::Product')
+ and ($changes->{maxvotesperbug} or $changes->{votesperuser}
+ or $changes->{votestoconfirm}) )
+ {
+ _modify_bug_votes($object, $changes);
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ my ($bug, $changes) = @$args{qw(bug changes)};
+
+ if ($changes->{'product'}) {
+ my @msgs;
+ # If some votes have been removed, RemoveVotes() returns
+ # a list of messages to send to voters.
+ @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved');
+ _confirm_if_vote_confirmed($bug);
+
+ foreach my $msg (@msgs) {
+ MessageToMTA($msg);
+ }
+ }
+}
+
+#############
+# Templates #
+#############
+
+sub template_before_create {
+ my ($self, $args) = @_;
+ my $config = $args->{config};
+ my $constants = $config->{CONSTANTS};
+ $constants->{REL_VOTER} = REL_VOTER;
+ $constants->{CMT_POPULAR_VOTES} = CMT_POPULAR_VOTES;
+ $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG;
+}
+
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my ($vars, $file) = @$args{qw(vars file)};
+ if ($file eq 'admin/users/confirm-delete.html.tmpl') {
+ my $who = $vars->{otheruser};
+ my $votes = Bugzilla->dbh->selectrow_array(
+ 'SELECT COUNT(*) FROM votes WHERE who = ?', undef, $who->id);
+ if ($votes) {
+ $vars->{other_safe} = 1;
+ $vars->{votes} = $votes;
+ }
+ }
+}
+
+###########
+# Bugmail #
+###########
+
+sub bugmail_recipients {
+ my ($self, $args) = @_;
+ my ($bug, $recipients) = @$args{qw(bug recipients)};
+ my $dbh = Bugzilla->dbh;
+
+ my $voters = $dbh->selectcol_arrayref(
+ "SELECT who FROM votes WHERE bug_id = ?", undef, $bug->id);
+ $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters);
+}
+
+sub bugmail_relationships {
+ my ($self, $args) = @_;
+ my $relationships = $args->{relationships};
+ $relationships->{+REL_VOTER} = 'Voter';
+}
+
+###############
+# Sanitycheck #
+###############
+
+sub sanitycheck_check {
+ my ($self, $args) = @_;
+ my $status = $args->{status};
+
+ # Vote Cache
+ $status->('voting_count_start');
+ my $dbh = Bugzilla->dbh;
+ my %cached_counts = @{ $dbh->selectcol_arrayref(
+ 'SELECT bug_id, votes FROM bugs', {Columns=>[1,2]}) };
+
+ my %real_counts = @{ $dbh->selectcol_arrayref(
+ 'SELECT bug_id, SUM(vote_count) FROM votes '
+ . $dbh->sql_group_by('bug_id'), {Columns=>[1,2]}) };
+
+ my $needs_rebuild;
+ foreach my $id (keys %cached_counts) {
+ my $cached_count = $cached_counts{$id};
+ my $real_count = $real_counts{$id} || 0;
+ if ($cached_count < 0) {
+ $status->('voting_count_alert', { id => $id }, 'alert');
+ }
+ elsif ($cached_count != $real_count) {
+ $status->('voting_cache_alert', { id => $id }, 'alert');
+ $needs_rebuild = 1;
+ }
+ }
+
+ $status->('voting_cache_rebuild_fix') if $needs_rebuild;
+}
+
+sub sanitycheck_repair {
+ my ($self, $args) = @_;
+ my $status = $args->{status};
+ my $input = Bugzilla->input_params;
+ my $dbh = Bugzilla->dbh;
+
+ return if !$input->{rebuild_vote_cache};
+
+ $status->('voting_cache_rebuild_start');
+ $dbh->bz_start_transaction();
+ $dbh->do('UPDATE bugs SET votes = 0');
+
+ my $sth = $dbh->prepare(
+ 'SELECT bug_id, SUM(vote_count) FROM votes '
+ . $dbh->sql_group_by('bug_id'));
+ $sth->execute();
+
+ my $sth_update = $dbh->prepare(
+ 'UPDATE bugs SET votes = ? WHERE bug_id = ?');
+ while (my ($id, $count) = $sth->fetchrow_array) {
+ $sth_update->execute($count, $id);
+ }
+ $dbh->bz_commit_transaction();
+ $status->('voting_cache_rebuild_end');
+}
+
+
+##############
+# Validators #
+##############
+
+sub _check_votesperuser {
+ return _check_votes(0, @_);
+}
+
+sub _check_maxvotesperbug {
+ return _check_votes(DEFAULT_VOTES_PER_BUG, @_);
+}
+
+sub _check_votestoconfirm {
+ return _check_votes(0, @_);
+}
+
+# This subroutine is only used internally by other _check_votes_* validators.
+sub _check_votes {
+ my ($default, $invocant, $votes, $field) = @_;
+
+ detaint_natural($votes) if defined $votes;
+ # On product creation, if the number of votes is not a valid integer,
+ # we silently fall back to the given default value.
+ # If the product already exists and the change is illegal, we complain.
+ if (!defined $votes) {
+ if (ref $invocant) {
+ ThrowUserError('voting_product_illegal_votes',
+ { field => $field, votes => $_[2] });
+ }
+ else {
+ $votes = $default;
+ }
+ }
+ return $votes;
+}
+
+#########
+# Pages #
+#########
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{page_id};
+ my $vars = $args->{vars};
+
+ if ($page =~ m{^voting/bug\.}) {
+ _page_bug($vars);
+ }
+ elsif ($page =~ m{^voting/user\.}) {
+ _page_user($vars);
+ }
+}
+
+sub _page_bug {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+
+ my $bug_id = $input->{bug_id};
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ $vars->{'bug'} = $bug;
+ $vars->{'users'} =
+ $dbh->selectall_arrayref('SELECT profiles.login_name,
+ profiles.userid AS id,
+ votes.vote_count
+ FROM votes
+ INNER JOIN profiles
+ ON profiles.userid = votes.who
+ WHERE votes.bug_id = ?',
+ {Slice=>{}}, $bug->id);
+}
+
+sub _page_user {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $input = Bugzilla->input_params;
+
+ my $action = $input->{action};
+ if ($action and $action eq 'vote') {
+ _update_votes($vars);
+ }
+
+ # If a bug_id is given, and we're editing, we'll add it to the votes list.
+
+ my $bug_id = $input->{bug_id};
+ my $bug = Bugzilla::Bug->check($bug_id) if $bug_id;
+ my $who_id = $input->{user_id} || $user->id;
+
+ # Logged-out users must specify a user_id.
+ Bugzilla->login(LOGIN_REQUIRED) if !$who_id;
+
+ my $who = Bugzilla::User->check({ id => $who_id });
+
+ my $canedit = $user->id == $who->id;
+
+ $dbh->bz_start_transaction();
+
+ if ($canedit && $bug) {
+ # Make sure there is an entry for this bug
+ # in the vote table, just so that things display right.
+ my $has_votes = $dbh->selectrow_array('SELECT vote_count FROM votes
+ WHERE bug_id = ? AND who = ?',
+ undef, ($bug->id, $who->id));
+ if (!$has_votes) {
+ $dbh->do('INSERT INTO votes (who, bug_id, vote_count)
+ VALUES (?, ?, 0)', undef, ($who->id, $bug->id));
+ }
+ }
+
+ my (@products, @all_bug_ids);
+ # Read the votes data for this user for each product.
+ foreach my $product (@{ $user->get_selectable_products }) {
+ next unless ($product->{votesperuser} > 0);
+
+ my $vote_list =
+ $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count
+ FROM votes
+ INNER JOIN bugs
+ ON votes.bug_id = bugs.bug_id
+ WHERE votes.who = ?
+ AND bugs.product_id = ?',
+ undef, ($who->id, $product->id));
+
+ my %votes = map { $_->[0] => $_->[1] } @$vote_list;
+ my @bug_ids = sort keys %votes;
+ # Exclude bugs that the user can no longer see.
+ @bug_ids = @{ $user->visible_bugs(\@bug_ids) };
+ next unless scalar @bug_ids;
+
+ push(@all_bug_ids, @bug_ids);
+ my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
+ $_->{count} = $votes{$_->id} foreach @bugs;
+ # We include votes from bugs that the user can no longer see.
+ my $total = sum(values %votes) || 0;
+
+ my $onevoteonly = 0;
+ $onevoteonly = 1 if (min($product->{votesperuser},
+ $product->{maxvotesperbug}) == 1);
+
+ push(@products, { name => $product->name,
+ bugs => \@bugs,
+ bug_ids => \@bug_ids,
+ onevoteonly => $onevoteonly,
+ total => $total,
+ maxvotes => $product->{votesperuser},
+ maxperbug => $product->{maxvotesperbug} });
+ }
+
+ if ($canedit && $bug) {
+ $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?',
+ undef, $who->id);
+ }
+ $dbh->bz_commit_transaction();
+
+ $vars->{'canedit'} = $canedit;
+ $vars->{'voting_user'} = { "login" => $who->name };
+ $vars->{'products'} = \@products;
+ $vars->{'this_bug'} = $bug;
+ $vars->{'all_bug_ids'} = \@all_bug_ids;
+}
+
+sub _update_votes {
+ my ($vars) = @_;
+
+ ############################################################################
+ # Begin Data/Security Validation
+ ############################################################################
+
+ my $cgi = Bugzilla->cgi;
+ my $dbh = Bugzilla->dbh;
+ my $template = Bugzilla->template;
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $input = Bugzilla->input_params;
+
+ # Build a list of bug IDs for which votes have been submitted. Votes
+ # are submitted in form fields in which the field names are the bug
+ # IDs and the field values are the number of votes.
+
+ my @buglist = grep {/^\d+$/} keys %$input;
+ my (%bugs, %votes);
+
+ # If no bugs are in the buglist, let's make sure the user gets notified
+ # that their votes will get nuked if they continue.
+ if (scalar(@buglist) == 0) {
+ if (!defined $cgi->param('delete_all_votes')) {
+ print $cgi->header();
+ $template->process("voting/delete-all.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+ elsif ($cgi->param('delete_all_votes') == 0) {
+ print $cgi->redirect("page.cgi?id=voting/user.html");
+ exit;
+ }
+ }
+ else {
+ $user->visible_bugs(\@buglist);
+ my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist);
+ $bugs{$_->id} = $_ foreach @$bugs_obj;
+ }
+
+ # Call check_is_visible() on each bug to make sure it is an existing bug
+ # that the user is authorized to access, and make sure the number of votes
+ # submitted is also an integer.
+ foreach my $id (@buglist) {
+ my $bug = $bugs{$id}
+ or ThrowUserError('bug_id_does_not_exist', { bug_id => $id });
+ $bug->check_is_visible;
+ $id = $bug->id;
+ $votes{$id} = $input->{$id};
+ detaint_natural($votes{$id})
+ || ThrowUserError("voting_must_be_nonnegative");
+ }
+
+ my $token = $cgi->param('token');
+ check_hash_token($token, ['vote']);
+
+ ############################################################################
+ # End Data/Security Validation
+ ############################################################################
+ my $who = $user->id;
+
+ # If the user is voting for bugs, make sure they aren't overstuffing
+ # the ballot box.
+ if (scalar @buglist) {
+ my (%prodcount, %products);
+ foreach my $bug_id (keys %bugs) {
+ my $bug = $bugs{$bug_id};
+ my $prod = $bug->product;
+ $products{$prod} ||= $bug->product_obj;
+ $prodcount{$prod} ||= 0;
+ $prodcount{$prod} += $votes{$bug_id};
+
+ # Make sure we haven't broken the votes-per-bug limit
+ ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug})
+ || ThrowUserError("voting_too_many_votes_for_bug",
+ {max => $products{$prod}->{maxvotesperbug},
+ product => $prod,
+ votes => $votes{$bug_id}});
+ }
+
+ # Make sure we haven't broken the votes-per-product limit
+ foreach my $prod (keys(%prodcount)) {
+ ($prodcount{$prod} <= $products{$prod}->{votesperuser})
+ || ThrowUserError("voting_too_many_votes_for_product",
+ {max => $products{$prod}->{votesperuser},
+ product => $prod,
+ votes => $prodcount{$prod}});
+ }
+ }
+
+ # Update the user's votes in the database.
+ $dbh->bz_start_transaction();
+
+ my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes
+ WHERE who = ?', undef, $who);
+
+ my %old_votes = map { $_->[0] => $_->[1] } @$old_list;
+
+ my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count)
+ VALUES (?, ?, ?)');
+ my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ?
+ WHERE bug_id = ? AND who = ?');
+
+ my %affected = map { $_ => 1 } (@buglist, keys %old_votes);
+ my @deleted_votes;
+
+ foreach my $id (keys %affected) {
+ if (!$votes{$id}) {
+ push(@deleted_votes, $id);
+ next;
+ }
+ if ($votes{$id} == ($old_votes{$id} || 0)) {
+ delete $affected{$id};
+ next;
+ }
+ # We use 'defined' in case 0 was accidentally stored in the DB.
+ if (defined $old_votes{$id}) {
+ $sth_updateVotes->execute($votes{$id}, $id, $who);
+ }
+ else {
+ $sth_insertVotes->execute($who, $id, $votes{$id});
+ }
+ }
+
+ if (@deleted_votes) {
+ $dbh->do('DELETE FROM votes WHERE who = ? AND ' .
+ $dbh->sql_in('bug_id', \@deleted_votes), undef, $who);
+ }
+
+ # Update the cached values in the bugs table
+ my @updated_bugs = ();
+
+ my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes
+ WHERE bug_id = ?");
+
+ $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
+
+ foreach my $id (keys %affected) {
+ $sth_getVotes->execute($id);
+ my $v = $sth_getVotes->fetchrow_array || 0;
+ $sth_updateVotes->execute($v, $id);
+
+ my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
+ push (@updated_bugs, $id) if $confirmed;
+ }
+
+ $dbh->bz_commit_transaction();
+
+ print $cgi->header() if scalar @updated_bugs;
+ $vars->{'type'} = "votes";
+ $vars->{'title_tag'} = 'change_votes';
+ foreach my $bug_id (@updated_bugs) {
+ $vars->{'id'} = $bug_id;
+ $vars->{'sent_bugmail'} =
+ Bugzilla::BugMail::Send($bug_id, { 'changer' => $user });
+
+ $template->process("bug/process/results.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ # Set header_done to 1 only after the first bug.
+ $vars->{'header_done'} = 1;
+ }
+ $vars->{'votes_recorded'} = 1;
+}
+
+######################
+# Helper Subroutines #
+######################
+
+sub _modify_bug_votes {
+ my ($product, $changes) = @_;
+ my $dbh = Bugzilla->dbh;
+ my @msgs;
+
+ # 1. too many votes for a single user on a single bug.
+ my @toomanyvotes_list;
+ if ($product->{maxvotesperbug} < $product->{votesperuser}) {
+ my $votes = $dbh->selectall_arrayref(
+ 'SELECT votes.who, votes.bug_id
+ FROM votes
+ INNER JOIN bugs ON bugs.bug_id = votes.bug_id
+ WHERE bugs.product_id = ?
+ AND votes.vote_count > ?',
+ undef, ($product->id, $product->{maxvotesperbug}));
+
+ foreach my $vote (@$votes) {
+ my ($who, $id) = (@$vote);
+ # If some votes are removed, _remove_votes() returns a list
+ # of messages to send to voters.
+ push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug'));
+ my $name = user_id_to_login($who);
+
+ push(@toomanyvotes_list, {id => $id, name => $name});
+ }
+ }
+
+ $changes->{'_too_many_votes'} = \@toomanyvotes_list;
+
+ # 2. too many total votes for a single user.
+ # This part doesn't work in the general case because _remove_votes
+ # doesn't enforce votesperuser (except per-bug when it's less
+ # than maxvotesperbug). See _remove_votes().
+
+ my $votes = $dbh->selectall_arrayref(
+ 'SELECT votes.who, votes.vote_count
+ FROM votes
+ INNER JOIN bugs ON bugs.bug_id = votes.bug_id
+ WHERE bugs.product_id = ?',
+ undef, $product->id);
+
+ my %counts;
+ foreach my $vote (@$votes) {
+ my ($who, $count) = @$vote;
+ if (!defined $counts{$who}) {
+ $counts{$who} = $count;
+ } else {
+ $counts{$who} += $count;
+ }
+ }
+
+ my @toomanytotalvotes_list;
+ foreach my $who (keys(%counts)) {
+ if ($counts{$who} > $product->{votesperuser}) {
+ my $bug_ids = $dbh->selectcol_arrayref(
+ 'SELECT votes.bug_id
+ FROM votes
+ INNER JOIN bugs ON bugs.bug_id = votes.bug_id
+ WHERE bugs.product_id = ?
+ AND votes.who = ?',
+ undef, $product->id, $who);
+
+ foreach my $bug_id (@$bug_ids) {
+ # _remove_votes returns a list of messages to send
+ # in case some voters had too many votes.
+ push(@msgs, _remove_votes($bug_id, $who,
+ 'votes_too_many_per_user'));
+ my $name = user_id_to_login($who);
+
+ push(@toomanytotalvotes_list, {id => $bug_id, name => $name});
+ }
+ }
+ }
+
+ $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list;
+
+ # 3. enough votes to confirm
+ my $bug_list = $dbh->selectcol_arrayref(
+ 'SELECT bug_id FROM bugs
+ WHERE product_id = ? AND bug_status = ? AND votes >= ?',
+ undef, ($product->id, 'UNCONFIRMED', $product->{votestoconfirm}));
+
+ my @updated_bugs;
+ foreach my $bug_id (@$bug_list) {
+ my $confirmed = _confirm_if_vote_confirmed($bug_id);
+ push (@updated_bugs, $bug_id) if $confirmed;
+ }
+ $changes->{'_confirmed_bugs'} = \@updated_bugs;
+
+ # Now that changes are done, we can send emails to voters.
+ foreach my $msg (@msgs) {
+ MessageToMTA($msg);
+ }
+ # And send out emails about changed bugs
+ foreach my $bug_id (@updated_bugs) {
+ my $sent_bugmail = Bugzilla::BugMail::Send(
+ $bug_id, { changer => Bugzilla->user });
+ $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail;
+ }
+}
+
+# If a bug is moved to a product which allows less votes per bug
+# compared to the previous product, extra votes need to be removed.
+sub _remove_votes {
+ my ($id, $who, $reason) = (@_);
+ my $dbh = Bugzilla->dbh;
+
+ my $whopart = ($who) ? " AND votes.who = $who" : "";
+
+ my $sth = $dbh->prepare("SELECT profiles.login_name, " .
+ "profiles.userid, votes.vote_count, " .
+ "products.votesperuser, products.maxvotesperbug " .
+ "FROM profiles " .
+ "LEFT JOIN votes ON profiles.userid = votes.who " .
+ "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " .
+ "LEFT JOIN products ON products.id = bugs.product_id " .
+ "WHERE votes.bug_id = ? " . $whopart);
+ $sth->execute($id);
+ my @list;
+ while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) {
+ push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
+ }
+
+ # @messages stores all emails which have to be sent, if any.
+ # This array is passed to the caller which will send these emails itself.
+ my @messages = ();
+
+ if (scalar(@list)) {
+ foreach my $ref (@list) {
+ my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
+
+ $maxvotesperbug = min($votesperuser, $maxvotesperbug);
+
+ # If this product allows voting and the user's votes are in
+ # the acceptable range, then don't do anything.
+ next if $votesperuser && $oldvotes <= $maxvotesperbug;
+
+ # If the user has more votes on this bug than this product
+ # allows, then reduce the number of votes so it fits
+ my $newvotes = $maxvotesperbug;
+
+ my $removedvotes = $oldvotes - $newvotes;
+
+ if ($newvotes) {
+ $dbh->do("UPDATE votes SET vote_count = ? " .
+ "WHERE bug_id = ? AND who = ?",
+ undef, ($newvotes, $id, $userid));
+ } else {
+ $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?",
+ undef, ($id, $userid));
+ }
+
+ # Notice that we did not make sure that the user fit within the $votesperuser
+ # range. This is considered to be an acceptable alternative to losing votes
+ # during product moves. Then next time the user attempts to change their votes,
+ # they will be forced to fit within the $votesperuser limit.
+
+ # Now lets send the e-mail to alert the user to the fact that their votes have
+ # been reduced or removed.
+ my $vars = {
+ 'to' => $name . Bugzilla->params->{'emailsuffix'},
+ 'bugid' => $id,
+ 'reason' => $reason,
+
+ 'votesremoved' => $removedvotes,
+ 'votesold' => $oldvotes,
+ 'votesnew' => $newvotes,
+ };
+
+ my $voter = new Bugzilla::User($userid);
+ my $template = Bugzilla->template_inner($voter->setting('lang'));
+
+ my $msg;
+ $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg);
+ push(@messages, $msg);
+ }
+
+ my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " .
+ "FROM votes WHERE bug_id = ?",
+ undef, $id) || 0;
+ $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?",
+ undef, ($votes, $id));
+ }
+ # Now return the array containing emails to be sent.
+ return @messages;
+}
+
+# If a user votes for a bug, or the number of votes required to
+# confirm a bug has been reduced, check if the bug is now confirmed.
+sub _confirm_if_vote_confirmed {
+ my $id = shift;
+ my $bug = ref $id ? $id : new Bugzilla::Bug($id);
+
+ my $ret = 0;
+ if (!$bug->everconfirmed
+ and $bug->product_obj->{votestoconfirm}
+ and $bug->votes >= $bug->product_obj->{votestoconfirm})
+ {
+ $bug->add_comment('', { type => CMT_POPULAR_VOTES });
+
+ if ($bug->bug_status eq 'UNCONFIRMED') {
+ # Get a valid open state.
+ my $new_status;
+ foreach my $state (@{$bug->status->can_change_to}) {
+ if ($state->is_open && $state->name ne 'UNCONFIRMED') {
+ $new_status = $state->name;
+ last;
+ }
+ }
+ ThrowCodeError('voting_no_open_bug_status') unless $new_status;
+
+ # We cannot call $bug->set_bug_status() here, because a user without
+ # canconfirm privs should still be able to confirm a bug by
+ # popular vote. We already know the new status is valid, so it's safe.
+ $bug->{bug_status} = $new_status;
+ $bug->{everconfirmed} = 1;
+ delete $bug->{'status'}; # Contains the status object.
+ }
+ else {
+ # If the bug is in a closed state, only set everconfirmed to 1.
+ # Do not call $bug->_set_everconfirmed(), for the same reason as above.
+ $bug->{everconfirmed} = 1;
+ }
+ $bug->update();
+
+ $ret = 1;
+ }
+ return $ret;
+}
+
+
+__PACKAGE__->NAME;
diff --git a/extensions/Voting/disabled b/extensions/Voting/disabled
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/Voting/disabled
diff --git a/extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl b/extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl
new file mode 100644
index 0000000..846d742
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/account/prefs/email-relationships.html.tmpl
@@ -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.
+ #%]
+
+[% relationships.push({ id = constants.REL_VOTER, description = "Voter" }) %]
+[% no_added_removed.push(constants.REL_VOTER) %]
diff --git a/extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl b/extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl
new file mode 100644
index 0000000..9a8373b
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl
@@ -0,0 +1,47 @@
+[%# 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.
+ #%]
+
+[% DEFAULT
+ product.maxvotesperbug = constants.DEFAULT_VOTES_PER_BUG
+ product.votesperuser = 0
+ product.votestoconfirm = 0
+%]
+
+<tr>
+ <th align="right">Maximum votes per person:</th>
+ <td><input size="5" maxlength="5" name="votesperuser" id="votesperuser"
+ value="[% product.votesperuser FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <th align="right">
+ Maximum votes a person can put on a single [% terms.bug %]:
+ </th>
+ <td><input size="5" maxlength="5" name="maxvotesperbug" id="maxvotesperbug"
+ value="[% product.maxvotesperbug FILTER html %]">
+ </td>
+</tr>
+
+<tr id="votes_to_confirm_container"
+ [%- ' class="bz_default_hidden"' IF !product.allows_unconfirmed %]>
+ <th align="right">
+ Confirm [% terms.abug %] if it gets this many votes:
+ </th>
+ <td>
+ <input size="3" maxlength="5" name="votestoconfirm" id="votestoconfirm"
+ value="[% product.votestoconfirm FILTER html %]">
+ <br>(Setting this to 0 disables auto-confirming [% terms.bugs %]
+ by vote.)
+ <script type="text/javascript">
+ YAHOO.util.Event.addListener('allows_unconfirmed', 'change',
+ function() { bz_toggleClass('votes_to_confirm_container',
+ 'bz_default_hidden'); });
+ </script>
+ </td>
+</tr>
diff --git a/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl b/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl
new file mode 100644
index 0000000..64db63d
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl
@@ -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.
+ #%]
+
+[% SET checkvotes = 0 %]
+
+[% IF changes.votesperuser.defined %]
+ <p>
+ Updated votes per user from
+ [%+ changes.votesperuser.0 FILTER html %] to
+ [%+ product.votesperuser FILTER html %].
+ </p>
+ [% checkvotes = 1 %]
+[% END %]
+
+[% IF changes.maxvotesperbug.defined %]
+ <p>
+ Updated maximum votes per [% terms.bug %] from
+ [%+ changes.maxvotesperbug.0 FILTER html %] to
+ [%+ product.maxvotesperbug FILTER html %].
+ </p>
+ [% checkvotes = 1 %]
+[% END %]
+
+[% IF changes.votestoconfirm.defined %]
+ <p>
+ Updated number of votes needed to confirm a [% terms.bug %] from
+ [%+ changes.votestoconfirm.0 FILTER html %] to
+ [%+ product.votestoconfirm FILTER html %].
+ </p>
+ [% checkvotes = 1 %]
+[% END %]
+
+[%# Note that this display of changed votes and/or confirmed bugs is
+ not very scalable. We could have a _lot_, and we just list them all.
+ One day we should limit this perhaps, or have a more scalable display %]
+
+[% IF checkvotes %]
+ <hr>
+
+ <p>Checking existing votes in this product for anybody who now
+ has too many votes for [% terms.abug %]...<br>
+ [% IF changes._too_many_votes.size %]
+ [% FOREACH detail = changes._too_many_votes %]
+ &rarr;removed votes for [% terms.bug %] <a href="show_bug.cgi?id=
+ [%- detail.id FILTER uri %]">
+ [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br>
+ [% END %]
+ [% ELSE %]
+ &rarr;there were none.
+ [% END %]
+ </p>
+
+ <p>Checking existing votes in this product for anybody
+ who now has too many total votes...<br>
+ [% IF changes._too_many_total_votes.size %]
+ [% FOREACH detail = changes._too_many_total_votes %]
+ &rarr;removed votes for [% terms.bug %] <a href="show_bug.cgi?id=
+ [%- detail.id FILTER uri %]">
+ [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br>
+ [% END %]
+ [% ELSE %]
+ &rarr;there were none.
+ [% END %]
+ </p>
+
+ <p>Checking unconfirmed [% terms.bugs %] in this product for any which now have
+ sufficient votes...<br>
+ [% IF changes._confirmed_bugs.size %]
+ [% FOREACH id = changes._confirmed_bugs %]
+
+ [%# This is INCLUDED instead of PROCESSED to avoid variables getting
+ overwritten, which happens otherwise %]
+ [% INCLUDE bug/process/results.html.tmpl
+ type = 'votes'
+ header_done = 1
+ sent_bugmail = changes._confirmed_bugs_sent_bugmail.$id
+ id = id
+ %]
+ [% END %]
+ [% ELSE %]
+ &rarr;there were none.
+ [% END %]
+ </p>
+
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl b/extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
new file mode 100644
index 0000000..926fead
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl
@@ -0,0 +1,29 @@
+[%# 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 san_tag == "voting_cache_rebuild_fix" %]
+ <a href="sanitycheck.cgi?rebuild_vote_cache=1&amp;token=
+ [%- issue_hash_token(['sanitycheck']) FILTER uri %]">Click here to
+ rebuild the vote cache</a>
+
+[% ELSIF san_tag == "voting_cache_alert" %]
+ Bad vote cache for [% PROCESS bug_link bug_id = id %]
+
+[% ELSIF san_tag == "voting_count_start" %]
+ Checking cached vote counts.
+
+[% ELSIF san_tag == "voting_count_alert" %]
+ Bad vote sum for [% terms.bug %] [%+ id FILTER html %].
+
+[% ELSIF san_tag == "voting_cache_rebuild_start" %]
+ OK, now rebuilding vote cache.
+
+[% ELSIF san_tag == "voting_cache_rebuild_end" %]
+ Vote cache has been rebuilt
+
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl b/extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl
new file mode 100644
index 0000000..b971dac
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/admin/users/confirm-delete-warn_safe.html.tmpl
@@ -0,0 +1,26 @@
+[%# 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 votes %]
+ <li>
+ [% otheruser.login FILTER html %] has voted on
+ [% IF votes == 1 %]
+ [%+ terms.abug %]
+ [% ELSE %]
+ [%+ votes FILTER html %] [%+ terms.bugs %]
+ [% END %].
+
+ If you delete the user account,
+ [% IF votes == 1 %]
+ this vote
+ [% ELSE %]
+ these votes
+ [% END %]
+ will be deleted along with the user account.
+ </li>
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
new file mode 100644
index 0000000..6710ab3
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
@@ -0,0 +1,25 @@
+[%# 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 bug.product_obj.votesperuser %]
+ <span id="votes_container">
+ [% IF bug.votes %]
+ with
+ <a href="page.cgi?id=voting/bug.html&amp;bug_id=
+ [%- bug.id FILTER uri %]">
+ [%- bug.votes FILTER html %]
+ [% IF bug.votes == 1 %]
+ vote
+ [% ELSE %]
+ votes
+ [% END %]</a>
+ [% END %]
+ (<a href="page.cgi?id=voting/user.html&amp;bug_id=
+ [%- bug.id FILTER uri %]#vote_
+ [%- bug.id FILTER uri %]">vote</a>)
+ </span>
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.tmpl b/extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.tmpl
new file mode 100644
index 0000000..64d3f65
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/bug/format_comment-type.txt.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 comment.type == constants.CMT_POPULAR_VOTES %]
+*** This [% terms.bug %] has been confirmed by popular vote. ***
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/bug/process/header-title.html.tmpl b/extensions/Voting/template/en/default/hook/bug/process/header-title.html.tmpl
new file mode 100644
index 0000000..200fe4b
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/bug/process/header-title.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 title_tag == "change_votes" %]
+ [% title = "Change Votes" %]
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl b/extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl
new file mode 100644
index 0000000..86e48de
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/bug/process/results-title.html.tmpl
@@ -0,0 +1,9 @@
+[%# 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.
+ #%]
+
+[% title.votes = "$Link confirmed by number of votes" %]
diff --git a/extensions/Voting/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/Voting/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 0000000..92b373d
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,9 @@
+[%# 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.
+ #%]
+
+[% style_urls.push('extensions/Voting/web/style.css') %]
diff --git a/extensions/Voting/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/Voting/template/en/default/hook/global/code-error-errors.html.tmpl
new file mode 100644
index 0000000..81d9fff
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/global/code-error-errors.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 error == "voting_no_open_bug_status" %]
+ [% title = "$terms.Bug Cannot Be Confirmed" %]
+ There is no valid transition from
+ [%+ display_value("bug_status", "UNCONFIRMED") FILTER html %] to an open state
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/global/field-descs-end.none.tmpl b/extensions/Voting/template/en/default/hook/global/field-descs-end.none.tmpl
new file mode 100644
index 0000000..33e8ac7
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/global/field-descs-end.none.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 in_template_var %]
+ [% vars.field_descs.votes = "Votes" %]
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/global/reason-descs-end.none.tmpl b/extensions/Voting/template/en/default/hook/global/reason-descs-end.none.tmpl
new file mode 100644
index 0000000..28a1e36
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/global/reason-descs-end.none.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.
+ #%]
+
+[% reason_descs.${constants.REL_VOTER} = "You voted for the ${terms.bug}." %]
+[% watch_reason_descs.${constants.REL_VOTER} =
+ "You are watching a voter for the ${terms.bug}." %]
diff --git a/extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 0000000..55c32d4
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,43 @@
+[%# 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 == "voting_must_be_nonnegative" %]
+ [% title = "Votes Must Be Non-negative" %]
+ [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %]
+ Only use non-negative numbers for your [% terms.bug %] votes.
+
+[% ELSIF error == "voting_product_illegal_votes" %]
+ [% title = "Votes Must Be Non-negative" %]
+ [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %]
+ '[% votes FILTER html %]' is an invalid value for the
+ <em>
+ [% IF field == "votesperuser" %]
+ Votes Per User
+ [% ELSIF field == "maxvotesperbug" %]
+ Maximum Votes Per [% terms.Bug %]
+ [% ELSIF field == "votestoconfirm" %]
+ Votes To Confirm
+ [% END %]
+ </em> field, which should contain a non-negative number.
+
+[% ELSIF error == "voting_too_many_votes_for_bug" %]
+ [% title = "Illegal Vote" %]
+ [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %]
+ You may only use at most [% max FILTER html %] votes for a single
+ [%+ terms.bug %] in the
+ <tt>[% product FILTER html %]</tt> product, but you are trying to
+ use [% votes FILTER html %].
+
+[% ELSIF error == "voting_too_many_votes_for_product" %]
+ [% title = "Illegal Vote" %]
+ [% admindocslinks = {'voting.html' => 'Setting up the voting feature'} %]
+ You tried to use [% votes FILTER html %] votes in the
+ <tt>[% product FILTER html %]</tt> product, which exceeds the maximum of
+ [%+ max FILTER html %] votes for this product.
+
+[% END %]
diff --git a/extensions/Voting/template/en/default/hook/search/form-after_freetext_fields.html.tmpl b/extensions/Voting/template/en/default/hook/search/form-after_freetext_fields.html.tmpl
new file mode 100644
index 0000000..cb549e3
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/search/form-after_freetext_fields.html.tmpl
@@ -0,0 +1,16 @@
+[%# 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.
+ #%]
+
+<div class="search_field_row">
+ <span class="field_label ">
+ <label for="votes">Only [% terms.bugs %] with at least</label>:
+ </span>
+ <input name="votes" id="votes" size="3"
+ value="[% default.votes.0 FILTER html %]"> votes
+ <input type="hidden" name="votes_type" value="greaterthaneq">
+</div>
diff --git a/extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl b/extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl
new file mode 100644
index 0000000..d8ec155
--- /dev/null
+++ b/extensions/Voting/template/en/default/hook/search/search-report-select-rep_fields.html.tmpl
@@ -0,0 +1,9 @@
+[%# 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.
+ #%]
+
+[% rep_fields.push('votes') %]
diff --git a/extensions/Voting/template/en/default/pages/voting.html.tmpl b/extensions/Voting/template/en/default/pages/voting.html.tmpl
new file mode 100644
index 0000000..82a4bbd
--- /dev/null
+++ b/extensions/Voting/template/en/default/pages/voting.html.tmpl
@@ -0,0 +1,55 @@
+[%# 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.
+ #%]
+
+[% INCLUDE global/header.html.tmpl title = "Voting" %]
+
+<p>[% terms.Bugzilla %] has a "voting" feature. Each product allows users to
+have a certain number of votes. (Some products may not allow any, which means
+you can't vote on things in those products at all.) With your vote, you
+indicate which [% terms.bugs %] you think are the most important and
+would like to see fixed. Note that voting is nowhere near as effective
+as providing a fix yourself.</p>
+
+<p>Depending on how the administrator has configured the relevant product,
+you may be able to vote for the same [% terms.bug %] more than once.
+Remember that you have a limited number of votes. When weighted voting
+is allowed and a limited number of votes are available to you, you will
+have to decide whether you want to distribute your votes among a large
+number of [% terms.bugs %] indicating your minimal interest or focus on
+a few [% terms.bugs %] indicating your strong support for them.
+</p>
+
+<p>To look at votes:</p>
+
+<ul>
+ <li>Go to the query page. Do a normal query, but enter 1 in the "At least
+ ___ votes" field. This will show you items that match your query that
+ have at least one vote.</li>
+</ul>
+
+<p>To vote for [% terms.abug %]:</p>
+
+<ul>
+ <li>Bring up the [% terms.bug %] in question.</li>
+
+ <li>Click on the "(vote)" link that appears on the right of the "Importance"
+ fields. (If no such link appears, then voting may not be allowed in
+ this [% terms.bug %]'s product.)</li>
+
+ <li>Indicate how many votes you want to give this [% terms.bug %]. This page
+ also displays how many votes you've given to other [% terms.bugs %], so you
+ may rebalance your votes as necessary.</li>
+</ul>
+
+<p>You will automatically get email notifying you of any changes that occur
+on [% terms.bugs %] you vote for.</p>
+
+<p>You may review your votes at any time by clicking on the "<a href=
+"page.cgi?id=voting/user.html">My Votes</a>" link in the page footer.</p>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/Voting/template/en/default/pages/voting/bug.html.tmpl b/extensions/Voting/template/en/default/pages/voting/bug.html.tmpl
new file mode 100644
index 0000000..2ba784f
--- /dev/null
+++ b/extensions/Voting/template/en/default/pages/voting/bug.html.tmpl
@@ -0,0 +1,50 @@
+[%# 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.
+ #%]
+
+[%# INTERFACE:
+ # bug: Bugzilla::Bug that we are listing the votes for.
+ # users: list of hashes. May be empty. Each hash has two members:
+ # login_name: string. The login name of the user whose vote is attached
+ # vote_count: integer. The number of times that user has votes for this bug.
+ #%]
+
+[% subheader = BLOCK %]
+ [% "$terms.Bug $bug.id" FILTER bug_link(bug) FILTER none %] - [% bug.short_desc FILTER html %]
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Show Votes"
+ subheader = subheader
+ %]
+
+[% total = 0 %]
+<table cellspacing="4">
+ <tr>
+ <th>Who</th>
+ <th>Number of votes</th>
+ </tr>
+
+ [% FOREACH voter = users %]
+ [% total = total + voter.vote_count %]
+ <tr>
+ <td>
+ <a href="page.cgi?id=voting/user.html&amp;user_id=
+ [%- voter.id FILTER uri %]">
+ [% voter.login_name FILTER email FILTER html %]
+ </a>
+ </td>
+ <td align="right">
+ [% voter.vote_count FILTER html %]
+ </td>
+ </tr>
+ [% END %]
+</table>
+
+<p>Total votes: [% total FILTER html %]</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
new file mode 100644
index 0000000..8777e03
--- /dev/null
+++ b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
@@ -0,0 +1,178 @@
+[%# 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.
+ #%]
+
+[%# INTERFACE:
+ # voting_user: hash containing a 'login' field
+ #
+ # products: list of hashes containing details of products relating to
+ # voting:
+ # name: name of product
+ # bugs: list of bugs the user has voted for
+ # bug_ids: list of bug ids the user has voted for
+ # onevoteonly: one or more votes allowed per bug?
+ # total: users current vote count for the product
+ # maxvotes: max votes allowed for a user in this product
+ # maxperbug: max votes per bug allowed for a user in this product
+ #
+ # this_bug: Bugzilla::Bug; if the user is voting for a bug, this is the bug
+ #
+ # canedit: boolean; Should the votes be presented in a form, or readonly?
+ #
+ # all_bug_ids: List of all bug ids the user has voted for, across all products
+ #%]
+
+[% IF !header_done %]
+ [% subheader = voting_user.login FILTER html %]
+ [% IF canedit %]
+ [% title = "Change Votes" %]
+ [% IF this_bug %]
+ [%# We .select and .focus the input so it works for textbox and
+ checkbox %]
+ [% onload = "document.forms['voting_form'].bug_" _ this_bug.id _
+ ".select();document.forms['voting_form'].bug_" _ this_bug.id _
+ ".focus()" %]
+ [% END %]
+ [% ELSE %]
+ [% title = "Show Votes" %]
+ [% END %]
+ [% PROCESS global/header.html.tmpl
+ style_urls = [ "extensions/Voting/web/style.css" ]
+ %]
+[% ELSE %]
+ <hr>
+[% END %]
+
+[% IF votes_recorded %]
+ <p>
+ <font color="red">
+ The changes to your votes have been saved.
+ </font>
+ </p>
+[% ELSE %]
+ <br>
+[% END %]
+
+[% IF products.size %]
+ <form name="voting_form" method="post" action="page.cgi?id=voting/user.html">
+ <input type="hidden" name="action" value="vote">
+ <input type="hidden" name="token" value="[% issue_hash_token(['vote']) FILTER html %]">
+ <table cellspacing="4">
+ <tr>
+ <td></td>
+ <th>Votes</th>
+ <th>[% terms.Bug %] #</th>
+ <th>Summary</th>
+ </tr>
+
+ [% onevoteproduct = 0 %]
+ [% multivoteproduct = 0 %]
+ [% FOREACH product = products %]
+ [% IF product.onevoteonly %]
+ [% onevoteproduct = 1 %]
+ [% ELSE %]
+ [% multivoteproduct = 1 %]
+ [% END %]
+ <tr>
+ <th>[% product.name FILTER html %]</th>
+ <td colspan="2" ><a href="buglist.cgi?bug_id=
+ [%- product.bug_ids.join(",") FILTER uri %]">([% terms.bug %] list)</a>
+ </td>
+ <td>
+ [% IF product.maxperbug < product.maxvotes AND
+ product.maxperbug > 1 %]
+ <font size="-1">
+ (Note: only [% product.maxperbug FILTER html %] vote
+ [% "s" IF product.maxperbug != 1 %] allowed per [% terms.bug %] in
+ this product.)
+ </font>
+ [% END %]
+ </td>
+ </tr>
+
+ [% FOREACH bug = product.bugs %]
+ <tr [% IF bug.id == this_bug.id && canedit %] class="bz_bug_being_voted_on"[% END %]>
+ <td>
+ [% IF bug.id == this_bug.id && canedit %]
+ [% IF product.onevoteonly %]
+ Vote For This [% terms.Bug %] &rarr;
+ [% ELSE %]
+ Enter Votes Here &rarr;
+ [% END %]
+ [%- END %]
+ </td>
+ <td align="right"><a name="vote_[% bug.id FILTER none %]">
+ [% IF canedit %]
+ [% IF product.onevoteonly %]
+ <input type="checkbox" name="[% bug.id FILTER none %]" value="1"
+ [% " checked" IF bug.count %] id="bug_[% bug.id FILTER none %]">
+ [% ELSE %]
+ <input name="[% bug.id FILTER none %]" value="[% bug.count FILTER html %]"
+ size="2" id="bug_[% bug.id FILTER none %]">
+ [% END %]
+ [% ELSE %]
+ [% bug.count FILTER html %]
+ [% END %]
+ </a></td>
+ <td align="center">
+ [% PROCESS bug/link.html.tmpl bug = bug, link_text = bug.id %]
+ </td>
+ <td>
+ [% bug.short_desc FILTER html %]
+ (<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id FILTER none %]">Show Votes</a>)
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <td></td>
+ <td colspan="3">[% product.total FILTER html %] vote
+ [% "s" IF product.total != 1 %] used out of [% product.maxvotes FILTER html %]
+ allowed.
+ <br>
+ <br>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ [% IF canedit %]
+ <input type="submit" value="Change My Votes" id="change"> or
+ <a href="buglist.cgi?bug_id=[% all_bug_ids.join(",") FILTER uri %]">view all
+ as [% terms.bug %] list</a>
+ <br>
+ <br>
+ To change your votes,
+ [% IF multivoteproduct %]
+ type in new numbers (using zero to mean no votes)
+ [% " or " IF onevoteproduct %]
+ [% END %]
+ [% IF onevoteproduct %]
+ change the checkbox
+ [% END %]
+ and then click <b>Change My Votes</b>.
+ [% ELSE %]
+ <a href="buglist.cgi?bug_id=[% all_bug_ids.join(",") FILTER uri %]">View all
+ as [% terms.bug %] list</a>
+ [% END %]
+ </form>
+[% ELSE %]
+ <p>
+ [% IF canedit %]
+ You are
+ [% ELSE %]
+ This user is
+ [% END %]
+ currently not voting on any [% terms.bugs %].
+ </p>
+[% END %]
+
+<p>
+ <a href="page.cgi?id=voting.html">Help with voting</a>.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Voting/template/en/default/voting/delete-all.html.tmpl b/extensions/Voting/template/en/default/voting/delete-all.html.tmpl
new file mode 100644
index 0000000..683ec06
--- /dev/null
+++ b/extensions/Voting/template/en/default/voting/delete-all.html.tmpl
@@ -0,0 +1,38 @@
+[%# 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.
+ #%]
+
+[%# INTERFACE:
+ # This template has no interface.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Remove your votes?"
+ %]
+
+<p>
+ You are about to remove all of your [% terms.bug %] votes. Are you sure you wish to
+ remove your vote from every [% terms.bug %] you've voted on?
+</p>
+
+<form action="page.cgi?id=voting/user.html" method="post">
+ <input type="hidden" name="action" value="vote">
+ <input type="hidden" name="token" value="[% issue_hash_token(['vote']) FILTER html %]">
+ <p>
+ <input type="radio" name="delete_all_votes" value="1">
+ Yes, delete all my votes
+ </p>
+ <p>
+ <input type="radio" name="delete_all_votes" value="0" checked="checked">
+ No, go back and review my votes
+ </p>
+ <p>
+ <input type="submit" id="vote" value="Submit">
+ </p>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl b/extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl
new file mode 100644
index 0000000..e3cb34d
--- /dev/null
+++ b/extensions/Voting/template/en/default/voting/votes-removed.txt.tmpl
@@ -0,0 +1,40 @@
+[%# 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.
+ #%]
+
+From: [% Param('mailfrom') %]
+To: [% to %]
+Subject: [% terms.Bug %] [%+ bugid %] Some or all of your votes have been removed.
+X-Bugzilla-Type: voteremoved
+
+Some or all of your votes have been removed from [% terms.bug %] [%+ bugid %].
+
+You had [% votesold FILTER html %] [%+ IF votesold == 1 %]vote[% ELSE %]votes[% END
+%] on this [% terms.bug %], but [% votesremoved FILTER html %] have been removed.
+
+[% IF votesnew %]
+You still have [% votesnew FILTER html %] [%+ IF votesnew == 1 %]vote[% ELSE %]votes[% END %] on this [% terms.bug %].
+[% ELSE %]
+You have no more votes remaining on this [% terms.bug %].
+[% END %]
+
+Reason:
+[% IF reason == "votes_bug_moved" %]
+ This [% terms.bug %] has been moved to a different product.
+
+[% ELSIF reason == "votes_too_many_per_bug" %]
+ The rules for voting on this product has changed;
+ you had too many votes for a single [% terms.bug %].
+
+[% ELSIF reason == "votes_too_many_per_user" %]
+ The rules for voting on this product has changed; you had
+ too many total votes, so all votes have been removed.
+[% END %]
+
+
+
+[% urlbase %]show_bug.cgi?id=[% bugid %]
diff --git a/extensions/Voting/web/style.css b/extensions/Voting/web/style.css
new file mode 100644
index 0000000..3f06200
--- /dev/null
+++ b/extensions/Voting/web/style.css
@@ -0,0 +1,21 @@
+/* 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.
+ */
+
+/* Highlight the row for the bug being voted on */
+tr.bz_bug_being_voted_on {
+ background-color: #e2e2e2;
+}
+
+tr.bz_bug_being_voted_on td {
+ border-style: solid none solid none;
+ border-width: thin;
+}
+
+#votes_container {
+ white-space: nowrap;
+}
diff --git a/extensions/create.pl b/extensions/create.pl
new file mode 100755
index 0000000..7bcf103
--- /dev/null
+++ b/extensions/create.pl
@@ -0,0 +1,71 @@
+#!/usr/bin/perl -w
+# 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;
+use lib qw(. lib);
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Util qw(get_text);
+
+use File::Path qw(mkpath);
+
+my $base_dir = bz_locations()->{'extensionsdir'};
+
+my $name = $ARGV[0] or ThrowUserError('extension_create_no_name');
+$name = ucfirst($name);
+if ($name !~ /^[A-Z]/) {
+ ThrowUserError('extension_first_letter_caps', { name => $name });
+}
+
+my $extension_dir = "$base_dir/$name";
+mkpath($extension_dir)
+ || die "$extension_dir already exists or cannot be created.\n";
+
+my $lcname = lc($name);
+foreach my $path (qw(lib web template/en/default/hook),
+ "template/en/default/$lcname")
+{
+ mkpath("$extension_dir/$path") || die "$extension_dir/$path: $!";
+}
+
+my $template = Bugzilla->template;
+my $vars = { name => $name, path => $extension_dir };
+my %create_files = (
+ 'config.pm.tmpl' => 'Config.pm',
+ 'extension.pm.tmpl' => 'Extension.pm',
+ 'util.pm.tmpl' => 'lib/Util.pm',
+ 'web-readme.txt.tmpl' => 'web/README',
+ 'hook-readme.txt.tmpl' => 'template/en/default/hook/README',
+ 'name-readme.txt.tmpl' => "template/en/default/$lcname/README",
+);
+
+foreach my $template_file (keys %create_files) {
+ my $target = $create_files{$template_file};
+ my $output;
+ $template->process("extensions/$template_file", $vars, \$output)
+ or ThrowTemplateError($template->error());
+ open(my $fh, '>', "$extension_dir/$target");
+ print $fh $output;
+ close($fh);
+}
+
+print get_text('extension_created', $vars), "\n";
+
+__END__
+
+=head1 NAME
+
+extensions/create.pl - Create a framework for a new Bugzilla Extension.
+
+=head1 SYNOPSIS
+
+ extensions/create.pl NAME
+
+ Creates a framework for an extension called NAME in the F<extensions/>
+ directory.