root/branches/release-38/lib/MT/App/Search.pm @ 2318

Revision 2318, 29.1 kB (checked in by fumiakiy, 19 months ago)

Improved the error message returned when the request was throttled. BugId:50084, BugId:66624

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