report.cgi 12.5 KB
Newer Older
1
#!/usr/bin/perl -wT
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Gervase Markham <gerv@gerv.net>
#                 <rdean@cambianetworks.com>

use strict;
use lib ".";

require "CGI.pl";

29
use vars qw($template $vars @legal_opsys @legal_platform @legal_severity);
30

31
use Bugzilla;
32
use Bugzilla::Constants;
33

34 35
my $cgi = Bugzilla->cgi;

36 37 38
# Go straight back to query.cgi if we are adding a boolean chart.
if (grep(/^cmd-/, $cgi->param())) {
    my $params = $cgi->canonicalise_query("format", "ctype");
39
    my $location = "query.cgi?format=" . $cgi->param('query_format') . 
40
      ($params ? "&$params" : "");
41 42

    print $cgi->redirect($location);
43 44 45
    exit;
}

46 47 48 49
use Bugzilla::Search;

GetVersionTable();

50
Bugzilla->login(LOGIN_REQUIRED);
51

52
Bugzilla->switch_to_shadow_db();
53

54 55 56 57
my $action = $cgi->param('action') || 'menu';

if ($action eq "menu") {
    # No need to do any searching in this case, so bail out early.
58
    print $cgi->header();
59 60 61 62
    $template->process("reports/menu.html.tmpl", $vars)
      || ThrowTemplateError($template->error());
    exit;
}
63

64 65 66 67 68 69 70 71 72 73
my $col_field = $cgi->param('x_axis_field') || '';
my $row_field = $cgi->param('y_axis_field') || '';
my $tbl_field = $cgi->param('z_axis_field') || '';

if (!($col_field || $row_field || $tbl_field)) {
    ThrowUserError("no_axes_defined");
}

my $width = $cgi->param('width');
my $height = $cgi->param('height');
74

75 76 77 78
if (defined($width)) {
   (detaint_natural($width) && $width > 0)
     || ThrowCodeError("invalid_dimensions");
   $width <= 2000 || ThrowUserError("chart_too_large");
79 80
}

81 82 83 84 85 86 87 88 89
if (defined($height)) {
   (detaint_natural($height) && $height > 0)
     || ThrowCodeError("invalid_dimensions");
   $height <= 2000 || ThrowUserError("chart_too_large");
}

# These shenanigans are necessary to make sure that both vertical and 
# horizontal 1D tables convert to the correct dimension when you ask to
# display them as some sort of chart.
90
if (defined $cgi->param('format') && $cgi->param('format') eq "table") {
91 92 93 94 95 96 97 98 99 100 101 102 103
    if ($col_field && !$row_field) {    
        # 1D *tables* should be displayed vertically (with a row_field only)
        $row_field = $col_field;
        $col_field = '';
    }
}
else {
    if ($row_field && !$col_field) {
        # 1D *charts* should be displayed horizontally (with an col_field only)
        $col_field = $row_field;
        $row_field = '';
    }
}
104 105 106 107 108 109 110 111 112 113 114 115

my %columns;
$columns{'bug_severity'}     = "bugs.bug_severity";        
$columns{'priority'}         = "bugs.priority";
$columns{'rep_platform'}     = "bugs.rep_platform";
$columns{'assigned_to'}      = "map_assigned_to.login_name";
$columns{'reporter'}         = "map_reporter.login_name";
$columns{'qa_contact'}       = "map_qa_contact.login_name";
$columns{'bug_status'}       = "bugs.bug_status";
$columns{'resolution'}       = "bugs.resolution";
$columns{'component'}        = "map_components.name";
$columns{'product'}          = "map_products.name";
116
$columns{'classification'}   = "map_classifications.name";
117 118 119 120 121
$columns{'version'}          = "bugs.version";
$columns{'op_sys'}           = "bugs.op_sys";
$columns{'votes'}            = "bugs.votes";
$columns{'keywords'}         = "bugs.keywords";
$columns{'target_milestone'} = "bugs.target_milestone";
122 123 124
# One which means "nothing". Any number would do, really. It just gets SELECTed
# so that we always select 3 items in the query.
$columns{''}                 = "42217354";
125

126 127 128
# Validate the values in the axis fields or throw an error.
!$row_field 
  || ($columns{$row_field} && trick_taint($row_field))
129
  || ThrowCodeError("report_axis_invalid", {fld => "x", val => $row_field});
130 131
!$col_field 
  || ($columns{$col_field} && trick_taint($col_field))
132
  || ThrowCodeError("report_axis_invalid", {fld => "y", val => $col_field});
