#!/usr/bin/perl -wT
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Gervase Markham <gerv@gerv.net>
#
# Generates mostfreq list from data collected by collectstats.pl.


use strict;

use AnyDBM_File;

use lib qw(.);

require "globals.pl";

use Bugzilla;
use Bugzilla::Search;
use Bugzilla::Config qw(:DEFAULT $datadir);
use Bugzilla::Constants;

my $cgi = Bugzilla->cgi;

# Go directly to the XUL version of the duplicates report (duplicates.xul)
# if the user specified ctype=xul.  Adds params if they exist, and directs
# the user to a signed copy of the script in duplicates.jar if it exists.
if (defined $cgi->param('ctype') && $cgi->param('ctype') eq "xul") {
    my $params = CanonicaliseParams($cgi->query_string(), ["format", "ctype"]);
    my $url = (-e "duplicates.jar" ? "duplicates.jar!/" : "") . 
          "duplicates.xul" . ($params ? "?$params" : "") . "\n\n";

    print $cgi->redirect($url);
    exit;
}

my $template = Bugzilla->template;
my $vars = {};

GetVersionTable();

# collectstats.pl uses duplicates.cgi to generate the RDF duplicates stats.
# However, this conflicts with requirelogin if it's enabled; so we make
# logging-in optional if we are running from the command line.
if ($::ENV{'GATEWAY_INTERFACE'} eq "cmdline") {
    Bugzilla->login(LOGIN_OPTIONAL);
}
else {
    Bugzilla->login();
}

my $dbh = Bugzilla->switch_to_shadow_db();

use vars qw (@legal_product);

my %dbmcount;
my %count;
my %before;

# Get params from URL
sub formvalue {
    my ($name, $default) = (@_);
    return $cgi->param($name) || $default || "";
}

my $sortby = formvalue("sortby");
my $changedsince = formvalue("changedsince", 7);
my $maxrows = formvalue("maxrows", 100);
my $openonly = formvalue("openonly");
my $reverse = formvalue("reverse") ? 1 : 0;
my @query_products = $cgi->param('product');
my $sortvisible = formvalue("sortvisible");
my @buglist = (split(/[:,]/, formvalue("bug_id")));

my $product_id;
foreach my $p (@query_products) {
    $product_id = get_product_id($p);
    if (!$product_id) {
        ThrowUserError("invalid_product_name",
                       { product => $p });
    }
}

# Small backwards-compatibility hack, dated 2002-04-10.
$sortby = "count" if $sortby eq "dup_count";

# Open today's record of dupes
my $today = days_ago(0);
my $yesterday = days_ago(1);

# We don't know the exact file name, because the extention depends on the
# underlying dbm library, which could be anything. We can't glob, because
# perl < 5.6 considers if (<*>) { ... } to be tainted
# Instead, just check the return value for today's data and yesterday's,
# and ignore file not found errors

use Errno;
use Fcntl;

if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$today",
         O_RDONLY, 0644)) {
    if ($!{ENOENT}) {
        if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$yesterday",
                 O_RDONLY, 0644)) {
            my $vars = { today => $today };
            if ($!{ENOENT}) {
                ThrowUserError("no_dupe_stats", $vars);
            } else {
                $vars->{'error_msg'} = $!;
                ThrowUserError("no_dupe_stats_error_yesterday", $vars);
            }
        }
    } else {
        ThrowUserError("no_dupe_stats_error_today",
                       { error_msg => $! });
    }
}

# Copy hash (so we don't mess up the on-disk file when we remove entries)
%count = %dbmcount;

# Remove all those dupes under the threshold parameter. 
# We do this, before the sorting, for performance reasons.
my $threshold = Param("mostfreqthreshold");

while (my ($key, $value) = each %count) {
    delete $count{$key} if ($value < $threshold);
    
    # If there's a buglist, restrict the bugs to that list.
    delete $count{$key} if $sortvisible && (lsearch(\@buglist, $key) == -1);
}

my $origmaxrows = $maxrows;
detaint_natural($maxrows)
  || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows});

my $origchangedsince = $changedsince;
detaint_natural($changedsince)
  || ThrowUserError("invalid_changedsince", 
                    { changedsince => $origchangedsince });

