diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm index d1e397b2d..233415b79 100644 --- a/Bugzilla/Chart.pm +++ b/Bugzilla/Chart.pm @@ -1,5 +1,3 @@ -# -*- Mode: perl; indent-tabs-mode: nil -*- -# # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of @@ -39,7 +37,8 @@ use Date::Format; use Date::Parse; use List::Util qw(max); -sub new { +sub new +{ my $invocant = shift; my $class = ref($invocant) || $invocant; my ($params) = @_; @@ -56,157 +55,170 @@ sub new { return $self; } -sub init { +sub init +{ my $self = shift; my ($params) = @_; - # The data structure is a list of lists (lines) of Series objects. + # The data structure is a list of lists (lines) of Series objects. # There is a separate list for the labels. # # The URL encoding is: # line0=67&line0=73&line1=81&line2=67... # &label0=B+/+R+/+NEW&label1=... - # &select0=1&select3=1... + # &select0=1&select3=1... # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... - # >=1&labelgt=Grand+Total - foreach my $param (keys %$params) { + # >=1&labelgt=Grand+Total + foreach my $param (keys %$params) + { # Store all the lines - if ($param =~ /^line(\d+)$/) { - foreach my $series_id (list $params->{$param}) { - detaint_natural($series_id) - || ThrowCodeError("invalid_series_id"); + if ($param =~ /^line(\d+)$/) + { + foreach my $series_id (list $params->{$param}) + { + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); my $series = new Bugzilla::Series($series_id); - push(@{$self->{'lines'}[$1]}, $series) if $series; + push @{$self->{lines}[$1]}, $series if $series; } } # Store all the labels - if ($param =~ /^label(\d+)$/) { - $self->{'labels'}[$1] = $params->{$param}; - } + if ($param =~ /^label(\d+)$/) + { + $self->{labels}[$1] = $params->{$param}; + } } - + # Store the miscellaneous metadata - $self->{'cumulate'} = $params->{cumulate} ? 1 : 0; - $self->{'gt'} = $params->{gt} ? 1 : 0; - $self->{'labelgt'} = $params->{labelgt}; - $self->{'datefrom'} = $params->{datefrom}; - $self->{'dateto'} = $params->{dateto}; - + $self->{cumulate} = $params->{cumulate} ? 1 : 0; + $self->{gt} = $params->{gt} ? 1 : 0; + $self->{labelgt} = $params->{labelgt}; + $self->{datefrom} = $params->{datefrom}; + $self->{dateto} = $params->{dateto}; + # If we are cumulating, a grand total makes no sense - $self->{'gt'} = 0 if $self->{'cumulate'}; - + $self->{gt} = 0 if $self->{cumulate}; + # Make sure the dates are ones we are able to interpret - foreach my $date ('datefrom', 'dateto') { - if ($self->{$date}) { - $self->{$date} = str2time($self->{$date}) - || ThrowUserError("illegal_date", { date => $self->{$date}}); + foreach my $date ('datefrom', 'dateto') + { + if ($self->{$date}) + { + $self->{$date} = str2time($self->{$date}) + || ThrowUserError("illegal_date", { date => $self->{$date}}); } } # datefrom can't be after dateto - if ($self->{'datefrom'} && $self->{'dateto'} && - $self->{'datefrom'} > $self->{'dateto'}) + if ($self->{datefrom} && $self->{dateto} && $self->{datefrom} > $self->{dateto}) { - ThrowUserError("misarranged_dates", - {'datefrom' => $params->{datefrom}, - 'dateto' => $params->{dateto}}); - } + ThrowUserError("misarranged_dates", { + datefrom => $params->{datefrom}, + dateto => $params->{dateto}, + }); + } } # Alter Chart so that the selected series are added to it. -sub add { +sub add +{ my $self = shift; my @series_ids = @_; # Get the current size of the series; required for adding Grand Total later my $current_size = scalar($self->getSeriesIDs()); - + # Count the number of added series my $added = 0; # Create new Series and push them on to the list of lines. # Note that new lines have no label; the display template is responsible # for inventing something sensible. - foreach my $series_id (@series_ids) { + foreach my $series_id (@series_ids) + { my $series = new Bugzilla::Series($series_id); - if ($series) { - push(@{$self->{'lines'}}, [$series]); - push(@{$self->{'labels'}}, ""); + if ($series) + { + push @{$self->{lines}}, [ $series ]; + push @{$self->{labels}}, ""; $added++; } } - + # If we are going from < 2 to >= 2 series, add the Grand Total line. - if (!$self->{'gt'}) { - if ($current_size < 2 && - $current_size + $added >= 2) - { - $self->{'gt'} = 1; - } + if (!$self->{gt} && $current_size < 2 && $current_size+$added >= 2) + { + $self->{gt} = 1; } } # Alter Chart so that the selections are removed from it. -sub remove { +sub remove +{ my $self = shift; my @line_ids = @_; - - foreach my $line_id (@line_ids) { - if ($line_id == 65536) { + foreach my $line_id (@line_ids) + { + if ($line_id == 65536) + { # Magic value - delete Grand Total. - $self->{'gt'} = 0; - } - else { - delete($self->{'lines'}->[$line_id]); - delete($self->{'labels'}->[$line_id]); + $self->{gt} = 0; + } + else + { + delete($self->{lines}->[$line_id]); + delete($self->{labels}->[$line_id]); } } } # Alter Chart so that the selections are summed. -sub sum { +sub sum +{ my $self = shift; my @line_ids = @_; - + # We can't add the Grand Total to things. - @line_ids = grep(!/^65536$/, @line_ids); - + @line_ids = grep !/^65536$/, @line_ids; + # We can't add less than two things. return if scalar(@line_ids) < 2; - + my @series; my $label = ""; my $biggestlength = 0; - + # We rescue the Series objects of all the series involved in the sum. - foreach my $line_id (@line_ids) { - my @line = @{$self->{'lines'}->[$line_id]}; - - foreach my $series (@line) { + foreach my $line_id (@line_ids) + { + my @line = @{$self->{lines}->[$line_id]}; + foreach my $series (@line) + { push(@series, $series); } - # We keep the label that labels the line with the most series. - if (scalar(@line) > $biggestlength) { + if (scalar(@line) > $biggestlength) + { $biggestlength = scalar(@line); - $label = $self->{'labels'}->[$line_id]; + $label = $self->{labels}->[$line_id]; } } $self->remove(@line_ids); - push(@{$self->{'lines'}}, \@series); - push(@{$self->{'labels'}}, $label); + push(@{$self->{lines}}, \@series); + push(@{$self->{labels}}, $label); } -sub data { +sub data +{ my $self = shift; - $self->{'_data'} ||= $self->readData(); - return $self->{'_data'}; + $self->{_data} ||= $self->readData(); + return $self->{_data}; } -# Convert the Chart's data into a plottable form in $self->{'_data'}. -sub readData { +# Convert the Chart's data into a plottable form in $self->{_data}. +sub readData +{ my $self = shift; my @data; my @maxvals; @@ -219,25 +231,26 @@ sub readData { # Work out the date boundaries for our data. my $dbh = Bugzilla->dbh; - + # The date used is the one given if it's in a sensible range; otherwise, # it's the earliest or latest date in the database as appropriate. - my $datefrom = $dbh->selectrow_array("SELECT MIN(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); + my $datefrom = $dbh->selectrow_array( + "SELECT MIN(series_date) FROM series_data WHERE series_id IN ($series_ids)" + ); $datefrom = str2time($datefrom); - if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { - $datefrom = $self->{'datefrom'}; + if ($self->{datefrom} && $self->{datefrom} > $datefrom) + { + $datefrom = $self->{datefrom}; } - my $dateto = $dbh->selectrow_array("SELECT MAX(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); - $dateto = str2time($dateto); + my $dateto = $dbh->selectrow_array( + "SELECT MAX(series_date) FROM series_data WHERE series_id IN ($series_ids)" + ); + $dateto = str2time($dateto); - if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { - $dateto = $self->{'dateto'}; + if ($self->{dateto} && $self->{dateto} < $dateto) { + $dateto = $self->{dateto}; } # Convert UNIX times back to a date format usable for SQL queries. @@ -246,55 +259,61 @@ sub readData { # Prepare the query which retrieves the data for each series my $query = "SELECT " . $dbh->sql_to_days('series_date') . " - " . - $dbh->sql_to_days('?') . ", series_value " . - "FROM series_data " . - "WHERE series_id = ? " . - "AND series_date >= ?"; - if ($dateto) { + $dbh->sql_to_days('?') . ", series_value" . + " FROM series_data WHERE series_id = ? AND series_date >= ?"; + if ($dateto) + { $query .= " AND series_date <= ?"; } - + my $sth = $dbh->prepare($query); - my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; + my $gt_index = $self->{gt} ? scalar @{$self->{lines}} : undef; my $line_index = 0; $maxvals[$gt_index] = 0 if $gt_index; my @datediff_total; - foreach my $line (@{$self->{'lines'}}) { + foreach my $line (@{$self->{lines}}) + { # Even if we end up with no data, we need an empty arrayref to prevent # errors in the PNG-generating code $data[$line_index] = []; $maxvals[$line_index] = 0; - foreach my $series (@$line) { - + foreach my $series (@$line) + { # Get the data for this series and add it on - if ($dateto) { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to); + if ($dateto) + { + $sth->execute($sql_from, $series->{series_id}, $sql_from, $sql_to); } - else { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from); + else + { + $sth->execute($sql_from, $series->{series_id}, $sql_from); } my $points = $sth->fetchall_arrayref(); - foreach my $point (@$points) { + foreach my $point (@$points) + { my ($datediff, $value) = @$point; $data[$line_index][$datediff] ||= 0; $data[$line_index][$datediff] += $value; - if ($data[$line_index][$datediff] > $maxvals[$line_index]) { + if ($data[$line_index][$datediff] > $maxvals[$line_index]) + { $maxvals[$line_index] = $data[$line_index][$datediff]; } $datediff_total[$datediff] += $value; # Add to the grand total, if we are doing that - if ($gt_index) { + if ($gt_index) + { $data[$gt_index][$datediff] ||= 0; $data[$gt_index][$datediff] += $value; - if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) { + if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) + { $maxvals[$gt_index] = $data[$gt_index][$datediff]; } } @@ -306,54 +325,62 @@ sub readData { } # calculate maximum y value - if ($self->{'cumulate'}) { + if ($self->{cumulate}) + { # Make sure we do not try to take the max of an array with undef values my @processed_datediff; - while (@datediff_total) { + while (@datediff_total) + { my $datediff = shift @datediff_total; push @processed_datediff, $datediff if defined($datediff); } - $self->{'y_max_value'} = max(@processed_datediff); + $self->{y_max_value} = max(@processed_datediff); } - else { - $self->{'y_max_value'} = max(@maxvals); + else + { + $self->{y_max_value} = max(@maxvals); } - $self->{'y_max_value'} |= 1; # For log() + $self->{y_max_value} |= 1; # For log() # Align the max y value: # For one- or two-digit numbers, increase y_max_value until divisible by 8 # For larger numbers, see the comments below to figure out what's going on - if ($self->{'y_max_value'} < 100) { - do { - ++$self->{'y_max_value'}; - } while ($self->{'y_max_value'} % 8 != 0); + if ($self->{y_max_value} < 100) + { + do + { + ++$self->{y_max_value}; + } while ($self->{y_max_value} % 8 != 0); } - else { + else + { # First, get the # of digits in the y_max_value - my $num_digits = 1+int(log($self->{'y_max_value'})/log(10)); + my $num_digits = 1+int(log($self->{y_max_value})/log(10)); # We want to zero out all but the top 2 digits my $mask_length = $num_digits - 2; - $self->{'y_max_value'} /= 10**$mask_length; - $self->{'y_max_value'} = int($self->{'y_max_value'}); - $self->{'y_max_value'} *= 10**$mask_length; + $self->{y_max_value} /= 10**$mask_length; + $self->{y_max_value} = int($self->{y_max_value}); + $self->{y_max_value} *= 10**$mask_length; # Add 10^$mask_length to the max value # Continue to increase until it's divisible by 8 * 10^($mask_length-1) # (Throwing in the -1 keeps at least the smallest digit at zero) - do { - $self->{'y_max_value'} += 10**$mask_length; - } while ($self->{'y_max_value'} % (8*(10**($mask_length-1))) != 0); + do + { + $self->{y_max_value} += 10**$mask_length; + } while ($self->{y_max_value} % (8*(10**($mask_length-1))) != 0); } - + # Add the x-axis labels into the data structure my $date_progression = generateDateProgression($datefrom, $dateto); - unshift(@data, $date_progression); + unshift @data, $date_progression; - if ($self->{'gt'}) { + if ($self->{gt}) + { # Add Grand Total to label list - push(@{$self->{'labels'}}, $self->{'labelgt'}); + push @{$self->{labels}}, $self->{labelgt}; $data[$gt_index] ||= []; } @@ -362,43 +389,42 @@ sub readData { } # Flatten the data structure into a list of series_ids -sub getSeriesIDs { +sub getSeriesIDs +{ my $self = shift; my @series_ids; - - foreach my $line (@{$self->{'lines'}}) { - foreach my $series (@$line) { - push(@series_ids, $series->{'series_id'}); + foreach my $line (@{$self->{lines}}) + { + foreach my $series (@$line) + { + push @series_ids, $series->{series_id}; } } - return @series_ids; } # Class method to get the data necessary to populate the "select series" # widgets on various pages. -sub getVisibleSeries { +sub getVisibleSeries +{ my %cats; my $grouplist = Bugzilla->user->groups_as_string; - + # Get all visible series my $dbh = Bugzilla->dbh; - my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " . - "series.name, series.series_id " . - "FROM series " . - "INNER JOIN series_categories AS cc1 " . - " ON series.category = cc1.id " . - "INNER JOIN series_categories AS cc2 " . - " ON series.subcategory = cc2.id " . - "LEFT JOIN category_group_map AS cgm " . - " ON series.category = cgm.category_id " . - " AND cgm.group_id NOT IN($grouplist) " . - "WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL) " . - $dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' . - 'series.name'), - undef, Bugzilla->user->id); - foreach my $series (@$serieses) { + my $serieses = $dbh->selectall_arrayref( + "SELECT cc1.name, cc2.name, series.name, series.series_id FROM series". + " INNER JOIN series_categories AS cc1 ON series.category = cc1.id" . + " INNER JOIN series_categories AS cc2 ON series.subcategory = cc2.id" . + " LEFT JOIN category_group_map AS cgm ON series.category = cgm.category_id" . + " AND cgm.group_id NOT IN ($grouplist)" . + " WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL)" . + " GROUP BY series.series_id, cc1.name, cc2.name, series.name", + undef, Bugzilla->user->id + ); + foreach my $series (@$serieses) + { my ($cat, $subcat, $name, $series_id) = @$series; $cats{$cat}{$subcat}{$name} = $series_id; } @@ -406,7 +432,8 @@ sub getVisibleSeries { return \%cats; } -sub generateDateProgression { +sub generateDateProgression +{ my ($datefrom, $dateto) = @_; my @progression; @@ -420,15 +447,17 @@ sub generateDateProgression { $datefrom += $oneday / 3; $dateto += (2 * $oneday) / 3; - while ($datefrom < $dateto) { - push (@progression, time2str("%Y-%m-%d", $datefrom)); + while ($datefrom < $dateto) + { + push @progression, time2str("%Y-%m-%d", $datefrom); $datefrom += $oneday; } return \@progression; } -sub dump { +sub dump +{ my $self = shift; # Make sure we've read in our data diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm index c0ff9e40a..e0a96d74a 100644 --- a/Bugzilla/Series.pm +++ b/Bugzilla/Series.pm @@ -1,5 +1,3 @@ -# -*- Mode: perl; indent-tabs-mode: nil -*- -# # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of @@ -24,7 +22,7 @@ use strict; # This module implements a series - a set of data to be plotted on a chart. # -# This Series is in the database if and only if self->{'series_id'} is defined. +# This Series is in the database if and only if self->{series_id} is defined. # Note that the series being in the database does not mean that the fields of # this object are the same as the DB entries, as the object may have been # altered. @@ -33,6 +31,7 @@ package Bugzilla::Series; use Bugzilla::Error; use Bugzilla::Util; +use Bugzilla::User; sub new { @@ -108,47 +107,51 @@ sub set_all $self->{series_id} ||= $self->existsInDatabase(); } -sub writeToDatabase { +sub writeToDatabase +{ my $self = shift; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); + my $category_id = getCategoryID($self->{category}); + my $subcategory_id = getCategoryID($self->{subcategory}); my $exists; - if ($self->{'series_id'}) { - $exists = - $dbh->selectrow_array("SELECT series_id FROM series - WHERE series_id = $self->{'series_id'}"); + if ($self->{series_id}) + { + $exists = $dbh->selectrow_array( + "SELECT series_id FROM series WHERE series_id = ?", + undef, $self->{series_id} + ); } # Is this already in the database? - if ($exists) { + if ($exists) + { # Update existing series my $dbh = Bugzilla->dbh; - $dbh->do("UPDATE series SET " . - "category = ?, subcategory = ?," . - "name = ?, frequency = ?, is_public = ? " . - "WHERE series_id = ?", undef, - $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'public'}, - $self->{'series_id'}); + $dbh->do( + "UPDATE series SET category = ?, subcategory = ?,". + " name = ?, frequency = ?, is_public = ? WHERE series_id = ?", undef, + $category_id, $subcategory_id, $self->{name}, + $self->{frequency}, $self->{public}, $self->{series_id} + ); } - else { + else + { # Insert the new series into the series table - $dbh->do("INSERT INTO series (creator, category, subcategory, " . - "name, frequency, query, is_public) VALUES " . - "(?, ?, ?, ?, ?, ?, ?)", undef, - $self->{'creator_id'}, $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'query'}, $self->{'public'}); + $dbh->do( + "INSERT INTO series (creator, category, subcategory, " . + "name, frequency, query, is_public) VALUES " . + "(?, ?, ?, ?, ?, ?, ?)", undef, + $self->{creator_id}, $category_id, $subcategory_id, $self->{name}, + $self->{frequency}, $self->{query}, $self->{public} + ); # Retrieve series_id - $self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " . - "FROM series"); - $self->{'series_id'} - || ThrowCodeError("missing_series_id", { 'series' => $self }); + $self->{series_id} = $dbh->bz_last_key('series', 'series_id'); + $self->{series_id} || ThrowCodeError("missing_series_id", { 'series' => $self }); } $dbh->bz_commit_transaction(); @@ -156,63 +159,66 @@ sub writeToDatabase { # Check whether a series with this name, category and subcategory exists in # the DB and, if so, returns its series_id. -sub existsInDatabase { +sub existsInDatabase +{ my $self = shift; my $dbh = Bugzilla->dbh; - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); + my $category_id = getCategoryID($self->{category}); + my $subcategory_id = getCategoryID($self->{subcategory}); - trick_taint($self->{'name'}); - my $series_id = $dbh->selectrow_array("SELECT series_id " . - "FROM series WHERE category = $category_id " . - "AND subcategory = $subcategory_id AND name = " . - $dbh->quote($self->{'name'})); + trick_taint($self->{name}); + my ($series_id) = $dbh->selectrow_array( + "SELECT series_id FROM series WHERE category=? AND subcategory=? AND name=?", + undef, $category_id, $subcategory_id, $self->{name} + ); - return($series_id); + return $series_id; } # Get a category or subcategory IDs, creating the category if it doesn't exist. -sub getCategoryID { +sub getCategoryID +{ my ($category) = @_; my $category_id; my $dbh = Bugzilla->dbh; # This seems for the best idiom for "Do A. Then maybe do B and A again." - while (1) { + while (1) + { # We are quoting this to put it in the DB, so we can remove taint trick_taint($category); - $category_id = $dbh->selectrow_array("SELECT id " . - "from series_categories " . - "WHERE name =" . $dbh->quote($category)); + $category_id = $dbh->selectrow_array( + "SELECT id FROM series_categories WHERE name=?", undef, $category + ); + last if defined $category_id; - last if defined($category_id); - - $dbh->do("INSERT INTO series_categories (name) " . - "VALUES (" . $dbh->quote($category) . ")"); + $dbh->do("INSERT INTO series_categories (name) VALUES (?)", undef, $category); } return $category_id; } -########## -# Methods -########## -sub id { return $_[0]->{'series_id'}; } -sub name { return $_[0]->{'name'}; } +########### +# Methods # +########### -sub creator { +sub id { $_[0]->{series_id} } +sub name { $_[0]->{name} } + +sub creator +{ my $self = shift; - - if (!$self->{creator} && $self->{creator_id}) { - require Bugzilla::User; + if (!$self->{creator} && $self->{creator_id}) + { $self->{creator} = new Bugzilla::User($self->{creator_id}); } return $self->{creator}; } -sub remove_from_db { +sub remove_from_db +{ my $self = shift; my $dbh = Bugzilla->dbh; diff --git a/chart.cgi b/chart.cgi index e3a4c6e15..1e2c9819d 100755 --- a/chart.cgi +++ b/chart.cgi @@ -1,6 +1,4 @@ #!/usr/bin/perl -wT -# -*- Mode: perl; indent-tabs-mode: nil -*- -# # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of @@ -62,7 +60,8 @@ my $dbh = Bugzilla->dbh; my $user = Bugzilla->login(LOGIN_REQUIRED); -if (!Bugzilla->feature('new_charts')) { +if (!Bugzilla->feature('new_charts')) +{ ThrowCodeError('feature_disabled', { feature => 'new_charts' }); } @@ -77,14 +76,15 @@ if (grep /^cmd-/, keys %$ARGS) my $action = $ARGS->{action}; my $series_id = $ARGS->{series_id}; -$vars->{'doc_section'} = 'reporting.html#charts'; +$vars->{doc_section} = 'reporting.html#charts'; # Because some actions are chosen by buttons, we can't encode them as the value # of the action param, because that value is localization-dependent. So, we # encode it in the name, as "action-". Some params even contain the # series_id they apply to (e.g. subscribe, unsubscribe). my @actions = grep /^action-/, keys %$ARGS; -if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) { +if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) +{ $action = $1; $series_id = $2 if $2; } @@ -92,26 +92,30 @@ if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) { $action ||= "assemble"; # Go to buglist.cgi if we are doing a search. -if ($action eq "search") { +if ($action eq "search") +{ delete $ARGS->{$_} for qw(format ctype action); my $params = http_build_query($ARGS); print Bugzilla->cgi->redirect("buglist.cgi" . ($params ? "?$params" : "")); exit; } -$user->in_group(Bugzilla->params->{"chartgroup"}) - || ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"}, - action => "use", - object => "charts"}); +$user->in_group(Bugzilla->params->{chartgroup}) || ThrowUserError("auth_failure", { + group => Bugzilla->params->{chartgroup}, + action => "use", + object => "charts", +}); # Only admins may create public queries $user->in_group('admin') || delete $ARGS->{public}; # All these actions relate to chart construction. -if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) { +if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) +{ # These two need to be done before the creation of the Chart object, so # that the changes they make will be reflected in it. - if ($action =~ /^subscribe|unsubscribe$/) { + if ($action =~ /^subscribe|unsubscribe$/) + { detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); my $series = new Bugzilla::Series($series_id); $series->$action($user->id); @@ -119,31 +123,38 @@ if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) { my $chart = new Bugzilla::Chart($ARGS); - if ($action =~ /^remove|sum$/) { + if ($action eq 'remove' || $action eq 'sum') + { $chart->$action(getSelectedLines()); } - elsif ($action eq "add") { + elsif ($action eq "add") + { my @series_ids = getAndValidateSeriesIDs(); $chart->add(@series_ids); } view($chart); } -elsif ($action eq "plot") { +elsif ($action eq "plot") +{ plot(); } -elsif ($action eq "wrap") { +elsif ($action eq "wrap") +{ # For CSV "wrap", we go straight to "plot". - if ($ARGS->{ctype} && $ARGS->{ctype} eq "csv") { + if ($ARGS->{ctype} && $ARGS->{ctype} eq "csv") + { plot(); } - else { + else + { wrap(); } } -elsif ($action eq "create") { +elsif ($action eq "create") +{ assertCanCreate(); - check_hash_token($ARGS->{token}, ['create-series']); + check_hash_token($ARGS->{token}, [ 'create-series' ]); my $q = { %$ARGS }; delete $q->{$_} for qw(series_id category newcategory subcategory newsubcategory name frequency public); @@ -156,21 +167,25 @@ elsif ($action eq "create") { query => http_build_query($q), }); - ThrowUserError("series_already_exists", {'series' => $series}) + # Check if another series with the same name exists + # FIXME: Should be done by validator + ThrowUserError("series_already_exists", { series => $series }) if $series->existsInDatabase; $series->writeToDatabase(); - $vars->{'message'} = "series_created"; - $vars->{'series'} = $series; + $vars->{message} = "series_created"; + $vars->{series} = $series; my $chart = new Bugzilla::Chart($ARGS); view($chart); } -elsif ($action eq "edit") { +elsif ($action eq "edit") +{ my $series = assertCanEdit($series_id); edit($series); } -elsif ($action eq "alter") { +elsif ($action eq "alter") +{ my $series = assertCanEdit($series_id); check_hash_token($ARGS->{token}, [ $series->id, $series->name ]); @@ -188,22 +203,24 @@ elsif ($action eq "alter") { # the return value is us or some other series we need to avoid stomping # on. my $id_of_series_in_db = $series->existsInDatabase(); - if (defined($id_of_series_in_db) && $id_of_series_in_db != $series->{'series_id'}) + if (defined($id_of_series_in_db) && $id_of_series_in_db != $series->{series_id}) { ThrowUserError("series_already_exists", {'series' => $series}); } $series->writeToDatabase(); - $vars->{'changes_saved'} = 1; + $vars->{changes_saved} = 1; edit($series); } -elsif ($action eq "confirm-delete") { - $vars->{'series'} = assertCanEdit($series_id); +elsif ($action eq "confirm-delete") +{ + $vars->{series} = assertCanEdit($series_id); $template->process("reports/delete-series.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); } -elsif ($action eq "delete") { +elsif ($action eq "delete") +{ my $series = assertCanEdit($series_id); check_hash_token($ARGS->{token}, [$series->id, $series->name]); @@ -211,25 +228,30 @@ elsif ($action eq "delete") { $series->remove_from_db(); # Remove (sub)categories which no longer have any series. - foreach my $cat (qw(category subcategory)) { - my $is_used = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?", - undef, $series->{"${cat}_id"}); - if (!$is_used) { - $dbh->do('DELETE FROM series_categories WHERE id = ?', - undef, $series->{"${cat}_id"}); + foreach my $cat (qw(category subcategory)) + { + my $is_used = $dbh->selectrow_array( + "SELECT COUNT(*) FROM series WHERE $cat = ?", + undef, $series->{$cat.'_id'} + ); + if (!$is_used) + { + $dbh->do('DELETE FROM series_categories WHERE id = ?', undef, $series->{$cat.'_id'}); } } $dbh->bz_commit_transaction(); - $vars->{'message'} = "series_deleted"; - $vars->{'series'} = $series; + $vars->{message} = "series_deleted"; + $vars->{series} = $series; view(); } -elsif ($action eq "convert_search") { +elsif ($action eq "convert_search") +{ my $saved_search = $ARGS->{series_from_search} || ''; my ($query) = grep { $_->name eq $saved_search } @{ $user->queries }; my $url = ''; - if ($query) { + if ($query) + { my $params = http_decode_query($query->query); # These two parameters conflict with the one below. delete $params->{$_} for ('format', 'query_format'); @@ -237,35 +259,37 @@ elsif ($action eq "convert_search") { } print Bugzilla->cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url"); } -else { +else +{ ThrowCodeError("unknown_action"); } exit; # Find any selected series and return either the first or all of them. -sub getAndValidateSeriesIDs { - my @series_ids = grep(/^\d+$/, list Bugzilla->input_params->{name}); - +sub getAndValidateSeriesIDs +{ + my @series_ids = grep /^\d+$/, list Bugzilla->input_params->{name}; return wantarray ? @series_ids : $series_ids[0]; } # Return a list of IDs of all the lines selected in the UI. -sub getSelectedLines { - my @ids = map { /^select(\d+)$/ ? $1 : () } keys %{ Bugzilla->input_params }; - - return @ids; +sub getSelectedLines +{ + return map { /^select(\d+)$/ ? $1 : () } keys %{ Bugzilla->input_params }; } -# Check if the user is the owner of series_id or is an admin. -sub assertCanEdit { +# Check if the user is the owner of series_id or is an admin. +sub assertCanEdit +{ my $series_id = shift; my $user = Bugzilla->user; my $series = new Bugzilla::Series($series_id) - || ThrowCodeError('invalid_series_id'); + || ThrowCodeError('invalid_series_id'); - if (!$user->in_group('admin') && $series->{creator_id} != $user->id) { + if (!$user->in_group('admin') && $series->{creator_id} != $user->id) + { ThrowUserError('illegal_series_edit'); } @@ -273,104 +297,115 @@ sub assertCanEdit { } # Check if the user is permitted to create this series with these parameters. -sub assertCanCreate { +sub assertCanCreate +{ my $user = Bugzilla->user; $user->in_group("editbugs") || ThrowUserError("illegal_series_creation"); # Check permission for frequency my $min_freq = 7; - if (Bugzilla->input_params->{frequency} < $min_freq && !$user->in_group("admin")) { + if (Bugzilla->input_params->{frequency} < $min_freq && !$user->in_group("admin")) + { ThrowUserError("illegal_frequency", { 'minimum' => $min_freq }); } } -sub validateWidthAndHeight { - $vars->{'width'} = Bugzilla->input_params->{width}; - $vars->{'height'} = Bugzilla->input_params->{height}; +sub validateWidthAndHeight +{ + $vars->{width} = Bugzilla->input_params->{width}; + $vars->{height} = Bugzilla->input_params->{height}; - if (defined($vars->{'width'})) { - (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0) - || ThrowCodeError("invalid_dimensions"); + if (defined($vars->{width})) + { + (detaint_natural($vars->{width}) && $vars->{width} > 0) + || ThrowCodeError("invalid_dimensions"); } - if (defined($vars->{'height'})) { - (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0) - || ThrowCodeError("invalid_dimensions"); + if (defined($vars->{height})) + { + (detaint_natural($vars->{height}) && $vars->{height} > 0) + || ThrowCodeError("invalid_dimensions"); } # The equivalent of 2000 square seems like a very reasonable maximum size. # This is merely meant to prevent accidental or deliberate DOS, and should # have no effect in practice. - if ($vars->{'width'} && $vars->{'height'}) { - (($vars->{'width'} * $vars->{'height'}) <= 4000000) - || ThrowUserError("chart_too_large"); + if ($vars->{width} && $vars->{height} && $vars->{width} * $vars->{height} > 4000000) + { + ThrowUserError("chart_too_large"); } } -sub edit { +sub edit +{ my $series = shift; - $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); - $vars->{'default'} = $series; + $vars->{category} = Bugzilla::Chart::getVisibleSeries(); + $vars->{default} = $series; $template->process("reports/edit-series.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); } -sub plot { +sub plot +{ validateWidthAndHeight(); my $ARGS = Bugzilla->input_params; - $vars->{'chart'} = new Bugzilla::Chart($ARGS); + $vars->{chart} = new Bugzilla::Chart($ARGS); my $format = $template->get_format("reports/chart", "", $ARGS->{ctype}); # Debugging PNGs is a pain; we need to be able to see the error messages - if ($ARGS->{debug}) { + if ($ARGS->{debug}) + { Bugzilla->cgi->send_header(); $vars->{chart}->dump(); } - Bugzilla->cgi->send_header($format->{'ctype'}); - disable_utf8() if ($format->{'ctype'} =~ /^image\//); + Bugzilla->cgi->send_header($format->{ctype}); + disable_utf8() if ($format->{ctype} =~ /^image\//); - $template->process($format->{'template'}, $vars) - || ThrowTemplateError($template->error()); + $template->process($format->{template}, $vars) + || ThrowTemplateError($template->error()); } -sub wrap { +sub wrap +{ validateWidthAndHeight(); - + my $chart = new Bugzilla::Chart(Bugzilla->input_params); - - $vars->{'time'} = localtime(time()); + + $vars->{time} = localtime(time()); my $q = { %{ Bugzilla->input_params } }; delete $q->{$_} for qw(action action-wrap ctype format width height); - $vars->{'imagebase'} = http_build_query($q); + $vars->{imagebase} = http_build_query($q); $template->process("reports/chart.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); } -sub view { +sub view +{ my $chart = shift; my $ARGS = Bugzilla->input_params; # Set defaults - foreach my $field ('category', 'subcategory', 'name', 'ctype') { - $vars->{'default'}{$field} = $ARGS->{$field} || 0; + foreach my $field ('category', 'subcategory', 'name', 'ctype') + { + $vars->{default}->{$field} = $ARGS->{$field} || 0; } # Pass the state object to the display UI. - $vars->{'chart'} = $chart; - $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); + $vars->{chart} = $chart; + $vars->{category} = Bugzilla::Chart::getVisibleSeries(); # If we have having problems with bad data, we can set debug=1 to dump # the data structure. $chart->dump() if $ARGS->{debug}; $template->process("reports/create-chart.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); }