# 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 5.14.0;
use strict;
use warnings;

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({id => $bug_id, cache => 1}) 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);
    $bugs{$id}->{votes} = $v if $bugs{$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->{'message'} = 'votes_recorded';
}

######################
# 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 = Bugzilla::User->new($who)->login;

      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
      );

      my $name = Bugzilla::User->new($who)->login;
      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'));

        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 => $id, cache => 1});

  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;
