diff options
Diffstat (limited to 'extensions')
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 <[% login FILTER html %]> 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&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 %] + →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 %] + →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 %] + →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 %] + →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 %] + →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&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&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&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&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 %] → + [% ELSE %] + Enter Votes Here → + [% 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&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. |