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

Revision 2438, 28.9 kB (checked in by fumiakiy, 18 months ago)

Removed inline comments in favor of documents.

  • 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        $ctx->stash('blog_id', $blog_id);
499        $ctx->stash('blog',    $app->model('blog')->load($blog_id));
500    }
501    $ctx;
502}
503
504sub load_search_tmpl {
505    my $app = shift;
506    my $q = $app->param;
507    my ( $ctx ) = @_;
508
509    my $tmpl;
510    if ( $q->param('Template') && ( 'default' ne $q->param('Template') ) ) {
511        # load specified template
512        my $filename;
513        if (my @tmpls = (
514          $app->config->default('SearchAltTemplate'),
515          $app->config->SearchAltTemplate) ) {
516            for my $tmpl (@tmpls) {
517                next unless defined $tmpl;
518                my ( $nickname, $file ) = split /\s+/, $tmpl;
519                if ( $nickname eq $q->param('Template') ) {
520                    $filename = $file;
521                    last;
522                }
523            }
524        }
525        return $app->errtrans( "No alternate template is specified for the Template '[_1]'",
526          encode_html( $q->param('Template') ) )
527            unless $filename;
528        # template_paths method does the magic
529        $tmpl = $app->load_tmpl( $filename )
530            or return $app->errtrans( "Opening local file '[_1]' failed: [_2]", $filename, "$!" );
531    }
532    else {
533        # load default template
534        # first look for appropriate blog_id
535        if ( my $blog_id = $ctx->stash('blog_id') ) {
536            # look for 'search_results'
537            my $tmpl_class = $app->model('template');
538            $tmpl = $tmpl_class->load(
539                { blog_id => $blog_id, type => 'search_results' }
540            );
541        }
542        unless ( $tmpl ) {
543            # load template from search_template path
544            # template_paths method does the magic
545            $tmpl = $app->load_tmpl( $app->config->SearchDefaultTemplate );
546        }
547    }
548    return $app->error($app->errstr)
549        unless $tmpl;
550
551    $ctx->var('system_template', '1');
552    $ctx->var('search_results', '1');
553
554    $tmpl->context($ctx);
555    $tmpl;
556}
557
558sub render {
559    my $app = shift;
560    my ( $count, $iter ) = @_;
561
562    my @arguments = $app->prepare_context( $count, $iter )
563        or return $app->error($app->errstr);
564    my $tmpl = $app->load_search_tmpl( @arguments )
565        or return $app->error($app->errstr);
566    $tmpl;
567}
568
569sub renderjs {
570    my $app = shift;
571    my ( $count, $iter ) = @_;
572
573    my ( $ctx ) = $app->prepare_context( $count, $iter )
574        or return $app->json_error($app->errstr);
575    my $search_tmpl = $app->load_search_tmpl( $ctx )
576        or return $app->json_error($app->errstr);
577    my $result_node = $search_tmpl->getElementById('search_results')
578        or return $app->json_error('Search template does not have markup for search results.');
579    my $t = $result_node->innerHTML();
580
581    require MT::Template;
582    my $tmpl = MT::Template->new( type => 'scalarref', source => \$t );
583    $ctx->stash('format', q()); # don't propagate "js" format
584    $tmpl->context( $ctx );
585    my $content = $tmpl->output
586        or return $app->json_error($tmpl->errstr);
587
588    my $next_link = $ctx->_hdlr_next_link();
589    return $app->json_result({ content => $content, next_url => $next_link });
590}
591
592sub query_parse {
593    my $app = shift;
594    my ( %columns ) = @_;
595
596    my $search = $app->{search_string};
597
598    my $reg = $app->registry( $app->mode, 'types', $app->{searchparam}{Type} );
599    my $filter_types = $reg->{ 'filter_types' };
600    foreach my $type ( keys %$filter_types ) {
601        if ( my $filter = $app->param($type) ) {
602            $search .= " $type:$filter";
603        }
604    }
605
606    require Lucene::QueryParser;
607    my $lucene_struct = eval { Lucene::QueryParser::parse_query( $search ); };
608    return if $@;
609    my ( $terms, $joins ) = $app->_query_parse_core( $lucene_struct, \%columns, $filter_types );
610    my $return = {
611        $terms && @$terms ? (terms => $terms) : ()
612    };
613    if ( $joins && @$joins ) {
614        my $args = {};
615        _create_join_arg( $args, $joins );
616        if ( $args && %$args ) {
617            $return->{args} = $args;
618        }
619    }
620    $return;
621}
622
623sub _create_join_arg {
624    my ( $args, $joins ) = @_;
625    my $join = shift @$joins;
626    return unless $join && @$join;
627    my $next = $join->[3];
628    if ( defined $next ) {
629        if ( exists $next->{'join'} ) {
630            $next = $next->{'join'}->[3];
631        }
632    }
633    else {
634        $next = {};
635        $join->[3] = $next;
636    }
637    _create_join_arg($next, $joins);
638    $args->{'join'} = $join;
639}
640
641sub _query_parse_core {
642    my $app = shift;
643    my ( $lucene_struct, $columns, $filter_types ) = @_;
644
645    my $rvalue = sub {
646        my %rvalues = (
647            REQUIREDlike => { like => '%' . $_[1] . '%' },
648            REQUIRED1    => $_[1],
649            NORMALlike => { like => '%' . $_[1] . '%' },
650            NORMAL1    => $_[1],
651            PROHIBITEDlike => { not_like => '%' . $_[1] . '%' },
652            PROHIBITED1    => { not => $_[1] }
653        );
654        $rvalues{$_[0]};
655    };
656
657    my ( @structure, @joins );
658    while ( my $term = shift @$lucene_struct ) {
659        if ( exists $term->{field} ) {
660            unless ( exists $columns->{ $term->{field} } ) {
661                if ( $filter_types && %$filter_types
662                  && !exists( $filter_types->{ $term->{field} } ) ) {
663                    # Colon in query but was not to specify a field.
664                    # Treat it as a phrase including the colon.
665                    my $field = delete $term->{field};
666                    $term->{term} = $field . ':' . $term->{term};
667                    unshift @$lucene_struct, $term;
668                }
669            }
670        }
671
672        my @tmp;
673        if ( ( 'TERM' eq $term->{query} ) || ( 'PHRASE' eq $term->{query} ) ){
674            my $test;
675            if ( exists( $term->{field} ) ) {
676                if ( $filter_types && %$filter_types
677                  && exists( $filter_types->{ $term->{field} } ) ) {
678                    my $code = $app->handler_to_coderef($filter_types->{ $term->{field} });
679                    if ( $code ) {
680                        my $join_args = $code->( $app, $term );
681                        push @joins, $join_args;
682                        next;
683                    }
684                }
685                elsif ( exists $columns->{ $term->{field} } ) {
686                    my $test = $rvalue->(
687                        ( $term->{type} || '' ) . $columns->{ $term->{field} },
688                        $term->{term}
689                    );
690                    push @tmp, { $term->{field} => $test };
691                }
692            }
693            else {
694                my @cols = keys %$columns;
695                my $number = scalar @cols;
696                for ( my $i = 0; $i < $number; $i++ ) {
697                    my $test = $rvalue->(
698                        ( $term->{type} || '' ) . $columns->{ $cols[$i] },
699                        $term->{term}
700                    );
701                    if ( 'PROHIBITED' eq $term->{type} ) {
702                        my @this_term; 
703                        push @this_term, { $cols[$i] => $test };
704                        push @this_term, '-or';
705                        push @this_term, { $cols[$i] => \' IS NULL' };
706                        push @tmp, \@this_term;
707                        unless ( $i == $number - 1 ) {
708                            push @tmp, '-and';
709                        }
710                    }
711                    else {
712                        push @tmp, { $cols[$i] => $test };
713                        unless ( $i == $number - 1 ) {
714                            push @tmp, '-or';
715                        }
716                    }
717                }
718            }
719        }
720        elsif ( 'SUBQUERY' eq $term->{query} ) {
721            my ( $test, $more_joins ) = $app->_query_parse_core(
722                $term->{subquery}, $columns, $filter_types );
723            next unless $test && @$test;
724            if ( @structure ) {
725                push @structure, 'PROHIBITED' eq $term->{type}
726                  ? '-and_not'
727                  : '-and';
728            }
729            push @structure, @$test;
730            push @joins, @$more_joins;
731            next;
732        }
733
734        if ( exists($term->{conj}) && ( 'OR' eq $term->{conj} ) ) {
735            if ( my $prev = pop @structure ) {
736                push @structure, [ $prev, -or => \@tmp ];
737            }
738        }
739        else {
740            if ( @structure ) {
741                push @structure, '-and';
742            }
743            push @structure, \@tmp;
744        }
745    }
746    ( \@structure, \@joins );
747}
748
749# add category filter to entry search
750sub _join_category {
751    my ( $app, $term ) = @_;
752
753    my $query = $term->{term};
754    if ( 'PHRASE' eq $term->{query} ) {
755        $query =~ s/'/"/g;
756    }
757
758    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
759    if ( 'PROHIBITED' eq $term->{type} ) {
760        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
761    }
762    # search for exact match
763    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { label => 1 }, {} );
764    return unless $terms && @$terms;
765    push @$terms, '-and', {
766        id => \'= placement_category_id',
767        blog_id => \'= entry_blog_id',
768    };
769
770    require MT::Placement;
771    require MT::Category;
772    return MT::Placement->join_on( undef,
773        { entry_id => \'= entry_id', blog_id => \'= entry_blog_id' },
774        { join => MT::Category->join_on( undef, $terms, {} ),
775          unique => 1 }
776    );
777}
778
779# add author filter to entry search
780sub _join_author {
781    my ( $app, $term ) = @_;
782
783    my $query = $term->{term};
784    if ( 'PHRASE' eq $term->{query} ) {
785        $query =~ s/'/"/g;
786    }
787
788    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
789    if ( 'PROHIBITED' eq $term->{type} ) {
790        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
791    }
792    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { nickname => 'like' }, {} );
793    return unless $terms && @$terms;
794    push @$terms, '-and', {
795        id => \'= entry_author_id',
796    };
797    require MT::Author;
798    return MT::Author->join_on( undef,
799        $terms,
800        { unique => 1 }
801    );
802}
803
804# throttling related methods
805sub throttle_control {
806    my $app = shift;
807    my ( $messages ) = @_;
808    my $result;
809    $app->run_callbacks( 'prepare_throttle', $app, \$result, $messages );
810    $result;
811}
812
813sub throttle_response {
814    my $app = shift;
815    my ( $messages ) = @_;
816    my $tmpl = $app->param('Template') || '';
817    if ($tmpl eq 'feed') {
818        $app->response_code(503);
819        $app->set_header('Retry-After' => $app->config->SearchThrottleSeconds);
820        $app->send_http_header("text/plain");
821        $app->{no_print_body} = 1;
822    }
823    my $msg = $messages && @$messages
824      ? join '; ', @$messages
825      : $app->translate('The search you conducted has timed out.  Please simplify your query and try again.');
826    return $app->error($msg);
827}
828
829sub _default_throttle {
830    my ( $cb, $app, $result, $messages ) = @_;
831
832    # Don't bother if a callback proiritized higher
833    # set up its throttle already
834    return $$result if defined $$result;
835
836    ## Get login information if user is logged in (via cookie).
837    ## If no login cookie, this fails silently, and that's fine.
838    ($app->{user}) = $app->login;
839
840    ## Don't throttle MT registered users
841    if ( $app->{user} && $app->{user}->type == MT::Author::AUTHOR() ) {
842        $$result = 1;
843        return 1;
844    }
845
846    my $ip = $app->remote_ip;
847    my $whitelist = $app->config->SearchThrottleIPWhitelist;
848    if ($whitelist) {
849        # check for $ip in $whitelist
850        my @list = split /(\s*[,;]\s*|\s+)/, $whitelist;
851        foreach (@list) {
852            next unless $_ =~ m/^\d{1,3}(\.\d{0,3}){0,3}$/;
853            if (($ip eq $_) || ($ip =~ m/^\Q$_\E/)) {
854                $$result = 1;
855                return 1;
856            }
857        }
858    }
859
860    unless ( $^O eq 'Win32' ) {
861        # Use SIGALRM to stop processing in 5 seconds for each request
862        $SIG{ALRM} = sub {
863            my $msg = $app->translate('The search you conducted has timed out.  Please simplify your query and try again.');
864            $app->error($msg);
865            die $msg;
866        };
867        $app->{__have_throttle} = 1;
868        alarm($app->config->SearchThrottleSeconds);
869        $$result = 1;
870    }
871    1;
872}
873
874sub _default_takedown {
875    my ( $cb, $app ) = @_;
876    alarm(0) if $app->{__have_throttle};
877    1;
878}
879
8801;
881__END__
882
883=head1 NAME
884
885MT::App::Search
886
887=head1 Callbacks
888
889Callbacks called by the package are as follows:
890
891=over 4
892
893=item search_post_execute
894
895    callback($cb, $app, \$count, \$iter)
896
897Called immediately after the search from the database (or however
898search executed depending on the algorithm).
899
900=item search_post_render
901
902    callback($cb, $app, $count, $out_html)
903
904Called immediately after the search template was loaded and its
905context populated.
906
907=item search_cache_hit
908
909    callback($cb, $app, $count, $out_html)
910
911Called immediately after cached results was retrieved.
912
913=item search_blog_list
914
915    callback($cb, $app, \%list, \$processed)
916
917Called during init_request in which a plugin can fill %list.
918The list must has the following data structure.
919
920    %list = ( 1 => 1, 2 => 1, 3 => 1 );
921
922where the hash keys (1, 2, and 3) are the IDs of the blogs to search for.
923
924Plugins must also set $processed = 1 in order to specify the app that
925the app must not overwrite the blog list created by the plugin.
926
927=item prepare_throttle
928
929    callback($cb, $app, \$result, \@messages);
930
931Called right before the beginning of the search processing.
932Each callback should see if certain condition is met, and
933set 0 to $$result if the request should be throttled.
934
935There can be more than one throttling method set up.
936Callbacks are called in order of priority set up when add_callback
937was called.  Each callback should start its own code by something like
938below, to prevent itself overwriting throttle set up in the callback
939whose priority is higher than itself.
940
941    return $$result if defined $$result;
942
943=head1 AUTHOR & COPYRIGHT
944
945Please see L<MT/AUTHOR & COPYRIGHT>.
946
947=cut
Note: See TracBrowser for help on using the browser.