--- /dev/null
+# This module is part of debbugs, and is released
+# under the terms of the GPL version 2, or any later version. See the
+# file README and COPYING for more information.
+#
+# [Other people have contributed to this file; their copyrights should
+# be listed here too.]
+# Copyright 2008 by Don Armstrong <don@donarmstrong.com>.
+
+
+package Debbugs::CGI::Pkgreport;
+
+=head1 NAME
+
+Debbugs::CGI::Pkgreport -- specific routines for the pkgreport cgi script
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+
+=head1 BUGS
+
+None known.
+
+=cut
+
+use warnings;
+use strict;
+use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
+use Exporter qw(import);
+
+use IO::Scalar;
+use Params::Validate qw(validate_with :types);
+
+use Debbugs::Collection::Bug;
+
+use Carp;
+use List::AllUtils qw(apply);
+
+use Debbugs::Config qw(:config :globals);
+use Debbugs::CGI qw(:url :html :util);
+use Debbugs::Common qw(:misc :util :date);
+use Debbugs::Status qw(:status);
+use Debbugs::Bugs qw(bug_filter);
+use Debbugs::Packages qw(:mapping);
+
+use Debbugs::Text qw(:templates);
+use Encode qw(decode_utf8);
+
+use POSIX qw(strftime);
+
+
+BEGIN{
+ ($VERSION) = q$Revision: 494 $ =~ /^Revision:\s+([^\s+])/;
+ $DEBUG = 0 unless defined $DEBUG;
+
+ @EXPORT = ();
+ %EXPORT_TAGS = (html => [qw(short_bug_status_html pkg_htmlizebugs),
+ ],
+ misc => [qw(generate_package_info),
+ qw(determine_ordering),
+ ],
+ );
+ @EXPORT_OK = (qw());
+ Exporter::export_ok_tags(keys %EXPORT_TAGS);
+ $EXPORT_TAGS{all} = [@EXPORT_OK];
+}
+
+=head2 generate_package_info
+
+ generate_package_info($srcorbin,$package)
+
+Generates the informational bits for a package and returns it
+
+=cut
+
+sub generate_package_info{
+ my %param = validate_with(params => \@_,
+ spec => {binary => {type => BOOLEAN,
+ default => 1,
+ },
+ package => {type => SCALAR,#|ARRAYREF,
+ },
+ options => {type => HASHREF,
+ },
+ bugs => {type => ARRAYREF,
+ },
+ schema => {type => OBJECT,
+ optional => 1,
+ },
+ },
+ );
+
+ my $output_scalar = '';
+ my $output = globify_scalar(\$output_scalar);
+
+ my $package = $param{package};
+
+ my %pkgsrc = %{getpkgsrc()};
+ my $srcforpkg = $package;
+ if ($param{binary}) {
+ $srcforpkg =
+ binary_to_source(source_only => 1,
+ scalar_only => 1,
+ binary => $package,
+ hash_slice(%param,qw(schema)),
+ );
+ }
+
+ my $showpkg = html_escape($package);
+ my @maint = package_maintainer($param{binary}?'binary':'source',
+ $package,
+ hash_slice(%param,qw(schema)),
+ );
+ if (@maint) {
+ print {$output} '<p>';
+ print {$output} (@maint > 1? "Maintainer for $showpkg is "
+ : "Maintainers for $showpkg are ") .
+ package_links(maintainer => \@maint);
+ print {$output} ".</p>\n";
+ }
+ else {
+ print {$output} "<p>There is no maintainer for $showpkg. ".
+ "This means that this package no longer exists (or never existed). ".
+ "Please do not report new bugs against this package. </p>\n";
+ }
+ my @pkgs = source_to_binary(source => $srcforpkg,
+ hash_slice(%param,qw(schema)),
+ binary_only => 1,
+ # if there are distributions, only bother to
+ # show packages which are currently in a
+ # distribution.
+ @{$config{distributions}//[]} ?
+ (dist => [@{$config{distributions}}]) : (),
+ ) if defined $srcforpkg;
+ @pkgs = grep( !/^\Q$package\E$/, @pkgs );
+ if ( @pkgs ) {
+ @pkgs = sort @pkgs;
+ if ($param{binary}) {
+ print {$output} "<p>You may want to refer to the following packages that are part of the same source:\n";
+ }
+ else {
+ print {$output} "<p>You may want to refer to the following individual bug pages:\n";
+ }
+ #push @pkgs, $src if ( $src && !grep(/^\Q$src\E$/, @pkgs) );
+ print {$output} scalar package_links(package=>[@pkgs]);
+ print {$output} ".\n";
+ }
+ my @references;
+ my $pseudodesc = getpseudodesc();
+ if ($package and defined($pseudodesc) and exists($pseudodesc->{$package})) {
+ push @references, "to the <a href=\"$config{web_domain}/pseudo-packages$config{html_suffix}\">".
+ "list of other pseudo-packages</a>";
+ }
+ else {
+ if ($package and defined $config{package_pages} and length $config{package_pages}) {
+ push @references, sprintf "to the <a href=\"%s\">%s package page</a>",
+ html_escape("$config{package_pages}/$package"), html_escape("$package");
+ }
+ if (defined $config{package_tracking_domain} and
+ length $config{package_tracking_domain}) {
+ my $ptslink = $param{binary} ? $srcforpkg : $package;
+ # the pts only wants the source, and doesn't care about src: (#566089)
+ $ptslink =~ s/^src://;
+ push @references, q(to the <a href=").html_escape("$config{package_tracking_domain}/$ptslink").q(">Package Tracking System</a>);
+ }
+ # Only output this if the source listing is non-trivial.
+ if ($param{binary} and $srcforpkg) {
+ push @references,
+ "to the source package ".
+ package_links(src=>$srcforpkg,
+ options => $param{options}) .
+ "'s bug page";
+ }
+ }
+ if (@references) {
+ $references[$#references] = "or $references[$#references]" if @references > 1;
+ print {$output} "<p>You might like to refer ", join(", ", @references), ".</p>\n";
+ }
+ if (@maint) {
+ print {$output} "<p>If you find a bug not listed here, please\n";
+ printf {$output} "<a href=\"%s\">report it</a>.</p>\n",
+ html_escape("$config{web_domain}/Reporting$config{html_suffix}");
+ }
+ return decode_utf8($output_scalar);
+}
+
+
+=head2 short_bug_status_html
+
+ print short_bug_status_html(status => read_bug(bug => 5),
+ options => \%param,
+ );
+
+=over
+
+=item status -- status hashref as returned by read_bug
+
+=item options -- hashref of options to pass to package_links (defaults
+to an empty hashref)
+
+=item bug_options -- hashref of options to pass to bug_links (default
+to an empty hashref)
+
+=item snippet -- optional snippet of information about the bug to
+display below
+
+
+=back
+
+
+
+=cut
+
+sub short_bug_status_html {
+ my %param = validate_with(params => \@_,
+ spec => {bug => {type => OBJECT,
+ isa => 'Debbugs::Bug',
+ },
+ },
+ );
+
+ return fill_in_template(template => 'cgi/short_bug_status',
+ variables => {bug => $param{bug},
+ isstrongseverity => \&Debbugs::Status::isstrongseverity,
+ html_escape => \&Debbugs::CGI::html_escape,
+ looks_like_number => \&Scalar::Util::looks_like_number,
+ },
+ hole_var => {'&package_links' => \&Debbugs::CGI::package_links,
+ '&bug_links' => \&Debbugs::CGI::bug_links,
+ '&version_url' => \&Debbugs::CGI::version_url,
+ '&secs_to_english' => \&Debbugs::Common::secs_to_english,
+ '&strftime' => \&POSIX::strftime,
+ '&maybelink' => \&Debbugs::CGI::maybelink,
+ },
+ );
+}
+
+
+sub pkg_htmlizebugs {
+ my %param = validate_with(params => \@_,
+ spec => {bugs => {type => OBJECT,
+ },
+ names => {type => ARRAYREF,
+ },
+ title => {type => ARRAYREF,
+ },
+ prior => {type => ARRAYREF,
+ },
+ order => {type => ARRAYREF,
+ },
+ ordering => {type => SCALAR,
+ },
+ bugusertags => {type => HASHREF,
+ default => {},
+ },
+ bug_rev => {type => BOOLEAN,
+ default => 0,
+ },
+ bug_order => {type => SCALAR,
+ },
+ repeatmerged => {type => BOOLEAN,
+ default => 1,
+ },
+ include => {type => ARRAYREF,
+ default => [],
+ },
+ exclude => {type => ARRAYREF,
+ default => [],
+ },
+ this => {type => SCALAR,
+ default => '',
+ },
+ options => {type => HASHREF,
+ default => {},
+ },
+ dist => {type => SCALAR,
+ optional => 1,
+ },
+ schema => {type => OBJECT,
+ optional => 1,
+ },
+ }
+ );
+ my $bugs = $param{bugs};
+ my %count;
+ my $header = '';
+ my $footer = "<h2 class=\"outstanding\">Summary</h2>\n";
+
+ if ($bugs->count == 0) {
+ return "<HR><H2>No reports found!</H2></HR>\n";
+ }
+
+ my %seenmerged;
+
+ my %common = (
+ 'show_list_header' => 1,
+ 'show_list_footer' => 1,
+ );
+
+ my %section = ();
+ # Make the include/exclude map
+ my %include;
+ my %exclude;
+ for my $include (make_list($param{include})) {
+ next unless defined $include;
+ my ($key,$value) = split /\s*:\s*/,$include,2;
+ unless (defined $value) {
+ $key = 'tags';
+ $value = $include;
+ }
+ push @{$include{$key}}, split /\s*,\s*/, $value;
+ }
+ for my $exclude (make_list($param{exclude})) {
+ next unless defined $exclude;
+ my ($key,$value) = split /\s*:\s*/,$exclude,2;
+ unless (defined $value) {
+ $key = 'tags';
+ $value = $exclude;
+ }
+ push @{$exclude{$key}}, split /\s*,\s*/, $value;
+ }
+
+ my $sorter = sub {$_[0]->id <=> $_[1]->id};
+ if ($param{bug_rev}) {
+ $sorter = sub {$_[1]->id <=> $_[0]->id}
+ }
+ elsif ($param{bug_order} eq 'age') {
+ $sorter = sub {$_[0]->modified->epoch <=> $_[1]->modified->epoch};
+ }
+ elsif ($param{bug_order} eq 'agerev') {
+ $sorter = sub {$_[1]->modified->epoch <=> $_[0]->modified->epoch};
+ }
+ my @status;
+ for my $bug ($bugs->sort($sorter)) {
+ next if
+ $bug->filter(repeat_merged => $param{repeatmerged},
+ seen_merged => \%seenmerged,
+ (keys %include ? (include => \%include):()),
+ (keys %exclude ? (exclude => \%exclude):()),
+ );
+
+ my $html = "<li>"; #<a href=\"%s\">#%d: %s</a>\n<br>",
+ $html .= short_bug_status_html(bug => $bug,
+ ) . "\n";
+ push @status, [ $bug, $html ];
+ }
+ # parse bug order indexes into subroutines
+ my @order_subs =
+ map {
+ my $a = $_;
+ [map {parse_order_statement_to_subroutine($_)} @{$a}];
+ } @{$param{prior}};
+ for my $entry (@status) {
+ my $key = "";
+ for my $i (0..$#order_subs) {
+ my $v = get_bug_order_index($order_subs[$i], $entry->[0]);
+ $count{"g_${i}_${v}"}++;
+ $key .= "_$v";
+ }
+ $section{$key} .= $entry->[1];
+ $count{"_$key"}++;
+ }
+
+ my $result = "";
+ if ($param{ordering} eq "raw") {
+ $result .= "<UL class=\"bugs\">\n" . join("", map( { $_->[ 1 ] } @status ) ) . "</UL>\n";
+ }
+ else {
+ $header .= "<div class=\"msgreceived\">\n<ul>\n";
+ my @keys_in_order = ("");
+ for my $o (@{$param{order}}) {
+ push @keys_in_order, "X";
+ while ((my $k = shift @keys_in_order) ne "X") {
+ for my $k2 (@{$o}) {
+ $k2+=0;
+ push @keys_in_order, "${k}_${k2}";
+ }
+ }
+ }
+ for my $order (@keys_in_order) {
+ next unless defined $section{$order};
+ my @ttl = split /_/, $order;
+ shift @ttl;
+ my $title = $param{title}[0]->[$ttl[0]] . " bugs";
+ if ($#ttl > 0) {
+ $title .= " -- ";
+ $title .= join("; ", grep {($_ || "") ne ""}
+ map { $param{title}[$_]->[$ttl[$_]] } 1..$#ttl);
+ }
+ $title = html_escape($title);
+
+ my $count = $count{"_$order"};
+ my $bugs = $count == 1 ? "bug" : "bugs";
+
+ $header .= "<li><a href=\"#$order\">$title</a> ($count $bugs)</li>\n";
+ if ($common{show_list_header}) {
+ my $count = $count{"_$order"};
+ my $bugs = $count == 1 ? "bug" : "bugs";
+ $result .= "<H2 CLASS=\"outstanding\"><a name=\"$order\"></a>$title ($count $bugs)</H2>\n";
+ }
+ else {
+ $result .= "<H2 CLASS=\"outstanding\">$title</H2>\n";
+ }
+ $result .= "<div class=\"msgreceived\">\n<UL class=\"bugs\">\n";
+ $result .= "\n\n\n\n";
+ $result .= $section{$order};
+ $result .= "\n\n\n\n";
+ $result .= "</UL>\n</div>\n";
+ }
+ $header .= "</ul></div>\n";
+
+ $footer .= "<div class=\"msgreceived\">\n<ul>\n";
+ for my $i (0..$#{$param{prior}}) {
+ my $local_result = '';
+ foreach my $key ( @{$param{order}[$i]} ) {
+ my $count = $count{"g_${i}_$key"};
+ next if !$count or !$param{title}[$i]->[$key];
+ $local_result .= "<li>$count $param{title}[$i]->[$key]</li>\n";
+ }
+ if ( $local_result ) {
+ $footer .= "<li>$param{names}[$i]<ul>\n$local_result</ul></li>\n";
+ }
+ }
+ $footer .= "</ul>\n</div>\n";
+ }
+
+ $result = $header . $result if ( $common{show_list_header} );
+ $result .= $footer if ( $common{show_list_footer} );
+ return $result;
+}
+
+sub parse_order_statement_to_subroutine {
+ my ($statement) = @_;
+ if (not defined $statement or not length $statement) {
+ return sub {return 1};
+ }
+ croak "invalid statement '$statement'" unless
+ $statement =~ /^(?:(package|tag|pending|severity) # field
+ = # equals
+ ([^=|\&,\+]+(?:,[^=|\&,+])*) #value
+ (\+|,|$) # joiner or end
+ )+ # one or more of these statements
+ /x;
+ my @sub_bits;
+ while ($statement =~ /(?<joiner>^|,|\+) # joiner
+ (?<field>package|tag|pending|severity) # field
+ = # equals
+ (?<value>[^=|\&,\+]+(?:,[^=|\&,\+])*) #value
+ /xg) {
+ my $field = $+{field};
+ my $value = $+{value};
+ my $joiner = $+{joiner} // '';
+ my @vals = apply {quotemeta($_)} split /,/,$value;
+ if (length $joiner) {
+ if ($joiner eq '+') {
+ push @sub_bits, ' and ';
+ }
+ else {
+ push @sub_bits, ' or ';
+ }
+ }
+ my @vals_bits;
+ for my $val (@vals) {
+ if ($field =~ /package|severity/o) {
+ push @vals_bits, '$_[0]->status->'.$field.
+ ' eq q('.$val.')';
+ } elsif ($field eq 'tag') {
+ push @vals_bits, '$_[0]->tags->is_set('.
+ 'q('.$val.'))';
+ } elsif ($field eq 'pending') {
+ push @vals_bits, '$_[0]->'.$field.
+ ' eq q('.$val.')';
+ }
+ }
+ push @sub_bits ,' ('.join(' or ',@vals_bits).') ';
+ }
+ # return a subroutine reference which determines whether an order statement
+ # matches this bug
+ my $sub = 'sub { return ('.join ("\n",@sub_bits).');};';
+ my $subref = eval $sub;
+ if ($@) {
+ croak "Unable to generate subroutine: $@; $sub";
+ }
+ return $subref;
+}
+
+sub parse_order_statement_into_boolean {
+ my ($statement,$status,$tags) = @_;
+
+ if (not defined $tags) {
+ $tags = {map { $_, 1 } split / /, $status->{"tags"}
+ }
+ if defined $status->{"tags"};
+
+ }
+ # replace all + with &&
+ $statement =~ s/\+/&&/g;
+ # replace all , with ||
+ $statement =~ s/,/||/g;
+ $statement =~ s{([^\&\|\=]+) # field
+ =
+ ([^\&\|\=]+) # value
+ }{
+ my $ok = 0;
+ if ($1 eq 'tag') {
+ $ok = 1 if defined $tags->{$2};
+ } else {
+ $ok = 1 if defined $status->{$1} and
+ $status->{$1} eq $2;
+ }
+ $ok;
+ }exg;
+ # check that the parsed statement is just valid boolean statements
+ if ($statement =~ /^([01\(\)\&\|]+)$/) {
+ return eval "$1";
+ } else {
+ # this is an invalid boolean statement
+ return 0;
+ }
+}
+
+sub get_bug_order_index {
+ my ($order,$bug) = @_;
+ my $pos = 0;
+ for my $el (@{$order}) {
+ if ($el->($bug)) {
+ return $pos;
+ }
+ $pos++;
+ }
+ return $pos;
+}
+
+# sets: my @names; my @prior; my @title; my @order;
+
+sub determine_ordering {
+ my %param = validate_with(params => \@_,
+ spec => {cats => {type => HASHREF,
+ },
+ param => {type => HASHREF,
+ },
+ ordering => {type => SCALARREF,
+ },
+ names => {type => ARRAYREF,
+ },
+ pend_rev => {type => BOOLEAN,
+ default => 0,
+ },
+ sev_rev => {type => BOOLEAN,
+ default => 0,
+ },
+ prior => {type => ARRAYREF,
+ },
+ title => {type => ARRAYREF,
+ },
+ order => {type => ARRAYREF,
+ },
+ },
+ );
+ $param{cats}{status}[0]{ord} = [ reverse @{$param{cats}{status}[0]{ord}} ]
+ if ($param{pend_rev});
+ $param{cats}{severity}[0]{ord} = [ reverse @{$param{cats}{severity}[0]{ord}} ]
+ if ($param{sev_rev});
+
+ my $i;
+ if (defined $param{param}{"pri0"}) {
+ my @c = ();
+ $i = 0;
+ while (defined $param{param}{"pri$i"}) {
+ my $h = {};
+
+ my ($pri) = make_list($param{param}{"pri$i"});
+ if ($pri =~ m/^([^:]*):(.*)$/) {
+ $h->{"nam"} = $1; # overridden later if necesary
+ $h->{"pri"} = [ map { "$1=$_" } (split /,/, $2) ];
+ }
+ else {
+ $h->{"pri"} = [ split /,/, $pri ];
+ }
+
+ ($h->{"nam"}) = make_list($param{param}{"nam$i"})
+ if (defined $param{param}{"nam$i"});
+ $h->{"ord"} = [ map {split /\s*,\s*/} make_list($param{param}{"ord$i"}) ]
+ if (defined $param{param}{"ord$i"});
+ $h->{"ttl"} = [ map {split /\s*,\s*/} make_list($param{param}{"ttl$i"}) ]
+ if (defined $param{param}{"ttl$i"});
+
+ push @c, $h;
+ $i++;
+ }
+ $param{cats}{"_"} = [@c];
+ ${$param{ordering}} = "_";
+ }
+
+ ${$param{ordering}} = "normal" unless defined $param{cats}{${$param{ordering}}};
+
+ sub get_ordering {
+ my @res;
+ my $cats = shift;
+ my $o = shift;
+ for my $c (@{$cats->{$o}}) {
+ if (ref($c) eq "HASH") {
+ push @res, $c;
+ }
+ else {
+ push @res, get_ordering($cats, $c);
+ }
+ }
+ return @res;
+ }
+ my @cats = get_ordering($param{cats}, ${$param{ordering}});
+
+ sub toenglish {
+ my $expr = shift;
+ $expr =~ s/[+]/ and /g;
+ $expr =~ s/[a-z]+=//g;
+ return $expr;
+ }
+
+ $i = 0;
+ for my $c (@cats) {
+ $i++;
+ push @{$param{prior}}, $c->{"pri"};
+ push @{$param{names}}, ($c->{"nam"} || "Bug attribute #" . $i);
+ if (defined $c->{"ord"}) {
+ push @{$param{order}}, $c->{"ord"};
+ }
+ else {
+ push @{$param{order}}, [ 0..$#{$param{prior}[-1]} ];
+ }
+ my @t = @{ $c->{"ttl"} } if defined $c->{ttl};
+ if (@t < $#{$param{prior}[-1]}) {
+ push @t, map { toenglish($param{prior}[-1][$_]) } @t..($#{$param{prior}[-1]});
+ }
+ push @t, $c->{"def"} || "";
+ push @{$param{title}}, [@t];
+ }
+}
+
+
+
+
+1;
+
+
+__END__
+
+
+
+
+
+