blob: 89dee1c9a5361f6160917a648bf420761a6ebd55 [file] [log] [blame]
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +00001#!/usr/bin/perl -T
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/.
timothy@apple.comf42518d2008-02-06 20:19:16 +00005#
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +00006# This Source Code Form is "Incompatible With Secondary Licenses", as
7# defined by the Mozilla Public License, v. 2.0.
timothy@apple.comf42518d2008-02-06 20:19:16 +00008
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +00009use 5.10.1;
timothy@apple.comf42518d2008-02-06 20:19:16 +000010use strict;
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000011use warnings;
timothy@apple.comf42518d2008-02-06 20:19:16 +000012
ddkilzer@apple.com097da082009-07-03 02:14:25 +000013use lib qw(. lib);
timothy@apple.comf42518d2008-02-06 20:19:16 +000014
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000015use Bugzilla;
16use Bugzilla::Constants;
17use Bugzilla::Util;
18use Bugzilla::Error;
ddkilzer@apple.com097da082009-07-03 02:14:25 +000019use Bugzilla::Status;
timothy@apple.comf42518d2008-02-06 20:19:16 +000020
ddkilzer@apple.com57772842014-10-16 16:00:58 +000021use File::Basename;
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000022use Digest::SHA qw(hmac_sha256_base64);
timothy@apple.comf42518d2008-02-06 20:19:16 +000023
24# If we're using bug groups for products, we should apply those restrictions
25# to viewing reports, as well. Time to check the login in that case.
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000026my $user = Bugzilla->login();
timothy@apple.comf42518d2008-02-06 20:19:16 +000027my $cgi = Bugzilla->cgi;
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000028my $template = Bugzilla->template;
29my $vars = {};
timothy@apple.comf42518d2008-02-06 20:19:16 +000030
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000031# We use a dummy product instance with ID 0, representing all products
32my $product_all = {id => 0};
33bless($product_all, 'Bugzilla::Product');
34
ddkilzer@apple.com57772842014-10-16 16:00:58 +000035if (!Bugzilla->feature('old_charts')) {
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000036 ThrowUserError('feature_disabled', { feature => 'old_charts' });
ddkilzer@apple.com57772842014-10-16 16:00:58 +000037}
timothy@apple.comf42518d2008-02-06 20:19:16 +000038
ddkilzer@apple.com57772842014-10-16 16:00:58 +000039my $dir = bz_locations()->{'datadir'} . "/mining";
40my $graph_dir = bz_locations()->{'graphsdir'};
41my $graph_url = basename($graph_dir);
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000042my $product_id = $cgi->param('product_id');
ddkilzer@apple.com57772842014-10-16 16:00:58 +000043
44Bugzilla->switch_to_shadow_db();
45
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000046if (! defined($product_id)) {
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000047 # Can we do bug charts?
48 (-d $dir && -d $graph_dir)
49 || ThrowCodeError('chart_dir_nonexistent',
50 {dir => $dir, graph_dir => $graph_dir});
timothy@apple.comf42518d2008-02-06 20:19:16 +000051
ddkilzer@apple.com097da082009-07-03 02:14:25 +000052 my %default_sel = map { $_ => 1 } BUG_STATE_OPEN;
timothy@apple.comf42518d2008-02-06 20:19:16 +000053
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000054 my @datasets;
55 my @data = get_data($dir);
56
57 foreach my $dataset (@data) {
58 my $datasets = {};
59 $datasets->{'value'} = $dataset;
60 $datasets->{'selected'} = $default_sel{$dataset} ? 1 : 0;
61 push(@datasets, $datasets);
62 }
63
64 $vars->{'datasets'} = \@datasets;
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000065
66 print $cgi->header();
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000067}
68else {
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000069 my $product;
70 # For security and correctness, validate the value of the "product_id" form
71 # variable. Valid values are IDs of those products for which the user has
72 # permissions which appear in the "product_id" drop-down menu on the report
73 # generation form. The product_id 0 is a special case, meaning "All
74 # Products".
75 if ($product_id) {
76 $product = Bugzilla::Product->new($product_id);
77 $product && $user->can_see_product($product->name)
78 || ThrowUserError('product_access_denied',
79 {id => $product_id});
80 }
81 else {
82 $product = $product_all;
83 }
timothy@apple.comf42518d2008-02-06 20:19:16 +000084
ddkilzer@apple.com57772842014-10-16 16:00:58 +000085 # Make sure there is something to plot.
86 my @datasets = $cgi->param('datasets');
87 scalar(@datasets) || ThrowUserError('missing_datasets');
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000088
ddkilzer@apple.com57772842014-10-16 16:00:58 +000089 if (grep { $_ !~ /^[A-Za-z0-9:_-]+$/ } @datasets) {
90 ThrowUserError('invalid_datasets', {'datasets' => \@datasets});
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000091 }
92
ddkilzer@apple.com57772842014-10-16 16:00:58 +000093 # Filenames must not be guessable as they can point to products
94 # you are not allowed to see. Also, different projects can have
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000095 # the same product IDs.
ddkilzer@apple.com57772842014-10-16 16:00:58 +000096 my $project = bz_locations()->{'project'} || '';
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +000097 my $image_file = join(':', ($project, $product->id, @datasets));
98 my $key = Bugzilla->localconfig->{'site_wide_secret'};
99 $image_file = hmac_sha256_base64($image_file, $key) . '.png';
100 $image_file =~ s/\+/-/g;
101 $image_file =~ s/\//_/g;
ddkilzer@apple.com57772842014-10-16 16:00:58 +0000102 trick_taint($image_file);
103
104 if (! -e "$graph_dir/$image_file") {
105 generate_chart($dir, "$graph_dir/$image_file", $product, \@datasets);
106 }
107
108 $vars->{'url_image'} = "$graph_url/$image_file";
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000109
timothy@apple.comf42518d2008-02-06 20:19:16 +0000110 print $cgi->header(-Content_Disposition=>'inline; filename=bugzilla_report.html');
timothy@apple.comf42518d2008-02-06 20:19:16 +0000111}
112
ddkilzer@apple.com57772842014-10-16 16:00:58 +0000113$template->process('reports/old-charts.html.tmpl', $vars)
114 || ThrowTemplateError($template->error());
115
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000116#####################
117# Subroutines #
118#####################
timothy@apple.comf42518d2008-02-06 20:19:16 +0000119
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000120sub get_data {
121 my $dir = shift;
timothy@apple.comf42518d2008-02-06 20:19:16 +0000122
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000123 my @datasets;
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +0000124 open(DATA, '<', "$dir/0")
125 || ThrowCodeError('chart_file_open_fail', {filename => "$dir/0"});
timothy@apple.comf42518d2008-02-06 20:19:16 +0000126
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000127 while (<DATA>) {
128 if (/^# fields?: (.+)\s*$/) {
129 @datasets = grep ! /date/i, (split /\|/, $1);
130 last;
131 }
132 }
133 close(DATA);
134 return @datasets;
timothy@apple.comf42518d2008-02-06 20:19:16 +0000135}
136
timothy@apple.comf42518d2008-02-06 20:19:16 +0000137sub generate_chart {
ddkilzer@apple.com57772842014-10-16 16:00:58 +0000138 my ($dir, $image_file, $product, $datasets) = @_;
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +0000139 my $data_file = $dir . '/' . $product->id;
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000140
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +0000141 if (!open(FILE, '<', $data_file)) {
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000142 ThrowCodeError('chart_data_not_generated', {'product' => $product});
timothy@apple.comf42518d2008-02-06 20:19:16 +0000143 }
144
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +0000145 my $product_in_title = $product->id ? $product->name : 'All Products';
timothy@apple.comf42518d2008-02-06 20:19:16 +0000146 my @fields;
147 my @labels = qw(DATE);
ddkilzer@apple.com57772842014-10-16 16:00:58 +0000148 my %datasets = map { $_ => 1 } @$datasets;
timothy@apple.comf42518d2008-02-06 20:19:16 +0000149
150 my %data = ();
151 while (<FILE>) {
152 chomp;
153 next unless $_;
154 if (/^#/) {
155 if (/^# fields?: (.*)\s*$/) {
156 @fields = split /\||\r/, $1;
ddkilzer@apple.com097da082009-07-03 02:14:25 +0000157 $data{$_} ||= [] foreach @fields;
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000158 unless ($fields[0] =~ /date/i) {
159 ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
160 }
timothy@apple.comf42518d2008-02-06 20:19:16 +0000161 push @labels, grep($datasets{$_}, @fields);
162 }
163 next;
164 }
165
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000166 unless (@fields) {
167 ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
168 }
169
timothy@apple.comf42518d2008-02-06 20:19:16 +0000170 my @line = split /\|/;
171 my $date = $line[0];
172 my ($yy, $mm, $dd) = $date =~ /^\d{2}(\d{2})(\d{2})(\d{2})$/;
173 push @{$data{DATE}}, "$mm/$dd/$yy";
174
175 for my $i (1 .. $#fields) {
176 my $field = $fields[$i];
177 if (! defined $line[$i] or $line[$i] eq '') {
178 # no data point given, don't plot (this will probably
179 # generate loads of Chart::Base warnings, but that's not
180 # our fault.)
181 push @{$data{$field}}, undef;
182 }
183 else {
184 push @{$data{$field}}, $line[$i];
185 }
186 }
187 }
188
189 shift @labels;
190
191 close FILE;
192
193 if (! @{$data{DATE}}) {
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000194 ThrowUserError('insufficient_data_points');
timothy@apple.comf42518d2008-02-06 20:19:16 +0000195 }
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000196
timothy@apple.comf42518d2008-02-06 20:19:16 +0000197 my $img = Chart::Lines->new (800, 600);
198 my $i = 0;
199
200 my $MAXTICKS = 20; # Try not to show any more x ticks than this.
201 my $skip = 1;
202 if (@{$data{DATE}} > $MAXTICKS) {
203 $skip = int((@{$data{DATE}} + $MAXTICKS - 1) / $MAXTICKS);
204 }
205
206 my %settings =
207 (
ddkilzer@apple.com8040bb02017-03-21 16:27:49 +0000208 "title" => "Status Counts for $product_in_title",
timothy@apple.comf42518d2008-02-06 20:19:16 +0000209 "x_label" => "Dates",
210 "y_label" => "Bug Counts",
211 "legend_labels" => \@labels,
212 "skip_x_ticks" => $skip,
213 "y_grid_lines" => "true",
214 "grey_background" => "false",
215 "colors" => {
216 # default dataset colours are too alike
217 dataset4 => [0, 0, 0], # black
218 },
219 );
220
221 $img->set (%settings);
ddkilzer@apple.com57772842014-10-16 16:00:58 +0000222 $img->png($image_file, [ @data{('DATE', @labels)} ]);
timothy@apple.comf42518d2008-02-06 20:19:16 +0000223}