plugins/SpecialSearch.php
changeset 0 902822492a68
child 36 425261984266
equal deleted inserted replaced
-1:000000000000 0:902822492a68
       
     1 <?php
       
     2 /*
       
     3 Plugin Name: Search UI/frontend
       
     4 Plugin URI: http://www.enanocms.org/
       
     5 Description: Provides the page Special:Search, which is a frontend to the Enano search engine.
       
     6 Author: Dan Fuhry
       
     7 Version: 1.0
       
     8 Author URI: http://www.enanocms.org/
       
     9 */
       
    10 
       
    11 /*
       
    12  * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
       
    13  * Version 1.0 release candidate 2
       
    14  * Copyright (C) 2006-2007 Dan Fuhry
       
    15  *
       
    16  * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
       
    17  * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
       
    18  *
       
    19  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
       
    20  * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
       
    21  */
       
    22 
       
    23 $plugins->attachHook('base_classes_initted', '
       
    24   global $paths;
       
    25     $paths->add_page(Array(
       
    26       \'name\'=>\'Rebuild search index\',
       
    27       \'urlname\'=>\'SearchRebuild\',
       
    28       \'namespace\'=>\'Special\',
       
    29       \'special\'=>0,\'visible\'=>0,\'comments_on\'=>0,\'protected\'=>1,\'delvotes\'=>0,\'delvote_ips\'=>\'\',
       
    30       ));
       
    31     
       
    32     $paths->add_page(Array(
       
    33       \'name\'=>\'Search\',
       
    34       \'urlname\'=>\'Search\',
       
    35       \'namespace\'=>\'Special\',
       
    36       \'special\'=>0,\'visible\'=>1,\'comments_on\'=>0,\'protected\'=>1,\'delvotes\'=>0,\'delvote_ips\'=>\'\',
       
    37       ));
       
    38     ');
       
    39 
       
    40 function page_Special_SearchRebuild()
       
    41 {
       
    42   global $db, $session, $paths, $template, $plugins; // Common objects
       
    43   if(!$session->get_permissions('mod_misc')) die_friendly('Unauthorized', '<p>You need to be an administrator to rebuild the search index</p>');
       
    44   $template->header();
       
    45   if($paths->rebuild_search_index())
       
    46     echo '<p>Index rebuilt!</p>';
       
    47   else
       
    48     echo '<p>Index was not rebuilt due to an error.';
       
    49   $template->footer();
       
    50 }
       
    51 
       
    52 function page_Special_Search()
       
    53 {
       
    54   global $db, $session, $paths, $template, $plugins; // Common objects
       
    55   if(!$q = $paths->getParam(0)) $q = ( isset($_GET['q']) ) ? $_GET['q'] : false;
       
    56   if(isset($_GET['words_any']))
       
    57   {
       
    58     $q = '';
       
    59     if(!empty($_GET['words_any']))
       
    60     {
       
    61       $q .= $_GET['words_any'] . ' ';
       
    62     }
       
    63     if(!empty($_GET['exact_phrase']))
       
    64     {
       
    65       $q .= '"' . $_GET['exact_phrase'] . '" ';
       
    66     }
       
    67     if(!empty($_GET['exclude_words']))
       
    68     {
       
    69       $not = explode(' ', $_GET['exclude_words']);
       
    70       foreach ( $not as $i => $foo )
       
    71       {
       
    72         $not[$i] = '-' . $not[$i];
       
    73       }
       
    74       $q .= implode(' ', $not);
       
    75     }
       
    76     if(!empty($_GET['require_words']))
       
    77     {
       
    78       $req = explode(' ', $_GET['require_words']);
       
    79       foreach ( $req as $i => $foo )
       
    80       {
       
    81         $req[$i] = '+' . $req[$i];
       
    82       }
       
    83       $q .= implode(' ', $req);
       
    84     }
       
    85   }
       
    86   $template->header();
       
    87   if(!empty($q))
       
    88   {
       
    89     switch(SEARCH_MODE)
       
    90     {
       
    91       
       
    92       case "FULLTEXT":
       
    93         if ( isset($_GET['offset']) )
       
    94         {
       
    95           $offset = intval($_GET['offset']);
       
    96         }
       
    97         else
       
    98         {
       
    99           $offset = 0;
       
   100         }
       
   101         $sql = $db->sql_query('SELECT search_id FROM '.table_prefix.'search_cache WHERE query=\''.$db->escape($q).'\';');
       
   102         if(!$sql)
       
   103         {
       
   104           $db->_die('Error scanning search query cache');
       
   105         }
       
   106         if($db->numrows() > 0)
       
   107         {
       
   108           $row = $db->fetchrow();
       
   109           $db->free_result();
       
   110           search_fetch_fulltext_results(intval($row['search_id']), $offset);
       
   111         }
       
   112         else
       
   113         {
       
   114           // Perform search
       
   115           
       
   116           $search = new MySQL_Fulltext_Search();
       
   117           
       
   118           // Parse the query
       
   119           $parse = new Searcher();
       
   120           $query = $parse->parseQuery($q);
       
   121           unset($parse);
       
   122           
       
   123           // Send query to MySQL
       
   124           $sql = $search->search($q);
       
   125           $results = Array();
       
   126           if ( $row = $db->fetchrow($sql) )
       
   127           {
       
   128             do {
       
   129               $results[] = $row;
       
   130             } while ( $row = $db->fetchrow($sql) );
       
   131           }
       
   132           else
       
   133           {
       
   134             // echo '<div class="warning-box">No pages that matched your search criteria could be found.</div>';
       
   135           }
       
   136           $texts = Array();
       
   137           foreach ( $results as $result )
       
   138           {
       
   139             $texts[] = render_fulltext_result($result, $query);
       
   140           }
       
   141           
       
   142           // Store the result in the search cache...if someone makes the same query later we can skip searching and rendering
       
   143           // This cache is cleared when an affected page is saved.
       
   144           
       
   145           $results = serialize($texts);
       
   146           
       
   147           $sql = $db->sql_query('INSERT INTO '.table_prefix.'search_cache(search_time,query,results) VALUES('.time().', \''.$db->escape($q).'\', \''.$db->escape($results).'\');');
       
   148           if($sql)
       
   149           {
       
   150             search_render_fulltext_results(unserialize($results), $offset, $q);
       
   151           }
       
   152           else
       
   153           {
       
   154             $db->_die('Error inserting search into cache');
       
   155           }
       
   156           
       
   157         }
       
   158         break;
       
   159 
       
   160       case "BUILTIN":
       
   161         $titles = $paths->makeTitleSearcher(isset($_GET['match_case']));
       
   162         if ( isset($_GET['offset']) )
       
   163         {
       
   164           $offset = intval($_GET['offset']);
       
   165         }
       
   166         else
       
   167         {
       
   168           $offset = 0;
       
   169         }
       
   170         $sql = $db->sql_query('SELECT search_id FROM '.table_prefix.'search_cache WHERE query=\''.$db->escape($q).'\';');
       
   171         if(!$sql)
       
   172         {
       
   173           $db->_die('Error scanning search query cache');
       
   174         }
       
   175         if($db->numrows() > 0)
       
   176         {
       
   177           $row = $db->fetchrow();
       
   178           $db->free_result();
       
   179           search_show_results(intval($row['search_id']), $offset);
       
   180         }
       
   181         else
       
   182         {
       
   183           $titles->search($q, $paths->get_page_titles());
       
   184           $search = $paths->makeSearcher(isset($_GET['match_case']));
       
   185           $texts = $paths->fetch_page_search_resource();
       
   186           $search->searchMySQL($q, $texts);
       
   187           
       
   188           $results = Array();
       
   189           $results['text'] = $search->results;
       
   190           $results['page'] = $titles->results;
       
   191           $results['warn'] = $search->warnings;
       
   192           
       
   193           $results = serialize($results);
       
   194           
       
   195           $sql = $db->sql_query('INSERT INTO '.table_prefix.'search_cache(search_time,query,results) VALUES('.time().', \''.$db->escape($q).'\', \''.$db->escape($results).'\');');
       
   196           if($sql)
       
   197           {
       
   198             search_render_results(unserialize($results), $offset, $q);
       
   199           }
       
   200           else
       
   201           {
       
   202             $db->_die('Error inserting search into cache');
       
   203           }
       
   204         }
       
   205         break;
       
   206     }
       
   207     $code = $plugins->setHook('search_results'); // , Array('query'=>$q));
       
   208     foreach ( $code as $cmd )
       
   209     {
       
   210       eval($cmd);
       
   211     }
       
   212     ?>
       
   213     <form action="<?php echo makeUrl($paths->page); ?>" method="get">
       
   214       <p>
       
   215         <input type="text" name="q" size="40" value="<?php echo htmlspecialchars( $q ); ?>" />  <input type="submit" value="Search" />  <small><a href="<?php echo makeUrlNS('Special', 'Search'); ?>">Advanced Search</a></small>
       
   216       </p>
       
   217     </form>
       
   218     <?php
       
   219   }
       
   220   else
       
   221   {
       
   222   ?>
       
   223     <br />
       
   224     <form action="<?php echo makeUrl($paths->page); ?>" method="get">
       
   225       <div class="tblholder">
       
   226         <table border="0" style="width: 100%;" cellspacing="1" cellpadding="4">
       
   227           <tr><th colspan="2">Advanced Search</th></tr>
       
   228           <tr>
       
   229             <td class="row1">Search for pages with <b>any of these words</b>:</td>
       
   230             <td class="row1"><input type="text" name="words_any" size="40" /></td>
       
   231           </tr>
       
   232           <tr>
       
   233             <td class="row2">with <b>this exact phrase</b>:</td>
       
   234             <td class="row2"><input type="text" name="exact_phrase" size="40" /></td>
       
   235           </tr>
       
   236           <tr>
       
   237             <td class="row1">with <b>none of these words</b>:</td>
       
   238             <td class="row1"><input type="text" name="exclude_words" size="40" /></td>
       
   239           </tr>
       
   240           <tr>
       
   241             <td class="row2">with <b>all of these words</b>:</td>
       
   242             <td class="row2"><input type="text" name="require_words" size="40" /></td>
       
   243           </tr>
       
   244           <tr>
       
   245             <td class="row1">
       
   246               <label for="chk_case">Case-sensitive search:</label>
       
   247             </td>
       
   248             <td class="row1">
       
   249               <input type="checkbox" name="match_case" id="chk_case" />
       
   250             </td>
       
   251           </tr>
       
   252           <tr>
       
   253             <th colspan="2" class="subhead">
       
   254               <input type="submit" name="do_search" value="Search" />
       
   255             </td>
       
   256           </tr>
       
   257         </table>
       
   258       </div>
       
   259     </form>
       
   260   <?php
       
   261   }
       
   262   $template->footer();
       
   263 }
       
   264 
       
   265 function search_show_results($search_id, $start = 0)
       
   266 {
       
   267   global $db, $session, $paths, $template, $plugins; // Common objects
       
   268   $q = $db->sql_query('SELECT query,results,search_time FROM '.table_prefix.'search_cache WHERE search_id='.intval($search_id).';');
       
   269   if(!$q)
       
   270     return $db->get_error('Error selecting cached search results');
       
   271   $row = $db->fetchrow();
       
   272   $db->free_result();
       
   273   $results = unserialize($row['results']);
       
   274   search_render_results($results, $start, $row['query']);
       
   275 }
       
   276 
       
   277 function search_render_results($results, $start = 0, $q = '')
       
   278 {
       
   279   global $db, $session, $paths, $template, $plugins; // Common objects
       
   280   $nr1 = sizeof($results['page']);
       
   281   $nr2 = sizeof($results['text']);
       
   282   $nr  = ( $nr1 > $nr2 ) ? $nr1 : $nr2;
       
   283   $results['page'] = array_slice($results['page'], $start, SEARCH_RESULTS_PER_PAGE);
       
   284   $results['text'] = array_slice($results['text'], $start, SEARCH_RESULTS_PER_PAGE);
       
   285   
       
   286   // Pagination
       
   287   $pagination = '';
       
   288   if ( $nr1 > SEARCH_RESULTS_PER_PAGE || $nr2 > SEARCH_RESULTS_PER_PAGE )
       
   289   {
       
   290     $pagination .= '<div class="tblholder" style="padding: 0; display: table; margin: 0 0 0 auto; float: right;">
       
   291           <table border="0" style="width: 100%;" cellspacing="1" cellpadding="4">
       
   292           <tr>
       
   293           <th>Page:</th>';
       
   294     $num_pages = ceil($nr / SEARCH_RESULTS_PER_PAGE);
       
   295     $j = 0;
       
   296     for ( $i = 1; $i <= $num_pages; $i++ ) 
       
   297     {
       
   298       if ($j == $start)
       
   299         $pagination .= '<td class="row1"><b>' . $i . '</b></td>';
       
   300       else
       
   301         $pagination .= '<td class="row1"><a href="' . makeUrlNS('Special', 'Search', 'q=' . urlencode($q) . '&offset=' . $j, true) . '">' . $i . '</a></td>';
       
   302       $j = $j + SEARCH_RESULTS_PER_PAGE;
       
   303     }
       
   304     $pagination .= '</tr></table></div>';
       
   305   }
       
   306   
       
   307   echo $pagination;
       
   308   
       
   309   if ( $nr1 >= $start )
       
   310   {
       
   311     echo '<h3>Page title matches</h3>';
       
   312     if(count($results['page']) < 1)
       
   313     {
       
   314       echo '<div class="error-box">No pages with a title that matched your search criteria could be found.</div>';
       
   315     }
       
   316     else
       
   317     {
       
   318       echo '<p>';
       
   319       foreach($results['page'] as $page => $text)
       
   320       {
       
   321         echo '<a href="'.makeUrl($page).'">'.$paths->pages[$page]['name'].'</a><br />';
       
   322       }
       
   323       echo '</p>';
       
   324     }
       
   325   }
       
   326   if ( $nr2 >= $start )
       
   327   {
       
   328     echo '<h3>Page text matches</h3>';
       
   329     if(count($results['text']) < 1)
       
   330     {
       
   331       echo '<div class="error-box">No page text that matched your search criteria could be found.</div>';
       
   332     }
       
   333     else
       
   334     {
       
   335       foreach($results['text'] as $kpage => $text)
       
   336       {
       
   337         preg_match('#^ns=('.implode('|', array_keys($paths->nslist)).');pid=(.*?)$#i', $kpage, $matches);
       
   338         $page = $paths->nslist[$matches[1]] . $matches[2];
       
   339         echo '<p><span style="font-size: larger;"><a href="'.makeUrl($page).'">'.$paths->pages[$page]['name'].'</a></span><br />'.$text.'</p>';
       
   340       }
       
   341     }
       
   342   }
       
   343   if(count($results['warn']) > 0)
       
   344     echo '<div class="warning-box"><b>Your search may not include all results.</b><br />The following errors were encountered during the search:<br /><ul><li>'.implode('</li><li>', $results['warn']).'</li></ul></div>';
       
   345   echo $pagination;
       
   346 }
       
   347 
       
   348 function render_fulltext_result($result, $query)
       
   349 {
       
   350   global $db, $session, $paths, $template, $plugins; // Common objects
       
   351   preg_match('#^ns=('.implode('|', array_keys($paths->nslist)).');pid=(.*?)$#i', $result['page_identifier'], $matches);
       
   352   $page = $paths->nslist[$matches[1]] . $matches[2];
       
   353   //$score = round($result['score'] * 100, 1);
       
   354   $score = number_format($result['score'], 2);
       
   355   $char_length = $result['length'];
       
   356   $result_template = <<<TPLCODE
       
   357   <div class="search-result">
       
   358     <h3><a href="{HREF}">{TITLE}</a></h3>
       
   359     <p>{TEXT}</p>
       
   360     <p>
       
   361       <span class="search-result-info">{NAMESPACE} - Relevance score: {SCORE} ({LENGTH} bytes)</span>
       
   362     </p>
       
   363   </div>
       
   364 TPLCODE;
       
   365   $parser = $template->makeParserText($result_template);
       
   366   
       
   367   $pt =& $result['page_text'];
       
   368   $space_chars = Array("\t", "\n", "\r", " ");
       
   369   
       
   370   $words = array_merge($query['any'], $query['req']);
       
   371   $pt = htmlspecialchars($pt);
       
   372   $words2 = array();
       
   373   
       
   374   for ( $i = 0; $i < sizeof($words); $i++)
       
   375   {
       
   376     if(!empty($words[$i]))
       
   377       $words2[] = preg_quote($words[$i]);
       
   378   }
       
   379   
       
   380   $regex = '/(' . implode('|', $words2) . ')/i';
       
   381   $pt = preg_replace($regex, '<span class="search-term">\\1</span>', $pt);
       
   382   
       
   383   $title = preg_replace($regex, '<span class="title-search-term">\\1</span>', $paths->pages[$page]['name']);
       
   384   
       
   385   $cut_off = false;
       
   386   
       
   387   foreach ( $words as $word )
       
   388   {
       
   389     // Boldface searched words
       
   390     $ptlen = strlen($pt);
       
   391     for ( $i = 0; $i < $ptlen; $i++ )
       
   392     {
       
   393       $len = strlen($word);
       
   394       if ( strtolower(substr($pt, $i, $len)) == strtolower($word) )
       
   395       {
       
   396         $chunk1 = substr($pt, 0, $i);
       
   397         $chunk2 = substr($pt, $i, $len);
       
   398         $chunk3 = substr($pt, ( $i + $len ));
       
   399         $pt = $chunk1 . $chunk2 . $chunk3;
       
   400         $ptlen = strlen($pt);
       
   401         // Cut off text to 150 chars or so
       
   402         if ( !$cut_off )
       
   403         {
       
   404           $cut_off = true;
       
   405           if ( $i - 75 > 0 )
       
   406           {
       
   407             // Navigate backwards until a space character is found
       
   408             $chunk = substr($pt, 0, ( $i - 75 ));
       
   409             $final_chunk = $chunk;
       
   410             for ( $j = strlen($chunk); $j > 0; $j = $j - 1 )
       
   411             {
       
   412               if ( in_array($chunk{$j}, $space_chars) )
       
   413               {
       
   414                 $final_chunk = substr($chunk, $j + 1);
       
   415                 break;
       
   416               }
       
   417             }
       
   418             $mid_chunk = substr($pt, ( $i - 75 ), 75);
       
   419             
       
   420             $clipped = '...' . $final_chunk . $mid_chunk . $chunk2;
       
   421             
       
   422             $chunk = substr($pt, ( $i + strlen($chunk2) + 75 ));
       
   423             $final_chunk = $chunk;
       
   424             for ( $j = 0; $j < strlen($chunk); $j++ )
       
   425             {
       
   426               if ( in_array($chunk{$j}, $space_chars) )
       
   427               {
       
   428                 $final_chunk = substr($chunk, 0, $j);
       
   429                 break;
       
   430               }
       
   431             }
       
   432             
       
   433             $end_chunk = substr($pt, ( $i + strlen($chunk2) ), 75 );
       
   434             
       
   435             $clipped .= $end_chunk . $final_chunk . '...';
       
   436             
       
   437             $pt = $clipped;
       
   438           }
       
   439           else if ( strlen($pt) > 200 )
       
   440           {
       
   441             $mid_chunk = substr($pt, ( $i - 75 ), 75);
       
   442             
       
   443             $clipped = $chunk1 . $chunk2;
       
   444             
       
   445             $chunk = substr($pt, ( $i + strlen($chunk2) + 75 ));
       
   446             $final_chunk = $chunk;
       
   447             for ( $j = 0; $j < strlen($chunk); $j++ )
       
   448             {
       
   449               if ( in_array($chunk{$j}, $space_chars) )
       
   450               {
       
   451                 $final_chunk = substr($chunk, 0, $j);
       
   452                 break;
       
   453               }
       
   454             }
       
   455             
       
   456             $end_chunk = substr($pt, ( $i + strlen($chunk2) ), 75 );
       
   457             
       
   458             $clipped .= $end_chunk . $final_chunk . '...';
       
   459             
       
   460             $pt = $clipped;
       
   461             
       
   462           }
       
   463           break 2;
       
   464         }
       
   465       }
       
   466     }
       
   467     $cut_off = false;
       
   468   }
       
   469   
       
   470   $parser->assign_vars(Array(
       
   471       'TITLE' => $title,
       
   472       'TEXT' => $pt,
       
   473       'NAMESPACE' => $matches[1],
       
   474       'SCORE' => $score,
       
   475       'LENGTH' => $char_length,
       
   476       'HREF' => makeUrl($page)
       
   477     ));
       
   478   
       
   479   return $parser->run();
       
   480   
       
   481 }
       
   482 
       
   483 function search_fetch_fulltext_results($search_id, $offset = 0)
       
   484 {
       
   485   global $db, $session, $paths, $template, $plugins; // Common objects
       
   486   $q = $db->sql_query('SELECT query,results,search_time FROM '.table_prefix.'search_cache WHERE search_id='.intval($search_id).';');
       
   487   if(!$q)
       
   488     return $db->get_error('Error selecting cached search results');
       
   489   $row = $db->fetchrow();
       
   490   $db->free_result();
       
   491   $results = unserialize($row['results']);
       
   492   search_render_fulltext_results($results, $offset, $row['query']);
       
   493 }
       
   494 
       
   495 function search_render_fulltext_results($results, $offset = 0, $query)
       
   496 {
       
   497   $num_results = sizeof($results);
       
   498   $slice = array_slice($results, $offset, SEARCH_RESULTS_PER_PAGE);
       
   499   
       
   500   if ( $num_results < 1 )
       
   501   {
       
   502     echo '<div class="warning-box">No pages that matched your search criteria could be found.</div>';
       
   503     return null;
       
   504   }
       
   505   
       
   506   $html = paginate_array($results, sizeof($results), makeUrlNS('Special', 'Search', 'q=' . urlencode($query) . '&offset=%s'), $offset, 10);
       
   507   echo $html . '<br />';
       
   508   
       
   509 }
       
   510 
       
   511 ?>