| #!/usr/bin/env 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 |
| # the License at http://www.mozilla.org/MPL/ |
| # |
| # Software distributed under the License is distributed on an "AS |
| # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or |
| # implied. See the License for the specific language governing |
| # rights and limitations under the License. |
| # |
| # The Original Code is the Bugzilla Bug Tracking System. |
| # |
| # The Initial Developer of the Original Code is Netscape Communications |
| # Corporation. Portions created by Netscape are |
| # Copyright (C) 1998 Netscape Communications Corporation. All |
| # Rights Reserved. |
| # |
| # Contributor(s): Gervase Markham <gerv@gerv.net> |
| # Lance Larsh <lance.larsh@oracle.com> |
| |
| # 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::Error; |
| use Bugzilla::Util; |
| use Bugzilla::Chart; |
| use Bugzilla::Series; |
| use Bugzilla::User; |
| |
| # 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 = {}; |
| |
| # 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 "Location: query.cgi?format=" . $cgi->param('query_format') . |
| ($params ? "&$params" : "") . "\n\n"; |
| 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 "Location: buglist.cgi" . ($params ? "?$params" : "") . "\n\n"; |
| exit; |
| } |
| |
| my $user = Bugzilla->login(LOGIN_REQUIRED); |
| |
| Bugzilla->user->in_group(Bugzilla->params->{"chartgroup"}) |
| || ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"}, |
| action => "use", |
| object => "charts"}); |
| |
| # Only admins may create public queries |
| Bugzilla->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 $series = new Bugzilla::Series($cgi); |
| |
| if (!$series->existsInDatabase()) { |
| $series->writeToDatabase(); |
| $vars->{'message'} = "series_created"; |
| } |
| else { |
| ThrowUserError("series_already_exists", {'series' => $series}); |
| } |
| |
| $vars->{'series'} = $series; |
| |
| print $cgi->header(); |
| $template->process("global/message.html.tmpl", $vars) |
| || ThrowTemplateError($template->error()); |
| } |
| elsif ($action eq "edit") { |
| detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); |
| assertCanEdit($series_id); |
| |
| my $series = new Bugzilla::Series($series_id); |
| |
| edit($series); |
| } |
| elsif ($action eq "alter") { |
| # This is the "commit" action for editing a series |
| detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); |
| assertCanEdit($series_id); |
| |
| my $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); |
| } |
| 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+$/, $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) = @_; |
| my $user = Bugzilla->user; |
| |
| return if $user->in_group('admin'); |
| |
| my $dbh = Bugzilla->dbh; |
| my $iscreator = $dbh->selectrow_array("SELECT CASE WHEN creator = ? " . |
| "THEN 1 ELSE 0 END FROM series " . |
| "WHERE series_id = ?", undef, |
| $user->id, $series_id); |
| $iscreator || ThrowUserError("illegal_series_edit"); |
| } |
| |
| # Check if the user is permitted to create this series with these parameters. |
| sub assertCanCreate { |
| my ($cgi) = shift; |
| |
| Bugzilla->user->in_group("editbugs") || ThrowUserError("illegal_series_creation"); |
| |
| # Check permission for frequency |
| my $min_freq = 7; |
| if ($cgi->param('frequency') < $min_freq && !Bugzilla->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) |
| || 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"); |
| } |
| } |
| |
| sub edit { |
| my $series = shift; |
| |
| $vars->{'category'} = Bugzilla::Chart::getVisibleSeries(); |
| $vars->{'creator'} = new Bugzilla::User($series->{'creator'}); |
| $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'} = 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()); |
| } |