reports.cgi 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. #!/usr/bin/env perl -wT
  2. # -*- Mode: perl; indent-tabs-mode: nil -*-
  3. #
  4. # The contents of this file are subject to the Mozilla Public
  5. # License Version 1.1 (the "License"); you may not use this file
  6. # except in compliance with the License. You may obtain a copy of
  7. # the License at http://www.mozilla.org/MPL/
  8. #
  9. # Software distributed under the License is distributed on an "AS
  10. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  11. # implied. See the License for the specific language governing
  12. # rights and limitations under the License.
  13. #
  14. # The Original Code is the Bugzilla Bug Tracking System.
  15. #
  16. # The Initial Developer of the Original Code is Netscape Communications
  17. # Corporation. Portions created by Netscape are
  18. # Copyright (C) 1998 Netscape Communications Corporation. All
  19. # Rights Reserved.
  20. #
  21. # Contributor(s): Harrison Page <harrison@netscape.com>,
  22. # Terry Weissman <terry@mozilla.org>,
  23. # Dawn Endico <endico@mozilla.org>
  24. # Bryce Nesbitt <bryce@nextbus.COM>,
  25. # Joe Robins <jmrobins@tgix.com>,
  26. # Gervase Markham <gerv@gerv.net> and Adam Spiers <adam@spiers.net>
  27. # Added ability to chart any combination of resolutions/statuses.
  28. # Derive the choice of resolutions/statuses from the -All- data file
  29. # Removed hardcoded order of resolutions/statuses when reading from
  30. # daily stats file, so now works independently of collectstats.pl
  31. # version
  32. # Added image caching by date and datasets
  33. # Myk Melez <myk@mozilla.org>:
  34. # Implemented form field validation and reorganized code.
  35. # Frédéric Buclin <LpSolit@gmail.com>:
  36. # Templatization.
  37. use strict;
  38. use lib qw(. lib);
  39. use Bugzilla;
  40. use Bugzilla::Constants;
  41. use Bugzilla::Util;
  42. use Bugzilla::Error;
  43. use Bugzilla::Status;
  44. eval "use GD";
  45. $@ && ThrowCodeError("gd_not_installed");
  46. eval "use Chart::Lines";
  47. $@ && ThrowCodeError("chart_lines_not_installed");
  48. my $dir = bz_locations()->{'datadir'} . "/mining";
  49. my $graph_url = 'graphs';
  50. my $graph_dir = bz_locations()->{'libpath'} . '/' .$graph_url;
  51. # If we're using bug groups for products, we should apply those restrictions
  52. # to viewing reports, as well. Time to check the login in that case.
  53. my $user = Bugzilla->login();
  54. Bugzilla->switch_to_shadow_db();
  55. my $cgi = Bugzilla->cgi;
  56. my $template = Bugzilla->template;
  57. my $vars = {};
  58. # We only want those products that the user has permissions for.
  59. my @myproducts;
  60. push( @myproducts, "-All-");
  61. # Extract product names from objects and add them to the list.
  62. push( @myproducts, map { $_->name } @{$user->get_selectable_products} );
  63. if (! defined $cgi->param('product')) {
  64. # Can we do bug charts?
  65. (-d $dir && -d $graph_dir)
  66. || ThrowCodeError('chart_dir_nonexistent',
  67. {dir => $dir, graph_dir => $graph_dir});
  68. my %default_sel = map { $_ => 1 } BUG_STATE_OPEN;
  69. my @datasets;
  70. my @data = get_data($dir);
  71. foreach my $dataset (@data) {
  72. my $datasets = {};
  73. $datasets->{'value'} = $dataset;
  74. $datasets->{'selected'} = $default_sel{$dataset} ? 1 : 0;
  75. push(@datasets, $datasets);
  76. }
  77. $vars->{'datasets'} = \@datasets;
  78. $vars->{'products'} = \@myproducts;
  79. print $cgi->header();
  80. $template->process('reports/old-charts.html.tmpl', $vars)
  81. || ThrowTemplateError($template->error());
  82. exit;
  83. }
  84. else {
  85. my $product = $cgi->param('product');
  86. # For security and correctness, validate the value of the "product" form variable.
  87. # Valid values are those products for which the user has permissions which appear
  88. # in the "product" drop-down menu on the report generation form.
  89. grep($_ eq $product, @myproducts)
  90. || ThrowUserError("invalid_product_name", {product => $product});
  91. # We've checked that the product exists, and that the user can see it
  92. # This means that is OK to detaint
  93. trick_taint($product);
  94. defined($cgi->param('datasets')) || ThrowUserError('missing_datasets');
  95. my $datasets = join('', $cgi->param('datasets'));
  96. my $type = chart_image_type();
  97. my $data_file = daily_stats_filename($product);
  98. my $image_file = chart_image_name($data_file, $type, $datasets);
  99. my $url_image = correct_urlbase() . "$graph_url/$image_file";
  100. if (! -e "$graph_dir/$image_file") {
  101. generate_chart("$dir/$data_file", "$graph_dir/$image_file", $type,
  102. $product, $datasets);
  103. }
  104. $vars->{'url_image'} = $url_image;
  105. print $cgi->header(-Content_Disposition=>'inline; filename=bugzilla_report.html');
  106. $template->process('reports/old-charts.html.tmpl', $vars)
  107. || ThrowTemplateError($template->error());
  108. exit;
  109. }
  110. #####################
  111. # Subroutines #
  112. #####################
  113. sub get_data {
  114. my $dir = shift;
  115. my @datasets;
  116. my $datafile = daily_stats_filename('-All-');
  117. open(DATA, '<', "$dir/$datafile")
  118. || ThrowCodeError('chart_file_open_fail', {filename => "$dir/$datafile"});
  119. while (<DATA>) {
  120. if (/^# fields?: (.+)\s*$/) {
  121. @datasets = grep ! /date/i, (split /\|/, $1);
  122. last;
  123. }
  124. }
  125. close(DATA);
  126. return @datasets;
  127. }
  128. sub daily_stats_filename {
  129. my ($prodname) = @_;
  130. $prodname =~ s/\//-/gs;
  131. return $prodname;
  132. }
  133. sub chart_image_type {
  134. # what chart type should we be generating?
  135. my $testimg = Chart::Lines->new(2,2);
  136. my $type = $testimg->can('gif') ? "gif" : "png";
  137. undef $testimg;
  138. return $type;
  139. }
  140. sub chart_image_name {
  141. my ($data_file, $type, $datasets) = @_;
  142. # This routine generates a filename from the requested fields. The problem
  143. # is that we have to check the safety of doing this. We can't just require
  144. # that the fields exist, because what stats were collected could change
  145. # over time (eg by changing the resolutions available)
  146. # Instead, just require that each field name consists only of letters,
  147. # numbers, underscores and hyphens.
  148. if ($datasets !~ m/^[A-Za-z0-9:_-]+$/) {
  149. ThrowUserError('invalid_datasets', {'datasets' => $datasets});
  150. }
  151. # Since we pass the tests, consider it OK
  152. trick_taint($datasets);
  153. # Cache charts by generating a unique filename based on what they
  154. # show. Charts should be deleted by collectstats.pl nightly.
  155. my $id = join ("_", split (":", $datasets));
  156. return "${data_file}_${id}.$type";
  157. }
  158. sub generate_chart {
  159. my ($data_file, $image_file, $type, $product, $datasets) = @_;
  160. if (! open FILE, $data_file) {
  161. if ($product eq '-All-') {
  162. $product = '';
  163. }
  164. ThrowCodeError('chart_data_not_generated', {'product' => $product});
  165. }
  166. my @fields;
  167. my @labels = qw(DATE);
  168. my %datasets = map { $_ => 1 } split /:/, $datasets;
  169. my %data = ();
  170. while (<FILE>) {
  171. chomp;
  172. next unless $_;
  173. if (/^#/) {
  174. if (/^# fields?: (.*)\s*$/) {
  175. @fields = split /\||\r/, $1;
  176. $data{$_} ||= [] foreach @fields;
  177. unless ($fields[0] =~ /date/i) {
  178. ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
  179. }
  180. push @labels, grep($datasets{$_}, @fields);
  181. }
  182. next;
  183. }
  184. unless (@fields) {
  185. ThrowCodeError('chart_datafile_corrupt', {'file' => $data_file});
  186. }
  187. my @line = split /\|/;
  188. my $date = $line[0];
  189. my ($yy, $mm, $dd) = $date =~ /^\d{2}(\d{2})(\d{2})(\d{2})$/;
  190. push @{$data{DATE}}, "$mm/$dd/$yy";
  191. for my $i (1 .. $#fields) {
  192. my $field = $fields[$i];
  193. if (! defined $line[$i] or $line[$i] eq '') {
  194. # no data point given, don't plot (this will probably
  195. # generate loads of Chart::Base warnings, but that's not
  196. # our fault.)
  197. push @{$data{$field}}, undef;
  198. }
  199. else {
  200. push @{$data{$field}}, $line[$i];
  201. }
  202. }
  203. }
  204. shift @labels;
  205. close FILE;
  206. if (! @{$data{DATE}}) {
  207. ThrowUserError('insufficient_data_points');
  208. }
  209. my $img = Chart::Lines->new (800, 600);
  210. my $i = 0;
  211. my $MAXTICKS = 20; # Try not to show any more x ticks than this.
  212. my $skip = 1;
  213. if (@{$data{DATE}} > $MAXTICKS) {
  214. $skip = int((@{$data{DATE}} + $MAXTICKS - 1) / $MAXTICKS);
  215. }
  216. my %settings =
  217. (
  218. "title" => "Status Counts for $product",
  219. "x_label" => "Dates",
  220. "y_label" => "Bug Counts",
  221. "legend_labels" => \@labels,
  222. "skip_x_ticks" => $skip,
  223. "y_grid_lines" => "true",
  224. "grey_background" => "false",
  225. "colors" => {
  226. # default dataset colours are too alike
  227. dataset4 => [0, 0, 0], # black
  228. },
  229. );
  230. $img->set (%settings);
  231. $img->$type($image_file, [ @data{('DATE', @labels)} ]);
  232. }