<?php
/**
* This file implements misc functions that handle search for posts, comments, categories, etc.
*
* This file is part of the evoCore framework - {@link http://evocore.net/}
* See also {@link https://github.com/b2evolution/b2evolution}.
*
* @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
*
* @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
*
* @package evocore
*/
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
/**
* Return a search score for the given text and search keywords
*
* @param string the text to score
* @param string the search keywords to score by
* @return integer the result score
*/
function score_text( $text, $search_term, $words = array(), $quoted_terms = array(), $score_weight = 1 )
{
global $Debuglog;
$score = 0.0;
$scores_map = array();
if( !empty( $search_term ) && utf8_stripos( $text, $search_term ) !== false )
{
// the max score what it may received in this methods
$score = ( ( count( $words ) * 4 ) + ( count( $quoted_terms ) * 6 ) ) * $score_weight;
$scores_map['whole_term'] = $score;
return array( 'score' => $score, 'map' => $scores_map );
}
$all_found = true;
foreach( $quoted_terms as $quoted_term )
{ // find quoted keywords
$count = empty( $quoted_term ) ? 0 : substr_count( $text, $quoted_term );
if( $count == 0 )
{
$all_found = false;
}
else
{
$score += ( ( $count > 1 ) ? 8 : 6 );
$scores_map['quoted_term'][$quoted_term] = ( $count > 1 ) ? 8 : 6;
}
}
if( $all_found && count( $quoted_terms ) > 0 )
{
$score += 4;
$scores_map['quoted_term_all'] = 4;
}
$all_case_sensitive_match = true;
$all_whole_word_match = true;
foreach( $words as $word )
{
$count = empty( $word ) ? 0 : substr_count( $text, $word );
if( $count === 0 )
{
$all_case_sensitive_match = false;
}
else
{
$scores_map['word_case_sensitive_match'][$word] = $score_weight;
$score += $scores_map['word_case_sensitive_match'][$word];
}
if( $word_match_count = preg_match_all( '/\b'.preg_quote( $word, '/' ).'\b/i', $text, $matches ) )
{ // Every word match gives one more score
$scores_map['whole_word_match'][$word] = $score_weight;
$score += $scores_map['whole_word_match'][$word];
}
else
{
$all_whole_word_match = false;
}
if( $any_match_count = preg_match_all( '/'.preg_quote( $word, '/' ).'/i', $text, $matches ) )
{ // Every word match gives one more score
$scores_map['word_case_insensitive_match'][$word] = $score_weight;
$score += $scores_map['word_case_insensitive_match'][$word];
if( $any_match_count > 1 )
{ // there are multiple occurrences
$scores_map['word_multiple_occurences'][$word] = score_multiple_occurences( $any_match_count, $score_weight );
$score += $scores_map['word_multiple_occurences'][$word];
}
}
}
if( $all_case_sensitive_match )
{
$score += 4;
$scores_map['all_case_sensitive'] = 4;
}
if( $all_whole_word_match )
{
$score += 4;
$scores_map['all_whole_words'] = 4;
}
return array( 'score' => $score, 'score_weight' => $score_weight, 'map' => $scores_map );
}
/**
* Return a search score for the given text and search keywords
*
* @param string Text to score
* @param string The search keywords to score by
* @param integer Score multiplier
* @return integer Result score
*/
function score_tags( $tag_name, $search_term, $score_weight = 4 )
{
$score = 0.0;
$scores_map = array();
if( $tag_name == utf8_trim( $search_term ) )
{ // We use only EXACT match for post tags:
$score = $score_weight;
$scores_map['tags'] = $score;
$scores_map['word_case_insensitive_match'][$tag_name] = $score;
}
return array( 'score' => $score, 'score_weight' => $score_weight, 'map' => $scores_map );
}
/**
* Return a search score for the given date. Recent dates get higher scores.
*
* @param string the date to score
* @param array Score weights depending on date:
* - 'future' - Wrong date because it is more than current local time
* - 'more_month' - Date is more month
* - 'last_month' - Last month
* - 'two_weeks' - Last two weeks
* - 'last_week' - Last week, We subtract from this score a number of day and result cannot be less than 'two_weeks'
* @return integer the result score
*/
function score_date( $date, $score_weights = array() )
{
global $localtimenow;
$score_weights = array_merge( array(
'future' => 0,
'more_month' => 0,
'last_month' => 1,
'two_weeks' => 2,
'last_week' => 8,
), $score_weights );
$day_diff = floor( ($localtimenow - strtotime($date)) / (60 * 60 * 24) );
if( $day_diff < 0 )
{ // If date is more current locale time:
return $score_weights['future'];
}
if( $day_diff <= 7 )
{ // Last week:
$score = $score_weights['last_week'] - $day_diff;
return ( $score > $score_weights['two_weeks'] ) ? $score : $score_weights['two_weeks'] + 1;
}
if( $day_diff < 15 )
{ // Last two weeks:
return $score_weights['two_weeks'];
}
if( $day_diff < 31 )
{ // Last month:
return $score_weights['last_month'];
}
else
{ // More month:
return $score_weights['more_month'];
}
}
/**
* Count score of multiple occurrences
* The score is sum( $score_weight / x ) where x goes from 2 to match count
*
* @param integer match count
* @param integer score weight
* @return float multiple occurrences score based on the received parameters
*/
function score_multiple_occurences( $match_count, $score_weight )
{
$result = 0.0;
for( $i = 2; $i <= $match_count; $i++ )
{
$result = $result + floatval($score_weight) / floatval($match_count);
}
return $result;
}
/**
* Get percentage value from the detailed result information
*
* @return number the calculated percentage value about result - search term fit
*/
function get_percentage_from_result_map( $type, $scores_map, $quoted_parts, $keywords )
{
switch( $type )
{
case 'item':
$searched_parts = array( 'title', 'content', 'tags', 'excerpt', 'titletag', 'metakeywords' );
break;
case 'comment':
case 'meta':
$searched_parts = array( 'item_title', 'content' );
break;
case 'category':
$searched_parts = array( 'name', 'description' );
break;
case 'tag':
$searched_parts = array( 'name' );
break;
case 'file':
$searched_parts = array( 'filename', 'title', 'alt', 'description' );
break;
default:
debug_die( 'Invalid search type received!' );
}
// Check whole term match
foreach( $searched_parts as $searched_part )
{
if( isset( $scores_map[$searched_part]['map']['whole_term'] ) )
{ // The whole search term was found
return 100;
}
}
// Whole search term was not found, count percentage based on the matched parts
$matched_quoted_parts = 0;
foreach( $quoted_parts as $quoted_part )
{
foreach( $searched_parts as $searched_part )
{
if( isset( $scores_map[$searched_part]['map']['quoted_term'][$quoted_part] ) )
{
$matched_quoted_parts++;
break; // go to the next quoted part
}
}
}
$matched_keywords = 0;
foreach( $keywords as $keyword )
{
foreach( $searched_parts as $searched_part )
{
if( isset( $scores_map[$searched_part]['map']['word_case_insensitive_match'][$keyword] ) )
{
$matched_keywords++;
break; // go to the next quoted part
}
}
}
// return round( ( $matched_keywords + ( 2 * $matched_quoted_parts ) ) * 100 / ( count( $keywords ) + ( 2 * count( $quoted_parts ) ) ) );
return round( ( $matched_keywords + $matched_quoted_parts ) * 100 / ( count( $keywords ) + count( $quoted_parts ) ) );
}
/**
* Get date by content age value
*
* @param string Content age:
* @return string Date in format YYYYmmddHHiiss
*/
function get_search_date_by_content_age( $content_age )
{
global $localtimenow;
switch( $content_age )
{
case 'hour':
// Last hour:
$date = date( 'YmdHis', $localtimenow - 3600 ); // 60 * 60
break;
case 'day':
// Last day:
$date = date( 'YmdHis', $localtimenow - 86400 ); // 3600 * 24
break;
case 'week':
// Last week:
$date = date( 'YmdHis', $localtimenow - 604800 ); // 86400 * 7
break;
case '30d':
// Last 30 days:
$date = date( 'YmdHis', $localtimenow - 2592000 ); // 86400 * 30
break;
case '90d':
// Last 90 days:
$date = date( 'YmdHis', $localtimenow - 7776000 ); // 86400 * 90
break;
case 'year':
// Last year:
$date = date( 'YmdHis', $localtimenow - 31190400 ); // 86400 * 361
break;
case 'anytime':
default:
// Unknown age value:
$date = NULL;
break;
}
return $date;
}
/**
* Search and score items
*
* @param string original search term
* @param array all separated words from the search term
* @param array all quoted parts from the search term
* @param string Post IDs to exclude from result, Separated with ','(comma)
* @param string If logged in, comma separated Author IDs to filter, otherwise author user login
* @param string Content age to consider
* @return array Results
*/
function search_and_score_items( $search_term, $keywords, $quoted_parts, $exclude_posts = '', $authors = '', $content_age = '' )
{
global $DB, $Collection, $Blog;
$exclude_posts = trim( $exclude_posts, ' ,' );
if( empty( $exclude_posts ) )
{ // No posts to exclude:
$exclude_posts = '';
}
else
{ // Check if values are correct:
$exclude_posts = explode( ',', $exclude_posts );
foreach( $exclude_posts as $e => $exclude_post )
{
$exclude_post = intval( trim( $exclude_post ) );
if( empty( $exclude_post ) )
{
unset( $exclude_posts[ $e ] );
}
}
$exclude_posts = empty( $exclude_posts ) ? '' : '-'.implode( ',', $exclude_posts );
}
// Prepare filters:
$search_ItemList = new ItemList2( $Blog, $Blog->get_timestamp_min(), $Blog->get_timestamp_max(), '', 'ItemCache', 'search_item' );
$search_filters = array(
'keywords' => $search_term,
'keyword_scope' => 'title,content,tags,excerpt,titletag,metakeywords', // TODO: add more fields
'ymdhms_min' => get_search_date_by_content_age( $content_age ),
'phrase' => 'OR',
'itemtype_usage'=> '-sidebar', // Exclude from search: 'sidebar' item types
'orderby' => 'datemodified',
'order' => 'DESC',
'posts' => 1000,
'post_ID_list' => $exclude_posts,
'itemtype_usage'=> '-content-block,special',
);
if( ! empty( $authors ) && is_logged_in() )
{
$search_filters['authors'] = $authors;
}
$search_ItemList->set_filters( $search_filters );
// Generate query from filters above and count results:
$search_ItemList->query_init();
if( ! is_logged_in() && ! empty( $authors ) )
{ // This is necessary because the 'authors_login' filter above will not work for non-existent logins and
$search_ItemList->ItemQuery->WHERE_and( 'user_login = '.$DB->quote( $authors ) );
}
// Make a custom search query:
$search_query = 'SELECT DISTINCT post_ID, post_datestart, post_datemodified, post_title, post_content,'
.' user_login as creator_login, tag_name, post_excerpt, post_titletag, item_metakeywords.iset_value AS item_setting_metakeywords'
.$search_ItemList->ItemQuery->get_from()
.' LEFT JOIN T_users ON post_creator_user_ID = user_ID'
.' LEFT JOIN T_items__item_settings AS item_metakeywords ON item_metakeywords.iset_item_ID = post_ID AND item_metakeywords.iset_name = "metakeywords"'
.$search_ItemList->ItemQuery->get_where()
.$search_ItemList->ItemQuery->get_group_by()
.$search_ItemList->ItemQuery->get_order_by()
.$search_ItemList->ItemQuery->get_limit();
// Run query:
$query_result = $DB->get_results( $search_query, OBJECT, 'Search items query' );
// Compute scores:
$search_result = array();
foreach( $query_result as $row )
{
$scores_map = array();
$scores_map['title'] = score_text( $row->post_title, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_post_title' ) );
$scores_map['content'] = score_text( $row->post_content, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_post_content' ) );
$scores_map['tags'] = score_tags( $row->tag_name, $search_term, /* multiplier: */ $Blog->get_setting( 'search_score_post_tags' ) );
$scores_map['excerpt'] = score_text( $row->post_excerpt, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_post_excerpt' ) );
$scores_map['titletag'] = score_text( $row->post_titletag, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_post_titletag' ) );
$scores_map['metakeywords'] = score_text( $row->item_setting_metakeywords, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_post_metakeywords' ) );
if( !empty( $search_term ) && !empty( $row->creator_login ) && utf8_stripos( $row->creator_login, $search_term ) !== false )
{
$scores_map['creator_login'] = /* multiplier: */ $Blog->get_setting( 'search_score_post_author' );
}
$scores_map['last_mod_date'] = score_date( $row->post_datemodified, /* multipliers: */ array(
'future' => $Blog->get_setting( 'search_score_post_date_future' ),
'more_month' => $Blog->get_setting( 'search_score_post_date_moremonth' ),
'last_month' => $Blog->get_setting( 'search_score_post_date_lastmonth' ),
'two_weeks' => $Blog->get_setting( 'search_score_post_date_twoweeks' ),
'last_week' => $Blog->get_setting( 'search_score_post_date_lastweek' ),
) );
$final_score = $scores_map['title']['score']
+ $scores_map['content']['score']
+ $scores_map['tags']['score']
+ $scores_map['excerpt']['score']
+ $scores_map['titletag']['score']
+ $scores_map['metakeywords']['score']
+ ( isset( $scores_map['creator_login'] ) ? $scores_map['creator_login'] : 0 )
+ $scores_map['last_mod_date'];
$search_result[] = array(
'type' => 'item',
'score' => $final_score,
'date' => $row->post_datestart,
'ID' => $row->post_ID,
'scores_map' => $scores_map,
);
}
return $search_result;
}
/**
* Search and score comments
*
* @param string original search term
* @param array all separated words from the search term
* @param array all quoted parts from the search term
* @param string If logged in, comma separated Author IDs to filter, otherwise author user login
* @param string Content age to consider
* @param string Type of comments: 'comment' - normal comments with types(comment,trackback,pingback,webmention), 'meta' - only meta/internal comments
* @return array Results
*/
function search_and_score_comments( $search_term, $keywords, $quoted_parts, $authors = '', $content_age = '', $type = 'comment' )
{
global $DB, $Collection, $Blog;
// Search between comments
$search_CommentList = new CommentList2( $Blog, '', 'CommentCache', 'search_comment' );
$search_filters = array(
'keywords' => $search_term,
'ymdhms_min' => get_search_date_by_content_age( $content_age ),
'phrase' => 'OR',
'order_by' => 'date',
'order' => 'DESC',
'comments' => 1000,
'author_IDs' => is_logged_in() ? $authors : NULL
);
if( ! is_logged_in() && ! empty( $authors ) )
{
$search_filters['authors_login'] = $authors;
}
if( $type == 'meta' )
{ // Search only for meta/internal comments:
$search_filters['types'] = array( 'meta' );
}
$search_CommentList->set_filters( $search_filters );
$search_CommentList->query_init();
$search_query = 'SELECT comment_ID, post_title, comment_content, comment_date,
IFNULL(comment_author, user_login) as author'
.$search_CommentList->CommentQuery->get_from()
.' LEFT JOIN T_items__item ON comment_item_ID = post_ID'
.' LEFT JOIN T_users ON comment_author_user_ID = user_ID'
.$search_CommentList->CommentQuery->get_where()
.$search_CommentList->CommentQuery->get_group_by()
.$search_CommentList->CommentQuery->get_order_by()
.$search_CommentList->CommentQuery->get_limit();
$query_result = $DB->get_results( $search_query, OBJECT, 'Search comments query' );
$search_result = array();
foreach( $query_result as $row )
{
$scores_map = array();
$scores_map['item_title'] = score_text( $row->post_title, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_cmnt_post_title' ) );
$scores_map['content'] = score_text( $row->comment_content, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_cmnt_content' ) );
if( !empty( $row->author ) && !empty( $search_term ) && utf8_stripos( $row->author, $search_term ) !== false )
{
$scores_map['author_name'] = /* multiplier: */ $Blog->get_setting( 'search_score_cmnt_author' );
}
$scores_map['creation_date'] = score_date( $row->comment_date, /* multipliers: */ array(
'future' => $Blog->get_setting( 'search_score_cmnt_date_future' ),
'more_month' => $Blog->get_setting( 'search_score_cmnt_date_moremonth' ),
'last_month' => $Blog->get_setting( 'search_score_cmnt_date_lastmonth' ),
'two_weeks' => $Blog->get_setting( 'search_score_cmnt_date_twoweeks' ),
'last_week' => $Blog->get_setting( 'search_score_cmnt_date_lastweek' ),
) );
$final_score = $scores_map['item_title']['score']
+ $scores_map['content']['score']
+ ( isset( $scores_map['author_name'] ) ? $scores_map['author_name'] : 0 )
+ $scores_map['creation_date'];
$search_result[] = array(
'type' => $type, // 'comment' or 'meta'
'score' => $final_score,
'date' => $row->comment_date,
'ID' => $row->comment_ID,
'scores_map' => $scores_map
);
}
return $search_result;
}
/**
* Search and score chapters
*
* @param string original search term
* @param array all separated words from the search term
* @param array all quoted parts from the search term
*/
function search_and_score_chapters( $search_term, $keywords, $quoted_parts )
{
global $DB, $Collection, $Blog;
// Init result array:
$search_result = array();
// Set query conditions:
$or = '';
$cat_where_condition = '';
foreach( $keywords as $keyword )
{
$keyword = $DB->escape( $keyword );
$cat_where_condition .= $or.' ( cat_name LIKE \'%'.$keyword.'%\' OR cat_description LIKE \'%'.$keyword.'%\' )';
$or = ' OR';
}
// Search between chapters:
$ChapterCache = & get_ChapterCache();
$ChapterCache->clear();
$cat_where_condition = '( cat_blog_ID = '.$DB->quote( $Blog->ID ).' ) AND ( '.$cat_where_condition.' )';
$ChapterCache->load_where( $cat_where_condition );
while( ( $iterator_Chapter = & $ChapterCache->get_next() ) != NULL )
{
$scores_map = array();
$scores_map['name'] = score_text( $iterator_Chapter->get( 'name' ), $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_cat_name' ) );
$scores_map['description'] = score_text( $iterator_Chapter->get( 'description' ), $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_cat_desc' ) );
$final_score = $scores_map['name']['score']
+ $scores_map['description']['score'];
$search_result[] = array(
'type' => 'category',
'score' => $final_score,
'ID' => $iterator_Chapter->ID,
'scores_map' => $scores_map
);
}
return $search_result;
}
/**
* Search and score tags
*
* @param string original search term
* @param array all separated words from the search term
* @param array all quoted parts from the search term
*/
function search_and_score_tags( $search_term, $keywords, $quoted_parts )
{
global $DB, $Collection, $Blog;
// Init result array:
$search_result = array();
// Set query conditions:
$or = '';
$tag_where_condition = '';
foreach( $keywords as $keyword )
{
$tag_where_condition .= $or.'tag_name LIKE \'%'.$DB->escape( $keyword ).'%\'';
$or = ' OR ';
}
// Search between tags:
$tags_SQL = new SQL( 'Get tags matching to the search keywords' );
$tags_SQL->SELECT( 'tag_ID, tag_name, COUNT( DISTINCT itag_itm_ID ) AS post_count' );
$tags_SQL->FROM( 'T_items__tag' );
$tags_SQL->FROM_add( 'INNER JOIN T_items__itemtag ON itag_tag_ID = tag_ID' );
$tags_SQL->FROM_add( 'INNER JOIN T_postcats ON itag_itm_ID = postcat_post_ID' );
$tags_SQL->FROM_add( 'INNER JOIN T_categories ON postcat_cat_ID = cat_ID' );
$tags_SQL->WHERE( 'cat_blog_ID = '.$DB->quote( $Blog->ID ) );
$tags_SQL->WHERE_and( $tag_where_condition );
$tags_SQL->GROUP_BY( 'tag_name' );
$tags = $DB->get_results( $tags_SQL );
foreach( $tags as $tag )
{
if( $tag->post_count == 0 )
{ // Count only those tags which have at least one post linked to it, Skip this:
continue;
}
$scores_map = array();
$scores_map['name'] = score_text( $tag->tag_name, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_tag_name' ) );
$final_score = $scores_map['name']['score'];
$search_result[] = array(
'type' => 'tag',
'score' => $final_score,
'ID' => $tag->tag_ID,
'name' => $tag->tag_name.','.$tag->post_count,
'scores_map' => $scores_map,
);
}
return $search_result;
}
/**
* Search and score files
*
* @param string original search term
* @param array all separated words from the search term
* @param array all quoted parts from the search term
* @param string Author IDs to filter, separated with ',' (comma)
* @param string Content age to consider
* @return array Results
*/
function search_and_score_files( $search_term, $keywords, $quoted_parts, $authors = '', $content_age = '' )
{
global $DB, $Collection, $Blog, $time_difference;
// Init result array:
$search_result = array();
// Set query conditions:
$or = '';
$file_where_condition = '';
foreach( $keywords as $keyword )
{
$keyword = $DB->escape( $keyword );
$file_where_condition .= $or.' ( file_path LIKE \'%'.$keyword.'%\' OR file_title LIKE \'%'.$keyword.'%\' OR file_alt LIKE \'%'.$keyword.'%\' OR file_desc LIKE \'%'.$keyword.'%\' )';
$or = ' OR';
}
// Search between files:
$files_SQL = new SQL( 'Get files matching to the search keywords' );
$files_SQL->SELECT( 'file_ID, file_path, file_title, file_alt, file_desc, GROUP_CONCAT( DISTINCT link_itm_ID SEPARATOR "," ) AS post_IDs, GROUP_CONCAT( DISTINCT link_cmt_ID SEPARATOR "," ) AS comment_IDs' );
$files_SQL->FROM( 'T_files' );
$files_SQL->FROM_add( 'INNER JOIN T_links ON link_file_ID = file_ID' );
$files_SQL->FROM_add( 'LEFT JOIN T_postcats AS ipc ON link_itm_ID = ipc.postcat_post_ID' );
$files_SQL->FROM_add( 'LEFT JOIN T_items__item ON post_ID = ipc.postcat_post_ID' );
$files_SQL->FROM_add( 'LEFT JOIN T_categories AS icat ON ipc.postcat_cat_ID = icat.cat_ID' );
$files_SQL->FROM_add( 'LEFT JOIN T_comments ON link_cmt_ID = comment_ID' );
$files_SQL->FROM_add( 'LEFT JOIN T_postcats AS cpc ON comment_item_ID = cpc.postcat_post_ID' );
$files_SQL->FROM_add( 'LEFT JOIN T_categories AS ccat ON cpc.postcat_cat_ID = ccat.cat_ID' );
if( ! empty( $authors ) && ! is_logged_in() )
{
$files_SQL->FROM_add( 'LEFT JOIN T_users AS iuser ON comment_author_user_ID = iuser.user_ID' );
$files_SQL->FROM_add( 'LEFT JOIN T_users AS cuser ON comment_author_user_ID = cuser.user_ID' );
}
$files_SQL->WHERE( '( icat.cat_blog_ID = '.$DB->quote( $Blog->ID ).' OR ccat.cat_blog_ID = '.$DB->quote( $Blog->ID ).' )' );
$files_SQL->WHERE_and( $file_where_condition );
if( ! empty( $authors ) )
{
if( is_logged_in() )
{
if( preg_match( '/^[0-9]+(,[0-9]+)*$/', $authors ) === false )
{
debug_die( 'Invalid comment author filter request' );
}
$files_SQL->WHERE_and( '( comment_author_user_ID IN ('.$authors.') OR post_creator_user_ID IN ('.$authors.') )' );
}
else
{
$files_SQL->WHERE_and( '( comment_author = '.$DB->quote( $authors ).' OR cuser.user_login = '.$DB->quote( $authors ).' OR iuser.user_login = '.$DB->quote( $authors ).' )' );
}
}
if( $content_age != '' )
{
$date_min = remove_seconds( strtotime( get_search_date_by_content_age( $content_age ) ) + $time_difference );
//$date_max = remove_seconds( $timestamp_max + $time_difference );
$files_SQL->WHERE_and( '( post_datestart >= '.$DB->quote( $date_min ).' OR comment_date >= '.$DB->quote( $date_min ).' )' );
//$files_SQL->WHERE_and( '( post_datestart <= '.$DB->quote( $date_min ).' OR comment_date <= '.$DB->quote( $date_min ).' )' );
}
// Restrict files from posts and comments which are visible for current User:
$files_SQL->WHERE_and( 'post_ID IS NULL OR '.statuses_where_clause( get_inskin_statuses( $Blog->ID, 'post' ), 'post_', $Blog->ID, 'blog_post!' ) );
$files_SQL->WHERE_and( 'comment_ID IS NULL OR '.statuses_where_clause( get_inskin_statuses( $Blog->ID, 'comment' ), 'comment_', $Blog->ID, 'blog_comment!' ) );
// Group same files linked with different objects:
$files_SQL->GROUP_BY( 'file_path, file_title, file_alt, file_desc' );
$files = $DB->get_results( $files_SQL );
foreach( $files as $file )
{
if( empty( $file->post_IDs ) && empty( $file->comment_IDs ) )
{ // Count only those files which have at least one post linked to it, Skip this:
continue;
}
$scores_map = array();
$scores_map['filename'] = score_text( basename( $file->file_path ), $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_file_name' ) );
$scores_map['filepath'] = score_text( $file->file_path, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_file_path' ) );
$scores_map['title'] = score_text( $file->file_title, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_file_title' ) );
$scores_map['alt'] = score_text( $file->file_alt, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_file_alt' ) );
$scores_map['description'] = score_text( $file->file_desc, $search_term, $keywords, $quoted_parts, /* multiplier: */ $Blog->get_setting( 'search_score_file_description' ) );
$final_score = $scores_map['filename']['score']
+ ( isset( $scores_map['filepath'] ) ? $scores_map['filepath']['score'] : 0 )
+ ( isset( $scores_map['title'] ) ? $scores_map['title']['score'] : 0 )
+ ( isset( $scores_map['alt'] ) ? $scores_map['alt']['score'] : 0 )
+ ( isset( $scores_map['description'] ) ? $scores_map['description']['score'] : 0 );
$search_result[] = array(
'type' => 'file',
'score' => $final_score,
'ID' => $file->file_ID,
'full_path' => $file->file_path,
'post_IDs' => $file->post_IDs,
'comment_IDs'=> $file->comment_IDs,
'scores_map' => $scores_map,
);
}
return $search_result;
}
/**
* Perform a scrored search
* This searches matching objects and gives a match-quality-score to each found object
*
* @param string the search keywords
* @param string What types to search: 'all', 'item', 'comment', 'meta', 'category', 'tag'
* Use ','(comma) as separator to use several kinds, e.g: 'item,comment' or 'tag,comment,category'.
* An empty value is equivalent to 'all'.
* @param string Post IDs to exclude from result, Separated with ','(comma)
* @param string Author IDs to filter, separated with ',' (comma)
* @param string Content age to consider: 'anytime', 'hour', 'day', 'week', '30d', '90d', 'year'
* An empty value is equivalent to 'anytime'.
* See @get_search_date_by_content_age().
* @return array scored search result, each element is an array( type, ID, score )
*/
function perform_scored_search( $search_keywords, $searched_content_types = 'all', $exclude_posts = '', $search_authors = '', $content_age = 'anytime' )
{
$keywords = trim( $search_keywords );
if( empty( $keywords ) )
{
return array();
}
global $Collection, $Blog, $DB, $debug;
global $scores_map, $score_prefix, $score_map_key, $Debuglog;
// Get quoted parts parts of the search query
$quoted_parts = array();
if( preg_match_all( '/(["\'])(?:(?=(\\\\?))\\2.)*?\\1/', $search_keywords, $matches ) )
{ // There are quoted search terms
$quoted_parts = $matches[0];
for( $index = 0; $index < count( $quoted_parts ); $index++ )
{ // Remove douple quotes around the parts
$quoted_part_length = utf8_strlen( $quoted_parts[$index] );
if( $quoted_part_length > 2 )
{ // The quoted part is not an empty string
$quoted_parts[$index] = utf8_substr( $quoted_parts[$index], 1, $quoted_part_length - 2 );
}
}
$quoted_parts = array_unique( $quoted_parts );
};
// Remove word separators:
$keywords = preg_replace( '/, +/', '', $search_keywords );
$keywords = str_replace( ',', ' ', $keywords );
// Remove quotes:
$keywords = str_replace( '"', ' ', $keywords );
// Remove extra white space:
$keywords = trim( $keywords );
// Split keywords:
$keywords = preg_split( '/\s+/', $keywords );
// Remove duplicate keywords:
$keywords = array_unique( $keywords );
if( isset( $keywords[0] ) && empty( $keywords[0] ) )
{ // The first item is an empty string when the search keyword contains only irrelevant characters
unset( $keywords[0] ); // remove empty word
}
if( empty( $keywords ) && empty( $quoted_parts ) )
{ // There is nothing to search for
return array();
// TODO: return NULL and display a specific error message like "Please enter some keywords to search."
}
if( $searched_content_types == 'all' || empty( $searched_content_types ) )
{ // Search all result types:
$search_type_item = true;
$search_type_comment = true;
$search_type_meta = true;
$search_type_category = true;
$search_type_tag = true;
$search_type_file = true;
}
else
{ // Check what types should be searched:
$searched_content_types = explode( ',', $searched_content_types );
$search_type_item = in_array( 'item', $searched_content_types );
$search_type_comment = in_array( 'comment', $searched_content_types );
$search_type_meta = in_array( 'meta', $searched_content_types );
$search_type_category = in_array( 'category', $searched_content_types );
$search_type_tag = in_array( 'tag', $searched_content_types );
$search_type_file = in_array( 'file', $searched_content_types );
}
if( ! empty( $search_authors ) || ( ! empty( $content_age ) && ( $content_age != 'anytime' ) ) )
{ // If search by author or age is enabled then don't search results in categories and tags because they don't have such data:
$search_type_category = false;
$search_type_tag = false;
}
$search_result = array();
if( $search_type_item &&
$Blog->get_setting( 'search_include_posts' ) )
{ // Perform search on Items:
$item_search_result = search_and_score_items( $search_keywords, $keywords, $quoted_parts, $exclude_posts, $search_authors, $content_age );
$search_result = $item_search_result;
if( $debug )
{
echo '<p class="text-muted">Just found '.count( $item_search_result ).' Items.</p>';
evo_flush();
}
}
if( $search_type_comment &&
$Blog->get_setting( 'search_include_cmnts' ) )
{ // Perform search on Comments:
$comment_search_result = search_and_score_comments( $search_keywords, $keywords, $quoted_parts, $search_authors, $content_age );
$search_result = array_merge( $search_result, $comment_search_result );
if( $debug )
{
echo '<p class="text-muted">Just found '.count( $comment_search_result ).' Comments.</p>';
evo_flush();
}
}
if( $search_type_meta &&
$Blog->get_setting( 'search_include_metas' ) &&
check_user_perm( 'meta_comment', 'view', false, $Blog->ID ) )
{ // Perform search on Meta/Internal Comments:
$meta_search_result = search_and_score_comments( $search_keywords, $keywords, $quoted_parts, $search_authors, $content_age, 'meta' );
$search_result = array_merge( $search_result, $meta_search_result );
if( $debug )
{
echo '<p class="text-muted">Just found '.count( $meta_search_result ).' Internal comments.</p>';
evo_flush();
}
}
if( $search_type_category &&
$Blog->get_setting( 'search_include_cats' ) )
{ // Perform search on Chapters:
$cats_search_result = search_and_score_chapters( $search_keywords, $keywords, $quoted_parts );
$search_result = array_merge( $search_result, $cats_search_result );
if( $debug )
{
echo '<p class="text-muted">Just found '.count( $cats_search_result ).' Categories.</p>';
evo_flush();
}
}
if( $search_type_tag &&
$Blog->get_setting( 'search_include_tags' ) )
{ // Perform search on Tags:
$tags_search_result = search_and_score_tags( $search_keywords, $keywords, $quoted_parts );
$search_result = array_merge( $search_result, $tags_search_result );
if( $debug )
{
echo '<p class="text-muted">Just found '.count( $tags_search_result ).' Tags.</p>';
evo_flush();
}
}
if( $search_type_file &&
$Blog->get_setting( 'search_include_files' ) )
{
$files_search_result = search_and_score_files( $search_keywords, $keywords, $quoted_parts, $search_authors, $content_age );
$search_result = array_merge( $search_result, $files_search_result );
if( $debug )
{
echo '<p class="text-muted">Just found '.count( $files_search_result ).' Files.</p>';
evo_flush();
}
}
if( count( $search_result ) > 0 )
{
// Sort results by score or date:
$score_result = array();
$max_score_result = $search_result[0];
foreach( $search_result as $r => $result_item )
{
if( $Blog->get_setting( 'search_sort_by' ) == 'date' )
{ // Sort by date:
if( isset( $result_item['date'] ) )
{ // If search item has a date, e-g posts and comments:
$score_result[] = preg_replace( '#[^\d]+#', '', $result_item['date'] ).'-'.$result_item['score'];
}
else
{ // If search item has no date, e-g categories and tags:
// Put them at the end of list with sorting by score:
// (00000000000000 is fake null date YYYYmmddHHiiss)
$score_result[] = '00000000000000-'.$result_item['score'];
}
}
else
{ // Sort by score:
$score_result[] = $result_item['score'];
}
if( $max_score_result['score'] < $result_item['score'] )
{ // Find item with max score:
$max_score_result = $result_item;
}
if( ! $debug )
{ // Don't store scores map in Sessions when debug is OFF:
unset( $search_result[ $r ]['scores_map'] );
}
}
array_multisort( $score_result, SORT_DESC, $search_result );
$max_percentage = get_percentage_from_result_map( $max_score_result['type'], $max_score_result['scores_map'], $quoted_parts, $keywords );
$search_result[0]['max_percentage'] = $max_percentage;
$search_result[0]['max_score'] = $max_score_result['score'];
if( $search_type_item )
{
$search_result[0]['nr_of_items'] = count( $item_search_result );
}
if( $search_type_comment )
{
$search_result[0]['nr_of_comments'] = count( $comment_search_result );
}
if( $search_type_meta )
{
$search_result[0]['nr_of_metas'] = count( $meta_search_result );
}
if( $search_type_category )
{
$search_result[0]['nr_of_cats'] = count( $cats_search_result );
}
if( $search_type_tag )
{
$search_result[0]['nr_of_tags'] = count( $tags_search_result );
}
}
// Handle searched results by modules:
modules_call_method( 'handle_searched_results', array(
'keywords' => $search_keywords,
'results' => $search_result,
) );
return $search_result;
}
/*
* Perform search (after having displayed the first part of the page) & display results.
* The search results are cached in the session for faster page by page navigation.
*
* @param array Display Params
*/
function search_result_block( $params = array() )
{
global $Collection, $Blog, $Session, $debug;
$search_keywords = param( 's', 'string', '', true );
$search_authors = param( 'search_author', 'string', '' );
$search_content_age = param( 'search_content_age', 'string', '' );
$search_content_type = param( 'search_type', 'string', '' );
$allow_cache = param( 'allow_cache', 'boolean', false );
// Try to load existing search results from Session:
$search_params = $Session->get( 'search_params' );
$search_result = $Session->get( 'search_result' );
$search_result_loaded = false;
if( ! $allow_cache // Force to load new search results, e-g when form is submitted, but don't force on page switching
|| empty( $search_params ) // We have no saved search params
|| ( $search_params['search_keywords'] != $search_keywords ) // We had saved search results but for a different search string
|| ( $search_params['search_blog'] != $Blog->ID ) // We had saved search results but for a different collection
|| ( $search_result === NULL ) )
{ // We need to perform a new search:
if( $debug )
{
echo '<p class="text-muted">Starting a new search...</p>';
}
// Display search spinner:
echo '<div id="search_loader_wrapper" class="text-center"><span class="loader_img"></span></div>';
// Flush first part of the page before starting search, which can be long...
evo_flush();
$search_params = array(
'search_keywords' => $search_keywords,
'search_blog' => $Blog->ID
);
$searched_content_types = array();
if( $Blog->get_setting( 'search_include_posts' ) )
{ // Search items:
$searched_content_types[] = 'item';
}
if( $Blog->get_setting( 'search_include_cmnts' ) )
{ // Search comments:
$searched_content_types[] = 'comment';
}
if( $Blog->get_setting( 'search_include_metas' ) &&
check_user_perm( 'meta_comment', 'view', false, $Blog->ID ) )
{ // Search meta/internal comments:
$searched_content_types[] = 'meta';
}
if( $Blog->get_setting( 'search_include_cats' ) )
{ // Search categories:
$searched_content_types[] = 'category';
}
if( $Blog->get_setting( 'search_include_tags' ) )
{ // Search tags:
$searched_content_types[] = 'tag';
}
if( $Blog->get_setting( 'search_include_files' ) )
{ // Search files:
$searched_content_types[] = 'file';
}
$searched_content_types = count( $searched_content_types ) == 6 ? '' : implode( ',', $searched_content_types );
if( ! empty( $search_content_type ) )
{
$searched_content_types = $search_content_type;
}
// Perform new search:
$search_result = perform_scored_search( $search_keywords, $searched_content_types, '', $search_authors, $search_content_age );
// Save results into session:
$Session->set( 'search_params', $search_params );
$Session->set( 'search_result', $search_result );
$search_result_loaded = true;
}
else
{ // We found the desired saved search results in the Session:
if( $debug )
{ // Display counts
echo '<div class="text-muted">';
echo '<p>We found the desired saved search results in the Session:</p>';
echo '<ul>';
if( isset( $search_result[0]['nr_of_items'] ) )
{
echo '<li>'.sprintf( '%d posts', $search_result[0]['nr_of_items'] ).'</li>';
}
if( isset( $search_result[0]['nr_of_comments'] ) )
{
echo '<li>'.sprintf( '%d comments', $search_result[0]['nr_of_comments'] ).'</li>';
}
if( isset( $search_result[0]['nr_of_metas'] ) )
{
echo '<li>'.sprintf( '%d internal comments', $search_result[0]['nr_of_metas'] ).'</li>';
}
if( isset( $search_result[0]['nr_of_cats'] ) )
{
echo '<li>'.sprintf( '%d chapters', $search_result[0]['nr_of_cats'] ).'</li>';
}
if( isset( $search_result[0]['nr_of_tags'] ) )
{
echo '<li>'.sprintf( '%d tags', $search_result[0]['nr_of_tags'] ).'</li>';
}
if( isset( $search_result[0]['nr_of_files'] ) )
{
echo '<li>'.sprintf( '%d files', $search_result[0]['nr_of_files'] ).'</li>';
}
echo '</ul>';
echo '</div>';
}
// Flush first part of the page before starting search, which can be long...
evo_flush();
}
// Make sure we are not missing any display params:
$params = array_merge( array(
'no_match_message' => '<p class="alert alert-info msg_nothing" style="margin: 2em 0">'.T_('Sorry, we could not find anything matching your request, please try to broaden your search.').'<p>',
'block_start' => '',
'block_end' => '',
'pagination' => array(),
'use_editor' => false, // Use editor instead of author if it is allowed (only the posts have an editor)
'author_format' => 'avatar_name', // @see User::get_identity_link() // avatar_name | avatar_login | only_avatar | name | login | nickname | firstname | lastname | fullname | preferredname
'date_format' => locale_datefmt(),
), $params );
$search_result = $Session->get( 'search_result' );
if( empty( $search_result ) )
{
echo $params['no_match_message'];
hide_spinner();
return;
}
// Prepare pagination:
$result_count = count( $search_result );
$result_per_page = $Blog->get_setting( 'search_per_page' );
if( $result_count > $result_per_page )
{ // We will have multiple search result pages:
$current_page = param( 'page', 'integer', 1 );
$total_pages = ceil($result_count / $result_per_page);
if( $current_page > $total_pages )
{
$current_page = $total_pages;
}
$page_params = array_merge( array(
'total' => $result_count,
'current_page' => $current_page,
'total_pages' => $total_pages,
'list_span' => 11, // Number of visible pages on navigation line
), $params['pagination'] );
search_page_links( $page_params );
}
else
{ // Only one page of results:
$current_page = 1;
$total_pages = 1;
}
// Set current page indexes:
$from = ( ( $current_page -1 ) * $result_per_page );
$to = ( $current_page < $total_pages ) ? ( $from + $result_per_page ) : ( $result_count );
// Init caches
$ItemCache = & get_ItemCache();
$CommentCache = & get_CommentCache();
$ChapterCache = & get_ChapterCache();
$FileCache = & get_FileCache();
$FiletypeCache = & get_FiletypeCache();
if( !$search_result_loaded )
{ // Search result objects are not loaded into memory yet, load them
// Group required object ids by type:
$required_ids = array();
for( $index = $from; $index < $to; $index++ )
{
$row = $search_result[ $index ];
if( isset( $required_ids[ $row['type'] ] ) )
{
$required_ids[ $row['type'] ][] = $row['ID'];
}
else
{
$required_ids[ $row['type'] ] = array( $row['ID'] );
}
}
// Load each required object into the corresponding cache:
foreach( $required_ids as $type => $object_ids )
{
switch( $type )
{
case 'item':
$ItemCache->load_list( $object_ids );
break;
case 'comment':
case 'meta':
$CommentCache->load_list( $object_ids );
break;
case 'category':
$ChapterCache->load_list( $object_ids );
break;
case 'file':
$FileCache->load_list( $object_ids );
break;
// TODO: we'll probably load "tag" objects once we support tag-synonyms.
default: // Not handled search result type
break;
}
}
}
// ----------- Display ------------
echo $params['block_start'];
// Memorize best scores:
$max_percentage = isset( $search_result[0]['max_percentage'] ) ? $search_result[0]['max_percentage'] : 100;
$max_score = isset( $search_result[0]['max_score'] ) ? $search_result[0]['max_score'] : $search_result[0]['score'];
if( $max_score == 0 )
{ // This must not happens, however use 1 to avoid error of division by zero:
$max_score = 1;
}
// Display results for current page:
for( $index = $from; $index < $to; $index++ )
{
$row = $search_result[ $index ];
switch( $row['type'] )
{
case 'item':
// Prepare to display an Item:
$Item = $ItemCache->get_by_ID( $row['ID'], false );
if( empty( $Item ) )
{ // This Item was deleted, since the search process was executed
continue 2; // skip from switch and skip to the next item in loop
}
$display_params = array(
'search_result_type' => 'item',
'search_result_object' => $Item,
);
break;
case 'comment':
case 'meta':
// Prepare to display a Comment or Meta/Internal Comment:
$Comment = $CommentCache->get_by_ID( $row['ID'], false );
if( empty( $Comment ) || ( $Comment->status == 'trash' ) )
{ // This Comment was deleted, since the search process was executed
continue 2; // skip from switch and skip to the next item in loop
}
$display_params = array(
'search_result_type' => $row['type'],
'search_result_object' => $Comment,
);
break;
case 'category':
// Prepare to display a Category:
$Chapter = $ChapterCache->get_by_ID( $row['ID'], false );
if( empty( $Chapter ) )
{ // This Chapter was deleted, since the search process was executed
continue 2; // skip from switch and skip to the next item in loop
}
$display_params = array(
'search_result_type' => 'category',
'search_result_object' => $Chapter,
);
break;
case 'tag':
// Prepare to display a Tag:
list( $tag_name, $post_count ) = explode( ',', $row['name'] );
$display_params = array(
'search_result_type' => 'tag',
'search_result_tag' => $tag_name,
'search_result_collection' => $Blog,
'tag_post_count' => $post_count,
);
break;
case 'file':
// Prepare to display a File:
$File = $FileCache->get_by_id( $row['ID'], false );
/* TODO: Create File function to get this:
$link_owners = array();
if( ! empty( $row['post_IDs'] ) )
{
$post_IDs = explode( ',', $row['post_IDs'] );
foreach( $post_IDs as $post_ID )
{
$Item = $ItemCache->get_by_ID( $post_ID, false );
$link_owners[] = $Item->get_title( array( 'link_type' => 'permalink' ) );
}
}
if( ! empty( $row['comment_IDs'] ) )
{
$comment_IDs = explode( ',', $row['comment_IDs'] );
foreach( $comment_IDs as $comment_ID )
{
$Comment = $CommentCache->get_by_ID( $comment_ID, false );
$link_owners[] = $Comment->get_permanent_link( '#item#' );
}
}
*/
$display_params = array(
'search_result_type' => 'file',
'search_result_object' => $File,
);
break;
default:
// Other type of result is not implemented
// TODO: maybe find collections (especially in case of aggregation)? users? files?
continue 2;
}
// Common display params for all types:
$display_params['score_date'] = isset( $row['date'] ) ? $row['date'] : 0;
$display_params['score'] = $row['score'];
$display_params['percentage'] = round( $row['score'] * $max_percentage / $max_score );
if( isset( $row['scores_map'] ) )
{ // Scores map is defiend only with enabled debug mode:
$display_params['scores_map'] = $row['scores_map'];
}
$display_params['type'] = $row['type'];
$display_params['best_result'] = $index == 0;
$display_params['max_score'] = sprintf( ( floor( $max_score ) != $max_score ) ? '%.2f' : '%d', $max_score );
$display_params['max_percentage'] = $max_percentage;
// Display one search result:
display_search_result( array_merge( $params, $display_params ) );
}
echo $params['block_end'];
// Display pagination:
if( $result_count > $result_per_page )
{
search_page_links( $page_params );
}
hide_spinner();
}
/**
* Display search result page links
*
* @param array params
*/
function search_page_links( $params = array() )
{
if( !isset( $params['total_pages'] ) )
{ // this param is required
return;
}
$params = array_merge( array(
'block_start' => '<p class="center">',
'block_end' => '</p>',
'page_current_template' => '<b>$page_num$</b>',
'page_item_before' => ' ',
'page_item_after' => '',
'page_item_current_before' => ' ',
'page_item_current_after' => '',
'prev_text' => '<<',
'next_text' => '>>',
'prev_class' => '',
'next_class' => '',
), $params );
$page_list_span = $params['list_span'];
$total_pages = $params['total_pages'];
$current_page = isset( $params['current_page'] ) ? $params['current_page'] : 1;
$page_url = regenerate_url( 'page', 'allow_cache=1' );
// Initialize a start of pages list:
if( $current_page <= intval( $page_list_span / 2 ) )
{ // the current page number is small
$page_list_start = 1;
}
elseif( $current_page > $total_pages - intval( $page_list_span / 2 ) )
{ // the current page number is big
$page_list_start = max( 1, $total_pages - $page_list_span+1);
}
else
{ // the current page number can be centered
$page_list_start = $current_page - intval( $page_list_span / 2 );
}
// Initialize an end of pages list:
if( $current_page > $total_pages - intval( $page_list_span / 2 ) )
{ //the current page number is big
$page_list_end = $total_pages;
}
else
{
$page_list_end = min( $total_pages, $page_list_start + $page_list_span - 1 );
}
echo $params['block_start'];
if( $current_page > 1 )
{ // A link to previous page:
echo add_tag_class( $params['page_item_before'], 'listnav_prev' );
$prev_attrs = empty( $params['prev_class'] ) ? '' : ' class="'.$params['prev_class'].'"';
echo '<a href="'.url_add_param( $page_url, 'page='.( $current_page - 1 ) ).'" rel="prev"'.$prev_attrs.'>'.$params['prev_text'].'</a>';
echo $params['page_item_after'];
}
// Display a link to first page:
if( $current_page == 1 )
{
echo add_tag_class( $params['page_item_current_before'], 'listnav_first' );
echo '<a href="'.url_add_param( $page_url, 'page=1' ).'">1</a>';
echo $params['page_item_current_after'];
}
else
{
echo add_tag_class( $params['page_item_before'], 'listnav_first' );
echo '<a href="'.url_add_param( $page_url, 'page=1' ).'">1</a>';
echo $params['page_item_after'];
}
if( $page_list_start > 2 )
{ // Display a link to previous pages range:
$page_no = ceil( $page_list_start / 2 );
echo add_tag_class( $params['page_item_before'], 'listnav_prev_list' );
echo '<a href="'.url_add_param( $page_url, 'page='.$page_no ).'">...</a>';
echo $params['page_item_after'];
}
$hidden_active_distances = array( 1, 2 );
$page_prev_i = $current_page - 1;
$page_next_i = $current_page + 1;
$pib = add_tag_class( $params['page_item_before'], '**active_distance_**' );
// Do not include first page in the page list range
if( $page_list_start == 1 )
{
$page_list_start++;
if( ( $page_list_end + 1 ) < $total_pages )
{
$page_list_end++;
}
}
// Also, do not include last page in the page list range
if( $page_list_end == $total_pages )
{
$page_list_end--;
if( $page_list_start > 2 )
{
$page_list_start--;
}
}
for( $i = $page_list_start; $i <= $page_list_end; $i++ )
{
if( $current_page <= 4 )
{
$a = ( $i - 4 );
$active_dist = $a > 0 ? $a : null;
}
elseif( $current_page > ( $total_pages - 3 ) )
{
if( $i > ( $total_pages - 3 ) )
{
$active_dist = null;
}
else
{
$active_dist = ( ( $total_pages - 3 ) - $i );
}
}
else
{
$active_dist = abs( $current_page - $i );
}
if( in_array( $active_dist, $hidden_active_distances ) && ( $i < $current_page ) && ( $i > 2 ) && ( $current_page > 4 ) )
{
$page_no = ceil( $page_list_start / 2 );
if( $page_no == 1 )
{
$page_no++;
}
if( isset( $params['page_item_before'] ) && trim( $params['page_item_before'] ) )
{
echo add_tag_class( $params['page_item_before'], 'listnav_distance_'.$active_dist );
echo '<a href="'.url_add_param( $page_url, 'page='.$page_no ).'">...</a>';
}
else
{
echo add_tag_class( '<a href="'.url_add_param( $page_url, 'page='.$page_no ).'">...</a>', 'listnav_distance_'.$active_dist );
}
echo $params['page_item_after'];
}
if( $i == $current_page )
{ // Current page
echo $params['page_item_current_before'];
echo str_replace( '$page_num$', $i, $params['page_current_template'] );
echo $params['page_item_current_after'];
}
else
{
$attr_rel = '';
if( $page_prev_i == $i )
{ // Add attribute rel="prev" for previous page
$attr_rel = ' rel="prev"';
}
elseif( $page_next_i == $i )
{ // Add attribute rel="next" for next page
$attr_rel = ' rel="next"';
}
if( $active_dist )
{
echo str_replace( '**active_distance_**', 'active_distance_'.$active_dist, $pib );
}
else
{
echo str_replace( '**active_distance_**', '', $pib );
}
echo '<a href="'.url_add_param( $page_url, 'page='.$i ).'"'.$attr_rel.'>'.$i.'</a>';
echo $params['page_item_after'];
}
if( in_array( $active_dist, $hidden_active_distances ) && ( $i > $current_page ) && ( $i < ( $total_pages - 1 ) ) )
{
$page_no = $page_list_end + floor( ( $total_pages - $page_list_end ) / 2 );
if( $page_no == $total_pages )
{
$page_no--;
}
if( isset( $params['page_item_before'] ) && trim( $params['page_item_before'] ) )
{
echo add_tag_class( $params['page_item_before'], 'listnav_distance_'.$active_dist );
echo '<a href="'.url_add_param( $page_url, 'page='.$page_no ).'">...</a>';
}
else
{
echo add_tag_class( '<a href="'.url_add_param( $page_url, 'page='.$page_no ).'">...</a>', 'listnav_distance_'.$active_dist );
}
echo $params['page_item_after'];
}
}
if( ( $page_list_end < $total_pages ) && ( $page_list_end < $total_pages - 1 ) )
{ // Display a link to next pages range:
$page_no = $page_list_end + floor( ( $total_pages - $page_list_end ) / 2 );
echo add_tag_class( $params['page_item_before'], 'listnav_next_list' );
echo '<a href="'.url_add_param( $page_url, 'page='.$page_no ).'">...</a>';
echo $params['page_item_after'];
}
// Display a link to last page:
if( $current_page == $total_pages )
{
echo add_tag_class( $params['page_item_current_before'], 'listnav_last' );
echo '<a href="'.url_add_param( $page_url, 'page='.$total_pages ).'">'.$total_pages.'</a>';
echo $params['page_item_current_after'];
}
else
{
echo add_tag_class( $params['page_item_before'], 'listnav_last' );
echo '<a href="'.url_add_param( $page_url, 'page='.$total_pages ).'">'.$total_pages.'</a>';
echo $params['page_item_after'];
}
if( $current_page < $total_pages )
{ // A link to next page:
echo add_tag_class( $params['page_item_before'], 'listnav_next' );
$next_attrs = empty( $params['next_class'] ) ? '' : ' class="'.$params['next_class'].'"';
echo ' <a href="'.url_add_param( $page_url, 'page='.( $current_page + 1 ) ).'" rel="next"'.$next_attrs.'>'.$params['next_text'].'</a>';
echo $params['page_item_after'];
}
echo $params['block_end'];
}
/**
* Display one search result object
*
* @param array result object params
*/
function display_search_result( $params = array() )
{
global $debug, $Blog;
// TODO: Check if we still need this:
// Make sure we are not missing any param:
// $params = array_merge( array(
// 'title' => '',
// 'author' => '',
// 'creation_date' => '',
// 'excerpt' => '',
// 'row_start' => '<div class="search_result">',
// 'row_end' => '</div>',
// 'cell_title_start' => '<div class="search_title">',
// 'cell_title_end' => '</div>',
// 'cell_chapter_start' => '<div class="search_info dimmed">',
// 'cell_chapter_end' => '</div>',
// 'cell_author_start' => '<div class="search_info dimmed">',
// 'cell_author_end' => '</div>',
// 'cell_author_empty' => false, // false - to display author only when it is defined, use string to print text instead of empty author
// 'cell_content_start' => '<div class="result_content">',
// 'cell_content_end' => '</div>',
// ), $params );
$render_template_objects = array();
switch( $params['search_result_type'] )
{
case 'item':
$render_template_objects['Item'] = $params['search_result_object'];
break;
case 'comment':
case 'meta':
$render_template_objects['Comment'] = $params['search_result_object'];
break;
case 'file':
$render_template_objects['File'] = $params['search_result_object'];
break;
case 'category':
$render_template_objects['Chapter'] = $params['search_result_object'];
break;
case 'tag':
$object_name = 'Tag';
$render_template_objects['tag'] = $params['search_result_tag'];
$render_template_objects['Collection'] = $params['search_result_collection'];
break;
}
$TemplateCache = & get_TemplateCache();
$template_code = $Blog->get_setting( 'search_result_template_'.$params['search_result_type'] );
if( $template_code && ! $TemplateCache->get_by_code( $template_code, false, false ) )
{
$this->display_error_message( sprintf( 'Template not found: %s', '<code>'.$template_code.'</code>' ) );
return false;
}
$search_result = render_template_code( $template_code, $params, $render_template_objects );
// Modify the search result content by modules:
modules_call_method_reference_params( 'modify_search_result', $search_result );
// Display search result item:
echo $search_result;
if( $debug )
{
display_score_map( $params );
}
}
/**
* Display score map regarding a search result
*
* @param array detailed information about the received search scores
*/
function display_score_map( $params )
{
global $Blog;
echo '<ul class="search_score_map dimmed">';
echo '<li>Date: '.( empty( $params['score_date'] ) ? 'None' : mysql2localedatetime( $params['score_date'] ) ).'</li>';
if( ! empty( $params['scores_map'] ) )
{ // If score map is provided:
foreach( $params['scores_map'] as $result_part => $score_map )
{
if( ! is_array( $score_map ) )
{
if( $score_map > 0 )
{ // Score received for this field
echo '<li>'.sprintf( 'Extra points for [%s]', $result_part ).'</li><ul>';
switch ( $result_part )
{
case 'last_mod_date':
case 'creation_date':
echo '<li>'.sprintf( '%d points.', $score_map );
echo ' Rule: The number of points are calculated based on the days passed since creation or last modification';
echo '<ul><li>days_passed < 5 => ( '.$Blog->get_setting( 'search_score_post_date_lastweek' ).' - days_passed )</li>';
echo '<li>'.$Blog->get_setting( 'search_score_post_date_lastweek' ).' <= days_passed < 8 => '.( $Blog->get_setting( 'search_score_post_date_twoweeks' ) + 1 ).'</li>';
echo '<li>when days_passed >= 8: ( days_passed < 15 ? '.$Blog->get_setting( 'search_score_post_date_twoweeks' ).' : ( days_passed < 31 ? '.$Blog->get_setting( 'search_score_post_date_lastmonth' ).' : '.$Blog->get_setting( 'search_score_post_date_moremonth' ).' ) )</li>';
echo '</ul>';
break;
default:
echo '<li>'.sprintf( '%d points for [%s]', $score_map, $result_part ).'</li>';
break;
}
echo '</ul>';
}
continue;
}
elseif( isset( $score_map['score_weight'] ) && $score_map['score_weight'] > 1 )
{ // add note that the score was multiplied because of the importance of the field where it was found
$note = sprintf( ' - the match scores are multiplied with %d', $score_map['score_weight'] );
}
else
{ // note is not required
$note = '';
}
echo '<li>'.sprintf( 'Searching in [%s]', $result_part ).$note.'</li><ul>';
if( $score_map['score'] == 0 )
{
echo '</ul>';
continue;
}
$keyword_match = null;
foreach( $score_map['map'] as $match_type => $scores )
{
switch( $match_type )
{
case 'whole_term':
// Example: We searched [several images attached] and we matched it in:
// - [This post has several images attached to it]
// OR we searched [wild] and we matched it in:
// - [I was bewildered!]
// - [A wild cat!]
echo '<li>'.sprintf( '%d points for whole term match', $scores ).'</li>';
continue 2;
case 'quoted_term_all':
// Example: We searched ["images attached" "has several" word] and we matched it in:
// - [This post has several images attached to it]
// - [The comment has several private images attached]
echo '<li>'.sprintf( '%d extra points for all quoted term match', $scores ).'</li>';
continue 2;
case 'all_case_sensitive':
// Example: We searched [several Thi ost mage Each] and we matched it in:
// - [This post has several images attached to it. Each one uses a different Attachment Position.]
echo '<li>'.sprintf( '%d extra points for all word case sensitive match', $scores ).'</li>';
continue 2;
case 'all_whole_words':
// Example: We searched [several this post images each] and we matched it in:
// - [This post has several images attached to it. Each one uses a different Attachment Position.]
echo '<li>'.sprintf( '%d extra points for all word complete match', $scores ).'</li>';
continue 2;
case 'tags':
// Example: We searched [photo album] and we matched it if the post has a tag with name:
// - [photo album]
echo '<li>'.sprintf( '%d points for tag term match', $scores ).'</li>';
continue 2;
}
if( !is_array( $scores ) )
{
continue;
}
if( $keyword_match != $match_type && $keyword_match !== null )
{ // close previously started list
echo '</ul>';
}
if( $keyword_match != $match_type )
{
switch( $match_type )
{
case 'word_case_sensitive_match':
// If at least one word from requested phrase is case sensitive matched:
echo '<li>Case sensitive matches</li>';
break;
case 'whole_word_match':
// If at least one whole word from requested phrase is case insensitive matched:
echo '<li>Whole word matches</li>';
break;
case 'word_case_insensitive_match':
// If at least one word from requested phrase is case insensitive matched:
echo '<li>Case insensitive matches</li>';
break;
case 'word_multiple_occurences':
// If at least one word from requested phrase is case insensitive matched at least two times in the content:
echo '<li>Extra points for multiple occurrences - Rule: sum( score weight / x ) where x goes from 2 to number of occurrences</li>';
break;
}
$keyword_match = $match_type;
echo '<ul>';
}
foreach( $scores as $word => $score )
{
if( is_float( $score ) )
{
$points_label = '%.2F points - match on [%s]';
}
else
{
$points_label = ( $score > 1 ) ? '%d points - match on [%s]' : '%d point - match on [%s]';
}
echo '<li>'.sprintf( $points_label, $score, $word ).'</li>';
}
}
if( $keyword_match != null )
{ // display the end of the specific match type
echo '</ul>';
}
echo '</ul>';
}
}
$total_score_pattern = ( floor( $params['score'] ) != $params['score'] ) ? '%.2f' : '%d';
echo '<li>'.sprintf( 'Total: '.$total_score_pattern.' points', $params['score'] ).'</li>';
if( $params['best_result'] )
{
echo '<ul><li>This is the best result. Percentage value is calculated based on the number of matching words and matching quoted terms.</li>';
echo '<li>Note: In case of the number of matching words even case insensitive partial matches are counted.</li>';
echo '<li>Percentage = ( Number of matching words + Number of matching quoted terms ) * 100 / ( Number of words in search text + Number of quoted terms in search text )</li></ul>';
}
else
{
echo '<ul><li>Percentage value is calculated based on the received points compared to the best result</li>';
echo '<li>'.sprintf( 'Percentage [%d%%] = ( This result total points ['.$total_score_pattern.'] ) * ( Best result percentage [%d] ) / ( Best result total points [%s] )', $params['percentage'], $params['score'], $params['max_percentage'], $params['max_score'] ).'</li></ul>';
}
echo '</ul>';
}
/**
* Add debug information about the search result scores
*
* @param array search_result
*/
function display_search_debug_info( $search_result )
{
global $Debuglog;
$Debuglog->add( sprintf('Number of search result: [%d]', count( $search_result ) ), 'info' );
foreach( $search_result as $index => $result_value )
{
$score_map_key = $result_value['type'].'_'.$result_value['ID'];
// Displaying the scores of the result map
foreach( $result_value['scores_map'] as $search_key => $search_value )
{
if( is_array( $search_value ) )
{
if( $search_value['score'] == 0 )
{
continue;
}
foreach( $search_value['map'] as $score_key => $score_value )
{
if( is_array( $score_value ) )
{
foreach( $score_value as $key => $value )
{
$Debuglog->add( sprintf('Score result: [%s]:[%s]:[%s]:[%s]:[%d]', $score_map_key, $search_key, $score_key, $key, $value), 'info' );
}
}
else
{
$Debuglog->add( sprintf('Score result: [%s]:[%s]:[%s]:[%d]', $score_map_key, $search_key, $score_key, $score_value), 'info' );
}
}
}
else
{
$Debuglog->add( sprintf('Score result: [%s]:[%s]:[%d]', $score_map_key, $search_key, $search_value), 'info' );
}
}
$Debuglog->add( sprintf('Result for [%s]: [Percentage:%d%%][Percentage score:%d][Total score:%d]', $score_map_key, $search_result[$index]['percentage'], $search_result[$index]['percentage_score'], $search_result[$index]['score']), 'info' );
}
}
/**
* Hide spinner
*/
function hide_spinner()
{
?>
<script>
// Hide spinner:
( function() {
var search_spinner = document.querySelector( "#search_loader_wrapper" );
if( search_spinner )
{
search_spinner.classList.add( 'hide' );
}
} )();
</script>
<?php
}