duplicates.cgi 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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): Gervase Markham <gerv@gerv.net>
  22. #
  23. # Generates mostfreq list from data collected by collectstats.pl.
  24. use strict;
  25. use AnyDBM_File;
  26. use lib qw(. lib);
  27. use Bugzilla;
  28. use Bugzilla::Constants;
  29. use Bugzilla::Util;
  30. use Bugzilla::Error;
  31. use Bugzilla::Search;
  32. use Bugzilla::Product;
  33. my $cgi = Bugzilla->cgi;
  34. my $template = Bugzilla->template;
  35. my $vars = {};
  36. # collectstats.pl uses duplicates.cgi to generate the RDF duplicates stats.
  37. # However, this conflicts with requirelogin if it's enabled; so we make
  38. # logging-in optional if we are running from the command line.
  39. if ($::ENV{'GATEWAY_INTERFACE'} eq "cmdline") {
  40. Bugzilla->login(LOGIN_OPTIONAL);
  41. }
  42. else {
  43. Bugzilla->login();
  44. }
  45. my $dbh = Bugzilla->switch_to_shadow_db();
  46. my %dbmcount;
  47. my %count;
  48. my %before;
  49. # Get params from URL
  50. sub formvalue {
  51. my ($name, $default) = (@_);
  52. return Bugzilla->cgi->param($name) || $default || "";
  53. }
  54. my $sortby = formvalue("sortby");
  55. my $changedsince = formvalue("changedsince", 7);
  56. my $maxrows = formvalue("maxrows", 100);
  57. my $openonly = formvalue("openonly");
  58. my $reverse = formvalue("reverse") ? 1 : 0;
  59. my @query_products = $cgi->param('product');
  60. my $sortvisible = formvalue("sortvisible");
  61. my @buglist = (split(/[:,]/, formvalue("bug_id")));
  62. # Make sure all products are valid.
  63. foreach my $p (@query_products) {
  64. Bugzilla::Product::check_product($p);
  65. }
  66. # Small backwards-compatibility hack, dated 2002-04-10.
  67. $sortby = "count" if $sortby eq "dup_count";
  68. # Open today's record of dupes
  69. my $today = days_ago(0);
  70. my $yesterday = days_ago(1);
  71. # We don't know the exact file name, because the extension depends on the
  72. # underlying dbm library, which could be anything. We can't glob, because
  73. # perl < 5.6 considers if (<*>) { ... } to be tainted
  74. # Instead, just check the return value for today's data and yesterday's,
  75. # and ignore file not found errors
  76. use Errno;
  77. use Fcntl;
  78. my $datadir = bz_locations()->{'datadir'};
  79. if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$today",
  80. O_RDONLY, 0644)) {
  81. if ($!{ENOENT}) {
  82. if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$yesterday",
  83. O_RDONLY, 0644)) {
  84. my $vars = { today => $today };
  85. if ($!{ENOENT}) {
  86. ThrowUserError("no_dupe_stats", $vars);
  87. } else {
  88. $vars->{'error_msg'} = $!;
  89. ThrowUserError("no_dupe_stats_error_yesterday", $vars);
  90. }
  91. }
  92. } else {
  93. ThrowUserError("no_dupe_stats_error_today",
  94. { error_msg => $! });
  95. }
  96. }
  97. # Copy hash (so we don't mess up the on-disk file when we remove entries)
  98. %count = %dbmcount;
  99. # Remove all those dupes under the threshold parameter.
  100. # We do this, before the sorting, for performance reasons.
  101. my $threshold = Bugzilla->params->{"mostfreqthreshold"};
  102. while (my ($key, $value) = each %count) {
  103. delete $count{$key} if ($value < $threshold);
  104. # If there's a buglist, restrict the bugs to that list.
  105. delete $count{$key} if $sortvisible && (lsearch(\@buglist, $key) == -1);
  106. }
  107. my $origmaxrows = $maxrows;
  108. detaint_natural($maxrows)
  109. || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows});
  110. my $origchangedsince = $changedsince;
  111. detaint_natural($changedsince)
  112. || ThrowUserError("invalid_changedsince",
  113. { changedsince => $origchangedsince });
  114. # Try and open the database from "changedsince" days ago
  115. my $dobefore = 0;
  116. my %delta;
  117. my $whenever = days_ago($changedsince);
  118. if (!tie(%before, 'AnyDBM_File', "$datadir/duplicates/dupes$whenever",
  119. O_RDONLY, 0644)) {
  120. # Ignore file not found errors
  121. if (!$!{ENOENT}) {
  122. ThrowUserError("no_dupe_stats_error_whenever",
  123. { error_msg => $!,
  124. changedsince => $changedsince,
  125. whenever => $whenever,
  126. });
  127. }
  128. } else {
  129. # Calculate the deltas
  130. ($delta{$_} = $count{$_} - ($before{$_} || 0)) foreach (keys(%count));
  131. $dobefore = 1;
  132. }
  133. my @bugs;
  134. my @bug_ids;
  135. if (scalar(%count)) {
  136. # use Bugzilla::Search so that we get the security checking
  137. my $params = new Bugzilla::CGI({ 'bug_id' => [keys %count] });
  138. if ($openonly) {
  139. $params->param('resolution', '---');
  140. } else {
  141. # We want to show bugs which:
  142. # a) Aren't CLOSED; and
  143. # b) i) Aren't VERIFIED; OR
  144. # ii) Were resolved INVALID/WONTFIX
  145. # The rationale behind this is that people will eventually stop
  146. # reporting fixed bugs when they get newer versions of the software,
  147. # but if the bug is determined to be erroneous, people will still
  148. # keep reporting it, so we do need to show it here.
  149. # a)
  150. $params->param('field0-0-0', 'bug_status');
  151. $params->param('type0-0-0', 'notequals');
  152. $params->param('value0-0-0', 'CLOSED');
  153. # b) i)
  154. $params->param('field0-1-0', 'bug_status');
  155. $params->param('type0-1-0', 'notequals');
  156. $params->param('value0-1-0', 'VERIFIED');
  157. # b) ii)
  158. $params->param('field0-1-1', 'resolution');
  159. $params->param('type0-1-1', 'anyexact');
  160. $params->param('value0-1-1', 'INVALID,WONTFIX');
  161. }
  162. # Restrict to product if requested
  163. if ($cgi->param('product')) {
  164. $params->param('product', join(',', @query_products));
  165. }
  166. my $query = new Bugzilla::Search('fields' => [qw(bugs.bug_id
  167. map_components.name
  168. bugs.bug_severity
  169. bugs.op_sys
  170. bugs.target_milestone
  171. bugs.short_desc
  172. bugs.bug_status
  173. bugs.resolution
  174. )
  175. ],
  176. 'params' => $params,
  177. );
  178. my $results = $dbh->selectall_arrayref($query->getSQL());
  179. foreach my $result (@$results) {
  180. # Note: maximum row count is dealt with in the template.
  181. my ($id, $component, $bug_severity, $op_sys, $target_milestone,
  182. $short_desc, $bug_status, $resolution) = @$result;
  183. push (@bugs, { id => $id,
  184. count => $count{$id},
  185. delta => $delta{$id},
  186. component => $component,
  187. bug_severity => $bug_severity,
  188. op_sys => $op_sys,
  189. target_milestone => $target_milestone,
  190. short_desc => $short_desc,
  191. bug_status => $bug_status,
  192. resolution => $resolution });
  193. push (@bug_ids, $id);
  194. }
  195. }
  196. $vars->{'bugs'} = \@bugs;
  197. $vars->{'bug_ids'} = \@bug_ids;
  198. $vars->{'dobefore'} = $dobefore;
  199. $vars->{'sortby'} = $sortby;
  200. $vars->{'sortvisible'} = $sortvisible;
  201. $vars->{'changedsince'} = $changedsince;
  202. $vars->{'maxrows'} = $maxrows;
  203. $vars->{'openonly'} = $openonly;
  204. $vars->{'reverse'} = $reverse;
  205. $vars->{'format'} = $cgi->param('format');
  206. $vars->{'query_products'} = \@query_products;
  207. $vars->{'products'} = Bugzilla->user->get_selectable_products;
  208. my $format = $template->get_format("reports/duplicates",
  209. scalar($cgi->param('format')),
  210. scalar($cgi->param('ctype')));
  211. # We set the charset in Bugzilla::CGI, but CGI.pm ignores it unless the
  212. # Content-Type is a text type. In some cases, such as when we are
  213. # generating RDF, it isn't, so we specify the charset again here.
  214. print $cgi->header(
  215. -type => $format->{'ctype'},
  216. (Bugzilla->params->{'utf8'} ? ('charset', 'utf8') : () )
  217. );
  218. # Generate and return the UI (HTML page) from the appropriate template.
  219. $template->process($format->{'template'}, $vars)
  220. || ThrowTemplateError($template->error());
  221. sub days_ago {
  222. my ($dom, $mon, $year) = (localtime(time - ($_[0]*24*60*60)))[3, 4, 5];
  223. return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
  224. }