aboutsummaryrefslogtreecommitdiff
path: root/chart.cgi
diff options
context:
space:
mode:
authorFathi Boudra <fathi.boudra@linaro.org>2014-06-02 17:14:02 +0300
committerFathi Boudra <fathi.boudra@linaro.org>2014-06-02 17:14:02 +0300
commit46f29f243b1e7642e860b40214d5f893aad678e1 (patch)
tree7ac82cee0957299e51c36f22ce1b172670513348 /chart.cgi
Initial commit - Bugzilla 4.4.4
Signed-off-by: Fathi Boudra <fathi.boudra@linaro.org>
Diffstat (limited to 'chart.cgi')
-rwxr-xr-xchart.cgi355
1 files changed, 355 insertions, 0 deletions
diff --git a/chart.cgi b/chart.cgi
new file mode 100755
index 0000000..7f21fd0
--- /dev/null
+++ b/chart.cgi
@@ -0,0 +1,355 @@
+#!/usr/bin/perl -wT
+# 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.
+
+# Glossary:
+# series: An individual, defined set of data plotted over time.
+# data set: What a series is called in the UI.
+# line: A set of one or more series, to be summed and drawn as a single
+# line when the series is plotted.
+# chart: A set of lines
+#
+# So when you select rows in the UI, you are selecting one or more lines, not
+# series.
+
+# Generic Charting TODO:
+#
+# JS-less chart creation - hard.
+# Broken image on error or no data - need to do much better.
+# Centralise permission checking, so Bugzilla->user->in_group('editbugs')
+# not scattered everywhere.
+# User documentation :-)
+#
+# Bonus:
+# Offer subscription when you get a "series already exists" error?
+
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::CGI;
+use Bugzilla::Error;
+use Bugzilla::Util;
+use Bugzilla::Chart;
+use Bugzilla::Series;
+use Bugzilla::User;
+use Bugzilla::Token;
+
+# For most scripts we don't make $cgi and $template global variables. But
+# when preparing Bugzilla for mod_perl, this script used these
+# variables in so many subroutines that it was easier to just
+# make them globals.
+local our $cgi = Bugzilla->cgi;
+local our $template = Bugzilla->template;
+local our $vars = {};
+my $dbh = Bugzilla->dbh;
+
+my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+if (!Bugzilla->feature('new_charts')) {
+ ThrowUserError('feature_disabled', { feature => 'new_charts' });
+}
+
+# Go back to query.cgi if we are adding a boolean chart parameter.
+if (grep(/^cmd-/, $cgi->param())) {
+ my $params = $cgi->canonicalise_query("format", "ctype", "action");
+ print $cgi->redirect("query.cgi?format=" . $cgi->param('query_format') .
+ ($params ? "&$params" : ""));
+ exit;
+}
+
+my $action = $cgi->param('action');
+my $series_id = $cgi->param('series_id');
+$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-<action>". Some params even contain the
+# series_id they apply to (e.g. subscribe, unsubscribe).
+my @actions = grep(/^action-/, $cgi->param());
+if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
+ $action = $1;
+ $series_id = $2 if $2;
+}
+
+$action ||= "assemble";
+
+# Go to buglist.cgi if we are doing a search.
+if ($action eq "search") {
+ my $params = $cgi->canonicalise_query("format", "ctype", "action");
+ print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : ""));
+ exit;
+}
+
+$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') || $cgi->delete('public');
+
+# All these actions relate to chart construction.
+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$/) {
+ detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
+ my $series = new Bugzilla::Series($series_id);
+ $series->$action($user->id);
+ }
+
+ my $chart = new Bugzilla::Chart($cgi);
+
+ if ($action =~ /^remove|sum$/) {
+ $chart->$action(getSelectedLines());
+ }
+ elsif ($action eq "add") {
+ my @series_ids = getAndValidateSeriesIDs();
+ $chart->add(@series_ids);
+ }
+
+ view($chart);
+}
+elsif ($action eq "plot") {
+ plot();
+}
+elsif ($action eq "wrap") {
+ # For CSV "wrap", we go straight to "plot".
+ if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
+ plot();
+ }
+ else {
+ wrap();
+ }
+}
+elsif ($action eq "create") {
+ assertCanCreate($cgi);
+ my $token = $cgi->param('token');
+ check_hash_token($token, ['create-series']);
+
+ my $series = new Bugzilla::Series($cgi);
+
+ ThrowUserError("series_already_exists", {'series' => $series})
+ if $series->existsInDatabase;
+
+ $series->writeToDatabase();
+ $vars->{'message'} = "series_created";
+ $vars->{'series'} = $series;
+
+ my $chart = new Bugzilla::Chart($cgi);
+ view($chart);
+}
+elsif ($action eq "edit") {
+ my $series = assertCanEdit($series_id);
+ edit($series);
+}
+elsif ($action eq "alter") {
+ my $series = assertCanEdit($series_id);
+ my $token = $cgi->param('token');
+ check_hash_token($token, [$series->id, $series->name]);
+ # XXX - This should be replaced by $series->set_foo() methods.
+ $series = new Bugzilla::Series($cgi);
+
+ # We need to check if there is _another_ series in the database with
+ # our (potentially new) name. So we call existsInDatabase() to see if
+ # 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'})
+ {
+ ThrowUserError("series_already_exists", {'series' => $series});
+ }
+
+ $series->writeToDatabase();
+ $vars->{'changes_saved'} = 1;
+
+ edit($series);
+}
+elsif ($action eq "confirm-delete") {
+ $vars->{'series'} = assertCanEdit($series_id);
+
+ print $cgi->header();
+ $template->process("reports/delete-series.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+elsif ($action eq "delete") {
+ my $series = assertCanEdit($series_id);
+ my $token = $cgi->param('token');
+ check_hash_token($token, [$series->id, $series->name]);
+
+ $dbh->bz_start_transaction();
+
+ $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"});
+ }
+ }
+ $dbh->bz_commit_transaction();
+
+ $vars->{'message'} = "series_deleted";
+ $vars->{'series'} = $series;
+ view();
+}
+elsif ($action eq "convert_search") {
+ my $saved_search = $cgi->param('series_from_search') || '';
+ my ($query) = grep { $_->name eq $saved_search } @{ $user->queries };
+ my $url = '';
+ if ($query) {
+ my $params = new Bugzilla::CGI($query->edit_link);
+ # These two parameters conflict with the one below.
+ $url = $params->canonicalise_query('format', 'query_format');
+ $url = '&amp;' . html_quote($url);
+ }
+ print $cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url");
+}
+else {
+ ThrowUserError('unknown_action', {action => $action});
+}
+
+exit;
+
+# Find any selected series and return either the first or all of them.
+sub getAndValidateSeriesIDs {
+ my @series_ids = grep(/^\d+$/, $cgi->param("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 : () } $cgi->param();
+
+ return @ids;
+}
+
+# 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');
+
+ if (!$user->in_group('admin') && $series->{creator_id} != $user->id) {
+ ThrowUserError('illegal_series_edit');
+ }
+
+ return $series;
+}
+
+# Check if the user is permitted to create this series with these parameters.
+sub assertCanCreate {
+ my ($cgi) = shift;
+ my $user = Bugzilla->user;
+
+ $user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
+
+ # Check permission for frequency
+ my $min_freq = 7;
+ if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) {
+ ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
+ }
+}
+
+sub validateWidthAndHeight {
+ $vars->{'width'} = $cgi->param('width');
+ $vars->{'height'} = $cgi->param('height');
+
+ if (defined($vars->{'width'})) {
+ (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
+ || ThrowUserError("invalid_dimensions");
+ }
+
+ if (defined($vars->{'height'})) {
+ (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
+ || ThrowUserError("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");
+ }
+}
+
+sub edit {
+ my $series = shift;
+
+ $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+ $vars->{'default'} = $series;
+
+ print $cgi->header();
+ $template->process("reports/edit-series.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub plot {
+ validateWidthAndHeight();
+ $vars->{'chart'} = new Bugzilla::Chart($cgi);
+
+ my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
+
+ # Debugging PNGs is a pain; we need to be able to see the error messages
+ if ($cgi->param('debug')) {
+ print $cgi->header();
+ $vars->{'chart'}->dump();
+ }
+
+ print $cgi->header($format->{'ctype'});
+ disable_utf8() if ($format->{'ctype'} =~ /^image\//);
+
+ $template->process($format->{'template'}, $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub wrap {
+ validateWidthAndHeight();
+
+ # We create a Chart object so we can validate the parameters
+ my $chart = new Bugzilla::Chart($cgi);
+
+ $vars->{'time'} = localtime(time());
+
+ $vars->{'imagebase'} = $cgi->canonicalise_query(
+ "action", "action-wrap", "ctype", "format", "width", "height");
+
+ print $cgi->header();
+ $template->process("reports/chart.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}
+
+sub view {
+ my $chart = shift;
+
+ # Set defaults
+ foreach my $field ('category', 'subcategory', 'name', 'ctype') {
+ $vars->{'default'}{$field} = $cgi->param($field) || 0;
+ }
+
+ # Pass the state object to the display UI.
+ $vars->{'chart'} = $chart;
+ $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+
+ print $cgi->header();
+
+ # If we have having problems with bad data, we can set debug=1 to dump
+ # the data structure.
+ $chart->dump() if $cgi->param('debug');
+
+ $template->process("reports/create-chart.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+}