]>
Commit | Line | Data |
---|---|---|
9f3d18d4 | 1 | #!/usr/bin/perl -T |
fe2a998b FB |
2 | # This Source Code Form is subject to the terms of the Mozilla Public |
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this | |
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
aaf6ccb9 | 5 | # |
fe2a998b FB |
6 | # This Source Code Form is "Incompatible With Secondary Licenses", as |
7 | # defined by the Mozilla Public License, v. 2.0. | |
aaf6ccb9 | 8 | |
1001cbba | 9 | use 5.10.1; |
aaf6ccb9 | 10 | use strict; |
9f3d18d4 FB |
11 | use warnings; |
12 | ||
415e32d4 | 13 | use lib qw(. lib); |
aaf6ccb9 | 14 | |
6368cfad | 15 | use Bugzilla; |
4df1c8fd | 16 | use Bugzilla::Constants; |
c4111734 | 17 | use Bugzilla::Util; |
18 | use Bugzilla::Error; | |
2545c095 | 19 | use Bugzilla::Field; |
48f3c648 | 20 | use Bugzilla::Search; |
14d7441b JH |
21 | use Bugzilla::Report; |
22 | use Bugzilla::Token; | |
48f3c648 | 23 | |
9b146191 | 24 | use List::MoreUtils qw(uniq); |
6368cfad | 25 | |
9488a890 | 26 | my $cgi = Bugzilla->cgi; |
44de29d0 | 27 | my $template = Bugzilla->template; |
28 | my $vars = {}; | |
9488a890 | 29 | |
3e4c5032 | 30 | # Go straight back to query.cgi if we are adding a boolean chart. |
31 | if (grep(/^cmd-/, $cgi->param())) { | |
32 | my $params = $cgi->canonicalise_query("format", "ctype"); | |
9488a890 | 33 | my $location = "query.cgi?format=" . $cgi->param('query_format') . |
15ca6f2c | 34 | ($params ? "&$params" : ""); |
9488a890 | 35 | |
36 | print $cgi->redirect($location); | |
3e4c5032 | 37 | exit; |
38 | } | |
39 | ||
9677cbec | 40 | Bugzilla->login(); |
dad29731 | 41 | my $action = $cgi->param('action') || 'menu'; |
14d7441b | 42 | my $token = $cgi->param('token'); |
dad29731 | 43 | |
44 | if ($action eq "menu") { | |
45 | # No need to do any searching in this case, so bail out early. | |
9488a890 | 46 | print $cgi->header(); |
f1ddf54f | 47 | $template->process("reports/menu.html.tmpl", $vars) |
48 | || ThrowTemplateError($template->error()); | |
49 | exit; | |
14d7441b JH |
50 | |
51 | } | |
52 | elsif ($action eq 'add') { | |
53 | my $user = Bugzilla->login(LOGIN_REQUIRED); | |
54 | check_hash_token($token, ['save_report']); | |
55 | ||
56 | my $name = clean_text($cgi->param('name')); | |
57 | my $query = $cgi->param('query'); | |
58 | ||
59 | if (my ($report) = grep{ lc($_->name) eq lc($name) } @{$user->reports}) { | |
60 | $report->set_query($query); | |
61 | $report->update; | |
62 | $vars->{'message'} = "report_updated"; | |
63 | } else { | |
64 | my $report = Bugzilla::Report->create({name => $name, query => $query}); | |
65 | $vars->{'message'} = "report_created"; | |
66 | } | |
67 | ||
68 | $user->flush_reports_cache; | |
69 | ||
70 | print $cgi->header(); | |
71 | ||
72 | $vars->{'reportname'} = $name; | |
73 | ||
74 | $template->process("global/message.html.tmpl", $vars) | |
75 | || ThrowTemplateError($template->error()); | |
76 | exit; | |
77 | } | |
78 | elsif ($action eq 'del') { | |
79 | my $user = Bugzilla->login(LOGIN_REQUIRED); | |
80 | my $report_id = $cgi->param('saved_report_id'); | |
81 | check_hash_token($token, ['delete_report', $report_id]); | |
82 | ||
83 | my $report = Bugzilla::Report->check({id => $report_id}); | |
84 | $report->remove_from_db(); | |
85 | ||
86 | $user->flush_reports_cache; | |
87 | ||
88 | print $cgi->header(); | |
89 | ||
90 | $vars->{'message'} = 'report_deleted'; | |
91 | $vars->{'reportname'} = $report->name; | |
92 | ||
93 | $template->process("global/message.html.tmpl", $vars) | |
94 | || ThrowTemplateError($template->error()); | |
95 | exit; | |
f1ddf54f | 96 | } |
aaf6ccb9 | 97 | |
4357cedb FB |
98 | # Sanitize the URL, to make URLs shorter. |
99 | $cgi->clean_search_url; | |
100 | ||
dad29731 | 101 | my $col_field = $cgi->param('x_axis_field') || ''; |
102 | my $row_field = $cgi->param('y_axis_field') || ''; | |
103 | my $tbl_field = $cgi->param('z_axis_field') || ''; | |
104 | ||
105 | if (!($col_field || $row_field || $tbl_field)) { | |
106 | ThrowUserError("no_axes_defined"); | |
107 | } | |
108 | ||
8a17e45f FB |
109 | # There is no UI for these parameters anymore, |
110 | # but they are still here just in case. | |
111 | my $width = $cgi->param('width') || 1024; | |
112 | my $height = $cgi->param('height') || 600; | |
aaf6ccb9 | 113 | |
8a17e45f | 114 | (detaint_natural($width) && $width > 0) |
2a6f7d46 | 115 | || ThrowUserError("invalid_dimensions"); |
8a17e45f | 116 | $width <= 2000 || ThrowUserError("chart_too_large"); |
f1ddf54f | 117 | |
8a17e45f | 118 | (detaint_natural($height) && $height > 0) |
2a6f7d46 | 119 | || ThrowUserError("invalid_dimensions"); |
8a17e45f | 120 | $height <= 2000 || ThrowUserError("chart_too_large"); |
dad29731 | 121 | |
324ea8a4 FB |
122 | my $formatparam = $cgi->param('format') || ''; |
123 | ||
dad29731 | 124 | # These shenanigans are necessary to make sure that both vertical and |
125 | # horizontal 1D tables convert to the correct dimension when you ask to | |
126 | # display them as some sort of chart. | |
324ea8a4 | 127 | if ($formatparam eq "table") { |
dad29731 | 128 | if ($col_field && !$row_field) { |
129 | # 1D *tables* should be displayed vertically (with a row_field only) | |
130 | $row_field = $col_field; | |
131 | $col_field = ''; | |
132 | } | |
133 | } | |
134 | else { | |
a2dd3b00 | 135 | if (!Bugzilla->feature('graphical_reports')) { |
2a6f7d46 | 136 | ThrowUserError('feature_disabled', { feature => 'graphical_reports' }); |
a2dd3b00 | 137 | } |
138 | ||
dad29731 | 139 | if ($row_field && !$col_field) { |
140 | # 1D *charts* should be displayed horizontally (with an col_field only) | |
141 | $col_field = $row_field; | |
142 | $row_field = ''; | |
143 | } | |
144 | } | |
aaf6ccb9 | 145 | |
89b15c8f | 146 | # Valid bug fields that can be reported on. |
3e4ca161 | 147 | my $valid_columns = Bugzilla::Search::REPORT_COLUMNS; |
aaf6ccb9 | 148 | |
feb2db26 | 149 | # Validate the values in the axis fields or throw an error. |
150 | !$row_field | |
3e4ca161 | 151 | || ($valid_columns->{$row_field} && trick_taint($row_field)) |
2a6f7d46 | 152 | || ThrowUserError("report_axis_invalid", {fld => "x", val => $row_field}); |
feb2db26 | 153 | !$col_field |
3e4ca161 | 154 | || ($valid_columns->{$col_field} && trick_taint($col_field)) |
2a6f7d46 | 155 | || ThrowUserError("report_axis_invalid", {fld => "y", val => $col_field}); |
feb2db26 | 156 | !$tbl_field |
3e4ca161 | 157 | || ($valid_columns->{$tbl_field} && trick_taint($tbl_field)) |
2a6f7d46 | 158 | || ThrowUserError("report_axis_invalid", {fld => "z", val => $tbl_field}); |
feb2db26 | 159 | |
b468c6e0 | 160 | my @axis_fields = grep { $_ } ($row_field, $col_field, $tbl_field); |
aaf6ccb9 | 161 | |
818ce46d | 162 | # Clone the params, so that Bugzilla::Search can modify them |
163 | my $params = new Bugzilla::CGI($cgi); | |
60712d5d MKA |
164 | my $search = new Bugzilla::Search( |
165 | fields => \@axis_fields, | |
166 | params => scalar $params->Vars, | |
167 | allow_unlimited => 1, | |
168 | ); | |
aaf6ccb9 | 169 | |
dad29731 | 170 | $::SIG{TERM} = 'DEFAULT'; |
171 | $::SIG{PIPE} = 'DEFAULT'; | |
172 | ||
d8643908 FB |
173 | Bugzilla->switch_to_shadow_db(); |
174 | my ($results, $extra_data) = $search->data; | |
aaf6ccb9 | 175 | |
f1ddf54f | 176 | # We have a hash of hashes for the data itself, and a hash to hold the |
177 | # row/col/table names. | |
aaf6ccb9 | 178 | my %data; |
f1ddf54f | 179 | my %names; |
aaf6ccb9 | 180 | |
dad29731 | 181 | # Read the bug data and count the bugs for each possible value of row, column |
182 | # and table. | |
26413f0d | 183 | # |
184 | # We detect a numerical field, and sort appropriately, if all the values are | |
185 | # numeric. | |
186 | my $col_isnumeric = 1; | |
187 | my $row_isnumeric = 1; | |
188 | my $tbl_isnumeric = 1; | |
189 | ||
1622591f PK |
190 | # define which fields are multiselect |
191 | my @multi_selects = map { $_->name } @{Bugzilla->fields( | |
192 | { | |
193 | obsolete => 0, | |
194 | type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_KEYWORDS] | |
195 | } | |
196 | )}; | |
197 | my $col_ismultiselect = scalar grep {$col_field eq $_} @multi_selects; | |
198 | my $row_ismultiselect = scalar grep {$row_field eq $_} @multi_selects; | |
199 | my $tbl_ismultiselect = scalar grep {$tbl_field eq $_} @multi_selects; | |
200 | ||
201 | ||
f4915ace | 202 | foreach my $result (@$results) { |
80808090 | 203 | # handle empty dimension member names |
26413f0d | 204 | |
1622591f PK |
205 | my @rows = check_value($row_field, $result, $row_ismultiselect); |
206 | my @cols = check_value($col_field, $result, $col_ismultiselect); | |
207 | my @tbls = check_value($tbl_field, $result, $tbl_ismultiselect); | |
208 | ||
209 | my %in_total_row; | |
210 | my %in_total_col; | |
211 | for my $tbl (@tbls) { | |
212 | my %in_row_total; | |
213 | for my $col (@cols) { | |
214 | for my $row (@rows) { | |
215 | $data{$tbl}{$col}{$row}++; | |
216 | $names{"row"}{$row}++; | |
217 | $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o); | |
218 | if ($formatparam eq "table") { | |
219 | if (!$in_row_total{$row}) { | |
220 | $data{$tbl}{'-total-'}{$row}++; | |
221 | $in_row_total{$row} = 1; | |
222 | } | |
223 | if (!$in_total_row{$row}) { | |
224 | $data{'-total-'}{'-total-'}{$row}++; | |
225 | $in_total_row{$row} = 1; | |
226 | } | |
227 | } | |
228 | } | |
229 | if ($formatparam eq "table") { | |
230 | $data{$tbl}{$col}{'-total-'}++; | |
231 | if (!$in_total_col{$col}) { | |
232 | $data{'-total-'}{$col}{'-total-'}++; | |
233 | $in_total_col{$col} = 1; | |
234 | } | |
235 | } | |
236 | $names{"col"}{$col}++; | |
237 | $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o); | |
238 | } | |
239 | $names{"tbl"}{$tbl}++; | |
240 | $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o); | |
241 | if ($formatparam eq "table") { | |
242 | $data{$tbl}{'-total-'}{'-total-'}++; | |
243 | } | |
244 | } | |
245 | if ($formatparam eq "table") { | |
246 | $data{'-total-'}{'-total-'}{'-total-'}++; | |
247 | } | |
aaf6ccb9 | 248 | } |
249 | ||
48f3c648 FB |
250 | my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field); |
251 | my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field); | |
252 | my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field); | |
dad29731 | 253 | |
254 | # The GD::Graph package requires a particular format of data, so once we've | |
255 | # gathered everything into the hashes and made sure we know the size of the | |
256 | # data, we reformat it into an array of arrays of arrays of data. | |
257 | push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1); | |
258 | ||
259 | my @image_data; | |
260 | foreach my $tbl (@tbl_names) { | |
261 | my @tbl_data; | |
262 | push(@tbl_data, \@col_names); | |
263 | foreach my $row (@row_names) { | |
264 | my @col_data; | |
265 | foreach my $col (@col_names) { | |
266 | $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0; | |
267 | push(@col_data, $data{$tbl}{$col}{$row}); | |
268 | if ($tbl ne "-total-") { | |
269 | # This is a bit sneaky. We spend every loop except the last | |
270 | # building up the -total- data, and then last time round, | |
271 | # we process it as another tbl, and push() the total values | |
272 | # into the image_data array. | |
273 | $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row}; | |
274 | } | |
275 | } | |
276 | ||
277 | push(@tbl_data, \@col_data); | |
278 | } | |
279 | ||
94c8b7f2 | 280 | unshift(@image_data, \@tbl_data); |
dad29731 | 281 | } |
282 | ||
aaf6ccb9 | 283 | $vars->{'col_field'} = $col_field; |
f1ddf54f | 284 | $vars->{'row_field'} = $row_field; |
285 | $vars->{'tbl_field'} = $tbl_field; | |
615cfb88 | 286 | $vars->{'time'} = localtime(time()); |
aaf6ccb9 | 287 | |
dad29731 | 288 | $vars->{'col_names'} = \@col_names; |
289 | $vars->{'row_names'} = \@row_names; | |
290 | $vars->{'tbl_names'} = \@tbl_names; | |
1622591f | 291 | $vars->{'note_multi_select'} = $row_ismultiselect || $col_ismultiselect; |
dad29731 | 292 | |
2c3828a7 | 293 | # Below a certain width, we don't see any bars, so there needs to be a minimum. |
324ea8a4 | 294 | if ($formatparam eq "bar") { |
e5086e90 | 295 | my $min_width = (scalar(@col_names) || 1) * 20; |
2c3828a7 | 296 | |
297 | if (!$cgi->param('cumulate')) { | |
298 | $min_width *= (scalar(@row_names) || 1); | |
299 | } | |
300 | ||
61fa0b00 | 301 | $vars->{'min_width'} = $min_width; |
2c3828a7 | 302 | } |
303 | ||
8a17e45f FB |
304 | $vars->{'width'} = $width; |
305 | $vars->{'height'} = $height; | |
d8643908 | 306 | $vars->{'queries'} = $extra_data; |
14d7441b | 307 | $vars->{'saved_report_id'} = $cgi->param('saved_report_id'); |
e2c8da0d SG |
308 | |
309 | if ($cgi->param('debug') | |
310 | && Bugzilla->params->{debug_group} | |
311 | && Bugzilla->user->in_group(Bugzilla->params->{debug_group}) | |
312 | ) { | |
313 | $vars->{'debug'} = 1; | |
314 | } | |
dad29731 | 315 | |
dad29731 | 316 | if ($action eq "wrap") { |
317 | # So which template are we using? If action is "wrap", we will be using | |
318 | # no format (it gets passed through to be the format of the actual data), | |
319 | # and either report.csv.tmpl (CSV), or report.html.tmpl (everything else). | |
320 | # report.html.tmpl produces an HTML framework for either tables of HTML | |
321 | # data, or images generated by calling report.cgi again with action as | |
322 | # "plot". | |
323 | $formatparam =~ s/[^a-zA-Z\-]//g; | |
dad29731 | 324 | $vars->{'format'} = $formatparam; |
325 | $formatparam = ''; | |
326 | ||
3e4c5032 | 327 | # We need to keep track of the defined restrictions on each of the |
328 | # axes, because buglistbase, below, throws them away. Without this, we | |
329 | # get buglistlinks wrong if there is a restriction on an axis field. | |
4357cedb FB |
330 | $vars->{'col_vals'} = get_field_restrictions($col_field); |
331 | $vars->{'row_vals'} = get_field_restrictions($row_field); | |
332 | $vars->{'tbl_vals'} = get_field_restrictions($tbl_field); | |
333 | ||
dad29731 | 334 | # We need a number of different variants of the base URL for different |
335 | # URLs in the HTML. | |
336 | $vars->{'buglistbase'} = $cgi->canonicalise_query( | |
3e4c5032 | 337 | "x_axis_field", "y_axis_field", "z_axis_field", |
13143eae | 338 | "ctype", "format", "query_format", @axis_fields); |
dad29731 | 339 | $vars->{'imagebase'} = $cgi->canonicalise_query( |
340 | $tbl_field, "action", "ctype", "format", "width", "height"); | |
341 | $vars->{'switchbase'} = $cgi->canonicalise_query( | |
5852e768 | 342 | "query_format", "action", "ctype", "format", "width", "height"); |
dad29731 | 343 | $vars->{'data'} = \%data; |
344 | } | |
345 | elsif ($action eq "plot") { | |
346 | # If action is "plot", we will be using a format as normal (pie, bar etc.) | |
347 | # and a ctype as normal (currently only png.) | |
348 | $vars->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; | |
8cd838d6 | 349 | $vars->{'x_labels_vertical'} = $cgi->param('x_labels_vertical') ? 1 : 0; |
dad29731 | 350 | $vars->{'data'} = \@image_data; |
351 | } | |
352 | else { | |
c558f9f5 | 353 | ThrowUserError('unknown_action', {action => $action}); |
dad29731 | 354 | } |
355 | ||
2a5664ad | 356 | my $format = $template->get_format("reports/report", $formatparam, |
357 | scalar($cgi->param('ctype'))); | |
aaf6ccb9 | 358 | |
dad29731 | 359 | # If we get a template or CGI error, it comes out as HTML, which isn't valid |
360 | # PNG data, and the browser just displays a "corrupt PNG" message. So, you can | |
361 | # set debug=1 to always get an HTML content-type, and view the error. | |
c0fc50d3 | 362 | $format->{'ctype'} = "text/html" if $cgi->param('debug'); |
aaf6ccb9 | 363 | |
21b50cba GM |
364 | $cgi->set_dated_content_disp("inline", "report", $format->{extension}); |
365 | print $cgi->header($format->{'ctype'}); | |
dad29731 | 366 | |
367 | # Problems with this CGI are often due to malformed data. Setting debug=1 | |
368 | # prints out both data structures. | |
c0fc50d3 | 369 | if ($cgi->param('debug')) { |
3e4c5032 | 370 | require Data::Dumper; |
45a4eea5 | 371 | say "<pre>data hash:"; |
f8813fc6 | 372 | say html_quote(Data::Dumper::Dumper(%data)); |
45a4eea5 | 373 | say "\ndata array:"; |
f8813fc6 | 374 | say html_quote(Data::Dumper::Dumper(@image_data)) . "\n\n</pre>"; |
dad29731 | 375 | } |
376 | ||
d5adbc10 | 377 | # All formats point to the same section of the documentation. |
707773ab | 378 | $vars->{'doc_section'} = 'using/reports-and-charts.html#reports'; |
d5adbc10 | 379 | |
ee5cbb72 | 380 | disable_utf8() if ($format->{'ctype'} =~ /^image\//); |
381 | ||
aaf6ccb9 | 382 | $template->process("$format->{'template'}", $vars) |
383 | || ThrowTemplateError($template->error()); | |
98571aa0 | 384 | |
98571aa0 | 385 | |
386 | sub get_names { | |
48f3c648 | 387 | my ($names, $isnumeric, $field_name) = @_; |
48f3c648 | 388 | my ($field, @sorted); |
bcdeb0b9 FB |
389 | # XXX - This is a hack to handle the actual_time/work_time field, |
390 | # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm. | |
391 | $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time'; | |
392 | ||
05ddc23d MKA |
393 | # _realname fields aren't real Bugzilla::Field objects, but they are a |
394 | # valid axis, so we don't vailidate them as Bugzilla::Field objects. | |
b043f1bb | 395 | $field = Bugzilla::Field->check($field_name) |
b043f1bb | 396 | if ($field_name && $field_name !~ /_realname$/); |
98571aa0 | 397 | |
48f3c648 FB |
398 | if ($field && $field->is_select) { |
399 | foreach my $value (@{$field->legal_values}) { | |
400 | push(@sorted, $value->name) if $names->{$value->name}; | |
271477d8 | 401 | } |
1622591f PK |
402 | unshift(@sorted, '---') if ($field_name eq 'resolution' |
403 | || $field->type == FIELD_TYPE_MULTI_SELECT); | |
48f3c648 | 404 | @sorted = uniq @sorted; |
98571aa0 | 405 | } |
406 | elsif ($isnumeric) { | |
407 | # It's not a field we are preserving the order of, so sort it | |
408 | # numerically... | |
48f3c648 FB |
409 | @sorted = sort { $a <=> $b } keys %$names; |
410 | } | |
411 | else { | |
98571aa0 | 412 | # ...or alphabetically, as appropriate. |
48f3c648 | 413 | @sorted = sort keys %$names; |
98571aa0 | 414 | } |
2bd074dc | 415 | |
48f3c648 | 416 | return @sorted; |
98571aa0 | 417 | } |
b468c6e0 FB |
418 | |
419 | sub check_value { | |
1622591f | 420 | my ($field, $result, $ismultiselect) = @_; |
b468c6e0 FB |
421 | |
422 | my $value; | |
423 | if (!defined $field) { | |
424 | $value = ''; | |
425 | } | |
426 | elsif ($field eq '') { | |
427 | $value = ' '; | |
428 | } | |
429 | else { | |
430 | $value = shift @$result; | |
431 | $value = ' ' if (!defined $value || $value eq ''); | |
1622591f PK |
432 | $value = '---' if (($field eq 'resolution' || $ismultiselect ) && |
433 | $value eq ' '); | |
434 | } | |
435 | if ($ismultiselect) { | |
436 | # Some DB servers have a space after the comma, some others don't. | |
437 | return split(/, ?/, $value); | |
438 | } else { | |
439 | return ($value); | |
b468c6e0 | 440 | } |
b468c6e0 | 441 | } |
4357cedb FB |
442 | |
443 | sub get_field_restrictions { | |
444 | my $field = shift; | |
445 | my $cgi = Bugzilla->cgi; | |
446 | ||
aecf0a17 | 447 | return join('&', map {url_quote($field) . '=' . url_quote($_)} $cgi->param($field)); |
4357cedb | 448 | } |