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   109636 use strict;
  15         22  
  15         624  
3             our $VERSION = '0.15';
4              
5 15     15   9521 use CGI qw( :standard );
  15         277521  
  15         77  
6 15     15   33699 use Wiki::Toolkit::Plugin::Locator::Grid;
  15         17534  
  15         415  
7 15     15   6030 use File::Spec::Functions qw(:ALL);
  15         8061  
  15         2492  
8 15     15   5713 use OpenGuides::Template;
  15         39  
  15         405  
9 15     15   75 use OpenGuides::Utils;
  15         18  
  15         242  
10 15     15   15989 use Parse::RecDescent;
  15         353348  
  15         90  
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 70126 my ($class, %args) = @_;
46 27         68 my $config = $args{config};
47 27         95 my $self = { config => $config };
48 27         59 bless $self, $class;
49              
50 27         283 my $wiki = OpenGuides::Utils->make_wiki_object( config => $config );
51              
52 27         111 $self->{wiki} = $wiki;
53 27         91 $self->{wikimain} = $config->script_url . $config->script_name;
54 27         287 $self->{css} = $config->stylesheet_url;
55 27         241 $self->{head} = $config->site_name . " Search";
56              
57 27         269 my $geo_handler = $config->geo_handler;
58 27         185 my %locator_params;
59 27 100       134 if ( $geo_handler == 1 ) {
    100          
    50          
60 14         70 %locator_params = ( x => "os_x", y => "os_y" );
61             } elsif ( $geo_handler == 2 ) {
62 6         35 %locator_params = ( x => "osie_x", y => "osie_y" );
63             } elsif ( $geo_handler == 3 ) {
64 7         31 %locator_params = ( x => "easting", y => "northing" );
65             }
66              
67 27         268 my $locator = Wiki::Toolkit::Plugin::Locator::Grid->new( %locator_params );
68 27         532 $wiki->register_plugin( plugin => $locator );
69 27         1523 $self->{locator} = $locator;
70              
71 27         117 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 1204 my $self = shift;
84 316         884 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 432 my $self = shift;
97 158         665 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 1580788 my ($self, %args) = @_;
168 68   100     390 $self->{return_output} = $args{return_output} || 0;
169 68   100     362 $self->{return_tt_vars} = $args{return_tt_vars} || 0;
170              
171 68         105 my $want_raw;
172 68 100 100     289 if ( $args{vars}{format} && $args{vars}{format} eq "raw" ) {
173 4         5 $want_raw = 1;
174             }
175              
176 68         290 $self->process_params( $args{vars} );
177 68 50       175 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         340 format => $args{'vars'}->{'format'},
186             ss_version => $VERSION,
187             ss_info_url => 'http://openguides.org/search_help'
188             );
189              
190 68         83 my $doing_search;
191              
192             # Run a text search if we have a search string.
193 68 100       174 if ( $self->{search_string} ) {
194 39         56 $doing_search = 1;
195 39         97 $tt_vars{search_terms} = $self->{search_string};
196 39         144 $self->run_text_search;
197             }
198              
199             # Run a distance search if we have sufficient criteria.
200 68 50 66     7930 if ( defined $self->{distance_in_metres}
      66        
201             && defined $self->{x} && defined $self->{y} ) {
202 22         37 $doing_search = 1;
203             # Make sure to pass the criteria to the template.
204 22         69 $tt_vars{dist} = $self->{distance_in_metres};
205 22         55 $tt_vars{latitude} = $self->{latitude};
206 22         43 $tt_vars{longitude} = $self->{longitude};
207 22 100       69 if ( $self->config->geo_handler eq 1 ) {
    100          
    50          
208 12         118 $tt_vars{coord_field_1_value} = $self->{os_x};
209 12         31 $tt_vars{coord_field_2_value} = $self->{os_y};
210             } elsif ( $self->config->geo_handler eq 2 ) {
211 4         42 $tt_vars{coord_field_1_value} = $self->{osie_x};
212 4         17 $tt_vars{coord_field_2_value} = $self->{osie_y};
213             } elsif ( $self->config->geo_handler eq 3 ) {
214 6         49 $tt_vars{coord_field_1_value} = $self->{latitude};
215 6         41 $tt_vars{coord_field_2_value} = $self->{longitude};
216             }
217 22         91 $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       203 if ( !$doing_search ) {
223 10 100       26 if ( $want_raw ) {
224 1         4 return ( );
225             } else {
226 9         43 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       193 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       84 my %results_hash = %{ $self->{results} || [] };
  58         396  
248              
249             # If we were asked for just the raw results, return them now, after
250             # grabbing additional info.
251 58 100       177 if ( $want_raw ) {
252 3         8 foreach my $node ( keys %results_hash ) {
253 4         11 my %data = $self->wiki->retrieve_node( $node );
254 4         4930 $results_hash{$node}{summary} = $data{metadata}{summary}[0];
255 4         8 my $lat = $data{metadata}{latitude}[0];
256 4         8 my $long = $data{metadata}{longitude}[0];
257 4         10 my ( $wgs84_lat, $wgs84_long ) = OpenGuides::Utils->get_wgs84_coords( latitude => $lat, longitude => $long, config => $self->config );
258 4         8 $results_hash{$node}{wgs84_lat} = $wgs84_lat;
259 4         28 $results_hash{$node}{wgs84_long} = $wgs84_long;
260             }
261 3         23 return %results_hash;
262             }
263              
264 55         146 my @results = values %results_hash;
265 55         87 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     413 if ( $self->{search_string} && $numres == 1 && !$self->{return_tt_vars}) {
      100        
271 6         21 my %fuzzies = $self->wiki->fuzzy_title_match($self->{search_string});
272 6 100       24655 if ( scalar keys %fuzzies ) {
273 2         4 my $node = $results[0]{name};
274 2         6 my $formatter = $self->wiki->formatter;
275 2         14 my $node_param = CGI::escape(
276             $formatter->node_name_to_node_param( $node )
277             );
278 2         86 my $output = CGI::redirect( $self->{wikimain} . "?$node_param" );
279 2 50       685 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       151 if ( $numres == 0 ) {
287 13         127 %tt_vars = (
288             %tt_vars,
289             first_num => 0,
290             results => [],
291             );
292 13         67 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     215 my $startpos = $args{vars}{next} || 0;
300 40 50       151 $tt_vars{first_num} = $numres ? $startpos + 1 : 0;
301 40 100       146 $tt_vars{last_num} = $numres > $startpos + 20 ? $startpos + 20 : $numres;
302 40         74 $tt_vars{total_num} = $numres;
303 40 100       113 if ( $numres > $startpos + 20 ) {
304 3         6 $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       97 if ( $self->{distance_in_metres} ) {
310 13         51 @results = sort { $a->{distance} <=> $b->{distance} } @results;
  170         145  
311             } else {
312 27         101 @results = sort { $b->{score} <=> $a->{score} } @results;
  66         95  
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       142 my $from = $tt_vars{first_num} ? $tt_vars{first_num} - 1 : 0;
318 40         73 my $to = $tt_vars{last_num} - 1; # kludge to empty arr for no results
319 40         161 @results = @results[ $from .. $to ];
320              
321             # Add the URL to each result hit.
322 40         164 my $formatter = $self->wiki->formatter;
323 40         232 foreach my $i ( 0 .. $#results ) {
324 143         216 my $name = $results[$i]{name};
325              
326             # Add the one-line summary of the node, if there is one.
327 143         294 my %node = $self->wiki->retrieve_node($name);
328 143         158803 $results[$i]{summary} = $node{metadata}{summary}[0];
329              
330 143         525 my $node_param = $formatter->node_name_to_node_param( $name );
331 143         3959 $results[$i]{url} = $self->{wikimain} . "?$node_param";
332             }
333              
334             # Finally pass the results to the template.
335 40         126 $tt_vars{results} = \@results;
336 40         176 $self->process_template( tt_vars => \%tt_vars );
337             }
338              
339             sub run_text_search {
340 39     39 0 60 my $self = shift;
341 39         71 my $searchstr = $self->{search_string};
342 39         125 my $wiki = $self->wiki;
343 39         121 my $config = $self->config;
344              
345 39 50       169 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         710 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       952939 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         438 my $tree = $parser->search( $searchstr );
384 39 50       71514 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         189 my %results = $self->_run_search_tree( tree => $tree );
391 39         164 $self->{results} = \%results;
392 39         261 return $self;
393             }
394              
395             sub _run_search_tree {
396 58     58   140 my ($self, %args) = @_;
397 58         109 my $tree = $args{tree};
398 58         171 my @tree_arr = @$tree;
399 58         100 my $op = shift @tree_arr;
400 58         155 my $method = "_run_" . $op . "_search";
401 58 50       498 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   84 my ($self, $word) = @_;
420             # A word is just a small phrase.
421 47         150 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   23 my ($self, @subsearches) = @_;
437              
438             # Do the first subsearch.
439 8         34 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         34 foreach my $tree ( @subsearches[ 1 .. $#subsearches ] ) {
445 9         31 my %subres = $self->_run_search_tree( tree => $tree );
446 9         45 my @pages = keys %results;
447 9         24 foreach my $page ( @pages ) {
448 19 100       52 if ( exists $subres{$page} ) {
449 13         50 $results{$page}{score} += $subres{$page}{score};
450             } else {
451 6         20 delete $results{$page};
452             }
453             }
454             }
455              
456 8         56 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   2 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         5 my %subres = $self->_run_search_tree( tree => $tree );
479 2         6 foreach my $page ( keys %subres ) {
480 3 50       7 if ( $results{$page} ) {
481 0         0 $results{$page}{score} += $subres{$page}{score};
482             } else {
483 3         8 $results{$page} = $subres{$page};
484             }
485             }
486             }
487 1         7 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   68 my ($self, $phrase) = @_;
503 49         162 my $wiki = $self->wiki;
504              
505             # Search title and body.
506 49         256 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         339070 my $num_results = scalar keys %contents_res;
511 49         175 foreach my $node ( keys %contents_res ) {
512 74         196 $contents_res{$node} = int( $contents_res{$node} / $num_results ) + 1;
513             }
514              
515 49         139 my @tmp = keys %contents_res;
516 49         111 foreach my $node ( @tmp ) {
517 74         284 my $content = $wiki->retrieve_node( $node );
518              
519             # Don't include redirects in search results.
520 74 50       26111 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       268 if ( $phrase =~ /\s/ ) {
529 2 100 66     25 unless ( $content =~ /$phrase/i || $node =~ /$phrase/i ) {
530 1         4 delete $contents_res{$node};
531             }
532             }
533              
534             }
535              
536 49         145 my %results = map { $_ => { name => $_, score => $contents_res{$_} } }
  73         289  
537             keys %contents_res;
538              
539             # Bump up the score if the title matches.
540 49         146 foreach my $node ( keys %results ) {
541 73 100       617 $results{$node}{score} += 10 if $node =~ /$phrase/i;
542             }
543              
544             # Search categories.
545 49         265 my @catmatches = $wiki->list_nodes_by_metadata(
546             metadata_type => "category",
547             metadata_value => $phrase,
548             ignore_case => 1,
549             );
550              
551 49         15806 foreach my $node ( @catmatches ) {
552 23 50       64 if ( $results{$node} ) {
553 0         0 $results{$node}{score} += 3;
554             } else {
555 23         88 $results{$node} = { name => $node, score => 3 };
556             }
557             }
558              
559             # Search locales.
560 49         194 my @locmatches = $wiki->list_nodes_by_metadata(
561             metadata_type => "locale",
562             metadata_value => $phrase,
563             ignore_case => 1,
564             );
565              
566 49         10884 foreach my $node ( @locmatches ) {
567 16 50       37 if ( $results{$node} ) {
568 0         0 $results{$node}{score} += 3;
569             } else {
570 16         55 $results{$node} = { name => $node, score => 3 };
571             }
572             }
573              
574 49         432 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 29 my $self = shift;
611 22         40 my $x = $self->{x};
612 22         30 my $y = $self->{y};
613 22         40 my $dist = $self->{distance_in_metres};
614              
615             my @close = $self->{locator}->find_within_distance(
616 22         110 x => $x,
617             y => $y,
618             metres => $dist,
619             );
620              
621 22 100       256041 if ( $self->{results} ) {
622 3         7 my %close_hash = map { $_ => 1 } @close;
  9         20  
623 3         5 my %results = %{ $self->{results} };
  3         13  
624 3         12 my @candidates = keys %results;
625 3         7 foreach my $node ( @candidates ) {
626 12 100       21 if ( exists $close_hash{$node} ) {
627 6         16 my $distance = $self->_get_distance(
628             node => $node,
629             x => $x,
630             y => $y,
631             );
632 6         7173 $results{$node}{distance} = $distance;
633             } else {
634 6         12 delete $results{$node};
635             }
636             }
637 3         13 $self->{results} = \%results;
638             } else {
639 19         32 my %results;
640 19         46 foreach my $node ( @close ) {
641 173         342 my $distance = $self->_get_distance (
642             node => $node,
643             x => $x,
644             y => $y,
645             );
646 173         242927 $results{$node} = {
647             name => $node,
648             distance => $distance,
649             };
650             }
651 19         52 $self->{results} = \%results;
652             }
653 22         60 return $self;
654             }
655              
656             sub _get_distance {
657 179     179   380 my ($self, %args) = @_;
658 179         270 my ($node, $x, $y) = @args{ qw( node x y ) };
659             return $self->{locator}->distance(
660 179         399 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 111 my ($self, $vars_hashref) = @_;
669 68 50       110 my %vars = %{ $vars_hashref || {} };
  68         358  
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         141 delete $self->{x};
675 68         119 delete $self->{y};
676 68         98 delete $self->{distance_in_metres};
677 68         108 delete $self->{search_string};
678 68         137 delete $self->{results};
679              
680             # Strip out any non-digits from distance and OS co-ords.
681 68         169 foreach my $param ( qw( os_x os_y osie_x osie_y
682             osie_dist os_dist latlong_dist ) ) {
683 476 100       785 if ( defined $vars{$param} ) {
684 72         123 $vars{$param} =~ s/[^0-9]//g;
685             # 0 is an allowed value but the empty string isn't.
686 72 50       151 delete $vars{$param} if $vars{$param} eq "";
687             }
688             }
689              
690             # Latitude and longitude are also allowed '-' and '.'
691 68         130 foreach my $param( qw( latitude longitude ) ) {
692 136 100       306 if ( defined $vars{$param} ) {
693 26         139 $vars{$param} =~ s/[^-\.0-9]//g;
694             # 0 is an allowed value but the empty string isn't.
695 26 100       87 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     847 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         79 $self->{x} = $vars{os_x};
706 7         12 $self->{y} = $vars{os_y};
707 7         14 $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         36 $self->{x} = $vars{osie_x};
712 3         8 $self->{y} = $vars{osie_y};
713 3         7 $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       39 if ( $self->config->geo_handler eq 1 ) {
    100          
    50          
718 5         6483 require Geo::Coordinates::OSGB;
719             my ( $x, $y ) = Geo::Coordinates::OSGB::ll_to_grid(
720 5         6852 $vars{latitude}, $vars{longitude} );
721 5         579 $self->{x} = sprintf( "%d", $x );
722 5         13 $self->{y} = sprintf( "%d", $y );
723             } elsif ( $self->config->geo_handler eq 2 ) {
724 1         609 require Geo::Coordinates::ITM;
725             my ( $x, $y ) = Geo::Coordinates::ITM::ll_to_grid(
726 1         17346 $vars{latitude}, $vars{longitude} );
727 1         180 $self->{x} = sprintf( "%d", $x );
728 1         22 $self->{y} = sprintf( "%d", $y );
729             } elsif ( $self->config->geo_handler eq 3 ) {
730 6         1224 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         13593 );
736 6         521 $self->{x} = $x;
737 6         14 $self->{y} = $y;
738             }
739 12         42 $self->{distance_in_metres} = $vars{latlong_dist};
740             }
741              
742             # Store os_x etc so we can pass them to template.
743 68         235 foreach my $param ( qw( os_x os_y osie_x osie_y latitude longitude ) ) {
744 408         521 $self->{$param} = $vars{$param};
745             }
746              
747             # Strip leading and trailing whitespace from search text.
748 68   100     264 $vars{search} ||= ""; # avoid uninitialised value warning
749 68         302 $vars{search} =~ s/^\s*//;
750 68         298 $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     476 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         142 $self->{search_string} = $vars{search};
761              
762 68         146 return $self;
763             }
764              
765             # thin wrapper around OpenGuides::Template, or OpenGuides::Feed
766             sub process_template {
767 62     62 0 162 my ($self, %args) = @_;
768              
769 62   50     240 my $tt_vars = $args{tt_vars} || {};
770 62         140 $tt_vars->{not_editable} = 1;
771 62         120 $tt_vars->{not_deletable} = 1;
772 62 100       652 return %$tt_vars if $self->{return_tt_vars};
773              
774             # Do we want a feed, or TT html?
775 32         45 my $output;
776 32 100       83 if($tt_vars->{'format'}) {
777 2         4 my $format = $tt_vars->{'format'};
778 2         4 my @nodes = @{$tt_vars->{'results'}};
  2         4  
779              
780 2         7 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         11 );
789              
790 2         7 $output = "Content-Type: ".$feed->default_content_type($format)."\n";
791 2         8 $output .= $feed->build_mini_feed_for_nodes($format,@nodes);
792             } else {
793 30         101 $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       21108 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;