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
21 # Contributor(s): Gervase Markham <gerv@gerv.net>
22 # Lance Larsh <lance.larsh@oracle.com>
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
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 :-)
43 # Offer subscription when you get a "series already exists" error?
49 use Bugzilla
::Constants
;
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
60 local our $cgi = Bugzilla
->cgi;
61 local our $template = Bugzilla
->template;
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";
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*)$/) {
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";
95 my $user = Bugzilla
->login(LOGIN_REQUIRED
);
97 Bugzilla
->user->in_group(Bugzilla
->params->{"chartgroup"})
98 || ThrowUserError
("auth_failure", {group
=> Bugzilla
->params->{"chartgroup"},
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);
127 elsif ($action eq "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") {
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";
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);
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
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;
190 ThrowCodeError
("unknown_action");
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();
209 # Check if the user is the owner of series_id or is an admin.
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
{
228 Bugzilla
->user->in_group("editbugs") || ThrowUserError
("illegal_series_creation");
230 # Check permission for frequency
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");
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());
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());
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());
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());