1 #!/usr/bin/env perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
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/
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.
14 # The Original Code is the Bugzilla Bug Tracking System.
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
21 # Contributor(s): Gervase Markham <gerv@gerv.net>
23 # Generates mostfreq list from data collected by collectstats.pl.
33 use Bugzilla
::Constants
;
37 use Bugzilla
::Product
;
39 my $cgi = Bugzilla
->cgi;
40 my $template = Bugzilla
->template;
43 # collectstats.pl uses duplicates.cgi to generate the RDF duplicates stats.
44 # However, this conflicts with requirelogin if it's enabled; so we make
45 # logging-in optional if we are running from the command line.
46 if ($::ENV
{'GATEWAY_INTERFACE'} eq "cmdline") {
47 Bugzilla
->login(LOGIN_OPTIONAL
);
53 my $dbh = Bugzilla
->switch_to_shadow_db();
61 my ($name, $default) = (@_);
62 return Bugzilla
->cgi->param($name) || $default || "";
65 my $sortby = formvalue
("sortby");
66 my $changedsince = formvalue
("changedsince", 7);
67 my $maxrows = formvalue
("maxrows", 100);
68 my $openonly = formvalue
("openonly");
69 my $reverse = formvalue
("reverse") ?
1 : 0;
70 my @query_products = $cgi->param('product');
71 my $sortvisible = formvalue
("sortvisible");
72 my @buglist = (split(/[:,]/, formvalue
("bug_id")));
74 # Make sure all products are valid.
75 foreach my $p (@query_products) {
76 Bugzilla
::Product
::check_product
($p);
79 # Small backwards-compatibility hack, dated 2002-04-10.
80 $sortby = "count" if $sortby eq "dup_count";
82 # Open today's record of dupes
83 my $today = days_ago
(0);
84 my $yesterday = days_ago
(1);
86 # We don't know the exact file name, because the extension depends on the
87 # underlying dbm library, which could be anything. We can't glob, because
88 # perl < 5.6 considers if (<*>) { ... } to be tainted
89 # Instead, just check the return value for today's data and yesterday's,
90 # and ignore file not found errors
95 my $datadir = bz_locations
()->{'datadir'};
97 if (!tie
(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$today",
100 if (!tie
(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$yesterday",
102 my $vars = { today
=> $today };
104 ThrowUserError
("no_dupe_stats", $vars);
106 $vars->{'error_msg'} = $!;
107 ThrowUserError
("no_dupe_stats_error_yesterday", $vars);
111 ThrowUserError
("no_dupe_stats_error_today",
112 { error_msg
=> $! });
116 # Copy hash (so we don't mess up the on-disk file when we remove entries)
119 # Remove all those dupes under the threshold parameter.
120 # We do this, before the sorting, for performance reasons.
121 my $threshold = Bugzilla
->params->{"mostfreqthreshold"};
123 while (my ($key, $value) = each %count) {
124 delete $count{$key} if ($value < $threshold);
126 # If there's a buglist, restrict the bugs to that list.
127 delete $count{$key} if $sortvisible && (lsearch
(\
@buglist, $key) == -1);
130 my $origmaxrows = $maxrows;
131 detaint_natural
($maxrows)
132 || ThrowUserError
("invalid_maxrows", { maxrows
=> $origmaxrows});
134 my $origchangedsince = $changedsince;
135 detaint_natural
($changedsince)
136 || ThrowUserError
("invalid_changedsince",
137 { changedsince
=> $origchangedsince });
139 # Try and open the database from "changedsince" days ago
142 my $whenever = days_ago
($changedsince);
144 if (!tie
(%before, 'AnyDBM_File', "$datadir/duplicates/dupes$whenever",
146 # Ignore file not found errors
148 ThrowUserError
("no_dupe_stats_error_whenever",
150 changedsince
=> $changedsince,
151 whenever
=> $whenever,
155 # Calculate the deltas
156 ($delta{$_} = $count{$_} - ($before{$_} || 0)) foreach (keys(%count));
164 if (scalar(%count)) {
165 # use Bugzilla::Search so that we get the security checking
166 my $params = new Bugzilla
::CGI
({ 'bug_id' => [keys %count] });
169 $params->param('resolution', '---');
171 # We want to show bugs which:
172 # a) Aren't CLOSED; and
173 # b) i) Aren't VERIFIED; OR
174 # ii) Were resolved INVALID/WONTFIX
176 # The rationale behind this is that people will eventually stop
177 # reporting fixed bugs when they get newer versions of the software,
178 # but if the bug is determined to be erroneous, people will still
179 # keep reporting it, so we do need to show it here.
182 $params->param('field0-0-0', 'bug_status');
183 $params->param('type0-0-0', 'notequals');
184 $params->param('value0-0-0', 'CLOSED');
187 $params->param('field0-1-0', 'bug_status');
188 $params->param('type0-1-0', 'notequals');
189 $params->param('value0-1-0', 'VERIFIED');
192 $params->param('field0-1-1', 'resolution');
193 $params->param('type0-1-1', 'anyexact');
194 $params->param('value0-1-1', 'INVALID,WONTFIX');
197 # Restrict to product if requested
198 if ($cgi->param('product')) {
199 $params->param('product', join(',', @query_products));
202 my $query = new Bugzilla
::Search
('fields' => [qw(bugs.bug_id
206 bugs.target_milestone
215 my $results = $dbh->selectall_arrayref($query->getSQL());
217 foreach my $result (@
$results) {
218 # Note: maximum row count is dealt with in the template.
220 my ($id, $component, $bug_severity, $op_sys, $target_milestone,
221 $short_desc, $bug_status, $resolution) = @
$result;
223 push (@bugs, { id
=> $id,
224 count
=> $count{$id},
225 delta
=> $delta{$id},
226 component
=> $component,
227 bug_severity
=> $bug_severity,
229 target_milestone
=> $target_milestone,
230 short_desc
=> $short_desc,
231 bug_status
=> $bug_status,
232 resolution
=> $resolution });
233 push (@bug_ids, $id);
237 $vars->{'bugs'} = \
@bugs;
238 $vars->{'bug_ids'} = \
@bug_ids;
240 $vars->{'dobefore'} = $dobefore;
241 $vars->{'sortby'} = $sortby;
242 $vars->{'sortvisible'} = $sortvisible;
243 $vars->{'changedsince'} = $changedsince;
244 $vars->{'maxrows'} = $maxrows;
245 $vars->{'openonly'} = $openonly;
246 $vars->{'reverse'} = $reverse;
247 $vars->{'format'} = $cgi->param('format');
248 $vars->{'query_products'} = \
@query_products;
249 $vars->{'products'} = Bugzilla
->user->get_selectable_products;
252 my $format = $template->get_format("reports/duplicates",
253 scalar($cgi->param('format')),
254 scalar($cgi->param('ctype')));
256 # We set the charset in Bugzilla::CGI, but CGI.pm ignores it unless the
257 # Content-Type is a text type. In some cases, such as when we are
258 # generating RDF, it isn't, so we specify the charset again here.
260 -type
=> $format->{'ctype'},
261 (Bugzilla
->params->{'utf8'} ?
('charset', 'utf8') : () )
264 # Generate and return the UI (HTML page) from the appropriate template.
265 $template->process($format->{'template'}, $vars)
266 || ThrowTemplateError
($template->error());
270 my ($dom, $mon, $year) = (localtime(time - ($_[0]*24*60*60)))[3, 4, 5];
271 return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;