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

Revision 1708, 24.1 kB (checked in by fumiakiy, 20 months ago)

Added ability to filter search by author and/or category. BugId:69286

  • 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    $tmpl->context($ctx);
527    $tmpl;
528}
529
530sub render {
531    my $app = shift;
532    my ( $count, $iter ) = @_;
533
534    my @arguments = $app->prepare_context( $count, $iter )
535        or return $app->error($app->errstr);
536    my $tmpl = $app->load_search_tmpl( @arguments )
537        or return $app->error($app->errstr);
538    $tmpl;
539}
540
541sub renderjs {
542    my $app = shift;
543    my ( $count, $iter ) = @_;
544
545    my ( $ctx ) = $app->prepare_context( $count, $iter )
546        or return $app->json_error($app->errstr);
547    my $search_tmpl = $app->load_search_tmpl( $ctx )
548        or return $app->json_error($app->errstr);
549    my $result_node = $search_tmpl->getElementById('search_results')
550        or return $app->json_error('Search template does not have markup for search results.');
551    my $t = $result_node->innerHTML();
552
553    require MT::Template;
554    my $tmpl = MT::Template->new( type => 'scalarref', source => \$t );
555    $ctx->stash('format', q()); # don't propagate "js" format
556    $tmpl->context( $ctx );
557    my $content = $tmpl->output
558        or return $app->json_error($tmpl->errstr);
559
560    my $next_link = $ctx->_hdlr_next_link();
561    return $app->json_result({ content => $content, next_url => $next_link });
562}
563
564sub query_parse {
565    my $app = shift;
566    my ( %columns ) = @_;
567
568    my $search = $app->{search_string};
569
570    my $reg = $app->registry( $app->mode, 'types', $app->{searchparam}{Type} );
571    my $filter_types = $reg->{ 'filter_types' };
572    foreach my $type ( keys %$filter_types ) {
573        if ( my $filter = $app->param($type) ) {
574            $search .= " $type:$filter";
575        }
576    }
577
578    require Lucene::QueryParser;
579    my $lucene_struct = Lucene::QueryParser::parse_query( $search );
580    my ( $terms, $joins ) = $app->_query_parse_core( $lucene_struct, \%columns, $filter_types );
581    my $return = {
582        $terms && @$terms ? (terms => $terms) : ()
583    };
584    if ( $joins && @$joins ) {
585        my $args = {};
586        _create_join_arg( $args, $joins );
587        if ( $args && %$args ) {
588            $return->{args} = $args;
589        }
590    }
591    $return;
592}
593
594sub _create_join_arg {
595    my ( $args, $joins ) = @_;
596    my $join = shift @$joins;
597    return unless $join && @$join;
598    my $next = $join->[3];
599    if ( defined $next ) {
600        if ( exists $next->{'join'} ) {
601            $next = $next->{'join'}->[3];
602        }
603    }
604    else {
605        $next = {};
606        $join->[3] = $next;
607    }
608    _create_join_arg($next, $joins);
609    $args->{'join'} = $join;
610}
611
612sub _query_parse_core {
613    my $app = shift;
614    my ( $lucene_struct, $columns, $filter_types ) = @_;
615
616    my $rvalue = sub {
617        my %rvalues = (
618            NORMALlike => { like => '%' . $_[1] . '%' },
619            NORMAL1    => $_[1],
620            PROHIBITEDlike => { not_like => '%' . $_[1] . '%' },
621            PROHIBITED1    => { not => $_[1] }
622        );
623        $rvalues{$_[0]};
624    };
625
626    my ( @structure, @joins );
627    for my $term ( @$lucene_struct ) {
628        if ( exists $term->{field} ) {
629            unless ( exists $columns->{ $term->{field} } ) {
630                next if $filter_types && %$filter_types
631                    && !exists( $filter_types->{ $term->{field} } );
632            }
633        }
634
635        my @tmp;
636        if ( ( 'TERM' eq $term->{query} ) || ( 'PHRASE' eq $term->{query} ) ){
637            my $test;
638            if ( exists( $term->{field} ) ) {
639                if ( $filter_types && %$filter_types
640                  && exists( $filter_types->{ $term->{field} } ) ) {
641                    my $code = $app->handler_to_coderef($filter_types->{ $term->{field} });
642                    if ( $code ) {
643                        my $join_args = $code->( $app, $term );
644                        push @joins, $join_args;
645                        next;
646                    }
647                }
648                elsif ( exists $columns->{ $term->{field} } ) {
649                    my $test = $rvalue->(
650                        ( $term->{type} || '' ) . $columns->{ $term->{field} },
651                        $term->{term}
652                    );
653                    push @tmp, { $term->{field} => $test };
654                }
655            }
656            else {
657                my @cols = keys %$columns;
658                my $number = scalar @cols;
659                for ( my $i = 0; $i < $number; $i++ ) {
660                    my $test = $rvalue->(
661                        ( $term->{type} || '' ) . $columns->{ $cols[$i] },
662                        $term->{term}
663                    );
664                    push @tmp, { $cols[$i] => $test };
665                    unless ( $i == $number - 1 ) {
666                        push @tmp, '-or';
667                    }
668                }
669            }
670        }
671        elsif ( 'SUBQUERY' eq $term->{query} ) {
672            my ( $test, $more_joins ) = $app->_query_parse_core(
673                $term->{subquery}, $columns, $filter_types );
674            next unless $test && @$test;
675            if ( @structure ) {
676                push @structure, 'PROHIBITED' eq $term->{type}
677                  ? '-and_not'
678                  : '-and';
679            }
680            push @structure, $test->[0];
681            push @joins, @$more_joins;
682            next;
683        }
684
685        if ( exists($term->{conj}) && ( 'OR' eq $term->{conj} ) ) {
686            if ( my $prev = pop @structure ) {
687                push @structure, [ $prev, -or => \@tmp ];
688            }
689        }
690        else {
691            if ( @structure ) {
692                push @structure, '-and';
693            }
694            push @structure, \@tmp;
695        }
696    }
697    ( \@structure, \@joins );
698}
699
700# add category filter to entry search
701sub _join_category {
702    my ( $app, $term ) = @_;
703
704    my $query = $term->{term};
705    if ( 'PHRASE' eq $term->{query} ) {
706        $query =~ s/'/"/g;
707    }
708
709    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
710    if ( 'PROHIBITED' eq $term->{type} ) {
711        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
712    }
713    # search for exact match
714    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { label => 1 }, {} );
715    return unless $terms && @$terms;
716    push @$terms, '-and', {
717        id => \'= placement_category_id',
718        blog_id => \'= entry_blog_id',
719    };
720
721    require MT::Placement;
722    require MT::Category;
723    return MT::Placement->join_on( undef,
724        { entry_id => \'= entry_id', blog_id => \'= entry_blog_id' },
725        { join => MT::Category->join_on( undef, $terms, {} ),
726          unique => 1 }
727    );
728}
729
730# add author filter to entry search
731sub _join_author {
732    my ( $app, $term ) = @_;
733
734    my $query = $term->{term};
735    if ( 'PHRASE' eq $term->{query} ) {
736        $query =~ s/'/"/g;
737    }
738
739    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
740    if ( 'PROHIBITED' eq $term->{type} ) {
741        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
742    }
743    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { nickname => 'like' }, {} );
744    return unless $terms && @$terms;
745    push @$terms, '-and', {
746        id => \'= entry_author_id',
747    };
748    require MT::Author;
749    return MT::Author->join_on( undef,
750        $terms,
751        { unique => 1 }
752    );
753}
754
7551;
756__END__
757
758=head1 NAME
759
760MT::App::Search
761
762=head1 Callbacks
763
764Callbacks called by the package are as follows:
765
766=over 4
767
768=item search_post_execute
769
770    callback($cb, $app, \$count, \$iter)
771
772Called immediately after the search from the database (or however
773search executed depending on the algorithm).
774
775=item search_post_render
776
777    callback($cb, $app, $count, $out_html)
778
779Called immediately after the search template was loaded and its
780context populated.
781
782=item search_cache_hit
783
784    callback($cb, $app, $count, $out_html)
785
786Called immediately after cached results was retrieved.
787
788=item search_blog_list
789
790    callback($cb, $app, \%list, \$processed)
791
792Called during init_request in which a plugin can fill %list.
793The list must has the following data structure.
794
795    %list = ( 1 => 1, 2 => 1, 3 => 1 );
796
797where the hash keys (1, 2, and 3) are the IDs of the blogs to search for.
798
799Plugins must also set $processed = 1 in order to specify the app that
800the app must not overwrite the blog list created by the plugin.
801
802=head1 AUTHOR & COPYRIGHT
803
804Please see L<MT/AUTHOR & COPYRIGHT>.
805
806=cut
Note: See TracBrowser for help on using the browser.