]> git.donarmstrong.com Git - debbugs.git/blob - cgi/common.pl
[project @ 2005-07-18 04:13:21 by cjwatson]
[debbugs.git] / cgi / common.pl
1 #!/usr/bin/perl -w
2
3 use DB_File;
4 use Fcntl qw/O_RDONLY/;
5 use Mail::Address;
6 use MLDBM qw/DB_File/;
7 use POSIX qw/ceil/;
8
9 use URI::Escape;
10
11 $config_path = '/etc/debbugs';
12 $lib_path = '/usr/lib/debbugs';
13 require "$lib_path/errorlib";
14
15 use Debbugs::Versions;
16 use Debbugs::MIME qw(decode_rfc1522);
17
18 $MLDBM::RemoveTaint = 1;
19
20 my $common_archive = 0;
21 my $common_repeatmerged = 1;
22 my %common_include = ();
23 my %common_exclude = ();
24 my $common_raw_sort = 0;
25 my $common_bug_reverse = 0;
26
27 my %common_reverse = (
28     'pending' => 0,
29     'severity' => 0,
30 );
31 my %common = (
32     'show_list_header' => 1,
33     'show_list_footer' => 1,
34 );
35
36 sub exact_field_match {
37     my ($field, $values, $status) = @_; 
38     my @values = @$values;
39     my @ret = grep {$_ eq $status->{$field} } @values;
40     $#ret != -1;
41 }
42 sub contains_field_match {
43     my ($field, $values, $status) = @_; 
44     foreach my $data (@$values) {
45         return 1 if (index($status->{$field}, $data) > -1);
46     }
47     return 0;        
48 }
49
50 sub detect_user_agent {
51     my $userAgent = $ENV{HTTP_USER_AGENT};
52     return { 'name' => 'unknown' } unless defined $userAgent;
53     return { 'name' => 'links' } if ( $userAgent =~ m,^ELinks,);
54     return { 'name' => 'lynx' } if ( $userAgent =~ m,^Lynx,);
55     return { 'name' => 'wget' } if ( $userAgent =~ m,^Wget,);
56     return { 'name' => 'gecko' } if ( $userAgent =~ m,^Mozilla.* Gecko/,);
57     return { 'name' => 'ie' } if ( $userAgent =~ m,^.*MSIE.*,);
58     return { 'name' => 'unknown' };
59 }
60
61 my %field_match = (
62     'subject' => \&contains_field_match,
63     'tags' => sub {
64         my ($field, $values, $status) = @_; 
65         my %values = map {$_=>1} @$values;
66         foreach my $t (split /\s+/, $status->{$field}) {
67             return 1 if (defined $values{$t});
68         }
69         return 0;
70     },
71     'severity' => \&exact_field_match,
72     'pending' => \&exact_field_match,
73     'originator' => \%contains_field_match,
74     'forwarded' => \%contains_field_match,
75     'owner' => \%contains_field_match,
76 );
77 my @common_grouping = ( 'severity', 'pending' );
78 my %common_grouping_order = (
79     'pending' => [ qw( pending forwarded pending-fixed fixed done absent ) ],
80     'severity' => \@debbugs::gSeverityList,
81 );
82 my %common_grouping_display = (
83     'pending' => 'Status',
84     'severity' => 'Severity',
85 );
86 my %common_headers = (
87     'pending' => {
88         "pending"       => "outstanding",
89         "pending-fixed" => "pending upload",
90         "fixed"         => "fixed in NMU",
91         "done"          => "resolved",
92         "forwarded"     => "forwarded to upstream software authors",
93         "absent"        => "not applicable to this version",
94     },
95     'severity' => \%debbugs::gSeverityDisplay,
96 );
97
98 my $common_version;
99 my $common_dist;
100 my $common_arch;
101
102 my $debug = 0;
103
104 sub array_option($) {
105     my ($val) = @_;
106     my @vals;
107     @vals = ( $val ) if (ref($val) eq "" && $val );
108     @vals = ( $$val ) if (ref($val) eq "SCALAR" && $$val );
109     @vals = @{$val} if (ref($val) eq "ARRAY" );
110     return @vals;
111 }
112
113 sub filter_include_exclude($\%) {
114     my ($val, $filter_map) = @_;
115     my @vals = array_option($val);
116     my @data = map {
117         if (/^([^:]*):(.*)$/) { if ($1 eq 'subj') { ['subject', $2]; } else { [$1, $2] } } else { ['tags', $_] }
118     } split /[\s,]+/, join ',', @vals;
119     foreach my $data (@data) {
120         &quitcgi("Invalid filter key: '$data->[0]'") if (!exists($field_match{$data->[0]}));
121         push @{$filter_map->{$data->[0]}}, $data->[1];
122     }
123 }
124
125 sub filter_option($$\%) {
126     my ($key, $val, $filter_map) = @_;
127     my @vals = array_option($val);
128     foreach $val (@vals) {
129         push @{$filter_map->{$key}}, $val;
130     }
131 }
132
133 sub set_option {
134     my ($opt, $val) = @_;
135     if ($opt =~ m/^show_list_(foot|head)er$/) { $common{$opt} = $val; }
136     if ($opt eq "archive") { $common_archive = $val; }
137     if ($opt eq "repeatmerged") { $common_repeatmerged = $val; }
138     if ($opt eq "exclude") {
139         filter_include_exclude($val, %common_exclude);
140     }
141     if ($opt eq "include") {
142         filter_include_exclude($val, %common_include);
143     }
144     if ($opt eq "raw") { $common_raw_sort = $val; }
145     if ($opt eq "bug-rev") { $common_bug_reverse = $val; }
146     if ($opt eq "pend-rev") { $common_reverse{pending} = $val; }
147     if ($opt eq "sev-rev") { $common_reverse{severity} = $val; }
148     if ($opt eq "pend-exc") {
149         filter_option('pending', $val, %common_exclude);
150     }
151     if ($opt eq "pend-inc") {
152         filter_option('pending', $val, %common_include);
153     }
154     if ($opt eq "sev-exc") {
155         filter_option('severity', $val, %common_exclude);
156     }
157     if ($opt eq "sev-inc") {
158         filter_option('severity', $val, %common_include);
159     }
160     if ($opt eq "version") { $common_version = $val; }
161     if ($opt eq "dist") { $common_dist = $val; }
162     if ($opt eq "arch") { $common_arch = $val; }
163 }
164
165 sub readparse {
166     my ($in, $key, $val, %ret);
167     if (defined $ENV{"QUERY_STRING"} && $ENV{"QUERY_STRING"} ne "") {
168         $in=$ENV{QUERY_STRING};
169     } elsif(defined $ENV{"REQUEST_METHOD"}
170         && $ENV{"REQUEST_METHOD"} eq "POST")
171     {
172         read(STDIN,$in,$ENV{CONTENT_LENGTH});
173     } else {
174         return;
175     }
176     foreach (split(/[&;]/,$in)) {
177         s/\+/ /g;
178         ($key, $val) = split(/=/,$_,2);
179         $key=~s/%(..)/pack("c",hex($1))/ge;
180         $val=~s/%(..)/pack("c",hex($1))/ge;
181         if ( exists $ret{$key} ) {
182             if ( !exists $ret{"&$key"} ) {
183                 $ret{"&$key"} = [ $ret{$key} ];
184             }
185             push @{$ret{"&$key"}},$val;
186         }
187         $ret{$key}=$val;
188     }
189 $debug = 1 if (defined $ret{"debug"} && $ret{"debug"} eq "aj");
190     return %ret;
191 }
192
193 sub quitcgi {
194     my $msg = shift;
195     print "Content-Type: text/html\n\n";
196     print "<HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>\n";
197     print "An error occurred. Dammit.\n";
198     print "Error was: $msg.\n";
199     print "</BODY></HTML>\n";
200     exit 0;
201 }
202
203 #sub abort {
204 #    my $msg = shift;
205 #    my $Archive = $common_archive ? "archive" : "";
206 #    print header . start_html("Sorry");
207 #    print "Sorry bug #$msg doesn't seem to be in the $Archive database.\n";
208 #    print end_html;
209 #    exit 0;
210 #}
211
212 # Split a package string from the status file into a list of package names.
213 sub splitpackages {
214     my $pkgs = shift;
215     return unless defined $pkgs;
216     return map lc, split /[ \t?,()]+/, $pkgs;
217 }
218
219 my %_parsedaddrs;
220 sub getparsedaddrs {
221     my $addr = shift;
222     return () unless defined $addr;
223     return @{$_parsedaddrs{$addr}} if exists $_parsedaddrs{$addr};
224     @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
225     return @{$_parsedaddrs{$addr}};
226 }
227
228 # Generate a comma-separated list of HTML links to each package given in
229 # $pkgs. $pkgs may be empty, in which case an empty string is returned, or
230 # it may be a comma-separated list of package names.
231 sub htmlpackagelinks {
232     my $pkgs = shift;
233     return unless defined $pkgs and $pkgs ne '';
234     my $strong = shift;
235     my @pkglist = splitpackages($pkgs);
236
237     my $openstrong  = $strong ? '<strong>' : '';
238     my $closestrong = $strong ? '</strong>' : '';
239
240     return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
241            join(', ',
242                 map {
243                     '<a href="' . pkgurl($_) . '">' .
244                     $openstrong . htmlsanit($_) . $closestrong . '</a>'
245                 } @pkglist
246            );
247 }
248
249 # Generate a comma-separated list of HTML links to each address given in
250 # $addresses, which should be a comma-separated list of RFC822 addresses.
251 # $urlfunc should be a reference to a function like mainturl or submitterurl
252 # which returns the URL for each individual address.
253 sub htmladdresslinks {
254     my ($prefixfunc, $urlfunc, $addresses) = @_;
255     if (defined $addresses and $addresses ne '') {
256         my @addrs = getparsedaddrs($addresses);
257         my $prefix = (ref $prefixfunc) ? $prefixfunc->(scalar @addrs)
258                                        : $prefixfunc;
259         return $prefix .
260                join ', ', map { sprintf '<a href="%s">%s</a>',
261                                         $urlfunc->($_->address),
262                                         htmlsanit($_->format) || '(unknown)'
263                               } @addrs;
264     } else {
265         my $prefix = (ref $prefixfunc) ? $prefixfunc->(1) : $prefixfunc;
266         return sprintf '%s<a href="%s">(unknown)</a>', $prefix, $urlfunc->('');
267     }
268 }
269
270 # Generate a comma-separated list of HTML links to each maintainer given in
271 # $maints, which should be a comma-separated list of RFC822 addresses.
272 sub htmlmaintlinks {
273     my ($prefixfunc, $maints) = @_;
274     return htmladdresslinks($prefixfunc, \&mainturl, $maints);
275 }
276
277 sub htmlindexentry {
278     my $ref = shift;
279     my %status = %{getbugstatus($ref)};
280     return htmlindexentrystatus(%status) if (%status);
281     return "";
282 }
283
284 sub htmlindexentrystatus {
285     my $s = shift;
286     my %status = %{$s};
287
288     my $result = "";
289
290     if  ($status{severity} eq 'normal') {
291         $showseverity = '';
292     } elsif (isstrongseverity($status{severity})) {
293         $showseverity = "<strong>Severity: $status{severity}</strong>;\n";
294     } else {
295         $showseverity = "Severity: <em>$status{severity}</em>;\n";
296     }
297
298     $result .= htmlpackagelinks($status{"package"}, 1);
299
300     my $showversions = '';
301     if (@{$status{found_versions}}) {
302         my @found = @{$status{found_versions}};
303         local $_;
304         s{/}{ } foreach @found;
305         $showversions .= join ', ', map htmlsanit($_), @found;
306     }
307     if (@{$status{fixed_versions}}) {
308         $showversions .= '; ' if length $showversions;
309         $showversions .= '<strong>fixed</strong>: ';
310         my @fixed = @{$status{fixed_versions}};
311         local $_;
312         s{/}{ } foreach @fixed;
313         $showversions .= join ', ', map htmlsanit($_), @fixed;
314     }
315     $result .= " ($showversions)" if length $showversions;
316     $result .= ";\n";
317
318     $result .= $showseverity;
319     $result .= htmladdresslinks("Reported by: ", \&submitterurl,
320                                 $status{originator});
321     $result .= ";\nOwned by: " . htmlsanit($status{owner})
322                if length $status{owner};
323     $result .= ";\nTags: <strong>" 
324                  . htmlsanit(join(", ", sort(split(/\s+/, $status{tags}))))
325                  . "</strong>"
326                        if (length($status{tags}));
327
328     my @merged= split(/ /,$status{mergedwith});
329     my $mseparator= ";\nmerged with ";
330     for my $m (@merged) {
331         $result .= $mseparator."<A href=\"" . bugurl($m) . "\">#$m</A>";
332         $mseparator= ", ";
333     }
334
335     if (length($status{done})) {
336         $result .= ";\n<strong>Done:</strong> " . htmlsanit($status{done});
337         $days = ceil($debbugs::gRemoveAge - -M buglog($status{id}));
338         if ($days >= 0) {
339             $result .= ";\n<strong>Will be archived:</strong>" . ( $days == 0 ? " today" : $days == 1 ? " in $days day" : " in $days days" );
340         } else {
341             $result .= ";\n<strong>Archived</strong>";
342         }
343     }
344
345     unless (length($status{done})) {
346         if (length($status{forwarded})) {
347             $result .= ";\n<strong>Forwarded</strong> to "
348                        . maybelink($status{forwarded});
349         }
350         my $daysold = int((time - $status{date}) / 86400);   # seconds to days
351         if ($daysold >= 7) {
352             my $font = "";
353             my $efont = "";
354             $font = "em" if ($daysold > 30);
355             $font = "strong" if ($daysold > 60);
356             $efont = "</$font>" if ($font);
357             $font = "<$font>" if ($font);
358
359             my $yearsold = int($daysold / 365);
360             $daysold -= $yearsold * 365;
361
362             $result .= ";\n $font";
363             my @age;
364             push @age, "1 year" if ($yearsold == 1);
365             push @age, "$yearsold years" if ($yearsold > 1);
366             push @age, "1 day" if ($daysold == 1);
367             push @age, "$daysold days" if ($daysold > 1);
368             $result .= join(" and ", @age);
369             $result .= " old$efont";
370         }
371     }
372
373     $result .= ".";
374
375     return $result;
376 }
377
378 sub urlargs {
379     my $args = '';
380     $args .= "&archive=yes" if $common_archive;
381     $args .= "&repeatmerged=no" unless $common_repeatmerged;
382     $args .= "&version=$common_version" if defined $common_version;
383     $args .= "&dist=$common_dist" if defined $common_dist;
384     $args .= "&arch=$common_arch" if defined $common_arch;
385     return $args;
386 }
387
388 sub submitterurl {
389     my $ref = shift || "";
390     my $params = "submitter=" . emailfromrfc822($ref);
391     $params .= urlargs();
392     return urlsanit("pkgreport.cgi" . "?" . $params);
393 }
394
395 sub mainturl {
396     my $ref = shift || "";
397     my $params = "maint=" . emailfromrfc822($ref);
398     $params .= urlargs();
399     return urlsanit("pkgreport.cgi" . "?" . $params);
400 }
401
402 sub pkgurl {
403     my $ref = shift;
404     my $params = "pkg=$ref";
405     $params .= urlargs();
406     return urlsanit("pkgreport.cgi" . "?" . "$params");
407 }
408
409 sub srcurl {
410     my $ref = shift;
411     my $params = "src=$ref";
412     $params .= urlargs();
413     return urlsanit("pkgreport.cgi" . "?" . "$params");
414 }
415
416 sub tagurl {
417     my $ref = shift;
418     my $params = "tag=$ref";
419     $params .= urlargs();
420     return urlsanit("pkgreport.cgi" . "?" . "$params");
421 }
422
423 sub urlsanit {
424     my $url = shift;
425     $url =~ s/%/%25/g;
426     $url =~ s/#/%23/g;
427     $url =~ s/\+/%2b/g;
428     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
429     $url =~ s/([<>&"])/\&$saniarray{$1};/g;
430     return $url;
431 }
432
433 sub htmlsanit {
434     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
435     my $in = shift || "";
436     $in =~ s/([<>&"])/\&$saniarray{$1};/g;
437     return $in;
438 }
439
440 sub maybelink {
441     my $in = shift;
442     if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
443         return qq{<a href="$in">} . htmlsanit($in) . '</a>';
444     } else {
445         return htmlsanit($in);
446     }
447 }
448
449 sub bugurl {
450     my $ref = shift;
451     my $params = "bug=$ref";
452     foreach my $val (@_) {
453         $params .= "\&msg=$1" if ($val =~ /^msg=([0-9]+)/);
454         $params .= "\&archive=yes" if (!$common_archive && $val =~ /^archive.*$/);
455     }
456     $params .= "&archive=yes" if ($common_archive);
457     $params .= "&repeatmerged=no" unless ($common_repeatmerged);
458
459     return urlsanit("bugreport.cgi" . "?" . "$params");
460 }
461
462 sub dlurl {
463     my $ref = shift;
464     my $params = "bug=$ref";
465     my $filename = '';
466     foreach my $val (@_) {
467         $params .= "\&$1=$2" if ($val =~ /^(msg|att)=([0-9]+)/);
468         $filename = $1 if ($val =~ /^filename=(.*)$/);
469     }
470     $params .= "&archive=yes" if ($common_archive);
471     my $pathinfo = '';
472     $pathinfo = '/'.uri_escape($filename) if $filename ne '';
473
474     return urlsanit("bugreport.cgi$pathinfo?$params");
475 }
476
477 sub mboxurl {
478     my $ref = shift;
479     return urlsanit("bugreport.cgi" . "?" . "bug=$ref&mbox=yes");
480 }
481
482 sub allbugs {
483     return @{getbugs(sub { 1 })};
484 }
485
486 sub bugmatches(\%\%) {
487     my ($hash, $status) = @_;
488     foreach my $key( keys( %$hash ) ) {
489         my $value = $hash->{$key};
490         my $sub = $field_match{$key};
491         return 1 if ($sub->($key, $value, $status));
492     }
493     return 0;
494 }
495 sub bugfilter($%) {
496     my ($bug, %status) = @_;
497     our (%seenmerged);
498     if (%common_include) {
499         return 1 if (!bugmatches(%common_include, %status));
500     }
501     if (%common_exclude) {
502         return 1 if (bugmatches(%common_exclude, %status));
503     }
504     my @merged = sort {$a<=>$b} $bug, split(/ /, $status{mergedwith});
505     return 1 unless ($common_repeatmerged || !$seenmerged{$merged[0]});
506     $seenmerged{$merged[0]} = 1;
507     return 0;
508 }
509
510 sub htmlizebugs {
511     $b = $_[0];
512     my @bugs = @$b;
513     my $anydone = 0;
514
515     my @status = ();
516     my %count;
517     my $header = '';
518     my $footer = '';
519
520     if (@bugs == 0) {
521         return "<HR><H2>No reports found!</H2></HR>\n";
522     }
523
524     if ( $common_bug_reverse ) {
525         @bugs = sort {$b<=>$a} @bugs;
526     } else {
527         @bugs = sort {$a<=>$b} @bugs;
528     }
529     my %seenmerged;
530     foreach my $bug (@bugs) {
531         my %status = %{getbugstatus($bug)};
532         next unless %status;
533         next if bugfilter($bug, %status);
534
535         my $html = sprintf "<li><a href=\"%s\">#%d: %s</a>\n<br>",
536             bugurl($bug), $bug, htmlsanit($status{subject});
537         $html .= htmlindexentrystatus(\%status) . "\n";
538         my $key = join( '_', map( {$status{$_}} @common_grouping ) );
539         $section{$key} .= $html;
540         $count{"_$key"}++;
541         foreach my $grouping ( @common_grouping ) {
542             $count{"${grouping}_$status{$grouping}"}++;
543         }
544         $anydone = 1 if $status{pending} eq 'done';
545         push @status, [ $bug, \%status, $html ];
546     }
547
548     my $result = "";
549     if ($common_raw_sort) {
550         $result .= "<UL>\n" . join("", map( { $_->[ 2 ] } @status ) ) . "</UL>\n";
551     } else {
552         my (@order, @headers);
553         for( my $i = 0; $i < @common_grouping; $i++ ) {
554             my $grouping_name = $common_grouping[ $i ];
555             my @items = @{ $common_grouping_order{ $grouping_name } };
556             @items = reverse( @items ) if ( $common_reverse{ $grouping_name } );
557             my @neworder = ();
558             my @newheaders = ();
559             if ( @order ) {
560                 foreach my $grouping ( @items ) {
561                     push @neworder, map( { "${_}_$grouping" } @order );
562                     push @newheaders, map( { "$_ - $common_headers{$grouping_name}{$grouping}" } @headers );
563                 }
564                 @order = @neworder;
565                 @headers = @newheaders;
566             } else {
567                 push @order, @items;
568                 push @headers, map( { $common_headers{$common_grouping[$i]}{$_} } @items );
569             }
570         }
571         $header .= "<ul>\n";
572         for ( my $i = 0; $i < @order; $i++ ) {
573             my $order = $order[ $i ];
574             next unless defined $section{$order};
575             my $count = $count{"_$order"};
576             my $bugs = $count == 1 ? "bug" : "bugs";
577             $header .= "<li><a href=\"#$order\">$headers[$i]</a> ($count $bugs)</li>\n";
578         }
579         $header .= "</ul>\n";
580         for ( my $i = 0; $i < @order; $i++ ) {
581             my $order = $order[ $i ];
582             next unless defined $section{$order};
583             if ($common{show_list_header}) {
584                 my $count = $count{"_$order"};
585                 my $bugs = $count == 1 ? "bug" : "bugs";
586                 $result .= "<HR><H2><a name=\"$order\"></a>$headers[$i] ($count $bugs)</H2>\n";
587             } else {
588                 $result .= "<HR><H2>$headers[$i]</H2>\n";
589             }
590             $result .= "<UL>\n";
591             $result .= $section{$order};
592             $result .= "</UL>\n";
593         }    
594         $footer .= "<ul>\n";
595         foreach my $grouping ( @common_grouping ) {
596             my $local_result = '';
597             foreach my $key ( @{$common_grouping_order{ $grouping }} ) {
598                 my $count = $count{"${grouping}_$key"};
599                 next if !$count;
600                 $local_result .= "<li>$count $common_headers{$grouping}{$key}</li>\n";
601             }
602             if ( $local_result ) {
603                 $footer .= "<li>$common_grouping_display{$grouping}<ul>\n$local_result</ul></li>\n";
604             }
605         }
606         $footer .= "</ul>\n";
607     }
608
609     $result = $header . $result if ( $common{show_list_header} );
610     $result .= $debbugs::gHTMLExpireNote if $gRemoveAge and $anydone;
611     $result .= "<hr>" . $footer if ( $common{show_list_footer} );
612     return $result;
613 }
614
615 sub countbugs {
616     my $bugfunc = shift;
617     if ($common_archive) {
618         open I, "<$debbugs::gSpoolDir/index.archive"
619             or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
620     } else {
621         open I, "<$debbugs::gSpoolDir/index.db"
622             or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
623     }
624
625     my %count = ();
626     while(<I>) 
627     {
628         if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
629             my @x = $bugfunc->(pkg => $1, bug => $2, status => $4, 
630                                submitter => $5, severity => $6, tags => $7);
631             local $_;
632             $count{$_}++ foreach @x;
633         }
634     }
635     close I;
636     return %count;
637 }
638
639 sub getbugs {
640     my $bugfunc = shift;
641     my $opt = shift;
642
643     my @result = ();
644
645     if (!$common_archive && defined $opt && 
646         -e "$debbugs::gSpoolDir/by-$opt.idx") 
647     {
648         my %lookup;
649 print STDERR "optimized\n" if ($debug);
650         tie %lookup, DB_File => "$debbugs::gSpoolDir/by-$opt.idx", O_RDONLY
651             or die "$0: can't open $debbugs::gSpoolDir/by-$opt.idx ($!)\n";
652         while ($key = shift) {
653             my $bugs = $lookup{$key};
654             if (defined $bugs) {
655                 push @result, (unpack 'N*', $bugs);
656             }
657         }
658         untie %lookup;
659 print STDERR "done optimized\n" if ($debug);
660     } else {
661         if ( $common_archive ) {
662             open I, "<$debbugs::gSpoolDir/index.archive" 
663                 or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
664         } else {
665             open I, "<$debbugs::gSpoolDir/index.db" 
666                 or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
667         }
668         while(<I>) {
669             if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
670                 if ($bugfunc->(pkg => $1, bug => $2, status => $4,
671                             submitter => $5, severity => $6, tags => $7)) 
672                 {
673                     push (@result, $2);
674                 }
675             }
676         }
677         close I;
678     }
679     @result = sort {$a <=> $b} @result;
680     return \@result;
681 }
682
683 sub emailfromrfc822 {
684     my $email = shift;
685     $email =~ s/\s*\(.*\)\s*//;
686     $email = $1 if ($email =~ m/<(.*)>/);
687     return $email;
688 }
689
690 sub maintencoded {
691     my $input = shift;
692     my $encoded = '';
693
694     while ($input =~ m/\W/) {
695         $encoded.=$`.sprintf("-%02x_",unpack("C",$&));
696         $input= $';
697     }
698
699     $encoded.= $input;
700     $encoded =~ s/-2e_/\./g;
701     $encoded =~ s/^([^,]+)-20_-3c_(.*)-40_(.*)-3e_/$1,$2,$3,/;
702     $encoded =~ s/^(.*)-40_(.*)-20_-28_([^,]+)-29_$/,$1,$2,$3/;
703     $encoded =~ s/-20_/_/g;
704     $encoded =~ s/-([^_]+)_-/-$1/g;
705     return $encoded;
706 }
707
708 my $_maintainer;
709 sub getmaintainers {
710     return $_maintainer if $_maintainer;
711     my %maintainer;
712
713     open(MM,"$gMaintainerFile") or &quitcgi("open $gMaintainerFile: $!");
714     while(<MM>) {
715         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
716         ($a,$b)=($1,$2);
717         $a =~ y/A-Z/a-z/;
718         $maintainer{$a}= $b;
719     }
720     close(MM);
721     if (defined $gMaintainerFileOverride) {
722         open(MM,"$gMaintainerFileOverride") or &quitcgi("open $gMaintainerFileOverride: $!");
723         while(<MM>) {
724             next unless m/^(\S+)\s+(\S.*\S)\s*$/;
725             ($a,$b)=($1,$2);
726             $a =~ y/A-Z/a-z/;
727             $maintainer{$a}= $b;
728         }
729         close(MM);
730     }
731     $_maintainer = \%maintainer;
732     return $_maintainer;
733 }
734
735 my $_pkgsrc;
736 my $_pkgcomponent;
737 sub getpkgsrc {
738     return $_pkgsrc if $_pkgsrc;
739     return {} unless defined $gPackageSource;
740     my %pkgsrc;
741     my %pkgcomponent;
742
743     open(MM,"$gPackageSource") or &quitcgi("open $gPackageSource: $!");
744     while(<MM>) {
745         next unless m/^(\S+)\s+(\S+)\s+(\S.*\S)\s*$/;
746         ($a,$b,$c)=($1,$2,$3);
747         $a =~ y/A-Z/a-z/;
748         $pkgsrc{$a}= $c;
749         $pkgcomponent{$a}= $b;
750     }
751     close(MM);
752     $_pkgsrc = \%pkgsrc;
753     $_pkgcomponent = \%pkgcomponent;
754     return $_pkgsrc;
755 }
756
757 sub getpkgcomponent {
758     return $_pkgcomponent if $_pkgcomponent;
759     getpkgsrc();
760     return $_pkgcomponent;
761 }
762
763 my $_pseudodesc;
764 sub getpseudodesc {
765     return $_pseudodesc if $_pseudodesc;
766     my %pseudodesc;
767
768     open(PSEUDO, "< $gPseudoDescFile") or &quitcgi("open $gPseudoDescFile: $!");
769     while(<PSEUDO>) {
770         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
771         $pseudodesc{lc $1} = $2;
772     }
773     close(PSEUDO);
774     $_pseudodesc = \%pseudodesc;
775     return $_pseudodesc;
776 }
777
778 sub getbugstatus {
779     my $bugnum = shift;
780
781     my %status;
782
783     my $location = getbuglocation( $bugnum, 'summary' );
784     return {} if ( !$location );
785     %status = %{ readbug( $bugnum, $location ) };
786     $status{ id } = $bugnum;
787
788     $status{tags} = $status{keywords};
789     my %tags = map { $_ => 1 } split ' ', $status{tags};
790
791     $status{"package"} =~ s/\s*$//;
792     $status{"package"} = 'unknown' if ($status{"package"} eq '');
793     $status{"severity"} = 'normal' if ($status{"severity"} eq '');
794
795     $status{"pending"} = 'pending';
796     $status{"pending"} = 'forwarded'        if (length($status{"forwarded"}));
797     $status{"pending"} = 'pending-fixed'    if ($tags{pending});
798     $status{"pending"} = 'fixed'            if ($tags{fixed});
799
800     my @versions;
801     if (defined $common_version) {
802         @versions = ($common_version);
803     } elsif (defined $common_dist) {
804         @versions = getversions($status{package}, $common_dist, $common_arch);
805     }
806
807     # TODO: This should probably be handled further out for efficiency and
808     # for more ease of distinguishing between pkg= and src= queries.
809     my @sourceversions = makesourceversions($status{package}, $common_arch,
810                                             @versions);
811
812     if (@sourceversions) {
813         # Resolve bugginess states (we might be looking at multiple
814         # architectures, say). Found wins, then fixed, then absent.
815         my $maxbuggy = 'absent';
816         for my $version (@sourceversions) {
817             my $buggy = buggyversion($bugnum, $version, \%status);
818             if ($buggy eq 'found') {
819                 $maxbuggy = 'found';
820                 last;
821             } elsif ($buggy eq 'fixed' and $maxbuggy ne 'found') {
822                 $maxbuggy = 'fixed';
823             }
824         }
825         if ($maxbuggy eq 'absent') {
826             $status{"pending"} = 'absent';
827         } elsif ($maxbuggy eq 'fixed') {
828             $status{"pending"} = 'done';
829         }
830     }
831     
832     if (length($status{done}) and
833             (not @sourceversions or not @{$status{fixed_versions}})) {
834         $status{"pending"} = 'done';
835     }
836
837     return \%status;
838 }
839
840 sub getsrcpkgs {
841     my $src = shift;
842     return () if !$src;
843     my %pkgsrc = %{getpkgsrc()};
844     my @pkgs;
845     foreach ( keys %pkgsrc ) {
846         push @pkgs, $_ if $pkgsrc{$_} eq $src;
847     }
848     return @pkgs;
849 }
850    
851 sub buglog {
852     my $bugnum = shift;
853     my $location = getbuglocation($bugnum, 'log');
854     return getbugcomponent($bugnum, 'log', $location) if ($location);
855     $location = getbuglocation($bugnum, 'log.gz');
856     return getbugcomponent($bugnum, 'log.gz', $location);
857 }
858
859 # Canonicalize versions into source versions, which have an explicitly
860 # named source package. This is used to cope with source packages whose
861 # names have changed during their history, and with cases where source
862 # version numbers differ from binary version numbers.
863 sub makesourceversions {
864     my $pkg = shift;
865     my $arch = shift;
866     my %sourceversions;
867
868     for my $version (@_) {
869         if ($version =~ m[/]) {
870             # Already a source version.
871             $sourceversions{$version} = 1;
872         } else {
873             my @srcinfo = binarytosource($pkg, $version, $arch);
874             next unless @srcinfo;
875             $sourceversions{"$_->[0]/$_->[1]"} = 1 foreach @srcinfo;
876         }
877     }
878
879     return sort keys %sourceversions;
880 }
881
882 my %_versionobj;
883 sub buggyversion {
884     my ($bug, $ver, $status) = @_;
885     return '' unless defined $gVersionPackagesDir;
886     my $src = getpkgsrc()->{$status->{package}};
887     $src = $status->{package} unless defined $src;
888
889     my $tree;
890     if (exists $_versionobj{$src}) {
891         $tree = $_versionobj{$src};
892     } else {
893         $tree = Debbugs::Versions->new(\&DpkgVer::vercmp);
894         my $srchash = substr $src, 0, 1;
895         if (open VERFILE, "< $gVersionPackagesDir/$srchash/$src") {
896             $tree->load(\*VERFILE);
897             close VERFILE;
898         }
899         $_versionobj{$src} = $tree;
900     }
901
902     my @found = makesourceversions($status->{package}, undef,
903                                    @{$status->{found_versions}});
904     my @fixed = makesourceversions($status->{package}, undef,
905                                    @{$status->{fixed_versions}});
906
907     return $tree->buggy($ver, \@found, \@fixed);
908 }
909
910 my %_versions;
911 sub getversions {
912     my ($pkg, $dist, $arch) = @_;
913     return () unless defined $gVersionIndex;
914     $dist = 'unstable' unless defined $dist;
915
916     unless (tied %_versions) {
917         tie %_versions, 'MLDBM', $gVersionIndex, O_RDONLY
918             or die "can't open versions index: $!";
919     }
920
921     if (defined $arch and exists $_versions{$pkg}{$dist}{$arch}) {
922         my $ver = $_versions{$pkg}{$dist}{$arch};
923         return $ver if defined $ver;
924         return ();
925     } else {
926         my %uniq;
927         for my $ar (keys %{$_versions{$pkg}{$dist}}) {
928             $uniq{$_versions{$pkg}{$dist}{$ar}} = 1 unless $ar eq 'source';
929         }
930         return keys %uniq;
931     }
932 }
933
934 sub getversiondesc {
935     my $pkg = shift;
936
937     if (defined $common_version) {
938         return "version $common_version";
939     } elsif (defined $common_dist) {
940         my @distvers = getversions($pkg, $common_dist, $common_arch);
941         @distvers = sort @distvers;
942         local $" = ', ';
943         if (@distvers > 1) {
944             return "versions @distvers";
945         } elsif (@distvers == 1) {
946             return "version @distvers";
947         }
948     }
949
950     return undef;
951 }
952
953 # Returns an array of zero or more references to (srcname, srcver) pairs.
954 # If $binarch is undef, returns results for all architectures.
955 my %_binarytosource;
956 sub binarytosource {
957     my ($binname, $binver, $binarch) = @_;
958
959     # TODO: This gets hit a lot, especially from buggyversion() - probably
960     # need an extra cache for speed here.
961
962     if (tied %_binarytosource or
963             tie %_binarytosource, 'MLDBM', $gBinarySourceMap, O_RDONLY) {
964         # avoid autovivification
965         if (exists $_binarytosource{$binname} and
966                 exists $_binarytosource{$binname}{$binver}) {
967             if (defined $binarch) {
968                 my $src = $_binarytosource{$binname}{$binver}{$binarch};
969                 return () unless defined $src; # not on this arch
970                 # Copy the data to avoid tiedness problems.
971                 return [@$src];
972             } else {
973                 # Get (srcname, srcver) pairs for all architectures and
974                 # remove any duplicates. This involves some slightly tricky
975                 # multidimensional hashing; sorry. Fortunately there'll
976                 # usually only be one pair returned.
977                 my %uniq;
978                 for my $ar (keys %{$_binarytosource{$binname}{$binver}}) {
979                     my $src = $_binarytosource{$binname}{$binver}{$ar};
980                     next unless defined $src;
981                     $uniq{$src->[0]}{$src->[1]} = 1;
982                 }
983                 my @uniq;
984                 for my $sn (sort keys %uniq) {
985                     push @uniq, [$sn, $_] for sort keys %{$uniq{$sn}};
986                 }
987                 return @uniq;
988             }
989         }
990     }
991
992     # No $gBinarySourceMap, or it didn't have an entry for this name and
993     # version. Try $gPackageSource (unversioned) instead.
994     my $pkgsrc = getpkgsrc();
995     if (exists $pkgsrc->{$binname}) {
996         return [$pkgsrc->{$binname}, $binver];
997     } else {
998         return ();
999     }
1000 }
1001
1002 # Returns an array of zero or more references to
1003 # (binname, binver[, binarch]) triplets.
1004 my %_sourcetobinary;
1005 sub sourcetobinary {
1006     my ($srcname, $srcver) = @_;
1007
1008     if (tied %_sourcetobinary or
1009             tie %_sourcetobinary, 'MLDBM', $gSourceBinaryMap, O_RDONLY) {
1010         # avoid autovivification
1011         if (exists $_sourcetobinary{$srcname} and
1012                 exists $_sourcetobinary{$srcname}{$srcver}) {
1013             my $bin = $_sourcetobinary{$srcname}{$srcver};
1014             return () unless defined $bin;
1015             # Copy the data to avoid tiedness problems.
1016             return @$bin;
1017         }
1018     }
1019
1020     # No $gSourceBinaryMap, or it didn't have an entry for this name and
1021     # version. Try $gPackageSource (unversioned) instead.
1022     my @srcpkgs = getsrcpkgs($srcname);
1023     return map [$_, $srcver], @srcpkgs;
1024 }
1025
1026 1;