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

Revision 2463, 28.9 kB (checked in by bchoate, 18 months ago)

Updates to optimize recently_commented_on, category and tag attributes for Entries tag - BugId:79914. Search app can now supply user state - BugId:79906. Fix for 2-digit year bug - BugId:79924

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