]> git.donarmstrong.com Git - debbugs.git/blob - cgi/pkgreport.cgi
- Allow selecting both archived and unarchived bugs (closes: #320175)
[debbugs.git] / cgi / pkgreport.cgi
1 #!/usr/bin/perl -wT
2 # This script is part of debbugs, and is released
3 # under the terms of the GPL version 2, or any later
4 # version at your option.
5 # See the file README and COPYING for more information.
6 #
7 # [Other people have contributed to this file; their copyrights should
8 # go here too.]
9 # Copyright 2004-2006 by Anthony Towns <ajt@debian.org>
10 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
11
12
13 package debbugs;
14
15 use strict;
16 use POSIX qw(strftime nice);
17
18 require './common.pl';
19
20 use Debbugs::Config qw(:globals :text :config);
21 use Debbugs::User;
22 use Debbugs::CGI qw(version_url maint_decode);
23 use Debbugs::Common qw(getparsedaddrs :date make_list);
24 use Debbugs::Bugs qw(get_bugs);
25 use Debbugs::Packages qw(getsrcpkgs getpkgsrc get_versions);
26 use Debbugs::Status qw(get_bug_status);
27
28 use vars qw($gPackagePages $gWebDomain %gSeverityDisplay @gSeverityList);
29
30 if (defined $ENV{REQUEST_METHOD} and $ENV{REQUEST_METHOD} eq 'HEAD') {
31     print "Content-Type: text/html; charset=utf-8\n\n";
32     exit 0;
33 }
34
35 nice(5);
36
37 my $userAgent = detect_user_agent();
38
39 use CGI::Simple;
40 my $q = new CGI::Simple;
41 #my %param = readparse();
42
43 my %param = cgi_parameters(query => $q,
44                            single => [qw(ordering archive repeatmerged),
45                                       qw(bug-rev pend-rev sev-rev),
46                                       qw(maxdays mindays version),
47                                       qw(data which dist),
48                                      ],
49                            default => {ordering => 'normal',
50                                        archive  => 0,
51                                        repeatmerged => 1,
52                                       },
53                           );
54
55 # map from yes|no to 1|0
56 for my $key (qw(repeatmerged bug-rev pend-rev sev-rev)) {
57      if (exists $param{$key}){
58           if ($param{$key} =~ /^no$/i) {
59                $param{$key} = 0;
60           }
61           elsif ($param{$key}) {
62                $param{$key} = 1;
63           }
64      }
65 }
66
67 if (lc($param{archive}) eq 'no') {
68      $param{archive} = 0;
69 }
70 elsif (lc($param{archive}) eq 'yes') {
71      $param{archive} = 1;
72 }
73
74
75 my $repeatmerged = ($param{'repeatmerged'} || "yes") eq "yes";
76 my $archive = ($param{'archive'} || "no") eq "yes";
77 my $include = $param{'&include'} || $param{'include'} || "";
78 my $exclude = $param{'&exclude'} || $param{'exclude'} || "";
79
80 my $users = $param{'users'} || "";
81
82 my $ordering = $param{'ordering'};
83 my $raw_sort = ($param{'raw'} || "no") eq "yes";
84 my $old_view = ($param{'oldview'} || "no") eq "yes";
85 my $age_sort = ($param{'age'} || "no") eq "yes";
86 unless (defined $ordering) {
87    $ordering = "normal";
88    $ordering = "oldview" if $old_view;
89    $ordering = "raw" if $raw_sort;
90    $ordering = 'age' if $age_sort;
91 }
92 my ($bug_order) = $ordering =~ /(age(?:rev)?)/;
93 $bug_order = '' if not defined $bug_order;
94
95 my $bug_rev = ($param{'bug-rev'} || "no") eq "yes";
96 my $pend_rev = ($param{'pend-rev'} || "no") eq "yes";
97 my $sev_rev = ($param{'sev-rev'} || "no") eq "yes";
98 my $pend_exc = $param{'&pend-exc'} || $param{'pend-exc'} || "";
99 my $pend_inc = $param{'&pend-inc'} || $param{'pend-inc'} || "";
100 my $sev_exc = $param{'&sev-exc'} || $param{'sev-exc'} || "";
101 my $sev_inc = $param{'&sev-inc'} || $param{'sev-inc'} || "";
102 my $maxdays = ($param{'maxdays'} || -1);
103 my $mindays = ($param{'mindays'} || 0);
104 my $version = $param{'version'} || undef;
105 my $dist = $param{'dist'} || undef;
106 my $arch = $param{'arch'} || undef;
107 my $show_list_header = ($param{'show_list_header'} || $userAgent->{'show_list_header'} || "yes" ) eq "yes";
108 my $show_list_footer = ($param{'show_list_footer'} || $userAgent->{'show_list_footer'} || "yes" ) eq "yes";
109
110 {
111     if (defined $param{'vt'}) {
112         my $vt = $param{'vt'};
113         if ($vt eq "none") { $dist = undef; $arch = undef; $version = undef; }
114         if ($vt eq "bysuite") {
115             $version = undef;
116             $arch = undef if ($arch eq "any");
117         }
118         if ($vt eq "bypkg" || $vt eq "bysrc") { $dist = undef; $arch = undef; }
119     }
120     if (defined $param{'includesubj'}) {
121         my $is = $param{'includesubj'};
122         $include .= "," . join(",", map { "subj:$_" } (split /[\s,]+/, $is));
123     }
124     if (defined $param{'excludesubj'}) {
125         my $es = $param{'excludesubj'};
126         $exclude .= "," . join(",", map { "subj:$_" } (split /[\s,]+/, $es));
127     }
128 }
129
130
131 our %hidden = map { $_, 1 } qw(status severity classification);
132 our %cats = (
133     "status" => [ {
134         "nam" => "Status",
135         "pri" => [map { "pending=$_" }
136             qw(pending forwarded pending-fixed fixed done absent)],
137         "ttl" => ["Outstanding","Forwarded","Pending Upload",
138                   "Fixed in NMU","Resolved","From other Branch"],
139         "def" => "Unknown Pending Status",
140         "ord" => [0,1,2,3,4,5,6],
141     } ],
142     "severity" => [ {
143         "nam" => "Severity",
144         "pri" => [map { "severity=$_" } @gSeverityList],
145         "ttl" => [map { $gSeverityDisplay{$_} } @gSeverityList],
146         "def" => "Unknown Severity",
147         "ord" => [0..@gSeverityList],
148     } ],
149     "classification" => [ {
150         "nam" => "Classification",
151         "pri" => [qw(pending=pending+tag=wontfix 
152                      pending=pending+tag=moreinfo
153                      pending=pending+tag=patch
154                      pending=pending+tag=confirmed
155                      pending=pending)],
156         "ttl" => ["Will Not Fix","More information needed",
157                   "Patch Available","Confirmed"],
158         "def" => "Unclassified",
159         "ord" => [2,3,4,1,0,5],
160     } ],
161     "oldview" => [ qw(status severity) ],
162     "normal" => [ qw(status severity classification) ],
163 );
164
165 my @select_key = (qw(submitter maint pkg package src usertag),
166                   qw(status tag maintenc owner)
167                  );
168
169 if (exists $param{which} and exists $param{data}) {
170      $param{$param{which}} = [exists $param{$param{which}}?(make_list($param{$param{which}})):(),
171                               make_list($param{data}),
172                              ];
173      delete $param{which};
174      delete $param{data};
175 }
176
177 if (defined $param{maintenc}) {
178      $param{maint} = maint_decode($param{maintenc});
179      delete $param{maintenc}
180 }
181
182
183 if (not grep {exists $param{$_}} @select_key and exists $param{users}) {
184      $param{usertag} = [make_list($param{users})];
185 }
186
187 quitcgi("You have to choose something to select by") unless grep {exists $param{$_}} @select_key;
188
189 if (exists $param{pkg}) {
190      $param{package} = $param{pkg};
191      delete $param{pkg};
192 }
193
194 our %bugusertags;
195 our %ut;
196 for my $user (map {split /[\s*,\s*]+/} make_list($param{users}||[])) {
197     next unless length($user);
198     add_user($user);
199 }
200
201 if (defined $param{usertag}) {
202     my %select_ut = ();
203     my ($u, $t) = split /:/, $param{usertag}, 2;
204     Debbugs::User::read_usertags(\%select_ut, $u);
205     unless (defined $t && $t ne "") {
206         $t = join(",", keys(%select_ut));
207     }
208
209     add_user($u);
210     push @{$param{tag}}, split /,/, $t;
211 }
212
213 my $Archived = $archive ? " Archived" : "";
214
215 my $this = "";
216
217 my %indexentry;
218 my %strings = ();
219
220 my $dtime = strftime "%a, %e %b %Y %T UTC", gmtime;
221 my $tail_html = $debbugs::gHTMLTail;
222 $tail_html = $debbugs::gHTMLTail;
223 $tail_html =~ s/SUBSTITUTE_DTIME/$dtime/;
224
225 our %seen_users;
226 sub add_user {
227     my $ut = \%ut;
228     my $u = shift;
229
230     return if $seen_users{$u};
231     $seen_users{$u} = 1;
232
233     my $user = Debbugs::User::get_user($u);
234
235     my %vis = map { $_, 1 } @{$user->{"visible_cats"}};
236     for my $c (keys %{$user->{"categories"}}) {
237         $cats{$c} = $user->{"categories"}->{$c};
238         $hidden{$c} = 1 unless defined $vis{$c};
239     }
240
241     for my $t (keys %{$user->{"tags"}}) {
242         $ut->{$t} = [] unless defined $ut->{$t};
243         push @{$ut->{$t}}, @{$user->{"tags"}->{$t}};
244     }
245
246     %bugusertags = ();
247     for my $t (keys %{$ut}) {
248         for my $b (@{$ut->{$t}}) {
249             $bugusertags{$b} = [] unless defined $bugusertags{$b};
250             push @{$bugusertags{$b}}, $t;
251         }
252     }
253     set_option("bugusertags", \%bugusertags);
254 }
255
256 my @bugs;
257
258 # addusers for source and binary packages being searched for
259 my $pkgsrc = getpkgsrc();
260 my $srcpkg = getsrcpkgs();
261 for my $package (# For binary packages, add the binary package
262                  # and corresponding source package
263                  make_list($param{package}||[]),
264                  (map {defined $pkgsrc->{$_}?($pkgsrc->{$_}):()}
265                   make_list($param{package}||[]),
266                  ),
267                  # For source packages, add the source package
268                  # and corresponding binary packages
269                  make_list($param{src}||[]),
270                  (map {defined $srcpkg->{$_}?($srcpkg->{$_}):()}
271                   make_list($param{src}||[]),
272                  ),
273                 ) {
274      next unless defined $package;
275      add_user($package.'@'.$config{usertag_package_domain})
276           if defined $config{usertag_package_domain};
277 }
278
279
280 # walk through the keys and make the right get_bugs query.
281
282 my @search_key_order = (package   => 'in package',
283                         tag       => 'tagged',
284                         severity  => 'with severity',
285                         src       => 'in source package',
286                         maint     => 'in packages maintained by',
287                         submitter => 'submitted by',
288                         owner     => 'owned by',
289                         status    => 'with status',
290                        );
291 my %search_keys = @search_key_order;
292
293 # Set the title sanely and clean up parameters
294 my @title;
295 use Data::Dumper;
296 print STDERR Dumper(\%param);
297 while (my ($key,$value) = splice @search_key_order, 0, 2) {
298      next unless exists $param{$key};
299      my @entries = ();
300      $param{$key} = [map {split /\s*,\s*/} make_list($param{$key})];
301      for my $entry (make_list($param{$key})) {
302           my $extra = '';
303           if (exists $param{dist} and ($key eq 'package' or $key eq 'src')) {
304                my @versions = get_versions(package => $entry,
305                                            (exists $param{dist}?(dist => $param{dist}):()),
306                                            (exists $param{arch}?(arch => $param{arch}):()),
307                                            ($key eq 'src'?(arch => q(source)):()),
308                                           );
309                my $verdesc = join(', ',@versions);
310                $verdesc = 'version'.(@versions>1?'s ':' ').$verdesc;
311                $extra= " ($verdesc)" if @versions;
312           }
313           push @entries, $entry.$extra;
314      }
315      push @title,$value.' '.join(' or ', @entries);
316 }
317 my $title = join(' and ', map {/ or /?"($_)":$_} @title);
318 @title = ();
319
320 # we have to special case the maint="" search, unfortunatly.
321 if (defined $param{maint} and $param{maint} eq "") {
322      my %maintainers = %{getmaintainers()};
323      @bugs = get_bugs(function =>
324                       sub {my %d=@_;
325                            foreach my $try (splitpackages($d{"pkg"})) {
326                                 return 1 if !getparsedaddrs($maintainers{$try});
327                            }
328                            return 0;
329                       }
330                      );
331      $title = 'in packages with no maintainer';
332 }
333 else {
334      #yeah for magick!
335      @bugs = get_bugs(map {exists $param{$_}?($_,$param{$_}):()}
336                       keys %search_keys, 'archive'
337                      );
338 }
339
340 if (defined $param{version}) {
341      $title .= " at version $version";
342 }
343 elsif (defined $param{dist}) {
344      $title .= " in $dist";
345 }
346
347 $title = htmlsanit($title);
348
349 my @names; my @prior; my @order;
350 determine_ordering();
351
352 # strip out duplicate bugs
353 my %bugs;
354 @bugs{@bugs} = @bugs;
355 @bugs = keys %bugs;
356
357 my $result = pkg_htmlizebugs(\@bugs);
358
359 print "Content-Type: text/html; charset=utf-8\n\n";
360
361 print "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n";
362 print "<HTML><HEAD>\n" . 
363     "<TITLE>$title -- $gProject$Archived $gBug report logs</TITLE>\n" .
364     qq(<link rel="stylesheet" href="$gWebHostBugDir/css/bugs.css" type="text/css">) .
365     "</HEAD>\n" .
366     '<BODY onload="pagemain();">' .
367     "\n";
368 print "<H1>" . "$gProject$Archived $gBug report logs: $gBugs $title" .
369       "</H1>\n";
370
371 my $showresult = 1;
372
373 my $pkg = $param{package} if defined $param{package};
374 my $src = $param{src} if defined $param{src};
375
376 my $pseudodesc = getpseudodesc();
377 if (defined $pseudodesc and defined $pkg and exists $pseudodesc->{$pkg}) {
378      delete $param{dist};
379 }
380
381 # output infomration about the packages
382
383 for my $package (make_list($param{package}||[])) {
384      output_package_info('binary',$package);
385 }
386 for my $package (make_list($param{src}||[])) {
387      output_package_info('source',$package);
388 }
389
390 sub output_package_info{
391     my ($srcorbin,$package) = @_;
392     my $showpkg = htmlsanit($package);
393     my $maintainers = getmaintainers();
394     my $maint = $maintainers->{$package};
395     if (defined $maint) {
396          print '<p>';
397          print htmlmaintlinks(sub { $_[0] == 1 ? "Maintainer for $showpkg is "
398                                          : "Maintainers for $showpkg are "
399                                     },
400                               $maint);
401          print ".</p>\n";
402     } else {
403          print "<p>No maintainer for $showpkg. Please do not report new bugs against this package.</p>\n";
404     }
405     my %pkgsrc = %{getpkgsrc()};
406     my $srcforpkg = $package;
407     if ($srcorbin eq 'binary') {
408          $srcforpkg = $pkgsrc{$package};
409          defined $srcforpkg or $srcforpkg = $package;
410     }
411     my @pkgs = getsrcpkgs($srcforpkg);
412     @pkgs = grep( !/^\Q$package\E$/, @pkgs );
413     if ( @pkgs ) {
414          @pkgs = sort @pkgs;
415          if ($srcorbin eq 'binary') {
416               print "<p>You may want to refer to the following packages that are part of the same source:\n";
417          } else {
418               print "<p>You may want to refer to the following individual bug pages:\n";
419          }
420          #push @pkgs, $src if ( $src && !grep(/^\Q$src\E$/, @pkgs) );
421          print join( ", ", map( "<A href=\"" . pkgurl($_) . "\">$_</A>", @pkgs ) );
422          print ".\n";
423     }
424     my @references;
425     my $pseudodesc = getpseudodesc();
426     if ($package and defined($pseudodesc) and exists($pseudodesc->{$package})) {
427          push @references, "to the <a href=\"http://${debbugs::gWebDomain}/pseudo-packages${debbugs::gHTMLSuffix}\">".
428               "list of other pseudo-packages</a>";
429     } else {
430          if ($package and defined $gPackagePages) {
431               push @references, sprintf "to the <a href=\"%s\">%s package page</a>",
432                    urlsanit("http://${debbugs::gPackagePages}/$package"), htmlsanit("$package");
433          }
434          if (defined $gSubscriptionDomain) {
435               my $ptslink = $package ? $srcforpkg : $src;
436               push @references, "to the <a href=\"http://$gSubscriptionDomain/$ptslink\">Package Tracking System</a>";
437          }
438          # Only output this if the source listing is non-trivial.
439          if ($srcorbin eq 'binary' and $srcforpkg) {
440               push @references, sprintf "to the source package <a href=\"%s\">%s</a>'s bug page", srcurl($srcforpkg), htmlsanit($srcforpkg);
441          }
442     }
443     if (@references) {
444          $references[$#references] = "or $references[$#references]" if @references > 1;
445          print "<p>You might like to refer ", join(", ", @references), ".</p>\n";
446     }
447     if (defined $param{maint} || defined $param{maintenc}) {
448          print "<p>If you find a bug not listed here, please\n";
449          printf "<a href=\"%s\">report it</a>.</p>\n",
450               urlsanit("http://${debbugs::gWebDomain}/Reporting${debbugs::gHTMLSuffix}");
451     }
452     if (not $maint and not @bugs) {
453          print "<p>There is no record of the " .
454               ($srcorbin eq 'binary' ? htmlsanit($package) . " package"
455                : htmlsanit($src) . " source package").
456                     ", and no bugs have been filed against it.</p>";
457          $showresult = 0;
458     }
459 }
460
461 if (exists $param{maint} or exists $param{maintenc}) {
462     print "<p>Note that maintainers may use different Maintainer fields for\n";
463     print "different packages, so there may be other reports filed under\n";
464     print "different addresses.\n";
465 }
466 if (exists $param{submitter}) {
467     print "<p>Note that people may use different email accounts for\n";
468     print "different bugs, so there may be other reports filed under\n";
469     print "different addresses.\n";
470 }
471
472 my $archive_links;
473 my @archive_links;
474 my %archive_values = (both => 'archived and unarchived',
475                       0    => 'not archived',
476                       1    => 'archived',
477                      );
478 while (my ($key,$value) = each %archive_values) {
479      next if $key eq lc($param{archive});
480      push @archive_links, qq(<a href=").
481           urlsanit(pkg_url((
482                        map {
483                             $_ eq 'archive'?():($_,$param{$_})
484                        } keys %param),
485                             archive => $key
486                            )).qq(">$value reports </a>);
487 }
488 print '<p>See the '.join (' or ',@archive_links)."</p>\n";
489
490 print $result if $showresult;
491
492 print pkg_javascript() . "\n";
493 print "<h2 class=\"outstanding\"><a class=\"options\" href=\"javascript:toggle(1)\">Options</a></h2>\n";
494 print "<div id=\"a_1\">\n";
495 printf "<form action=\"%s\" method=POST>\n", myurl();
496
497 print "<table class=\"forms\">\n";
498
499 my ($checked_any, $checked_sui, $checked_ver) = ("", "", "");
500 if (defined $dist) {
501   $checked_sui = "CHECKED";
502 } elsif (defined $version) {
503   $checked_ver = "CHECKED";
504 } else {
505   $checked_any = "CHECKED";
506 }
507
508 print "<tr><td>Show bugs applicable to</td>\n";
509 print "    <td><input id=\"b_1_1\" name=vt value=none type=radio onchange=\"enable(1);\" $checked_any>anything</td></tr>\n";
510 print "<tr><td></td>";
511 print "    <td><input id=\"b_1_2\" name=vt value=bysuite type=radio onchange=\"enable(1);\" $checked_sui>" . pkg_htmlselectsuite(1,2,1) . " for " . pkg_htmlselectarch(1,2,2) . "</td></tr>\n";
512
513 if (defined $pkg) {
514     my $v = htmlsanit($version) || "";
515     my $pkgsane = htmlsanit($pkg);
516     print "<tr><td></td>";
517     print "    <td><input id=\"b_1_3\" name=vt value=bypkg type=radio onchange=\"enable(1);\" $checked_ver>$pkgsane version <input id=\"b_1_3_1\" name=version value=\"$v\"></td></tr>\n";
518 } elsif (defined $src) {
519     my $v = htmlsanit($version) || "";
520     my $srcsane = htmlsanit($src);
521     print "<tr><td></td>";
522     print "    <td><input name=vt value=bysrc type=radio onchange=\"enable(1);\" $checked_ver>$srcsane version <input id=\"b_1_3_1\" name=version value=\"$v\"></td></tr>\n";
523 }
524 print "<tr><td>&nbsp;</td></tr>\n";
525
526 my $includetags = htmlsanit(join(" ", grep { !m/^subj:/i } map {split /[\s,]+/} ref($include)?@{$include}:$include));
527 my $excludetags = htmlsanit(join(" ", grep { !m/^subj:/i } map {split /[\s,]+/} ref($exclude)?@{$exclude}:$exclude));
528 my $includesubj = htmlsanit(join(" ", map { s/^subj://i; $_ } grep { m/^subj:/i } map {split /[\s,]+/} ref($include)?@{$include}:$include));
529 my $excludesubj = htmlsanit(join(" ", map { s/^subj://i; $_ } grep { m/^subj:/i } map {split /[\s,]+/} ref($exclude)?@{$exclude}:$exclude));
530 my $vismindays = ($mindays == 0 ? "" : $mindays);
531 my $vismaxdays = ($maxdays == -1 ? "" : $maxdays);
532
533 my $sel_rmy = ($repeatmerged ? " selected" : "");
534 my $sel_rmn = ($repeatmerged ? "" : " selected");
535 my $sel_ordraw = ($ordering eq "raw" ? " selected" : "");
536 my $sel_ordold = ($ordering eq "oldview" ? " selected" : "");
537 my $sel_ordnor = ($ordering eq "normal" ? " selected" : "");
538 my $sel_ordage = ($ordering eq "age" ? " selected" : "");
539
540 my $chk_bugrev = ($bug_rev ? " checked" : "");
541 my $chk_pendrev = ($pend_rev ? " checked" : "");
542 my $chk_sevrev = ($sev_rev ? " checked" : "");
543
544 print <<EOF;
545 <tr><td>Only include bugs tagged with </td><td><input name=include value="$includetags"> or that have <input name=includesubj value="$includesubj"> in their subject</td></tr>
546 <tr><td>Exclude bugs tagged with </td><td><input name=exclude value="$excludetags"> or that have <input name=excludesubj value="$excludesubj"> in their subject</td></tr>
547 <tr><td>Only show bugs older than</td><td><input name=mindays value="$vismindays" size=5> days, and younger than <input name=maxdays value="$vismaxdays" size=5> days</td></tr>
548
549 <tr><td>&nbsp;</td></tr>
550
551 </td></tr>
552 <tr><td>Merged bugs should be</td><td>
553 <select name=repeatmerged>
554 <option value=yes$sel_rmy>displayed separately</option>
555 <option value=no$sel_rmn>combined</option>
556 </select>
557 <tr><td>Categorise bugs by</td><td>
558 <select name=ordering>
559 <option value=raw$sel_ordraw>bug number only</option>
560 <option value=old$sel_ordold>status and severity</option>
561 <option value=normal$sel_ordnor>status, severity and classification</option>
562 <option value=age$sel_ordage>status, severity, classification, and age</option>
563 EOF
564
565 {
566 my $any = 0;
567 my $o = $param{"ordering"} || "";
568 for my $n (keys %cats) {
569     next if ($n eq "normal" || $n eq "oldview");
570     next if defined $hidden{$n};
571     unless ($any) {
572         $any = 1;
573         print "<option disabled>------</option>\n";
574     }
575     my @names = map { ref($_) eq "HASH" ? $_->{"nam"} : $_ } @{$cats{$n}};
576     my $name;
577     if (@names == 1) { $name = $names[0]; }
578     else { $name = " and " . pop(@names); $name = join(", ", @names) . $name; }
579
580     printf "<option value=\"%s\"%s>%s</option>\n",
581         $n, ($o eq $n ? " selected" : ""), $name;
582 }
583 }
584
585 print "</select></td></tr>\n";
586
587 printf "<tr><td>Order bugs by</td><td>%s</td></tr>\n",
588     pkg_htmlselectyesno("pend-rev", "outstanding bugs first", "done bugs first", $pend_rev);
589 printf "<tr><td></td><td>%s</td></tr>\n",
590     pkg_htmlselectyesno("sev-rev", "highest severity first", "lowest severity first", $sev_rev);
591 printf "<tr><td></td><td>%s</td></tr>\n",
592     pkg_htmlselectyesno("bug-rev", "oldest bugs first", "newest bugs first", $bug_rev);
593
594 print <<EOF;
595 <tr><td>&nbsp;</td></tr>
596 <tr><td colspan=2><input value="Reload page" type="submit"> with new settings</td></tr>
597 EOF
598
599 print "</table></form></div>\n";
600
601 print "<hr>\n";
602 print "<p>$tail_html";
603
604 print "</body></html>\n";
605
606 sub pkg_htmlindexentrystatus {
607     my $s = shift;
608     my %status = %{$s};
609
610     my $result = "";
611
612     my $showseverity;
613     if  ($status{severity} eq 'normal') {
614         $showseverity = '';
615     } elsif (isstrongseverity($status{severity})) {
616         $showseverity = "Severity: <em class=\"severity\">$status{severity}</em>;\n";
617     } else {
618         $showseverity = "Severity: <em>$status{severity}</em>;\n";
619     }
620
621     $result .= pkg_htmlpackagelinks($status{"package"}, 1);
622
623     my $showversions = '';
624     if (@{$status{found_versions}}) {
625         my @found = @{$status{found_versions}};
626         $showversions .= join ', ', map {s{/}{ }; htmlsanit($_)} @found;
627     }
628     if (@{$status{fixed_versions}}) {
629         $showversions .= '; ' if length $showversions;
630         $showversions .= '<strong>fixed</strong>: ';
631         my @fixed = @{$status{fixed_versions}};
632         $showversions .= join ', ', map {s{/}{ }; htmlsanit($_)} @fixed;
633     }
634     $result .= ' (<a href="'.
635          version_url($status{package},
636                      $status{found_versions},
637                      $status{fixed_versions},
638                     ).qq{">$showversions</a>)} if length $showversions;
639     $result .= ";\n";
640
641     $result .= $showseverity;
642     $result .= pkg_htmladdresslinks("Reported by: ", \&submitterurl,
643                                 $status{originator});
644     $result .= ";\nOwned by: " . htmlsanit($status{owner})
645                if length $status{owner};
646     $result .= ";\nTags: <strong>" 
647                  . htmlsanit(join(", ", sort(split(/\s+/, $status{tags}))))
648                  . "</strong>"
649                        if (length($status{tags}));
650
651     $result .= buglinklist(";\nMerged with ", ", ",
652         split(/ /,$status{mergedwith}));
653     $result .= buglinklist(";\nBlocked by ", ", ",
654         split(/ /,$status{blockedby}));
655     $result .= buglinklist(";\nBlocks ", ", ",
656         split(/ /,$status{blocks}));
657
658     if (length($status{done})) {
659         $result .= "<br><strong>Done:</strong> " . htmlsanit($status{done});
660         my $days = bug_archiveable(bug => $status{id},
661                                    status => \%status,
662                                    days_until => 1,
663                                   );
664         if ($days >= 0 and defined $status{location} and $status{location} ne 'archive') {
665             $result .= ";\n<strong>Can be archived" . ( $days == 0 ? " today" : $days == 1 ? " in $days day" : " in $days days" ) . "</strong>";
666         }
667         elsif (defined $status{location} and $status{location} eq 'archived') {
668              $result .= ";\n<strong>Archived.</strong>";
669         }
670     }
671
672     unless (length($status{done})) {
673         if (length($status{forwarded})) {
674             $result .= ";\n<strong>Forwarded</strong> to "
675                        . join(', ',
676                               map {maybelink($_)}
677                               split /[,\s]+/,$status{forwarded}
678                              );
679         }
680         # Check the age of the logfile
681         my ($days_last,$eng_last) = secs_to_english(time - $status{log_modified});
682         my ($days,$eng) = secs_to_english(time - $status{date});
683         
684         if ($days >= 7) {
685             my $font = "";
686             my $efont = "";
687             $font = "em" if ($days > 30);
688             $font = "strong" if ($days > 60);
689             $efont = "</$font>" if ($font);
690             $font = "<$font>" if ($font);
691
692             $result .= ";\n ${font}$eng old$efont";
693         }
694         if ($days_last > 7) {
695             my $font = "";
696             my $efont = "";
697             $font = "em" if ($days_last > 30);
698             $font = "strong" if ($days_last > 60);
699             $efont = "</$font>" if ($font);
700             $font = "<$font>" if ($font);
701
702             $result .= ";\n ${font}Modified $eng_last ago$efont";
703         }
704     }
705
706     $result .= ".";
707
708     return $result;
709 }
710
711
712 sub pkg_htmlizebugs {
713     $b = $_[0];
714     my @bugs = @$b;
715
716     my @status = ();
717     my %count;
718     my $header = '';
719     my $footer = "<h2 class=\"outstanding\">Summary</h2>\n";
720
721     my @dummy = ($gRemoveAge); #, @gSeverityList, @gSeverityDisplay);  #, $gHTMLExpireNote);
722
723     if (@bugs == 0) {
724         return "<HR><H2>No reports found!</H2></HR>\n";
725     }
726
727     if ( $bug_rev ) {
728         @bugs = sort {$b<=>$a} @bugs;
729     } else {
730         @bugs = sort {$a<=>$b} @bugs;
731     }
732     my %seenmerged;
733
734     my %common = (
735         'show_list_header' => 1,
736         'show_list_footer' => 1,
737     );
738
739     my %section = ();
740
741     foreach my $bug (@bugs) {
742         my %status = %{get_bug_status(bug=>$bug,
743                                       (exists $param{dist}?(dist => $param{dist}):()),
744                                      )};
745         next unless %status;
746         next if bugfilter($bug, %status);
747
748         my $html = sprintf "<li><a href=\"%s\">#%d: %s</a>\n<br>",
749             bugurl($bug), $bug, htmlsanit($status{subject});
750         $html .= pkg_htmlindexentrystatus(\%status) . "\n";
751         push @status, [ $bug, \%status, $html ];
752     }
753     if ($bug_order eq 'age') {
754          # MWHAHAHAHA
755          @status = sort {$a->[1]{log_modified} <=> $b->[1]{log_modified}} @status;
756     }
757     elsif ($bug_order eq 'agerev') {
758          @status = sort {$b->[1]{log_modified} <=> $a->[1]{log_modified}} @status;
759     }
760     for my $entry (@status) {
761         my $key = "";
762         for my $i (0..$#prior) {
763             my $v = get_bug_order_index($prior[$i], $entry->[1]);
764             $count{"g_${i}_${v}"}++;
765             $key .= "_$v";
766         }
767         $section{$key} .= $entry->[2];
768         $count{"_$key"}++;
769     }
770
771     my $result = "";
772     if ($ordering eq "raw") {
773         $result .= "<UL class=\"bugs\">\n" . join("", map( { $_->[ 2 ] } @status ) ) . "</UL>\n";
774     } else {
775         $header .= "<ul>\n<div class=\"msgreceived\">\n";
776         my @keys_in_order = ("");
777         for my $o (@order) {
778             push @keys_in_order, "X";
779             while ((my $k = shift @keys_in_order) ne "X") {
780                 for my $k2 (@{$o}) {
781                     $k2+=0;
782                     push @keys_in_order, "${k}_${k2}";
783                 }
784             }
785         }
786         for my $order (@keys_in_order) {
787             next unless defined $section{$order};
788             my @ttl = split /_/, $order; shift @ttl;
789             my $title = $title[0]->[$ttl[0]] . " bugs";
790             if ($#ttl > 0) {
791                 $title .= " -- ";
792                 $title .= join("; ", grep {($_ || "") ne ""}
793                         map { $title[$_]->[$ttl[$_]] } 1..$#ttl);
794             }
795             $title = htmlsanit($title);
796
797             my $count = $count{"_$order"};
798             my $bugs = $count == 1 ? "bug" : "bugs";
799
800             $header .= "<li><a href=\"#$order\">$title</a> ($count $bugs)</li>\n";
801             if ($common{show_list_header}) {
802                 my $count = $count{"_$order"};
803                 my $bugs = $count == 1 ? "bug" : "bugs";
804                 $result .= "<H2 CLASS=\"outstanding\"><a name=\"$order\"></a>$title ($count $bugs)</H2>\n";
805             } else {
806                 $result .= "<H2 CLASS=\"outstanding\">$title</H2>\n";
807             }
808             $result .= "<div class=\"msgreceived\">\n<UL class=\"bugs\">\n";
809             $result .= "\n\n\n\n";
810             $result .= $section{$order};
811             $result .= "\n\n\n\n";
812             $result .= "</UL>\n</div>\n";
813         } 
814         $header .= "</ul></div>\n";
815
816         $footer .= "<ul>\n<div class=\"msgreceived\">";
817         for my $i (0..$#prior) {
818             my $local_result = '';
819             foreach my $key ( @{$order[$i]} ) {
820                 my $count = $count{"g_${i}_$key"};
821                 next if !$count or !$title[$i]->[$key];
822                 $local_result .= "<li>$count $title[$i]->[$key]</li>\n";
823             }
824             if ( $local_result ) {
825                 $footer .= "<li>$names[$i]<ul>\n$local_result</ul></li>\n";
826             }
827         }
828         $footer .= "</div></ul>\n";
829     }
830
831     $result = $header . $result if ( $common{show_list_header} );
832     $result .= $footer if ( $common{show_list_footer} );
833     return $result;
834 }
835
836 sub pkg_htmlpackagelinks {
837     my $pkgs = shift;
838     return unless defined $pkgs and $pkgs ne '';
839     my $strong = shift;
840     my @pkglist = splitpackages($pkgs);
841
842     $strong = 0;
843     my $openstrong  = $strong ? '<strong>' : '';
844     my $closestrong = $strong ? '</strong>' : '';
845
846     return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
847            join(', ',
848                 map {
849                     '<a class="submitter" href="' . pkgurl($_) . '">' .
850                     $openstrong . htmlsanit($_) . $closestrong . '</a>'
851                 } @pkglist
852            );
853 }
854
855 sub pkg_htmladdresslinks {
856      htmlize_addresslinks(@_,'submitter');
857 }
858
859 sub pkg_javascript {
860     return <<EOF ;
861 <script type="text/javascript">
862 <!--
863 function pagemain() {
864         toggle(1);
865 //      toggle(2);
866         enable(1);
867 }
868
869 function setCookie(name, value, expires, path, domain, secure) {
870   var curCookie = name + "=" + escape(value) +
871       ((expires) ? "; expires=" + expires.toGMTString() : "") +
872       ((path) ? "; path=" + path : "") +
873       ((domain) ? "; domain=" + domain : "") +
874       ((secure) ? "; secure" : "");
875   document.cookie = curCookie;
876 }
877
878 function save_cat_cookies() {
879   var cat = document.categories.categorisation.value;
880   var exp = new Date();
881   exp.setTime(exp.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
882   var oldexp = new Date();
883   oldexp.setTime(oldexp.getTime() - 1 * 365 * 24 * 60 * 60 * 1000);
884   var lev;
885   var done = 0;
886
887   var u = document.getElementById("users");
888   if (u != null) { u = u.value; }
889   if (u == "") { u = null; }
890   if (u != null) {
891       setCookie("cat" + cat + "_users", u, exp, "/");
892   } else {
893       setCookie("cat" + cat + "_users", "", oldexp, "/");
894   }
895
896   var bits = new Array("nam", "pri", "ttl", "ord");
897   for (var i = 0; i < 4; i++) {
898       for (var j = 0; j < bits.length; j++) {
899           var e = document.getElementById(bits[j] + i);
900           if (e) e = e.value;
901           if (e == null) { e = ""; }
902           if (j == 0 && e == "") { done = 1; }
903           if (done || e == "") {
904               setCookie("cat" + cat + "_" + bits[j] + i, "", oldexp, "/");
905           } else {
906               setCookie("cat" + cat + "_" + bits[j] + i, e, exp, "/");
907           }
908       }
909   }
910 }
911
912 function toggle(i) {
913         var a = document.getElementById("a_" + i);
914         if (a) {
915              if (a.style.display == "none") {
916                      a.style.display = "";
917              } else {
918                      a.style.display = "none";
919              }
920         }
921 }
922
923 function enable(x) {
924     for (var i = 1; ; i++) {
925         var a = document.getElementById("b_" + x + "_" + i);
926         if (a == null) break;
927         var ischecked = a.checked;
928         for (var j = 1; ; j++) {
929             var b = document.getElementById("b_" + x + "_"+ i + "_" + j);
930             if (b == null) break;
931             if (ischecked) {
932                 b.disabled = false;
933             } else {
934                 b.disabled = true;
935             }
936         }
937     }
938 }
939 -->
940 </script>
941 EOF
942 }
943
944 sub pkg_htmlselectyesno {
945     my ($name, $n, $y, $default) = @_;
946     return sprintf('<select name="%s"><option value=no%s>%s</option><option value=yes%s>%s</option></select>', $name, ($default ? "" : " selected"), $n, ($default ? " selected" : ""), $y);
947 }
948
949 sub pkg_htmlselectsuite {
950     my $id = sprintf "b_%d_%d_%d", $_[0], $_[1], $_[2];
951     my @suites = ("stable", "testing", "unstable", "experimental");
952     my %suiteaka = ("stable", "etch", "testing", "lenny", "unstable", "sid");
953     my $defaultsuite = "unstable";
954
955     my $result = sprintf '<select name=dist id="%s">', $id;
956     for my $s (@suites) {
957         $result .= sprintf '<option value="%s"%s>%s%s</option>',
958                 $s, ($defaultsuite eq $s ? " selected" : ""),
959                 $s, (defined $suiteaka{$s} ? " (" . $suiteaka{$s} . ")" : "");
960     }
961     $result .= '</select>';
962     return $result;
963 }
964
965 sub pkg_htmlselectarch {
966     my $id = sprintf "b_%d_%d_%d", $_[0], $_[1], $_[2];
967     my @arches = qw(alpha amd64 arm hppa i386 ia64 m68k mips mipsel powerpc s390 sparc);
968
969     my $result = sprintf '<select name=arch id="%s">', $id;
970     $result .= '<option value="any">any architecture</option>';
971     for my $a (@arches) {
972         $result .= sprintf '<option value="%s">%s</option>', $a, $a;
973     }
974     $result .= '</select>';
975     return $result;
976 }
977
978 sub myurl {
979      return urlsanit(pkg_url(map {exists $param{$_}?($_,$param{$_}):()}
980                              qw(archive repeatmerged mindays maxdays),
981                              qw(version dist arch pkg src tag maint submitter)
982                             )
983                     );
984 }
985
986 sub make_order_list {
987     my $vfull = shift;
988     my @x = ();
989
990     if ($vfull =~ m/^([^:]+):(.*)$/) {
991         my $v = $1;
992         for my $vv (split /,/, $2) {
993             push @x, "$v=$vv";
994         }
995     } else {
996         for my $v (split /,/, $vfull) {
997             next unless $v =~ m/.=./;
998             push @x, $v;
999         }
1000     }
1001     push @x, "";  # catch all
1002     return @x;
1003 }
1004
1005 sub get_bug_order_index {
1006     my $order = shift;
1007     my $status = shift;
1008     my $pos = -1;
1009
1010     my %tags = ();
1011     %tags = map { $_, 1 } split / /, $status->{"tags"}
1012         if defined $status->{"tags"};
1013
1014     for my $el (@${order}) {
1015         $pos++;
1016         my $match = 1;
1017         for my $item (split /[+]/, $el) {
1018             my ($f, $v) = split /=/, $item, 2;
1019             next unless (defined $f and defined $v);
1020             my $isokay = 0;
1021             $isokay = 1 if (defined $status->{$f} and $v eq $status->{$f});
1022             $isokay = 1 if ($f eq "tag" && defined $tags{$v});
1023             unless ($isokay) {
1024                 $match = 0;
1025                 last;
1026             }
1027         }
1028         if ($match) {
1029             return $pos;
1030             last;
1031         }
1032     }
1033     return $pos + 1;
1034 }
1035
1036 sub buglinklist {
1037     my ($prefix, $infix, @els) = @_;
1038     return '' if not @els;
1039     return $prefix . bug_linklist($infix,'submitter',@els);
1040 }
1041
1042
1043 # sets: my @names; my @prior; my @title; my @order;
1044
1045 sub determine_ordering {
1046     $cats{status}[0]{ord} = [ reverse @{$cats{status}[0]{ord}} ]
1047         if ($pend_rev);
1048     $cats{severity}[0]{ord} = [ reverse @{$cats{severity}[0]{ord}} ]
1049         if ($sev_rev);
1050
1051     my $i;
1052     if (defined $param{"pri0"}) {
1053         my @c = ();
1054         $i = 0;
1055         while (defined $param{"pri$i"}) {
1056             my $h = {};
1057
1058             my $pri = $param{"pri$i"};
1059             if ($pri =~ m/^([^:]*):(.*)$/) {
1060               $h->{"nam"} = $1;  # overridden later if necesary
1061               $h->{"pri"} = [ map { "$1=$_" } (split /,/, $2) ];
1062             } else {
1063               $h->{"pri"} = [ split /,/, $pri ];
1064             }
1065
1066             $h->{"nam"} = $param{"nam$i"}
1067                 if (defined $param{"nam$i"}); 
1068             $h->{"ord"} = [ split /\s*,\s*/, $param{"ord$i"} ]
1069                 if (defined $param{"ord$i"}); 
1070             $h->{"ttl"} = [ split /\s*,\s*/, $param{"ttl$i"} ]
1071                 if (defined $param{"ttl$i"}); 
1072
1073             push @c, $h;
1074             $i++;
1075         }
1076         $cats{"_"} = [@c];
1077         $ordering = "_";
1078     }
1079
1080     $ordering = "normal" unless defined $cats{$ordering};
1081
1082     sub get_ordering {
1083         my @res;
1084         my $cats = shift;
1085         my $o = shift;
1086         for my $c (@{$cats->{$o}}) {
1087             if (ref($c) eq "HASH") {
1088                 push @res, $c;
1089             } else {
1090                 push @res, get_ordering($cats, $c);
1091             }
1092         }
1093         return @res;
1094     }
1095     my @cats = get_ordering(\%cats, $ordering);
1096
1097     sub toenglish {
1098         my $expr = shift;
1099         $expr =~ s/[+]/ and /g;
1100         $expr =~ s/[a-z]+=//g;
1101         return $expr;
1102     }
1103  
1104     $i = 0;
1105     for my $c (@cats) {
1106         $i++;
1107         push @prior, $c->{"pri"};
1108         push @names, ($c->{"nam"} || "Bug attribute #" . $i);
1109         if (defined $c->{"ord"}) {
1110             push @order, $c->{"ord"};
1111         } else {
1112             push @order, [ 0..$#{$prior[-1]} ];
1113         }
1114         my @t = @{ $c->{"ttl"} } if defined $c->{ttl};
1115         if (@t < $#{$prior[-1]}) {
1116              push @t, map { toenglish($prior[-1][$_]) } @t..($#{$prior[-1]});
1117         }
1118         push @t, $c->{"def"} || "";
1119         push @title, [@t];
1120     }
1121 }