2010-08-30 Alejandro G. Castro <alex@igalia.com>
[WebKit-https.git] / BugsSite / chart.cgi
1 #!/usr/bin/env perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
3 #
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/
8 #
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.
13 #
14 # The Original Code is the Bugzilla Bug Tracking System.
15 #
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.
20 #
21 # Contributor(s): Gervase Markham <gerv@gerv.net>
22 #                 Lance Larsh <lance.larsh@oracle.com>
23
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
30 #
31 # So when you select rows in the UI, you are selecting one or more lines, not
32 # series.
33
34 # Generic Charting TODO:
35 #
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 :-)
41 #
42 # Bonus:
43 # Offer subscription when you get a "series already exists" error?
44
45 use strict;
46 use lib qw(. lib);
47
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;
55
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 = {};
63
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;
70 }
71
72 my $action = $cgi->param('action');
73 my $series_id = $cgi->param('series_id');
74 $vars->{'doc_section'} = 'reporting.html#charts';
75
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;
84 }
85
86 $action ||= "assemble";
87
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;
93 }
94
95 my $user = Bugzilla->login(LOGIN_REQUIRED);
96
97 Bugzilla->user->in_group(Bugzilla->params->{"chartgroup"})
98   || ThrowUserError("auth_failure", {group  => Bugzilla->params->{"chartgroup"},
99                                      action => "use",
100                                      object => "charts"});
101
102 # Only admins may create public queries
103 Bugzilla->user->in_group('admin') || $cgi->delete('public');
104
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);
113     }
114
115     my $chart = new Bugzilla::Chart($cgi);
116
117     if ($action =~ /^remove|sum$/) {
118         $chart->$action(getSelectedLines());
119     }
120     elsif ($action eq "add") {
121         my @series_ids = getAndValidateSeriesIDs();
122         $chart->add(@series_ids);
123     }
124
125     view($chart);
126 }
127 elsif ($action eq "plot") {
128     plot();
129 }
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();
134     }
135     else {
136         wrap();
137     }
138 }
139 elsif ($action eq "create") {
140     assertCanCreate($cgi);
141     
142     my $series = new Bugzilla::Series($cgi);
143
144     if (!$series->existsInDatabase()) {
145         $series->writeToDatabase();
146         $vars->{'message'} = "series_created";
147     }
148     else {
149         ThrowUserError("series_already_exists", {'series' => $series});
150     }
151
152     $vars->{'series'} = $series;
153
154     print $cgi->header();
155     $template->process("global/message.html.tmpl", $vars)
156       || ThrowTemplateError($template->error());
157 }
158 elsif ($action eq "edit") {
159     detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
160     assertCanEdit($series_id);
161
162     my $series = new Bugzilla::Series($series_id);
163     
164     edit($series);
165 }
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);
170
171     my $series = new Bugzilla::Series($cgi);
172
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'}) 
180     {
181         ThrowUserError("series_already_exists", {'series' => $series});
182     }
183     
184     $series->writeToDatabase();
185     $vars->{'changes_saved'} = 1;
186     
187     edit($series);
188 }
189 else {
190     ThrowCodeError("unknown_action");
191 }
192
193 exit;
194
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"));
198
199     return wantarray ? @series_ids : $series_ids[0];
200 }
201
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();
205
206     return @ids;
207 }
208
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;
213
214     return if $user->in_group('admin');
215
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");
222 }
223
224 # Check if the user is permitted to create this series with these parameters.
225 sub assertCanCreate {
226     my ($cgi) = shift;
227     
228     Bugzilla->user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
229
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 });
234     }    
235 }
236
237 sub validateWidthAndHeight {
238     $vars->{'width'} = $cgi->param('width');
239     $vars->{'height'} = $cgi->param('height');
240
241     if (defined($vars->{'width'})) {
242        (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
243          || ThrowCodeError("invalid_dimensions");
244     }
245
246     if (defined($vars->{'height'})) {
247        (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
248          || ThrowCodeError("invalid_dimensions");
249     }
250
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");
257     }
258 }
259
260 sub edit {
261     my $series = shift;
262
263     $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
264     $vars->{'creator'} = new Bugzilla::User($series->{'creator'});
265     $vars->{'default'} = $series;
266
267     print $cgi->header();
268     $template->process("reports/edit-series.html.tmpl", $vars)
269       || ThrowTemplateError($template->error());
270 }
271
272 sub plot {
273     validateWidthAndHeight();
274     $vars->{'chart'} = new Bugzilla::Chart($cgi);
275
276     my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
277
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();
282     }
283
284     print $cgi->header($format->{'ctype'});
285     disable_utf8() if ($format->{'ctype'} =~ /^image\//);
286
287     $template->process($format->{'template'}, $vars)
288       || ThrowTemplateError($template->error());
289 }
290
291 sub wrap {
292     validateWidthAndHeight();
293     
294     # We create a Chart object so we can validate the parameters
295     my $chart = new Bugzilla::Chart($cgi);
296     
297     $vars->{'time'} = time();
298
299     $vars->{'imagebase'} = $cgi->canonicalise_query(
300                 "action", "action-wrap", "ctype", "format", "width", "height");
301
302     print $cgi->header();
303     $template->process("reports/chart.html.tmpl", $vars)
304       || ThrowTemplateError($template->error());
305 }
306
307 sub view {
308     my $chart = shift;
309
310     # Set defaults
311     foreach my $field ('category', 'subcategory', 'name', 'ctype') {
312         $vars->{'default'}{$field} = $cgi->param($field) || 0;
313     }
314
315     # Pass the state object to the display UI.
316     $vars->{'chart'} = $chart;
317     $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
318
319     print $cgi->header();
320
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');
324
325     $template->process("reports/create-chart.html.tmpl", $vars)
326       || ThrowTemplateError($template->error());
327 }