Show search dots
[webmin.git] / webmin_search.cgi
1 #!/usr/local/bin/perl
2 # Search Webmin modules and help pages and text and config.info
3
4 BEGIN { push(@INC, ".."); };
5 use WebminCore;
6
7 &init_config();
8 &ReadParse();
9
10 $prod = &get_product_name();
11 $ucprod = ucfirst($prod);
12 &ui_print_unbuffered_header(
13         undef, &text('wsearch_title', $ucprod), "", undef, 0, 1);
14
15 # Validate search text
16 $re = $in{'search'};
17 if ($re !~ /\S/) {
18         &error($text{'wsearch_esearch'});
19         }
20 $re =~ s/^\s+//;
21 $re =~ s/\s+$//;
22
23 # Work out this Webmin's URL base
24 $urlhost = $ENV{'HTTP_HOST'};
25 if ($urlhost !~ /:/) {
26         $urlhost .= ":".$ENV{'SERVER_PORT'};
27         }
28 $urlbase = ($ENV{'HTTPS'} eq 'ON' ? 'https://' : 'http://').$urlhost;
29
30 # Start printing dots 
31 print &text('wsearch_searching', "<i>".&html_escape($re)."</i>"),"\n";
32
33 # Search module names and add to results list
34 @rv = ( );
35 @mods = sort { $b->{'longdesc'} cmp $a->{'longdesc'} }
36              grep { !$_->{'clone'} } &get_available_module_infos();
37 foreach $m (@mods) {
38         if ($m->{'desc'} =~ /\Q$re\E/i) {
39                 # Module description match
40                 push(@rv, { 'mod' => $m,
41                             'rank' => 10,
42                             'type' => 'mod',
43                             'link' => $m->{'dir'}.'/',
44                             'text' => $m->{'desc'} });
45                 }
46         elsif ($m->{'dir'} =~ /\Q$re\E/i) {
47                 # Module directory match
48                 push(@rv, { 'mod' => $m,
49                             'rank' => 12,
50                             'type' => 'dir',
51                             'link' => $m->{'dir'}.'/',
52                             'text' => $urlbase."/".$m->{'dir'}."/" });
53                 }
54         &print_search_dot();
55         }
56
57 # Search module configs and their help pages
58 foreach $m (@mods) {
59         %access = &get_module_acl(undef, $m);
60         next if ($access{'noconfig'});
61         $file = $prod eq 'webmin' ? "$m->{'dir'}/config.info"
62                                   : "$m->{'dir'}/uconfig.info";
63         %info = ( );
64         @info_order = ( );
65         &read_file($file, \%info, \@info_order);
66         foreach $o (@lang_order_list) {
67                 &read_file("$file.$o", \%info);
68                 }
69         $section = undef;
70         foreach $c (@info_order) {
71                 @p = split(/,/, $info{$c});
72                 if ($p[1] == 11) {
73                         $section = $c;
74                         }
75                 if ($p[0] =~ /\Q$re\E/i) {
76                         # Config description matches
77                         push(@rv, { 'mod' => $m,
78                                     'rank' => 8,
79                                     'type' => 'config',
80                                     'link' => "config.cgi?module=$m->{'dir'}&".
81                                              "section=".&urlize($section)."#$c",
82                                     'text' => $p[0],
83                                   });
84                         }
85                 $hfl = &help_file($mod->{'dir'}, "config_".$c);
86                 ($title, $help) = &help_file_match($hfl);
87                 if ($help) {
88                         # Config help matches
89                         push(@rv, { 'mod' => $m,
90                                     'rank' => 6,
91                                     'type' => 'help',
92                                     'link' => "help.cgi/$m->{'dir'}/config_".$c,
93                                     'desc' => &text('wsearch_helpfor', $p[0]),
94                                     'text' => $help,
95                                     'cgis' => [ "/config.cgi?".
96                                                 "module=$m->{'dir'}&section=".
97                                                 &urlize($section)."#$c" ],
98                                    });
99                         }
100                 }
101         &print_search_dot();
102         }
103
104 # Search other help pages
105 %lang_order_list = map { $_, 1 } @lang_order_list;
106 foreach $m (@mods) {
107         $helpdir = &module_root_directory($m->{'dir'})."/help";
108         %donepage = ( );
109         opendir(DIR, $helpdir);
110         foreach $f (sort { length($b) <=> length($a) } readdir(DIR)) {
111                 next if ($f =~ /^config_/);     # For config help, already done
112
113                 # Work out if we should grep this help page - don't do the same
114                 # page twice for different languages
115                 $grep = 0;
116                 if ($f =~ /^(\S+)\.([^\.]+)\.html$/) {
117                         ($page, $lang) = ($1, $2);
118                         if ($lang_order_list{$lang} && !$donepage{$page}++) {
119                                 $grep = 1;
120                                 }
121                         }
122                 elsif ($f =~ /^(\S+)\.html$/) {
123                         $page = $1;
124                         if (!$donepage{$page}++) {
125                                 $grep = 1;
126                                 }
127                         }
128
129                 # If yes, search it
130                 if ($grep) {
131                         ($title, $help) = &help_file_match("$helpdir/$f");
132                         if ($title) {
133                                 my @cgis = &find_cgi_text(
134                                         [ "hlink\\(.*'$page'",
135                                           "hlink\\(.*\"$page\"",
136                                         ], $m, 1);
137                                 push(@rv, { 'mod' => $m,
138                                             'rank' => 6,
139                                             'type' => 'help',
140                                             'link' => "help.cgi/$m->{'dir'}/$page",
141                                             'desc' => $title,
142                                             'text' => $help,
143                                             'cgis' => \@cgis });
144                                 }
145                         }
146                 &print_search_dot();
147                 }
148         closedir(DIR);
149         }
150
151 # Then do text strings
152 %gtext = &load_language("");
153 MODULE: foreach $m (@mods) {
154         %mtext = &load_language($m->{'dir'});
155         foreach $k (keys %mtext) {
156                 next if ($gtext{$k});   # Skip repeated global strings
157                 $mtext{$k} =~ s/\$[0-9]//g;
158                 if ($mtext{$k} =~ /\Q$re\E/i) {
159                         # Find CGIs that use this text
160                         my @cgis = &find_cgi_text(
161                                 [ "\$text{'$k'}",
162                                   "\$text{\"$k\"}",
163                                   "\$text{$k}",
164                                   "&text('$k'",
165                                   "&text(\"$k\"" ], $m);
166                         if (@cgis) {
167                                 push(@rv, { 'mod' => $m,
168                                             'rank' => 4,
169                                             'type' => 'text',
170                                             'text' => $mtext{$k},
171                                             'cgis' => \@cgis });
172                                 }
173                         }
174                 }
175         &print_search_dot();
176         }
177
178 print &text('wsearch_found', scalar(@rv)),"<p>\n";
179
180 # Sort results by relevancy
181 # XXX can do better?
182 @rv = sort { $b->{'rank'} <=> $a->{'rank'} } @rv;
183
184 # Show in table
185 if (@rv) {
186         print &ui_columns_start(
187                 [ $text{'wsearch_htext'}, $text{'wsearch_htype'},
188                   $text{'wsearch_hmod'}, $text{'wsearch_hcgis'} ], 100);
189         foreach my $r (@rv) {
190                 $hi = &highlight_text($r->{'text'});
191                 if ($r->{'link'}) {
192                         $hi = "<a href='$r->{'link'}'>$hi</a>";
193                         }
194                 @links = ( );
195                 foreach my $c (@{$r->{'cgis'}}) {
196                         ($cmod, $cpage) = split(/\//, $c);
197                         ($cpage, $cargs) = split(/\?/, $cpage);
198                         $ctitle = &cgi_page_title($cmod, $cpage) || $cpage;
199                         if ($r->{'mod'}->{'installed'}) {
200                                 $cargs ||= &cgi_page_args($cmod, $cpage);
201                                 }
202                         else {
203                                 # For modules that aren't installed, linking
204                                 # to a CGI is likely useless
205                                 $cargs ||= "none";
206                                 }
207                         if ($cargs eq "none") {
208                                 push(@links, $ctitle);
209                                 }
210                         else {
211                                 push(@links,
212                                    "<a href='$cmod/$cpage?$cargs'>$ctitle</a>");
213                                 }
214                         }
215                 if (@links > 2) {
216                         @links = ( @links[0..1], "..." );
217                         }
218                 print &ui_columns_row([
219                         $hi,
220                         $text{'wsearch_type_'.$r->{'type'}},
221                         "<a href='$r->{'mod'}->{'dir'}/'>$r->{'mod'}->{'desc'}</a>",
222                         &ui_links_row(\@links),
223                         ]);
224                 }
225         print &ui_columns_end();
226         }
227 else {
228         print "<b>",&text('wsearch_enone',
229                 "<tt>".&html_escape($re)."</tt>"),"</b><p>\n";
230         }
231
232 &ui_print_footer();
233
234 # highlight_text(text, [length])
235 # Returns text with the search term bolded, and truncated to 60 characters
236 sub highlight_text
237 {
238 local ($str, $len) = @_;
239 $len ||= 50;
240 local $hlen = $len / 2;
241 $str =~ s/<[^>]*>//g;
242 if ($str =~ /(.*)(\Q$re\E)(.*)/i) {
243         local ($before, $match, $after) = ($1, $2, $3);
244         if (length($before) > $hlen) {
245                 $before = "...".substr($before, length($before)-$hlen);
246                 }
247         if (length($after) > $hlen) {
248                 $after = substr($after, 0, $hlen)."...";
249                 }
250         $str = $before."<b>".&html_escape($match)."</b>".$after;
251         }
252 return $str;
253 }
254
255 # find_cgi_text(&regexps, module, re-mode)
256 # Returns the relative URLs of CGIs that matches some regexps, in the given
257 # module. Does not include those that don't call some header function, as
258 # they cannot be linked to normally
259 sub find_cgi_text
260 {
261 local ($res, $m, $remode) = @_;
262 local $mdir = &module_root_directory($m);
263 local @rv;
264 foreach my $f (glob("$mdir/*.cgi")) {
265         local $found = 0;
266         local $header = 0;
267         open(CGI, $f);
268         LINE: while(my $line = <CGI>) {
269                 if ($line =~ /(header|ui_print_header|ui_print_unbuffered_header)\(/) {
270                         $header++;
271                         }
272                 foreach my $r (@$res) {
273                         if (!$remode && index($line, $r) >= 0 ||
274                             $remode && $line =~ /$r/) {
275                                 $found++;
276                                 last LINE;
277                                 }
278                         }
279                 }
280         close(CGI);
281         if ($found && $header) {
282                 local $url = $f;
283                 $url =~ s/^\Q$root_directory\E\///;
284                 push(@rv, $url);
285                 }
286         }
287 return @rv;
288 }
289
290 # help_file_match(file)
291 # Returns the title if some help file matches the current search
292 sub help_file_match
293 {
294 local ($f) = @_;
295 local $data = &read_file_contents($f);
296 local $title;
297 if ($data =~ /<header>([^<]*)<\/header>/) {
298         $title = $1;
299         }
300 else {
301         $title = $f;
302         }
303 $data =~ s/\s+/ /g;
304 $data =~ s/<p>/\n\n/gi;
305 $data =~ s/<br>/\n/gi;
306 $data =~ s/<[^>]+>//g;
307 if ($data =~ /\Q$re\E/i) {
308         return ($title, $data);
309         }
310 return ( );
311 }
312
313 # cgi_page_title(module, cgi)
314 # Given a CGI, return the text for its page title, if possible
315 sub cgi_page_title
316 {
317 local ($m, $cgi) = @_;
318 local $data = &read_file_contents(&module_root_directory($m)."/".$cgi);
319 local $rv;
320 if ($data =~ /(header|ui_print_header|ui_print_unbuffered_header)\([^,]+,\s*(\$text{'([^']+)'|\$text{"([^"]+)"|\&text\('([^']+)'|\&text\("([^"]+)")/) {
321         local $msg = $3 || $4 || $5 || $6;
322         local %mtext = &load_language($m);
323         $rv = $mtext{$msg};
324         }
325 if ($cgi eq "index.cgi" && !$rv) {
326         # If no title was found for an index.cgi, use module title
327         local %minfo = &get_module_info($m);
328         $rv = $minfo{'desc'};
329         }
330 return $rv;
331 }
332
333 # cgi_page_args(module, cgi)
334 # Given a module and CGI name, returns a string of URL parameters that can be
335 # used for linking to it. Returns "none" if parameters are needed, but cannot
336 # be determined.
337 sub cgi_page_args
338 {
339 local ($m, $cgi) = @_;
340 local $mroot = &module_root_directory($m);
341 if (-r "$mroot/cgi_args.pl") {
342         # Module can tell us what args to use
343         &foreign_require($m, "cgi_args.pl");
344         $args = &foreign_call($m, "cgi_args", $cgi);
345         if (defined($args)) {
346                 return $args;
347                 }
348         }
349 # Guess if any are needed
350 if ($cgi eq "index.cgi") {
351         return undef;
352         }
353 local $data = &read_file_contents($mroot."/".$cgi);
354 if ($data =~ /ReadParse\(/) {
355         return "none";
356         }
357 return undef;
358 }
359
360 # print_search_dot()
361 # Print one dot per second
362 sub print_search_dot
363 {
364 local $now = time();
365 if ($now > $last_print_search_dot) {
366         print ". ";
367         $last_print_search_dot = $now;
368         }
369 }
370