# Try and open the database from "changedsince" days ago
my $dobefore = 0;
my %delta;
my $whenever = days_ago($changedsince);    

if (!tie(%before, 'AnyDBM_File', "$datadir/duplicates/dupes$whenever",
         O_RDONLY, 0644)) {
    # Ignore file not found errors
    if (!$!{ENOENT}) {
        ThrowUserError("no_dupe_stats_error_whenever",
                       { error_msg => $!,
                         changedsince => $changedsince,
                         whenever => $whenever,
                       });
    }
} else {
    # Calculate the deltas
    ($delta{$_} = $count{$_} - ($before{$_} || 0)) foreach (keys(%count));

    $dobefore = 1;
}

my @bugs;
my @bug_ids; 

if (scalar(%count)) {
    # use Bugzilla::Search so that we get the security checking
    my $params = new Bugzilla::CGI({ 'bug_id' => [keys %count] });

    if ($openonly) {
        $params->param('resolution', '---');
    } else {
        # We want to show bugs which:
        # a) Aren't CLOSED; and
        # b)  i) Aren't VERIFIED; OR
        #    ii) Were resolved INVALID/WONTFIX

        # The rationale behind this is that people will eventually stop
        # reporting fixed bugs when they get newer versions of the software,
        # but if the bug is determined to be erroneous, people will still
        # keep reporting it, so we do need to show it here.

        # a)
        $params->param('field0-0-0', 'bug_status');
        $params->param('type0-0-0', 'notequals');
        $params->param('value0-0-0', 'CLOSED');

        # b) i)
        $params->param('field0-1-0', 'bug_status');
        $params->param('type0-1-0', 'notequals');
        $params->param('value0-1-0', 'VERIFIED');

        # b) ii)
        $params->param('field0-1-1', 'resolution');
        $params->param('type0-1-1', 'anyexact');
        $params->param('value0-1-1', 'INVALID,WONTFIX');
    }

    # Restrict to product if requested
    if ($cgi->param('product')) {
        $params->param('product', join(',', @query_products));
    }

    my $query = new Bugzilla::Search('fields' => [qw(bugs.bug_id
                                                     map_components.name
                                                     bugs.bug_severity
                                                     bugs.op_sys
                                                     bugs.target_milestone
                                                     bugs.short_desc
                                                     bugs.bug_status
                                                     bugs.resolution
                                                    )
                                                 ],
                                     'params' => $params,
                                    );

    my $results = $dbh->selectall_arrayref($query->getSQL());

    foreach my $result (@$results) {
        # Note: maximum row count is dealt with in the template.

        my ($id, $component, $bug_severity, $op_sys, $target_milestone, 
            $short_desc, $bug_status, $resolution) = @$result;

        push (@bugs, { id => $id,
                       count => $count{$id},
                       delta => $delta{$id}, 
                       component => $component,
                       bug_severity => $bug_severity,
                       op_sys => $op_sys,
                       target_milestone => $target_milestone,
                       short_desc => $short_desc,
                       bug_status => $bug_status, 
                       resolution => $resolution });
        push (@bug_ids, $id); 
    }
}

$vars->{'bugs'} = \@bugs;
$vars->{'bug_ids'} = \@bug_ids;

$vars->{'dobefore'} = $dobefore;
$vars->{'sortby'} = $sortby;
$vars->{'sortvisible'} = $sortvisible;
$vars->{'changedsince'} = $changedsince;
$vars->{'maxrows'} = $maxrows;
$vars->{'openonly'} = $openonly;
$vars->{'reverse'} = $reverse;
$vars->{'format'} = $cgi->param('format');
$vars->{'query_products'} = \@query_products;
$vars->{'products'} = Bugzilla->user->get_selectable_products;


my $format = $template->get_format("reports/duplicates",
                                   scalar($cgi->param('format')),
                                   scalar($cgi->param('ctype')));

print $cgi->header($format->{'ctype'});

# Generate and return the UI (HTML page) from the appropriate template.
$template->process($format->{'template'}, $vars)
  || ThrowTemplateError($template->error());


sub days_ago {
    my ($dom, $mon, $year) = (localtime(time - ($_[0]*24*60*60)))[3, 4, 5];
    return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
}