root/branches/release-41/lib/MT/App/Search.pm @ 2738

Revision 2738, 29.1 kB (checked in by fumiakiy, 17 months ago)

Typo!

  • Property svn:keywords set to Author Date Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::App::Search;
8
9use strict;
10use base qw( MT::App );
11
12use MT::Util qw( encode_html encode_url );
13
14sub id { 'new_search' }
15
16sub init {
17    my $app = shift;
18    $app->SUPER::init(@_) or return;
19    $app->set_no_cache;
20    $app->{default_mode} = 'default';
21
22    ## process pathinfo
23    #if ( my $pi = $app->path_info ) {
24    #    $pi =~ s!^/!!;
25    #    my ($mode, $tag, @args) = split /\//, $pi;
26    #    $app->mode($mode);
27    #    $app->param($mode, $tag);
28    #    for my $arg (@args) {
29    #        my ($k, $v) = split /=/, $arg, 2;
30    #        $app->param($k, $v);
31    #    }
32    #}
33    my $pkg = ref($app);
34    $app->_register_core_callbacks({
35        "${pkg}::search_post_execute" => \&_log_search,
36        "${pkg}::search_post_render"  => \&_cache_out,
37        "${pkg}::prepare_throttle"    => \&_default_throttle,
38        "${pkg}::take_down"           => \&_default_takedown,
39    });
40    $app;
41}
42
43sub core_methods {
44    my $app = shift;
45    return {
46        'default' => \&process,
47        'tag'     => '$Core::MT::App::Search::TagSearch::process',
48    };
49}
50
51sub core_parameters {
52    my $app = shift;
53    return {
54        params => [ qw( searchTerms search count limit startIndex offset
55            category author )],
56        types  => {
57            entry => {
58                columns => {
59                    title     => 'like',
60                    keywords  => 'like',
61                    text      => 'like',
62                    text_more => 'like'
63                },
64                'sort'  => 'authored_on',
65                terms   => { status => 2, class => '*' }, #MT::Entry::RELEASE()
66                filter_types => {
67                    author   => \&_join_author,
68                    category => \&_join_category,
69                },
70            },
71        },
72        cache_driver => {
73            'package' => 'MT::Cache::Negotiate',
74        },
75    };
76}
77
78sub init_request{
79    my $app = shift;
80    $app->SUPER::init_request(@_);
81
82    $app->mode('tag') if $app->param('tag');
83
84    my $q = $app->param;
85
86    my $params = $app->registry( $app->mode, 'params' );
87    foreach ( @$params ) {
88        delete $app->{$_} if exists $app->{$_}
89    }
90    delete $app->{__have_throttle} if exists $app->{__have_throttle};
91
92    my %no_override;
93    foreach my $no ( split /\s*,\s*/, $app->config->SearchNoOverride ) {
94        $no_override{ $no } = 1;
95        $no_override{ "Search$no" } = 1
96            if $no !~ /^Search.+/;
97    }
98
99    ## Set other search params--prefer per-query setting, default to
100    ## config file.
101    for my $key (qw( SearchResultDisplay SearchMaxResults SearchSortBy )) {
102        $app->{searchparam}{$key} = $no_override{$key} ?
103            $app->config->$key() : ($q->param($key) || $app->config->$key());
104    }
105
106    $app->{searchparam}{Type} = 'entry';
107    if ( my $type = $q->param('type') ) {
108        return $app->errtrans('Invalid type: [_1]', encode_html($type) )
109            if $type !~ /[\w\.]+/;
110        $app->{searchparam}{Type} = $type;
111    }
112
113    $app->generate_cache_keys();
114    $app->init_cache_driver();
115
116    my $processed = 0;
117    my $list      = {};
118    if ( $app->run_callbacks( 'search_blog_list', $app, $list, \$processed ) ) {
119        if ( $processed ) {
120            $app->{searchparam}{IncludeBlogs} = $list;
121        }
122        else {
123            my $blog_list = $app->create_blog_list( %no_override );
124            $app->{searchparam}{IncludeBlogs} = $blog_list->{IncludeBlogs}
125                if $blog_list && %$blog_list
126                && $blog_list->{IncludeBlogs}
127                && %{ $blog_list->{IncludeBlogs} };
128            if ( !exists($app->{searchparam}{IncludeBlogs})
129              && ( my $blog_id = $q->param('blog_id') ) ) {
130                $blog_id =~ s/\D//g;
131                $app->{searchparam}{IncludeBlogs}{$blog_id} = 1
132                    if $blog_id;
133            }
134        }
135    }
136    else {
137        return $app->error( $app->translate('Invalid request.') );
138    }
139}
140
141sub generate_cache_keys {
142    my $app = shift;
143
144    my $q = $app->param;
145    my @p = sort { $a cmp $b } $q->param;
146    my ( $key, $count_key );
147    $key .= lc($_) . encode_url($q->param($_))
148        foreach @p;
149    $count_key .= lc($_) . encode_url($q->param($_))
150        foreach grep { ('limit' ne lc($_)) && ('offset' ne lc($_)) } @p;
151    $app->{cache_keys} = { result => $key, count => $count_key };
152}
153
154sub init_cache_driver {
155    my $app = shift;
156
157    unless ( $app->config->SearchCacheTTL ) {
158        require MT::Cache::Null;
159        $app->{cache_driver} = MT::Cache::Null->new;
160        return;
161    }
162
163    my $registry = $app->registry( $app->mode, 'cache_driver' );
164    my $cache_driver = $registry->{'package'} || 'MT::Cache::Negotiate';
165    eval "require $cache_driver;";
166    if ( my $e = $@ ) {
167        require MT::Log;
168        $app->log({
169            message =>
170                $app->translate("Search: failed storing results in cache.  [_1] is not available: [_2]",
171                    $cache_driver, $e ),
172            level => MT::Log::INFO(),
173            class => 'search',
174        });
175        return;
176    }
177    $app->{cache_driver} = $cache_driver->new(
178        ttl => $app->config->SearchCacheTTL,
179        kind => 'CS',
180    );
181}
182
183sub create_blog_list {
184    my $app = shift;
185    my ( %no_override ) = @_;
186
187    my $q = $app->param;
188    my $cfg = $app->config;
189
190    unless ( %no_override ) {
191        my %no_override;
192        foreach my $no ( split /\s*,\s*/, $app->config->SearchNoOverride ) {
193            $no_override{ $no } = 1;
194            $no_override{ "Search$no" } = 1
195                if $no !~ /^Search.+/;
196        }
197    }
198
199    my %blog_list;
200    ## Combine user-selected included/excluded blogs
201    ## with config file settings.
202    for my $type (qw( IncludeBlogs ExcludeBlogs )) {
203        $blog_list{$type} = {};
204        if (my $list = $cfg->$type()) {
205            $blog_list{$type} =
206                { map { $_ => 1 } split /\s*,\s*/, $list };
207        }
208        next if exists($no_override{$type}) && $no_override{$type};
209        for my $blog_id ($q->param($type)) {
210            if ($blog_id =~ m/,/) {
211                my @ids = split /,/, $blog_id;
212                s/\D+//g for @ids; # only numeric values.
213                foreach my $id (@ids) {
214                    next unless $id;
215                    $blog_list{$type}{$id} = 1;
216                }
217            } else {
218                $blog_id =~ s/\D+//g; # only numeric values.
219                $blog_list{$type}{$blog_id} = 1;
220            }
221        }
222    }
223
224    ## If IncludeBlogs has not been set, we need to build a list of
225    ## the blogs to search. If ExcludeBlogs was set, exclude any blogs
226    ## set in that list from our final list.
227    unless ( exists $blog_list{IncludeBlogs} ) {
228        my $exclude = $blog_list{ExcludeBlogs};
229        my $iter = $app->model('blog')->load_iter;
230        while (my $blog = $iter->()) {
231            $blog_list{IncludeBlogs}{$blog->id} = 1
232                unless $exclude && $exclude->{$blog->id};
233        }
234    }
235
236    \%blog_list;
237}
238
239sub check_cache {
240    my $app = shift;
241
242    my $cache = $app->{cache_driver}->get_multi(
243        values %{ $app->{cache_keys} } );
244
245    my $count = $cache->{ $app->{cache_keys}{count} }
246        if exists $cache->{ $app->{cache_keys}{count} };
247    my $result = $cache->{ $app->{cache_keys}{result} }
248        if exists $cache->{ $app->{cache_keys}{result} };
249
250    ( $count, $result );
251}
252
253sub process {
254    my $app = shift;
255
256    my @messages;
257    return $app->throttle_response(\@messages) unless $app->throttle_control(\@messages);
258
259    my ( $count, $out ) = $app->check_cache();
260    if ( defined $out ) {
261        $app->run_callbacks( 'search_cache_hit', $count, $out );
262        return $out;
263    }
264
265    my @arguments = $app->search_terms();
266    return $app->error($app->errstr) if $app->errstr;
267
268    $count = 0;
269    my $iter;
270    if ( @arguments ) {
271        ( $count, $iter ) = $app->execute( @arguments );
272        return $app->error($app->errstr) unless $iter;
273
274        $app->run_callbacks( 'search_post_execute', $app, \$count, \$iter );
275    }
276
277    my $format = q();
278    if ( $format = $app->param('format') ) {
279        return $app->errtrans('Invalid format: [_1]', encode_html($format))
280            if $format !~ /\w+/;
281    }
282    my $method = "render";
283    $method .= $format if $format && $app->can($method . $format);
284    $out = $app->$method( $count, $iter );
285
286    my $result;
287    if (ref($out) && ($out->isa('MT::Template'))) {
288        defined( $result = $app->build_page($out) )
289            or return $app->error($out->errstr);
290    }
291    else {
292        $result = $out;
293    }
294
295    $app->run_callbacks( 'search_post_render', $app, $count, $result );
296    $result;
297}
298
299sub count {
300    my $app = shift;
301    my ( $class, $terms, $args ) = @_;
302    my $count = $app->{cache_driver}->get($app->{cache_keys}{count});
303    return $count if defined $count;
304
305    $count = $class->count( $terms, $args );
306    return $app->error($class->errstr) unless defined $count;
307
308    my $cache_driver = $app->{cache_driver};
309    $cache_driver->set( $app->{cache_keys}{count}, $count, $app->config->SearchCacheTTL );
310
311    $count;
312}
313
314sub execute {
315    my $app = shift;
316    my ( $terms, $args ) = @_;
317
318    my $class = $app->model( $app->{searchparam}{Type} )
319        or return $app->errtrans('Unsupported type: [_1]', encode_html($app->{searchparam}{Type}));
320
321    my $count = $app->count( $class, $terms, $args );
322    return $app->errtrans("Invalid query: [_1]", $app->errstr) unless defined $count;
323
324    my $iter = $class->load_iter( $terms, $args )
325        or $app->error($class->errstr);
326    ( $count, $iter );
327}
328
329sub search_terms {
330    my $app = shift;
331    my $q = $app->param;
332
333    my $search_string = $q->param('searchTerms') || $q->param('search')
334        or return $app->errtrans('No search term was specified.');
335    $app->{search_string} = $search_string;
336    my $offset = $q->param('startIndex') || $q->param('offset') || 0;
337    return $app->errtrans('Invalid value: [_1]', encode_html($offset))
338        if $offset && $offset !~ /^\d+$/;
339    my $limit = $q->param('count') || $q->param('limit');
340    return $app->errtrans('Invalid value: [_1]', encode_html($limit))
341        if $limit && $limit !~ /^\d+$/;
342    my $max = $app->{searchparam}{SearchMaxResults};
343    $max =~ s/\D//g if defined $max;
344    $limit = $max if !$limit || ( $limit - $offset > $max );
345
346    my $params = $app->registry( $app->mode, 'types', $app->{searchparam}{Type} );
347    my %def_terms = exists( $params->{terms} )
348          ? %{ $params->{terms} }
349          : ();
350    delete $def_terms{'plugin'}; #FIXME: why is this in here?
351
352    if ( exists $app->{searchparam}{IncludeBlogs} ) {
353        $def_terms{blog_id} = [ keys %{ $app->{searchparam}{IncludeBlogs} } ];
354    }
355
356    my @terms;
357    if (%def_terms) {
358        # If we have a term for the model's class column, add it separately, so
359        # array search() doesn't add the default class column term.
360        my $type = $app->{searchparam}{Type};
361        my $model_class = MT->model($type);
362        if (my $class_col = $model_class->properties->{class_column}) {
363            if ($def_terms{$class_col}) {
364                push @terms, { $class_col => delete $def_terms{$class_col} };
365            }
366        }
367
368        push @terms, \%def_terms;
369    }
370
371    my $columns = $params->{columns};
372    delete $columns->{'plugin'}; #FIXME: why is this in here?
373    return $app->errtrans('No column was specified to search for [_1].', $app->{searchparam}{Type})
374        unless $columns && %$columns;
375
376    my $parsed = $app->query_parse( %$columns );
377    return $app->errtrans('Invalid query: [_1]', encode_html($search_string))
378        unless $parsed && %$parsed;
379
380    push @terms, $parsed->{terms} if exists $parsed->{terms};
381
382    my $desc = 'descend' eq $app->{searchparam}{SearchResultDisplay} ? 'DESC' : 'ASC';
383    my @sort;
384    my $sort = $params->{'sort'};
385    if ( $sort !~ /\w+\!$/ && $app->{searchparam}{SearchSortBy} ) {
386        my $sort_by = $app->{searchparam}{SearchSortBy};
387        $sort_by =~ s/[^\w\-\.\,]+//g;
388        if ( $sort_by ) {
389            my @sort_bys = split ',', $sort_by;
390            foreach my $key ( @sort_bys ) {
391                push @sort, {
392                    desc   => $desc,
393                    column => $key
394                };
395            }
396        }
397    }
398    push @sort, {
399        desc   => $desc,
400        column => $sort
401    };
402
403    my %args = (
404      exists( $parsed->{args} ) ? %{ $parsed->{args} } : (),
405      $limit  ? ( 'limit' => $limit ) : (),
406      $offset ? ( 'offset' => $offset ) : (),
407      @sort   ? ( 'sort' => \@sort ) : (),
408    );
409
410    ( \@terms, \%args );
411}
412
413sub _cache_out {
414    my ( $cb, $app, $count, $out ) = @_;
415
416    my $result;
417    if (ref($out) && ($out->isa('MT::Template'))) {
418        defined($result = $app->build_page($out))
419            or die $out->errstr;
420    }
421    else {
422        $result = $out;
423    }
424
425    my $cache_driver = $app->{cache_driver};
426    $cache_driver->set( $app->{cache_keys}{result}, $out, $app->config->SearchCacheTTL );
427}
428
429sub _log_search {
430    my ( $cb, $app, $count_ref, $iter_ref ) = @_;
431
432    #FIXME: template name may not be 'feed' for search feed
433    unless ( $app->param('template') && ( 'feed' eq $app->param('template') ) ) {
434        my $blog_id = $app->first_blog_id();
435        require MT::Log;
436        $app->log({
437            message => $app->translate("Search: query for '[_1]'",
438                  $app->{search_string}),
439            level => MT::Log::INFO(),
440            class => 'search',
441            category => 'straight_search',
442            $blog_id ? (blog_id => $blog_id) : ()
443        });
444    }
445}
446
447sub template_paths {
448    my $app = shift;
449    my @paths = $app->SUPER::template_paths;
450    ( $app->config->SearchTemplatePath, @paths );
451}
452
453sub first_blog_id {
454    my $app = shift;
455    my $q = $app->param;
456
457    my $blog_id;
458    if ( $q->param('IncludeBlogs') ) {
459        my @ids = split ',', $q->param('IncludeBlogs');
460        $blog_id = $ids[0];
461    }
462    elsif ( exists($app->{searchparam}{IncludeBlogs})
463      && keys(%{ $app->{searchparam}{IncludeBlogs} }) ) {
464        my @blog_ids = keys %{ $app->{searchparam}{IncludeBlogs} };
465        $blog_id = $blog_ids[0] if @blog_ids;
466    }
467    $blog_id;
468}
469
470sub prepare_context {
471    my $app = shift;
472    my $q = $app->param;
473    my ( $count, $iter ) = @_;
474
475    ## Initialize and set up the context object.
476    require MT::Template::Context::Search;
477    my $ctx = MT::Template::Context::Search->new;
478    if ( my $str = $app->{search_string} ) {
479        $ctx->stash('search_string', encode_html($str));
480    }
481    if ( $q->param('type') ) {
482        $ctx->stash('type', $app->{searchparam}{Type});
483    }
484    if ( $app->{default_mode} ne $app->mode ) {
485        $ctx->stash('mode', $app->mode);
486    }
487    if ( my $template = $q->param('Template') ) {
488        $template =~ s/[^\w\-\.]//g;
489        $ctx->stash('template_id', $template);
490    }
491    $ctx->stash('stash_key'  , $app->{searchparam}{Type} );
492    $ctx->stash('maxresults' , $app->{searchparam}{SearchMaxResults});
493    $ctx->stash('include_blogs',
494        join ',', keys %{ $app->{searchparam}{IncludeBlogs} });
495    $ctx->stash('results'    , $iter);
496    $ctx->stash('count'      , $count);
497    $ctx->stash('offset'     , $q->param('startIndex') || $q->param('offset') || 0);
498    $ctx->stash('limit'      , $q->param('count') || $q->param('limit'));
499    $ctx->stash('format'     , $q->param('format')) if $q->param('format');
500
501    my $blog_id = $app->first_blog_id();
502    if ( $blog_id ) {
503        my $blog = $app->model('blog')->load($blog_id);
504        $app->blog( $blog );
505        $ctx->stash('blog_id', $blog_id);
506        $ctx->stash('blog',    $blog);
507    }
508    $ctx;
509}
510
511sub load_search_tmpl {
512    my $app = shift;
513    my $q = $app->param;
514    my ( $ctx ) = @_;
515
516    my $tmpl;
517    if ( $q->param('Template') && ( 'default' ne $q->param('Template') ) ) {
518        # load specified template
519        my $filename;
520        if (my @tmpls = (
521          $app->config->default('SearchAltTemplate'),
522          $app->config->SearchAltTemplate) ) {
523            for my $tmpl (@tmpls) {
524                next unless defined $tmpl;
525                my ( $nickname, $file ) = split /\s+/, $tmpl;
526                if ( $nickname eq $q->param('Template') ) {
527                    $filename = $file;
528                    last;
529                }
530            }
531        }
532        return $app->errtrans( "No alternate template is specified for the Template '[_1]'",
533          encode_html( $q->param('Template') ) )
534            unless $filename;
535        # template_paths method does the magic
536        $tmpl = $app->load_tmpl( $filename )
537            or return $app->errtrans( "Opening local file '[_1]' failed: [_2]", $filename, "$!" );
538    }
539    else {
540        # load default template
541        # first look for appropriate blog_id
542        if ( my $blog_id = $ctx->stash('blog_id') ) {
543            # look for 'search_results'
544            my $tmpl_class = $app->model('template');
545            $tmpl = $tmpl_class->load(
546                { blog_id => $blog_id, type => 'search_results' }
547            );
548        }
549        unless ( $tmpl ) {
550            # load template from search_template path
551            # template_paths method does the magic
552            $tmpl = $app->load_tmpl( $app->config->SearchDefaultTemplate );
553        }
554    }
555    return $app->error($app->errstr)
556        unless $tmpl;
557
558    $ctx->var('system_template', '1');
559    $ctx->var('search_results', '1');
560
561    $tmpl->context($ctx);
562    $tmpl;
563}
564
565sub render {
566    my $app = shift;
567    my ( $count, $iter ) = @_;
568
569    my @arguments = $app->prepare_context( $count, $iter )
570        or return $app->error($app->errstr);
571    my $tmpl = $app->load_search_tmpl( @arguments )
572        or return $app->error($app->errstr);
573    $tmpl;
574}
575
576sub renderjs {
577    my $app = shift;
578    my ( $count, $iter ) = @_;
579
580    my ( $ctx ) = $app->prepare_context( $count, $iter )
581        or return $app->json_error($app->errstr);
582    my $search_tmpl = $app->load_search_tmpl( $ctx )
583        or return $app->json_error($app->errstr);
584    my $result_node = $search_tmpl->getElementById('search_results')
585        or return $app->json_error('Search template does not have markup for search results.');
586    my $t = $result_node->innerHTML();
587
588    require MT::Template;
589    my $tmpl = MT::Template->new( type => 'scalarref', source => \$t );
590    $ctx->stash('format', q()); # don't propagate "js" format
591    $tmpl->context( $ctx );
592    my $content = $tmpl->output
593        or return $app->json_error($tmpl->errstr);
594
595    my $next_link = $ctx->_hdlr_next_link();
596    return $app->json_result({ content => $content, next_url => $next_link });
597}
598
599sub query_parse {
600    my $app = shift;
601    my ( %columns ) = @_;
602
603    my $search = $app->{search_string};
604
605    my $reg = $app->registry( $app->mode, 'types', $app->{searchparam}{Type} );
606    my $filter_types = $reg->{ 'filter_types' };
607    foreach my $type ( keys %$filter_types ) {
608        if ( my $filter = $app->param($type) ) {
609            $search .= " $type:$filter";
610        }
611    }
612
613    require Lucene::QueryParser;
614    my $lucene_struct = eval { Lucene::QueryParser::parse_query( $search ); };
615    return if $@;
616    my ( $terms, $joins ) = $app->_query_parse_core( $lucene_struct, \%columns, $filter_types );
617    my $return = {
618        $terms && @$terms ? (terms => $terms) : ()
619    };
620    if ( $joins && @$joins ) {
621        my $args = {};
622        _create_join_arg( $args, $joins );
623        if ( $args && %$args ) {
624            $return->{args} = $args;
625        }
626    }
627    $return;
628}
629
630sub _create_join_arg {
631    my ( $args, $joins ) = @_;
632    my $join = shift @$joins;
633    return unless $join && @$join;
634    my $next = $join->[3];
635    if ( defined $next ) {
636        if ( exists $next->{'join'} ) {
637            $next = $next->{'join'}->[3];
638        }
639    }
640    else {
641        $next = {};
642        $join->[3] = $next;
643    }
644    _create_join_arg($next, $joins);
645    $args->{'join'} = $join;
646}
647
648sub _query_parse_core {
649    my $app = shift;
650    my ( $lucene_struct, $columns, $filter_types ) = @_;
651
652    my $rvalue = sub {
653        my %rvalues = (
654            REQUIREDlike => { like => '%' . $_[1] . '%' },
655            REQUIRED1    => $_[1],
656            NORMALlike => { like => '%' . $_[1] . '%' },
657            NORMAL1    => $_[1],
658            PROHIBITEDlike => { not_like => '%' . $_[1] . '%' },
659            PROHIBITED1    => { not => $_[1] }
660        );
661        $rvalues{$_[0]};
662    };
663
664    my ( @structure, @joins );
665    while ( my $term = shift @$lucene_struct ) {
666        if ( exists $term->{field} ) {
667            unless ( exists $columns->{ $term->{field} } ) {
668                if ( $filter_types && %$filter_types
669                  && !exists( $filter_types->{ $term->{field} } ) ) {
670                    # Colon in query but was not to specify a field.
671                    # Treat it as a phrase including the colon.
672                    my $field = delete $term->{field};
673                    $term->{term} = $field . ':' . $term->{term};
674                    unshift @$lucene_struct, $term;
675                }
676            }
677        }
678
679        my @tmp;
680        if ( ( 'TERM' eq $term->{query} ) || ( 'PHRASE' eq $term->{query} ) ){
681            my $test;
682            if ( exists( $term->{field} ) ) {
683                if ( $filter_types && %$filter_types
684                  && exists( $filter_types->{ $term->{field} } ) ) {
685                    my $code = $app->handler_to_coderef($filter_types->{ $term->{field} });
686                    if ( $code ) {
687                        my $join_args = $code->( $app, $term );
688                        push @joins, $join_args;
689                        next;
690                    }
691                }
692                elsif ( exists $columns->{ $term->{field} } ) {
693                    my $test = $rvalue->(
694                        ( $term->{type} || '' ) . $columns->{ $term->{field} },
695                        $term->{term}
696                    );
697                    push @tmp, { $term->{field} => $test };
698                }
699            }
700            else {
701                my @cols = keys %$columns;
702                my $number = scalar @cols;
703                for ( my $i = 0; $i < $number; $i++ ) {
704                    my $test = $rvalue->(
705                        ( $term->{type} || '' ) . $columns->{ $cols[$i] },
706                        $term->{term}
707                    );
708                    if ( 'PROHIBITED' eq $term->{type} ) {
709                        my @this_term; 
710                        push @this_term, { $cols[$i] => $test };
711                        push @this_term, '-or';
712                        push @this_term, { $cols[$i] => \' IS NULL' };
713                        push @tmp, \@this_term;
714                        unless ( $i == $number - 1 ) {
715                            push @tmp, '-and';
716                        }
717                    }
718                    else {
719                        push @tmp, { $cols[$i] => $test };
720                        unless ( $i == $number - 1 ) {
721                            push @tmp, '-or';
722                        }
723                    }
724                }
725            }
726        }
727        elsif ( 'SUBQUERY' eq $term->{query} ) {
728            my ( $test, $more_joins ) = $app->_query_parse_core(
729                $term->{subquery}, $columns, $filter_types );
730            next unless $test && @$test;
731            if ( @structure ) {
732                push @structure, 'PROHIBITED' eq $term->{type}
733                  ? '-and_not'
734                  : '-and';
735            }
736            push @structure, @$test;
737            push @joins, @$more_joins;
738            next;
739        }
740
741        if ( exists($term->{conj}) && ( 'OR' eq $term->{conj} ) ) {
742            if ( my $prev = pop @structure ) {
743                push @structure, [ $prev, -or => \@tmp ];
744            }
745        }
746        else {
747            if ( @structure ) {
748                push @structure, '-and';
749            }
750            push @structure, \@tmp;
751        }
752    }
753    ( \@structure, \@joins );
754}
755
756# add category filter to entry search
757sub _join_category {
758    my ( $app, $term ) = @_;
759
760    my $query = $term->{term};
761    if ( 'PHRASE' eq $term->{query} ) {
762        $query =~ s/'/"/g;
763    }
764
765    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
766    if ( 'PROHIBITED' eq $term->{type} ) {
767        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
768    }
769    # search for exact match
770    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { label => 1 }, {} );
771    return unless $terms && @$terms;
772    push @$terms, '-and', {
773        id => \'= placement_category_id',
774        blog_id => \'= entry_blog_id',
775    };
776
777    require MT::Placement;
778    require MT::Category;
779    return MT::Placement->join_on( undef,
780        { entry_id => \'= entry_id', blog_id => \'= entry_blog_id' },
781        { join => MT::Category->join_on( undef, $terms, {} ),
782          unique => 1 }
783    );
784}
785
786# add author filter to entry search
787sub _join_author {
788    my ( $app, $term ) = @_;
789
790    my $query = $term->{term};
791    if ( 'PHRASE' eq $term->{query} ) {
792        $query =~ s/'/"/g;
793    }
794
795    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
796    if ( 'PROHIBITED' eq $term->{type} ) {
797        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
798    }
799    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { nickname => 'like' }, {} );
800    return unless $terms && @$terms;
801    push @$terms, '-and', {
802        id => \'= entry_author_id',
803    };
804    require MT::Author;
805    return MT::Author->join_on( undef,
806        $terms,
807        { unique => 1 }
808    );
809}
810
811# throttling related methods
812sub throttle_control {
813    my $app = shift;
814    my ( $messages ) = @_;
815    my $result;
816    $app->run_callbacks( 'prepare_throttle', $app, \$result, $messages );
817    $result;
818}
819
820sub throttle_response {
821    my $app = shift;
822    my ( $messages ) = @_;
823    my $tmpl = $app->param('Template') || '';
824    if ($tmpl eq 'feed') {
825        $app->response_code(503);
826        $app->set_header('Retry-After' => $app->config->SearchThrottleSeconds);
827        $app->send_http_header("text/plain");
828        $app->{no_print_body} = 1;
829    }
830    my $msg = $messages && @$messages
831      ? join '; ', @$messages
832      : $app->translate('The search you conducted has timed out.  Please simplify your query and try again.');
833    return $app->error($msg);
834}
835
836sub _default_throttle {
837    my ( $cb, $app, $result, $messages ) = @_;
838
839    # Don't bother if a callback proiritized higher
840    # set up its throttle already
841    return $$result if defined $$result;
842
843    ## Get login information if user is logged in (via cookie).
844    ## If no login cookie, this fails silently, and that's fine.
845    ($app->{user}) = $app->login;
846
847    ## Don't throttle MT registered users
848    if ( $app->{user} && $app->{user}->type == MT::Author::AUTHOR() ) {
849        $$result = 1;
850        return 1;
851    }
852
853    my $ip = $app->remote_ip;
854    my $whitelist = $app->config->SearchThrottleIPWhitelist;
855    if ($whitelist) {
856        # check for $ip in $whitelist
857        my @list = split /(\s*[,;]\s*|\s+)/, $whitelist;
858        foreach (@list) {
859            next unless $_ =~ m/^\d{1,3}(\.\d{0,3}){0,3}$/;
860            if (($ip eq $_) || ($ip =~ m/^\Q$_\E/)) {
861                $$result = 1;
862                return 1;
863            }
864        }
865    }
866
867    unless ( $^O eq 'Win32' ) {
868        # Use SIGALRM to stop processing in 5 seconds for each request
869        $SIG{ALRM} = sub {
870            my $msg = $app->translate('The search you conducted has timed out.  Please simplify your query and try again.');
871            $app->error($msg);
872            die $msg;
873        };
874        $app->{__have_throttle} = 1;
875        alarm($app->config->SearchThrottleSeconds);
876        $$result = 1;
877    }
878    1;
879}
880
881sub _default_takedown {
882    my ( $cb, $app ) = @_;
883    alarm(0) if $app->{__have_throttle};
884    if ( my $cache_driver = $app->{cache_driver} ) {
885        $cache_driver->purge_stale( 2 * $app->config->SearchCacheTTL );
886    }
887    1;
888}
889
8901;
891__END__
892
893=head1 NAME
894
895MT::App::Search
896
897=head1 Callbacks
898
899Callbacks called by the package are as follows:
900
901=over 4
902
903=item search_post_execute
904
905    callback($cb, $app, \$count, \$iter)
906
907Called immediately after the search from the database (or however
908search executed depending on the algorithm).
909
910=item search_post_render
911
912    callback($cb, $app, $count, $out_html)
913
914Called immediately after the search template was loaded and its
915context populated.
916
917=item search_cache_hit
918
919    callback($cb, $app, $count, $out_html)
920
921Called immediately after cached results was retrieved.
922
923=item search_blog_list
924
925    callback($cb, $app, \%list, \$processed)
926
927Called during init_request in which a plugin can fill %list.
928The list must has the following data structure.
929
930    %list = ( 1 => 1, 2 => 1, 3 => 1 );
931
932where the hash keys (1, 2, and 3) are the IDs of the blogs to search for.
933
934Plugins must also set $processed = 1 in order to specify the app that
935the app must not overwrite the blog list created by the plugin.
936
937=item prepare_throttle
938
939    callback($cb, $app, \$result, \@messages);
940
941Called right before the beginning of the search processing.
942Each callback should see if certain condition is met, and
943set 0 to $$result if the request should be throttled.
944
945There can be more than one throttling method set up.
946Callbacks are called in order of priority set up when add_callback
947was called.  Each callback should start its own code by something like
948below, to prevent itself overwriting throttle set up in the callback
949whose priority is higher than itself.
950
951    return $$result if defined $$result;
952
953=head1 AUTHOR & COPYRIGHT
954
955Please see L<MT/AUTHOR & COPYRIGHT>.
956
957=cut
Note: See TracBrowser for help on using the browser.