]> git.donarmstrong.com Git - debbugs.git/blob - cgi/common.pl
[project @ 2003-09-16 20:57:19 by doogie]
[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
8 $config_path = '/etc/debbugs';
9 $lib_path = '/usr/lib/debbugs';
10 require "$lib_path/errorlib";
11
12 use Debbugs::Versions;
13
14 $MLDBM::RemoveTaint = 1;
15
16 my $common_archive = 0;
17 my $common_repeatmerged = 1;
18 my %common_include = ();
19 my %common_exclude = ();
20 my $common_raw_sort = 0;
21 my $common_bug_reverse = 0;
22
23 my %common_reverse = (
24     'pending' => 0,
25     'severity' => 0,
26 );
27
28 sub exact_field_match {
29     my ($field, $values, $status) = @_; 
30     my @values = @$values;
31     my @ret = grep {$_ eq $status->{$field} } @values;
32     $#ret != -1;
33 }
34 sub contains_field_match {
35     my ($field, $values, $status) = @_; 
36     foreach my $data (@$values) {
37         return 1 if (index($status->{$field}, $data) > -1);
38     }
39     return 0;        
40 }
41
42 my %field_match = (
43     'subject' => \&contains_field_match,
44     'tags' => sub {
45         my ($field, $values, $status) = @_; 
46         my %values = map {$_=>1} @$values;
47         foreach my $t (split /\s+/, $status->{$field}) {
48             return 1 if (defined $values{$t});
49         }
50         return 0;
51     },
52     'severity' => \&exact_field_match,
53     'pending' => \&exact_field_match,
54     'originator' => \%contains_field_match,
55     'forwarded' => \%contains_field_match,
56     'owner' => \%contains_field_match,
57 );
58 my @common_grouping = ( 'severity', 'pending' );
59 my %common_grouping_order = (
60     'pending' => [ qw( pending forwarded pending-fixed fixed done absent ) ],
61     'severity' => \@debbugs::gSeverityList,
62 );
63 my %common_headers = (
64     'pending' => {
65         "pending"       => "outstanding",
66         "pending-fixed" => "pending upload",
67         "fixed"         => "fixed in NMU",
68         "done"          => "resolved",
69         "forwarded"     => "forwarded to upstream software authors",
70         "absent"        => "not applicable to this version",
71     },
72     'severity' => \%debbugs::gSeverityDisplay,
73 );
74         
75 my $common_version;
76 my $common_dist;
77 my $common_arch;
78
79 my $debug = 0;
80
81 sub array_option($) {
82     my ($val) = @_;
83     my @vals;
84     @vals = ( $val ) if (ref($val) eq "" && $val );
85     @vals = ( $$val ) if (ref($val) eq "SCALAR" && $$val );
86     @vals = @{$val} if (ref($val) eq "ARRAY" );
87     return @vals;
88 }
89
90 sub filter_include_exclude($\%) {
91     my ($val, $filter_map) = @_;
92     my @vals = array_option($val);
93     my @data = map {
94         if (/^([^:]*):(.*)$/) { if ($1 eq 'subj') { ['subject', $2]; } else { [$1, $2] } } else { ['tags', $_] }
95     } split /[\s,]+/, join ',', @vals;
96     foreach my $data (@data) {
97         &quitcgi("Invalid filter key: '$data->[0]'") if (!exists($field_match{$data->[0]}));
98         push @{$filter_map->{$data->[0]}}, $data->[1];
99     }
100 }
101
102 sub filter_option($$\%) {
103     my ($key, $val, $filter_map) = @_;
104     my @vals = array_option($val);
105     foreach $val (@vals) {
106         push @{$filter_map->{$key}}, $val;
107     }
108 }
109
110 sub set_option {
111     my ($opt, $val) = @_;
112     if ($opt eq "archive") { $common_archive = $val; }
113     if ($opt eq "repeatmerged") { $common_repeatmerged = $val; }
114     if ($opt eq "exclude") {
115         filter_include_exclude($val, %common_exclude);
116     }
117     if ($opt eq "include") {
118         filter_include_exclude($val, %common_include);
119     }
120     if ($opt eq "raw") { $common_raw_sort = $val; }
121     if ($opt eq "bug-rev") { $common_bug_reverse = $val; }
122     if ($opt eq "pend-rev") { $common_reverse{pending} = $val; }
123     if ($opt eq "sev-rev") { $common_reverse{severity} = $val; }
124     if ($opt eq "pend-exc") {
125         filter_option('pending', $val, %common_exclude);
126     }
127     if ($opt eq "pend-inc") {
128         filter_option('pending', $val, %common_include);
129     }
130     if ($opt eq "sev-exc") {
131         filter_option('severity', $val, %common_exclude);
132     }
133     if ($opt eq "sev-inc") {
134         filter_option('severity', $val, %common_include);
135     }
136     if ($opt eq "version") { $common_version = $val; }
137     if ($opt eq "dist") { $common_dist = $val; }
138     if ($opt eq "arch") { $common_arch = $val; }
139 }
140
141 sub readparse {
142     my ($in, $key, $val, %ret);
143     if (defined $ENV{"QUERY_STRING"} && $ENV{"QUERY_STRING"} ne "") {
144         $in=$ENV{QUERY_STRING};
145     } elsif(defined $ENV{"REQUEST_METHOD"}
146         && $ENV{"REQUEST_METHOD"} eq "POST")
147     {
148         read(STDIN,$in,$ENV{CONTENT_LENGTH});
149     } else {
150         return;
151     }
152     foreach (split(/[&;]/,$in)) {
153         s/\+/ /g;
154         ($key, $val) = split(/=/,$_,2);
155         $key=~s/%(..)/pack("c",hex($1))/ge;
156         $val=~s/%(..)/pack("c",hex($1))/ge;
157         if ( exists $ret{$key} ) {
158             if ( !exists $ret{"&$key"} ) {
159                 $ret{"&$key"} = [ $ret{$key} ];
160             }
161             push @{$ret{"&$key"}},$val;
162         }
163         $ret{$key}=$val;
164     }
165 $debug = 1 if (defined $ret{"debug"} && $ret{"debug"} eq "aj");
166     return %ret;
167 }
168
169 sub quitcgi {
170     my $msg = shift;
171     print "Content-Type: text/html\n\n";
172     print "<HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>\n";
173     print "An error occurred. Dammit.\n";
174     print "Error was: $msg.\n";
175     print "</BODY></HTML>\n";
176     exit 0;
177 }
178
179 #sub abort {
180 #    my $msg = shift;
181 #    my $Archive = $common_archive ? "archive" : "";
182 #    print header . start_html("Sorry");
183 #    print "Sorry bug #$msg doesn't seem to be in the $Archive database.\n";
184 #    print end_html;
185 #    exit 0;
186 #}
187
188 # Split a package string from the status file into a list of package names.
189 sub splitpackages {
190     my $pkgs = shift;
191     return unless defined $pkgs;
192     return map lc, split /[ \t?,()]+/, $pkgs;
193 }
194
195 my %_parsedaddrs;
196 sub getparsedaddrs {
197     my $addr = shift;
198     return () unless defined $addr;
199     return @{$_parsedaddrs{$addr}} if exists $_parsedaddrs{$addr};
200     @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
201     return @{$_parsedaddrs{$addr}};
202 }
203
204 # Generate a comma-separated list of HTML links to each package given in
205 # $pkgs. $pkgs may be empty, in which case an empty string is returned, or
206 # it may be a comma-separated list of package names.
207 sub htmlpackagelinks {
208     my $pkgs = shift;
209     return unless defined $pkgs and $pkgs ne '';
210     my $strong = shift;
211     my @pkglist = splitpackages($pkgs);
212
213     my $openstrong  = $strong ? '<strong>' : '';
214     my $closestrong = $strong ? '</strong>' : '';
215
216     return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
217            join(', ',
218                 map {
219                     '<a href="' . pkgurl($_) . '">' .
220                     $openstrong . htmlsanit($_) . $closestrong . '</a>'
221                 } @pkglist
222            ) . ";\n";
223 }
224
225 # Generate a comma-separated list of HTML links to each maintainer given in
226 # $maints, which should be a comma-separated list of RFC822 addresses.
227 sub htmlmaintlinks {
228     my ($prefixfunc, $maints) = @_;
229     if (defined $maints and $maints ne '') {
230         my @maintaddrs = getparsedaddrs($maints);
231         my $prefix = (ref $prefixfunc) ? $prefixfunc->(scalar @maintaddrs)
232                                        : $prefixfunc;
233         return $prefix .
234                join ', ', map { sprintf '<a href="%s">%s</a>',
235                                         mainturl($_->address),
236                                         htmlsanit($_->format) || '(unknown)'
237                               } @maintaddrs;
238     } else {
239         my $prefix = (ref $prefixfunc) ? $prefixfunc->(1) : $prefixfunc;
240         return sprintf '%s<a href="%s">(unknown)</a>', $prefix, mainturl('');
241     }
242 }
243
244 sub htmlindexentry {
245     my $ref = shift;
246     my %status = %{getbugstatus($ref)};
247     return htmlindexentrystatus(%status) if (%status);
248     return "";
249 }
250
251 sub htmlindexentrystatus {
252     my $s = shift;
253     my %status = %{$s};
254
255     my $result = "";
256
257     if  ($status{severity} eq 'normal') {
258         $showseverity = '';
259     } elsif (isstrongseverity($status{severity})) {
260         $showseverity = "<strong>Severity: $status{severity}</strong>;\n";
261     } else {
262         $showseverity = "Severity: <em>$status{severity}</em>;\n";
263     }
264
265     $result .= htmlpackagelinks($status{"package"}, 1);
266     $result .= $showseverity;
267     $result .= "Reported by: <a href=\"" . submitterurl($status{originator})
268                . "\">" . htmlsanit($status{originator}) . "</a>";
269     $result .= ";\nOwned by: " . htmlsanit($status{owner})
270                if length $status{owner};
271     $result .= ";\nTags: <strong>" 
272                  . htmlsanit(join(", ", sort(split(/\s+/, $status{tags}))))
273                  . "</strong>"
274                        if (length($status{tags}));
275
276     my @merged= split(/ /,$status{mergedwith});
277     my $mseparator= ";\nmerged with ";
278     for my $m (@merged) {
279         $result .= $mseparator."<A href=\"" . bugurl($m) . "\">#$m</A>";
280         $mseparator= ", ";
281     }
282
283     if (@{$status{found_versions}}) {
284         $result .= ";\nfound in ";
285         $result .= (@{$status{found_versions}} == 1) ? 'version '
286                                                      : 'versions ';
287         $result .= join ', ', map htmlsanit($_), @{$status{found_versions}};
288     }
289
290     if (@{$status{fixed_versions}}) {
291         $result .= ";\n<strong>fixed</strong> in ";
292         $result .= (@{$status{fixed_versions}} == 1) ? 'version '
293                                                      : 'versions ';
294         $result .= join ', ', map htmlsanit($_), @{$status{fixed_versions}};
295         if (length($status{done})) {
296             $result .= ' by ' . htmlsanit($status{done});
297         }
298     } elsif (length($status{done})) {
299         $result .= ";\n<strong>Done:</strong> " . htmlsanit($status{done});
300     }
301
302     unless (length($status{done})) {
303         if (length($status{forwarded})) {
304             $result .= ";\n<strong>Forwarded</strong> to "
305                        . maybelink($status{forwarded});
306         }
307         my $daysold = int((time - $status{date}) / 86400);   # seconds to days
308         if ($daysold >= 7) {
309             my $font = "";
310             my $efont = "";
311             $font = "em" if ($daysold > 30);
312             $font = "strong" if ($daysold > 60);
313             $efont = "</$font>" if ($font);
314             $font = "<$font>" if ($font);
315
316             my $yearsold = int($daysold / 365);
317             $daysold -= $yearsold * 365;
318
319             $result .= ";\n $font";
320             my @age;
321             push @age, "1 year" if ($yearsold == 1);
322             push @age, "$yearsold years" if ($yearsold > 1);
323             push @age, "1 day" if ($daysold == 1);
324             push @age, "$daysold days" if ($daysold > 1);
325             $result .= join(" and ", @age);
326             $result .= " old$efont";
327         }
328     }
329
330     $result .= ".";
331
332     return $result;
333 }
334
335 sub urlargs {
336     my $args = '';
337     $args .= "&archive=yes" if $common_archive;
338     $args .= "&repeatmerged=no" unless $common_repeatmerged;
339     $args .= "&version=$common_version" if defined $common_version;
340     $args .= "&dist=$common_dist" if defined $common_dist;
341     $args .= "&arch=$common_arch" if defined $common_arch;
342     return $args;
343 }
344
345 sub submitterurl {
346     my $ref = shift || "";
347     my $params = "submitter=" . emailfromrfc822($ref);
348     $params .= urlargs();
349     return urlsanit("pkgreport.cgi" . "?" . $params);
350 }
351
352 sub mainturl {
353     my $ref = shift || "";
354     my $params = "maint=" . emailfromrfc822($ref);
355     $params .= urlargs();
356     return urlsanit("pkgreport.cgi" . "?" . $params);
357 }
358
359 sub pkgurl {
360     my $ref = shift;
361     my $params = "pkg=$ref";
362     $params .= urlargs();
363     return urlsanit("pkgreport.cgi" . "?" . "$params");
364 }
365
366 sub srcurl {
367     my $ref = shift;
368     my $params = "src=$ref";
369     $params .= urlargs();
370     return urlsanit("pkgreport.cgi" . "?" . "$params");
371 }
372
373 sub tagurl {
374     my $ref = shift;
375     my $params = "tag=$ref";
376     $params .= urlargs();
377     return urlsanit("pkgreport.cgi" . "?" . "$params");
378 }
379
380 sub urlsanit {
381     my $url = shift;
382     $url =~ s/%/%25/g;
383     $url =~ s/\+/%2b/g;
384     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
385     $url =~ s/([<>&"])/\&$saniarray{$1};/g;
386     return $url;
387 }
388
389 sub htmlsanit {
390     my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot');
391     my $in = shift || "";
392     $in =~ s/([<>&"])/\&$saniarray{$1};/g;
393     return $in;
394 }
395
396 sub maybelink {
397     my $in = shift;
398     if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
399         return qq{<a href="$in">} . htmlsanit($in) . '</a>';
400     } else {
401         return htmlsanit($in);
402     }
403 }
404
405 sub bugurl {
406     my $ref = shift;
407     my $params = "bug=$ref";
408     foreach my $val (@_) {
409         $params .= "\&msg=$1" if ($val =~ /^msg=([0-9]+)/);
410         $params .= "\&archive=yes" if (!$common_archive && $val =~ /^archive.*$/);
411     }
412     $params .= "&archive=yes" if ($common_archive);
413     $params .= "&repeatmerged=no" unless ($common_repeatmerged);
414
415     return urlsanit("bugreport.cgi" . "?" . "$params");
416 }
417
418 sub dlurl {
419     my $ref = shift;
420     my $params = "bug=$ref";
421     my $filename = '';
422     foreach my $val (@_) {
423         $params .= "\&$1=$2" if ($val =~ /^(msg|att)=([0-9]+)/);
424         $filename = $1 if ($val =~ /^filename=(.*)$/);
425     }
426     $params .= "&archive=yes" if ($common_archive);
427     my $pathinfo = '';
428     $pathinfo = "/$filename" if $filename ne '';
429
430     return urlsanit("bugreport.cgi$pathinfo?$params");
431 }
432
433 sub mboxurl {
434     my $ref = shift;
435     return urlsanit("bugreport.cgi" . "?" . "bug=$ref&mbox=yes");
436 }
437
438 sub allbugs {
439     return @{getbugs(sub { 1 })};
440 }
441
442 sub bugmatches(\%\%) {
443     my ($hash, $status) = @_;
444     while ((my ($key, $value) = each(%$hash))) {
445         my $sub = $field_match{$key};
446         return 1 if ($sub->($key, $value, $status));
447     }
448     return 0;
449 }
450 sub bugfilter($%) {
451     my ($bug, %status) = @_;
452     local (%seenmerged);
453     if (%common_include) {
454         return 1 if (!bugmatches(%common_include, %status));
455     }
456     if (%common_exclude) {
457         return 1 if (bugmatches(%common_exclude, %status));
458     }
459     my @merged = sort {$a<=>$b} $bug, split(/ /, $status{mergedwith});
460     return 1 unless ($common_repeatmerged || !$seenmerged{$merged[0]});
461     $seenmerged{$merged[0]} = 1;
462     return 0;
463 }
464
465 sub htmlizebugs {
466     $b = $_[0];
467     my @bugs = @$b;
468     my $anydone = 0;
469
470     my @status = ();
471
472     if (@bugs == 0) {
473         return "<HR><H2>No reports found!</H2></HR>\n";
474     }
475
476     if ( $common_bug_reverse ) {
477         @bugs = sort {$b<=>$a} @bugs;
478     } else {
479         @bugs = sort {$a<=>$b} @bugs;
480     }
481     my %seenmerged;
482     foreach my $bug (@bugs) {
483         my %status = %{getbugstatus($bug)};
484         next unless %status;
485         next if bugfilter($bug, %status);
486
487         my $html = sprintf "<li><a href=\"%s\">#%d: %s</a>\n<br>",
488             bugurl($bug), $bug, htmlsanit($status{subject});
489         $html .= htmlindexentrystatus(\%status) . "\n";
490         $section{join( '_', map( {$status{$_}} @common_grouping ) )} .= $html;
491         $anydone = 1 if $status{pending} eq 'done';
492         push @status, [ $bug, \%status, $html ];
493     }
494
495     my $result = "";
496     if ($common_raw_sort) {
497         $result .= "<UL>\n" . join("", map( { $_->[ 2 ] } @status ) ) . "</UL>\n";
498     } else {
499         my (@order, @headers);
500         for( my $i = 0; $i < @common_grouping; $i++ ) {
501             my $grouping_name = $common_grouping[ $i ];
502             my @items = @{ $common_grouping_order{ $grouping_name } };
503             @items = reverse( @items ) if ( $common_reverse{ $grouping_name } );
504             my @neworder = ();
505             my @newheaders = ();
506             if ( @order ) {
507                 foreach my $grouping ( @items ) {
508                     push @neworder, map( { "${_}_$grouping" } @order );
509                     push @newheaders, map( { "$_ - $common_headers{$grouping_name}{$grouping}" } @headers );
510                 }
511                 @order = @neworder;
512                 @headers = @newheaders;
513             } else {
514                 push @order, @items;
515                 push @headers, map( { $common_headers{$common_grouping[$i]}{$_} } @items );
516             }
517         }
518         for ( my $i = 0; $i < @order; $i++ ) {
519             my $order = $order[ $i ];
520             next unless defined $section{$order};
521             $result .= "<HR><H2>$headers[$i]</H2>\n";
522             $result .= "<UL>\n";
523             $result .= $section{$order};
524             $result .= "</UL>\n";
525         }    
526     }
527
528     $result .= $debbugs::gHTMLExpireNote if $gRemoveAge and $anydone;
529     return $result;
530 }
531
532 sub countbugs {
533     my $bugfunc = shift;
534     if ($common_archive) {
535         open I, "<$debbugs::gSpoolDir/index.archive"
536             or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
537     } else {
538         open I, "<$debbugs::gSpoolDir/index.db"
539             or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
540     }
541
542     my %count = ();
543     while(<I>) 
544     {
545         if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
546             my @x = $bugfunc->(pkg => $1, bug => $2, status => $4, 
547                                submitter => $5, severity => $6, tags => $7);
548             local $_;
549             $count{$_}++ foreach @x;
550         }
551     }
552     close I;
553     return %count;
554 }
555
556 sub getbugs {
557     my $bugfunc = shift;
558     my $opt = shift;
559
560     my @result = ();
561
562     if (!$common_archive && defined $opt && 
563         -e "$debbugs::gSpoolDir/by-$opt.idx") 
564     {
565         my %lookup;
566 print STDERR "optimized\n" if ($debug);
567         tie %lookup, DB_File => "$debbugs::gSpoolDir/by-$opt.idx", O_RDONLY
568             or die "$0: can't open $debbugs::gSpoolDir/by-$opt.idx ($!)\n";
569         while ($key = shift) {
570             my $bugs = $lookup{$key};
571             if (defined $bugs) {
572                 push @result, (unpack 'N*', $bugs);
573             }
574         }
575         untie %lookup;
576 print STDERR "done optimized\n" if ($debug);
577     } else {
578         if ( $common_archive ) {
579             open I, "<$debbugs::gSpoolDir/index.archive" 
580                 or &quitcgi("$debbugs::gSpoolDir/index.archive: $!");
581         } else {
582             open I, "<$debbugs::gSpoolDir/index.db" 
583                 or &quitcgi("$debbugs::gSpoolDir/index.db: $!");
584         }
585         while(<I>) {
586             if (m/^(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+\[\s*([^]]*)\s*\]\s+(\w+)\s+(.*)$/) {
587                 if ($bugfunc->(pkg => $1, bug => $2, status => $4,
588                             submitter => $5, severity => $6, tags => $7)) 
589                 {
590                     push (@result, $2);
591                 }
592             }
593         }
594         close I;
595     }
596     @result = sort {$a <=> $b} @result;
597     return \@result;
598 }
599
600 sub emailfromrfc822 {
601     my $email = shift;
602     $email =~ s/\s*\(.*\)\s*//;
603     $email = $1 if ($email =~ m/<(.*)>/);
604     return $email;
605 }
606
607 sub maintencoded {
608     my $input = shift;
609     my $encoded = '';
610
611     while ($input =~ m/\W/) {
612         $encoded.=$`.sprintf("-%02x_",unpack("C",$&));
613         $input= $';
614     }
615
616     $encoded.= $input;
617     $encoded =~ s/-2e_/\./g;
618     $encoded =~ s/^([^,]+)-20_-3c_(.*)-40_(.*)-3e_/$1,$2,$3,/;
619     $encoded =~ s/^(.*)-40_(.*)-20_-28_([^,]+)-29_$/,$1,$2,$3/;
620     $encoded =~ s/-20_/_/g;
621     $encoded =~ s/-([^_]+)_-/-$1/g;
622     return $encoded;
623 }
624
625 my $_maintainer;
626 sub getmaintainers {
627     return $_maintainer if $_maintainer;
628     my %maintainer;
629
630     open(MM,"$gMaintainerFile") or &quitcgi("open $gMaintainerFile: $!");
631     while(<MM>) {
632         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
633         ($a,$b)=($1,$2);
634         $a =~ y/A-Z/a-z/;
635         $maintainer{$a}= $b;
636     }
637     close(MM);
638     if (defined $gMaintainerFileOverride) {
639         open(MM,"$gMaintainerFileOverride") or &quitcgi("open $gMaintainerFileOverride: $!");
640         while(<MM>) {
641             next unless m/^(\S+)\s+(\S.*\S)\s*$/;
642             ($a,$b)=($1,$2);
643             $a =~ y/A-Z/a-z/;
644             $maintainer{$a}= $b;
645         }
646         close(MM);
647     }
648     $_maintainer = \%maintainer;
649     return $_maintainer;
650 }
651
652 my $_pkgsrc;
653 my $_pkgcomponent;
654 sub getpkgsrc {
655     return $_pkgsrc if $_pkgsrc;
656     return {} unless defined $gPackageSource;
657     my %pkgsrc;
658     my %pkgcomponent;
659
660     open(MM,"$gPackageSource") or &quitcgi("open $gPackageSource: $!");
661     while(<MM>) {
662         next unless m/^(\S+)\s+(\S+)\s+(\S.*\S)\s*$/;
663         ($a,$b,$c)=($1,$2,$3);
664         $a =~ y/A-Z/a-z/;
665         $pkgsrc{$a}= $c;
666         $pkgcomponent{$a}= $b;
667     }
668     close(MM);
669     $_pkgsrc = \%pkgsrc;
670     $_pkgcomponent = \%pkgcomponent;
671     return $_pkgsrc;
672 }
673
674 sub getpkgcomponent {
675     return $_pkgcomponent if $_pkgcomponent;
676     getpkgsrc();
677     return $_pkgcomponent;
678 }
679
680 my $_pseudodesc;
681 sub getpseudodesc {
682     return $_pseudodesc if $_pseudodesc;
683     my %pseudodesc;
684
685     open(PSEUDO, "< $gPseudoDescFile") or &quitcgi("open $gPseudoDescFile: $!");
686     while(<PSEUDO>) {
687         next unless m/^(\S+)\s+(\S.*\S)\s*$/;
688         $pseudodesc{lc $1} = $2;
689     }
690     close(PSEUDO);
691     $_pseudodesc = \%pseudodesc;
692     return $_pseudodesc;
693 }
694
695 sub getbugstatus {
696     my $bugnum = shift;
697
698     my %status;
699
700     my $location = getbuglocation( $bugnum, 'summary' );
701     return {} if ( !$location );
702     %status = %{ readbug( $bugnum, $location ) };
703
704     $status{found_versions} = [];
705     $status{fixed_versions} = [];
706     if (defined $gVersionBugsDir and
707             (defined $common_version or defined $common_dist)) {
708         my $bughash = get_hashname($bugnum);
709         if (open BUGVER, "< $gVersionBugsDir/$bughash/$bugnum.versions") {
710             local $_;
711             while (<BUGVER>) {
712                 if (/^Found-in: (.*)/i) {
713                     $status{found_versions} = [split ' ', $1];
714                 } elsif (/^Fixed-in: (.*)/i) {
715                     $status{fixed_versions} = [split ' ', $1];
716                 }
717             }
718             close BUGVER;
719         }
720     }
721
722     $status{tags} = $status{keywords};
723     my %tags = map { $_ => 1 } split ' ', $status{tags};
724
725     $status{"package"} =~ s/\s*$//;
726     $status{"package"} = 'unknown' if ($status{"package"} eq '');
727     $status{"severity"} = 'normal' if ($status{"severity"} eq '');
728
729     $status{"pending"} = 'pending';
730     $status{"pending"} = 'forwarded'        if (length($status{"forwarded"}));
731     $status{"pending"} = 'pending-fixed'    if ($tags{pending});
732     $status{"pending"} = 'fixed'            if ($tags{fixed});
733
734     my $version;
735     if (defined $common_version) {
736         $version = $common_version;
737     } elsif (defined $common_dist) {
738         $version = getversion($status{package}, $common_dist, $common_arch);
739     }
740
741     if (defined $version) {
742         my $buggy = buggyversion($bugnum, $version, \%status);
743         if ($buggy eq 'absent') {
744             $status{"pending"} = 'absent';
745         } elsif ($buggy eq 'fixed') {
746             $status{"pending"} = 'done';
747         }
748     }
749     
750     if (length($status{done}) and
751             (not defined $version or not @{$status{fixed_versions}})) {
752         $status{"pending"} = 'done';
753     }
754
755     return \%status;
756 }
757
758 sub getsrcpkgs {
759     my $src = shift;
760     return () if !$src;
761     my %pkgsrc = %{getpkgsrc()};
762     my @pkgs;
763     foreach ( keys %pkgsrc ) {
764         push @pkgs, $_ if $pkgsrc{$_} eq $src;
765     }
766     return @pkgs;
767 }
768    
769 sub buglog {
770     my $bugnum = shift;
771     my $location = getbuglocation($bugnum, 'log');
772     return undef unless defined $location;
773     return getbugcomponent($bugnum, 'log', $location);
774 }
775
776 my %_versionobj;
777 sub buggyversion {
778     my ($bug, $ver, $status) = @_;
779     return '' unless defined $gVersionPackagesDir;
780     my $src = getpkgsrc()->{$status->{package}};
781     $src = $status->{package} unless defined $src;
782
783     my $tree;
784     if (exists $_versionobj{$src}) {
785         $tree = $_versionobj{$src};
786     } else {
787         $tree = Debbugs::Versions->new(\&DpkgVer::vercmp);
788         if (open VERFILE, "< $gVersionPackagesDir/$src") {
789             $tree->load(\*VERFILE);
790             close VERFILE;
791         }
792         $_versionobj{$src} = $tree;
793     }
794
795     return $tree->buggy($ver, $status->{found_versions},
796                         $status->{fixed_versions});
797 }
798
799 my %_versions;
800 sub getversion {
801     my ($pkg, $dist, $arch) = @_;
802     return undef unless defined $gVersionIndex;
803     $dist = 'unstable' unless defined $dist;
804     $arch = 'i386' unless defined $arch;
805
806     unless (tied %_versions) {
807         tie %_versions, 'MLDBM', $gVersionIndex, O_RDONLY
808             or die "can't open versions index: $!";
809     }
810
811     return $_versions{$pkg}{$dist}{$arch};
812 }
813
814 1;