root/branches/release-33/lib/MT/App/Search.pm @ 1726

Revision 1726, 24.2 kB (checked in by bchoate, 20 months ago)

Set template identifier variable and system_template for dynamic error and search result templates. BugId:69530

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