133 134
!$tbl_field 
  || ($columns{$tbl_field} && trick_taint($tbl_field))
135
  || ThrowCodeError("report_axis_invalid", {fld => "z", val => $tbl_field});
136

137
my @axis_fields = ($row_field, $col_field, $tbl_field);
138
my @selectnames = map($columns{$_}, @axis_fields);
139 140 141 142 143 144
# add product if person is requesting classification
if (lsearch(\@axis_fields,"classification") >= 0) {
    if (lsearch(\@axis_fields,"product") < 0) {
        push(@selectnames,($columns{'product'}));
    }
}
145

146 147
# Clone the params, so that Bugzilla::Search can modify them
my $params = new Bugzilla::CGI($cgi);
148
my $search = new Bugzilla::Search('fields' => \@selectnames, 
149
                                  'params' => $params);
150 151
my $query = $search->getSQL();

152 153 154
$::SIG{TERM} = 'DEFAULT';
$::SIG{PIPE} = 'DEFAULT';

155
SendSQL($query);
156

157 158
# We have a hash of hashes for the data itself, and a hash to hold the 
# row/col/table names.
159
my %data;
160
my %names;
161

162 163
# Read the bug data and count the bugs for each possible value of row, column
# and table.
164 165 166 167 168 169 170
#
# We detect a numerical field, and sort appropriately, if all the values are
# numeric.
my $col_isnumeric = 1;
my $row_isnumeric = 1;
my $tbl_isnumeric = 1;

171
while (MoreSQLData()) {
172
    my ($row, $col, $tbl) = FetchSQLData();
173
    $row = "" if ($row eq $columns{''});
174 175
    $col = "" if ($col eq $columns{''});
    $tbl = "" if ($tbl eq $columns{''});
176
    
177 178 179 180 181 182
    # account for the fact that names may start with '_' or '.'.  Change this 
    # so the template doesn't hide hash elements with those keys
    $row =~ s/^([._])/ $1/;
    $col =~ s/^([._])/ $1/;
    $tbl =~ s/^([._])/ $1/;

183 184 185 186
    $data{$tbl}{$col}{$row}++;
    $names{"col"}{$col}++;
    $names{"row"}{$row}++;
    $names{"tbl"}{$tbl}++;
187 188 189 190
    
    $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o);
    $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o);
    $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o);
191 192
}

193 194 195
my @col_names = @{get_names($names{"col"}, $col_isnumeric, $col_field)};
my @row_names = @{get_names($names{"row"}, $row_isnumeric, $row_field)};
my @tbl_names = @{get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field)};
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

# The GD::Graph package requires a particular format of data, so once we've
# gathered everything into the hashes and made sure we know the size of the
# data, we reformat it into an array of arrays of arrays of data.
push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1);
    
my @image_data;
foreach my $tbl (@tbl_names) {
    my @tbl_data;
    push(@tbl_data, \@col_names);
    foreach my $row (@row_names) {
        my @col_data;
        foreach my $col (@col_names) {
            $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
            push(@col_data, $data{$tbl}{$col}{$row});
            if ($tbl ne "-total-") {
                # This is a bit sneaky. We spend every loop except the last
                # building up the -total- data, and then last time round,
                # we process it as another tbl, and push() the total values 
                # into the image_data array.
                $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
            }
        }

        push(@tbl_data, \@col_data);
    }
    
223
    unshift(@image_data, \@tbl_data);
224 225
}

226
$vars->{'col_field'} = $col_field;
227 228 229
$vars->{'row_field'} = $row_field;
$vars->{'tbl_field'} = $tbl_field;
$vars->{'time'} = time();
230

231 232 233 234
$vars->{'col_names'} = \@col_names;
$vars->{'row_names'} = \@row_names;
$vars->{'tbl_names'} = \@tbl_names;

235
# Below a certain width, we don't see any bars, so there needs to be a minimum.
236 237
if ($width && $cgi->param('format') eq "bar") {
    my $min_width = (scalar(@col_names) || 1) * 20;
238 239 240 241 242

    if (!$cgi->param('cumulate')) {
        $min_width *= (scalar(@row_names) || 1);
    }

243
    $vars->{'min_width'} = $min_width;
244 245
}

246 247 248 249
$vars->{'width'} = $width if $width;
$vars->{'height'} = $height if $height;

$vars->{'query'} = $query;
250
$vars->{'debug'} = $cgi->param('debug');
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265

my $formatparam = $cgi->param('format');

