root/branches/release-39/lib/MT/App/Search.pm @ 2505

Revision 2505, 29.1 kB (checked in by fumiakiy, 18 months ago)

Purge stale search cache after it finishes processing, and also periodically. BugId:79999

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