root/trunk/lib/MT/App/Search.pm @ 2991

Revision 2991, 30.1 kB (checked in by fumiakiy, 15 months ago)

Run through the process and do not return the error when search string was not specified to mt-search, to enable MTNoSearch tag again. BugId:81304

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