if ($action eq "wrap") {
    # So which template are we using? If action is "wrap", we will be using
    # no format (it gets passed through to be the format of the actual data),
    # and either report.csv.tmpl (CSV), or report.html.tmpl (everything else).
    # report.html.tmpl produces an HTML framework for either tables of HTML
    # data, or images generated by calling report.cgi again with action as
    # "plot".
    $formatparam =~ s/[^a-zA-Z\-]//g;
    trick_taint($formatparam);
    $vars->{'format'} = $formatparam;
    $formatparam = '';

266 267 268 269 270 271 272
    # We need to keep track of the defined restrictions on each of the 
    # axes, because buglistbase, below, throws them away. Without this, we
    # get buglistlinks wrong if there is a restriction on an axis field.
    $vars->{'col_vals'} = join("&", $::buffer =~ /[&?]($col_field=[^&]+)/g);
    $vars->{'row_vals'} = join("&", $::buffer =~ /[&?]($row_field=[^&]+)/g);
    $vars->{'tbl_vals'} = join("&", $::buffer =~ /[&?]($tbl_field=[^&]+)/g);
    
273 274 275
    # We need a number of different variants of the base URL for different
    # URLs in the HTML.
    $vars->{'buglistbase'} = $cgi->canonicalise_query(
276
                                 "x_axis_field", "y_axis_field", "z_axis_field",
277
                               "ctype", "format", "query_format", @axis_fields);
278 279 280
    $vars->{'imagebase'}   = $cgi->canonicalise_query( 
                    $tbl_field, "action", "ctype", "format", "width", "height");
    $vars->{'switchbase'}  = $cgi->canonicalise_query( 
281
                "query_format", "action", "ctype", "format", "width", "height");
282 283 284 285 286 287
    $vars->{'data'} = \%data;
}
elsif ($action eq "plot") {
    # If action is "plot", we will be using a format as normal (pie, bar etc.)
    # and a ctype as normal (currently only png.)
    $vars->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0;
288
    $vars->{'x_labels_vertical'} = $cgi->param('x_labels_vertical') ? 1 : 0;
289 290 291 292 293 294 295
    $vars->{'data'} = \@image_data;
}
else {
    ThrowUserError("unknown_action", {action => $cgi->param('action')});
}

my $format = GetFormat("reports/report", $formatparam, $cgi->param('ctype'));
296

297 298 299
# If we get a template or CGI error, it comes out as HTML, which isn't valid
# PNG data, and the browser just displays a "corrupt PNG" message. So, you can
# set debug=1 to always get an HTML content-type, and view the error.
300
$format->{'ctype'} = "text/html" if $cgi->param('debug');
301

302 303 304
my @time = localtime(time());
my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
my $filename = "report-$date.$format->{extension}";
305 306
print $cgi->header(-type => $format->{'ctype'},
                   -content_disposition => "inline; filename=$filename");
307 308 309

# Problems with this CGI are often due to malformed data. Setting debug=1
# prints out both data structures.
310
if ($cgi->param('debug')) {
311
    require Data::Dumper;
312
    print "<pre>data hash:\n";
313
    print Data::Dumper::Dumper(%data) . "\n\n";
314
    print "data array:\n";
315
    print Data::Dumper::Dumper(@image_data) . "\n\n</pre>";
316 317
}

318 319
$template->process("$format->{'template'}", $vars)
  || ThrowTemplateError($template->error());
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359

exit;


sub get_names {
    my ($names, $isnumeric, $field) = @_;
  
    # These are all the fields we want to preserve the order of in reports.
    my %fields = ('priority'     => \@::legal_priority,
                  'bug_severity' => \@::legal_severity,
                  'rep_platform' => \@::legal_platform,
                  'op_sys'       => \@::legal_opsys,
                  'bug_status'   => \@::legal_bug_status,
                  'resolution'   => \@::legal_resolution);
    
    my $field_list = $fields{$field};
    my @sorted;
    
    if ($field_list) {
        my @unsorted = keys %{$names};
        
        # Extract the used fields from the field_list, in the order they 
        # appear in the field_list. This lets us keep e.g. severities in
        # the normal order.
        #
        # This is O(n^2) but it shouldn't matter for short lists.
        @sorted = map {lsearch(\@unsorted, $_) == -1 ? () : $_} @{$field_list};
    }  
    elsif ($isnumeric) {
        # It's not a field we are preserving the order of, so sort it 
        # numerically...
        sub numerically { $a <=> $b }
        @sorted = sort numerically keys(%{$names});
    } else {
        # ...or alphabetically, as appropriate.
        @sorted = sort(keys(%{$names}));
    }
  
    return \@sorted;
}