3 /* Reminder: always indent with 4 spaces (no tabs). */
4 // +---------------------------------------------------------------------------+
6 // +---------------------------------------------------------------------------+
7 // | search.class.php |
9 // | Geeklog search class. |
10 // +---------------------------------------------------------------------------+
11 // | Copyright (C) 2000-2009 by the following authors: |
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 // +---------------------------------------------------------------------------+
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. |
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. |
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. |
32 // +---------------------------------------------------------------------------+
34 if (strpos(strtolower($_SERVER['PHP_SELF']), 'search.class.php') !== false) {
35 die('This file can not be used on its own.');
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';
43 * Geeklog Search Class
45 * @author Tony Bibbs, tony AT geeklog DOT net
46 * @package net.geeklog.search
54 var $_dateStart = null;
59 var $_names = array();
60 var $_url_rewrite = array();
61 var $_append_query = array();
64 var $_verbose = false; // verbose logging
65 var $_titlesOnly = false;
70 * Sets up private search variables
72 * @author Tony Bibbs, tony AT geeklog DOT net
78 global $_CONF, $_TABLES;
80 // Set search criteria
81 if (isset ($_GET['query'])) {
82 $this->_query = strip_tags (COM_stripslashes ($_GET['query']));
84 if (isset ($_GET['topic'])) {
85 $this->_topic = COM_applyFilter ($_GET['topic']);
87 if (isset ($_GET['datestart'])) {
88 $this->_dateStart = COM_applyFilter ($_GET['datestart']);
90 if (isset ($_GET['dateend'])) {
91 $this->_dateEnd = COM_applyFilter ($_GET['dateend']);
93 if (isset ($_GET['author'])) {
94 $this->_author = COM_applyFilter($_GET['author']);
96 // In case we got a username instead of uid, convert it. This should
97 // make custom themes for search page easier.
98 if (!is_numeric($this->_author) && !preg_match('/^([0-9]+)$/', $this->_author) && $this->_author != '') {
99 $this->_author = DB_getItem($_TABLES['users'], 'uid', 'username=\'' . addslashes ($this->_author) . '\'');
102 if ($this->_author < 1) {
106 $this->_type = isset($_GET['type']) ? COM_applyFilter($_GET['type']) : 'all';
107 $this->_keyType = isset($_GET['keyType']) ? COM_applyFilter($_GET['keyType']) : $_CONF['search_def_keytype'];
109 $this->_titlesOnly = isset($_GET['title']) ? true : false;
113 * Shows an error message to anonymous users
115 * This is called when anonymous users attempt to access search
116 * functionality that has been locked down by the Geeklog admin.
118 * @author Tony Bibbs, tony AT geeklog DOT net
120 * @return string HTML output for error message
123 function _getAccessDeniedMessage()
125 global $_CONF, $LANG_LOGIN;
127 $retval .= COM_startBlock ($LANG_LOGIN[1], '',
128 COM_getBlockTemplate ('_msg_block', 'header'));
129 $login = new Template($_CONF['path_layout'] . 'submit');
130 $login->set_file (array ('login'=>'submitloginrequired.thtml'));
131 $login->set_var ( 'xhtml', XHTML );
132 $login->set_var ('login_message', $LANG_LOGIN[2]);
133 $login->set_var ('site_url', $_CONF['site_url']);
134 $login->set_var ('site_admin_url', $_CONF['site_admin_url']);
135 $login->set_var ('layout_url', $_CONF['layout_url']);
136 $login->set_var ('lang_login', $LANG_LOGIN[3]);
137 $login->set_var ('lang_newuser', $LANG_LOGIN[4]);
138 $login->parse ('output', 'login');
139 $retval .= $login->finish ($login->get_var('output'));
140 $retval .= COM_endBlock (COM_getBlockTemplate ('_msg_block', 'footer'));
146 * Determines if user is allowed to perform a search
148 * Geeklog has a number of settings that may prevent
149 * the access anonymous users have to the search engine.
150 * This performs those checks
152 * @author Tony Bibbs, tony AT geeklog DOT net
154 * @return boolean True if search is allowed, otherwise false
157 function _isSearchAllowed()
159 global $_USER, $_CONF;
161 if (empty($_USER['username'])) {
162 //check if an anonymous user is attempting to illegally access privilege search capabilities
163 if (($this->_type != 'all') OR !empty($this->_dateStart) OR !empty($this->_dateEnd) OR ($this->_author > 0) OR !empty($topic)) {
164 if (($_CONF['loginrequired'] == 1) OR ($_CONF['searchloginrequired'] >= 1)) {
168 if (($_CONF['loginrequired'] == 1) OR ($_CONF['searchloginrequired'] == 2)) {
178 * Determines if user is allowed to use the search form
180 * Geeklog has a number of settings that may prevent
181 * the access anonymous users have to the search engine.
182 * This performs those checks
184 * @author Dirk Haun, dirk AT haun-online DOT de
186 * @return boolean True if form usage is allowed, otherwise false
189 function _isFormAllowed ()
191 global $_CONF, $_USER;
193 if (empty($_USER['username']) AND (($_CONF['loginrequired'] == 1) OR ($_CONF['searchloginrequired'] >= 1))) {
203 * Shows advanced search page
205 * @author Tony Bibbs, tony AT geeklog DOT net
207 * @return string HTML output for form
212 global $_CONF, $_TABLES, $LANG09;
216 // Verify current user my use the search form
217 if (!$this->_isFormAllowed()) {
218 return $this->_getAccessDeniedMessage();
221 $retval .= COM_startBlock($LANG09[1],'advancedsearch.html');
222 $searchform = new Template($_CONF['path_layout'].'search');
223 $searchform->set_file (array ('searchform' => 'searchform.thtml',
224 'authors' => 'searchauthors.thtml'));
225 $searchform->set_var( 'xhtml', XHTML );
226 $searchform->set_var('search_intro', $LANG09[19]);
227 $searchform->set_var('site_url', $_CONF['site_url']);
228 $searchform->set_var('site_admin_url', $_CONF['site_admin_url']);
229 $searchform->set_var('layout_url', $_CONF['layout_url']);
230 $searchform->set_var('lang_keywords', $LANG09[2]);
231 $searchform->set_var('lang_date', $LANG09[20]);
232 $searchform->set_var('lang_to', $LANG09[21]);
233 $searchform->set_var('date_format', $LANG09[22]);
234 $searchform->set_var('lang_topic', $LANG09[3]);
235 $searchform->set_var('lang_all', $LANG09[4]);
236 $searchform->set_var('topic_option_list',
237 COM_topicList ('tid,topic', $this->_topic));
238 $searchform->set_var('lang_type', $LANG09[5]);
239 $searchform->set_var('lang_results', $LANG09[59]);
240 $searchform->set_var('lang_per_page', $LANG09[60]);
242 $searchform->set_var('lang_exact_phrase', $LANG09[43]);
243 $searchform->set_var('lang_all_words', $LANG09[44]);
244 $searchform->set_var('lang_any_word', $LANG09[45]);
245 $searchform->set_var('lang_titles', $LANG09[69]);
247 $escquery = htmlspecialchars($this->_query);
248 $escquery = str_replace(array('{', '}'), array('{', '}'),
250 $searchform->set_var ('query', $escquery);
251 $searchform->set_var ('datestart', $this->_dateStart);
252 $searchform->set_var ('dateend', $this->_dateEnd);
253 if ($this->_titlesOnly) {
254 $searchform->set_var('title_checked', ' checked="checked"');
256 $searchform->set_var('title_checked', '');
259 $phrase_selected = '';
262 if ($this->_keyType == 'phrase') {
263 $phrase_selected = 'selected="selected"';
264 } else if ($this->_keyType == 'all') {
265 $all_selected = 'selected="selected"';
266 } else if ($this->_keyType == 'any') {
267 $any_selected = 'selected="selected"';
269 $searchform->set_var ('key_phrase_selected', $phrase_selected);
270 $searchform->set_var ('key_all_selected', $all_selected);
271 $searchform->set_var ('key_any_selected', $any_selected);
274 $plugintypes = array('all' => $LANG09[4], 'stories' => $LANG09[6], 'comments' => $LANG09[7]);
275 $plugintypes = array_merge($plugintypes, PLG_getSearchTypes());
276 // Generally I don't like to hardcode HTML but this seems easiest
277 foreach ($plugintypes as $key => $val) {
278 $options .= "<option value=\"$key\"";
279 if ($this->_type == $key)
280 $options .= ' selected="selected"';
281 $options .= ">$val</option>".LB;
283 $searchform->set_var('plugin_types', $options);
285 if ($_CONF['contributedbyline'] == 1) {
286 $searchform->set_var('lang_authors', $LANG09[8]);
287 $searchusers = array();
288 $result = DB_query("SELECT DISTINCT uid FROM {$_TABLES['comments']}");
289 while ($A = DB_fetchArray($result)) {
290 $searchusers[$A['uid']] = $A['uid'];
292 $result = DB_query("SELECT DISTINCT uid FROM {$_TABLES['stories']} WHERE (date <= NOW()) AND (draft_flag = 0)");
293 while ($A = DB_fetchArray($result)) {
294 $searchusers[$A['uid']] = $A['uid'];
297 $inlist = implode(',', $searchusers);
299 if (!empty ($inlist)) {
300 $sql = "SELECT uid,username,fullname FROM {$_TABLES['users']} WHERE uid IN ($inlist)";
301 if (isset ($_CONF['show_fullname']) && ($_CONF['show_fullname'] == 1)) {
302 /* Caveat: This will group all users with an emtpy fullname
303 * together, so it's not exactly sorted by their
306 $sql .= ' ORDER BY fullname,username';
308 $sql .= ' ORDER BY username';
310 $result = DB_query ($sql);
312 while ($A = DB_fetchArray($result)) {
313 $options .= '<option value="' . $A['uid'] . '"';
314 if ($A['uid'] == $this->_author) {
315 $options .= ' selected="selected"';
317 $options .= '>' . htmlspecialchars(COM_getDisplayName('', $A['username'], $A['fullname'])) . '</option>';
319 $searchform->set_var('author_option_list', $options);
320 $searchform->parse('author_form_element', 'authors', true);
322 $searchform->set_var('author_form_element', '<input type="hidden" name="author" value="0"' . XHTML . '>');
325 $searchform->set_var ('author_form_element',
326 '<input type="hidden" name="author" value="0"' . XHTML . '>');
331 $limits = explode(',', $_CONF['search_limits']);
332 foreach ($limits as $limit) {
333 $options .= "<option value=\"$limit\"";
334 if ($_CONF['num_search_results'] == $limit) {
335 $options .= ' selected="selected"';
337 $options .= ">$limit</option>" . LB;
339 $searchform->set_var('search_limits', $options);
341 $searchform->set_var('lang_search', $LANG09[10]);
342 $searchform->parse('output', 'searchform');
344 $retval .= $searchform->finish($searchform->get_var('output'));
345 $retval .= COM_endBlock();
351 * Performs search on all stories
354 * @return object plugin object
357 function _searchStories()
359 global $_TABLES, $_DB_dbms, $LANG09;
361 // Make sure the query is SQL safe
362 $query = trim(addslashes($this->_query));
364 $sql = 'SELECT s.sid AS id, s.title AS title, s.introtext AS description, ';
365 $sql .= 'UNIX_TIMESTAMP(s.date) AS date, s.uid AS uid, s.hits AS hits, ';
366 $sql .= 'CONCAT(\'/article.php?story=\',s.sid) AS url ';
367 $sql .= 'FROM '.$_TABLES['stories'].' AS s, '.$_TABLES['users'].' AS u ';
368 $sql .= 'WHERE (draft_flag = 0) AND (date <= NOW()) AND (u.uid = s.uid) ';
369 $sql .= COM_getPermSQL('AND') . COM_getTopicSQL('AND') . COM_getLangSQL('sid', 'AND') . ' ';
371 if (!empty($this->_topic)) {
372 $sql .= 'AND (s.tid = \''.$this->_topic.'\') ';
374 if (!empty($this->_author)) {
375 $sql .= 'AND (s.uid = \''.$this->_author.'\') ';
378 $search_s = new SearchCriteria('stories', $LANG09[65]);
380 $columns = array('title' => 'title', 'introtext', 'bodytext');
381 $sql .= $search_s->getDateRangeSQL('AND', 'date', $this->_dateStart, $this->_dateEnd);
382 list($sql, $ftsql) = $search_s->buildSearchSQL($this->_keyType, $query, $columns, $sql);
384 $search_s->setSQL($sql);
385 $search_s->setFTSQL($ftsql);
386 $search_s->setRank(5);
387 $search_s->setURLRewrite(true);
389 // Search Story Comments
390 $sql = 'SELECT c.cid AS id, c.title AS title, c.comment AS description, ';
391 $sql .= 'UNIX_TIMESTAMP(c.date) AS date, c.uid AS uid, ';
393 // MSSQL has a problem when concatenating numeric values
394 if ($_DB_dbms == 'mssql') {
395 $sql .= '\'/comment.php?mode=view&cid=\' + CAST(c.cid AS varchar(10)) AS url ';
397 $sql .= 'CONCAT(\'/comment.php?mode=view&cid=\',c.cid) AS url ';
400 $sql .= 'FROM '.$_TABLES['users'].' AS u, '.$_TABLES['comments'].' AS c ';
401 $sql .= 'LEFT JOIN '.$_TABLES['stories'].' AS s ON ((s.sid = c.sid) ';
402 $sql .= COM_getPermSQL('AND',0,2,'s').COM_getTopicSQL('AND',0,'s').COM_getLangSQL('sid','AND','s').') ';
403 $sql .= 'WHERE (u.uid = c.uid) AND (s.draft_flag = 0) AND (s.commentcode >= 0) AND (s.date <= NOW()) ';
405 if (!empty($this->_topic)) {
406 $sql .= 'AND (s.tid = \''.$this->_topic.'\') ';
408 if (!empty($this->_author)) {
409 $sql .= 'AND (c.uid = \''.$this->_author.'\') ';
412 $search_c = new SearchCriteria('comments', array($LANG09[65],$LANG09[66]));
414 $columns = array('title' => 'c.title', 'comment');
415 $sql .= $search_c->getDateRangeSQL('AND', 'c.date', $this->_dateStart, $this->_dateEnd);
416 list($sql, $ftsql) = $search_c->buildSearchSQL($this->_keyType, $query, $columns, $sql);
418 $search_c->setSQL($sql);
419 $search_c->setFTSQL($ftsql);
420 $search_c->setRank(2);
422 return array($search_s, $search_c);
426 * Kicks off the appropriate search(es)
428 * Initiates the search engine and returns HTML formatted
429 * results. It also provides support to plugins using a
430 * search API. Backwards compatibility has been incorporated
431 * in this function to allow legacy support to plugins using
432 * the old API calls defined versions prior to Geeklog 1.5.1
435 * @return string HTML output for search results
440 global $_CONF, $LANG01, $LANG09, $LANG31;
442 // Verify current user can perform requested search
443 if (!$this->_isSearchAllowed())
445 return $this->_getAccessDeniedMessage();
448 // Make sure there is a query string
449 // Full text searches have a minimum word length of 3 by default
450 if ((empty($this->_query) && empty($this->_author) && empty($this->_topic)) || ($_CONF['search_use_fulltext'] && strlen($this->_query) < 3))
452 $retval = '<p>' . $LANG09[41] . '</p>' . LB;
453 $retval .= $this->showForm();
458 // Build the URL strings
459 $this->_searchURL = $_CONF['site_url'] . '/search.php?query=' . urlencode($this->_query) .
460 ((!empty($this->_keyType)) ? '&keyType=' . $this->_keyType : '' ) .
461 ((!empty($this->_dateStart)) ? '&datestart=' . $this->_dateStart : '' ) .
462 ((!empty($this->_dateEnd)) ? '&dateend=' . $this->_dateEnd : '' ) .
463 ((!empty($this->_topic)) ? '&topic=' . $this->_topic : '' ) .
464 ((!empty($this->_author)) ? '&author=' . $this->_author : '' ) .
465 ($this->_titlesOnly ? '&title=true' : '');
467 $url = "{$this->_searchURL}&type={$this->_type}&mode=";
468 $obj = new ListFactory($url.'search', $_CONF['search_limits'], $_CONF['num_search_results']);
469 $obj->setField('ID', 'id', false);
470 $obj->setField('URL', 'url', false);
472 $show_num = $_CONF['search_show_num'];
473 $show_type = $_CONF['search_show_type'];
474 $show_user = $_CONF['contributedbyline'];
475 $show_hits = !$_CONF['hideviewscount'];
476 $style = isset($_CONF['search_style']) ? $_CONF['search_style'] : 'google';
478 if ($style == 'table')
480 $obj->setStyle('table');
481 // Title Name Display Sort Format
482 $obj->setField($LANG09[62], LF_ROW_NUMBER, $show_num, false, '<b>%d.</b>');
483 $obj->setField($LANG09[5], LF_SOURCE_TITLE,$show_type, true, '<b>%s</b>');
484 $obj->setField($LANG09[16], 'title', true, true);
485 $obj->setField($LANG09[63], 'description', true, false);
486 $obj->setField($LANG09[17], 'date', true, true);
487 $obj->setField($LANG09[18], 'uid', $show_user, true);
488 $obj->setField($LANG09[50], 'hits', $show_hits, true);
489 $this->_wordlength = 7;
491 else if ($style == 'google')
493 $sort_uid = $this->_author == '' ? true : false;
494 $sort_date = empty($this->_dateStart) || empty($this->_dateEnd) || $this->_dateStart != $this->_dateEnd ? true : false;
495 $sort_type = $this->_type == 'all' ? true : false;
496 $obj->setStyle('inline');
497 $obj->setField('', LF_ROW_NUMBER, $show_num, false, '<b>%d.</b>');
498 $obj->setField($LANG09[16], 'title', true, true, '%s<br' . XHTML . '>');
499 $obj->setField('', 'description', true, false, '%s<br' . XHTML . '>');
500 $obj->setField('', '_html', true, false, '<span style="color:green;">');
501 $obj->setField($LANG09[18], 'uid', $show_user, $sort_uid, $LANG01[104].' %s ');
502 $obj->setField($LANG09[17], 'date', true, $sort_date, $LANG01[36].' %s');
503 $obj->setField($LANG09[5], LF_SOURCE_TITLE,$show_type, $sort_type, ' - %s');
504 $obj->setField($LANG09[50], 'hits', $show_hits, true, ' - %s '.$LANG09[50]);
505 $obj->setField('', '_html', true, false, '</span>');
506 $this->_wordlength = 50;
508 $obj->setDefaultSort('hits');
509 // set this only now, for compatibility with PHP 4
510 $obj->setRowFunction(array($this, 'searchFormatCallback'));
512 // Start search timer
513 $searchtimer = new timerobject();
514 $searchtimer->setPrecision(4);
515 $searchtimer->startTimer();
517 // Have plugins do their searches
518 $page = isset($_GET['page']) ? COM_applyFilter($_GET['page'], true) : 1;
519 $result_plugins = PLG_doSearch($this->_query, $this->_dateStart,
520 $this->_dateEnd, $this->_topic, $this->_type,
521 $this->_author, $this->_keyType, $page, 5);
524 $result_plugins = array_merge($result_plugins, $this->_searchStories());
526 // Loop through all plugins separating the new API from the old
531 foreach ($result_plugins as $result)
533 if (is_a($result, 'SearchCriteria'))
535 $debug_info = $result->getName().' using APIv2';
537 if ($this->_type != 'all' && $this->_type != $result->getName())
539 if ($this->_verbose) {
541 COM_errorLog($debug_info.'. Skipped as type is not '.$this->_type);
546 $api_results = $result->getResults();
547 if (!empty($api_results)) {
548 $obj->addResultArray($api_results);
551 $api_callback_func = $result->getCallback();
552 if (!empty($api_callback_func))
554 $debug_info .= ' with Callback Function.';
555 $obj->setCallback($result->getLabel(), $result->getName(), $api_callback_func, $result->getRank(), $result->getTotal());
559 if ($_CONF['search_use_fulltext'] == true && $result->getFTSQL() != '') {
560 $sql = $result->getFTSQL();
562 $sql = $result->getSQL();
565 $sql = $this->_convertsql($sql);
566 $debug_info .= ' with SQL = '.print_r($sql,1);
567 $obj->setQuery($result->getLabel(), $result->getName(), $sql, $result->getRank());
570 $this->_url_rewrite[ $result->getName() ] = $result->UrlRewriteEnable();
571 $this->_append_query[ $result->getName() ] = $result->AppendQueryEnable();
573 if ($this->_verbose) {
575 COM_errorLog($debug_info);
578 else if (is_a($result, 'Plugin') && $result->num_searchresults != 0)
580 // Some backwards compatibility
581 if ($this->_verbose) {
583 $debug_info = $result->plugin_name.' using APIv1 with backwards compatibility.';
584 $debug_info .= ' Count: ' . $result->num_searchresults;
585 $debug_info .= ' Headings: ' . implode(',', $result->searchheading);
586 COM_errorLog($debug_info);
589 // Find the column heading names that closely match what we are looking for
590 // There may be issues here on different languages, but this _should_ capture most of the data
591 $col_title = $this->_findColumn($result->searchheading, array($LANG09[16],$LANG31[4],'Question', 'Site Page'));//Title,Subject
592 $col_desc = $this->_findColumn($result->searchheading, array($LANG09[63],'Answer'));
593 $col_date = $this->_findColumn($result->searchheading, array($LANG09[17]));//'Date','Date Added','Last Updated','Date & Time'
594 $col_user = $this->_findColumn($result->searchheading, array($LANG09[18],'Submited by'));
595 $col_hits = $this->_findColumn($result->searchheading, array($LANG09[50],$LANG09[23],'Downloads','Clicks'));//'Hits','Views'
597 $label = str_replace($LANG09[59], '', $result->searchlabel);
598 $num_results += $result->num_itemssearched;
600 // Extract the results
601 for ($i = 0; $i < 5; $i++)
603 // If the plugin does not repect the $perpage perameter force it here.
604 $j = ($i + ($page * 5)) - 5;
605 if ($j >= count($result->searchresults))
608 $old_row = $result->searchresults[$j];
611 // Convert the date back to a timestamp
612 $date = $old_row[ $col_date ];
613 $date = substr($date, 0, strpos($date, '@'));
614 $date = ($date == '' ? $old_row[$col_date] : strtotime($date));
617 $api_results = array(
618 LF_SOURCE_NAME => $result->plugin_name,
619 LF_SOURCE_TITLE => $label,
620 'title' => $col_title == -1 ? '<i>' . $LANG09[70] . '</i>' : $old_row[$col_title],
621 'description' => $col_desc == -1 ? '<i>' . $LANG09[70] . '</i>' : $old_row[$col_desc],
622 'date' => $col_date == -1 ? 'LF_NULL' : $date,
623 'uid' => $col_user == -1 ? 'LF_NULL' : $old_row[$col_user],
624 'hits' => $col_hits == -1 ? 'LF_NULL' : str_replace(',', '', $old_row[$col_hits])
626 preg_match('/href="([^"]+)"/i', $api_results['title'], $links);
627 $api_results['url'] = empty($links) ? '#' : $links[1];
629 $obj->addResult($api_results);
634 // Find out how many plugins are on the old/new system
635 if ($this->_verbose) {
636 COM_errorLog('Search Plugins using APIv1: '.$old_api.' APIv2: '.$new_api);
639 // Execute the queries
640 $results = $obj->ExecuteQueries();
642 // Searches are done, stop timer
643 $searchtime = $searchtimer->stopTimer();
645 $escquery = htmlspecialchars($this->_query);
646 $escquery = str_replace(array('{', '}'), array('{', '}'),
648 if ($this->_keyType == 'any')
650 $searchQuery = str_replace(' ', "</b>' " . $LANG09[57] . " '<b>", $escquery);
651 $searchQuery = "'<b>$searchQuery</b>'";
653 else if ($this->_keyType == 'all')
655 $searchQuery = str_replace(' ', "</b>' " . $LANG09[56] . " '<b>", $escquery);
656 $searchQuery = "'<b>$searchQuery</b>'";
660 $searchQuery = $LANG09[55] . " '<b>$escquery</b>'";
663 // Clean the query string so that sprintf works as expected
664 $searchQuery = str_replace('%', '%%', $searchQuery);
666 $retval = "{$LANG09[25]} $searchQuery. ";
667 if (count($results) == 0)
669 $retval .= sprintf($LANG09[24], 0);
670 $retval = '<p>' . $retval . '</p>' . LB;
671 $retval .= '<p>' . $LANG09[13] . '</p>' . LB;
672 $retval .= $this->showForm();
676 $retval .= $LANG09[64] . " ($searchtime {$LANG09[27]}). ";
677 $retval .= str_replace('%', '%%', COM_createLink($LANG09[61], $url.'refine'));
678 $retval = '<p>' . $retval . '</p>' . LB;
679 $retval = $obj->getFormattedOutput($results, $LANG09[11], $retval, '',
680 $_CONF['search_show_sort'], $_CONF['search_show_limit']);
687 * Callback function for the ListFactory class
689 * This function gets called by the ListFactory class and formats
690 * each row accordingly for example pulling usernames from the
691 * users table and displaying a link to their profile.
694 * @param array $row An array of plain data to format
695 * @return array A reformatted version of the input array
698 function searchFormatCallback( $preSort, $row )
700 global $_CONF, $LANG09;
704 if (is_array($row[LF_SOURCE_TITLE])) {
705 $row[LF_SOURCE_TITLE] = implode($_CONF['search_separator'], $row[LF_SOURCE_TITLE]);
708 if (is_numeric($row['uid']))
710 if (empty($this->_names[ $row['uid'] ]))
712 $this->_names[ $row['uid'] ] = htmlspecialchars(COM_getDisplayName( $row['uid'] ));
713 if ($row['uid'] != 1)
715 $this->_names[$row['uid']] = COM_createLink($this->_names[ $row['uid'] ],
716 $_CONF['site_url'] . '/users.php?mode=profile&uid=' . $row['uid']);
719 $row['uid'] = $this->_names[ $row['uid'] ];
724 $row[LF_SOURCE_TITLE] = COM_createLink($row[LF_SOURCE_TITLE],
725 $this->_searchURL.'&type='.$row[LF_SOURCE_NAME].'&mode=search');
727 if ($row['url'] != '#')
729 $row['url'] = ($row['url'][0] == '/' ? $_CONF['site_url'] : '') . $row['url'];
730 if (isset($this->_url_rewrite[$row[LF_SOURCE_NAME]]) &&
731 $this->_url_rewrite[$row[LF_SOURCE_NAME]]) {
732 $row['url'] = COM_buildUrl($row['url']);
734 if (isset($this->_append_query[$row[LF_SOURCE_NAME]]) &&
735 $this->_append_query[$row[LF_SOURCE_NAME]]) {
736 $row['url'] .= (strpos($row['url'],'?') ? '&' : '?') . 'query=' . urlencode($this->_query);
740 $row['title'] = $this->_shortenText($this->_query, $row['title'], 8);
741 $row['title'] = stripslashes(str_replace('$', '$', $row['title']));
742 $row['title'] = COM_createLink($row['title'], $row['url']);
744 if ($row['description'] == 'LF_NULL') {
745 $row['description'] = '<i>' . $LANG09[70] . '</i>';
746 } elseif ($row['description'] != '<i>' . $LANG09[70] . '</i>') {
747 $row['description'] = stripslashes($this->_shortenText($this->_query, PLG_replaceTags($row['description']), $this->_wordlength));
750 if ($row['date'] != 'LF_NULL') {
751 $dt = COM_getUserDateTimeFormat(intval($row['date']));
752 $row['date'] = $dt[0];
755 if ($row['hits'] != 'LF_NULL') {
756 $row['hits'] = COM_NumberFormat($row['hits']) . ' '; // simple solution to a silly problem!
764 * Shortens a long text string to only a few words
766 * Returns a shorter version of the in putted text centred
767 * around the keyword. The keyword is highlighted in bold.
768 * Adds '...' to the beginning or the end of the shortened
769 * version depending where the text was cut. Works on a
770 * word basis, so long words wont get cut.
773 * @param string $keyword The word to centre around
774 * @param string $text The complete text string
775 * @param int $num_words The number of words to display, best to use an odd number
776 * @return string A short version of the text
779 function _shortenText($keyword, $text, $num_words = 7)
781 $text = COM_getTextContent($text);
782 $words = explode(' ', $text);
783 $word_count = count($words);
784 if ($word_count <= $num_words) {
785 return COM_highlightQuery($text, $keyword, 'b');
789 $pos = $this->_stripos($text, $keyword);
792 $pos_space = strpos($text, ' ', $pos);
793 if (empty($pos_space))
795 // Keyword at the end of text
796 $key = $word_count - 1;
797 $start = 0 - $num_words;
803 $str = substr($text, $pos, $pos_space - $pos);
804 $m = (int) (($num_words - 1) / 2);
805 $key = $this->_arraySearch($keyword, $words);
806 if ($key === false) {
807 // Keyword(s) not found - show start of text
810 $end = $num_words - 1;
811 } elseif ($key <= $m) {
812 // Keyword at the start of text
814 $end = $num_words - 1;
815 $end = ($key + $m <= $word_count - 1)
816 ? $key : $word_count - $m - 1;
817 $abs_length = abs($start) + abs($end) + 1;
818 if ($abs_length < $num_words) {
819 $end += ($num_words - $abs_length);
822 // Keyword in the middle of text
824 $end = ($key + $m <= $word_count - 1)
825 ? $m : $word_count - $key - 1;
826 $abs_length = abs($start) + abs($end) + 1;
827 if ($abs_length < $num_words) {
828 $start -= ($num_words - $abs_length);
838 $end = $num_words - 1;
841 for ($i = $start; $i <= $end; $i++) {
842 $rt .= $words[$key + $i] . ' ';
844 if ($key + $i != $word_count) {
845 $rt .= ' <b>...</b>';
848 return COM_highlightQuery($rt, $keyword, 'b');
852 * Search array of words for keyword(s)
854 * @param string $needle keyword(s), separated by spaces
855 * @param array $haystack array of words to search through
856 * @return mixed index in $haystack or false when not found
860 function _arraySearch($needle, $haystack)
862 $keywords = explode(' ', $needle);
863 $num_keywords = count($keywords);
865 foreach ($haystack as $key => $value) {
866 if ($this->_stripos($value, $keywords[0]) !== false) {
867 if ($num_keywords == 1) {
871 for ($i = 1; $i < $num_keywords; $i++) {
872 if ($this->_stripos($haystack[$key + $i], $keywords[$i]) === false) {
873 $matched_all = false;
888 * Finds the similarities between heading names
890 * Returns the index of a heading that matches a
891 * number of similar heading names. Used for backwards
892 * compatibility in the doSearch() function.
895 * @param array $headings All the headings
896 * @param array $find An array of alternative headings to find
897 * @return int The index of the alternative heading
900 function _findColumn( $headings, $find )
902 // We can't use normal for loops here as some of the
903 // heading indexes start from 1, so foreach works better
904 foreach ($find as $fh)
907 foreach ($headings as $h)
909 if (preg_match("/$fh/i", $h) > 0) {
919 * Converts the MySQL CONCAT function to the MSSQL equivalent
922 * @param string $sql The SQL to convert
923 * @return string MSSQL friendly SQL
926 function _convertsql( $sql )
929 if ($_DB_dbms == 'mssql')
931 if (is_string( $sql ))
933 $sql = preg_replace("/CONCAT\(([^\)]+)\)/ie", "preg_replace('/,?(\'[^\']+\'|[^,]+),/i', '\\\\1 + ', '\\1')", $sql);
935 else if (is_array( $sql ))
937 $sql['mssql'] = preg_replace("/CONCAT\(([^\)]+)\)/ie", "preg_replace('/,?(\'[^\']+\'|[^,]+),/i', '\\\\1 + ', '\\1')", $sql['mssql']);
944 * Helper function: Simulate stripos on PHP 4
946 * @param string $haystack string to search in
947 * @param string $needle string to search for
948 * @return mixed first pos of $needle in $haystack, or false
951 function _stripos($haystack, $needle)
953 if (function_exists('stripos')) {
954 return stripos($haystack, $needle);
955 } elseif (empty($needle)) {
958 return strpos(strtolower($haystack), strtolower($needle));