Changeset 1420

Show
Ignore:
Timestamp:
02/28/08 11:45:17 (5 months ago)
Author:
fumiakiy
Message:

Initial attempt to pluggable, extensible and hopefully faster search framework. BugId:69030, BugId:69029, BugId:69031

mt-search.cgi still defaults to legacy search application for now.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • branches/release-30/lib/MT/App/Search.pm

    r1308 r1420  
    55# $Id$ 
    66 
    7 # Original Copyright 2001-2002 Jay Allen. 
    8 # Modifications and integration Copyright 2001-2008 Six Apart. 
    9  
    107package MT::App::Search; 
    118 
     
    1613use MT::Util qw(encode_html ts2epoch epoch2ts); 
    1714use HTTP::Date qw(str2time time2str); 
    18  
    19 sub id { 'search' } 
     15use MT::Entry qw( :constants ); 
     16 
     17sub id { 'new_search' } 
    2018 
    2119sub init { 
     
    2321    $app->SUPER::init(@_) or return; 
    2422    $app->set_no_cache; 
    25     $app->add_methods( search => \&execute ); 
    2623    $app->{default_mode} = 'search'; 
    2724    $app; 
    2825} 
    2926 
    30 sub load_core_tags { 
    31     require MT::Template::Context; 
     27sub core_methods { 
     28    my $app = shift; 
    3229    return { 
    33         function => { 
    34             SearchString => \&MT::App::Search::Context::_hdlr_search_string, 
    35             SearchResultCount => \&MT::App::Search::Context::_hdlr_result_count, 
    36             MaxResults => \&MT::App::Search::Context::_hdlr_max_results, 
    37             SearchIncludeBlogs => \&MT::App::Search::Context::_hdlr_include_blogs, 
    38             SearchTemplateID => \&MT::App::Search::Context::_hdlr_template_id, 
    39         }, 
    40         block => { 
    41             SearchResults => \&MT::App::Search::Context::_hdlr_results, 
    42  
    43             'IfTagSearch?' => sub { MT->instance->{searchparam}{Type} eq 'tag' }, 
    44             'IfStraightSearch?' => sub { MT->instance->{searchparam}{Type} eq 'straight' }, 
    45             NoSearchResults => \&MT::Template::Context::_hdlr_pass_tokens, 
    46             NoSearch => \&MT::Template::Context::_hdlr_pass_tokens, 
    47             BlogResultHeader => \&MT::Template::Context::_hdlr_pass_tokens, 
    48             BlogResultFooter => \&MT::Template::Context::_hdlr_pass_tokens, 
    49             IfMaxResultsCutoff => \&MT::Template::Context::_hdlr_pass_tokens, 
    50         }, 
     30        'search' => \&process, 
     31        'author' => \&process, 
    5132    }; 
    5233} 
    5334 
     35sub core_query_params { 
     36    my $app = shift; 
     37    return [ 
     38        'searchTerms', 
     39        'search', 
     40        'count', 
     41        'limit', 
     42        'startIndex', 
     43        'offset', 
     44    ]; 
     45} 
     46 
    5447sub init_request{ 
    5548    my $app = shift; 
    5649    $app->SUPER::init_request(@_); 
    57  
    58     foreach (qw(searchparam templates search_string results 
    59                 user __have_throttle)) { 
     50    my $q = $app->param; 
     51 
     52    my $params = $app->registry('params'); 
     53    foreach ( @$params ) { 
    6054        delete $app->{$_} if exists $app->{$_} 
    6155    } 
     56 
     57    my %no_override;# = map { $_ => 1 } split /\s*,\s*/, $app->config->NoOverride; 
     58    my $blog_list = $app->create_blog_list( %no_override ); 
     59    $app->{searchparam}{IncludeBlogs} = $blog_list->{IncludeBlogs} 
     60        if $blog_list && %$blog_list 
     61        && $blog_list->{IncludeBlogs} 
     62        && %{ $blog_list->{IncludeBlogs} }; 
     63 
     64    ## Set other search params--prefer per-query setting, default to 
     65    ## config file. 
     66    for my $key (qw( ResultDisplay MaxResults SearchSortBy )) { 
     67        $app->{searchparam}{$key} = $no_override{$key} ? 
     68            $app->config->$key() : ($q->param($key) || $app->config->$key()); 
     69    } 
     70} 
     71 
     72sub create_blog_list { 
     73    my $app = shift; 
     74    my ( %no_override ) = @_; 
    6275 
    6376    my $q = $app->param; 
    6477    my $cfg = $app->config; 
    6578 
    66     my $tag = $q->param('tag') || ''; 
    67     $app->param('Type', 'tag') if $tag; 
    68     $app->param('search', $tag) if $tag; 
    69     my $blog_id = $q->param('blog_id') || ''; 
    70     my $include_blog_id = $q->param('IncludeBlogs') || ''; 
    71  
    72     unless ($include_blog_id){ 
    73         $app->param('IncludeBlogs', $blog_id) if $blog_id; 
    74     } 
    75  
    76     ## Get login information if user is logged in (via cookie). 
    77     ## If no login cookie, this fails silently, and that's fine. 
    78     ($app->{user}) = $app->login; 
    79  
    80     ## Check whether IP address has searched in the last 
    81     ## minute which is still progressing. If so, block it. 
    82     return $app->throttle_response() unless $app->throttle_control(); 
    83  
    84     my %no_override = map { $_ => 1 } split /\s*,\s*/, $cfg->NoOverride; 
    85  
     79    #%no_override = map { $_ => 1 } split /\s*,\s*/, $cfg->NoOverride 
     80    #    unless %no_override; 
     81 
     82    my %blog_list; 
    8683    ## Combine user-selected included/excluded blogs 
    8784    ## with config file settings. 
    8885    for my $type (qw( IncludeBlogs ExcludeBlogs )) { 
    89         $app->{searchparam}{$type} = { }; 
     86        $blog_list{$type} = {}; 
    9087        if (my $list = $cfg->$type()) { 
    91             $app->{searchparam}{$type} = 
     88            $blog_list{$type} = 
    9289                { map { $_ => 1 } split /\s*,\s*/, $list }; 
    9390        } 
    94         next if $no_override{$type}; 
     91        next if exists($no_override{$type}) && $no_override{$type}; 
    9592        for my $blog_id ($q->param($type)) { 
    9693            if ($blog_id =~ m/,/) { 
     
    9996                foreach my $id (@ids) { 
    10097                    next unless $id; 
    101                     $app->{searchparam}{$type}{$id} = 1; 
     98                    $blog_list{$type}{$id} = 1; 
    10299                } 
    103100            } else { 
    104101                $blog_id =~ s/\D+//g; # only numeric values. 
    105                 $app->{searchparam}{$type}{$blog_id} = 1; 
     102                $blog_list{$type}{$blog_id} = 1; 
    106103            } 
    107104        } 
     
    111108    ## the blogs to search. If ExcludeBlogs was set, exclude any blogs 
    112109    ## set in that list from our final list. 
    113     if (!keys %{ $app->{searchparam}{IncludeBlogs} }) { 
    114         my $exclude = $app->{searchparam}{ExcludeBlogs}; 
    115         require MT::Blog; 
    116         my $iter = MT::Blog->load_iter; 
     110    unless ( exists $blog_list{IncludeBlogs} ) { 
     111        my $exclude = $blog_list{ExcludeBlogs}; 
     112        my $iter = $app->model('blog')->load_iter; 
    117113        while (my $blog = $iter->()) { 
    118             $app->{searchparam}{IncludeBlogs}{$blog->id} = 1 
     114            $blog_list{IncludeBlogs}{$blog->id} = 1 
    119115                unless $exclude && $exclude->{$blog->id}; 
    120116        } 
    121117    } 
    122118 
    123     ## Set other search params--prefer per-query setting, default to 
    124     ## config file. 
    125     for my $key (qw( RegexSearch CaseSearch ResultDisplay SearchCutoff 
    126                      CommentSearchCutoff ExcerptWords SearchElement 
    127                      Type MaxResults SearchSortBy )) { 
    128         $app->{searchparam}{$key} = $no_override{$key} ? 
    129             $cfg->$key() : ($q->param($key) || $cfg->$key()); 
    130     } 
    131     $app->{searchparam}{entry_type} = $q->param('type'); 
    132     $app->{searchparam}{Template} = $q->param('Template') || 
    133         ($app->{searchparam}{Type} eq 'newcomments' ? 'comments' : 'default'); 
    134  
    135     ## Define alternate user templates from config file 
    136     if (my @tmpls = ($cfg->default('AltTemplate'), $cfg->AltTemplate)) { 
    137         for my $tmpl (@tmpls) { 
    138             next unless defined $tmpl; 
    139             my($nickname, $file) = split /\s+/, $tmpl; 
    140             $app->{templates}{$nickname} = $file; 
    141         } 
    142     } 
    143  
    144     $app->{templates}{default} = $cfg->DefaultTemplate; 
    145     $app->{searchparam}{SearchTemplatePath} = $cfg->SearchTemplatePath; 
    146  
    147     ## Set search_string (for display only) 
    148     if ( ( $app->{searchparam}{Type} eq 'straight' ) 
    149         || ( $app->{searchparam}{Type} eq 'tag' ) ) { 
    150         if ($q->param('search')) { 
    151             $app->{search_string} = $q->param('search'); 
    152             $app->{search_string_decoded} = MT::I18N::decode( 
    153                 $app->config->PublishCharset, 
    154                 $app->{search_string} 
    155             ); 
    156         } else { 
    157             $app->{search_string} = $app->{search_string_decoded} = q(); 
    158         } 
    159     } 
    160 
    161  
    162 sub throttle_response { 
    163     my $app = shift; 
    164     my $tmpl = $app->param('Template') || ''; 
    165     my $msg = $app->translate( 
    166         "You are currently performing a search. Please wait " . 
    167         "until your search is completed."); 
    168     if ($tmpl eq 'feed') { 
    169         $app->response_code(503); 
    170         $app->set_header('Retry-After' => $app->config('ThrottleSeconds')); 
    171         $app->send_http_header("text/plain"); 
    172         $app->{no_print_body} = 1; 
    173     } 
    174     return $app->error($msg); 
    175 
    176  
    177 sub throttle_control { 
    178     my $app = shift; 
    179  
    180     # Don't throttle MT registered users 
    181     return 1 if $app->{user} && $app->{user}->type == MT::Author::AUTHOR(); 
    182  
    183     my $type = $app->param('Type') || ''; 
    184  
    185     # Don't throttle tag listings 
    186     return 1 if $type eq 'tag'; 
    187  
    188     my $ip = $app->remote_ip; 
    189     my $whitelist = $app->config('SearchThrottleIPWhitelist'); 
    190     if ($whitelist) { 
    191         # check for $ip in $whitelist 
    192         my @list = split /(\s*[,;]\s*|\s+)/, $whitelist; 
    193         foreach (@list) { 
    194             next unless $_ =~ m/^\d{1,3}(\.\d{0,3}){0,3}$/; 
    195             if (($ip eq $_) || ($ip =~ m/^\Q$_\E/)) { 
    196                 return 1; 
    197             } 
    198         } 
    199     } 
    200  
    201     if (eval { require DB_File; 1 }) { 
    202         my $file = File::Spec->catfile($app->config('TempDir'), 'mt-throttle.db'); 
    203         my $DB = tie my %db, 'DB_File', $file; 
    204         if ($DB) { 
    205             if (my $time = $db{$ip}) { 
    206                 if ($time > time - $app->config('ThrottleSeconds')) { 
    207                     return 0; 
     119    \%blog_list; 
     120
     121 
     122sub process { 
     123    my $app = shift; 
     124 
     125    my ( $terms, $args ) = $app->search_terms(); 
     126    return $app->error($app->errstr) if $app->errstr; 
     127 
     128    my $count = 0; 
     129    my $iter; 
     130    if ( $terms && @$terms ) { 
     131        ( $count, $iter ) = $app->execute( $terms, $args ); 
     132        return $app->error($app->errstr) unless $iter; 
     133 
     134        $iter = $app->post_search( $count, $iter ); 
     135    } 
     136 
     137    return $app->render( $count, $iter ); 
     138
     139 
     140sub count { 
     141    my $app = shift; 
     142    my ( $class, $terms, $args ) = @_; 
     143    #TODO: cache! 
     144    $class->count( $terms, $args ); 
     145
     146 
     147sub execute { 
     148    my $app = shift; 
     149    my ( $terms, $args ) = @_; 
     150 
     151    my $class = $app->model( $app->mode eq 'search' ? 'entry' : $app->mode ); 
     152    my $count = $app->count( $class, $terms, $args ); 
     153    #TODO: cache! 
     154    my $iter = $class->load_iter( $terms, $args ) 
     155        or $app->error($class->errstr); 
     156    ( $count, $iter ); 
     157
     158 
     159sub search_terms { 
     160    my $app = shift; 
     161    my $q = $app->param; 
     162 
     163    my $search_string = $q->param('searchTerms') || $q->param('search') 
     164        or return ( undef, undef ); 
     165    $app->{search_string} = $search_string; 
     166    my $offset = $q->param('startIndex') || $q->param('offset') || 0; 
     167    my $limit = $q->param('count') || $q->param('limit'); 
     168    my $max = $app->{searchparam}{MaxResults}; 
     169    $limit = $max if !$limit || ( $limit - $offset > $max ); 
     170 
     171    my $type = $q->param('type'); 
     172    my $entry_type = $app->mode eq 'search' 
     173      ? 1 
     174      : $type 
     175        ? ( 'entry' eq lc($type) || 'page' eq lc($type) 
     176          ? 1 
     177          : 0 ) 
     178        : 0; 
     179 
     180    my $def_terms = { 
     181        $entry_type ? ( status => MT::Entry::RELEASE() ) : (), 
     182        $type ? ( class  => $type ) : (), 
     183        exists( $app->{searchparam}{IncludeBlogs} ) 
     184          ? ( blog_id => [ keys %{ $app->{searchparam}{IncludeBlogs} } ] ) 
     185          : (), 
     186    }; 
     187    my @terms; 
     188    push @terms, $def_terms if %$def_terms; 
     189 
     190    my $columns = $app->mode eq 'search' 
     191      ? [ [ qw( title keywords text text_more ) ] ] 
     192      : $app->registry( $app->mode, 'columns' ); 
     193    $columns = $columns->[0]; # FIXME: Why? 
     194    return $app->errtrans('No columns to search for was specified for [_1]', $app->mode) 
     195        unless $columns && @$columns; 
     196    my $number = scalar @$columns; 
     197    my @and; 
     198    for ( my $i = 0; $i < $number; $i++) { 
     199        push @and, { $columns->[$i] => { like => '%'.$search_string.'%' } }; 
     200        unless ( $i == $number - 1 ) { 
     201            push @and, '-or'; 
     202        } 
     203    } 
     204    push @terms, '-and' if @terms; 
     205    push @terms, \@and; 
     206 
     207    my %args = ( 
     208      $limit ? ( 'limit' => $limit ) : (), 
     209      $offset ? ( 'offset' => $offset ) : (), 
     210      'sort' => [ 
     211        { desc   => 'descend' eq $app->{searchparam}{ResultDisplay} ? 'DESC' : 'ASC', 
     212          column => $entry_type ? 'authored_on' : 'created_on' } 
     213      ] 
     214    ); 
     215 
     216    if ( exists $app->{searchparam}{IncludeBlogs} ) { 
     217        unshift @{ $args{'sort'} }, 
     218          { desc => 'ASC', 
     219            column    => 'blog_id' }; 
     220    } 
     221 
     222    ( \@terms, \%args ); 
     223
     224 
     225sub post_search { 
     226    my $app = shift; 
     227    my ( $count, $iter ) = @_; 
     228    # TODO: cache here? 
     229    $iter; 
     230
     231 
     232sub template_paths { 
     233    my $app = shift; 
     234    my @paths = $app->SUPER::template_paths; 
     235    ( $app->config->SearchTemplatePath, @paths ); 
     236
     237 
     238sub load_search_tmpl { 
     239    my $app = shift; 
     240    my $q = $app->param; 
     241    my ( $count, $iter ) = @_; 
     242 
     243    my $blog_id; 
     244    if ( $q->param('IncludeBlogs') ) { 
     245        my @ids = split ',', $q->param('IncludeBlogs'); 
     246        $blog_id = $ids[0]; 
     247    } 
     248    elsif ( exists $app->{searchparam}{IncludeBlogs} ) { 
     249        $blog_id = @{ keys %{ $app->{searchparam}{IncludeBlogs} } }[0]; 
     250    } 
     251 
     252    my $tmpl; 
     253    if ( $q->param('Template') && ( 'default' ne $q->param('Template') ) ) { 
     254        # load specified template 
     255        my $filename; 
     256        if (my @tmpls = ($app->config->default('AltTemplate'), $app->config->AltTemplate)) { 
     257            for my $tmpl (@tmpls) { 
     258                next unless defined $tmpl; 
     259                my ( $nickname, $file ) = split /\s+/, $tmpl; 
     260                if ( $nickname eq $q->param('Template') ) { 
     261                    $filename = $file; 
     262                    last; 
    208263                } 
    209264            } 
    210             $db{$ip} = time; 
    211             undef $DB; 
    212             untie %db; 
    213             $app->{__have_throttle} = 1; 
    214         } 
    215     } 
    216  
    217     1; 
    218 
    219  
    220 sub takedown { 
    221     my $app = shift; 
    222     if ($app->{__have_throttle}) { 
    223         my $file = File::Spec->catfile($app->config('TempDir'), 
    224                                        'mt-throttle.db'); 
    225         if (tie my %db, 'DB_File', $file) { 
    226             my $time = $db{$app->remote_ip}; 
    227             delete $db{$app->remote_ip} if ($time && $time < (time - $app->config('ThrottleSeconds'))); 
    228             untie %db; 
    229         } 
    230     } 
    231     $app->SUPER::takedown(@_); 
    232 
    233  
    234 sub execute { 
    235     my $app = shift; 
    236     return $app->error($app->errstr) if $app->errstr; 
    237  
    238     my @results; 
    239     if ($app->{searchparam}{RegexSearch}) { 
    240         eval { m/$app->{search_string}/ }; 
    241         return $app->error($app->translate("Search failed. Invalid pattern given: [_1]", $@)) 
    242             if $@; 
    243     } 
    244     if ($app->{searchparam}{Type} eq 'newcomments') { 
    245         $app->_new_comments 
    246             or return $app->error($app->translate( 
    247                 "Search failed: [_1]", $app->errstr)); 
    248         @results = $app->{results} ? @{ $app->{results} } : (); 
    249     } elsif ($app->{searchparam}{Type} eq 'tag') { 
    250         $app->_tag_search 
    251             or return $app->error($app->translate( 
    252                 "Search failed: [_1]", $app->errstr)); 
    253         my $col = $app->{searchparam}{SearchSortBy}; 
    254         my $order = $app->{searchparam}{ResultDisplay} || 'ascend'; 
    255         for my $blog (sort keys %{ $app->{results} }) { 
    256             my @res = @{ $app->{results}{$blog} }; 
    257             if ($col) { 
    258                 @res = $order eq 'ascend' ? 
    259                   sort { $a->{entry}->$col() cmp $b->{entry}->$col() } @res : 
    260                   sort { $b->{entry}->$col() cmp $a->{entry}->$col() } @res; 
    261             } 
    262             $res[0]{blogheader} = 1; 
    263             my $max = $#res; 
    264             $res[$max]{blogfooter} = 1; 
    265             push @results, @res; 
    266         } 
    267     } else { 
    268         $app->_straight_search 
    269             or return $app->error($app->translate( 
    270                 "Search failed: [_1]", $app->errstr)); 
    271         ## Results are stored per-blog, so we sort the list of blogs by name, 
    272         ## then add in the results to the final list. 
    273         my $col = $app->{searchparam}{SearchSortBy}; 
    274         my $order = $app->{searchparam}{ResultDisplay} || 'ascend'; 
    275         for my $blog (sort keys %{ $app->{results} }) { 
    276             my @res = @{ $app->{results}{$blog} }; 
    277             if ($col) { 
    278                 @res = $order eq 'ascend' ? 
    279                   sort { $a->{entry}->$col() cmp $b->{entry}->$col() } @res : 
    280                   sort { $b->{entry}->$col() cmp $a->{entry}->$col() } @res; 
    281             } 
    282             $res[0]{blogheader} = 1; 
    283             my $max = $#res; 
    284             $res[$max]{blogfooter} = 1; 
    285             push @results, @res; 
    286         } 
    287     } 
    288  
    289     ## We need to put a blog in context so that includes and <$MTBlog*$> 
    290     ## tags will work, if they are used. So we choose the first blog in 
    291     ## the result list. If there is no result list, just load the first 
    292     ## blog from the database. 
    293     my($blog); 
    294     my $include = $app->param('IncludeBlogs'); 
    295     if ($include) { 
    296         my @blog_ids = split ',', $include; 
    297         $blog = MT::Blog->load($blog_ids[0]); 
    298     } else { 
    299         if (@results) { 
    300             $blog = $results[0]{blog}; 
    301         } 
    302         if (!$blog) { 
    303             $blog = MT::Blog->load($app->param('blog_id')); 
    304         } 
    305         $include = $blog->id; 
    306     } 
     265        } 
     266        return $app->errtrans("No alternate template is specified for the Template '[_1]'", $q->param('Template')) 
     267            unless $filename; 
     268        # template_paths method does the magic 
     269        $tmpl = $app->load_tmpl( $filename ) 
     270            or return $app->errtrans( "Opening local file '[_1]' failed: [_2]", $filename, "$!" ); 
     271    } 
     272    else { 
     273        # load default template 
     274        # first look for appropriate blog_id 
     275        if ( $blog_id ) { 
     276            # look for 'search_results' 
     277            my $tmpl_class = $app->model('template'); 
     278            $tmpl = $tmpl_class->load( 
     279                { blog_id => $blog_id, type => 'search_results' } 
     280            ); 
     281        } 
     282        unless ( $tmpl ) { 
     283            # load template from search_template path 
     284            # template_paths method does the magic 
     285            $tmpl = $app->load_tmpl( $app->config->DefaultTemplate ); 
     286        } 
     287    } 
     288    return $app->error($app->errstr) 
     289        unless $tmpl; 
    307290 
    308291    ## Initialize and set up the context object. 
    309     my $ctx = MT::App::Search::Context->new; 
    310     $ctx->stash('blog', $blog); 
    311     $ctx->stash('blog_id', $blog->id); 
    312     $ctx->stash('results', \@results); 
    313     $ctx->stash('user', $app->{user}); 
    314     $ctx->stash('include_blogs', $include); 
    315     if (my $str = $app->{search_string}) { 
     292    require MT::Template::Context::Search; 
     293    my $ctx = MT::Template::Context::Search->new; 
     294    if ( $blog_id ) { 
     295        $ctx->stash('blog_id', $blog_id); 
     296        $ctx->stash('blog',    $app->model('blog')->load($blog_id)); 
     297    } 
     298    $ctx->stash('results', $iter); 
     299    $ctx->stash('count',   $count); 
     300    $ctx->stash('stash_key', $app->mode) 
     301        if 'search' ne $app->mode; 
     302    $ctx->stash('include_blogs', 
     303        join ',', keys %{ $app->{searchparam}{IncludeBlogs} }); 
     304    if ( my $str = $app->{search_string} ) { 
    316305        $ctx->stash('search_string', encode_html($str)); 
    317306    } 
    318     $ctx->stash('template_id', $app->{searchparam}{Template}); 
    319     $ctx->stash('maxresults', $app->{searchparam}{MaxResults}); 
    320     $ctx->var( 'page_layout', $blog->page_layout ) 
    321         if $blog && $blog->page_layout; 
    322     if (my $layout = $ctx->var('page_layout')) { 
    323         my $columns = { 
    324             'layout-wt'  => 2, 
    325             'layout-tw'  => 2, 
    326             'layout-wm'  => 2, 
    327             'layout-mw'  => 2, 
    328             'layout-wtt' => 3, 
    329             'layout-twt' => 3, 
    330         }->{$layout}; 
    331         $ctx->var( 'page_columns', $columns ) if $columns; 
    332     } 
    333  
    334     my $str; 
    335     if ($include) { 
    336         if ($app->{searchparam}{Template} eq 'default') { 
    337             # look for a 'search_results' 
    338             my ($blog_id) = split ',', $include; 
    339             require MT::Template; 
    340             my $tmpl = MT::Template->load({blog_id => $blog_id, type => 'search_results'}); 
    341             $str = $tmpl->text if $tmpl; 
    342         } 
    343     } 
    344  
    345     if (!$str) { 
    346         ## Load the search template. 
    347         my $tmpl_file = $app->{templates}{ $app->{searchparam}{Template} } 
    348             or return $app->error($app->translate("No alternate template is specified for the Template '[_1]'", $app->{searchparam}{Template})); 
    349         my $tmpl = File::Spec->catfile($app->{searchparam}{SearchTemplatePath}, 
    350             $tmpl_file); 
    351         local *FH; 
    352         open FH, $tmpl 
    353             or return $app->error($app->translate( 
    354                 "Opening local file '[_1]' failed: [_2]", $tmpl, "$!" )); 
    355  
    356         { local $/; $str = <FH> }; 
    357         close FH; 
    358     } 
    359     $str = $app->translate_templatized($str); 
    360  
    361     my $ifmax; 
    362     my $max; 
    363     if (($app->{searchparam}{MaxResults}) && ($app->{searchparam}{MaxResults} != 9999999)) { 
    364         $max = $app->{searchparam}{MaxResults}; 
    365         $ifmax = 1; 
    366     } else { 
    367         $ifmax = $max = 0; 
    368     } 
    369  
    370     ## Compile and build the search template with results. 
    371     require MT::Builder; 
    372     my $build = MT::Builder->new; 
    373     my $tokens = $build->compile($ctx, $str) 
    374         or return $app->error($app->translate( 
    375             "Publishing results failed: [_1]", $build->errstr)); 
    376     defined(my $res = $build->build($ctx, $tokens, {  
    377         NoSearch => $app->{query}->param('help') || 
    378                     ($app->{searchparam}{Type} ne 'newcomments' && 
    379                       (!$ctx->stash('search_string') || 
    380                       $ctx->stash('search_string') !~ /\S/)) ? 1 : 0, 
    381         NoSearchResults => $ctx->stash('search_string') && 
    382                            $ctx->stash('search_string') =~ /\S/ && 
    383                            !scalar @results, 
    384         SearchResults => scalar @results, 
    385         } )) 
    386         or return $app->error($app->translate( 
    387             "Publishing results failed: [_1]", $ctx->errstr)); 
    388     $res = $app->_set_form_elements($res); 
    389  
    390     if (defined($ctx->stash('content_type'))) { 
    391         $app->{no_print_body} = 1; 
    392         if ($app->{searchparam}{Template} eq 'feed') { 
    393             my $last_update; 
    394             for (@results) { 
    395                 my $authored_on = ts2epoch($_->{blog}, $_->{entry}->authored_on); 
    396                 $last_update = $authored_on if $authored_on > $last_update; 
    397             } 
    398             my $mod_since = $app->get_header('If-Modified-Since'); 
    399  
    400             if ( !@results || ($last_update && $mod_since && ($last_update <= str2time($mod_since))) ) { 
    401                 $app->response_code(304); 
    402                 $app->response_message('Not Modified'); 
    403                 $app->send_http_header($ctx->stash('content_type')); 
    404             } else { 
    405                 $app->set_header('Last-Modified', time2str($last_update)) if $last_update; 
    406                 $app->send_http_header($ctx->stash('content_type')); 
    407                 $app->print($res); 
    408             } 
    409         } else { 
    410             $app->send_http_header($ctx->stash('content_type')); 
    411             $app->print($res); 
    412         } 
    413     } 
    414     $res; 
    415 
    416  
    417 sub _tag_search { 
    418     my $app = shift; 
    419     return 1 unless $app->{search_string} =~ /\S/; 
    420  
    421     # since this technically isn't a search, but a dynamic view 
    422     # of data, suppress logging... 
    423     #require MT::Log; 
    424     #$app->log({ 
    425     #    message => $app->translate("Search: query for '[_1]'", 
    426     #          $app->{search_string}), 
    427     #    level => MT::Log::INFO(), 
    428     #    class => 'search', 
    429     #    category => 'tag_search', 
    430     #}); 
    431  
    432     require MT::Entry; 
    433     my %terms = (status => MT::Entry::RELEASE()); 
    434     my %args = (direction => $app->{searchparam}{ResultDisplay}, 
    435         'sort' => 'authored_on'); 
    436  
    437     require MT::Tag; 
    438     require MT::ObjectTag; 
    439     my $tags = $app->{search_string}; 
    440     my @tag_names = MT::Tag->split(',', $tags); 
    441     my %tags = map { $_ => 1, MT::Tag->normalize($_) => 1 } @tag_names; 
    442     my @tags = MT::Tag->load({ name => [ keys %tags ] }); 
    443     my @tag_ids; 
    444     foreach (@tags) { 
    445         push @tag_ids, $_->id; 
    446         my @more = MT::Tag->load({ n8d_id => $_->n8d_id ? $_->n8d_id : $_->id }); 
    447         push @tag_ids, $_->id foreach @more; 
    448     } 
    449     @tag_ids = ( 0 ) unless @tags; 
    450     $args{'join'} = ['MT::ObjectTag', 'object_id', 
    451         { tag_id => \@tag_ids, object_datasource => MT::Entry->datasource }, { unique => 1 } ]; 
    452  
    453     ## Load available blogs and iterate through entries looking for search term 
    454     require MT::Util; 
    455     require MT::Blog; 
    456     require MT::Entry; 
    457  
    458     # Override SearchCutoff if If-Modified-Since header is present 
    459     if ((my $mod_since = $app->get_header('If-Modified-Since')) && $app->{searchparam}{Template} eq 'feed') { 
    460         my $tz_offset = 15;  # Start with maximum possible offset to UTC 
    461         my $blog_selected; 
    462         my $iter; 
    463         if ($app->{searchparam}{IncludeBlogs}) { 
    464             $iter = MT::Blog->load_iter({ id => [ keys %{ $app->{searchparam}{IncludeBlogs} }] }); 
    465         } else { 
    466             $iter = MT::Blog->load_iter; 
    467         } 
    468         while (my $blog = $iter->()) { 
    469             my $blog_offset = $blog->server_offset ? 
    470                 $blog->server_offset : 0; 
    471             if ($blog_offset < $tz_offset) { 
    472                 $tz_offset = $blog_offset; 
    473                 $blog_selected = $blog; 
    474             } 
    475         } 
    476         $mod_since = epoch2ts($blog_selected, str2time($mod_since)); 
    477         $terms{authored_on} = [ $mod_since ]; 
    478         $args{range} = { authored_on => 1 }; 
    479     } else { 
    480         if ($app->{searchparam}{SearchCutoff} && 
    481             $app->{searchparam}{SearchCutoff} != 9999999) { 
    482             my @ago = MT::Util::offset_time_list(time - 3600 * 24 * 
    483                 $app->{searchparam}{SearchCutoff}); 
    484             my $ago = sprintf "%04d%02d%02d%02d%02d%02d", 
    485                 $ago[5]+1900, $ago[4]+1, @ago[3,2,1,0]; 
    486             $terms{authored_on} = [ $ago ]; 
    487             $args{range} = { authored_on => 1 }; 
    488         } 
    489     } 
    490  
    491     if (keys %{ $app->{searchparam}{IncludeBlogs} }) { 
    492         $terms{blog_id} = [ keys %{ $app->{searchparam}{IncludeBlogs} } ]; 
    493     } 
    494     $terms{class} = $app->{searchparam}{entry_type} || '*'; 
    495     my $iter = MT::Entry->load_iter(\%terms, \%args); 
    496     my(%blogs, %hits); 
    497     my $max = $app->{searchparam}{MaxResults};  
    498     while (my $entry = $iter->()) { 
    499         my $blog_id = $entry->blog_id; 
    500         if ($hits{$blog_id} && $hits{$blog_id} >= $max) { 
    501             my $blog = $blogs{$blog_id} || MT::Blog->load($blog_id); 
    502             my @res = @{ $app->{results}{$blog->name} }; 
    503             my $count = $#res; 
    504             $res[$count]{maxresults} = $max; 
    505             next; 
    506         } 
    507         if ($app->_search_hit($entry)) { 
    508             my $blog = $blogs{$blog_id} || MT::Blog->load($blog_id); 
    509             $app->_store_hit_data($blog, $entry, $hits{$blog_id}++); 
    510         } 
    511     } 
    512     1; 
    513 
    514  
    515 sub _straight_search { 
    516     my $app = shift; 
    517     return 1 unless $app->{search_string} =~ /\S/; 
    518  
    519     ## Parse, tokenize and optimize the search query. 
    520     $app->query_parse; 
    521  
    522     ## Load available blogs and iterate through entries looking for search term 
    523     require MT::Util; 
    524     require MT::Blog; 
    525     require MT::Entry; 
    526  
    527     my %terms = (status => MT::Entry::RELEASE()); 
    528     my %args = (direction => $app->{searchparam}{ResultDisplay}, 
    529                 'sort' => 'authored_on'); 
    530  
    531     # Override SearchCutoff if If-Modified-Since header is present 
    532     if ((my $mod_since = $app->get_header('If-Modified-Since')) && $app->{searchparam}{Template} eq 'feed') { 
    533         my $tz_offset = 15;  # Start with maximum possible offset to UTC 
    534         my $blog_selected; 
    535         my $iter; 
    536         if ($app->{searchparam}{IncludeBlogs}) { 
    537             $iter = MT::Blog->load_iter({ id => [ keys %{ $app->{searchparam}{IncludeBlogs} }] }); 
    538         } else { 
    539             $iter = MT::Blog->load_iter; 
    540         } 
    541         while (my $blog = $iter->()) { 
    542             my $blog_offset = $blog->server_offset ? 
    543                 $blog->server_offset : 0; 
    544             if ($blog_offset < $tz_offset) { 
    545                 $tz_offset = $blog_offset; 
    546                 $blog_selected = $blog; 
    547             } 
    548         } 
    549         $mod_since = epoch2ts($blog_selected, str2time($mod_since)); 
    550         $terms{authored_on} = [ $mod_since ]; 
    551         $args{range} = { authored_on => 1 }; 
    552     } else { 
    553         if ($app->{searchparam}{SearchCutoff} && 
    554             $app->{searchparam}{SearchCutoff} != 9999999) { 
    555             my @ago = MT::Util::offset_time_list(time - 3600 * 24 * 
    556                       $app->{searchparam}{SearchCutoff}); 
    557             my $ago = sprintf "%04d%02d%02d%02d%02d%02d", 
    558                 $ago[5]+1900, $ago[4]+1, @ago[3,2,1,0]; 
    559             $terms{authored_on} = [ $ago ]; 
    560             $args{range} = { authored_on => 1 }; 
    561         } 
    562     } 
    563  
    564     if (keys %{ $app->{searchparam}{IncludeBlogs} }) { 
    565         $terms{blog_id} = [ keys %{ $app->{searchparam}{IncludeBlogs} } ]; 
    566     } 
    567  
    568     my $blog_id; 
    569     if ($terms{blog_id} && (scalar(@{ $terms{blog_id} }) == 1)) { 
    570         $blog_id = $terms{blog_id}[0]; 
    571     } 
    572  
    573     #FIXME: template name may not be 'feed' for search feed 
    574     unless ($app->{searchparam}{Template} eq 'feed') { 
    575         require MT::Log; 
    576         $app->log({ 
    577             message => $app->translate("Search: query for '[_1]'", 
    578                   $app->{search_string}), 
    579             level => MT::Log::INFO(), 
    580             class => 'search', 
    581             category => 'straight_search', 
    582             $blog_id ? (blog_id => $blog_id) : () 
    583         }); 
    584     } 
    585  
    586     $terms{class} = $app->{searchparam}{entry_type} || '*'; 
    587  
    588     my $iter = MT::Entry->load_iter(\%terms, \%args); 
    589     my(%blogs, %hits); 
    590     my $max = $app->{searchparam}{MaxResults};  
    591     while (my $entry = $iter->()) { 
    592         my $blog_id = $entry->blog_id; 
    593         if ($hits{$blog_id} && $hits{$blog_id} >= $max) { 
    594             my $blog = $blogs{$blog_id} || MT::Blog->load($blog_id); 
    595             my @res = @{ $app->{results}{$blog->name} }; 
    596             my $count = $#res; 
    597             $res[$count]{maxresults} = $max; 
    598             next; 
    599         } 
    600         if ($app->_search_hit($entry)) { 
    601             my $blog = $blogs{$blog_id} || MT::Blog->load($blog_id); 
    602             $app->_store_hit_data($blog, $entry, $hits{$blog_id}++); 
    603         } 
    604     } 
    605     1; 
    606 
    607  
    608 sub _new_comments { 
    609     my $app = shift; 
    610     return 1 if $app->param('help'); 
    611  
    612     require MT::Log; 
    613     $app->log({ 
    614         message => $app->translate("Search: new comment search"), 
    615         level => MT::Log::INFO(), 
    616         class => 'search', 
    617         category => 'comment_search' 
    618     }); 
    619     require MT::Entry; 
    620     require MT::Blog; 
    621     require MT::Util; 
    622     my %args = ('join' => [ 
    623                     'MT::Comment', 'entry_id', { visible => 1 }, 
    624                     { 'sort' => 'created_on', 
    625                        direction => 'descend', 
    626                        unique => 1, } 
    627                ]); 
    628     if ($app->{searchparam}{CommentSearchCutoff} && 
    629         $app->{searchparam}{CommentSearchCutoff} != 9999999) { 
    630         my @ago = MT::Util::offset_time_list(time - 3600 * 24 * 
    631                   $app->{searchparam}{CommentSearchCutoff}); 
    632         my $ago = sprintf "%04d%02d%02d%02d%02d%02d", 
    633             $ago[5]+1900, $ago[4]+1, @ago[3,2,1,0]; 
    634         $args{'join'}->[2]{created_on} = [ $ago ]; 
    635         $args{'join'}->[3]{range} = { created_on => 1 }; 
    636     } elsif ($app->{searchparam}{MaxResults} && 
    637              $app->{searchparam}{MaxResults} != 9999999) { 
    638         $args{limit} = $app->{searchparam}{MaxResults}; 
    639     } 
    640     my %terms = { status => MT::Entry::RELEASE() }; 
    641     $terms{class} = $app->{searchparam}{entry_type} || '*'; 
    642     my $iter = MT::Entry->load_iter(\%terms, \%args); 
    643     my %blogs; 
    644     my $include = $app->{searchparam}{IncludeBlogs}; 
    645     while (my $entry = $iter->()) { 
    646         next unless $include->{ $entry->blog_id }; 
    647         my $blog = $blogs{ $entry->blog_id } || MT::Blog->load($entry->blog_id); 
    648         $app->_store_hit_data($blog, $entry); 
    649     } 
    650     1; 
    651 
    652  
    653 sub _set_form_elements { 
    654     my($app, $tmpl) = @_; 
    655     ## Fill in user-defined template with proper form settings. 
    656     if ($app->{searchparam}{Type} eq 'newcomments') { 
    657         if ($app->{searchparam}{CommentSearchCutoff}) { 
    658             $tmpl =~ s/(<select name="CommentSearchCutoff">.*<option value="$app->{searchparam}{CommentSearchCutoff}")/$1 selected="selected"/si; 
    659         } else { 
    660             $tmpl =~ s/(<select name="CommentSearchCutoff">.*<option value="9999999")/$1 selected="selected"/si; 
    661         } 
    662     } else { 
    663         if ($app->{searchparam}{SearchCutoff}) { 
    664             $tmpl =~ s/(<select name="SearchCutoff">.*<option value="$app->{searchparam}{SearchCutoff}")/$1 selected="selected"/si; 
    665         } else { 
    666             $tmpl =~ s/(<select name="SearchCutoff">.*<option value="9999999")/$1 selected="selected"/si; 
    667         } 
    668  
    669         if ($app->{searchparam}{CaseSearch}) { 
    670             $tmpl =~ s/(<input type="checkbox"[^>]+name="CaseSearch")/$1 checked="checked"/g; 
    671         } 
    672         if ($app->{searchparam}{RegexSearch}) { 
    673             $tmpl =~ s/(<input type="checkbox"[^>]+name="RegexSearch")/$1 checked="checked"/g; 
    674         } 
    675         $tmpl =~ s/(<input type="radio"[^>]+?$app->{searchparam}{SearchElement}\")/$1 checked="checked"/g; 
    676         for my $type (qw( IncludeBlogs ExcludeBlogs )) { 
    677             for my $blog_id (keys %{ $app->{searchparam}{$type} }) { 
    678                 $tmpl =~ s/(<input type="checkbox"[^>]+?$type" value="$blog_id")/$1 checked="checked"/g;    #" 
    679             } 
    680         } 
    681     } 
    682     if ($app->{searchparam}{MaxResults}) { 
    683         $tmpl =~ s/(<select name="MaxResults">.*<option value="$app->{searchparam}{MaxResults}")/$1 selected="selected"/si; 
    684     } else { 
    685         $tmpl =~ s/(<select name="MaxResults">.*<option value="9999999")/$1 selected="selected"/si; 
    686     } 
     307    $ctx->stash('template_id', $q->param('Template')); 
     308    $ctx->stash('maxresults' , $app->{searchparam}{MaxResults}); 
     309 
     310    $tmpl->context($ctx); 
    687311    $tmpl; 
    688312} 
    689313 
    690 my $decoder_ring; 
    691 sub is_a_match {  
    692     my($app, $txt) = @_; 
    693     use utf8; 
    694     unless ($decoder_ring) { 
    695         my $enc = $app->config->PublishCharset; 
    696         eval {  
    697             require Encode; 
    698             $decoder_ring = sub { Encode::decode($enc, shift) }; 
    699         }; 
    700         if ($@) { 
    701             $decoder_ring = sub { MT::I18N::decode($enc, shift) }; 
    702         } 
    703     } 
    704     $txt = $decoder_ring->($txt); 
    705  
    706     if ($app->{searchparam}{RegexSearch}) { 
    707         my $keyword = $app->{searchparam}{search_string_decoded}; 
    708         if ($app->{searchparam}{CaseSearch}) { 
    709             return unless $txt =~ m/$keyword/; 
    710         } else { 
    711             return unless $txt =~ m/$keyword/i; 
    712         } 
    713     } else { 
    714         my $casemod = $app->{searchparam}{CaseSearch} ? '' : '(?i)'; 
    715         for (@{$app->{searchparam}{search_keys}{AND}}) { 
    716             return unless $txt =~ /$casemod$_/; 
    717     } 
    718     for (@{$app->{searchparam}{search_keys}{NOT}}) { 
    719             return if $txt =~ /$casemod$_/; 
    720         } 
    721     } 
    722     1; 
    723 }        
     314sub pre_render { 
     315    my $app = shift; 
     316    my ( $tmpl ) = @_; 
     317    $tmpl 
     318
     319 
     320sub render { 
     321    my $app = shift; 
     322    my ( $count, $iter ) = @_; 
     323 
     324    my $tmpl = $app->load_search_tmpl( $count, $iter ); 
     325    $tmpl = $app->pre_render( $tmpl ); 
     326 
     327    $tmpl; 
     328
    724329 
    725330sub query_parse { 
     
    811416} 
    812417 
    813  
    814 sub _search_hit { 
    815     my($app, $entry) = @_; 
    816     my @text_elements; 
    817     if ($app->{searchparam}{SearchElement} ne 'comments') { 
    818         @text_elements = ($entry->title, $entry->text, $entry->text_more, 
    819                           $entry->keywords); 
    820     } 
    821     if ($app->{searchparam}{SearchElement} ne 'entries') { 
    822         my $comments = $entry->comments; 
    823         for my $comment (@$comments) { 
    824             push @text_elements, $comment->text, $comment->author, 
    825                                  $comment->url; 
    826         } 
    827     } 
    828     return 1 if $app->is_a_match(join("\n", map $_ || '', @text_elements)); 
    829 } 
    830  
    831 sub _store_hit_data { 
    832     my $app = shift; 
    833     my($blog, $entry, $banner_seen) = @_; 
    834     my %result_data = (blog => $blog); 
    835     ## Need to create entry excerpt here, because we can't rely on 
    836     ## the user's per-blog setting. 
    837     unless ($entry->excerpt) { 
    838         $entry->excerpt($entry->get_excerpt($app->{searchparam}{ExcerptWords})); 
    839     } 
    840     $result_data{entry} = $entry; 
    841     if ($app->{searchparam}{Type} eq 'newcomments') { 
    842         push @{ $app->{results} }, \%result_data; 
    843     } else { 
    844         push(@{ $app->{results}{ $blog->name } }, \%result_data); 
    845     } 
    846 } 
    847  
    848  
    849 package MT::App::Search::Context; 
    850  
    851 use strict; 
    852 use base qw( MT::Template::Context ); 
    853  
    854 sub _hdlr_include_blogs { $_[0]->stash('include_blogs') || '' } 
    855 sub _hdlr_search_string { $_[0]->stash('search_string') || '' } 
    856 sub _hdlr_template_id { $_[0]->stash('template_id') || '' } 
    857 sub _hdlr_max_results { $_[0]->stash('maxresults') || '' } 
    858  
    859 sub _hdlr_result_count { 
    860     my $results = $_[0]->stash('results'); 
    861     $results && ref($results) eq 'ARRAY' ? scalar @$results : 0; 
    862 } 
    863  
    864 sub _hdlr_results { 
    865     my($ctx, $args, $cond) = @_; 
    866  
    867     ## If there are no results, return the empty string, knowing 
    868     ## that the handler for <MTNoSearchResults> will fill in the 
    869     ## no results message. 
    870     my $results = $ctx->stash('results') or return ''; 
    871  
    872     my $output = ''; 
    873     my $build = $ctx->stash('builder'); 
    874     my $tokens = $ctx->stash('tokens'); 
    875     for my $res (@$results) { 
    876         $ctx->stash('entry', $res->{entry}); 
    877         local $ctx->{__stash}{blog} = $res->{blog}; 
    878         $ctx->stash('result', $res); 
    879         local $ctx->{current_timestamp} = $res->{entry}->created_on; 
    880         defined(my $out = $build->build($ctx, $tokens, 
    881             { %$cond,  
    882                 BlogResultHeader => $res->{blogheader} ? 1 : 0,  
    883                 BlogResultFooter => $res->{blogfooter} ? 1 : 0, 
    884                 IfMaxResultsCutoff => $res->{maxresults} ? 1 : 0, 
    885             } 
    886             )) or return $ctx->error( $build->errstr ); 
    887         $output .= $out;