Rubber-stamped by Brady Eidson.
[webbrowser.git] / BugsSite / chart.cgi
blob1d1193d3cf6c4a3bde400dfbc8f492d5dea4899c
1 #!/usr/bin/env perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
4 # The contents of this file are subject to the Mozilla Public
5 # License Version 1.1 (the "License"); you may not use this file
6 # except in compliance with the License. You may obtain a copy of
7 # the License at http://www.mozilla.org/MPL/
9 # Software distributed under the License is distributed on an "AS
10 # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
11 # implied. See the License for the specific language governing
12 # rights and limitations under the License.
14 # The Original Code is the Bugzilla Bug Tracking System.
16 # The Initial Developer of the Original Code is Netscape Communications
17 # Corporation. Portions created by Netscape are
18 # Copyright (C) 1998 Netscape Communications Corporation. All
19 # Rights Reserved.
21 # Contributor(s): Gervase Markham <gerv@gerv.net>
22 # Lance Larsh <lance.larsh@oracle.com>
24 # Glossary:
25 # series: An individual, defined set of data plotted over time.
26 # data set: What a series is called in the UI.
27 # line: A set of one or more series, to be summed and drawn as a single
28 # line when the series is plotted.
29 # chart: A set of lines
31 # So when you select rows in the UI, you are selecting one or more lines, not
32 # series.
34 # Generic Charting TODO:
36 # JS-less chart creation - hard.
37 # Broken image on error or no data - need to do much better.
38 # Centralise permission checking, so Bugzilla->user->in_group('editbugs')
39 # not scattered everywhere.
40 # User documentation :-)
42 # Bonus:
43 # Offer subscription when you get a "series already exists" error?
45 use strict;
46 use lib qw(. lib);
48 use Bugzilla;
49 use Bugzilla::Constants;
50 use Bugzilla::Error;
51 use Bugzilla::Util;
52 use Bugzilla::Chart;
53 use Bugzilla::Series;
54 use Bugzilla::User;
56 # For most scripts we don't make $cgi and $template global variables. But
57 # when preparing Bugzilla for mod_perl, this script used these
58 # variables in so many subroutines that it was easier to just
59 # make them globals.
60 local our $cgi = Bugzilla->cgi;
61 local our $template = Bugzilla->template;
62 local our $vars = {};
64 # Go back to query.cgi if we are adding a boolean chart parameter.
65 if (grep(/^cmd-/, $cgi->param())) {
66 my $params = $cgi->canonicalise_query("format", "ctype", "action");
67 print "Location: query.cgi?format=" . $cgi->param('query_format') .
68 ($params ? "&$params" : "") . "\n\n";
69 exit;
72 my $action = $cgi->param('action');
73 my $series_id = $cgi->param('series_id');
74 $vars->{'doc_section'} = 'reporting.html#charts';
76 # Because some actions are chosen by buttons, we can't encode them as the value
77 # of the action param, because that value is localization-dependent. So, we
78 # encode it in the name, as "action-<action>". Some params even contain the
79 # series_id they apply to (e.g. subscribe, unsubscribe).
80 my @actions = grep(/^action-/, $cgi->param());
81 if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
82 $action = $1;
83 $series_id = $2 if $2;
86 $action ||= "assemble";
88 # Go to buglist.cgi if we are doing a search.
89 if ($action eq "search") {
90 my $params = $cgi->canonicalise_query("format", "ctype", "action");
91 print "Location: buglist.cgi" . ($params ? "?$params" : "") . "\n\n";
92 exit;
95 my $user = Bugzilla->login(LOGIN_REQUIRED);
97 Bugzilla->user->in_group(Bugzilla->params->{"chartgroup"})
98 || ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"},
99 action => "use",
100 object => "charts"});
102 # Only admins may create public queries
103 Bugzilla->user->in_group('admin') || $cgi->delete('public');
105 # All these actions relate to chart construction.
106 if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
107 # These two need to be done before the creation of the Chart object, so
108 # that the changes they make will be reflected in it.
109 if ($action =~ /^subscribe|unsubscribe$/) {
110 detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
111 my $series = new Bugzilla::Series($series_id);
112 $series->$action($user->id);
115 my $chart = new Bugzilla::Chart($cgi);
117 if ($action =~ /^remove|sum$/) {
118 $chart->$action(getSelectedLines());
120 elsif ($action eq "add") {
121 my @series_ids = getAndValidateSeriesIDs();
122 $chart->add(@series_ids);
125 view($chart);
127 elsif ($action eq "plot") {
128 plot();
130 elsif ($action eq "wrap") {
131 # For CSV "wrap", we go straight to "plot".
132 if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
133 plot();
135 else {
136 wrap();
139 elsif ($action eq "create") {
140 assertCanCreate($cgi);
142 my $series = new Bugzilla::Series($cgi);
144 if (!$series->existsInDatabase()) {
145 $series->writeToDatabase();
146 $vars->{'message'} = "series_created";
148 else {
149 ThrowUserError("series_already_exists", {'series' => $series});
152 $vars->{'series'} = $series;
154 print $cgi->header();
155 $template->process("global/message.html.tmpl", $vars)
156 || ThrowTemplateError($template->error());
158 elsif ($action eq "edit") {
159 detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
160 assertCanEdit($series_id);
162 my $series = new Bugzilla::Series($series_id);
164 edit($series);
166 elsif ($action eq "alter") {
167 # This is the "commit" action for editing a series
168 detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
169 assertCanEdit($series_id);
171 my $series = new Bugzilla::Series($cgi);
173 # We need to check if there is _another_ series in the database with
174 # our (potentially new) name. So we call existsInDatabase() to see if
175 # the return value is us or some other series we need to avoid stomping
176 # on.
177 my $id_of_series_in_db = $series->existsInDatabase();
178 if (defined($id_of_series_in_db) &&
179 $id_of_series_in_db != $series->{'series_id'})
181 ThrowUserError("series_already_exists", {'series' => $series});
184 $series->writeToDatabase();
185 $vars->{'changes_saved'} = 1;
187 edit($series);
189 else {
190 ThrowCodeError("unknown_action");
193 exit;
195 # Find any selected series and return either the first or all of them.
196 sub getAndValidateSeriesIDs {
197 my @series_ids = grep(/^\d+$/, $cgi->param("name"));
199 return wantarray ? @series_ids : $series_ids[0];
202 # Return a list of IDs of all the lines selected in the UI.
203 sub getSelectedLines {
204 my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
206 return @ids;
209 # Check if the user is the owner of series_id or is an admin.
210 sub assertCanEdit {
211 my ($series_id) = @_;
212 my $user = Bugzilla->user;
214 return if $user->in_group('admin');
216 my $dbh = Bugzilla->dbh;
217 my $iscreator = $dbh->selectrow_array("SELECT CASE WHEN creator = ? " .
218 "THEN 1 ELSE 0 END FROM series " .
219 "WHERE series_id = ?", undef,
220 $user->id, $series_id);
221 $iscreator || ThrowUserError("illegal_series_edit");
224 # Check if the user is permitted to create this series with these parameters.
225 sub assertCanCreate {
226 my ($cgi) = shift;
228 Bugzilla->user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
230 # Check permission for frequency
231 my $min_freq = 7;
232 if ($cgi->param('frequency') < $min_freq && !Bugzilla->user->in_group("admin")) {
233 ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
237 sub validateWidthAndHeight {
238 $vars->{'width'} = $cgi->param('width');
239 $vars->{'height'} = $cgi->param('height');
241 if (defined($vars->{'width'})) {
242 (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
243 || ThrowCodeError("invalid_dimensions");
246 if (defined($vars->{'height'})) {
247 (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
248 || ThrowCodeError("invalid_dimensions");
251 # The equivalent of 2000 square seems like a very reasonable maximum size.
252 # This is merely meant to prevent accidental or deliberate DOS, and should
253 # have no effect in practice.
254 if ($vars->{'width'} && $vars->{'height'}) {
255 (($vars->{'width'} * $vars->{'height'}) <= 4000000)
256 || ThrowUserError("chart_too_large");
260 sub edit {
261 my $series = shift;
263 $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
264 $vars->{'creator'} = new Bugzilla::User($series->{'creator'});
265 $vars->{'default'} = $series;
267 print $cgi->header();
268 $template->process("reports/edit-series.html.tmpl", $vars)
269 || ThrowTemplateError($template->error());
272 sub plot {
273 validateWidthAndHeight();
274 $vars->{'chart'} = new Bugzilla::Chart($cgi);
276 my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
278 # Debugging PNGs is a pain; we need to be able to see the error messages
279 if ($cgi->param('debug')) {
280 print $cgi->header();
281 $vars->{'chart'}->dump();
284 print $cgi->header($format->{'ctype'});
285 disable_utf8() if ($format->{'ctype'} =~ /^image\//);
287 $template->process($format->{'template'}, $vars)
288 || ThrowTemplateError($template->error());
291 sub wrap {
292 validateWidthAndHeight();
294 # We create a Chart object so we can validate the parameters
295 my $chart = new Bugzilla::Chart($cgi);
297 $vars->{'time'} = time();
299 $vars->{'imagebase'} = $cgi->canonicalise_query(
300 "action", "action-wrap", "ctype", "format", "width", "height");
302 print $cgi->header();
303 $template->process("reports/chart.html.tmpl", $vars)
304 || ThrowTemplateError($template->error());
307 sub view {
308 my $chart = shift;
310 # Set defaults
311 foreach my $field ('category', 'subcategory', 'name', 'ctype') {
312 $vars->{'default'}{$field} = $cgi->param($field) || 0;
315 # Pass the state object to the display UI.
316 $vars->{'chart'} = $chart;
317 $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
319 print $cgi->header();
321 # If we have having problems with bad data, we can set debug=1 to dump
322 # the data structure.
323 $chart->dump() if $cgi->param('debug');
325 $template->process("reports/create-chart.html.tmpl", $vars)
326 || ThrowTemplateError($template->error());