root/branches/release-34/lib/MT/App/Search.pm @ 1886

Revision 1886, 28.4 kB (checked in by fumiakiy, 20 months ago)

NOT and "+/-" query now works as it should. BugId:79274, BugId:79260

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