Handle hostnames with upper-case letters
[webmin.git] / man / search.cgi
1 #!/usr/local/bin/perl
2 # search.cgi
3 # Search for manual pages, and display a list of matches or an exact page
4
5 require './man-lib.pl';
6 use Config;
7 &ReadParse();
8 $in{'for'} || &error($text{'search_efor'});
9
10 @for = split(/\s+/, $in{'for'});
11 @howto = split(/\s+/, $config{'howto_dir'});
12 @doc = split(/\s+/, $config{'doc_dir'});
13 foreach $s (split(/\0/, $in{'section'})) {
14         if ($s =~ /^([^:]+):(.*)/) {
15                 $section{$1}++;
16                 $opts{$1} = $2;
17                 }
18         else {
19                 $section{$s}++;
20                 }
21         }
22 if ($in{'check'} && $config{'check'}) {
23         @check = split(/\s+/, $config{'check'});
24         foreach $s (keys %section) {
25                 delete($section{$s}) if (&indexof($s, @check) < 0);
26                 }
27         }
28
29 if ($section{'doc'}) {
30         # Look in the system documentation directory (usually /usr/doc)
31         foreach $d (@doc) {
32                 push(@rv, map { [ $text{'search_doc'},
33                                    "view_doc.cgi?file=".&urlize($_->[0]),
34                                    substr($_->[0], length($d)+1),
35                                    $_->[1], 1 ] }
36                              &find_contents($d, \@for,
37                                             $howto[0], 1, $in{'exact'}));
38                 }
39         }
40 if ($section{'custom'}) {
41         # Look in the custom documentation directory
42         push(@rv, map { [ $text{'search_custom'},
43                            "view_doc.cgi?file=".&urlize($_->[0]),
44                            substr($_->[0], length($config{'custom_dir'})+1),
45                            $_->[1], 1 ] }
46                      &find_contents($config{'custom_dir'}, \@for,
47                                     $howto[0], 1, $in{'exact'}));
48         }
49 if ($section{'howto'}) {
50         # Look in the HOWTO directory
51         foreach $h (@howto) {
52                 push(@rv, map { [ $text{'search_howto'},
53                               "view_howto.cgi?file=".&urlize($_->[0]),
54                               $_->[2], $_->[1], 2 ] }
55                      &find_contents($h, \@for, undef, 0, $in{'exact'}));
56                 }
57         @rv = grep { $_->[2] !~ /^index/i } @rv;
58         }
59 if ($section{'kernel'}) {
60         # Look in the linux kernel Documentation directory
61          push(@rv, map { [ $text{'search_kernel'},
62                            "view_kernel.cgi?file=".&urlize($_->[0]),
63                            substr($_->[0], length($config{'kernel_dir'})+1),
64                            $_->[1], 1 ] }
65                      &find_contents($config{'kernel_dir'}, \@for,
66                                     undef, 1, $in{'exact'}));
67         }
68 if ($section{'kde'}) {
69         # Look in the KDE documentation directory
70          push(@rv, map { [ $text{'search_kde'},
71                            "view_kde.cgi?file=".&urlize($_->[0]),
72                            substr($_->[0], length($config{'kde_dir'})+1),
73                            $_->[1], 1 ] }
74                      &find_contents($config{'kde_dir'}, \@for,
75                                     undef, 1, $in{'exact'}));
76         }
77 if ($section{'perl'}) {
78         if ($in{'exact'}) {
79                 # Check for an exact module name match
80                 local @f = $in{'and'} ? ( $for[0] ) : @for;
81                 foreach $f (@f) {
82                         chop($out = &backquote_command("$perl_doc -l ".quotemeta($f)." 2>/dev/null", 1));
83                         if ($out) {
84                                 local $doc = &parse_perl_module($out);
85                                 $doc->{'name'} =~ s/^\s*(\S+)\s+-\s+//;
86                                 push(@rv, [ $text{'search_perl'},
87                                             "view_perl.cgi?mod=$f",
88                                             $f, $doc->{'name'}, 1 ]);
89                                 }
90                         }
91                 }
92         else {
93                 # Search the text of all perl modules
94                 foreach $d ($Config{'sitelib'}, $Config{'privlib'}) {
95                         &open_execute_command(FIND, "find $d -name '*.pm' -print", 1, 1);
96                         while($path = <FIND>) {
97                                 chop($path);
98                                 local $doc = &parse_perl_module($path);
99                                 local ($any = 0, $all = 1);
100                                 foreach $f (@for) {
101                                         if ($doc->{'name'} !~ /$f/i &&
102                                             $doc->{'description'} !~ /$f/i) {
103                                                 $all = 0;
104                                                 }
105                                         else {
106                                                 $any = 1;
107                                                 }
108                                         }
109                                 next if (!$all && $in{'and'} || !$any);
110
111                                 $doc->{'name'} =~ s/^\s*(\S+)\s+-\s+//;
112                                 local $modfile =
113                                    &backquote_command("$perl_doc -l ".quotemeta($doc->{'package'})." 2>/dev/null", 1);
114                                 if ($doc->{'package'} && $modfile) {
115                                         push(@rv, [ $text{'search_perl'},
116                                           "view_perl.cgi?mod=$doc->{'package'}",
117                                           $doc->{'package'}, $doc->{'name'},
118                                           1 ]);
119                                         }
120                                 }
121                         close(FIND);
122                         }
123                 }
124         }
125 if ($section{'help'}) {
126         # Look in the webmin module help pages
127         opendir(DIR, $root_directory);
128         foreach my $m (readdir(DIR)) {
129                 # Is this a module with help
130                 local $dir = "$root_directory/$m/help";
131                 next if (!-d $dir || $m =~ /^\./ || -l "$root_directory/$m");
132                 local %minfo = &get_module_info($m);
133                 next if (!%minfo || !&check_os_support(\%minfo));
134
135                 # Check the help pages
136                 local @pfx;
137                 opendir(DIR2, $dir);
138                 while($f = readdir(DIR2)) {
139                         push(@pfx, $1) if ($f =~ /^([^\.]+)\.html$/);
140                         }
141                 closedir(DIR2);
142                 HELP: foreach $p (&unique(@pfx)) {
143                         local $file = &help_file($m, $p);
144                         open(HELP, $file);
145                         local @st = stat($file);
146                         read(HELP, $help, $st[7]);
147                         close(HELP);
148                         if ($help =~ /<header>([^<]+)<\/header>/) {
149                                 $header = $1;
150                                 }
151                         else { next; }
152                         $help =~ s/<include\s+(\S+)>/inchelp($1, $m)/ge;
153                         $help =~ s/<[^>]+>//g;
154                         local $matches = 0;
155                         if ($in{'exact'}) {
156                                 # Just check header
157                                 foreach $f (@for) {
158                                         $matches++ if ($header =~ /\Q$f\E/i);
159                                         }
160                                 }
161                         else {
162                                 # Check entire body
163                                 foreach $f (@for) {
164                                         $matches++ if ($help =~ /\Q$f\E/i);
165                                         }
166                                 }
167                         if (($in{'and'} && $matches == @for) ||
168                             (!$in{'and'} && $matches)) {
169                                 push(@rv, [ $text{'search_help'},
170                                             "/help.cgi/$m/$p?x=1",
171                                             "$m/$p", $header,
172                                             2 ]);
173                                 }
174                         }
175                 }
176         }
177 if ($section{'man'}) {
178         # Look in manual pages (searches are never exact)
179         $cmd = $config{'search_cmd'};
180         map { s/\\/\\\\/g; s/'/\\'/g; } @for;
181         if ($in{'and'}) {
182                 local $qm = quotemeta($for[0]);
183                 $cmd =~ s/PAGE/$qm/;
184                 }
185         else {
186                 local $fors = join(" ", map { quotemeta($_) } @for);
187                 $cmd =~ s/PAGE/$fors/;
188                 }
189         &set_manpath($opts{'man'});
190         &open_execute_command(MAN, $cmd, 1, 1);
191         while(<MAN>) {
192                 $got .= $_;
193                 if (/(([^,\s]+).*)\s*\((\S+)\)\s+-\s+(.*)/ &&
194                     !$done{$2,$3}++) {
195                         local ($page, $sect, $desc) = ($1, $3, $4);
196                         if ($page =~ /^(\S+)\s*\[(.+)\]/) {
197                                 $page = "$1 $2";
198                                 }
199                         local @pp = split(/[\s+,]/, $page);
200                         map { s/\((\S+)\)//; } @pp;
201
202                         # Keywords must be page name or desc
203                         local ($any = 0, $all = 1, $exact);
204                         foreach $f (@for) {
205                                 if ($desc !~ /$f/i && $page !~ /$f/i &&
206                                     &indexof($f, @pp) < 0) {
207                                         $all = 0;
208                                         }
209                                 else {
210                                         $any = 1;
211                                         }
212                                 $exact++ if (&indexof($f, @pp) >= 0);
213                                 }
214                         next if (!$all && $in{'and'} || !$any);
215
216                         push(@rv, [ $text{'search_man'},
217                                     "view_man.cgi?page=$pp[0]&sec=$3&opts=".
218                                     $opts{'man'}, "$pp[0] ($sect)", $desc,
219                                     $exact ? 4 : 3 ]);
220                         }
221                 }
222         close(MAN);
223         }
224 if ($section{'google'}) {
225         # Try to call the Google search engine
226         local ($grv, $error);
227         local $j = $in{'and'} ? ' and ' : ' or ';
228         &http_download($google_host, $google_port, "$google_page?q=".
229                        &urlize(join($j, @for))."&sourceid=webmin&num=20",
230                        \$grv, \$error);
231         if (!$error) {
232                 # Parse the results
233                 while($grv =~ /(<p[^>]*>|<div[^>]*>)<a[^>]+href=([^>]+)>([\000-\377]+?)<\/a>([\000-\377]*)$/i) {
234                         $grv = $4;
235                         local ($url = $2, $desc = $3);
236                         $url =~ s/^"(.*)".*$/$1/;
237                         $url =~ s/^'(.*)'.*$/$1/;
238                         $desc =~ s/<\/?b>//g;
239                         local $matches = 0;
240                         foreach $f (@for) {
241                                 $matches++ if ($desc =~ /\Q$f\E/i);
242                                 }
243                         if (!$in{'exact'} ||
244                             ($in{'and'} && $matches == @for) ||
245                             (!$in{'and'} && $matches)) {
246                                 push(@rv, [ $text{'search_google'}, $url, length($url) > 60 ? substr($url, 0, 60)."..." : $url, $desc, 0.5 ]);
247                                 }
248                         }
249                 }
250         }
251
252 if (@rv == 1 && !$in{'check'}) {
253         # redirect to the exact page
254         &redirect($in{'exact'} ? $rv[0]->[1]
255                                : "$rv[0]->[1]&for=".&urlize($in{'for'}));
256         exit;
257         }
258
259 # Display search results
260 $for = join($in{'and'} ? " and " : " or ", map { "<tt>$_</tt>" } @for);
261 &ui_print_header(&text('search_for', $for), $text{'search_title'}, "");
262 if (@rv) {
263         #@rv = sort { $b->[4] <=> $a->[4] } @rv;
264         @rv = sort { &ranking($b) <=> &ranking($a) } @rv;
265         print &ui_columns_start([ $text{'search_file'},
266                                   $text{'search_type'},
267                                   $text{'search_desc'} ], 100);
268         foreach $r (@rv) {
269                 local @cols;
270                 if ($r->[1] =~ /^(http|ftp|https):/) {
271                         push(@cols, "<a href='$r->[1]'>".
272                                     &html_escape($r->[2])."</a>");
273                         }
274                 else {
275                         push(@cols, "<a href='$r->[1]&for=".&urlize($in{'for'}).
276                                     "'>".&html_escape($r->[2])."</a>");
277                         }
278                 push(@cols, $r->[0]);
279                 push(@cols, &html_escape($r->[3]));
280                 print &ui_columns_row(\@cols, [ undef, "nowrap", undef ]);
281                 }
282         print &ui_columns_end();
283         }
284 else {
285         print "<p><b>",&text('search_none', "<tt>$in{'for'}</tt>"),"</b><p>\n";
286         }
287
288 &ui_print_footer("", $text{'index_return'});
289
290 # find_contents(directory, &strings, [exclude], [descend], [nameonly])
291 # Find some string in a directory of files
292 sub find_contents
293 {
294 opendir(DIR, $_[0]);
295 local @f = readdir(DIR);
296 closedir(DIR);
297 local @rv;
298 foreach $f (@f) {
299         next if ($f =~ /^\./);
300         local $p = "$_[0]/$f";
301         next if ($p eq $_[2]);
302         if (-d $p) {
303                 # go into subdirectory
304                 push(@rv, &find_contents($p, $_[1], $_[2], $_[3], $_[4]))
305                         if ($_[3]);
306                 }
307         else {
308                 # Skip non-text or HTML files
309                 local $ff = $f;
310                 $ff =~ s/\.(gz|bz|bz2)$//i;
311                 next if ($ff !~ /\.(txt|htm|html|doc)$/ &&
312                          $ff =~ /\.[A-Za-z0-9]+$/);
313                 next if ($ff =~ /(^makefile$)|(^core$)/i);
314
315                 local $matches = 0;
316                 foreach $s (@{$_[1]}) {
317                         $matches++ if ($p =~ /\Q$s\E/i);
318                         }
319                 if ($_[4]) {
320                         # just compare filename
321                         if ($in{'and'} && $matches == @{$_[1]} ||
322                             !$in{'and'} && $matches) {
323                                 local ($desc, $data) = &read_doc_file($p);
324                                 if ($desc !~ /^#!/ && $desc !~ /^#\%/) {
325                                         push(@rv, [ $p, $desc, $f, $matches ]);
326                                         }
327                                 }
328                         }
329                 else {
330                         # compare file contents
331                         local ($desc, $data) = &read_doc_file($p);
332                         local $dmatches = 0;
333                         foreach $s (@{$_[1]}) {
334                                 $dmatches++ if ($data =~ /\Q$s\E/i);
335                                 }
336                         if (($in{'and'} && $dmatches == @{$_[1]} ||
337                              !$in{'and'} && $dmatches) &&
338                             $desc !~ /^#!/ && $desc !~ /^#\%/) {
339                                 push(@rv, [ $p, $desc, $f, $matches ]);
340                                 }
341                         }
342                 }
343         }
344 return @rv;
345 }
346
347 # read_doc_file(filename)
348 # Returns desc, data
349 sub read_doc_file
350 {
351 local ($two, $first, $title, $data);
352 open(FILE, $_[0]);
353 read(FILE, $two, 2);
354 local $qm = quotemeta($_[0]);
355 if ($two eq "\037\213") {
356         close(FILE);
357         &open_execute_command(FILE, "gunzip -c $qm", 1, 1);
358         }
359 elsif ($two eq "BZ") {
360         close(FILE);
361         &open_execute_command(FILE, "bunzip2 -c $qm", 1, 1);
362         }
363 seek(FILE, 0, 0);
364 while(<FILE>) {
365         $data .= $_;
366         if (/[A-Za-z0-9]/ && !/\$\S+:/ && !$first) {
367                 chop($first = $_);
368                 $first =~ s/.\010//g;
369                 }
370         }
371 close(FILE);
372 if ($data =~ /<\s*title\s*>([\000-\177]{0,200})<\s*\/\s*title\s*>/i) {
373         $title = $1;
374         }
375 return ($title ? $title : $first =~ /<.*>/ ? undef : $first, $data);
376 }
377
378 # parse_perl_module(file)
379 sub parse_perl_module
380 {
381 local (%doc, $inside);
382 open(MOD, $_[0]);
383 while(<MOD>) {
384         if (/^\s*package\s+(\S+)\s*;/ && !$doc{'package'}) {
385                 $doc{'package'} = $1;
386                 }
387         elsif (/^=head1\s+(\S+)/i) {
388                 $inside = $1;
389                 }
390         elsif (/^=cut/i) {
391                 undef($inside);
392                 }
393         elsif ($inside) {
394                 $doc{lc($inside)} .= $_;
395                 }
396         }
397 close(MOD);
398 return \%doc;
399 }
400
401 # inchelp(path, module)
402 sub inchelp
403 {
404 local $inc;
405 local $ipath = &help_file($_[1], $_[0]);
406 open(INC, $ipath) || return "<i>".&text('search_einclude', $_[0])."</i><br>\n";
407 local @st = stat(INC);
408 read(INC, $inc, $st[7]);
409 close(INC);
410 return $inc;
411 }
412
413 sub ranking
414 {
415 local ($name = 0, $desc = 0);
416 foreach $f (@for) {
417         $desc++ if ($_[0]->[3] =~ /$f/i);
418         $name++ if ($_[0]->[1] =~ /$f/i);
419         }
420 return $name ? $_[0]->[4] * 10 :
421        $desc ? $_[0]->[4] :
422                $_[0]->[4] / 10;
423 }
424