Find header references to help
[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                                           "header\\([^,]+,[^,]+,[^,]+,\\s*\"$page\"",
137                                           "header\\([^,]+,[^,]+,[^,]+,\\s*'$page'",
138                                         ], $m, 1);
139                                 push(@rv, { 'mod' => $m,
140                                             'rank' => 6,
141                                             'type' => 'help',
142                                             'link' => "help.cgi/$m->{'dir'}/$page",
143                                             'desc' => $title,
144                                             'text' => $help,
145                                             'cgis' => \@cgis });
146                                 }
147                         }
148                 &print_search_dot();
149                 }
150         closedir(DIR);
151         }
152
153 # Then do text strings
154 %gtext = &load_language("");
155 MODULE: foreach $m (@mods) {
156         %mtext = &load_language($m->{'dir'});
157         foreach $k (keys %mtext) {
158                 next if ($gtext{$k});   # Skip repeated global strings
159                 $mtext{$k} =~ s/\$[0-9]//g;
160                 if ($mtext{$k} =~ /\Q$re\E/i) {
161                         # Find CGIs that use this text
162                         my @cgis = &find_cgi_text(
163                                 [ "\$text{'$k'}",
164                                   "\$text{\"$k\"}",
165                                   "\$text{$k}",
166                                   "&text('$k'",
167                                   "&text(\"$k\"" ], $m);
168                         if (@cgis) {
169                                 push(@rv, { 'mod' => $m,
170                                             'rank' => 4,
171                                             'type' => 'text',
172                                             'text' => $mtext{$k},
173                                             'cgis' => \@cgis });
174                                 }
175                         }
176                 }
177         &print_search_dot();
178         }
179
180 print &text('wsearch_found', scalar(@rv)),"<p>\n";
181
182 # Sort results by relevancy
183 # XXX can do better?
184 @rv = sort { $b->{'rank'} <=> $a->{'rank'} } @rv;
185
186 # Show in table
187 if (@rv) {
188         print &ui_columns_start(
189                 [ $text{'wsearch_htext'}, $text{'wsearch_htype'},
190                   $text{'wsearch_hmod'}, $text{'wsearch_hcgis'} ], 100);
191         foreach my $r (@rv) {
192                 $hi = &highlight_text($r->{'text'});
193                 if ($r->{'link'}) {
194                         $hi = "<a href='$r->{'link'}'>$hi</a>";
195                         }
196                 @links = ( );
197                 foreach my $c (@{$r->{'cgis'}}) {
198                         ($cmod, $cpage) = split(/\//, $c);
199                         ($cpage, $cargs) = split(/\?/, $cpage);
200                         $ctitle = &cgi_page_title($cmod, $cpage) || $cpage;
201                         if ($r->{'mod'}->{'installed'}) {
202                                 $cargs ||= &cgi_page_args($cmod, $cpage);
203                                 }
204                         else {
205                                 # For modules that aren't installed, linking
206                                 # to a CGI is likely useless
207                                 $cargs ||= "none";
208                                 }
209                         if ($cargs eq "none") {
210                                 push(@links, $ctitle);
211                                 }
212                         else {
213                                 push(@links,
214                                    "<a href='$cmod/$cpage?$cargs'>$ctitle</a>");
215                                 }
216                         }
217                 if (@links > 2) {
218                         @links = ( @links[0..1], "..." );
219                         }
220                 print &ui_columns_row([
221                         $hi,
222                         $text{'wsearch_type_'.$r->{'type'}},
223                         "<a href='$r->{'mod'}->{'dir'}/'>$r->{'mod'}->{'desc'}</a>",
224                         &ui_links_row(\@links),
225                         ]);
226                 }
227         print &ui_columns_end();
228         }
229 else {
230         print "<b>",&text('wsearch_enone',
231                 "<tt>".&html_escape($re)."</tt>"),"</b><p>\n";
232         }
233
234 &ui_print_footer();
235
236 # highlight_text(text, [length])
237 # Returns text with the search term bolded, and truncated to 60 characters
238 sub highlight_text
239 {
240 local ($str, $len) = @_;
241 $len ||= 50;
242 local $hlen = $len / 2;
243 $str =~ s/<[^>]*>//g;
244 if ($str =~ /(.*)(\Q$re\E)(.*)/i) {
245         local ($before, $match, $after) = ($1, $2, $3);
246         if (length($before) > $hlen) {
247                 $before = "...".substr($before, length($before)-$hlen);
248                 }
249         if (length($after) > $hlen) {
250                 $after = substr($after, 0, $hlen)."...";
251                 }
252         $str = $before."<b>".&html_escape($match)."</b>".$after;
253         }
254 return $str;
255 }
256
257 # find_cgi_text(&regexps, module, re-mode)
258 # Returns the relative URLs of CGIs that matches some regexps, in the given
259 # module. Does not include those that don't call some header function, as
260 # they cannot be linked to normally
261 sub find_cgi_text
262 {
263 local ($res, $m, $remode) = @_;
264 local $mdir = &module_root_directory($m);
265 local @rv;
266 foreach my $f (glob("$mdir/*.cgi")) {
267         local $found = 0;
268         local $header = 0;
269         open(CGI, $f);
270         LINE: while(my $line = <CGI>) {
271                 if ($line =~ /(header|ui_print_header|ui_print_unbuffered_header)\(/) {
272                         $header++;
273                         }
274                 foreach my $r (@$res) {
275                         if (!$remode && index($line, $r) >= 0 ||
276                             $remode && $line =~ /$r/) {
277                                 $found++;
278                                 last LINE;
279                                 }
280                         }
281                 }
282         close(CGI);
283         if ($found && $header) {
284                 local $url = $f;
285                 $url =~ s/^\Q$root_directory\E\///;
286                 push(@rv, $url);
287                 }
288         }
289 return @rv;
290 }
291
292 # help_file_match(file)
293 # Returns the title if some help file matches the current search
294 sub help_file_match
295 {
296 local ($f) = @_;
297 local $data = &read_file_contents($f);
298 local $title;
299 if ($data =~ /<header>([^<]*)<\/header>/) {
300         $title = $1;
301         }
302 $data =~ s/\s+/ /g;
303 $data =~ s/<p>/\n\n/gi;
304 $data =~ s/<br>/\n/gi;
305 $data =~ s/<[^>]+>//g;
306 if ($data =~ /\Q$re\E/i) {
307         return ($title, $data);
308         }
309 return ( );
310 }
311
312 # cgi_page_title(module, cgi)
313 # Given a CGI, return the text for its page title, if possible
314 sub cgi_page_title
315 {
316 local ($m, $cgi) = @_;
317 local $data = &read_file_contents(&module_root_directory($m)."/".$cgi);
318 local $rv;
319 if ($data =~ /(ui_print_header|ui_print_unbuffered_header)\([^,]+,[^,]*(\$text{'([^']+)'|\$text{"([^"]+)"|\&text\('([^']+)'|\&text\("([^"]+)")/) {
320         # New header function, with arg before title
321         local $msg = $3 || $4 || $5 || $6;
322         local %mtext = &load_language($m);
323         $rv = $mtext{$msg};
324         }
325 elsif ($data =~ /(^|\s)header\(\s*(\$text{'([^']+)'|\$text{"([^"]+)"|\&text\('([^']+)'|\&text\("([^"]+)")/) {
326         # Old header function
327         local $msg = $3 || $4 || $5 || $6;
328         local %mtext = &load_language($m);
329         $rv = $mtext{$msg};
330         }
331 if ($cgi eq "index.cgi" && !$rv) {
332         # If no title was found for an index.cgi, use module title
333         local %minfo = &get_module_info($m);
334         $rv = $minfo{'desc'};
335         }
336 return $rv;
337 }
338
339 # cgi_page_args(module, cgi)
340 # Given a module and CGI name, returns a string of URL parameters that can be
341 # used for linking to it. Returns "none" if parameters are needed, but cannot
342 # be determined.
343 sub cgi_page_args
344 {
345 local ($m, $cgi) = @_;
346 local $mroot = &module_root_directory($m);
347 if (-r "$mroot/cgi_args.pl") {
348         # Module can tell us what args to use
349         &foreign_require($m, "cgi_args.pl");
350         $args = &foreign_call($m, "cgi_args", $cgi);
351         if (defined($args)) {
352                 return $args;
353                 }
354         }
355 if ($cgi eq "index.cgi") {
356         # Index page is always safe to link to
357         return undef;
358         }
359 # Otherwise check if it appears to parse any args
360 local $data = &read_file_contents($mroot."/".$cgi);
361 if ($data =~ /(ReadParse|ReadParseMime)\(/) {
362         return "none";
363         }
364 return undef;
365 }
366
367 # print_search_dot()
368 # Print one dot per second
369 sub print_search_dot
370 {
371 local $now = time();
372 if ($now > $last_print_search_dot) {
373         print ". ";
374         $last_print_search_dot = $now;
375         }
376 }
377