Rubber-stamped by Brady Eidson.
[webbrowser.git] / BugsSite / report.cgi
blobdf25318abd3a08e0acf6d7bc38b9a86df036bf01
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 # <rdean@cambianetworks.com>
24 use strict;
25 use lib qw(. lib);
27 use Bugzilla;
28 use Bugzilla::Constants;
29 use Bugzilla::Util;
30 use Bugzilla::Error;
31 use Bugzilla::Field;
33 my $cgi = Bugzilla->cgi;
34 my $template = Bugzilla->template;
35 my $vars = {};
36 my $buffer = $cgi->query_string();
38 # Go straight back to query.cgi if we are adding a boolean chart.
39 if (grep(/^cmd-/, $cgi->param())) {
40 my $params = $cgi->canonicalise_query("format", "ctype");
41 my $location = "query.cgi?format=" . $cgi->param('query_format') .
42 ($params ? "&$params" : "");
44 print $cgi->redirect($location);
45 exit;
48 use Bugzilla::Search;
50 Bugzilla->login();
52 my $dbh = Bugzilla->switch_to_shadow_db();
54 my $action = $cgi->param('action') || 'menu';
56 if ($action eq "menu") {
57 # No need to do any searching in this case, so bail out early.
58 print $cgi->header();
59 $template->process("reports/menu.html.tmpl", $vars)
60 || ThrowTemplateError($template->error());
61 exit;
64 my $col_field = $cgi->param('x_axis_field') || '';
65 my $row_field = $cgi->param('y_axis_field') || '';
66 my $tbl_field = $cgi->param('z_axis_field') || '';
68 if (!($col_field || $row_field || $tbl_field)) {
69 ThrowUserError("no_axes_defined");
72 my $width = $cgi->param('width');
73 my $height = $cgi->param('height');
75 if (defined($width)) {
76 (detaint_natural($width) && $width > 0)
77 || ThrowCodeError("invalid_dimensions");
78 $width <= 2000 || ThrowUserError("chart_too_large");
81 if (defined($height)) {
82 (detaint_natural($height) && $height > 0)
83 || ThrowCodeError("invalid_dimensions");
84 $height <= 2000 || ThrowUserError("chart_too_large");
87 # These shenanigans are necessary to make sure that both vertical and
88 # horizontal 1D tables convert to the correct dimension when you ask to
89 # display them as some sort of chart.
90 if (defined $cgi->param('format') && $cgi->param('format') eq "table") {
91 if ($col_field && !$row_field) {
92 # 1D *tables* should be displayed vertically (with a row_field only)
93 $row_field = $col_field;
94 $col_field = '';
97 else {
98 if ($row_field && !$col_field) {
99 # 1D *charts* should be displayed horizontally (with an col_field only)
100 $col_field = $row_field;
101 $row_field = '';
105 my %columns;
106 $columns{'bug_severity'} = "bugs.bug_severity";
107 $columns{'priority'} = "bugs.priority";
108 $columns{'rep_platform'} = "bugs.rep_platform";
109 $columns{'assigned_to'} = "map_assigned_to.login_name";
110 $columns{'reporter'} = "map_reporter.login_name";
111 $columns{'qa_contact'} = "map_qa_contact.login_name";
112 $columns{'bug_status'} = "bugs.bug_status";
113 $columns{'resolution'} = "bugs.resolution";
114 $columns{'component'} = "map_components.name";
115 $columns{'product'} = "map_products.name";
116 $columns{'classification'} = "map_classifications.name";
117 $columns{'version'} = "bugs.version";
118 $columns{'op_sys'} = "bugs.op_sys";
119 $columns{'votes'} = "bugs.votes";
120 $columns{'keywords'} = "bugs.keywords";
121 $columns{'target_milestone'} = "bugs.target_milestone";
122 # One which means "nothing". Any number would do, really. It just gets SELECTed
123 # so that we always select 3 items in the query.
124 $columns{''} = "42217354";
126 # Validate the values in the axis fields or throw an error.
127 !$row_field
128 || ($columns{$row_field} && trick_taint($row_field))
129 || ThrowCodeError("report_axis_invalid", {fld => "x", val => $row_field});
130 !$col_field
131 || ($columns{$col_field} && trick_taint($col_field))
132 || ThrowCodeError("report_axis_invalid", {fld => "y", val => $col_field});
133 !$tbl_field
134 || ($columns{$tbl_field} && trick_taint($tbl_field))
135 || ThrowCodeError("report_axis_invalid", {fld => "z", val => $tbl_field});
137 my @axis_fields = ($row_field, $col_field, $tbl_field);
138 my @selectnames = map($columns{$_}, @axis_fields);
140 # Clone the params, so that Bugzilla::Search can modify them
141 my $params = new Bugzilla::CGI($cgi);
142 my $search = new Bugzilla::Search('fields' => \@selectnames,
143 'params' => $params);
144 my $query = $search->getSQL();
146 $::SIG{TERM} = 'DEFAULT';
147 $::SIG{PIPE} = 'DEFAULT';
149 my $results = $dbh->selectall_arrayref($query);
151 # We have a hash of hashes for the data itself, and a hash to hold the
152 # row/col/table names.
153 my %data;
154 my %names;
156 # Read the bug data and count the bugs for each possible value of row, column
157 # and table.
159 # We detect a numerical field, and sort appropriately, if all the values are
160 # numeric.
161 my $col_isnumeric = 1;
162 my $row_isnumeric = 1;
163 my $tbl_isnumeric = 1;
165 foreach my $result (@$results) {
166 my ($row, $col, $tbl) = @$result;
168 # handle empty dimension member names
169 $row = ' ' if ($row eq '');
170 $col = ' ' if ($col eq '');
171 $tbl = ' ' if ($tbl eq '');
173 $row = "" if ($row eq $columns{''});
174 $col = "" if ($col eq $columns{''});
175 $tbl = "" if ($tbl eq $columns{''});
177 # account for the fact that names may start with '_' or '.'. Change this
178 # so the template doesn't hide hash elements with those keys
179 $row =~ s/^([._])/ $1/;
180 $col =~ s/^([._])/ $1/;
181 $tbl =~ s/^([._])/ $1/;
183 $data{$tbl}{$col}{$row}++;
184 $names{"col"}{$col}++;
185 $names{"row"}{$row}++;
186 $names{"tbl"}{$tbl}++;
188 $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o);
189 $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o);
190 $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o);
193 my @col_names = @{get_names($names{"col"}, $col_isnumeric, $col_field)};
194 my @row_names = @{get_names($names{"row"}, $row_isnumeric, $row_field)};
195 my @tbl_names = @{get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field)};
197 # The GD::Graph package requires a particular format of data, so once we've
198 # gathered everything into the hashes and made sure we know the size of the
199 # data, we reformat it into an array of arrays of arrays of data.
200 push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1);
202 my @image_data;
203 foreach my $tbl (@tbl_names) {
204 my @tbl_data;
205 push(@tbl_data, \@col_names);
206 foreach my $row (@row_names) {
207 my @col_data;
208 foreach my $col (@col_names) {
209 $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
210 push(@col_data, $data{$tbl}{$col}{$row});
211 if ($tbl ne "-total-") {
212 # This is a bit sneaky. We spend every loop except the last
213 # building up the -total- data, and then last time round,
214 # we process it as another tbl, and push() the total values
215 # into the image_data array.
216 $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
220 push(@tbl_data, \@col_data);
223 unshift(@image_data, \@tbl_data);
226 $vars->{'col_field'} = $col_field;
227 $vars->{'row_field'} = $row_field;
228 $vars->{'tbl_field'} = $tbl_field;
229 $vars->{'time'} = time();
231 $vars->{'col_names'} = \@col_names;
232 $vars->{'row_names'} = \@row_names;
233 $vars->{'tbl_names'} = \@tbl_names;
235 # Below a certain width, we don't see any bars, so there needs to be a minimum.
236 if ($width && $cgi->param('format') eq "bar") {
237 my $min_width = (scalar(@col_names) || 1) * 20;
239 if (!$cgi->param('cumulate')) {
240 $min_width *= (scalar(@row_names) || 1);
243 $vars->{'min_width'} = $min_width;
246 $vars->{'width'} = $width if $width;
247 $vars->{'height'} = $height if $height;
249 $vars->{'query'} = $query;
250 $vars->{'debug'} = $cgi->param('debug');
252 my $formatparam = $cgi->param('format');
254 if ($action eq "wrap") {
255 # So which template are we using? If action is "wrap", we will be using
256 # no format (it gets passed through to be the format of the actual data),
257 # and either report.csv.tmpl (CSV), or report.html.tmpl (everything else).
258 # report.html.tmpl produces an HTML framework for either tables of HTML
259 # data, or images generated by calling report.cgi again with action as
260 # "plot".
261 $formatparam =~ s/[^a-zA-Z\-]//g;
262 trick_taint($formatparam);
263 $vars->{'format'} = $formatparam;
264 $formatparam = '';
266 # We need to keep track of the defined restrictions on each of the
267 # axes, because buglistbase, below, throws them away. Without this, we
268 # get buglistlinks wrong if there is a restriction on an axis field.
269 $vars->{'col_vals'} = join("&", $buffer =~ /[&?]($col_field=[^&]+)/g);
270 $vars->{'row_vals'} = join("&", $buffer =~ /[&?]($row_field=[^&]+)/g);
271 $vars->{'tbl_vals'} = join("&", $buffer =~ /[&?]($tbl_field=[^&]+)/g);
273 # We need a number of different variants of the base URL for different
274 # URLs in the HTML.
275 $vars->{'buglistbase'} = $cgi->canonicalise_query(
276 "x_axis_field", "y_axis_field", "z_axis_field",
277 "ctype", "format", "query_format", @axis_fields);
278 $vars->{'imagebase'} = $cgi->canonicalise_query(
279 $tbl_field, "action", "ctype", "format", "width", "height");
280 $vars->{'switchbase'} = $cgi->canonicalise_query(
281 "query_format", "action", "ctype", "format", "width", "height");
282 $vars->{'data'} = \%data;
284 elsif ($action eq "plot") {
285 # If action is "plot", we will be using a format as normal (pie, bar etc.)
286 # and a ctype as normal (currently only png.)
287 $vars->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0;
288 $vars->{'x_labels_vertical'} = $cgi->param('x_labels_vertical') ? 1 : 0;
289 $vars->{'data'} = \@image_data;
291 else {
292 ThrowCodeError("unknown_action", {action => $cgi->param('action')});
295 my $format = $template->get_format("reports/report", $formatparam,
296 scalar($cgi->param('ctype')));
298 # If we get a template or CGI error, it comes out as HTML, which isn't valid
299 # PNG data, and the browser just displays a "corrupt PNG" message. So, you can
300 # set debug=1 to always get an HTML content-type, and view the error.
301 $format->{'ctype'} = "text/html" if $cgi->param('debug');
303 my @time = localtime(time());
304 my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
305 my $filename = "report-$date.$format->{extension}";
306 print $cgi->header(-type => $format->{'ctype'},
307 -content_disposition => "inline; filename=$filename");
309 # Problems with this CGI are often due to malformed data. Setting debug=1
310 # prints out both data structures.
311 if ($cgi->param('debug')) {
312 require Data::Dumper;
313 print "<pre>data hash:\n";
314 print Data::Dumper::Dumper(%data) . "\n\n";
315 print "data array:\n";
316 print Data::Dumper::Dumper(@image_data) . "\n\n</pre>";
319 # All formats point to the same section of the documentation.
320 $vars->{'doc_section'} = 'reporting.html#reports';
322 disable_utf8() if ($format->{'ctype'} =~ /^image\//);
324 $template->process("$format->{'template'}", $vars)
325 || ThrowTemplateError($template->error());
327 exit;
330 sub get_names {
331 my ($names, $isnumeric, $field) = @_;
333 # These are all the fields we want to preserve the order of in reports.
334 my %fields = ('priority' => get_legal_field_values('priority'),
335 'bug_severity' => get_legal_field_values('bug_severity'),
336 'rep_platform' => get_legal_field_values('rep_platform'),
337 'op_sys' => get_legal_field_values('op_sys'),
338 'bug_status' => get_legal_field_values('bug_status'),
339 'resolution' => [' ', @{get_legal_field_values('resolution')}]);
341 my $field_list = $fields{$field};
342 my @sorted;
344 if ($field_list) {
345 my @unsorted = keys %{$names};
347 # Extract the used fields from the field_list, in the order they
348 # appear in the field_list. This lets us keep e.g. severities in
349 # the normal order.
351 # This is O(n^2) but it shouldn't matter for short lists.
352 @sorted = map {lsearch(\@unsorted, $_) == -1 ? () : $_} @{$field_list};
354 elsif ($isnumeric) {
355 # It's not a field we are preserving the order of, so sort it
356 # numerically...
357 sub numerically { $a <=> $b }
358 @sorted = sort numerically keys(%{$names});
359 } else {
360 # ...or alphabetically, as appropriate.
361 @sorted = sort(keys(%{$names}));
364 return \@sorted;