report.cgi 10.3 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);
30

31 32
use Bugzilla;

33 34
my $cgi = Bugzilla->cgi;

35 36 37
# 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");
38 39 40 41
    my $location = "query.cgi?format=" . $cgi->param('query_format') . 
      ($params ? "&$params" : "") . "\n\n";

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

45 46 47 48 49 50
use Bugzilla::Search;

ConnectToDatabase();

GetVersionTable();

51
confirm_login();
52

53
Bugzilla->switch_to_shadow_db();
54

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

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

65 66 67 68 69 70 71 72 73 74
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');
75

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

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
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.
if ($::FORM{'format'} && $::FORM{'format'} eq "table") {
    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 = '';
    }
}
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

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";
$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 139 140

my @selectnames = map($columns{$_}, @axis_fields);

141 142
# Clone the params, so that Bugzilla::Search can modify them
my $params = new Bugzilla::CGI($cgi);
143
my $search = new Bugzilla::Search('fields' => \@selectnames, 
144
                                  'params' => $params);
145 146
my $query = $search->getSQL();

147 148 149
$::SIG{TERM} = 'DEFAULT';
$::SIG{PIPE} = 'DEFAULT';

150
SendSQL($query);
151

152 153
# We have a hash of hashes for the data itself, and a hash to hold the 
# row/col/table names.
154
my %data;
155
my %names;
156

157 158
# Read the bug data and count the bugs for each possible value of row, column
# and table.
159
while (MoreSQLData()) {
160
    my ($row, $col, $tbl) = FetchSQLData();
161
    $row = "" if ($row eq $columns{''});
162 163
    $col = "" if ($col eq $columns{''});
    $tbl = "" if ($tbl eq $columns{''});
164
    
165 166 167 168
    $data{$tbl}{$col}{$row}++;
    $names{"col"}{$col}++;
    $names{"row"}{$row}++;
    $names{"tbl"}{$tbl}++;
169 170
}

171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
my @col_names = sort(keys(%{$names{"col"}}));
my @row_names = sort(keys(%{$names{"row"}}));
my @tbl_names = sort(keys(%{$names{"tbl"}}));

# 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);
    }
    
    push(@image_data, \@tbl_data);
}

204
$vars->{'col_field'} = $col_field;
205 206 207
$vars->{'row_field'} = $row_field;
$vars->{'tbl_field'} = $tbl_field;
$vars->{'time'} = time();
208

209 210 211 212
$vars->{'col_names'} = \@col_names;
$vars->{'row_names'} = \@row_names;
$vars->{'tbl_names'} = \@tbl_names;

213
# Below a certain width, we don't see any bars, so there needs to be a minimum.
214 215
if ($width && $cgi->param('format') eq "bar") {
    my $min_width = (scalar(@col_names) || 1) * 20;
216 217 218 219 220

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

221
    $vars->{'min_width'} = $min_width;
222 223
}

224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
$vars->{'width'} = $width if $width;
$vars->{'height'} = $height if $height;

$vars->{'query'} = $query;
$vars->{'debug'} = $::FORM{'debug'};

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 = '';

244 245 246 247 248 249 250
    # 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);
    
251 252 253
    # We need a number of different variants of the base URL for different
    # URLs in the HTML.
    $vars->{'buglistbase'} = $cgi->canonicalise_query(
254 255
                                 "x_axis_field", "y_axis_field", "z_axis_field",
                                               "ctype", "format", @axis_fields);
256 257 258 259 260 261 262 263 264 265
    $vars->{'imagebase'}   = $cgi->canonicalise_query( 
                    $tbl_field, "action", "ctype", "format", "width", "height");
    $vars->{'switchbase'}  = $cgi->canonicalise_query( 
                                "action", "ctype", "format", "width", "height");
    $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;
266
    $vars->{'x_labels_vertical'} = $cgi->param('x_labels_vertical') ? 1 : 0;
267 268 269 270 271 272 273
    $vars->{'data'} = \@image_data;
}
else {
    ThrowUserError("unknown_action", {action => $cgi->param('action')});
}

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

275 276 277 278
# 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.
$format->{'ctype'} = "text/html" if $::FORM{'debug'};
279

280 281 282
my @time = localtime(time());
my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
my $filename = "report-$date.$format->{extension}";
283 284
print $cgi->header(-type => $format->{'ctype'},
                   -content_disposition => "inline; filename=$filename");
285 286 287 288

# Problems with this CGI are often due to malformed data. Setting debug=1
# prints out both data structures.
if ($::FORM{'debug'}) {
289
    require Data::Dumper;
290
    print "<pre>data hash:\n";
291
    print Data::Dumper::Dumper(%data) . "\n\n";
292
    print "data array:\n";
293
    print Data::Dumper::Dumper(@image_data) . "\n\n</pre>";
294 295
}

296 297
$template->process("$format->{'template'}", $vars)
  || ThrowTemplateError($template->error());