system/classes/search.class.php
author Sami Barakat <sami@sbarakat.co.uk>
Thu, 05 Nov 2009 21:35:39 +0000
branchHEAD
changeset 7442 5f5387780b30
parent 7439 57bcdd15e6d4
child 7449 ad0616900bdb
permissions -rw-r--r--
Dont display sort by author when searching by author (feature request #0000910)
     1 <?php
     2 
     3 /* Reminder: always indent with 4 spaces (no tabs). */
     4 // +---------------------------------------------------------------------------+
     5 // | Geeklog 1.6.1                                                             |
     6 // +---------------------------------------------------------------------------+
     7 // | search.class.php                                                          |
     8 // |                                                                           |
     9 // | Geeklog search class.                                                     |
    10 // +---------------------------------------------------------------------------+
    11 // | Copyright (C) 2000-2009 by the following authors:                         |
    12 // |                                                                           |
    13 // | Authors: Tony Bibbs       - tony AT geeklog DOT net                       |
    14 // |          Dirk Haun        - dirk AT haun-online DOT de                    |
    15 // |          Sami Barakat     - s.m.barakat AT gmail DOT com                  |
    16 // +---------------------------------------------------------------------------+
    17 // |                                                                           |
    18 // | This program is free software; you can redistribute it and/or             |
    19 // | modify it under the terms of the GNU General Public License               |
    20 // | as published by the Free Software Foundation; either version 2            |
    21 // | of the License, or (at your option) any later version.                    |
    22 // |                                                                           |
    23 // | This program is distributed in the hope that it will be useful,           |
    24 // | but WITHOUT ANY WARRANTY; without even the implied warranty of            |
    25 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             |
    26 // | GNU General Public License for more details.                              |
    27 // |                                                                           |
    28 // | You should have received a copy of the GNU General Public License         |
    29 // | along with this program; if not, write to the Free Software Foundation,   |
    30 // | Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.           |
    31 // |                                                                           |
    32 // +---------------------------------------------------------------------------+
    33 
    34 if (strpos(strtolower($_SERVER['PHP_SELF']), 'search.class.php') !== false) {
    35     die('This file can not be used on its own.');
    36 }
    37 
    38 require_once $_CONF['path_system'] . 'classes/plugin.class.php';
    39 require_once $_CONF['path_system'] . 'classes/searchcriteria.class.php';
    40 require_once $_CONF['path_system'] . 'classes/listfactory.class.php';
    41 
    42 /**
    43 * Geeklog Search Class
    44 *
    45 * @author Tony Bibbs, tony AT geeklog DOT net
    46 * @package net.geeklog.search
    47 *
    48 */
    49 class Search {
    50 
    51     // PRIVATE VARIABLES
    52     var $_query = '';
    53     var $_topic = '';
    54     var $_dateStart = null;
    55     var $_dateEnd = null;
    56     var $_author = '';
    57     var $_type = '';
    58     var $_keyType = '';
    59     var $_names = array();
    60     var $_url_rewrite = array();
    61     var $_append_query = array();
    62     var $_searchURL = '';
    63     var $_wordlength;
    64     var $_verbose = false; // verbose logging
    65 
    66     /**
    67     * Constructor
    68     *
    69     * Sets up private search variables
    70     *
    71     * @author Tony Bibbs, tony AT geeklog DOT net
    72     * @access public
    73     *
    74     */
    75     function Search()
    76     {
    77         global $_CONF, $_TABLES;
    78 
    79         // Set search criteria
    80         if (isset ($_GET['query'])) {
    81             $this->_query = strip_tags (COM_stripslashes ($_GET['query']));
    82         }
    83         if (isset ($_GET['topic'])) {
    84             $this->_topic = COM_applyFilter ($_GET['topic']);
    85         }
    86         if (isset ($_GET['datestart'])) {
    87             $this->_dateStart = COM_applyFilter ($_GET['datestart']);
    88         }
    89         if (isset ($_GET['dateend'])) {
    90             $this->_dateEnd = COM_applyFilter ($_GET['dateend']);
    91         }
    92         if (isset ($_GET['author'])) {
    93             $this->_author = COM_applyFilter($_GET['author']);
    94 
    95             // In case we got a username instead of uid, convert it.  This should
    96             // make custom themes for search page easier.
    97             if (!is_numeric($this->_author) && !preg_match('/^([0-9]+)$/', $this->_author) && $this->_author != '') {
    98                 $this->_author = DB_getItem($_TABLES['users'], 'uid', 'username=\'' . addslashes ($this->_author) . '\'');
    99             }
   100 
   101             if ($this->_author < 1) {
   102                 $this->_author = '';
   103             }
   104         }
   105         $this->_type = isset($_GET['type']) ? COM_applyFilter($_GET['type']) : 'all';
   106         $this->_keyType = isset($_GET['keyType']) ? COM_applyFilter($_GET['keyType']) : $_CONF['search_def_keytype'];
   107     }
   108 
   109     /**
   110     * Shows an error message to anonymous users
   111     *
   112     * This is called when anonymous users attempt to access search
   113     * functionality that has been locked down by the Geeklog admin.
   114     *
   115     * @author Tony Bibbs, tony AT geeklog DOT net
   116     * @access private
   117     * @return string HTML output for error message
   118     *
   119     */
   120     function _getAccessDeniedMessage()
   121     {
   122         global $_CONF, $LANG_LOGIN;
   123 
   124         $retval .= COM_startBlock ($LANG_LOGIN[1], '',
   125                         COM_getBlockTemplate ('_msg_block', 'header'));
   126         $login = new Template($_CONF['path_layout'] . 'submit');
   127         $login->set_file (array ('login'=>'submitloginrequired.thtml'));
   128         $login->set_var ( 'xhtml', XHTML );
   129         $login->set_var ('login_message', $LANG_LOGIN[2]);
   130         $login->set_var ('site_url', $_CONF['site_url']);
   131         $login->set_var ('site_admin_url', $_CONF['site_admin_url']);
   132         $login->set_var ('layout_url', $_CONF['layout_url']);
   133         $login->set_var ('lang_login', $LANG_LOGIN[3]);
   134         $login->set_var ('lang_newuser', $LANG_LOGIN[4]);
   135         $login->parse ('output', 'login');
   136         $retval .= $login->finish ($login->get_var('output'));
   137         $retval .= COM_endBlock (COM_getBlockTemplate ('_msg_block', 'footer'));
   138 
   139         return $retval;
   140     }
   141 
   142     /**
   143     * Determines if user is allowed to perform a search
   144     *
   145     * Geeklog has a number of settings that may prevent
   146     * the access anonymous users have to the search engine.
   147     * This performs those checks
   148     *
   149     * @author Tony Bibbs, tony AT geeklog DOT net
   150     * @access private
   151     * @return boolean True if search is allowed, otherwise false
   152     *
   153     */
   154     function _isSearchAllowed()
   155     {
   156         global $_USER, $_CONF;
   157 
   158         if (empty($_USER['username'])) {
   159             //check if an anonymous user is attempting to illegally access privilege search capabilities
   160             if (($this->_type != 'all') OR !empty($this->_dateStart) OR !empty($this->_dateEnd) OR ($this->_author > 0) OR !empty($topic)) {
   161                 if (($_CONF['loginrequired'] == 1) OR ($_CONF['searchloginrequired'] >= 1)) {
   162                     return false;
   163                 }
   164             } else {
   165                 if (($_CONF['loginrequired'] == 1) OR ($_CONF['searchloginrequired'] == 2)) {
   166                     return false;
   167                 }
   168             }
   169         }
   170 
   171         return true;
   172     }
   173 
   174     /**
   175     * Determines if user is allowed to use the search form
   176     *
   177     * Geeklog has a number of settings that may prevent
   178     * the access anonymous users have to the search engine.
   179     * This performs those checks
   180     *
   181     * @author Dirk Haun, dirk AT haun-online DOT de
   182     * @access private
   183     * @return boolean True if form usage is allowed, otherwise false
   184     *
   185     */
   186     function _isFormAllowed ()
   187     {
   188         global $_CONF, $_USER;
   189 
   190         if (empty($_USER['username']) AND (($_CONF['loginrequired'] == 1) OR ($_CONF['searchloginrequired'] >= 1))) {
   191             return false;
   192         }
   193 
   194         return true;
   195     }
   196 
   197     /**
   198     * Shows search form
   199     *
   200     * Shows advanced search page
   201     *
   202     * @author Tony Bibbs, tony AT geeklog DOT net
   203     * @access public
   204     * @return string HTML output for form
   205     *
   206     */
   207     function showForm ()
   208     {
   209         global $_CONF, $_TABLES, $LANG09;
   210 
   211         $retval = '';
   212 
   213         // Verify current user my use the search form
   214         if (!$this->_isFormAllowed()) {
   215             return $this->_getAccessDeniedMessage();
   216         }
   217 
   218         $retval .= COM_startBlock($LANG09[1],'advancedsearch.html');
   219         $searchform = new Template($_CONF['path_layout'].'search');
   220         $searchform->set_file (array ('searchform' => 'searchform.thtml',
   221                                       'authors'    => 'searchauthors.thtml'));
   222         $searchform->set_var( 'xhtml', XHTML );
   223         $searchform->set_var('search_intro', $LANG09[19]);
   224         $searchform->set_var('site_url', $_CONF['site_url']);
   225         $searchform->set_var('site_admin_url', $_CONF['site_admin_url']);
   226         $searchform->set_var('layout_url', $_CONF['layout_url']);
   227         $searchform->set_var('lang_keywords', $LANG09[2]);
   228         $searchform->set_var('lang_date', $LANG09[20]);
   229         $searchform->set_var('lang_to', $LANG09[21]);
   230         $searchform->set_var('date_format', $LANG09[22]);
   231         $searchform->set_var('lang_topic', $LANG09[3]);
   232         $searchform->set_var('lang_all', $LANG09[4]);
   233         $searchform->set_var('topic_option_list',
   234                             COM_topicList ('tid,topic', $this->_topic));
   235         $searchform->set_var('lang_type', $LANG09[5]);
   236         $searchform->set_var('lang_results', $LANG09[59]);
   237         $searchform->set_var('lang_per_page', $LANG09[60]);
   238 
   239         $searchform->set_var('lang_exact_phrase', $LANG09[43]);
   240         $searchform->set_var('lang_all_words', $LANG09[44]);
   241         $searchform->set_var('lang_any_word', $LANG09[45]);
   242         $searchform->set_var('lang_titles', $LANG09[69]);
   243 
   244         $escquery = htmlspecialchars($this->_query);
   245         $escquery = str_replace(array('{', '}'), array('&#123;', '&#125;'),
   246                                 $escquery);
   247         $searchform->set_var ('query', $escquery);
   248         $searchform->set_var ('datestart', $this->_dateStart);
   249         $searchform->set_var ('dateend', $this->_dateEnd);
   250 
   251         $phrase_selected = '';
   252         $all_selected = '';
   253         $any_selected = '';
   254         if ($this->_keyType == 'phrase') {
   255             $phrase_selected = 'selected="selected"';
   256         } else if ($this->_keyType == 'all') {
   257             $all_selected = 'selected="selected"';
   258         } else if ($this->_keyType == 'any') {
   259             $any_selected = 'selected="selected"';
   260         }
   261         $searchform->set_var ('key_phrase_selected', $phrase_selected);
   262         $searchform->set_var ('key_all_selected', $all_selected);
   263         $searchform->set_var ('key_any_selected', $any_selected);
   264 
   265         $options = '';
   266         $plugintypes = array('all' => $LANG09[4], 'stories' => $LANG09[6], 'comments' => $LANG09[7]);
   267         $plugintypes = array_merge($plugintypes, PLG_getSearchTypes());
   268         // Generally I don't like to hardcode HTML but this seems easiest
   269         foreach ($plugintypes as $key => $val) {
   270             $options .= "<option value=\"$key\"";
   271             if ($this->_type == $key)
   272                 $options .= ' selected="selected"';
   273             $options .= ">$val</option>".LB;
   274         }
   275         $searchform->set_var('plugin_types', $options);
   276 
   277         if ($_CONF['contributedbyline'] == 1) {
   278             $searchform->set_var('lang_authors', $LANG09[8]);
   279             $searchusers = array();
   280             $result = DB_query("SELECT DISTINCT uid FROM {$_TABLES['comments']}");
   281             while ($A = DB_fetchArray($result)) {
   282                 $searchusers[$A['uid']] = $A['uid'];
   283             }
   284             $result = DB_query("SELECT DISTINCT uid FROM {$_TABLES['stories']} WHERE (date <= NOW()) AND (draft_flag = 0)");
   285             while ($A = DB_fetchArray($result)) {
   286                 $searchusers[$A['uid']] = $A['uid'];
   287             }
   288 
   289             $inlist = implode(',', $searchusers);
   290 
   291             if (!empty ($inlist)) {
   292                 $sql = "SELECT uid,username,fullname FROM {$_TABLES['users']} WHERE uid IN ($inlist)";
   293                 if (isset ($_CONF['show_fullname']) && ($_CONF['show_fullname'] == 1)) {
   294                     /* Caveat: This will group all users with an emtpy fullname
   295                      *         together, so it's not exactly sorted by their
   296                      *         full name ...
   297                      */
   298                     $sql .= ' ORDER BY fullname,username';
   299                 } else {
   300                     $sql .= ' ORDER BY username';
   301                 }
   302                 $result = DB_query ($sql);
   303                 $options = '';
   304                 while ($A = DB_fetchArray($result)) {
   305                     $options .= '<option value="' . $A['uid'] . '"';
   306                     if ($A['uid'] == $this->_author) {
   307                         $options .= ' selected="selected"';
   308                     }
   309                     $options .= '>' . htmlspecialchars(COM_getDisplayName('', $A['username'], $A['fullname'])) . '</option>';
   310                 }
   311                 $searchform->set_var('author_option_list', $options);
   312                 $searchform->parse('author_form_element', 'authors', true);
   313             } else {
   314                 $searchform->set_var('author_form_element', '<input type="hidden" name="author" value="0"' . XHTML . '>');
   315             }
   316         } else {
   317             $searchform->set_var ('author_form_element',
   318                     '<input type="hidden" name="author" value="0"' . XHTML . '>');
   319         }
   320 
   321         // Results per page
   322         $options = '';
   323         $limits = explode(',', $_CONF['search_limits']);
   324         foreach ($limits as $limit) {
   325             $options .= "<option value=\"$limit\"";
   326             if ($_CONF['num_search_results'] == $limit) {
   327                 $options .= ' selected="selected"';
   328             }
   329             $options .= ">$limit</option>" . LB;
   330         }
   331         $searchform->set_var('search_limits', $options);
   332 
   333         $searchform->set_var('lang_search', $LANG09[10]);
   334         $searchform->parse('output', 'searchform');
   335 
   336         $retval .= $searchform->finish($searchform->get_var('output'));
   337         $retval .= COM_endBlock();
   338 
   339         return $retval;
   340     }
   341 
   342     /**
   343     * Performs search on all stories
   344     *
   345     * @access private
   346     * @return object plugin object
   347     *
   348     */
   349     function _searchStories()
   350     {
   351         global $_TABLES, $_DB_dbms, $LANG09;
   352 
   353         // Make sure the query is SQL safe
   354         $query = trim(addslashes($this->_query));
   355 
   356         $sql = 'SELECT s.sid AS id, s.title AS title, s.introtext AS description, ';
   357         $sql .= 'UNIX_TIMESTAMP(s.date) AS date, s.uid AS uid, s.hits AS hits, ';
   358         $sql .= 'CONCAT(\'/article.php?story=\',s.sid) AS url ';
   359         $sql .= 'FROM '.$_TABLES['stories'].' AS s, '.$_TABLES['users'].' AS u ';
   360         $sql .= 'WHERE (draft_flag = 0) AND (date <= NOW()) AND (u.uid = s.uid) ';
   361         $sql .= COM_getPermSQL('AND') . COM_getTopicSQL('AND') . COM_getLangSQL('sid', 'AND') . ' ';
   362 
   363         if (!empty($this->_topic)) {
   364             $sql .= 'AND (s.tid = \''.$this->_topic.'\') ';
   365         }
   366         if (!empty($this->_author)) {
   367             $sql .= 'AND (s.uid = \''.$this->_author.'\') ';
   368         }
   369 
   370         $search_s = new SearchCriteria('stories', $LANG09[65]);
   371 
   372         $columns = array('title' => 'title', 'introtext', 'bodytext');
   373         $sql .= $search_s->getDateRangeSQL('AND', 'date', $this->_dateStart, $this->_dateEnd);
   374         list($sql, $ftsql) = $search_s->buildSearchSQL($this->_keyType, $query, $columns, $sql);
   375 
   376         $search_s->setSQL($sql);
   377         $search_s->setFTSQL($ftsql);
   378         $search_s->setRank(5);
   379         $search_s->setURLRewrite(true);
   380 
   381         // Search Story Comments
   382         $sql = 'SELECT c.cid AS id, c.title AS title, c.comment AS description, ';
   383         $sql .= 'UNIX_TIMESTAMP(c.date) AS date, c.uid AS uid, ';
   384 
   385         // MSSQL has a problem when concatenating numeric values
   386         if ($_DB_dbms == 'mssql') {
   387             $sql .= '\'/comment.php?mode=view&amp;cid=\' + CAST(c.cid AS varchar(10)) AS url ';
   388         } else {
   389             $sql .= 'CONCAT(\'/comment.php?mode=view&amp;cid=\',c.cid) AS url ';
   390         }
   391 
   392         $sql .= 'FROM '.$_TABLES['users'].' AS u, '.$_TABLES['comments'].' AS c ';
   393         $sql .= 'LEFT JOIN '.$_TABLES['stories'].' AS s ON ((s.sid = c.sid) ';
   394         $sql .= COM_getPermSQL('AND',0,2,'s').COM_getTopicSQL('AND',0,'s').COM_getLangSQL('sid','AND','s').') ';
   395         $sql .= 'WHERE (u.uid = c.uid) AND (s.draft_flag = 0) AND (s.commentcode >= 0) AND (s.date <= NOW()) ';
   396 
   397         if (!empty($this->_topic)) {
   398             $sql .= 'AND (s.tid = \''.$this->_topic.'\') ';
   399         }
   400         if (!empty($this->_author)) {
   401             $sql .= 'AND (c.uid = \''.$this->_author.'\') ';
   402         }
   403 
   404         $search_c = new SearchCriteria('comments', array($LANG09[65],$LANG09[66]));
   405 
   406         $columns = array('title' => 'c.title', 'comment');
   407         $sql .= $search_c->getDateRangeSQL('AND', 'c.date', $this->_dateStart, $this->_dateEnd);
   408         list($sql, $ftsql) = $search_c->buildSearchSQL($this->_keyType, $query, $columns, $sql);
   409 
   410         $search_c->setSQL($sql);
   411         $search_c->setFTSQL($ftsql);
   412         $search_c->setRank(2);
   413 
   414         return array($search_s, $search_c);
   415     }
   416 
   417     /**
   418     * Kicks off the appropriate search(es)
   419     *
   420     * Initiates the search engine and returns HTML formatted
   421     * results. It also provides support to plugins using a
   422     * search API. Backwards compatibility has been incorporated
   423     * in this function to allow legacy support to plugins using
   424     * the old API calls defined versions prior to Geeklog 1.5.1
   425     *
   426     * @access public
   427     * @return string HTML output for search results
   428     *
   429     */
   430     function doSearch()
   431     {
   432         global $_CONF, $LANG01, $LANG09, $LANG31;
   433 
   434         // Verify current user can perform requested search
   435         if (!$this->_isSearchAllowed())
   436         {
   437             return $this->_getAccessDeniedMessage();
   438         }
   439 
   440         // Make sure there is a query string
   441         // Full text searches have a minimum word length of 3 by default
   442         if ((empty($this->_query) && empty($this->_author) && empty($this->_topic)) || ($_CONF['search_use_fulltext'] && strlen($this->_query) < 3))
   443         {
   444             $retval = '<p>' . $LANG09[41] . '</p>' . LB;
   445             $retval .= $this->showForm();
   446 
   447             return $retval;
   448         }
   449 
   450         // Build the URL strings
   451         $this->_searchURL = $_CONF['site_url'] . '/search.php?query=' . urlencode($this->_query) .
   452             ((!empty($this->_keyType))    ? '&amp;keyType=' . $this->_keyType : '' ) .
   453             ((!empty($this->_dateStart))  ? '&amp;datestart=' . $this->_dateStart : '' ) .
   454             ((!empty($this->_dateEnd))    ? '&amp;dateend=' . $this->_dateEnd : '' ) .
   455             ((!empty($this->_topic))      ? '&amp;topic=' . $this->_topic : '' ) .
   456             ((!empty($this->_author))     ? '&amp;author=' . $this->_author : '' );
   457 
   458         $url = "{$this->_searchURL}&amp;type={$this->_type}&amp;mode=";
   459         $obj = new ListFactory($url.'search', $_CONF['search_limits'], $_CONF['num_search_results']);
   460         $obj->setField('ID', 'id', false);
   461         $obj->setField('URL', 'url', false);
   462 
   463         $show_num  = $_CONF['search_show_num'];
   464         $show_type = $_CONF['search_show_type'];
   465         $show_user = $_CONF['contributedbyline'];
   466         $show_hits = !$_CONF['hideviewscount'];
   467         $style = isset($_CONF['search_style']) ? $_CONF['search_style'] : 'google';
   468 
   469         if ($style == 'table')
   470         {
   471             $obj->setStyle('table');
   472             //             Title        Name            Display     Sort   Format
   473             $obj->setField($LANG09[62], LF_ROW_NUMBER,  $show_num,  false, '<b>%d.</b>');
   474             $obj->setField($LANG09[5],  LF_SOURCE_TITLE,$show_type, true,  '<b>%s</b>');
   475             $obj->setField($LANG09[16], 'title',        true,       true);
   476             $obj->setField($LANG09[63], 'description',  true,       false);
   477             $obj->setField($LANG09[17], 'date',         true,       true);
   478             $obj->setField($LANG09[18], 'uid',          $show_user, true);
   479             $obj->setField($LANG09[50], 'hits',         $show_hits, true);
   480             $this->_wordlength = 7;
   481         }
   482         else if ($style == 'google')
   483         {
   484             $sort_uid = $this->_author == '' ? true : false;
   485             $obj->setStyle('inline');
   486             $obj->setField('',          LF_ROW_NUMBER,  $show_num,  false, '<b>%d.</b>');
   487             $obj->setField($LANG09[16], 'title',        true,       true,  '%s<br' . XHTML . '>');
   488             $obj->setField('',          'description',  true,       false, '%s<br' . XHTML . '>');
   489             $obj->setField('',          '_html',        true,       false, '<span style="color:green;">');
   490             $obj->setField($LANG09[18], 'uid',          $show_user, $sort_uid,  $LANG01[104].' %s ');
   491             $obj->setField($LANG09[17], 'date',         true,       true,  $LANG01[36].' %s');
   492             $obj->setField($LANG09[5],  LF_SOURCE_TITLE,$show_type, true,  ' - %s');
   493             $obj->setField($LANG09[50], 'hits',         $show_hits, true,  ' - %s '.$LANG09[50]);
   494             $obj->setField('',          '_html',        true,       false, '</span>');
   495             $this->_wordlength = 50;
   496         }
   497         $obj->setDefaultSort('hits');
   498         // set this only now, for compatibility with PHP 4
   499         $obj->setRowFunction(array($this, 'searchFormatCallback'));
   500 
   501         // Start search timer
   502         $searchtimer = new timerobject();
   503         $searchtimer->setPrecision(4);
   504         $searchtimer->startTimer();
   505 
   506         // Have plugins do their searches
   507         $page = isset($_GET['page']) ? COM_applyFilter($_GET['page'], true) : 1;
   508         $result_plugins = PLG_doSearch($this->_query, $this->_dateStart,
   509             $this->_dateEnd, $this->_topic, $this->_type,
   510             $this->_author, $this->_keyType, $page, 5);
   511 
   512         // Add core searches
   513         $result_plugins = array_merge($result_plugins, $this->_searchStories());
   514 
   515         // Loop through all plugins separating the new API from the old
   516         $new_api = 0;
   517         $old_api = 0;
   518         $num_results = 0;
   519 
   520         foreach ($result_plugins as $result)
   521         {
   522             if (is_a($result, 'SearchCriteria'))
   523             {
   524                 $debug_info = $result->getName().' using APIv2';
   525 
   526                 if ($this->_type != 'all' && $this->_type != $result->getName())
   527                 {
   528                     if ($this->_verbose) {
   529                         $new_api++;
   530                         COM_errorLog($debug_info.'. Skipped as type is not '.$this->_type);
   531                     }
   532                     continue;
   533                 }
   534 
   535                 $api_results = $result->getResults();
   536                 if (!empty($api_results)) {
   537                     $obj->addResultArray($api_results);
   538                 }
   539 
   540                 $api_callback_func = $result->getCallback();
   541                 if (!empty($api_callback_func))
   542                 {
   543                     $debug_info .= ' with Callback Function.';
   544                     $obj->setCallback($result->getLabel(), $result->getName(), $api_callback_func, $result->getRank(), $result->getTotal());
   545                 }
   546                 else
   547                 {
   548                     if ($_CONF['search_use_fulltext'] == true && $result->getFTSQL() != '') {
   549                         $sql = $result->getFTSQL();
   550                     } else {
   551                         $sql = $result->getSQL();
   552                     }
   553 
   554                     $sql = $this->_convertsql($sql);
   555                     $debug_info .= ' with SQL = '.print_r($sql,1);
   556                     $obj->setQuery($result->getLabel(), $result->getName(), $sql, $result->getRank());
   557                 }
   558 
   559                 $this->_url_rewrite[ $result->getName() ] = $result->UrlRewriteEnable();
   560                 $this->_append_query[ $result->getName() ] = $result->AppendQueryEnable();
   561 
   562                 if ($this->_verbose) {
   563                     $new_api++;
   564                     COM_errorLog($debug_info);
   565                 }
   566             }
   567             else if (is_a($result, 'Plugin') && $result->num_searchresults != 0)
   568             {
   569                 // Some backwards compatibility
   570                 if ($this->_verbose) {
   571                     $old_api++;
   572                     $debug_info = $result->plugin_name.' using APIv1 with backwards compatibility.';
   573                     $debug_info .= ' Count: ' . $result->num_searchresults;
   574                     $debug_info .= ' Headings: ' . implode(',', $result->searchheading);
   575                     COM_errorLog($debug_info);
   576                 }
   577 
   578                 // Find the column heading names that closely match what we are looking for
   579                 // There may be issues here on different languages, but this _should_ capture most of the data
   580                 $col_title = $this->_findColumn($result->searchheading, array($LANG09[16],$LANG31[4],'Question', 'Site Page'));//Title,Subject
   581                 $col_desc = $this->_findColumn($result->searchheading, array($LANG09[63],'Answer'));
   582                 $col_date = $this->_findColumn($result->searchheading, array($LANG09[17]));//'Date','Date Added','Last Updated','Date & Time'
   583                 $col_user = $this->_findColumn($result->searchheading, array($LANG09[18],'Submited by'));
   584                 $col_hits = $this->_findColumn($result->searchheading, array($LANG09[50],$LANG09[23],'Downloads','Clicks'));//'Hits','Views'
   585 
   586                 $label = str_replace($LANG09[59], '', $result->searchlabel);
   587                 $num_results += $result->num_itemssearched;
   588 
   589                 // Extract the results
   590                 for ($i = 0; $i < 5; $i++)
   591                 {
   592                     // If the plugin does not repect the $perpage perameter force it here.
   593                     $j = ($i + ($page * 5)) - 5;
   594                     if ($j >= count($result->searchresults))
   595                         break;
   596 
   597                     $old_row = $result->searchresults[$j];
   598                     if ($col_date != -1)
   599                     {
   600                         // Convert the date back to a timestamp
   601                         $date = $old_row[ $col_date ];
   602                         $date = substr($date, 0, strpos($date, '@'));
   603                         $date = ($date == '' ? $old_row[$col_date] : strtotime($date));
   604                     }
   605 
   606                     $api_results = array(
   607                                 LF_SOURCE_NAME =>   $result->plugin_name,
   608                                 LF_SOURCE_TITLE =>  $label,
   609                                 'title' =>        $col_title == -1 ? '<i>' . $LANG09[70] . '</i>' : $old_row[$col_title],
   610                                 'description' =>  $col_desc == -1 ? '<i>' . $LANG09[70] . '</i>' : $old_row[$col_desc],
   611                                 'date' =>         $col_date == -1 ? 'LF_NULL' : $date,
   612                                 'uid' =>          $col_user == -1 ? 'LF_NULL' : $old_row[$col_user],
   613                                 'hits' =>         $col_hits == -1 ? 'LF_NULL' : str_replace(',', '', $old_row[$col_hits])
   614                             );
   615                     preg_match('/href="([^"]+)"/i', $api_results['title'], $links);
   616                     $api_results['url'] = empty($links) ? '#' : $links[1];
   617 
   618                     $obj->addResult($api_results);
   619                 }
   620             }
   621         }
   622 
   623         // Find out how many plugins are on the old/new system
   624         if ($this->_verbose) {
   625             COM_errorLog('Search Plugins using APIv1: '.$old_api.' APIv2: '.$new_api);
   626         }
   627 
   628         // Execute the queries
   629         $results = $obj->ExecuteQueries();
   630 
   631         // Searches are done, stop timer
   632         $searchtime = $searchtimer->stopTimer();
   633 
   634         $escquery = htmlspecialchars($this->_query);
   635         $escquery = str_replace(array('{', '}'), array('&#123;', '&#125;'),
   636                                 $escquery);
   637         if ($this->_keyType == 'any')
   638         {
   639             $searchQuery = str_replace(' ', "</b>' " . $LANG09[57] . " '<b>", $escquery);
   640             $searchQuery = "'<b>$searchQuery</b>'";
   641         }
   642         else if ($this->_keyType == 'all')
   643         {
   644             $searchQuery = str_replace(' ', "</b>' " . $LANG09[56] . " '<b>", $escquery);
   645             $searchQuery = "'<b>$searchQuery</b>'";
   646         }
   647         else
   648         {
   649             $searchQuery = $LANG09[55] . " '<b>$escquery</b>'";
   650         }
   651 
   652         // Clean the query string so that sprintf works as expected
   653         $searchQuery = str_replace('%', '%%', $searchQuery);
   654 
   655         $retval = "{$LANG09[25]} $searchQuery. ";
   656         if (count($results) == 0)
   657         {
   658             $retval .= sprintf($LANG09[24], 0);
   659             $retval = '<p>' . $retval . '</p>' . LB;
   660             $retval .= '<p>' . $LANG09[13] . '</p>' . LB;
   661             $retval .= $this->showForm();
   662         }
   663         else
   664         {
   665             $retval .= $LANG09[64] . " ($searchtime {$LANG09[27]}). ";
   666             $retval .= str_replace('%', '%%', COM_createLink($LANG09[61], $url.'refine'));
   667             $retval = '<p>' . $retval . '</p>' . LB;
   668             $retval = $obj->getFormattedOutput($results, $LANG09[11], $retval, '',
   669                 $_CONF['search_show_sort'], $_CONF['search_show_limit']);
   670         }
   671 
   672         return $retval;
   673     }
   674 
   675     /**
   676     * Callback function for the ListFactory class
   677     *
   678     * This function gets called by the ListFactory class and formats
   679     * each row accordingly for example pulling usernames from the
   680     * users table and displaying a link to their profile.
   681     *
   682     * @access public
   683     * @param array $row An array of plain data to format
   684     * @return array A reformatted version of the input array
   685     *
   686     */
   687     function searchFormatCallback( $preSort, $row )
   688     {
   689         global $_CONF, $LANG09;
   690 
   691         if ($preSort)
   692         {
   693             if (is_array($row[LF_SOURCE_TITLE])) {
   694                 $row[LF_SOURCE_TITLE] = implode($_CONF['search_separator'], $row[LF_SOURCE_TITLE]);
   695             }
   696 
   697             if (is_numeric($row['uid']))
   698             {
   699                 if (empty($this->_names[ $row['uid'] ]))
   700                 {
   701                     $this->_names[ $row['uid'] ] = htmlspecialchars(COM_getDisplayName( $row['uid'] ));
   702                     if ($row['uid'] != 1)
   703                     {
   704                         $this->_names[$row['uid']] = COM_createLink($this->_names[ $row['uid'] ],
   705                                     $_CONF['site_url'] . '/users.php?mode=profile&amp;uid=' . $row['uid']);
   706                     }
   707                 }
   708                 $row['uid'] = $this->_names[ $row['uid'] ];
   709             }
   710         }
   711         else
   712         {
   713             $row[LF_SOURCE_TITLE] = COM_createLink($row[LF_SOURCE_TITLE],
   714                 $this->_searchURL.'&amp;type='.$row[LF_SOURCE_NAME].'&amp;mode=search');
   715 
   716             if ($row['url'] != '#')
   717             {
   718                 $row['url'] = ($row['url'][0] == '/' ? $_CONF['site_url'] : '') . $row['url'];
   719                 if (isset($this->_url_rewrite[$row[LF_SOURCE_NAME]]) &&
   720                         $this->_url_rewrite[$row[LF_SOURCE_NAME]]) {
   721                     $row['url'] = COM_buildUrl($row['url']);
   722                 }
   723                 if (isset($this->_append_query[$row[LF_SOURCE_NAME]]) &&
   724                         $this->_append_query[$row[LF_SOURCE_NAME]]) {
   725                     $row['url'] .= (strpos($row['url'],'?') ? '&amp;' : '?') . 'query=' . urlencode($this->_query);
   726                 }
   727             }
   728 
   729             $row['title'] = $this->_shortenText($this->_query, $row['title'], 8);
   730             $row['title'] = stripslashes(str_replace('$', '&#36;', $row['title']));
   731             $row['title'] = COM_createLink($row['title'], $row['url']);
   732 
   733             if ($row['description'] == 'LF_NULL') {
   734                 $row['description'] = '<i>' . $LANG09[70] . '</i>';
   735             } elseif ($row['description'] != '<i>' . $LANG09[70] . '</i>') {
   736                 $row['description'] = stripslashes($this->_shortenText($this->_query, PLG_replaceTags($row['description']), $this->_wordlength));
   737             }
   738 
   739             if ($row['date'] != 'LF_NULL') {
   740                 $dt = COM_getUserDateTimeFormat(intval($row['date']));
   741                 $row['date'] = $dt[0];
   742             }
   743 
   744             if ($row['hits'] != 'LF_NULL') {
   745                 $row['hits'] = COM_NumberFormat($row['hits']) . ' '; // simple solution to a silly problem!
   746             }
   747         }
   748 
   749         return $row;
   750     }
   751 
   752     /**
   753     * Shortens a long text string to only a few words
   754     *
   755     * Returns a shorter version of the in putted text centred
   756     * around the keyword. The keyword is highlighted in bold.
   757     * Adds '...' to the beginning or the end of the shortened
   758     * version depending where the text was cut. Works on a
   759     * word basis, so long words wont get cut.
   760     *
   761     * @access private
   762     * @param string $keyword The word to centre around
   763     * @param string $text The complete text string
   764     * @param int $num_words The number of words to display, best to use an odd number
   765     * @return string A short version of the text
   766     *
   767     */
   768     function _shortenText($keyword, $text, $num_words = 7)
   769     {
   770         $text = COM_getTextContent($text);
   771         $words = explode(' ', $text);
   772         $word_count = count($words);
   773         if ($word_count <= $num_words) {
   774             return COM_highlightQuery($text, $keyword, 'b');
   775         }
   776 
   777         $rt = '';
   778         $pos = $this->_stripos($text, $keyword);
   779         if ($pos !== false)
   780         {
   781             $pos_space = strpos($text, ' ', $pos);
   782             if (empty($pos_space))
   783             {
   784                 // Keyword at the end of text
   785                 $key = $word_count - 1;
   786                 $start = 0 - $num_words;
   787                 $end = 0;
   788                 $rt = '<b>...</b> ';
   789             }
   790             else
   791             {
   792                 $str = substr($text, $pos, $pos_space - $pos);
   793                 $m = (int) (($num_words - 1) / 2);
   794                 $key = $this->_arraySearch($keyword, $words);
   795                 if ($key === false) {
   796                     // Keyword(s) not found - show start of text
   797                     $key = 0;
   798                     $start = 0;
   799                     $end = $num_words - 1;
   800                 } elseif ($key <= $m) {
   801                     // Keyword at the start of text
   802                     $start = 0 - $key;
   803                     $end = $num_words - 1;
   804                     $end = ($key + $m <= $word_count - 1)
   805                          ? $key : $word_count - $m - 1;
   806                     $abs_length = abs($start) + abs($end) + 1;
   807                     if ($abs_length < $num_words) {
   808                         $end += ($num_words - $abs_length);
   809                     }
   810                 } else {
   811                     // Keyword in the middle of text
   812                     $start = 0 - $m;
   813                     $end = ($key + $m <= $word_count - 1)
   814                          ? $m : $word_count - $key - 1;
   815                     $abs_length = abs($start) + abs($end) + 1;
   816                     if ($abs_length < $num_words) {
   817                         $start -= ($num_words - $abs_length);
   818                     }
   819                     $rt = '<b>...</b> ';
   820                 }
   821             }
   822         }
   823         else
   824         {
   825             $key = 0;
   826             $start = 0;
   827             $end = $num_words - 1;
   828         }
   829 
   830         for ($i = $start; $i <= $end; $i++) {
   831             $rt .= $words[$key + $i] . ' ';
   832         }
   833         if ($key + $i != $word_count) {
   834             $rt .= ' <b>...</b>';
   835         }
   836 
   837         return COM_highlightQuery($rt, $keyword, 'b');
   838     }
   839 
   840     /**
   841     * Search array of words for keyword(s)
   842     *
   843     * @param   string  $needle    keyword(s), separated by spaces
   844     * @param   array   $haystack  array of words to search through
   845     * @return  mixed              index in $haystack or false when not found
   846     * @access  private
   847     *
   848     */
   849     function _arraySearch($needle, $haystack)
   850     {
   851         $keywords = explode(' ', $needle);
   852         $num_keywords = count($keywords);
   853 
   854         foreach ($haystack as $key => $value) {
   855             if ($this->_stripos($value, $keywords[0]) !== false) {
   856                 if ($num_keywords == 1) {
   857                     return $key;
   858                 } else {
   859                     $matched_all = true;
   860                     for ($i = 1; $i < $num_keywords; $i++) {
   861                         if ($this->_stripos($haystack[$key + $i], $keywords[$i]) === false) {
   862                             $matched_all = false;
   863                             break;
   864                         }
   865                     }
   866                     if ($matched_all) {
   867                         return $key;
   868                     }
   869                 }
   870             }
   871         }
   872 
   873         return false;
   874     }
   875 
   876     /**
   877     * Finds the similarities between heading names
   878     *
   879     * Returns the index of a heading that matches a
   880     * number of similar heading names. Used for backwards
   881     * compatibility in the doSearch() function.
   882     *
   883     * @access private
   884     * @param array $headings All the headings
   885     * @param array $find An array of alternative headings to find
   886     * @return int The index of the alternative heading
   887     *
   888     */
   889     function _findColumn( $headings, $find )
   890     {
   891         // We can't use normal for loops here as some of the
   892         // heading indexes start from 1, so foreach works better
   893         foreach ($find as $fh)
   894         {
   895             $j = 0;
   896             foreach ($headings as $h)
   897             {
   898                 if (preg_match("/$fh/i", $h) > 0) {
   899                     return $j;
   900                 }
   901                 $j++;
   902             }
   903         }
   904         return -1;
   905     }
   906 
   907     /**
   908     * Converts the MySQL CONCAT function to the MSSQL equivalent
   909     *
   910     * @access private
   911     * @param string $sql The SQL to convert
   912     * @return string MSSQL friendly SQL
   913     *
   914     */
   915     function _convertsql( $sql )
   916     {
   917         global $_DB_dbms;
   918         if ($_DB_dbms == 'mssql')
   919         {
   920             if (is_string( $sql ))
   921             {
   922                 $sql = preg_replace("/CONCAT\(([^\)]+)\)/ie", "preg_replace('/,?(\'[^\']+\'|[^,]+),/i', '\\\\1 + ', '\\1')", $sql);
   923             }
   924             else if (is_array( $sql ))
   925             {
   926                 $sql['mssql'] = preg_replace("/CONCAT\(([^\)]+)\)/ie", "preg_replace('/,?(\'[^\']+\'|[^,]+),/i', '\\\\1 + ', '\\1')", $sql['mssql']);
   927             }
   928         }
   929         return $sql;
   930     }
   931 
   932     /**
   933     * Helper function: Simulate stripos on PHP 4
   934     *
   935     * @param   string  $haystack  string to search in
   936     * @param   string  $needle    string to search for
   937     * @return  mixed              first pos of $needle in $haystack, or false
   938     *
   939     */
   940     function _stripos($haystack, $needle)
   941     {
   942         if (function_exists('stripos')) {
   943             return stripos($haystack, $needle);
   944         } elseif (empty($needle)) {
   945             return false;
   946         } else {
   947             return strpos(strtolower($haystack), strtolower($needle));
   948         }
   949     }
   950 }
   951 
   952 ?>