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

Revision 1883, 27.7 kB (checked in by fumiakiy, 20 months ago)

Got rid of references to ThrottleSeconds and use new, appropriate config directives in search.

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