1 # Functions for searching the webmin docs and UI
3 =head2 search_webmin(phrase, [callback-function])
5 Searches all Webmin help pages, UI text, module names and config.info files
6 for entries matching the given phrase or word. Returns them sorted by relevance
7 order, each as a hash ref with the following keys :
9 =item mod - A module hash reference for the module the search result was in
11 =item rank - A result ranking, higher being better
13 =item type - One of mod (for module name), dir (for module directory), config (configuration setting), help (help page) or text (UI text)
15 =item text - The text that matched
17 =items cgis - An array ref of pages on which the text appears, each formatted like module/script.cgi
22 my ($re, $cbfunc) = @_;
24 # Work out this Webmin's URL base
25 my $urlhost = $ENV{'HTTP_HOST'};
26 if ($urlhost !~ /:/) {
27 $urlhost .= ":".$ENV{'SERVER_PORT'};
29 my $urlbase = ($ENV{'HTTPS'} eq 'ON' ? 'https://' : 'http://').$urlhost;
31 # Search module names and add to results list
33 my $pn = &get_product_name();
34 my @mods = sort { $b->{'longdesc'} cmp $a->{'longdesc'} }
35 grep { !$_->{'clone'} }
36 grep { !$_->{'noui'} && !$_->{$pn.'_noui'} }
37 &get_available_module_infos();
38 foreach my $m (@mods) {
39 if ($m->{'desc'} =~ /\Q$re\E/i) {
40 # Module description match
41 push(@rv, { 'mod' => $m,
44 'link' => $m->{'dir'}.'/',
45 'text' => $m->{'desc'} });
47 elsif ($m->{'dir'} =~ /\Q$re\E/i) {
48 # Module directory match
49 push(@rv, { 'mod' => $m,
52 'link' => $m->{'dir'}.'/',
53 'text' => $urlbase."/".$m->{'dir'}."/" });
55 &$cbfunc() if ($cbfunc);
58 # Search module configs and their help pages
59 foreach my $m (@mods) {
60 my %access = &get_module_acl(undef, $m);
61 next if ($access{'noconfig'});
62 my $file = $prod eq 'webmin' ? "$m->{'dir'}/config.info"
63 : "$m->{'dir'}/uconfig.info";
66 &read_file($file, \%info, \@info_order);
67 foreach my $o (@lang_order_list) {
68 &read_file("$file.$o", \%info);
71 foreach my $c (@info_order) {
72 my @p = split(/,/, $info{$c});
76 if ($p[0] =~ /\Q$re\E/i) {
77 # Config description matches
78 push(@rv, { 'mod' => $m,
81 'link' => "/config.cgi?module=$m->{'dir'}&".
82 "section=".&urlize($section)."#$c",
86 my $hfl = &help_file($mod->{'dir'}, "config_".$c);
87 my ($title, $help) = &help_file_match($hfl);
90 push(@rv, { 'mod' => $m,
93 'link' => "/help.cgi/$m->{'dir'}/config_".$c,
94 'desc' => &text('wsearch_helpfor', $p[0]),
96 'cgis' => [ "/config.cgi?".
97 "module=$m->{'dir'}§ion=".
98 &urlize($section)."#$c" ],
102 &$cbfunc() if ($cbfunc);
105 # Search other help pages
106 my %lang_order_list = map { $_, 1 } @lang_order_list;
107 foreach my $m (@mods) {
108 my $helpdir = &module_root_directory($m->{'dir'})."/help";
110 opendir(DIR, $helpdir);
111 foreach my $f (sort { length($b) <=> length($a) } readdir(DIR)) {
112 next if ($f =~ /^config_/); # For config help, already done
114 # Work out if we should grep this help page - don't do the same
115 # page twice for different languages
118 if ($f =~ /^(\S+)\.([^\.]+)\.html$/) {
119 ($page, $lang) = ($1, $2);
120 if ($lang_order_list{$lang} && !$donepage{$page}++) {
124 elsif ($f =~ /^(\S+)\.html$/) {
126 if (!$donepage{$page}++) {
133 my ($title, $help) = &help_file_match("$helpdir/$f");
135 my @cgis = &find_cgi_text(
136 [ "hlink\\(.*'$page'",
137 "hlink\\(.*\"$page\"",
138 "header\\([^,]+,[^,]+,[^,]+,\\s*\"$page\"",
139 "header\\([^,]+,[^,]+,[^,]+,\\s*'$page'",
141 push(@rv, { 'mod' => $m,
144 'link' => "/help.cgi/$m->{'dir'}/$page",
150 &$cbfunc() if ($cbfunc);
155 # Then do text strings
156 my %gtext = &load_language("");
157 MODULE: foreach my $m (@mods) {
158 my %mtext = &load_language($m->{'dir'});
159 foreach my $k (keys %mtext) {
160 next if ($gtext{$k}); # Skip repeated global strings
161 $mtext{$k} =~ s/\$[0-9]//g;
162 if ($mtext{$k} =~ /\Q$re\E/i) {
163 # Find CGIs that use this text
164 my @cgis = &find_cgi_text(
169 "&text(\"$k\"" ], $m);
171 push(@rv, { 'mod' => $m,
174 'text' => $mtext{$k},
179 &$cbfunc() if ($cbfunc);
182 # Sort results by relevancy
184 @rv = sort { $b->{'rank'} <=> $a->{'rank'} ||
185 lc($a->{'mod'}->{'desc'}) cmp lc($b->{'mod'}->{'desc'}) } @rv;
189 # highlight_text(text, [length])
190 # Returns text with the search term bolded, and truncated to 50 characters
193 local ($str, $len) = @_;
195 local $hlen = $len / 2;
196 $str =~ s/<[^>]*>//g;
197 if ($str =~ /(.*)(\Q$re\E)(.*)/i) {
198 local ($before, $match, $after) = ($1, $2, $3);
199 if (length($before) > $hlen) {
200 $before = "...".substr($before, length($before)-$hlen);
202 if (length($after) > $hlen) {
203 $after = substr($after, 0, $hlen)."...";
205 $str = $before."<b>".&html_escape($match)."</b>".$after;
210 # find_cgi_text(®exps, module, re-mode)
211 # Returns the relative URLs of CGIs that matches some regexps, in the given
212 # module. Does not include those that don't call some header function, as
213 # they cannot be linked to normally
216 local ($res, $m, $remode) = @_;
217 local $mdir = &module_root_directory($m);
219 foreach my $f (glob("$mdir/*.cgi")) {
223 LINE: while(my $line = <CGI>) {
224 if ($line =~ /(header|ui_print_header|ui_print_unbuffered_header)\(/) {
227 foreach my $r (@$res) {
228 if (!$remode && index($line, $r) >= 0 ||
229 $remode && $line =~ /$r/) {
236 if ($found && $header) {
238 $url =~ s/^\Q$root_directory\E\///;
245 # help_file_match(file)
246 # Returns the title if some help file matches the current search
250 local $data = &read_file_contents($f);
252 if ($data =~ /<header>([^<]*)<\/header>/) {
256 $data =~ s/<p>/\n\n/gi;
257 $data =~ s/<br>/\n/gi;
258 $data =~ s/<[^>]+>//g;
259 if ($data =~ /\Q$re\E/i) {
260 return ($title, $data);
265 # cgi_page_title(module, cgi)
266 # Given a CGI, return the text for its page title, if possible
269 local ($m, $cgi) = @_;
270 local $data = &read_file_contents(&module_root_directory($m)."/".$cgi);
272 if ($data =~ /(ui_print_header|ui_print_unbuffered_header)\([^,]+,[^,]*(\$text{'([^']+)'|\$text{"([^"]+)"|\&text\('([^']+)'|\&text\("([^"]+)")/) {
273 # New header function, with arg before title
274 local $msg = $3 || $4 || $5 || $6;
275 local %mtext = &load_language($m);
278 elsif ($data =~ /(^|\s|mail_page_)header\(\s*(\$text{'([^']+)'|\$text{"([^"]+)"|\&text\('([^']+)'|\&text\("([^"]+)")/) {
279 # Old header function
280 local $msg = $3 || $4 || $5 || $6;
281 local %mtext = &load_language($m);
284 if ($cgi eq "index.cgi" && !$rv) {
285 # If no title was found for an index.cgi, use module title
286 local %minfo = &get_module_info($m);
287 $rv = $minfo{'desc'};
292 # cgi_page_args(module, cgi)
293 # Given a module and CGI name, returns a string of URL parameters that can be
294 # used for linking to it. Returns "none" if parameters are needed, but cannot
298 local ($m, $cgi) = @_;
299 local $mroot = &module_root_directory($m);
300 if (-r "$mroot/cgi_args.pl") {
301 # Module can tell us what args to use
302 &foreign_require($m, "cgi_args.pl");
303 $args = &foreign_call($m, "cgi_args", $cgi);
304 if (defined($args)) {
308 if ($cgi eq "index.cgi") {
309 # Index page is always safe to link to
312 # Otherwise check if it appears to parse any args
313 local $data = &read_file_contents($mroot."/".$cgi);
314 if ($data =~ /(ReadParse|ReadParseMime)\(/) {