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