root/branches/release-35/lib/MT/App/Search.pm @ 2003

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

L10 Japanese the Cal release, take one.

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