Commit 72bc89ef authored by Frédéric Buclin's avatar Frédéric Buclin

Bug 851267: Bugzilla times out when a user has several thousands of votes

r=dkl a=justdave
parent 2083d285
...@@ -466,9 +466,9 @@ sub redirect_search_url { ...@@ -466,9 +466,9 @@ sub redirect_search_url {
# GET requests that lacked a list_id are always redirected. POST requests # GET requests that lacked a list_id are always redirected. POST requests
# are only redirected if they're under the CGI_URI_LIMIT though. # are only redirected if they're under the CGI_URI_LIMIT though.
my $uri_length = length($self->self_url()); my $self_url = $self->self_url();
if ($self->request_method() ne 'POST' or $uri_length < CGI_URI_LIMIT) { if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) {
print $self->redirect(-url => $self->self_url()); print $self->redirect(-url => $self_url);
exit; exit;
} }
} }
...@@ -522,7 +522,7 @@ sub url_is_attachment_base { ...@@ -522,7 +522,7 @@ sub url_is_attachment_base {
$regex =~ s/\\\%bugid\\\%/\\d+/; $regex =~ s/\\\%bugid\\\%/\\d+/;
} }
$regex = "^$regex"; $regex = "^$regex";
return ($self->self_url =~ $regex) ? 1 : 0; return ($self->url =~ $regex) ? 1 : 0;
} }
########################## ##########################
......
...@@ -19,7 +19,7 @@ use Bugzilla::User; ...@@ -19,7 +19,7 @@ use Bugzilla::User;
use Bugzilla::Util qw(detaint_natural); use Bugzilla::Util qw(detaint_natural);
use Bugzilla::Token; use Bugzilla::Token;
use List::Util qw(min); use List::Util qw(min sum);
use constant VERSION => BUGZILLA_VERSION; use constant VERSION => BUGZILLA_VERSION;
use constant DEFAULT_VOTES_PER_BUG => 1; use constant DEFAULT_VOTES_PER_BUG => 1;
...@@ -185,7 +185,7 @@ sub bug_end_of_update { ...@@ -185,7 +185,7 @@ sub bug_end_of_update {
# If some votes have been removed, RemoveVotes() returns # If some votes have been removed, RemoveVotes() returns
# a list of messages to send to voters. # a list of messages to send to voters.
@msgs = _remove_votes($bug->id, 0, 'votes_bug_moved'); @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved');
_confirm_if_vote_confirmed($bug->id); _confirm_if_vote_confirmed($bug);
foreach my $msg (@msgs) { foreach my $msg (@msgs) {
MessageToMTA($msg); MessageToMTA($msg);
...@@ -418,46 +418,32 @@ sub _page_user { ...@@ -418,46 +418,32 @@ sub _page_user {
foreach my $product (@{ $user->get_selectable_products }) { foreach my $product (@{ $user->get_selectable_products }) {
next unless ($product->{votesperuser} > 0); next unless ($product->{votesperuser} > 0);
my @bugs;
my @bug_ids;
my $total = 0;
my $onevoteonly = 0;
my $vote_list = my $vote_list =
$dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count, $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count
bugs.short_desc
FROM votes FROM votes
INNER JOIN bugs INNER JOIN bugs
ON votes.bug_id = bugs.bug_id ON votes.bug_id = bugs.bug_id
WHERE votes.who = ? WHERE votes.who = ?
AND bugs.product_id = ? AND bugs.product_id = ?',
ORDER BY votes.bug_id',
undef, ($who->id, $product->id)); undef, ($who->id, $product->id));
$user->visible_bugs([map { $_->[0] } @$vote_list]); my %votes = map { $_->[0] => $_->[1] } @$vote_list;
foreach (@$vote_list) { my @bug_ids = sort keys %votes;
my ($id, $count, $summary) = @$_; # Exclude bugs that the user can no longer see.
$total += $count; @bug_ids = @{ $user->visible_bugs(\@bug_ids) };
next unless scalar @bug_ids;
# Next if user can't see this bug. So, the totals will be correct push(@all_bug_ids, @bug_ids);
# and they can see there are votes 'missing', but not on what bug my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
# they are. This seems a reasonable compromise; the alternative is $_->{count} = $votes{$_->id} foreach @bugs;
# to lie in the totals. # We include votes from bugs that the user can no longer see.
next if !$user->can_see_bug($id); my $total = sum(values %votes) || 0;
push (@bugs, { id => $id,
summary => $summary,
count => $count });
push (@bug_ids, $id);
push (@all_bug_ids, $id);
}
my $onevoteonly = 0;
$onevoteonly = 1 if (min($product->{votesperuser}, $onevoteonly = 1 if (min($product->{votesperuser},
$product->{maxvotesperbug}) == 1); $product->{maxvotesperbug}) == 1);
# Only add the product for display if there are any bugs in it. push(@products, { name => $product->name,
if ($#bugs > -1) {
push (@products, { name => $product->name,
bugs => \@bugs, bugs => \@bugs,
bug_ids => \@bug_ids, bug_ids => \@bug_ids,
onevoteonly => $onevoteonly, onevoteonly => $onevoteonly,
...@@ -465,7 +451,6 @@ sub _page_user { ...@@ -465,7 +451,6 @@ sub _page_user {
maxvotes => $product->{votesperuser}, maxvotes => $product->{votesperuser},
maxperbug => $product->{maxvotesperbug} }); maxperbug => $product->{maxvotesperbug} });
} }
}
if ($canedit && $bug) { if ($canedit && $bug) {
$dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?', $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?',
...@@ -498,6 +483,7 @@ sub _update_votes { ...@@ -498,6 +483,7 @@ sub _update_votes {
# IDs and the field values are the number of votes. # IDs and the field values are the number of votes.
my @buglist = grep {/^\d+$/} keys %$input; my @buglist = grep {/^\d+$/} keys %$input;
my (%bugs, %votes);
# If no bugs are in the buglist, let's make sure the user gets notified # If no bugs are in the buglist, let's make sure the user gets notified
# that their votes will get nuked if they continue. # that their votes will get nuked if they continue.
...@@ -513,16 +499,19 @@ sub _update_votes { ...@@ -513,16 +499,19 @@ sub _update_votes {
exit; exit;
} }
} }
else {
$user->visible_bugs(\@buglist);
my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist);
$bugs{$_->id} = $_ foreach @$bugs_obj;
}
# Call check() on each bug ID to make sure it is a positive # Call check_is_visible() on each bug to make sure it is an existing bug
# integer representing an existing bug that the user is authorized # that the user is authorized to access, and make sure the number of votes
# to access, and make sure the number of votes submitted is also # submitted is also an integer.
# a non-negative integer (a series of digits not preceded by a
# minus sign).
my (%votes, @bugs);
foreach my $id (@buglist) { foreach my $id (@buglist) {
my $bug = Bugzilla::Bug->check($id); my $bug = $bugs{$id}
push(@bugs, $bug); or ThrowUserError('bug_id_does_not_exist', { bug_id => $id });
$bug->check_is_visible;
$id = $bug->id; $id = $bug->id;
$votes{$id} = $input->{$id}; $votes{$id} = $input->{$id};
detaint_natural($votes{$id}) detaint_natural($votes{$id})
...@@ -539,10 +528,10 @@ sub _update_votes { ...@@ -539,10 +528,10 @@ sub _update_votes {
# If the user is voting for bugs, make sure they aren't overstuffing # If the user is voting for bugs, make sure they aren't overstuffing
# the ballot box. # the ballot box.
if (scalar @bugs) { if (scalar @buglist) {
my (%prodcount, %products); my (%prodcount, %products);
foreach my $bug (@bugs) { foreach my $bug_id (keys %bugs) {
my $bug_id = $bug->id; my $bug = $bugs{$bug_id};
my $prod = $bug->product; my $prod = $bug->product;
$products{$prod} ||= $bug->product_obj; $products{$prod} ||= $bug->product_obj;
$prodcount{$prod} ||= 0; $prodcount{$prod} ||= 0;
...@@ -566,56 +555,65 @@ sub _update_votes { ...@@ -566,56 +555,65 @@ sub _update_votes {
} }
} }
# Update the user's votes in the database. If the user did not submit # Update the user's votes in the database.
# any votes, they may be using a form with checkboxes to remove all their
# votes (checkboxes are not submitted along with other form data when
# they are not checked, and Bugzilla uses them to represent single votes
# for products that only allow one vote per bug). In that case, we still
# need to clear the user's votes from the database.
my %affected;
$dbh->bz_start_transaction(); $dbh->bz_start_transaction();
# Take note of, and delete the user's old votes from the database. my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes
my $bug_list = $dbh->selectcol_arrayref('SELECT bug_id FROM votes
WHERE who = ?', undef, $who); WHERE who = ?', undef, $who);
foreach my $id (@$bug_list) { my %old_votes = map { $_->[0] => $_->[1] } @$old_list;
$affected{$id} = 1;
}
$dbh->do('DELETE FROM votes WHERE who = ?', undef, $who);
my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count) my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count)
VALUES (?, ?, ?)'); VALUES (?, ?, ?)');
my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ?
WHERE bug_id = ? AND who = ?');
# Insert the new values in their place my %affected = map { $_ => 1 } (@buglist, keys %old_votes);
foreach my $id (@buglist) { my @deleted_votes;
if ($votes{$id} > 0) {
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}); $sth_insertVotes->execute($who, $id, $votes{$id});
} }
$affected{$id} = 1; }
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 # Update the cached values in the bugs table
print $cgi->header();
my @updated_bugs = (); my @updated_bugs = ();
my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes
WHERE bug_id = ?"); WHERE bug_id = ?");
my $sth_updateVotes = $dbh->prepare("UPDATE bugs SET votes = ? $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
WHERE bug_id = ?");
foreach my $id (keys %affected) { foreach my $id (keys %affected) {
$sth_getVotes->execute($id); $sth_getVotes->execute($id);
my $v = $sth_getVotes->fetchrow_array || 0; my $v = $sth_getVotes->fetchrow_array || 0;
$sth_updateVotes->execute($v, $id); $sth_updateVotes->execute($v, $id);
my $confirmed = _confirm_if_vote_confirmed($id); my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
push (@updated_bugs, $id) if $confirmed; push (@updated_bugs, $id) if $confirmed;
} }
$dbh->bz_commit_transaction(); $dbh->bz_commit_transaction();
print $cgi->header() if scalar @updated_bugs;
$vars->{'type'} = "votes"; $vars->{'type'} = "votes";
$vars->{'title_tag'} = 'change_votes'; $vars->{'title_tag'} = 'change_votes';
foreach my $bug_id (@updated_bugs) { foreach my $bug_id (@updated_bugs) {
...@@ -826,7 +824,7 @@ sub _remove_votes { ...@@ -826,7 +824,7 @@ sub _remove_votes {
# confirm a bug has been reduced, check if the bug is now confirmed. # confirm a bug has been reduced, check if the bug is now confirmed.
sub _confirm_if_vote_confirmed { sub _confirm_if_vote_confirmed {
my $id = shift; my $id = shift;
my $bug = new Bugzilla::Bug($id); my $bug = ref $id ? $id : new Bugzilla::Bug($id);
my $ret = 0; my $ret = 0;
if (!$bug->everconfirmed if (!$bug->everconfirmed
......
...@@ -95,8 +95,7 @@ ...@@ -95,8 +95,7 @@
</tr> </tr>
[% FOREACH bug = product.bugs %] [% FOREACH bug = product.bugs %]
<tr [% IF bug.id == this_bug.id && canedit %] <tr [% IF bug.id == this_bug.id && canedit %] class="bz_bug_being_voted_on"[% END %]>
class="bz_bug_being_voted_on" [% END %]>
<td> <td>
[% IF bug.id == this_bug.id && canedit %] [% IF bug.id == this_bug.id && canedit %]
[% IF product.onevoteonly %] [% IF product.onevoteonly %]
...@@ -106,25 +105,25 @@ ...@@ -106,25 +105,25 @@
[% END %] [% END %]
[%- END %] [%- END %]
</td> </td>
<td align="right"><a name="vote_[% bug.id FILTER html %]"> <td align="right"><a name="vote_[% bug.id FILTER none %]">
[% IF canedit %] [% IF canedit %]
[% IF product.onevoteonly %] [% IF product.onevoteonly %]
<input type="checkbox" name="[% bug.id FILTER html %]" value="1" <input type="checkbox" name="[% bug.id FILTER none %]" value="1"
[% " checked" IF bug.count %] id="bug_[% bug.id FILTER html %]"> [% " checked" IF bug.count %] id="bug_[% bug.id FILTER none %]">
[% ELSE %] [% ELSE %]
<input name="[% bug.id FILTER html %]" value="[% bug.count FILTER html %]" <input name="[% bug.id FILTER none %]" value="[% bug.count FILTER html %]"
size="2" id="bug_[% bug.id FILTER html %]"> size="2" id="bug_[% bug.id FILTER none %]">
[% END %] [% END %]
[% ELSE %] [% ELSE %]
[% bug.count FILTER html %] [% bug.count FILTER html %]
[% END %] [% END %]
</a></td> </a></td>
<td align="center"> <td align="center">
[% bug.id FILTER bug_link(bug) FILTER none %] [% PROCESS bug/link.html.tmpl bug = bug, link_text = bug.id %]
</td> </td>
<td> <td>
[% bug.summary FILTER html %] [% bug.short_desc FILTER html %]
(<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id FILTER uri %]">Show Votes</a>) (<a href="page.cgi?id=voting/bug.html&amp;bug_id=[% bug.id FILTER none %]">Show Votes</a>)
</td> </td>
</tr> </tr>
[% END %] [% END %]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment