<?php
/**
* /lib/utils.php
*
* @package Relevanssi
* @author Mikko Saari
* @license https://wordpress.org/about/gpl/ GNU General Public License
* @see https://www.relevanssi.com/
*/
/**
* Returns a Relevanssi_Taxonomy_Walker instance.
*
* Requires the class file and generates a new Relevanssi_Taxonomy_Walker instance.
*
* @return object A new Relevanssi_Taxonomy_Walker instance.
*/
function get_relevanssi_taxonomy_walker() {
require_once 'class-relevanssi-taxonomy-walker.php';
return new Relevanssi_Taxonomy_Walker();
}
/**
* Adds apostrophes around a string.
*
* @param string $str The string.
*
* @return string The string with apostrophes around it.
*/
function relevanssi_add_apostrophes( $str ) {
return "'" . $str . "'";
}
/**
* Adds quotes around a string.
*
* @param string $str The string.
*
* @return string The string with quotes around it.
*/
function relevanssi_add_quotes( $str ) {
return '"' . $str . '"';
}
/**
* Wraps the relevanssi_mb_trim() function so that it can be used as a callback
* for array_walk().
*
* @since 2.1.4
*
* @see relevanssi_mb_trim.
*
* @param string $str String to trim.
*/
function relevanssi_array_walk_trim( string &$str ) {
$str = relevanssi_mb_trim( $str );
}
/**
* Converts sums in an array to averages, based on an array containing counts.
*
* Both arrays need to have (key, value) pairs with the same keys. The values
* in $arr are then divided by the matching values in $counts, so when we have
* sums in $arr and counts in $counts, we end up with averages.
*
* @param array $arr The array with sums, passed as reference.
* @param array $counts The array with counts.
*/
function relevanssi_average_array( array &$arr, array $counts ) {
array_walk(
$arr,
function ( &$value, $key ) use ( $counts ) {
$value = round( $value / $counts[ $key ], 2 );
}
);
}
/**
* Returns 'checked' if the option is enabled.
*
* @param string $option Value to check.
*
* @return string If the option is 'on', returns 'checked', otherwise returns an
* empty string.
*/
function relevanssi_check( string $option ) {
$checked = '';
if ( 'on' === $option ) {
$checked = 'checked';
}
return $checked;
}
/**
* Closes tags in a bit of HTML code.
*
* Used to make sure no tags are left open in excerpts. This method is not
* foolproof, but it's good enough for now.
*
* @param string $html The HTML code to analyze.
*
* @return string The HTML code, with tags closed.
*/
function relevanssi_close_tags( string $html ) {
$result = array();
preg_match_all(
'#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU',
$html,
$result
);
$opened_tags = $result[1];
preg_match_all( '#</([a-z]+)>#iU', $html, $result );
$closed_tags = $result[1];
$len_opened = count( $opened_tags );
if ( count( $closed_tags ) === $len_opened ) {
return $html;
}
$opened_tags = array_reverse( $opened_tags );
for ( $i = 0; $i < $len_opened; $i++ ) {
if ( ! in_array( $opened_tags[ $i ], $closed_tags, true ) ) {
$html .= '</' . $opened_tags[ $i ] . '>';
} else {
unset(
$closed_tags[ array_search( $opened_tags[ $i ], $closed_tags, true ) ]
);
}
}
return $html;
}
/**
* Counts search term occurrances in the Relevanssi index.
*
* @param string $query The search query. Will be split at spaces.
* @param string $mode Output mode. Possible values 'array' or 'string'.
* Default is 'array'.
*
* @return array|string An array of search term occurrances, or a string with
* the number of occurrances.
*/
function relevanssi_count_term_occurrances( string $query, string $mode = 'array' ) {
global $wpdb, $relevanssi_variables;
$terms = explode( ' ', $query );
$counts = array();
foreach ( $terms as $term ) {
$term = trim( $term );
if ( empty( $term ) ) {
continue;
}
$counts[ $term ] = $wpdb->get_var(
$wpdb->prepare(
'SELECT SUM(content + title + comment + tag +
link + author + category + excerpt + taxonomy + customfield
+ mysqlcolumn) AS total FROM ' .
$relevanssi_variables['relevanssi_table'] . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
' WHERE term = %s
GROUP BY term',
$term
)
);
}
if ( 'array' === $mode ) {
return $counts;
} elseif ( 'string' === $mode ) {
$strings = array();
foreach ( $counts as $term => $count ) {
$strings[] = "<span class='search_term'>$term</span>: <span class='count'>$count</span>";
}
return implode( ', ', $strings );
}
}
/**
* Prints out debugging notices.
*
* If WP_CLI is available, prints out the debug notice as a WP_CLI::log(),
* otherwise if debug mode is on, uses error_log(), otherwise just echo.
*
* @param string $notice The notice to print out.
*/
function relevanssi_debug_echo( string $notice ) {
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::log( $notice );
} elseif ( relevanssi_log_debug() ) {
error_log( 'RELEVANSSI: ' . $notice ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
} else {
echo esc_html( $notice ) . "\n";
}
}
/**
* Runs do_shortcode() on content, but safeguards the global $post to make sure
* it isn't changed by the shortcodes. If shortcode expansion is disabled in
* Relevanssi settings, runs strip_shortcodes() on the content.
*
* @uses relevanssi_disable_shortcodes() Disables problem shortcodes.
* @see do_shortcode() Expands shortcodes.
* @see strip_shortcodes() Strips off shortcodes.
*
* @param string $content The content where the shortcodes are expanded.
*
* @return string
*/
function relevanssi_do_shortcode( string $content ): string {
if ( 'on' === get_option( 'relevanssi_expand_shortcodes' ) ) {
relevanssi_disable_shortcodes();
/**
* This needs to be global here, otherwise the safety mechanism doesn't
* work correctly.
*/
global $post;
$global_post_before_shortcode = null;
if ( isset( $post ) ) {
$global_post_before_shortcode = $post;
}
$content = do_shortcode( $content );
if ( $global_post_before_shortcode ) {
$post = $global_post_before_shortcode; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
unset( $tablepress_controller );
} else {
$content = strip_shortcodes( $content );
}
return $content;
}
/**
* Recursively flattens a multidimensional array to produce a string.
*
* @param array $arr The source array.
*
* @return string The array contents as a string.
*/
function relevanssi_flatten_array( array $arr ) {
$return_value = '';
foreach ( new RecursiveIteratorIterator( new RecursiveArrayIterator( $arr ) ) as $value ) {
$return_value .= ' ' . $value;
}
return trim( $return_value );
}
/**
* Generates from and to date values from ranges.
*
* Possible values in the $request array: 'from' and 'to' for direct dates,
* 'this_year' for Jan 1st to today, 'this_month' for 1st of month to today,
* 'last_month' for 1st of previous month to last of previous month,
* 'this_week' for Monday of this week to today (or Sunday, if the
* relevanssi_week_starts_on_sunday returns `true`), 'last_week' for the
* previous week, 'last_30' for from 30 days ago to today, 'last_7' for from
* 7 days ago to today.
*
* @param array $request The request array where the settings are.
* @param string $from The default 'from' date in "Y-m-d" format.
* @return array The from date in 'from' and the to date in 'to' in "Y-m-d"
* format.
*/
function relevanssi_from_and_to( array $request, string $from ): array {
$today = gmdate( 'Y-m-d' );
$week_start = 'monday';
$to = $today;
/**
* Controls whether the week starts on Sunday or Monday.
*
* @param boolean If `true`, week starts on Sunday. Default `false`, week
* starts on Monday.
*/
if ( apply_filters( 'relevanssi_week_starts_on_sunday', false ) ) {
$week_start = 'sunday';
}
if ( ! isset( $request['everything'] ) && isset( $request['from'] ) && $request['from'] > $from ) {
$from = $request['from'];
}
if ( ! isset( $request['everything'] ) && isset( $request['to'] ) && $request['to'] < $today ) {
$to = $request['to'];
}
if ( isset( $request['this_year'] ) ) {
$from = gmdate( 'Y-m-d', strtotime( 'first day of january this year' ) );
$to = gmdate( 'Y-m-d' );
}
if ( isset( $request['this_month'] ) ) {
$from = gmdate( 'Y-m-d', strtotime( 'first day of this month' ) );
$to = gmdate( 'Y-m-d' );
}
if ( isset( $request['last_month'] ) ) {
$from = gmdate( 'Y-m-d', strtotime( 'first day of previous month' ) );
$to = gmdate( 'Y-m-d', strtotime( 'last day of previous month' ) );
}
if ( isset( $request['this_week'] ) ) {
$from = gmdate( 'Y-m-d', strtotime( 'previous ' . $week_start ) );
$to = gmdate( 'Y-m-d' );
}
if ( isset( $request['last_week'] ) ) {
$start = 'sunday' === $week_start ? gmdate( 'w' ) + 7 : gmdate( 'w' ) + 6;
$end = 'sunday' === $week_start ? gmdate( 'w' ) + 1 : gmdate( 'w' );
$from = gmdate( 'Y-m-d', strtotime( '-' . $start . ' days' ) );
$to = gmdate( 'Y-m-d', strtotime( '-' . $end . ' days' ) );
}
if ( isset( $request['last_30'] ) ) {
$from = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
$to = gmdate( 'Y-m-d' );
}
if ( isset( $request['last_7'] ) ) {
$from = gmdate( 'Y-m-d', strtotime( '-7 days' ) );
$to = gmdate( 'Y-m-d' );
}
return array(
'from' => $from,
'to' => $to,
);
}
/**
* Generates closing tags for an array of tags.
*
* @param array $tags Array of tag names.
*
* @return array $closing_tags Array of closing tags.
*/
function relevanssi_generate_closing_tags( array $tags ) {
$closing_tags = array();
foreach ( $tags as $tag ) {
$a = str_replace( '<', '</', $tag );
$b = str_replace( '>', '/>', $tag );
$closing_tags[] = $a;
$closing_tags[] = $b;
}
return $closing_tags;
}
/**
* Returns a post object based on ID, **type**id notation or an object.
*
* @uses relevanssi_get_post_object() Fetches post objects.
*
* @param int|string|WP_Post $source The source identified to parse, either a
* post ID integer, a **type**id string or a post object.
*
* @return array An array containing the actual object in 'object' and the
* format of the original value in 'format'. The value can be 'object', 'id'
* or 'id=>parent'.
*/
function relevanssi_get_an_object( $source ) {
$object = $source;
$format = 'object';
if ( ! is_object( $source ) ) {
// Convert from post ID to post.
$object = relevanssi_get_post_object( $source );
$format = 'id';
} elseif ( isset( $source->type ) ) {
// Convert from id=>type to post.
$object = relevanssi_get_post_object( $source->ID );
$format = 'id=>type';
} elseif ( ! isset( $source->post_content ) ) {
// Convert from id=>parent to post.
$object = relevanssi_get_post_object( $source->ID );
$format = 'id=>parent';
}
return array(
'object' => $object,
'format' => $format,
);
}
/**
* Returns the attachment filename suffix.
*
* Reads the filename from $post->guid and returns the file suffix.
*
* @param WP_Post|int $post The post object or post ID.
* @return string The suffix if it is found, an empty string otherwise.
*/
function relevanssi_get_attachment_suffix( $post ): string {
if ( ! is_object( $post ) ) {
$post = relevanssi_get_post( $post );
if ( is_wp_error( $post ) ) {
return '';
}
}
if ( 'attachment' !== $post->post_type ) {
return '';
}
list( , $type ) = explode( '.', basename( $post->guid ) );
return $type;
}
/**
* Returns the locale or language code.
*
* If WPML or Polylang is not available, returns `get_locale()` value. With
* WPML or Polylang, first this function checks to see if the global $post is
* set. If it is, the function returns the language of the post, as we're
* working on a post and need to use the correct language.
*
* If the global $post is not set, this function returns for Polylang the
* results of `pll_current_language()`, for WPML it uses `wpml_current_language`
* and `wpml_active_languages`.
*
* @param boolean $locale If true, return locale; if false, return language
* code.
*
* @return string The locale or the language code.
*/
function relevanssi_get_current_language( bool $locale = true ) {
$current_language = get_locale();
if ( ! $locale ) {
$current_language = substr( $current_language, 0, 2 );
}
if ( class_exists( 'Polylang', false ) ) {
global $post;
if ( isset( $post ) ) {
if ( isset( $post->term_id ) && function_exists( 'pll_get_term_language' ) ) {
$term_language = pll_get_term_language( $post->term_id, $locale ? 'locale' : 'slug' );
if ( $term_language ) {
$current_language = $term_language;
}
} elseif ( ! isset( $post->user_id ) && function_exists( 'pll_get_post_language' ) ) {
$post_language = pll_get_post_language( $post->ID, $locale ? 'locale' : 'slug' );
if ( $post_language ) {
$current_language = $post_language;
}
}
} elseif ( function_exists( 'pll_current_language' ) ) {
$pll_language = pll_current_language( $locale ? 'locale' : 'slug' );
$current_language = $pll_language ? $pll_language : $current_language;
}
}
if ( function_exists( 'icl_object_id' ) && ! function_exists( 'pll_is_translated_post_type' ) ) {
global $post;
if ( isset( $post ) ) {
$language_details = array(
'locale' => '',
'language_code' => '',
);
if ( isset( $post->term_id ) ) {
// Terms don't have a locale, just a language code.
$element = array(
'element_id' => relevanssi_get_term_tax_id( $post->term_id, $post->post_type ),
'element_type' => $post->post_type,
);
$language_code = apply_filters( 'wpml_element_language_code', null, $element );
$language_details['language_code'] = $language_code;
} elseif ( ! isset( $post->user_id ) && 'post_type' !== $post->post_type ) {
// Users don't have language details.
$language_details = apply_filters( 'wpml_post_language_details', null, $post->ID );
}
if ( is_wp_error( $language_details ) ) {
$current_language = apply_filters( 'wpml_current_language', null );
} else {
$current_language = $language_details[ $locale ? 'locale' : 'language_code' ];
}
} elseif ( $locale ) {
$languages = apply_filters( 'wpml_active_languages', null );
foreach ( $languages as $l ) {
if ( $l['active'] ) {
$current_language = $l['default_locale'];
break;
}
}
} else {
$current_language = apply_filters( 'wpml_current_language', null );
}
}
return $current_language;
}
/**
* Gets the permalink to the current post within Loop.
*
* Uses get_permalink() to get the permalink, then adds the 'highlight'
* parameter if necessary using relevanssi_add_highlight().
*
* @param int|WP_Post $post Post ID or post object. Default is the global $post.
*
* @see get_permalink()
*
* @return string The permalink.
*/
function relevanssi_get_permalink( $post = 0 ) {
/**
* Filters the permalink.
*
* @param string The permalink, generated by get_permalink().
*/
$permalink = apply_filters( 'relevanssi_permalink', get_permalink( $post ) );
return $permalink;
}
/**
* Replacement for get_post() that uses the Relevanssi post cache.
*
* Tries to fetch the post from the Relevanssi post cache. If that doesn't work,
* gets the post using get_post().
*
* @param int|string $post_id The post ID. Usually an integer post ID, but can
* also be a string (u_<user ID>, p_<post type name> or
* **<taxonomy>**<term ID>).
* @param int $blog_id The blog ID, default -1. If -1, will be replaced
* with the actual current blog ID from get_current_blog_id().
*
* @return object|WP_Error The post object or a WP_Error object if the post
* doesn't exist.
*/
function relevanssi_get_post( $post_id, int $blog_id = -1 ) {
if ( -1 === $blog_id ) {
$blog_id = get_current_blog_id();
}
if ( function_exists( 'relevanssi_premium_get_post' ) ) {
return relevanssi_premium_get_post( $post_id, $blog_id );
}
global $relevanssi_post_array;
$post = null;
if ( isset( $relevanssi_post_array[ $post_id ] ) ) {
$post = $relevanssi_post_array[ $post_id ];
}
if ( ! $post ) {
$post = get_post( $post_id );
$relevanssi_post_array[ $post_id ] = $post;
}
if ( ! $post ) {
$post = new WP_Error( 'post_not_found', __( 'The requested post does not exist.' ) );
}
return $post;
}
/**
* Fetches post meta value for a large group of posts with just one query.
*
* This function can be used to reduce the number of database queries. Instead
* of looping through an array of posts and calling get_post_meta() for each
* individual post, you can get all the values with this function with just one
* database query.
*
* @param array $post_ids An array of post IDs.
* @param string $field The name of the field.
*
* @return array An array of post_id, meta_value pairs.
*/
function relevanssi_get_post_meta_for_all_posts( array $post_ids, string $field ): array {
global $wpdb;
$post_ids_string = implode( ',', $post_ids );
$meta_values = array();
if ( $post_ids_string ) {
$meta_values = $wpdb->get_results(
$wpdb->prepare(
"SELECT post_id, meta_value FROM $wpdb->postmeta
WHERE meta_key = %s
AND post_id IN ( $post_ids_string )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
$field
)
);
}
$results = array();
foreach ( $meta_values as $row ) {
$results[ $row->post_id ] = $row->meta_value;
}
return $results;
}
/**
* Returns an object based on ID.
*
* Wrapper to handle non-post cases (terms, user profiles). Regular posts are
* passed on to relevanssi_get_post().
*
* @uses relevanssi_get_post() Used to fetch regular posts.
*
* @param int|string $post_id An ID, either an integer post ID or a
* **type**id string for terms and users.
*
* @return WP_Post|WP_Term|WP_User|WP_Error An object, type of which depends on
* the target object. If relevanssi_get_post() doesn't find the post, this
* returns a WP_Error.
*/
function relevanssi_get_post_object( $post_id ) {
$object = null;
if ( '*' === substr( $post_id, 0, 1 ) ) {
// Convert from **type**id to a user or a term object.
$parts = explode( '**', $post_id );
$type = $parts[1] ?? null;
$id = $parts[2] ?? null;
if ( $type && $id ) {
if ( 'user' === $type ) {
$object = get_user_by( 'id', $id );
} else {
$object = get_term( $id, $type );
}
}
} else {
$object = relevanssi_get_post( $post_id );
}
return $object;
}
/**
* Returns the term taxonomy ID for a term based on term ID.
*
* @global object $wpdb The WordPress database interface.
*
* @param int $term_id The term ID.
* @param string $taxonomy The taxonomy.
*
* @return int Term taxonomy ID.
*/
function relevanssi_get_term_tax_id( int $term_id, string $taxonomy ) {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT term_taxonomy_id FROM $wpdb->term_taxonomy WHERE term_id = %d AND taxonomy = %s",
$term_id,
$taxonomy
)
);
}
/**
* Fetches the taxonomy based on term ID.
*
* Fetches the taxonomy from wp_term_taxonomy based on term_id.
*
* @global object $wpdb The WordPress database interface.
*
* @param int $term_id The term ID.
*
* @deprecated Will be removed in future versions.
*
* @return string $taxonomy The term taxonomy.
*/
function relevanssi_get_term_taxonomy( int $term_id ) {
global $wpdb;
$taxonomy = $wpdb->get_var( $wpdb->prepare( "SELECT taxonomy FROM $wpdb->term_taxonomy WHERE term_id = %d", $term_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $taxonomy;
}
/**
* Gets a list of tags for post.
*
* Replacement for get_the_tags() that does the same, but applies Relevanssi
* search term highlighting on the results.
*
* @uses relevanssi_the_tags() Does the actual work.
*
* @param string $before What is printed before the tags, default ''.
* @param string $separator The separator between items, default ', '.
* @param string $after What is printed after the tags, default ''.
* @param int $post_id The post ID. Default current post ID (in the Loop).
*/
function relevanssi_get_the_tags( string $before = '', string $separator = ', ', string $after = '', int $post_id = 0 ) {
return relevanssi_the_tags( $before, $separator, $after, false, $post_id );
}
/**
* Returns the post title with highlighting.
*
* Reads the highlighted title from $post->post_highlighted_title. Uses the
* relevanssi_get_post() to fecth the post.
*
* @uses relevanssi_get_post() Fetches post objects.
*
* @param int|WP_Post $post The post ID or a post object.
*
* @return string The post title with highlights and an empty string, if the
* post cannot be found.
*/
function relevanssi_get_the_title( $post ) {
if ( is_numeric( $post ) ) {
$post = relevanssi_get_post( $post );
}
if ( is_wp_error( $post ) ) {
return '';
}
if ( empty( $post->post_highlighted_title ) ) {
$post->post_highlighted_title = $post->post_title;
}
return $post->post_highlighted_title;
}
/**
* Adds a soft hyphen to a string at every five characters.
*
* @param string $str The string to hyphenate.
*
* @return string The hyphenated string.
*/
function relevanssi_hyphenate( $str ) {
$str = preg_replace( '/([^\s]{8})([^\s])/u', '$1­$2', html_entity_decode( $str ) );
return $str;
}
/**
* Returns an imploded string if the option exists and is an array, an empty
* string otherwise.
*
* @see implode()
*
* @param array $request An array of option values.
* @param string $option The key to check.
* @param string $glue The glue string for implode(), default ','.
*
* @return string Imploded string or an empty string.
*/
function relevanssi_implode( array $request, string $option, string $glue = ',' ) {
if ( isset( $request[ $option ] ) && is_array( $request[ $option ] ) ) {
return implode( $glue, $request[ $option ] );
}
return '';
}
/**
* Increases a value. If it's not set, sets it first to the default value.
*
* @param int $value The value to increase (passed by reference).
* @param int $increase The amount to increase the value, default 1.
* @param int $def_value The default value, default 0.
*/
function relevanssi_increase_value( &$value, $increase = 1, $def_value = 0 ) {
if ( ! isset( $value ) ) {
$value = $def_value;
}
$value += $increase;
}
/**
* Returns the intval of the option if it exists, null otherwise.
*
* @see intval()
*
* @param array $request An array of option values.
* @param string $option The key to check.
*
* @return int|null Integer value of the option, or null.
*/
function relevanssi_intval( array $request, string $option ) {
if ( isset( $request[ $option ] ) ) {
return intval( $request[ $option ] );
}
return null;
}
/**
* Returns true if the search is from Relevanssi Live Ajax Search.
*
* Checks if $wp_query->query_vars['action'] is set to "relevanssi_live_search".
*
* @return bool True if the search is from Relevanssi Live Ajax Search, false
* otherwise.
*/
function relevanssi_is_live_search() {
global $wp_query;
$relevanssi_live_search = false;
if ( isset( $wp_query->query_vars['action'] ) && 'relevanssi_live_search' === $wp_query->query_vars['action'] ) {
$relevanssi_live_search = true;
}
return $relevanssi_live_search;
}
/**
* Checks if a string is a multiple-word phrase.
*
* Replaces hyphens, quotes and ampersands with spaces if necessary based on
* the Relevanssi advanced indexing settings.
*
* @param string $str The string to check.
*
* @return boolean True if the string is a multiple-word phrase, false otherwise.
*/
function relevanssi_is_multiple_words( string $str ): bool {
if ( empty( $str ) ) {
return false;
}
$punctuation = get_option( 'relevanssi_punctuation' );
if ( 'replace' === $punctuation['hyphens'] ) {
$str = str_replace(
array(
'-',
'–',
'—',
),
' ',
$str
);
}
if ( 'replace' === $punctuation['quotes'] ) {
$str = str_replace(
array(
'’',
"'",
'’',
'‘',
'”',
'“',
'„',
'´',
'″',
),
' ',
$str
);
}
if ( 'replace' === $punctuation['ampersands'] ) {
$str = str_replace(
array(
'&',
'&',
'&',
),
' ',
$str
);
}
if ( count( explode( ' ', $str ) ) > 1 ) {
return true;
}
return false;
}
/**
* Launches an asynchronous Ajax action.
*
* Makes a wp_remote_post() call with the specific action. Handles nonce
* verification.
*
* @see wp_remove_post()
* @see wp_create_nonce()
*
* @param string $action The action to trigger (also the name of the
* nonce).
* @param array $payload_args The parameters sent to the action. Defaults to
* an empty array.
*
* @return WP_Error|array The wp_remote_post() response or WP_Error on failure.
*/
function relevanssi_launch_ajax_action( string $action, array $payload_args = array() ) {
$cookies = array();
foreach ( $_COOKIE as $name => $value ) {
$cookies[] = "$name=" . rawurlencode(
is_array( $value ) ? wp_json_encode( $value ) : $value
);
}
$default_payload = array(
'action' => $action,
'_nonce' => wp_create_nonce( $action ),
);
$payload = array_merge( $default_payload, $payload_args );
$args = array(
'timeout' => 0.01,
'blocking' => false,
'body' => $payload,
'headers' => array(
'cookie' => implode( '; ', $cookies ),
),
);
$url = admin_url( 'admin-ajax.php' );
return wp_remote_post( $url, $args );
}
/**
* Returns a legal value.
*
* @param array $request An array of option values.
* @param string $option The key to check.
* @param array $values The legal values.
* @param string $def_val The default value.
*
* @return string|null A legal value or the default value, null if the option
* isn't set.
*/
function relevanssi_legal_value( array $request, string $option, array $values, string $def_val ) {
$value = null;
if ( isset( $request[ $option ] ) ) {
$value = $def_val;
if ( in_array( $request[ $option ], $values, true ) ) {
$value = $request[ $option ];
}
}
return $value;
}
/**
* Multibyte friendly case-insensitive string comparison.
*
* If multibyte string functions are available, do strnatcmp() after using
* mb_strtoupper() to both strings. Otherwise use strnatcasecmp().
*
* @see strnatcasecmp() Falls back to this if multibyte functions are
* not available.
* @see strnatcmp() Used to compare the strings.
* @see mb_strtoupper() Used to convert strings to uppercase.
*
* @param string $str1 First string to compare.
* @param string $str2 Second string to compare.
* @param string $encoding The encoding to use, default mb_internal_encoding().
*
* @return int $val Returns < 0 if str1 is less than str2; > 0 if str1 is
* greater than str2, and 0 if they are equal.
*/
function relevanssi_mb_strcasecmp( $str1, $str2, $encoding = '' ): int {
if ( ! function_exists( 'mb_internal_encoding' ) ) {
return strnatcasecmp( $str1, $str2 );
} else {
if ( empty( $encoding ) ) {
$encoding = mb_internal_encoding();
}
return strnatcmp( mb_strtoupper( $str1, $encoding ), mb_strtoupper( $str2, $encoding ) );
}
}
/**
* Multibyte friendly case-insensitive string search.
*
* If multibyte string functions are available, do mb_stristr(). Otherwise,
* do stristr().
*
* @see stristr() Falls back to this if multibyte functions are not
* available.
*
* @param string $haystack The string to search in.
* @param string $needle The string to search for.
* @param string $encoding The encoding to use, default mb_internal_encoding().
*
* @return bool True if the needle was found in the haystack, false otherwise.
*/
function relevanssi_mb_stristr( $haystack, $needle, $encoding = '' ): bool {
if ( ! function_exists( 'mb_internal_encoding' ) ) {
return stristr( $haystack, $needle );
} else {
if ( empty( $encoding ) ) {
$encoding = mb_internal_encoding();
}
return mb_stristr( $haystack, $needle, false, $encoding );
}
}
/**
* Trims multibyte strings.
*
* Removes the 194+160 non-breakable spaces, removes null bytes and removes
* whitespace.
*
* @param string $str The source string.
*
* @return string Trimmed string.
*/
function relevanssi_mb_trim( string $str ) {
$str = str_replace( chr( 194 ) . chr( 160 ), '', $str );
$str = str_replace( "\0", '', $str );
$str = preg_replace( '/(^\s+)|(\s+$)/us', '', $str );
return $str;
}
/**
* Returns 'on' if option exists and value is not 'off', otherwise 'off'.
*
* @param array $request An array of option values.
* @param string $option The key to check.
*
* @return string 'on' or 'off'.
*/
function relevanssi_off_or_on( array $request, string $option ) {
if ( isset( $request[ $option ] ) && 'off' !== $request[ $option ] ) {
return 'on';
}
return 'off';
}
/**
* Removes quotes (", ”, “) from a string.
*
* @param string $str The string to clean.
*
* @return string The cleaned string.
*/
function relevanssi_remove_quotes( string $str ) {
return str_replace( array( '”', '“', '"' ), '', $str );
}
/**
* Removes quotes from array keys. Does not keep array values.
*
* Used to remove phrase quotes from search term array, which have the format
* of (term => hits). The number of hits is not needed, so this function
* discards it as a side effect.
*
* @uses relevanssi_remove_quotes() This does the actual work.
*
* @param array $arr An array to process.
*
* @return array The same array with quotes removed from the keys.
*/
function relevanssi_remove_quotes_from_array_keys( array $arr ) {
$arr = array_keys( $arr );
array_walk(
$arr,
function ( &$key ) {
$key = relevanssi_remove_quotes( $key );
}
);
return array_flip( $arr );
}
/**
* Returns an ID=>parent object from a post (or a term, or a user).
*
* @param WP_Post|WP_Term|WP_User $post_object The source object.
*
* @return object An object with the attributes ID and post_parent set. For
* terms and users, ID is the term or user ID and post_parent is 0. For bad
* inputs, returns 0 and 0.
*/
function relevanssi_return_id_parent( $post_object ) {
$id_parent_object = new stdClass();
if ( isset( $post_object->ID ) ) {
$id_parent_object->ID = $post_object->ID;
$id_parent_object->post_parent = $post_object->post_parent;
} elseif ( isset( $post_object->term_id ) ) {
$id_parent_object->ID = $post_object->term_id;
$id_parent_object->post_parent = 0;
} elseif ( isset( $post_object->user_id ) ) {
$id_parent_object->ID = $post_object->user_id;
$id_parent_object->post_parent = 0;
} else {
$id_parent_object->ID = 0;
$id_parent_object->post_parent = 0;
}
return $id_parent_object;
}
/**
* Returns an ID=>type object from a post (or a term, or a user).
*
* @param WP_Post|WP_Term|WP_User $post_object The source object.
*
* @return object An object with the attributes ID and type set. Type is
* 'post', 'user', 'term' or 'post_type'. For terms, also fills in 'taxonomy',
* for post types 'name'.
*/
function relevanssi_return_id_type( $post_object ) {
$id_type_object = new stdClass();
if ( isset( $post_object->ID ) ) {
$id_type_object->ID = $post_object->ID;
$id_type_object->type = 'post';
} elseif ( isset( $post_object->term_id ) ) {
$id_type_object->ID = $post_object->term_id;
$id_type_object->type = 'term';
$id_type_object->taxonomy = $post_object->taxonomy;
} elseif ( isset( $post_object->user_id ) ) {
$id_type_object->ID = $post_object->user_id;
$id_type_object->type = 'user';
} else {
$id_type_object->ID = 0;
$id_type_object->post_parent = 0;
}
return $id_type_object;
}
/**
* Returns "off".
*
* Useful for returning "off" to filters easily.
*
* @return string A string with value "off".
*/
function relevanssi_return_off() {
return 'off';
}
/**
* Returns "OR".
*
* @return string A string with value "OR".
*/
function relevanssi_return_or() {
return 'OR';
}
/**
* Gets a post object, returns ID, ID=>parent or the post object.
*
* @uses relevanssi_return_id_type() Used to return ID=>type results.
* @uses relevanssi_return_id_parent() Used to return ID=>parent results.
*
* @param object $post The post object.
* @param string $return_value The value to return, possible values are 'id'
* for returning the ID and 'id=>parent' for returning the ID=>parent object,
* otherwise the post object is returned.
*
* @return int|object|WP_Post The post object in the desired format.
*/
function relevanssi_return_value( $post, string $return_value ) {
if ( 'id' === $return_value ) {
return $post->ID;
} elseif ( 'id=>type' === $return_value ) {
return relevanssi_return_id_type( $post );
} elseif ( 'id=>parent' === $return_value ) {
return relevanssi_return_id_parent( $post );
}
return $post;
}
/**
* Sanitizes hex color strings.
*
* A copy of sanitize_hex_color(), because that isn't always available.
*
* @param string $color A hex color string to sanitize.
*
* @return string Sanitized hex string, or an empty string.
*/
function relevanssi_sanitize_hex_color( string $color ) {
if ( '' === $color ) {
return '';
}
if ( '#' !== substr( $color, 0, 1 ) ) {
$color = '#' . $color;
}
// 3 or 6 hex digits, or the empty string.
if ( preg_match( '|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) ) {
return $color;
}
return '';
}
/**
* Returns 'selected' if the option matches a value.
*
* @param string $option Value to check.
* @param string $value The 'selected' value.
*
* @return string If the option matches the value, returns 'selected', otherwise
* returns an empty string.
*/
function relevanssi_select( string $option, string $value ) {
$selected = '';
if ( $option === $value ) {
$selected = 'selected';
}
return $selected;
}
/**
* Strips all tags from content, keeping non-tags that look like tags.
*
* Strips content that matches <[!a-zA-Z\/]*> to remove HTML tags and HTML
* comments, but not things like "<30 grams, 4>1".
*
* @param string $content The content.
*
* @return string The content with tags stripped.
*/
function relevanssi_strip_all_tags( $content ): string {
if ( ! is_string( $content ) ) {
$content = '';
}
$content = preg_replace( '/<!--.*?-->/ums', '', $content );
$content = preg_replace( '/<[!a-zA-Z\/][^>].*?>/ums', ' ', $content );
return $content ?? '';
}
/**
* Strips invisible elements from text.
*
* Strips <style>, <script>, <object>, <embed>, <applet>, <noscript>, <noembed>,
* <iframe> and <del> tags and their contents and comments from the text.
*
* @param string $text The source text.
*
* @return string The processed text.
*/
function relevanssi_strip_invisibles( $text ) {
if ( ! is_string( $text ) ) {
$text = strval( $text );
}
$text = preg_replace(
array(
'@<style[^>]*?>.*?</style>@siu',
'@<script[^>]*?.*?</script>@siu',
'@<object[^>]*?.*?</object>@siu',
'@<embed[^>]*?.*?</embed>@siu',
'@<applet[^>]*?.*?</applet>@siu',
'@<noscript[^>]*?.*?</noscript>@siu',
'@<noembed[^>]*?.*?</noembed>@siu',
'@<iframe[^>]*?.*?</iframe>@siu',
'@<del[^>]*?.*?</del>@siu',
'@<!--.*?-->@siu',
),
' ',
$text
);
return $text;
}
/**
* Strips tags from contents, keeping the allowed tags.
*
* The allowable tags are read from the relevanssi_excerpt_allowable_tags
* option. Relevanssi also adds extra spaces after some tags to make sure words
* are not stuck together after the tags are removed. The function also removes
* invisible content.
*
* @uses relevanssi_strip_invisibles() Used to remove scripts and other tags.
* @see strip_tags() Used to remove tags.
*
* @param string|null $content The content.
*
* @return string The content without tags.
*/
function relevanssi_strip_tags( $content ) {
if ( ! is_string( $content ) ) {
$content = strval( $content );
}
$content = relevanssi_strip_invisibles( $content );
$space_tags = array(
'/(<\/?p.*?>)/',
'/(<\/?br.*?>)/',
'/(<\/?h[1-6].*?>)/',
'/(<\/?div.*?>)/',
'/(<\/?blockquote.*?>)/',
'/(<\/?hr.*?>)/',
'/(<\/?li.*?>)/',
'/(<img.*?>)/',
'/(<\/td>)/',
);
$content = preg_replace( $space_tags, '$1 ', $content );
return strip_tags(
$content,
get_option( 'relevanssi_excerpt_allowable_tags', '' )
);
}
/**
* Returns the position of substring in the string.
*
* Uses mb_stripos() if possible, falls back to mb_strpos() and mb_strtoupper()
* if that cannot be found, and falls back to just strpos() if even that is not
* possible.
*
* @param string $haystack String where to look.
* @param string $needle The string to look for.
* @param int $offset Where to start, default 0.
*
* @return mixed False, if no result or $offset outside the length of $haystack,
* otherwise the position (which can be non-false 0!).
*/
function relevanssi_stripos( $haystack, $needle, int $offset = 0 ) {
if ( ! is_string( $haystack ) ) {
$haystack = strval( $haystack );
}
if ( ! is_string( $needle ) ) {
$needle = strval( $needle );
}
if ( $offset > relevanssi_strlen( $haystack ) ) {
return false;
}
if ( preg_match( '/[\?\*]/', $needle ) ) {
// There's a ? or an * in the string, which means it's a wildcard search
// query (a Premium feature) and requires some extra steps.
$needle_regex = str_replace(
array( '?', '*' ),
array( '.', '.*' ),
preg_quote( $needle, '/' )
);
$pos_found = false;
while ( ! $pos_found ) {
preg_match(
"/$needle_regex/i",
$haystack,
$matches,
PREG_OFFSET_CAPTURE,
$offset
);
/**
* This trickery is necessary, because PREG_OFFSET_CAPTURE gives
* wrong offsets for multibyte strings. The mb_strlen() gives the
* correct offset, the rest of this is because the $offset received
* as a parameter can be before the first $position, leading to an
* infinite loop.
*/
$pos = isset( $matches[0][1] )
? mb_strlen( substr( $haystack, 0, $matches[0][1] ) )
: false;
if ( $pos && $pos > $offset ) {
$pos_found = true;
} elseif ( $pos ) {
++$offset;
} else {
$pos_found = true;
}
}
} elseif ( function_exists( 'mb_stripos' ) ) {
if ( '' === $haystack ) {
$pos = false;
} else {
$pos = mb_stripos( $haystack, $needle, $offset );
}
} elseif ( function_exists( 'mb_strpos' ) && function_exists( 'mb_strtoupper' ) && function_exists( 'mb_substr' ) ) {
$pos = mb_strpos(
mb_strtoupper( $haystack ),
mb_strtoupper( $needle ),
$offset
);
} else {
$pos = strpos( strtoupper( $haystack ), strtoupper( $needle ), $offset );
}
return $pos;
}
/**
* Returns the length of the string.
*
* Uses mb_strlen() if available, otherwise falls back to strlen().
*
* @param string $s The string to measure.
*
* @return int The length of the string.
*/
function relevanssi_strlen( $s ) {
if ( ! is_string( $s ) ) {
$s = strval( $s );
}
if ( function_exists( 'mb_strlen' ) ) {
return mb_strlen( $s );
}
return strlen( $s );
}
/**
* Multibyte friendly strtolower.
*
* If multibyte string functions are available, returns mb_strtolower() and
* falls back to strtolower() if multibyte functions are not available.
*
* @param string $str The string to lowercase.
*
* @return string $str The string in lowercase.
*/
function relevanssi_strtolower( $str ) {
if ( ! is_string( $str ) ) {
$str = strval( $str );
}
if ( ! function_exists( 'mb_strtolower' ) ) {
return strtolower( $str );
} else {
return mb_strtolower( $str );
}
}
/**
* Multibyte friendly substr.
*
* If multibyte string functions are available, returns mb_substr() and falls
* back to substr() if multibyte functions are not available.
*
* @param string $str The source string.
* @param int $start If start is non-negative, the returned string will
* start at the start'th position in str, counting from zero. If start is
* negative, the returned string will start at the start'th character from the
* end of string.
* @param int|null $length Maximum number of characters to use from string. If
* omitted or null is passed, extract all characters to the end of the string.
*
* @return string $str The string in lowercase.
*/
function relevanssi_substr( $str, int $start, $length = null ) {
if ( ! is_string( $str ) ) {
$str = strval( $str );
}
if ( ! function_exists( 'mb_substr' ) ) {
return substr( $str, $start, $length );
} else {
return mb_substr( $str, $start, $length );
}
}
/**
* Prints out the post excerpt.
*
* Prints out the post excerpt from $post->post_excerpt, unless the post is
* protected. Only works in the Loop.
*
* @see post_password_required() Used to check for password requirements.
*
* @global $post The global post object.
*/
function relevanssi_the_excerpt() {
global $post;
if ( ! post_password_required( $post ) ) {
echo '<p>' . $post->post_excerpt . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
esc_html_e( 'There is no excerpt because this is a protected post.', 'relevanssi' );
}
}
/**
* Echoes out the permalink to the current post within Loop.
*
* Uses get_permalink() to get the permalink, then adds the 'highlight'
* parameter if necessary using relevanssi_add_highlight(), then echoes it out.
*
* @param int|WP_Post $post Post ID or post object. Default is the global $post.
*
* @uses relevanssi_get_permalink() Fetches the current post permalink.
*/
function relevanssi_the_permalink( $post = 0 ) {
echo esc_url( relevanssi_get_permalink( $post ) );
}
/**
* Prints out a list of tags for post.
*
* Replacement for the_tags() that does the same, but applies Relevanssi search term
* highlighting on the results.
*
* @param string $before What is printed before the tags, default ''.
* @param string $separator The separator between items, default ', '.
* @param string $after What is printed after the tags, default ''.
* @param boolean $echoed If true, echo, otherwise return the result. Default true.
* @param int $post_id The post ID. Default current post ID (in the Loop).
*/
function relevanssi_the_tags( string $before = '', string $separator = ', ', string $after = '', bool $echoed = true, int $post_id = 0 ) {
$tag_list = get_the_tag_list( $before, $separator, $after, $post_id );
$found = preg_match_all( '~<a href=".*?" rel="tag">(.*?)</a>~', $tag_list, $matches );
if ( $found ) {
$originals = $matches[0];
$tag_names = $matches[1];
$highlighted = array();
$count = count( $matches[0] );
for ( $i = 0; $i < $count; $i++ ) {
$highlighted_tag_name = relevanssi_highlight_terms( $tag_names[ $i ], get_search_query(), true );
$highlighted[ $i ] = str_replace( '>' . $tag_names[ $i ] . '<', '>' . $highlighted_tag_name . '<', $originals[ $i ] );
}
$tag_list = str_replace( $originals, $highlighted, $tag_list );
}
if ( $echoed ) {
echo $tag_list; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
return $tag_list;
}
}
/**
* Prints out post title with highlighting.
*
* Uses the global $post object. Reads the highlighted title from
* $post->post_highlighted_title. This used to accept one parameter, the
* `$echo` boolean, but in 2.12.3 / 4.10.3 the function signature was matched
* to copy `the_title()` function in WordPress core. The original behaviour is
* still supported: `relevanssi_the_title()` without arguments works exactly as
* before and `relevanssi_the_title( false )` returns the title.
*
* @global object $post The global post object.
*
* @param boolean|string $before Markup to prepend to the title. Can also be a
* boolean for whether to echo or return the title.
* @param string $after Markup to append to the title.
* @param boolean $echoed Whether to echo or return the title. Default
* true for echo.
*
* @return void|string Void if $echoed argument is true, current post title with
* highlights if $echoed is false.
*/
function relevanssi_the_title( $before = true, string $after = '', bool $echoed = true ) {
if ( true === $before ) {
$before = '';
$echoed = true;
} elseif ( false === $before ) {
$before = '';
$echoed = false;
}
global $post;
if ( empty( $post->post_highlighted_title ) ) {
$post->post_highlighted_title = $post->post_title;
}
if ( relevanssi_strlen( $post->post_highlighted_title ) === 0 ) {
return;
}
$title = $before . $post->post_highlighted_title . $after;
if ( $echoed ) {
echo $title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
return $title;
}
}
/**
* Turns off options, ie. sets them to "off".
*
* If the specified options don't exist in the request array, they are set to
* "off".
*
* @param array $request The _REQUEST array, passed as reference.
* @param array $options An array of option names.
*/
function relevanssi_turn_off_options( array &$request, array $options ) {
array_walk(
$options,
function ( $option ) use ( &$request ) {
if ( ! isset( $request[ $option ] ) ) {
$request[ $option ] = 'off';
}
}
);
}
/**
* Sets an option after doing floatval.
*
* @param array $request An array of option values.
* @param string $option The key to check.
* @param boolean $autoload Should the option autoload, default true.
* @param float $def_val The default value if floatval() fails, default 0.
* @param boolean $positive If true, replace negative values and zeroes with
* $def_val.
*/
function relevanssi_update_floatval( array $request, string $option, bool $autoload = true, float $def_val = 0, bool $positive = false ) {
if ( isset( $request[ $option ] ) ) {
$value = floatval( $request[ $option ] );
if ( ! $value ) {
$value = $def_val;
}
if ( $positive && $value <= 0 ) {
$value = $def_val;
}
update_option( $option, $value, $autoload );
}
}
/**
* Sets an option after doing intval.
*
* @param array $request An array of option values.
* @param string $option The key to check.
* @param boolean $autoload Should the option autoload, default true.
* @param int $def_val The default value if intval() fails, default 0.
*/
function relevanssi_update_intval( array $request, string $option, bool $autoload = true, int $def_val = 0 ) {
if ( isset( $request[ $option ] ) ) {
$value = intval( $request[ $option ] );
if ( ! $value ) {
$value = $def_val;
}
update_option( $option, $value, $autoload );
}
}
/**
* Sets an option with one of the listed legal values.
*
* @param array $request An array of option values.
* @param string $option The key to check.
* @param array $values The legal values.
* @param string $def_val The default value.
* @param boolean $autoload Should the option autoload, default true.
*/
function relevanssi_update_legal_value( array $request, string $option, array $values, string $def_val, bool $autoload = true ) {
if ( isset( $request[ $option ] ) ) {
$value = $def_val;
if ( in_array( $request[ $option ], $values, true ) ) {
$value = $request[ $option ];
}
update_option( $option, $value, $autoload );
}
}
/**
* Sets an on/off option according to the request value.
*
* @param array $request An array of option values.
* @param string $option The key to check.
* @param boolean $autoload Should the option autoload, default true.
*/
function relevanssi_update_off_or_on( array $request, string $option, bool $autoload = true ) {
relevanssi_update_legal_value(
$request,
$option,
array( 'off', 'on' ),
'off',
$autoload
);
}
/**
* Sets an option after sanitizing and unslashing the value.
*
* @param array $request An array of option values.
* @param string $option The key to check.
* @param boolean $autoload Should the option autoload, default true.
*/
function relevanssi_update_sanitized( array $request, string $option, bool $autoload = true ) {
if ( isset( $request[ $option ] ) ) {
$value = sanitize_text_field( wp_unslash( $request[ $option ] ) );
update_option( $option, $value, $autoload );
}
}
/**
* Returns true if $_SERVER['HTTP_USER_AGENT'] is on the bot block list.
*
* Looks for bot user agents in the $_SERVER['HTTP_USER_AGENT'] and returns true
* if a match is found.
*
* @return bool True if $_SERVER['HTTP_USER_AGENT'] is a bot.
*/
function relevanssi_user_agent_is_bot(): bool {
if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
/**
* Filters the bots Relevanssi should block from search queries.
*
* Lets you filter the bots that are blocked from Relevanssi search
* queries.
*
* @param array $bots An array of bot user agents.
*/
$bots = apply_filters( 'relevanssi_bots_to_block', relevanssi_bot_block_list() );
foreach ( array_values( $bots ) as $lookfor ) {
if ( false !== stristr( $_SERVER['HTTP_USER_AGENT'], $lookfor ) ) {
return true;
}
}
}
return false;
}
/**
* Validates that the parameter is a valid taxonomy type.
*
* @parameter string $taxonomy The taxonomy to validate.
*
* @return string The validated taxonomy, empty string if invalid.
*/
function relevanssi_validate_taxonomy( $taxonomy ) {
$taxonomy = sanitize_text_field( $taxonomy );
if ( taxonomy_exists( $taxonomy ) ) {
return $taxonomy;
}
return '';
}
|