]> git.deb.at Git - deb/packages.git/blob - cgi-bin/search_packages.pl
Also move the result printing to a function
[deb/packages.git] / cgi-bin / search_packages.pl
1 #!/usr/bin/perl -wT
2 # $Id$
3 # search_packages.pl -- CGI interface to the Packages files on packages.debian.org
4 #
5 # Copyright (C) 1998 James Treacy
6 # Copyright (C) 2000, 2001 Josip Rodin
7 # Copyright (C) 2001 Adam Heath
8 # Copyright (C) 2004 Martin Schulze
9 # Copyright (C) 2004-2006 Frank Lichtenheld
10 #
11 # use is allowed under the terms of the GNU Public License (GPL)                              
12 # see http://www.fsf.org/copyleft/gpl.html for a copy of the license
13
14 use strict;
15 use CGI qw( -oldstyle_urls );
16 use CGI::Carp qw( fatalsToBrowser );
17 use POSIX;
18 use URI::Escape;
19 use HTML::Entities;
20 use DB_File;
21 use Benchmark;
22
23 use lib "../lib";
24
25 use Deb::Versions;
26 use Packages::Search qw( :all );
27 use Packages::HTML ();
28
29 my $thisscript = $Packages::HTML::SEARCH_CGI;
30 my $HOME = "http://www.debian.org";
31 my $ROOT = "";
32 my $SEARCHPAGE = "http://packages.debian.org/";
33 my @SUITES = qw( oldstable stable testing unstable experimental );
34 my @SECTIONS = qw( main contrib non-free );
35 my @ARCHIVES = qw( us security installer );
36 my @ARCHITECTURES = qw( alpha amd64 arm hppa hurd-i386 i386 ia64
37                         kfreebsd-i386 mips mipsel powerpc s390 sparc );
38 my %SUITES = map { $_ => 1 } @SUITES;
39 my %SECTIONS = map { $_ => 1 } @SECTIONS;
40 my %ARCHIVES = map { $_ => 1 } @ARCHIVES;
41 my %ARCHITECTURES = map { $_ => 1 } @ARCHITECTURES;
42
43 $ENV{PATH} = "/bin:/usr/bin";
44
45 # Read in all the variables set by the form
46 my $input;
47 if ($ARGV[0] && ($ARGV[0] eq 'php')) {
48         $input = new CGI(\*STDIN);
49 } else {
50         $input = new CGI;
51 }
52
53 my $pet0 = new Benchmark;
54 # use this to disable debugging in production mode completly
55 my $debug_allowed = 1;
56 my $debug = $debug_allowed && $input->param("debug");
57 $debug = 0 if not defined($debug);
58 #$Packages::Search::debug = 1 if $debug > 1;
59
60 if (my $path = $input->param('path')) {
61     my @components = map { lc $_ } split /\//, $path;
62
63     foreach (@components) {
64         if ($SUITES{$_}) {
65             $input->param('suite', $_);
66         } elsif ($SECTIONS{$_}) {
67             $input->param('section', $_);
68         } elsif ($ARCHIVES{$_}) {
69             $input->param('archive', $_);
70         }elsif ($ARCHITECTURES{$_}) {
71             $input->param('arch', $_);
72         }
73     }
74 }
75
76 my ( $format, $keyword, $case, $subword, $exact, $searchon,
77      @suites, @sections, @archs );
78
79 my %params_def = ( keywords => { default => undef,
80                                  match => '^\s*([-+\@\w\/.:]+)\s*$',
81                                  var => \$keyword },
82                    suite => { default => 'stable', match => '^(\w+)$',
83                               alias => 'version', array => ',',
84                               var => \@suites,
85                               replace => { all => \@SUITES } },
86                    case => { default => 'insensitive', match => '^(\w+)$',
87                              var => \$case },
88 #                  official => { default => 0, match => '^(\w+)$' },
89 #                  use_cache => { default => 1, match => '^(\w+)$' },
90                    subword => { default => 0, match => '^(\w+)$',
91                                 var => \$subword },
92                    exact => { default => undef, match => '^(\w+)$',
93                               var => \$exact },
94                    searchon => { default => 'all', match => '^(\w+)$',
95                                  var => \$searchon },
96                    section => { default => 'all', match => '^([\w-]+)$',
97                                 alias => 'release', array => ',',
98                                 var => \@sections,
99                                 replace => { all => \@SECTIONS } },
100                    arch => { default => 'any', match => '^(\w+)$',
101                              array => ',', var => \@archs, replace =>
102                              { any => \@ARCHITECTURES } },
103                    archive => { default => 'all', match => '^(\w+)$',
104                                 array => ',', replace =>
105                                 { all => \@ARCHIVES } },
106                    format => { default => 'html', match => '^(\w+)$',
107                                var => \$format },
108                    );
109 my %opts;
110 my %params = Packages::Search::parse_params( $input, \%params_def, \%opts );
111
112 #XXX: Don't use alternative output formats yet
113 $format = 'html';
114 if ($format eq 'html') {
115     print $input->header;
116 }
117
118 my (@errors, @debug, @msgs, @hints);
119 sub error {
120     push @errors, $_[0];
121 }
122 sub hint {
123     push @hints, $_[0];
124 }
125 sub debug {
126     my $lvl = $_[1] || 0;
127     push(@debug, $_[0]) if $debug > $lvl;
128 }
129 sub msg {
130     push @msgs, $_[0];
131 }
132 sub print_errors {
133     return unless @errors;
134     print '<div>';
135     foreach (@errors) {
136         print "<p style=\"background-color:#F99;font-weight:bold;padding:0.5em;margin:0;\">$_</p>";
137     }
138     print '</div>';
139 }
140 sub print_debug {
141     return unless $debug && @debug;
142     print '<div style="font-size:80%";border:solid thin grey">';
143     print '<h2>Debugging:</h2><pre>';
144     foreach (@debug) {
145         print "$_\n";
146     }
147     print '</pre></div>';
148
149 }
150 sub print_hints {
151     return unless @hints;
152     print '<div>';
153     foreach (@hints) {
154         print "<p style=\"background-color:#FF9;padding:0.5em;margin:0\">$_</p>";
155     }
156     print '</div>';
157 }
158 sub print_msgs {
159     foreach (@msgs) {
160         print "<p>$_</p>";
161     }
162 }
163
164 if ($params{errors}{keywords}) {
165     error( "Error: keyword not valid or missing" );
166 }
167
168 my $case_bool = ( $case !~ /insensitive/ );
169 $exact = !$subword unless defined $exact;
170 $opts{h_suites} = { map { $_ => 1 } @suites };
171 $opts{h_sections} = { map { $_ => 1 } @sections };
172 $opts{h_archs} = { map { $_ => 1 } @archs };
173
174 # for URL construction
175 my $suites_param = join ',', @{$params{values}{suite}{no_replace}};
176 my $sections_param = join ',', @{$params{values}{section}{no_replace}};
177 my $archs_param = join ',', @{$params{values}{arch}{no_replace}};
178
179 # for output
180 my $keyword_enc = encode_entities $keyword;
181 my $searchon_enc = encode_entities $searchon;
182 my $suites_enc = encode_entities join ', ', @{$params{values}{suite}{no_replace}};
183 my $sections_enc = encode_entities join ', ', @{$params{values}{section}{no_replace}};
184 my $archs_enc = encode_entities join ', ',  @{$params{values}{arch}{no_replace}};
185 my $pet1 = new Benchmark;
186 my $petd = timediff($pet1, $pet0);
187 debug( "Parameter evaluation took ".timestr($petd) );
188
189 # read the configuration
190 my $topdir;
191 if (!open (C, "../config.sh")) {
192     error( "Internal Error: Cannot open configuration file." );
193 }
194 while (<C>) {
195     $topdir = $1 if /^\s*topdir="?(.*)"?\s*$/;
196     $ROOT = $1 if /^\s*root="?(.*)"?\s*$/;
197 }
198 close (C);
199
200 my $DBDIR = $topdir . "/files/db";
201 my $search_on_sources = 0;
202
203 my $st0 = new Benchmark;
204 my @results;
205 my $too_many_hits;
206 if ($searchon eq 'sourcenames') {
207     $search_on_sources = 1;
208 }
209
210 sub print_header {
211     print Packages::HTML::header( title => 'Package Search Results' ,
212                                   lang => 'en',
213                                   title_tag => 'Debian Package Search Results',
214                                   print_title_above => 1,
215                                   print_search_field => 'packages',
216                                   search_field_values => { 
217                                       keywords => $keyword_enc,
218                                       searchon => $searchon,
219                                       arch => $archs_enc,
220                                       suite => $suites_enc,
221                                       section => $sections_enc,
222                                       subword => $subword,
223                                       exact => $exact,
224                                       case => $case,
225                                   },
226                                   );
227 }
228
229 sub read_entry {
230     my ($hash, $key, $results, $opts) = @_;
231     my $result = $hash->{$key} || '';
232     foreach (split /\000/, $result) {
233         my @data = split ( /\s/, $_, 7 );
234         debug( "Considering entry ".join( ':', @data), 2);
235         if ($opts->{h_suites}{$data[0]}
236             && ($opts->{h_archs}{$data[1]} || $data[1] eq 'all')
237             && $opts->{h_sections}{$data[2]}) {
238             debug( "Using entry ".join( ':', @data), 2);
239             push @$results, [ $key, @data ];
240         }
241     }
242 }
243 sub read_src_entry {
244     my ($hash, $key, $results, $opts) = @_;
245     my $result = $hash->{$key} || '';
246     foreach (split /\000/, $result) {
247         my @data = split ( /\s/, $_, 5 );
248         debug( "Considering entry ".join( ':', @data), 2);
249         if ($opts->{h_suites}{$data[0]} && $opts->{h_sections}{$data[1]}) {
250             debug( "Using entry ".join( ':', @data), 2);
251             push @$results, [ $key, @data ];
252         }
253     }
254 }
255 sub do_names_search {
256     my ($keyword, $file, $postfix_file, $read_entry, $opts) = @_;
257     my @results;
258
259     $keyword = lc $keyword unless $opts->{case_bool};
260     
261     my $obj = tie my %packages, 'DB_File', "$DBDIR/$file", O_RDONLY, 0666, $DB_BTREE
262         or die "couldn't tie DB $DBDIR/$file: $!";
263     
264     if ($opts->{exact}) {
265         &$read_entry( \%packages, $keyword, \@results, $opts );
266     } else {
267         my ($key, $prefixes) = ($keyword, '');
268         my %pkgs;
269         my $p_obj = tie my %pref, 'DB_File', "$DBDIR/$postfix_file", O_RDONLY, 0666, $DB_BTREE
270             or die "couldn't tie postfix db $DBDIR/$postfix_file: $!";
271         $p_obj->seq( $key, $prefixes, R_CURSOR );
272         while (index($key, $keyword) >= 0) {
273             if ($prefixes =~ /^\001(\d+)/o) {
274                 $too_many_hits += $1;
275             } else {
276                 foreach (split /\000/o, $prefixes) {
277                     $_ = '' if $_ eq '^';
278                     debug( "add word $_$key", 2);
279                     $pkgs{$_.$key}++;
280                 }
281             }
282             last if $p_obj->seq( $key, $prefixes, R_NEXT ) != 0;
283             last if $too_many_hits or keys %pkgs >= 100;
284         }
285         
286         my $no_results = keys %pkgs;
287         if ($too_many_hits || ($no_results >= 100)) {
288             $too_many_hits += $no_results;
289             %pkgs = ( $keyword => 1 );
290         }
291         foreach my $pkg (sort keys %pkgs) {
292             &$read_entry( \%packages, $pkg, \@results, $opts );
293         }
294     }
295     return \@results;
296 }
297 sub do_fulltext_search {
298     my ($keword, $file, $mapping, $lookup, $read_entry, $opts) = @_;
299     my @results;
300
301     my @lines;
302     my $regex;
303     if ($opts->{case_bool}) {
304         if ($opts->{exact}) {
305             $regex = qr/\b\Q$keyword\E\b/o;
306         } else {
307             $regex = qr/\Q$keyword\E/o;
308         }
309     } else {
310         if ($opts->{exact}) {
311             $regex = qr/\b\Q$keyword\E\b/io;
312         } else {
313             $regex = qr/\Q$keyword\E/io;
314         }
315     }
316
317     open DESC, '<', "$DBDIR/$file"
318         or die "couldn't open $DBDIR/$file: $!";
319     while (<DESC>) {
320         $_ =~ $regex or next;
321         debug( "Matched line $.", 2);
322         push @lines, $.;
323     }
324     close DESC;
325
326     tie my %packages, 'DB_File', "$DBDIR/$lookup", O_RDONLY, 0666, $DB_BTREE
327         or die "couldn't tie DB $DBDIR/$lookup: $!";
328     tie my %did2pkg, 'DB_File', "$DBDIR/$mapping", O_RDONLY, 0666, $DB_BTREE
329         or die "couldn't tie DB $DBDIR/$mapping: $!";
330
331     my %tmp_results;
332     foreach my $l (@lines) {
333         my $result = $did2pkg{$l};
334         foreach (split /\000/o, $result) {
335             my @data = split /\s/, $_, 3;
336             next unless $opts->{h_archs}{$data[2]};
337             $tmp_results{$data[0]}++;
338         }
339     }
340     foreach my $pkg (keys %tmp_results) {
341         &$read_entry( \%packages, $pkg, \@results, $opts );
342     }
343     return \@results;
344 }
345
346 sub find_binaries {
347     my ($pkg, $suite) = @_;
348
349     tie my %src2bin, 'DB_File', "$DBDIR/sources_packages.db", O_RDONLY, 0666, $DB_BTREE
350         or die "couldn't open $DBDIR/sources_packages.db: $!";
351
352     my $bins = $src2bin{$pkg} || '';
353     my %bins;
354     foreach (split /\000/o, $bins) {
355         my @data = split /\s/, $_, 4;
356
357         if ($data[0] eq $suite) {
358             $bins{$data[1]}++;
359         }
360     }
361
362     return [ keys %bins ];
363 }
364
365 if ($searchon eq 'names') {
366     push @results, @{ do_names_search( $keyword, 'packages_small.db',
367                                        'package_postfixes.db',
368                                        \&read_entry, \%opts ) };
369 } elsif ($searchon eq 'sourcenames') {
370     push @results, @{ do_names_search( $keyword, 'sources_small.db',
371                                        'source_postfixes.db',
372                                        \&read_src_entry, \%opts ) };
373 } else {
374     push @results, @{ do_names_search( $keyword, 'packages_small.db',
375                                        'package_postfixes.db',
376                                        \&read_entry, \%opts ) };
377     push @results, @{ do_fulltext_search( $keyword, 'descriptions.txt',
378                                           'descriptions_packages.db',
379                                           'packages_small.db',
380                                           \&read_entry, \%opts ) };
381 }
382
383 my $st1 = new Benchmark;
384 my $std = timediff($st1, $st0);
385 debug( "Search took ".timestr($std) );
386
387 if ($format eq 'html') {
388     my $suite_wording = $suites_enc eq "all" ? "all suites"
389         : "suite(s) <em>$suites_enc</em>";
390     my $section_wording = $sections_enc eq 'all' ? "all sections"
391         : "section(s) <em>$sections_enc</em>";
392     my $arch_wording = $archs_enc eq 'any' ? "all architectures"
393         : "architecture(s) <em>$archs_enc</em>";
394     if (($searchon eq "names") || ($searchon eq 'sourcenames')) {
395         my $source_wording = $search_on_sources ? "source " : "";
396         my $exact_wording = $exact ? "named" : "that names contain";
397         msg( "You have searched for ${source_wording}packages $exact_wording <em>$keyword_enc</em> in $suite_wording, $section_wording, and $arch_wording." );
398     } else {
399         my $exact_wording = $exact ? "" : " (including subword matching)";
400         msg( "You have searched for <em>$keyword_enc</em> in packages names and descriptions in $suite_wording, $section_wording, and $arch_wording$exact_wording." );
401     }
402 }
403
404 if ($too_many_hits) {
405     error( "Your search was too wide so we will only display exact matches. At least <em>$too_many_hits</em> results have been omitted and will not be displayed. Please consider using a longer keyword or more keywords." );
406 }
407
408 if (!@results) {
409     if ($format eq 'html') {
410         my $keyword_esc = uri_escape( $keyword );
411         my $printed = 0;
412         if (($searchon eq "names") || ($searchon eq 'sourcenames')) {
413             if (($suites_enc eq 'all')
414                 && ($archs_enc eq 'any')
415                 && ($sections_enc eq 'all')) {
416                 error( "Can't find that package." );
417             } else {
418                 error( "Can't find that package, at least not in that suite ".
419                     ( $search_on_sources ? "" : " and on that architecture" ) )
420             }
421             
422             if ($exact) {
423                 hint( "You have searched only for exact matches of the package name. You can try to search for <a href=\"$thisscript?exact=0&amp;searchon=$searchon&amp;suite=$suites_param&amp;case=$case&amp;section=$sections_param&amp;keywords=$keyword_esc&amp;arch=$archs_param\">package names that contain your search string</a>." );
424             }
425         } else {
426             if (($suites_enc eq 'all')
427                 && ($archs_enc eq 'any')
428                 && ($sections_enc eq 'all')) {
429                 error( "Can't find that string." );
430             } else {
431                 error( "Can't find that string, at least not in that suite ($suites_enc, section $sections_enc) and on that architecture ($archs_enc)." );
432             }
433             
434             unless ($subword) {
435                 hint( "You have searched only for words exactly matching your keywords. You can try to search <a href=\"$thisscript?subword=1&amp;searchon=$searchon&amp;suite=$suites_param&amp;case=$case&amp;section=$sections_param&amp;keywords=$keyword_esc&amp;arch=$archs_param\">allowing subword matching</a>." );
436             }
437         }
438         hint( ( @hints ? "Or you" : "You" )." can try a different search on the <a href=\"$SEARCHPAGE#search_packages\">Packages search page</a>." );
439             
440     }
441 }
442
443 print_header;    
444 print_msgs;
445 print_errors;
446 print_hints;
447 print_debug;
448 &print_results;
449 &printfooter;
450
451 sub print_results {
452     return unless @results;
453
454     my (%pkgs, %sect, %part, %desc, %binaries);
455
456     unless ($search_on_sources) {
457         foreach (@results) {
458             my ($pkg_t, $suite, $arch, $section, $subsection,
459                 $priority, $version, $desc) = @$_;
460         
461             my ($pkg) = $pkg_t =~ m/^(.+)/; # untaint
462             $pkgs{$pkg}{$suite}{$version}{$arch} = 1;
463             $sect{$pkg}{$suite}{$version} = $subsection;
464             $part{$pkg}{$suite}{$version} = $section
465                 unless $section eq 'main';
466             
467             $desc{$pkg}{$suite}{$version} = $desc;
468         }
469
470         if ($format eq 'html') {
471             my ($start, $end) = multipageheader( scalar keys %pkgs );
472             my $count = 0;
473         
474             foreach my $pkg (sort keys %pkgs) {
475                 $count++;
476                 next if $count < $start or $count > $end;
477                 printf "<h3>Package %s</h3>\n", $pkg;
478                 print "<ul>\n";
479                 foreach my $suite (@SUITES) {
480                     if (exists $pkgs{$pkg}{$suite}) {
481                         my @versions = version_sort keys %{$pkgs{$pkg}{$suite}};
482                         my $part_str = "";
483                         if ($part{$pkg}{$suite}{$versions[0]}) {
484                             $part_str = "[<span style=\"color:red\">$part{$pkg}{$suite}{$versions[0]}</span>]";
485                         }
486                         printf "<li><a href=\"$ROOT/%s/%s\">%s</a> (%s): %s   %s\n",
487                         $suite, $pkg, $suite, $sect{$pkg}{$suite}{$versions[0]},
488                         $desc{$pkg}{$suite}{$versions[0]}, $part_str;
489                         
490                         foreach my $v (@versions) {
491                             printf "<br>%s: %s\n",
492                             $v, join (" ", (sort keys %{$pkgs{$pkg}{$suite}{$v}}) );
493                         }
494                         print "</li>\n";
495                     }
496                 }
497                 print "</ul>\n";
498             }
499         }
500     } else {
501         foreach (@results) {
502             my ($pkg, $suite, $section, $subsection, $priority,
503                 $version) = @$_;
504         
505             $pkgs{$pkg}{$suite} = $version;
506             $sect{$pkg}{$suite}{source} = $subsection;
507             $part{$pkg}{$suite}{source} = $section
508                 unless $section eq 'main';
509
510             $binaries{$pkg}{$suite} = find_binaries( $pkg, $suite );
511         }
512
513         if ($format eq 'html') {
514             my ($start, $end) = multipageheader( scalar keys %pkgs );
515             my $count = 0;
516             
517             foreach my $pkg (sort keys %pkgs) {
518                 $count++;
519                 next if ($count < $start) or ($count > $end);
520                 printf "<h3>Source package %s</h3>\n", $pkg;
521                 print "<ul>\n";
522                 foreach my $suite (@SUITES) {
523                     if (exists $pkgs{$pkg}{$suite}) {
524                         my $part_str = "";
525                         if ($part{$pkg}{$suite}{source}) {
526                             $part_str = "[<span style=\"color:red\">$part{$pkg}{$suite}{source}</span>]";
527                         }
528                         printf( "<li><a href=\"$ROOT/%s/source/%s\">%s</a> (%s): %s   %s",
529                                 $suite, $pkg, $suite, $sect{$pkg}{$suite}{source},
530                                 $pkgs{$pkg}{$suite}, $part_str );
531                         
532                         print "<br>Binary packages: ";
533                         my @bp_links;
534                         foreach my $bp (@{$binaries{$pkg}{$suite}}) {
535                             my $bp_link = sprintf( "<a href=\"$ROOT/%s/%s\">%s</a>",
536                                                    $suite, uri_escape( $bp ),  $bp );
537                             push @bp_links, $bp_link;
538                         }
539                         print join( ", ", @bp_links );
540                         print "</li>\n";
541                     }
542                 }
543                 print "</ul>\n";
544             }
545         }
546     }
547     printindexline( scalar keys %pkgs );
548 }
549
550 exit;
551
552 sub printindexline {
553     my $no_results = shift;
554
555     my $index_line;
556     if ($no_results > $opts{number}) {
557         
558         $index_line = prevlink($input,\%params)." | ".
559             indexline( $input, \%params, $no_results)." | ".
560             nextlink($input,\%params, $no_results);
561         
562         print "<p style=\"text-align:center\">$index_line</p>";
563     }
564 }
565
566 sub multipageheader {
567     my $no_results = shift;
568
569     my ($start, $end);
570     if ($opts{number} =~ /^all$/i) {
571         $start = 1;
572         $end = $no_results;
573         $opts{number} = $no_results;
574     } else {
575         $start = Packages::Search::start( \%params );
576         $end = Packages::Search::end( \%params );
577         if ($end > $no_results) { $end = $no_results; }
578     }
579
580     print "<p>Found <em>$no_results</em> matching packages,";
581     if ($end == $start) {
582         print " displaying package $end.</p>";
583     } else {
584         print " displaying packages $start to $end.</p>";
585     }
586
587     printindexline( $no_results );
588
589     if ($no_results > 100) {
590         print "<p>Results per page: ";
591         my @resperpagelinks;
592         for (50, 100, 200) {
593             if ($opts{number} == $_) {
594                 push @resperpagelinks, $_;
595             } else {
596                 push @resperpagelinks, resperpagelink($input,\%params,$_);
597             }
598         }
599         if ($params{values}{number}{final} =~ /^all$/i) {
600             push @resperpagelinks, "all";
601         } else {
602             push @resperpagelinks, resperpagelink($input, \%params,"all");
603         }
604         print join( " | ", @resperpagelinks )."</p>";
605     }
606     return ( $start, $end );
607 }
608
609 sub printfooter {
610
611     my $pete = new Benchmark;
612     my $petd = timediff($pete, $pet0);
613     print "Total page evaluation took ".timestr($petd)."<br>"
614         if $debug_allowed;
615
616     my $trailer = Packages::HTML::trailer( $ROOT );
617     $trailer =~ s/LAST_MODIFIED_DATE/gmtime()/e; #FIXME
618     print $trailer;
619 }
620
621 # vim: ts=8 sw=4