blob: 1d1193d3cf6c4a3bde400dfbc8f492d5dea4899c [file] [log] [blame]
#!/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());
}