File Coverage

blib/lib/OpenGuides/Search.pm
Criterion Covered Total %
statement 275 303 90.7
branch 84 106 79.2
condition 42 55 76.3
subroutine 21 21 100.0
pod 4 8 50.0
total 426 493 86.4


line stmt bran cond sub pod time code
1             package OpenGuides::Search;
2 15     15   175975 use strict;
  15         28  
  15         710  
3             our $VERSION = '0.15';
4              
5 15     15   16993 use CGI qw( :standard );
  15         438967  
  15         118  
6 15     15   62376 use Wiki::Toolkit::Plugin::Locator::Grid;
  15         28573  
  15         468  
7 15     15   11748 use File::Spec::Functions qw(:ALL);
  15         12510  
  15         3628  
8 15     15   9061 use OpenGuides::Template;
  15         48  
  15         490  
9 15     15   92 use OpenGuides::Utils;
  15         29  
  15         324  
10 15     15   27930 use Parse::RecDescent;
  15         577432  
  15         117  
11              
12             =head1 NAME
13              
14             OpenGuides::Search - Search form generation and processing for OpenGuides.
15              
16             =head1 DESCRIPTION
17              
18             Does search stuff for OpenGuides. Distributed and installed as part of
19             the OpenGuides project, not intended for independent installation.
20             This documentation is probably only useful to OpenGuides developers.
21              
22             =head1 SYNOPSIS
23              
24             use CGI;
25             use OpenGuides::Config;
26             use OpenGuides::Search;
27              
28             my $config = OpenGuides::Config->new( file => "wiki.conf" );
29             my $search = OpenGuides::Search->new( config => $config );
30             my %vars = CGI::Vars();
31             $search->run( vars => \%vars );
32              
33             =head1 METHODS
34              
35             =over 4
36              
37             =item B
38              
39             my $config = OpenGuides::Config->new( file => "wiki.conf" );
40             my $search = OpenGuides::Search->new( config => $config );
41              
42             =cut
43              
44             sub new {
45 27     27 1 101010 my ($class, %args) = @_;
46 27         113 my $config = $args{config};
47 27         120 my $self = { config => $config };
48 27         72 bless $self, $class;
49              
50 27         324 my $wiki = OpenGuides::Utils->make_wiki_object( config => $config );
51              
52 27         151 $self->{wiki} = $wiki;
53 27         118 $self->{wikimain} = $config->script_url . $config->script_name;
54 27         384 $self->{css} = $config->stylesheet_url;
55 27         333 $self->{head} = $config->site_name . " Search";
56              
57 27         380 my $geo_handler = $config->geo_handler;
58 27         234 my %locator_params;
59 27 100       139 if ( $geo_handler == 1 ) {
    100          
    50          
60 14         81 %locator_params = ( x => "os_x", y => "os_y" );
61             } elsif ( $geo_handler == 2 ) {
62 6         31 %locator_params = ( x => "osie_x", y => "osie_y" );
63             } elsif ( $geo_handler == 3 ) {
64 7         39 %locator_params = ( x => "easting", y => "northing" );
65             }
66              
67 27         317 my $locator = Wiki::Toolkit::Plugin::Locator::Grid->new( %locator_params );
68 27         596 $wiki->register_plugin( plugin => $locator );
69 27         1752 $self->{locator} = $locator;
70              
71 27         143 return $self;
72             }
73              
74             =item B
75              
76             my $wiki = $search->wiki;
77              
78             An accessor; returns the underlying L object.
79              
80             =cut
81              
82             sub wiki {
83 316     316 1 940 my $self = shift;
84 316         1280 return $self->{wiki};
85             }
86              
87             =item B
88              
89             my $config = $search->config;
90              
91             An accessor; returns the underlying L object.
92              
93             =cut
94              
95             sub config {
96 158     158 1 606 my $self = shift;
97 158         939 return $self->{config};
98             }
99              
100             =item B
101              
102             my %vars = CGI::Vars();
103             $search->run(
104             vars => \%vars,
105             return_output => 1, # defaults to 0
106             return_tt_vars => 1, # defaults to 0
107             );
108              
109             The C parameter is optional. If supplied and true, the
110             stuff that would normally be printed to STDOUT will be returned as a
111             string instead.
112              
113             The C parameter is also optional. If supplied and
114             true, the template is not processed and the variables that would have
115             been passed to it are returned as a hash. This parameter takes
116             precedence over C.
117              
118             These two parameters exist to make testing easier; you probably don't
119             want to use them in production.
120              
121             You can also request just the raw search results:
122              
123             my %results = $search->run(
124             os_x => 528864,
125             os_y => 180797,
126             os_dist => 750,
127             format => "raw",
128             );
129              
130             Results are returned as a hash, keyed on the page name. All results
131             are returned, not just the first C. The values in the hash are
132             hashes themselves, with the following key/value pairs:
133              
134             =over 4
135              
136             =item * name
137              
138             =item * wgs84_lat - WGS-84 latitude
139              
140             =item * wgs84_long - WGS-84 longitude
141              
142             =item * summary
143              
144             =item * distance - distance (in metres) from origin, if origin exists
145              
146             =item * score - relevance to search string, if search string exists; higher score means more relevance
147              
148             =back
149              
150             In case you're struggling to follow the code, it does the following:
151             1) Processes the parameters, and bails out if it hit a problem with them
152             2) If a search string was given, do a text search
153             3) If distance search paramaters were given, do a distance search
154             4) If no search has occured, print out the search form
155             5) If an error occured, bail out
156             6) If we got a single hit on a string search, redirect to it
157             7) If no results were found, give an empty search results page
158             8) Sort the results by either score or distance
159             9) Decide which results to show, based on paging
160             10) Display the appropriate page of the results
161              
162             =back
163              
164             =cut
165              
166             sub run {
167 68     68 1 4022925 my ($self, %args) = @_;
168 68   100     512 $self->{return_output} = $args{return_output} || 0;
169 68   100     461 $self->{return_tt_vars} = $args{return_tt_vars} || 0;
170              
171 68         131 my $want_raw;
172 68 100 100     377 if ( $args{vars}{format} && $args{vars}{format} eq "raw" ) {
173 4         9 $want_raw = 1;
174             }
175              
176 68         357 $self->process_params( $args{vars} );
177 68 50       239 if ( $self->{error} ) {
178 0         0 warn $self->{error};
179 0         0 my %tt_vars = ( error_message => $self->{error} );
180 0         0 $self->process_template( tt_vars => \%tt_vars );
181 0         0 return;
182             }
183              
184             my %tt_vars = (
185 68         493 format => $args{'vars'}->{'format'},
186             ss_version => $VERSION,
187             ss_info_url => 'http://openguides.org/search_help'
188             );
189              
190 68         127 my $doing_search;
191              
192             # Run a text search if we have a search string.
193 68 100       238 if ( $self->{search_string} ) {
194 39         78 $doing_search = 1;
195 39         129 $tt_vars{search_terms} = $self->{search_string};
196 39         176 $self->run_text_search;
197             }
198              
199             # Run a distance search if we have sufficient criteria.
200 68 50 66     11149 if ( defined $self->{distance_in_metres}
      66        
201             && defined $self->{x} && defined $self->{y} ) {
202 22         103 $doing_search = 1;
203             # Make sure to pass the criteria to the template.
204 22         81 $tt_vars{dist} = $self->{distance_in_metres};
205 22         67 $tt_vars{latitude} = $self->{latitude};
206 22         62 $tt_vars{longitude} = $self->{longitude};
207 22 100       76 if ( $self->config->geo_handler eq 1 ) {
    100          
    50          
208 12         194 $tt_vars{coord_field_1_value} = $self->{os_x};
209 12         47 $tt_vars{coord_field_2_value} = $self->{os_y};
210             } elsif ( $self->config->geo_handler eq 2 ) {
211 4         53 $tt_vars{coord_field_1_value} = $self->{osie_x};
212 4         19 $tt_vars{coord_field_2_value} = $self->{osie_y};
213             } elsif ( $self->config->geo_handler eq 3 ) {
214 6         76 $tt_vars{coord_field_1_value} = $self->{latitude};
215 6         29 $tt_vars{coord_field_2_value} = $self->{longitude};
216             }
217 22         112 $self->run_distance_search;
218             }
219              
220             # If we're not doing a search then just print the search form (or return
221             # an empty hash if we were asked for raw results).
222 68 100       245 if ( !$doing_search ) {
223 10 100       32 if ( $want_raw ) {
224 1         5 return ( );
225             } else {
226 9         41 return $self->process_template( tt_vars => \%tt_vars );
227             }
228             }
229              
230             # At this point either $self->{error} or $self->{results} will be filled.
231 58 50       323 if ( $self->{error} ) {
232 0         0 $tt_vars{error_message} = $self->{error};
233 0         0 $self->process_template( tt_vars => \%tt_vars );
234 0         0 return;
235             }
236              
237             # So now we know that we have been asked to perform a search, and we
238             # have performed it.
239             #
240             # $self->{results} will be a hash of refs to hashes like so:
241             # 'Node Name' => {
242             # name => 'Node Name',
243             # distance => $distance_from_origin_if_any,
244             # score => $relevance_to_search_string
245             # }
246              
247 58 50       111 my %results_hash = %{ $self->{results} || [] };
  58         497  
248              
249             # If we were asked for just the raw results, return them now, after
250             # grabbing additional info.
251 58 100       231 if ( $want_raw ) {
252 3         11 foreach my $node ( keys %results_hash ) {
253 4         16 my %data = $self->wiki->retrieve_node( $node );
254 4         8024 $results_hash{$node}{summary} = $data{metadata}{summary}[0];
255 4         14 my $lat = $data{metadata}{latitude}[0];
256 4         11 my $long = $data{metadata}{longitude}[0];
257 4         15 my ( $wgs84_lat, $wgs84_long ) = OpenGuides::Utils->get_wgs84_coords( latitude => $lat, longitude => $long, config => $self->config );
258 4         13 $results_hash{$node}{wgs84_lat} = $wgs84_lat;
259 4         45 $results_hash{$node}{wgs84_long} = $wgs84_long;
260             }
261 3         36 return %results_hash;
262             }
263              
264 55         193 my @results = values %results_hash;
265 55         116 my $numres = scalar @results;
266              
267             # If we only have a single hit, and the title is a good enough match
268             # to the search string, redirect to that node.
269             # (Don't try a fuzzy search on a blank search string - Plucene chokes.)
270 55 100 100     539 if ( $self->{search_string} && $numres == 1 && !$self->{return_tt_vars}) {
      100        
271 6         28 my %fuzzies = $self->wiki->fuzzy_title_match($self->{search_string});
272 6 100       40456 if ( scalar keys %fuzzies ) {
273 2         7 my $node = $results[0]{name};
274 2         8 my $formatter = $self->wiki->formatter;
275 2         20 my $node_param = CGI::escape(
276             $formatter->node_name_to_node_param( $node )
277             );
278 2         149 my $output = CGI::redirect( $self->{wikimain} . "?$node_param" );
279 2 50       1050 return $output if $self->{return_output};
280 0         0 print $output;
281 0         0 return;
282             }
283             }
284              
285             # If we had no hits then go straight to the template.
286 53 100       196 if ( $numres == 0 ) {
287 13         154 %tt_vars = (
288             %tt_vars,
289             first_num => 0,
290             results => [],
291             );
292 13         94 return $self->process_template( tt_vars => \%tt_vars );
293             }
294              
295             # Otherwise, we browse through the results a page at a time.
296              
297             # Figure out which results we're going to be showing on this
298             # page, and what the first one for the next page will be.
299 40   100     281 my $startpos = $args{vars}{next} || 0;
300 40 50       199 $tt_vars{first_num} = $numres ? $startpos + 1 : 0;
301 40 100       170 $tt_vars{last_num} = $numres > $startpos + 20 ? $startpos + 20 : $numres;
302 40         108 $tt_vars{total_num} = $numres;
303 40 100       153 if ( $numres > $startpos + 20 ) {
304 3         9 $tt_vars{next_page_startpos} = $startpos + 20;
305             }
306              
307             # Sort the results - by distance if we're searching on that
308             # or by score otherwise.
309 40 100       127 if ( $self->{distance_in_metres} ) {
310 13         73 @results = sort { $a->{distance} <=> $b->{distance} } @results;
  170         280  
311             } else {
312 27         113 @results = sort { $b->{score} <=> $a->{score} } @results;
  67         138  
313             }
314              
315             # Now snip out just the ones for this page. The -1 is because
316             # arrays index from 0 and people from 1.
317 40 50       184 my $from = $tt_vars{first_num} ? $tt_vars{first_num} - 1 : 0;
318 40         96 my $to = $tt_vars{last_num} - 1; # kludge to empty arr for no results
319 40         189 @results = @results[ $from .. $to ];
320              
321             # Add the URL to each result hit.
322 40         164 my $formatter = $self->wiki->formatter;
323 40         287 foreach my $i ( 0 .. $#results ) {
324 143         320 my $name = $results[$i]{name};
325              
326             # Add the one-line summary of the node, if there is one.
327 143         406 my %node = $self->wiki->retrieve_node($name);
328 143         258676 $results[$i]{summary} = $node{metadata}{summary}[0];
329              
330 143         661 my $node_param = $formatter->node_name_to_node_param( $name );
331 143         5813 $results[$i]{url} = $self->{wikimain} . "?$node_param";
332             }
333              
334             # Finally pass the results to the template.
335 40         162 $tt_vars{results} = \@results;
336 40         221 $self->process_template( tt_vars => \%tt_vars );
337             }
338              
339             sub run_text_search {
340 39     39 0 77 my $self = shift;
341 39         98 my $searchstr = $self->{search_string};
342 39         141 my $wiki = $self->wiki;
343 39         155 my $config = $self->config;
344              
345 39 50       217 if ( $config->use_lucy ) {
346 0         0 require OpenGuides::Search::Lucy;
347 0         0 my $lucy = OpenGuides::Search::Lucy->new( config => $config );
348 0         0 my %results = $lucy->run_text_search( search_string => $searchstr );
349 0         0 $self->{results} = \%results;
350 0         0 return $self;
351             }
352              
353             # Create parser to parse the search string.
354 39         818 my $parser = Parse::RecDescent->new( q{
355              
356             search: list eostring {$return = $item[1]}
357              
358             list: comby(s)
359             {$return = (@{$item[1]}>1) ? ['AND', @{$item[1]}] : $item[1][0]}
360              
361             comby:
362             {$return = (@{$item[1]}>1) ? ['OR', @{$item[1]}] : $item[1][0]}
363              
364             term: '(' list ')' {$return = $item[2]}
365             | '-' term {$return = ['NOT', @{$item[2]}]}
366             | '"' word(s) '"' {$return = ['phrase', join " ", @{$item[2]}]}
367             | word {$return = ['word', $item[1]]}
368             | '[' word(s) ']' {$return = ['title', @{$item[2]}]}
369              
370             word: /[\w'*%]+/ {$return = $item[1]}
371              
372             eostring: /^\Z/
373              
374             } );
375              
376 39 50       1558424 unless ( $parser ) {
377 0         0 warn $@;
378 0         0 $self->{error} = "Can't create parse object - $@";
379 0         0 return $self;
380             }
381              
382             # Run parser over search string.
383 39         559 my $tree = $parser->search( $searchstr );
384 39 50       107302 unless ( $tree ) {
385 0         0 $self->{error} = "Syntax error in search: $searchstr";
386 0         0 return $self;
387             }
388              
389             # Run the search over the generated search tree.
390 39         228 my %results = $self->_run_search_tree( tree => $tree );
391 39         209 $self->{results} = \%results;
392 39         321 return $self;
393             }
394              
395             sub _run_search_tree {
396 58     58   197 my ($self, %args) = @_;
397 58         154 my $tree = $args{tree};
398 58         189 my @tree_arr = @$tree;
399 58         175 my $op = shift @tree_arr;
400 58         169 my $method = "_run_" . $op . "_search";
401 58 50       575 return $self->can($method) ? $self->$method(@tree_arr) : undef;
402             }
403              
404             =head1 INPUT
405              
406             =over
407              
408             =item B
409              
410             a single word will be matched as-is. For example, a search on
411              
412             escalator
413              
414             will return all pages containing the word "escalator".
415              
416             =cut
417              
418             sub _run_word_search {
419 47     47   114 my ($self, $word) = @_;
420             # A word is just a small phrase.
421 47         199 return $self->_run_phrase_search( $word );
422             }
423              
424             =item B
425              
426             A list of words with no punctuation will be ANDed, for example:
427              
428             restaurant vegetarian
429              
430             will return all pages containing both the word "restaurant" and the word
431             "vegetarian".
432              
433             =cut
434              
435             sub _run_AND_search {
436 8     8   25 my ($self, @subsearches) = @_;
437              
438             # Do the first subsearch.
439 8         42 my %results = $self->_run_search_tree( tree => $subsearches[0] );
440              
441             # Now do the rest one at a time and remove from the results anything
442             # that doesn't come up in each subsearch. Results that survive will
443             # have a score that's the sum of their score in each subsearch.
444 8         39 foreach my $tree ( @subsearches[ 1 .. $#subsearches ] ) {
445 9         37 my %subres = $self->_run_search_tree( tree => $tree );
446 9         43 my @pages = keys %results;
447 9         21 foreach my $page ( @pages ) {
448 19 100       67 if ( exists $subres{$page} ) {
449 13         67 $results{$page}{score} += $subres{$page}{score};
450             } else {
451 6         34 delete $results{$page};
452             }
453             }
454             }
455              
456 8         66 return %results;
457             }
458              
459             =item B
460              
461             A list of words separated by commas (and optional spaces) will be ORed,
462             for example:
463              
464             restaurant, cafe
465              
466             will return all pages containing either the word "restaurant" or the
467             word "cafe".
468              
469             =cut
470              
471             sub _run_OR_search {
472 1     1   4 my ($self, @subsearches) = @_;
473              
474             # Do all the searches. Results will have a score that's the sum
475             # of their score in each subsearch.
476 1         2 my %results;
477 1         3 foreach my $tree ( @subsearches ) {
478 2         9 my %subres = $self->_run_search_tree( tree => $tree );
479 2         7 foreach my $page ( keys %subres ) {
480 3 50       12 if ( $results{$page} ) {
481 0         0 $results{$page}{score} += $subres{$page}{score};
482             } else {
483 3         13 $results{$page} = $subres{$page};
484             }
485             }
486             }
487 1         12 return %results;
488             }
489              
490             =item B
491              
492             Enclose phrases in double quotes, for example:
493              
494             "meat pie"
495              
496             will return all pages that contain the exact phrase "meat pie" - not pages
497             that only contain, for example, "apple pie and meat sausage".
498              
499             =cut
500              
501             sub _run_phrase_search {
502 49     49   118 my ($self, $phrase) = @_;
503 49         194 my $wiki = $self->wiki;
504              
505             # Search title and body.
506 49         314 my %contents_res = $wiki->search_nodes( $phrase );
507              
508             # Rationalise the scores a little. The scores returned by
509             # Wiki::Toolkit::Search::Plucene are simply a ranking.
510 49         512600 my $num_results = scalar keys %contents_res;
511 49         201 foreach my $node ( keys %contents_res ) {
512 74         277 $contents_res{$node} = int( $contents_res{$node} / $num_results ) + 1;
513             }
514              
515 49         177 my @tmp = keys %contents_res;
516 49         128 foreach my $node ( @tmp ) {
517 74         316 my $content = $wiki->retrieve_node( $node );
518              
519             # Don't include redirects in search results.
520 74 50       35723 if ($content =~ /^#REDIRECT/) {
521 0         0 delete $contents_res{$node};
522 0         0 next;
523             }
524              
525             # It'll be a real phrase (as opposed to a word) if it has a space in it.
526             # In this case, dump out the nodes that don't match the search exactly.
527             # I don't know why the phrase searching isn't working properly. Fix later.
528 74 100       322 if ( $phrase =~ /\s/ ) {
529 2 100 66     38 unless ( $content =~ /$phrase/i || $node =~ /$phrase/i ) {
530 1         5 delete $contents_res{$node};
531             }
532             }
533              
534             }
535              
536 49         165 my %results = map { $_ => { name => $_, score => $contents_res{$_} } }
  73         433  
537             keys %contents_res;
538              
539             # Bump up the score if the title matches.
540 49         188 foreach my $node ( keys %results ) {
541 73 100       679 $results{$node}{score} += 10 if $node =~ /$phrase/i;
542             }
543              
544             # Search categories.
545 49         318 my @catmatches = $wiki->list_nodes_by_metadata(
546             metadata_type => "category",
547             metadata_value => $phrase,
548             ignore_case => 1,
549             );
550              
551 49         20650 foreach my $node ( @catmatches ) {
552 23 50       91 if ( $results{$node} ) {
553 0         0 $results{$node}{score} += 3;
554             } else {
555 23         122 $results{$node} = { name => $node, score => 3 };
556             }
557             }
558              
559             # Search locales.
560 49         228 my @locmatches = $wiki->list_nodes_by_metadata(
561             metadata_type => "locale",
562             metadata_value => $phrase,
563             ignore_case => 1,
564             );
565              
566 49         15273 foreach my $node ( @locmatches ) {
567 16 50       48 if ( $results{$node} ) {
568 0         0 $results{$node}{score} += 3;
569             } else {
570 16         80 $results{$node} = { name => $node, score => 3 };
571             }
572             }
573              
574 49         557 return %results;
575             }
576              
577             =back
578              
579             =head1 SEARCHING BY DISTANCE
580              
581             To perform a distance search, you need to supply one of the following
582             sets of criteria to specify the distance to search within, and the
583             origin (centre) of the search:
584              
585             =over
586              
587             =item B
588              
589             Only works if you chose to use British National Grid in wiki.conf
590              
591             =item B
592              
593             Only works if you chose to use Irish National Grid in wiki.conf
594              
595             =item B
596              
597             Should always work, but has a habit of "finding" things a couple of
598             metres away from themselves.
599              
600             =back
601              
602             You can perform both pure distance searches and distance searches in
603             combination with text searches.
604              
605             =cut
606              
607             # Note this is called after any text search is run, and it is only called
608             # if there are sufficient criteria to perform the search.
609             sub run_distance_search {
610 22     22 0 45 my $self = shift;
611 22         52 my $x = $self->{x};
612 22         49 my $y = $self->{y};
613 22         60 my $dist = $self->{distance_in_metres};
614              
615             my @close = $self->{locator}->find_within_distance(
616 22         153 x => $x,
617             y => $y,
618             metres => $dist,
619             );
620              
621 22 100       455802 if ( $self->{results} ) {
622 3         10 my %close_hash = map { $_ => 1 } @close;
  9         34  
623 3         8 my %results = %{ $self->{results} };
  3         26  
624 3         16 my @candidates = keys %results;
625 3         11 foreach my $node ( @candidates ) {
626 12 100       39 if ( exists $close_hash{$node} ) {
627 6         25 my $distance = $self->_get_distance(
628             node => $node,
629             x => $x,
630             y => $y,
631             );
632 6         14716 $results{$node}{distance} = $distance;
633             } else {
634 6         23 delete $results{$node};
635             }
636             }
637 3         18 $self->{results} = \%results;
638             } else {
639 19         46 my %results;
640 19         56 foreach my $node ( @close ) {
641 173         649 my $distance = $self->_get_distance (
642             node => $node,
643             x => $x,
644             y => $y,
645             );
646 173         428794 $results{$node} = {
647             name => $node,
648             distance => $distance,
649             };
650             }
651 19         64 $self->{results} = \%results;
652             }
653 22         100 return $self;
654             }
655              
656             sub _get_distance {
657 179     179   635 my ($self, %args) = @_;
658 179         438 my ($node, $x, $y) = @args{ qw( node x y ) };
659             return $self->{locator}->distance(
660 179         790 from_x => $x,
661             from_y => $y,
662             to_node => $node,
663             unit => "metres"
664             );
665             }
666              
667             sub process_params {
668 68     68 0 148 my ($self, $vars_hashref) = @_;
669 68 50       119 my %vars = %{ $vars_hashref || {} };
  68         446  
670              
671             # Make sure that we don't have any data left over from previous invocation.
672             # This is useful for testing purposes at the moment and will be essential
673             # for mod_perl implementations.
674 68         202 delete $self->{x};
675 68         149 delete $self->{y};
676 68         143 delete $self->{distance_in_metres};
677 68         148 delete $self->{search_string};
678 68         208 delete $self->{results};
679              
680             # Strip out any non-digits from distance and OS co-ords.
681 68         193 foreach my $param ( qw( os_x os_y osie_x osie_y
682             osie_dist os_dist latlong_dist ) ) {
683 476 100       1226 if ( defined $vars{$param} ) {
684 72         182 $vars{$param} =~ s/[^0-9]//g;
685             # 0 is an allowed value but the empty string isn't.
686 72 50       236 delete $vars{$param} if $vars{$param} eq "";
687             }
688             }
689              
690             # Latitude and longitude are also allowed '-' and '.'
691 68         155 foreach my $param( qw( latitude longitude ) ) {
692 136 100       565 if ( defined $vars{$param} ) {
693 26         179 $vars{$param} =~ s/[^-\.0-9]//g;
694             # 0 is an allowed value but the empty string isn't.
695 26 100       132 delete $vars{$param} if $vars{$param} eq "";
696             }
697             }
698              
699             # Set $self->{distance_in_metres}, $self->{x}, $self->{y},
700             # depending on whether we got
701             # OS co-ords or lat/long. Only store parameters if they're complete,
702             # and supported by our method of distance calculation.
703 68 100 66     1093 if ( defined $vars{os_x} && defined $vars{os_y} && defined $vars{os_dist}
    100 66        
    100 66        
      66        
      66        
      66        
      66        
      66        
704             && $self->config->geo_handler eq 1 ) {
705 7         124 $self->{x} = $vars{os_x};
706 7         19 $self->{y} = $vars{os_y};
707 7         20 $self->{distance_in_metres} = $vars{os_dist};
708             } elsif ( defined $vars{osie_x} && defined $vars{osie_y}
709             && defined $vars{osie_dist}
710             && $self->config->geo_handler eq 2 ) {
711 3         53 $self->{x} = $vars{osie_x};
712 3         12 $self->{y} = $vars{osie_y};
713 3         12 $self->{distance_in_metres} = $vars{osie_dist};
714             } elsif ( defined $vars{latitude} && defined $vars{longitude}
715             && defined $vars{latlong_dist} ) {
716             # All handlers can do lat/long, but they all do it differently.
717 12 100       52 if ( $self->config->geo_handler eq 1 ) {
    100          
    50          
718 5         1429 require Geo::Coordinates::OSGB;
719             my ( $x, $y ) = Geo::Coordinates::OSGB::ll_to_grid(
720 5         73481 $vars{latitude}, $vars{longitude} );
721 5         708 $self->{x} = sprintf( "%d", $x );
722 5         26 $self->{y} = sprintf( "%d", $y );
723             } elsif ( $self->config->geo_handler eq 2 ) {
724 1         962 require Geo::Coordinates::ITM;
725             my ( $x, $y ) = Geo::Coordinates::ITM::ll_to_grid(
726 1         16830 $vars{latitude}, $vars{longitude} );
727 1         216 $self->{x} = sprintf( "%d", $x );
728 1         4 $self->{y} = sprintf( "%d", $y );
729             } elsif ( $self->config->geo_handler eq 3 ) {
730 6         2000 require Geo::Coordinates::UTM;
731             my ($zone, $x, $y) = Geo::Coordinates::UTM::latlon_to_utm(
732             $self->config->ellipsoid,
733             $vars{latitude},
734             $vars{longitude},
735 6         21834 );
736 6         741 $self->{x} = $x;
737 6         17 $self->{y} = $y;
738             }
739 12         56 $self->{distance_in_metres} = $vars{latlong_dist};
740             }
741              
742             # Store os_x etc so we can pass them to template.
743 68         271 foreach my $param ( qw( os_x os_y osie_x osie_y latitude longitude ) ) {
744 408         871 $self->{$param} = $vars{$param};
745             }
746              
747             # Strip leading and trailing whitespace from search text.
748 68   100     438 $vars{search} ||= ""; # avoid uninitialised value warning
749 68         330 $vars{search} =~ s/^\s*//;
750 68         387 $vars{search} =~ s/\s*$//;
751              
752             # Check for only valid characters in tainted search param
753             # (quoted literals are OK, as they are escaped)
754             # This regex copied verbatim from Ivor's old supersearch.
755 68 50 66     634 if ( $vars{search}
756             && $vars{search} !~ /^("[^"]*"|[\w \-',()!*%\[\]])+$/i) {
757 0         0 $self->{error} = "Search expression $vars{search} contains invalid character(s)";
758 0         0 return $self;
759             }
760 68         196 $self->{search_string} = $vars{search};
761              
762 68         207 return $self;
763             }
764              
765             # thin wrapper around OpenGuides::Template, or OpenGuides::Feed
766             sub process_template {
767 62     62 0 250 my ($self, %args) = @_;
768              
769 62   50     264 my $tt_vars = $args{tt_vars} || {};
770 62         158 $tt_vars->{not_editable} = 1;
771 62         166 $tt_vars->{not_deletable} = 1;
772 62 100       835 return %$tt_vars if $self->{return_tt_vars};
773              
774             # Do we want a feed, or TT html?
775 32         52 my $output;
776 32 100       106 if($tt_vars->{'format'}) {
777 2         6 my $format = $tt_vars->{'format'};
778 2         4 my @nodes = @{$tt_vars->{'results'}};
  2         6  
779              
780 2         8 my $feed = OpenGuides::Feed->new(
781             wiki => $self->wiki,
782             config => $self->config,
783             og_version => $VERSION,
784             );
785             $feed->set_feed_name_and_url_params(
786             "Search Results for ".$tt_vars->{search_terms},
787             "search.cgi?search=".$tt_vars->{search_terms}
788 2         16 );
789              
790 2         12 $output = "Content-Type: ".$feed->default_content_type($format)."\n";
791 2         10 $output .= $feed->build_mini_feed_for_nodes($format,@nodes);
792             } else {
793 30         125 $output = OpenGuides::Template->output(
794             wiki => $self->wiki,
795             config => $self->config,
796             template => "search.tt",
797             vars => $tt_vars,
798             );
799             }
800              
801 32 50       30437 return $output if $self->{return_output};
802              
803 0           print $output;
804 0           return 1;
805             }
806              
807             =head1 OUTPUT
808              
809             Results will be put into some form of relevance ordering. These are
810             the rules we have tests for so far (and hence the only rules that can
811             be relied on):
812              
813             =over
814              
815             =item *
816              
817             A match on page title will score higher than a match on page category
818             or locale.
819              
820             =item *
821              
822             A match on page category or locale will score higher than a match on
823             page content.
824              
825             =item *
826              
827             Two matches in the title beats one match in the title and one in the content.
828              
829             =back
830              
831             =cut
832              
833             =head1 AUTHOR
834              
835             The OpenGuides Project (openguides-dev@lists.openguides.org)
836              
837             =head1 COPYRIGHT
838              
839             Copyright (C) 2003-2013 The OpenGuides Project. All Rights Reserved.
840              
841             The OpenGuides distribution is free software; you can redistribute it
842             and/or modify it under the same terms as Perl itself.
843              
844             =head1 SEE ALSO
845              
846             L
847              
848             =cut
849              
850             1;