ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 1 | #!/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.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 5 | # |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 6 | # This Source Code Form is "Incompatible With Secondary Licenses", as |
| 7 | # defined by the Mozilla Public License, v. 2.0. |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 8 | |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 9 | use 5.10.1; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 10 | use strict; |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 11 | use warnings; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 12 | |
ddkilzer@apple.com | 097da08 | 2009-07-03 02:14:25 +0000 | [diff] [blame] | 13 | use lib qw(. lib); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 14 | |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 15 | use Bugzilla; |
| 16 | use Bugzilla::Constants; |
| 17 | use Bugzilla::Util; |
| 18 | use Bugzilla::Error; |
ddkilzer@apple.com | 097da08 | 2009-07-03 02:14:25 +0000 | [diff] [blame] | 19 | use Bugzilla::Status; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 20 | |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 21 | use File::Basename; |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 22 | use Digest::SHA qw(hmac_sha256_base64); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 23 | |
| 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.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 26 | my $user = Bugzilla->login(); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 27 | my $cgi = Bugzilla->cgi; |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 28 | my $template = Bugzilla->template; |
| 29 | my $vars = {}; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 30 | |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 31 | # We use a dummy product instance with ID 0, representing all products |
| 32 | my $product_all = {id => 0}; |
| 33 | bless($product_all, 'Bugzilla::Product'); |
| 34 | |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 35 | if (!Bugzilla->feature('old_charts')) { |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 36 | ThrowUserError('feature_disabled', { feature => 'old_charts' }); |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 37 | } |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 38 | |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 39 | my $dir = bz_locations()->{'datadir'} . "/mining"; |
| 40 | my $graph_dir = bz_locations()->{'graphsdir'}; |
| 41 | my $graph_url = basename($graph_dir); |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 42 | my $product_id = $cgi->param('product_id'); |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 43 | |
| 44 | Bugzilla->switch_to_shadow_db(); |
| 45 | |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 46 | if (! defined($product_id)) { |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 47 | # 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.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 51 | |
ddkilzer@apple.com | 097da08 | 2009-07-03 02:14:25 +0000 | [diff] [blame] | 52 | my %default_sel = map { $_ => 1 } BUG_STATE_OPEN; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 53 | |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 54 | 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.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 65 | |
| 66 | print $cgi->header(); |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 67 | } |
| 68 | else { |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 69 | 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.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 84 | |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 85 | # Make sure there is something to plot. |
| 86 | my @datasets = $cgi->param('datasets'); |
| 87 | scalar(@datasets) || ThrowUserError('missing_datasets'); |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 88 | |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 89 | if (grep { $_ !~ /^[A-Za-z0-9:_-]+$/ } @datasets) { |
| 90 | ThrowUserError('invalid_datasets', {'datasets' => \@datasets}); |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 91 | } |
| 92 | |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 93 | # 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.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 95 | # the same product IDs. |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 96 | my $project = bz_locations()->{'project'} || ''; |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 97 | 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.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 102 | 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.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 109 | |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 110 | print $cgi->header(-Content_Disposition=>'inline; filename=bugzilla_report.html'); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 111 | } |
| 112 | |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 113 | $template->process('reports/old-charts.html.tmpl', $vars) |
| 114 | || ThrowTemplateError($template->error()); |
| 115 | |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 116 | ##################### |
| 117 | # Subroutines # |
| 118 | ##################### |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 119 | |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 120 | sub get_data { |
| 121 | my $dir = shift; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 122 | |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 123 | my @datasets; |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 124 | open(DATA, '<', "$dir/0") |
| 125 | || ThrowCodeError('chart_file_open_fail', {filename => "$dir/0"}); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 126 | |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 127 | 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.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 135 | } |
| 136 | |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 137 | sub generate_chart { |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 138 | my ($dir, $image_file, $product, $datasets) = @_; |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 139 | my $data_file = $dir . '/' . $product->id; |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 140 | |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 141 | if (!open(FILE, '<', $data_file)) { |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 142 | ThrowCodeError('chart_data_not_generated', {'product' => $product}); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 143 | } |
| 144 | |
ddkilzer@apple.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 145 | my $product_in_title = $product->id ? $product->name : 'All Products'; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 146 | my @fields; |
| 147 | my @labels = qw(DATE); |
ddkilzer@apple.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 148 | my %datasets = map { $_ => 1 } @$datasets; |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 149 | |
| 150 | my %data = (); |
| 151 | while (<FILE>) { |
| 152 | chomp; |
| 153 | next unless $_; |
| 154 | if (/^#/) { |
| 155 | if (/^# fields?: (.*)\s*$/) { |
| 156 | @fields = split /\||\r/, $1; |
ddkilzer@apple.com | 097da08 | 2009-07-03 02:14:25 +0000 | [diff] [blame] | 157 | $data{$_} ||= [] foreach @fields; |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 158 | unless ($fields[0] =~ /date/i) { |
| 159 | ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file}); |
| 160 | } |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 161 | push @labels, grep($datasets{$_}, @fields); |
| 162 | } |
| 163 | next; |
| 164 | } |
| 165 | |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 166 | unless (@fields) { |
| 167 | ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file}); |
| 168 | } |
| 169 | |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 170 | 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.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 194 | ThrowUserError('insufficient_data_points'); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 195 | } |
ddkilzer@apple.com | f3615fc | 2009-07-03 02:13:41 +0000 | [diff] [blame] | 196 | |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 197 | 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.com | 8040bb0 | 2017-03-21 16:27:49 +0000 | [diff] [blame] | 208 | "title" => "Status Counts for $product_in_title", |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 209 | "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.com | 5777284 | 2014-10-16 16:00:58 +0000 | [diff] [blame] | 222 | $img->png($image_file, [ @data{('DATE', @labels)} ]); |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 223 | } |