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