blob: 590bb573c786993f53518219d8e11b39b8a8e160 [file] [log] [blame]
ddkilzer@apple.comf573e632009-07-03 02:14:39 +00001#!/usr/bin/env perl -wT
timothy@apple.comf42518d2008-02-06 20:19:16 +00002# -*- 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
25
26use strict;
27
28use AnyDBM_File;
29
ddkilzer@apple.com097da082009-07-03 02:14:25 +000030use lib qw(. lib);
timothy@apple.comf42518d2008-02-06 20:19:16 +000031
timothy@apple.comf42518d2008-02-06 20:19:16 +000032use Bugzilla;
timothy@apple.comf42518d2008-02-06 20:19:16 +000033use Bugzilla::Constants;
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000034use Bugzilla::Util;
35use Bugzilla::Error;
36use Bugzilla::Search;
37use Bugzilla::Product;
timothy@apple.comf42518d2008-02-06 20:19:16 +000038
39my $cgi = Bugzilla->cgi;
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000040my $template = Bugzilla->template;
41my $vars = {};
timothy@apple.comf42518d2008-02-06 20:19:16 +000042
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.
46if ($::ENV{'GATEWAY_INTERFACE'} eq "cmdline") {
47 Bugzilla->login(LOGIN_OPTIONAL);
48}
49else {
50 Bugzilla->login();
51}
52
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000053my $dbh = Bugzilla->switch_to_shadow_db();
timothy@apple.comf42518d2008-02-06 20:19:16 +000054
55my %dbmcount;
56my %count;
57my %before;
58
59# Get params from URL
60sub formvalue {
61 my ($name, $default) = (@_);
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000062 return Bugzilla->cgi->param($name) || $default || "";
timothy@apple.comf42518d2008-02-06 20:19:16 +000063}
64
65my $sortby = formvalue("sortby");
66my $changedsince = formvalue("changedsince", 7);
67my $maxrows = formvalue("maxrows", 100);
68my $openonly = formvalue("openonly");
69my $reverse = formvalue("reverse") ? 1 : 0;
70my @query_products = $cgi->param('product');
71my $sortvisible = formvalue("sortvisible");
72my @buglist = (split(/[:,]/, formvalue("bug_id")));
73
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000074# Make sure all products are valid.
timothy@apple.comf42518d2008-02-06 20:19:16 +000075foreach my $p (@query_products) {
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000076 Bugzilla::Product::check_product($p);
timothy@apple.comf42518d2008-02-06 20:19:16 +000077}
78
79# Small backwards-compatibility hack, dated 2002-04-10.
80$sortby = "count" if $sortby eq "dup_count";
81
82# Open today's record of dupes
83my $today = days_ago(0);
84my $yesterday = days_ago(1);
85
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000086# We don't know the exact file name, because the extension depends on the
timothy@apple.comf42518d2008-02-06 20:19:16 +000087# 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
91
92use Errno;
93use Fcntl;
94
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +000095my $datadir = bz_locations()->{'datadir'};
96
timothy@apple.comf42518d2008-02-06 20:19:16 +000097if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$today",
98 O_RDONLY, 0644)) {
99 if ($!{ENOENT}) {
100 if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$yesterday",
101 O_RDONLY, 0644)) {
102 my $vars = { today => $today };
103 if ($!{ENOENT}) {
104 ThrowUserError("no_dupe_stats", $vars);
105 } else {
106 $vars->{'error_msg'} = $!;
107 ThrowUserError("no_dupe_stats_error_yesterday", $vars);
108 }
109 }
110 } else {
111 ThrowUserError("no_dupe_stats_error_today",
112 { error_msg => $! });
113 }
114}
115
116# Copy hash (so we don't mess up the on-disk file when we remove entries)
117%count = %dbmcount;
118
119# Remove all those dupes under the threshold parameter.
120# We do this, before the sorting, for performance reasons.
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000121my $threshold = Bugzilla->params->{"mostfreqthreshold"};
timothy@apple.comf42518d2008-02-06 20:19:16 +0000122
123while (my ($key, $value) = each %count) {
124 delete $count{$key} if ($value < $threshold);
125
126 # If there's a buglist, restrict the bugs to that list.
127 delete $count{$key} if $sortvisible && (lsearch(\@buglist, $key) == -1);
128}
129
130my $origmaxrows = $maxrows;
131detaint_natural($maxrows)
132 || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows});
133
134my $origchangedsince = $changedsince;
135detaint_natural($changedsince)
136 || ThrowUserError("invalid_changedsince",
137 { changedsince => $origchangedsince });
138
139# Try and open the database from "changedsince" days ago
140my $dobefore = 0;
141my %delta;
142my $whenever = days_ago($changedsince);
143
144if (!tie(%before, 'AnyDBM_File', "$datadir/duplicates/dupes$whenever",
145 O_RDONLY, 0644)) {
146 # Ignore file not found errors
147 if (!$!{ENOENT}) {
148 ThrowUserError("no_dupe_stats_error_whenever",
149 { error_msg => $!,
150 changedsince => $changedsince,
151 whenever => $whenever,
152 });
153 }
154} else {
155 # Calculate the deltas
156 ($delta{$_} = $count{$_} - ($before{$_} || 0)) foreach (keys(%count));
157
158 $dobefore = 1;
159}
160
161my @bugs;
162my @bug_ids;
163
164if (scalar(%count)) {
165 # use Bugzilla::Search so that we get the security checking
166 my $params = new Bugzilla::CGI({ 'bug_id' => [keys %count] });
167
168 if ($openonly) {
169 $params->param('resolution', '---');
170 } else {
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
175
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.
180
181 # a)
182 $params->param('field0-0-0', 'bug_status');
183 $params->param('type0-0-0', 'notequals');
184 $params->param('value0-0-0', 'CLOSED');
185
186 # b) i)
187 $params->param('field0-1-0', 'bug_status');
188 $params->param('type0-1-0', 'notequals');
189 $params->param('value0-1-0', 'VERIFIED');
190
191 # b) ii)
192 $params->param('field0-1-1', 'resolution');
193 $params->param('type0-1-1', 'anyexact');
194 $params->param('value0-1-1', 'INVALID,WONTFIX');
195 }
196
197 # Restrict to product if requested
198 if ($cgi->param('product')) {
199 $params->param('product', join(',', @query_products));
200 }
201
202 my $query = new Bugzilla::Search('fields' => [qw(bugs.bug_id
203 map_components.name
204 bugs.bug_severity
205 bugs.op_sys
206 bugs.target_milestone
207 bugs.short_desc
208 bugs.bug_status
209 bugs.resolution
210 )
211 ],
212 'params' => $params,
213 );
214
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000215 my $results = $dbh->selectall_arrayref($query->getSQL());
timothy@apple.comf42518d2008-02-06 20:19:16 +0000216
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000217 foreach my $result (@$results) {
timothy@apple.comf42518d2008-02-06 20:19:16 +0000218 # Note: maximum row count is dealt with in the template.
219
220 my ($id, $component, $bug_severity, $op_sys, $target_milestone,
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000221 $short_desc, $bug_status, $resolution) = @$result;
timothy@apple.comf42518d2008-02-06 20:19:16 +0000222
223 push (@bugs, { id => $id,
224 count => $count{$id},
225 delta => $delta{$id},
226 component => $component,
227 bug_severity => $bug_severity,
228 op_sys => $op_sys,
229 target_milestone => $target_milestone,
230 short_desc => $short_desc,
231 bug_status => $bug_status,
232 resolution => $resolution });
233 push (@bug_ids, $id);
234 }
235}
236
237$vars->{'bugs'} = \@bugs;
238$vars->{'bug_ids'} = \@bug_ids;
239
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;
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000249$vars->{'products'} = Bugzilla->user->get_selectable_products;
timothy@apple.comf42518d2008-02-06 20:19:16 +0000250
251
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000252my $format = $template->get_format("reports/duplicates",
253 scalar($cgi->param('format')),
254 scalar($cgi->param('ctype')));
timothy@apple.comf42518d2008-02-06 20:19:16 +0000255
ddkilzer@apple.comf3615fc2009-07-03 02:13:41 +0000256# 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.
259print $cgi->header(
260 -type => $format->{'ctype'},
261 (Bugzilla->params->{'utf8'} ? ('charset', 'utf8') : () )
262);
timothy@apple.comf42518d2008-02-06 20:19:16 +0000263
264# Generate and return the UI (HTML page) from the appropriate template.
265$template->process($format->{'template'}, $vars)
266 || ThrowTemplateError($template->error());
267
268
269sub days_ago {
270 my ($dom, $mon, $year) = (localtime(time - ($_[0]*24*60*60)))[3, 4, 5];
271 return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
272}