]> git.donarmstrong.com Git - debbugs.git/blob - cgi/common.pl
[project @ 2005-07-16 18:49:31 by don]
[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            ) . ";\n";
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     $result .= $showseverity;
300     $result .= htmladdresslinks("Reported by: ", \&submitterurl,
301                                 $status{originator});
302     $result .= ";\nOwned by: " . htmlsanit($status{owner})
303                if length $status{owner};
304     $result .= ";\nTags: <strong>" 
305                  . htmlsanit(join(", ", sort(split(/\s+/, $status{tags}))))
306                  . "</strong>"
307                        if (length($status{tags}));
308
309     my @merged= split(/ /,$status{mergedwith});
310     my $mseparator= ";\nmerged with ";
311     for my $m (@merged) {
312         $result .= $mseparator."<A href=\"" . bugurl($m) . "\">#$m</A>";
313         $mseparator= ", ";
314     }
315
316     if (@{$status{found_versions}}) {
317         $result .= ";\nfound in ";
318         $result .= (@{$status{found_versions}} == 1) ? 'version '
319                                                      : 'versions ';
320         $result .= join ', ', map htmlsanit($_), @{$status{found_versions}};
321     }
322
323     if (@{$status{fixed_versions}}) {
324         $result .= ";\n<strong>fixed</strong> in ";
325         $result .= (@{$status{fixed_versions}} == 1) ? 'version '
326                                                      : 'versions ';
327         $result .= join ', ', map htmlsanit($_), @{$status{fixed_versions}};
328         if (length($status{done})) {
329             $result .= ' by ' . htmlsanit($status{done});
330         }
331     } elsif (length($status{done})) {
332         $result .= ";\n<strong>Done:</strong> " . htmlsanit($status{done});
333         $days = ceil($debbugs::gRemoveAge - -M buglog($status{id}));
334         if ($days >= 0) {
335             $result .= ";\n<strong>Will be archived:</strong>" . ( $days == 0 ? " today" : $days == 1 ? " in $days day" : " in $days days" );
336         } else {
337             $result .= ";\n<strong>Archived</strong>";
338         }
339     }
340
341     unless (length($status{done})) {
342         if (length($status{forwarded})) {
343             $result .= ";\n<strong>Forwarded</strong> to "
344                        . maybelink($status{forwarded});
345         }
346         my $daysold = int((time - $status{date}) / 86400);   # seconds to days
347         if ($daysold >= 7) {
348             my $font = "";
349             my $efont = "";
350             $font = "em" if ($daysold > 30);
351             $font = "strong" if ($daysold > 60);
352             $efont = "</$font>" if ($font);
353             $font = "<$font>" if ($font);
354
355             my $yearsold = int($daysold / 365);
356             $daysold -= $yearsold * 365;
357
358             $result .= ";\n $font";
359             my @age;
360             push @age, "1 year" if ($yearsold == 1);
361             push @age, "$yearsold years" if ($yearsold > 1);
362             push @age, "1 day" if ($daysold == 1);
363             push @age, "$daysold days" if ($daysold > 1);
364             $result .= join(" and ", @age);
365             $result .= " old$efont";
366         }
367     }
368
369     $result .= ".";
370
371     return $result;
372 }
373
374 sub urlargs {
375     my $args = '';
376     $args .= "&archive=yes" if $common_archive;
377     $args .= "&repeatmerged=no" unless $common_repeatmerged;
378     $args .= "&version=$common_version" if defined $common_version;
379     $args .= "&dist=$common_dist" if defined $common_dist;
380     $args .= "&arch=$common_arch" if defined $common_arch;
381     return $args;
382 }
383
384 sub submitterurl {
385     my $ref = shift || "";
386     my $params = "submitter=" . emailfromrfc822($ref);
387     $params .= urlargs();
388     return urlsanit("pkgreport.cgi" . "?" . $params);
389 }
390
391 sub mainturl {
392     my $ref = shift || "";
393     my $params = "maint=" . emailfromrfc822($ref);
394     $params .= urlargs();
395     return urlsanit("pkgreport.cgi" . "?" . $params);
396 }
397
398 sub pkgurl {
399     my $ref = shift;
400     my $params = "pkg=$ref";
401     $params .= urlargs();
402     return urlsanit("pkgreport.cgi" . "?" . "$params");
403 }
404
405 sub srcurl {
406     my $ref = shift;
407     my $params = "src=$ref";
408     $params .= urlargs();
409     return urlsanit("pkgreport.cgi" . "?" . "$params");
410 }
411
412 sub tagurl {
413     my $ref = shift;
414     my $params = "tag=$ref";
415     $params .= urlargs();
416     return urlsanit("pkgreport.cgi" . "?" . "$params");
417 }
418
419 sub urlsanit {
420     my $url = shift;
421     $url =~ s/%/%25/g;
422     $url =~ s/#/%23/g;
423     $url =~ s/\+/%2b/g;
424     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
425     $url =~ s/([<>&"])/\&$saniarray{$1};/g;
426     return $url;
427 }
428
429 sub htmlsanit {
430     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
431     my $in = shift || "";
432     $in =~ s/([<>&"])/\&$saniarray{$1};/g;
433     return $in;
434 }
435
436 sub maybelink {
437     my $in = shift;
438     if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
439         return qq{<a href="$in">} . htmlsanit($in) . '</a>';
440     } else {
441         return htmlsanit($in);
442     }
443 }
444
445 sub bugurl {
446     my $ref = shift;
447     my $params = "bug=$ref";
448     foreach my $val (@_) {
449         $params .= "\&msg=$1" if ($val =~ /^msg=([0-9]+)/);
450         $params .= "\&archive=yes" if (!$common_archive && $val =~ /^archive.*$/);
451     }
452     $params .= "&archive=yes" if ($common_archive);
453     $params .= "&repeatmerged=no" unless ($common_repeatmerged);
454
455     return urlsanit("bugreport.cgi" . "?" . "$params");
456 }
457
458 sub dlurl {
459     my $ref = shift;
460     my $params = "bug=$ref";
461     my $filename = '';
462     foreach my $val (@_) {
463         $params .= "\&$1=$2" if ($val =~ /^(msg|att)=([0-9]+)/);
464         $filename = $1 if ($val =~ /^filename=(.*)$/);
465     }
466     $params .= "&archive=yes" if ($common_archive);
467     my $pathinfo = '';
468     $pathinfo = '/'.uri_escape($filename) if $filename ne '';
469
470     return urlsanit("bugreport.cgi$pathinfo?$params");
471 }
472
473 sub mboxurl {
474     my $ref = shift;
475     return urlsanit("bugreport.cgi" . "?" . "bug=$ref&mbox=yes");
476 }
477
478 sub allbugs {
479     return @{getbugs(sub { 1 })};
480 }
481
482 sub bugmatches(\%\%) {
483     my ($hash, $status) = @_;
484     foreach my $key( keys( %$hash ) ) {
485         my $value = $hash->{$key};
486         my $sub = $field_match{$key};
487         return 1 if ($sub->($key, $value, $status));
488     }
489     return 0;
490 }
491 sub bugfilter($%) {
492     my ($bug, %status) = @_;
493     our (%seenmerged);
494     if (%common_include) {
495         return 1 if (!bugmatches(%common_include, %status));
496     }
497     if (%common_exclude) {
498         return 1 if (bugmatches(%common_exclude, %status));
499     }
500     my @merged = sort {$a<=>$b} $bug, split(/ /, $status{mergedwith});
501     return 1 unless ($common_repeatmerged || !$seenmerged{$merged[0]});
502     $seenmerged{$merged[0]} = 1;
503     return 0;
504 }
505
506 sub htmlizebugs {
507     $b = $_[0];
508     my @bugs = @$b;
509     my $anydone = 0;
510
511     my @status = ();
512     my %count;
513     my $header = '';
514     my $footer = '';
515
516     if (@bugs == 0) {
517         return "<HR><H2>No reports found!</H2></HR>\n";
518     }
519
520     if ( $common_bug_reverse ) {
521         @bugs = sort {$b<=>$a} @bugs;
522     } else {
523         @bugs = sort {$a<=>$b} @bugs;
524     }
525     my %seenmerged;
526     foreach my $bug (@bugs) {
527         my %status = %{getbugstatus($bug)};
528         next unless %status;
529         next if bugfilter($bug, %status);
530
531         my $html = sprintf "<li><a href=\"%s\">#%d: %s</a>\n<br>",
532             bugurl($bug), $bug, htmlsanit($status{subject});
533         $html .= htmlindexentrystatus(\%status) . "\n";
534         my $key = join( '_', map( {$status{$_}} @common_grouping ) );
535         $section{$key} .= $html;
536         $count{"_$key"}++;
537         foreach my $grouping ( @common_grouping ) {
538             $count{"${grouping}_$status{$grouping}"}++;
539         }
540         $anydone = 1 if $status{pending} eq 'done';
541         push @status, [ $bug, \%status, $html ];
542     }
543
544     my $result = "";
545     if ($common_raw_sort) {
546         $result .= "<UL>\n" . join("", map( { $_->[ 2 ] } @status ) ) . "</UL>\n";
547     } else {
548         my (@order, @headers);
549         for( my $i = 0; $i < @common_grouping; $i++ ) {
550             my $grouping_name = $common_grouping[ $i ];
551             my @items = @{ $common_grouping_order{ $grouping_name } };
552             @items = reverse( @items ) if ( $common_reverse{ $grouping_name } );
553             my @neworder = ();
554             my @newheaders = ();
555             if ( @order ) {
556                 foreach my $grouping ( @items ) {
557                     push @neworder, map( { "${_}_$grouping" } @order );
558                     push @newheaders, map( { "$_ - $common_headers{$grouping_name}{$grouping}" } @headers );
559                 }
560                 @order = @neworder;
561                 @headers = @newheaders;
562             } else {
563                 push @order, @items;
564                 push @headers, map( { $common_headers{$common_grouping[$i]}{$_} } @items );
565             }
566         }
567         $header .= "<ul>\n";
568         for ( my $i = 0; $i < @order; $i++ ) {
569             my $order = $order[ $i ];
570             next unless defined $section{$order};
571             my $count = $count{"_$order"};
572             my $bugs = $count == 1 ? "bug" : "bugs";
573             $header .= "<li><a href=\"#$order\">$headers[$i]</a> ($count $bugs)</li>\n";
574         }
575         $header .= "</ul>\n";
576         for ( my $i = 0; $i < @order; $i++ ) {
577             my $order = $order[ $i ];
578             next unless defined $section{$order};
579             if ($common{show_list_header}) {
580                 my $count = $count{"_$order"};
581                 my $bugs = $count == 1 ? "bug" : "bugs";
582                 $result .= "<HR><H2><a name=\"$order\"></a>$headers[$i] ($count $bugs)</H2>\n";
583             } else {
584                 $result .= "<HR><H2>$headers[$i]</H2>\n";
585             }
586             $result .= "<UL>\n";
587             $result .= $section{$order};
588             $result .= "</UL>\n";
589         }    
590         $footer .= "<ul>\n";
591         foreach my $grouping ( @common_grouping ) {
592             my $local_result = '';
593             foreach my $key ( @{$common_grouping_order{ $grouping }} ) {
594                 my $count = $count{"${grouping}_$key"};
595                 next if !$count;
596                 $local_result .= "<li>$count $common_headers{$grouping}{$key}</li>\n";
597             }
598             if ( $local_result ) {
599                 $footer .= "<li>$common_grouping_display{$grouping}<ul>\n$local_result</ul></li>\n";
600             }
601         }
602         $footer .= "</ul>\n";
603     }
604
605     $result = $header . $result if ( $common{show_list_header} );
606     $result .= $debbugs::gHTMLExpireNote if $gRemoveAge and $anydone;
607     $result .= "<hr>" . $footer if ( $common{show_list_footer} );
608     return $result;
609 }
610
611 sub countbugs {
612     my $bugfunc = shift;
613     if ($common_archive) {
614         open I, "<$debbugs::gSpoolDir/index.archive"
615             or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
616     } else {
617         open I, "<$debbugs::gSpoolDir/index.db"
618             or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
619     }
620
621     my %count = ();
622     while(<I>) 
623     {
624         if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
625             my @x = $bugfunc->(pkg => $1, bug => $2, status => $4, 
626                                submitter => $5, severity => $6, tags => $7);
627             local $_;
628             $count{$_}++ foreach @x;
629         }
630     }
631     close I;
632     return %count;
633 }
634
635 sub getbugs {
636     my $bugfunc = shift;
637     my $opt = shift;
638
639     my @result = ();
640
641     if (!$common_archive && defined $opt && 
642         -e "$debbugs::gSpoolDir/by-$opt.idx") 
643     {
644         my %lookup;
645 print STDERR "optimized\n" if ($debug);
646         tie %lookup, DB_File => "$debbugs::gSpoolDir/by-$opt.idx", O_RDONLY
647             or die "$0: can't open $debbugs::gSpoolDir/by-$opt.idx ($!)\n";
648         while ($key = shift) {
649             my $bugs = $lookup{$key};
650             if (defined $bugs) {
651                 push @result, (unpack 'N*', $bugs);
652             }
653         }
654         untie %lookup;
655 print STDERR "done optimized\n" if ($debug);
656     } else {
657         if ( $common_archive ) {
658             open I, "<$debbugs::gSpoolDir/index.archive" 
659                 or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
660         } else {
661             open I, "<$debbugs::gSpoolDir/index.db" 
662                 or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
663         }
664         while(<I>) {
665             if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
666                 if ($bugfunc->(pkg => $1, bug => $2, status => $4,
667                             submitter => $5, severity => $6, tags => $7)) 
668                 {
669                     push (@result, $2);
670                 }
671             }
672         }
673         close I;
674     }
675     @result = sort {$a <=> $b} @result;
676     return \@result;
677 }
678
679 sub emailfromrfc822 {
680     my $email = shift;
681     $email =~ s/\s*\(.*\)\s*//;
682     $email = $1 if ($email =~ m/<(.*)>/);
683     return $email;
684 }
685
686 sub maintencoded {
687     my $input = shift;
688     my $encoded = '';
689
690     while ($input =~ m/\W/) {
691         $encoded.=$`.sprintf("-%02x_",unpack("C",$&));
692         $input= $';
693     }
694
695     $encoded.= $input;
696     $encoded =~ s/-2e_/\./g;
697     $encoded =~ s/^([^,]+)-20_-3c_(.*)-40_(.*)-3e_/$1,$2,$3,/;
698     $encoded =~ s/^(.*)-40_(.*)-20_-28_([^,]+)-29_$/,$1,$2,$3/;
699     $encoded =~ s/-20_/_/g;
700     $encoded =~ s/-([^_]+)_-/-$1/g;
701     return $encoded;
702 }
703
704 my $_maintainer;
705 sub getmaintainers {
706     return $_maintainer if $_maintainer;
707     my %maintainer;
708
709     open(MM,"$gMaintainerFile") or &quitcgi("open $gMaintainerFile: $!");
710     while(<MM>) {
711         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
712         ($a,$b)=($1,$2);
713         $a =~ y/A-Z/a-z/;
714         $maintainer{$a}= $b;
715     }
716     close(MM);
717     if (defined $gMaintainerFileOverride) {
718         open(MM,"$gMaintainerFileOverride") or &quitcgi("open $gMaintainerFileOverride: $!");
719         while(<MM>) {
720             next unless m/^(\S+)\s+(\S.*\S)\s*$/;
721             ($a,$b)=($1,$2);
722             $a =~ y/A-Z/a-z/;
723             $maintainer{$a}= $b;
724         }
725         close(MM);
726     }
727     $_maintainer = \%maintainer;
728     return $_maintainer;
729 }
730
731 my $_pkgsrc;
732 my $_pkgcomponent;
733 sub getpkgsrc {
734     return $_pkgsrc if $_pkgsrc;
735     return {} unless defined $gPackageSource;
736     my %pkgsrc;
737     my %pkgcomponent;
738
739     open(MM,"$gPackageSource") or &quitcgi("open $gPackageSource: $!");
740     while(<MM>) {
741         next unless m/^(\S+)\s+(\S+)\s+(\S.*\S)\s*$/;
742         ($a,$b,$c)=($1,$2,$3);
743         $a =~ y/A-Z/a-z/;
744         $pkgsrc{$a}= $c;
745         $pkgcomponent{$a}= $b;
746     }
747     close(MM);
748     $_pkgsrc = \%pkgsrc;
749     $_pkgcomponent = \%pkgcomponent;
750     return $_pkgsrc;
751 }
752
753 sub getpkgcomponent {
754     return $_pkgcomponent if $_pkgcomponent;
755     getpkgsrc();
756     return $_pkgcomponent;
757 }
758
759 my $_pseudodesc;
760 sub getpseudodesc {
761     return $_pseudodesc if $_pseudodesc;
762     my %pseudodesc;
763
764     open(PSEUDO, "< $gPseudoDescFile") or &quitcgi("open $gPseudoDescFile: $!");
765     while(<PSEUDO>) {
766         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
767         $pseudodesc{lc $1} = $2;
768     }
769     close(PSEUDO);
770     $_pseudodesc = \%pseudodesc;
771     return $_pseudodesc;
772 }
773
774 sub getbugstatus {
775     my $bugnum = shift;
776
777     my %status;
778
779     my $location = getbuglocation( $bugnum, 'summary' );
780     return {} if ( !$location );
781     %status = %{ readbug( $bugnum, $location ) };
782     $status{ id } = $bugnum;
783
784     $status{found_versions} = [];
785     $status{fixed_versions} = [];
786     if (defined $gVersionBugsDir and
787             (defined $common_version or defined $common_dist)) {
788         my $bughash = get_hashname($bugnum);
789         if (open BUGVER, "< $gVersionBugsDir/$bughash/$bugnum.versions") {
790             local $_;
791             while (<BUGVER>) {
792                 if (/^Found-in: (.*)/i) {
793                     $status{found_versions} = [split ' ', $1];
794                 } elsif (/^Fixed-in: (.*)/i) {
795                     $status{fixed_versions} = [split ' ', $1];
796                 }
797             }
798             close BUGVER;
799         }
800     }
801
802     $status{tags} = $status{keywords};
803     my %tags = map { $_ => 1 } split ' ', $status{tags};
804
805     $status{"package"} =~ s/\s*$//;
806     $status{"package"} = 'unknown' if ($status{"package"} eq '');
807     $status{"severity"} = 'normal' if ($status{"severity"} eq '');
808
809     $status{"pending"} = 'pending';
810     $status{"pending"} = 'forwarded'        if (length($status{"forwarded"}));
811     $status{"pending"} = 'pending-fixed'    if ($tags{pending});
812     $status{"pending"} = 'fixed'            if ($tags{fixed});
813
814     my $version;
815     if (defined $common_version) {
816         $version = $common_version;
817     } elsif (defined $common_dist) {
818         $version = getversion($status{package}, $common_dist, $common_arch);
819     }
820
821     if (defined $version) {
822         my $buggy = buggyversion($bugnum, $version, \%status);
823         if ($buggy eq 'absent') {
824             $status{"pending"} = 'absent';
825         } elsif ($buggy eq 'fixed') {
826             $status{"pending"} = 'done';
827         }
828     }
829     
830     if (length($status{done}) and
831             (not defined $version or not @{$status{fixed_versions}})) {
832         $status{"pending"} = 'done';
833     }
834
835     return \%status;
836 }
837
838 sub getsrcpkgs {
839     my $src = shift;
840     return () if !$src;
841     my %pkgsrc = %{getpkgsrc()};
842     my @pkgs;
843     foreach ( keys %pkgsrc ) {
844         push @pkgs, $_ if $pkgsrc{$_} eq $src;
845     }
846     return @pkgs;
847 }
848    
849 sub buglog {
850     my $bugnum = shift;
851     my $location = getbuglocation($bugnum, 'log');
852     return getbugcomponent($bugnum, 'log', $location) if ($location);
853     $location = getbuglocation($bugnum, 'log.gz');
854     return getbugcomponent($bugnum, 'log.gz', $location);
855 }
856
857 my %_versionobj;
858 sub buggyversion {
859     my ($bug, $ver, $status) = @_;
860     return '' unless defined $gVersionPackagesDir;
861     my $src = getpkgsrc()->{$status->{package}};
862     $src = $status->{package} unless defined $src;
863
864     my $tree;
865     if (exists $_versionobj{$src}) {
866         $tree = $_versionobj{$src};
867     } else {
868         $tree = Debbugs::Versions->new(\&DpkgVer::vercmp);
869         if (open VERFILE, "< $gVersionPackagesDir/$src") {
870             $tree->load(\*VERFILE);
871             close VERFILE;
872         }
873         $_versionobj{$src} = $tree;
874     }
875
876     return $tree->buggy($ver, $status->{found_versions},
877                         $status->{fixed_versions});
878 }
879
880 my %_versions;
881 sub getversion {
882     my ($pkg, $dist, $arch) = @_;
883     return undef unless defined $gVersionIndex;
884     $dist = 'unstable' unless defined $dist;
885     $arch = 'i386' unless defined $arch;
886
887     unless (tied %_versions) {
888         tie %_versions, 'MLDBM', $gVersionIndex, O_RDONLY
889             or die "can't open versions index: $!";
890     }
891
892     return $_versions{$pkg}{$dist}{$arch};
893 }
894
895 1;