query(
$wpdb->prepare(
"INSERT IGNORE INTO {$relevanssi_variables['tracking_table']} " . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
'(`post_id`, `query`, `rank`, `page`, `timestamp`) VALUES (%d, %s, %d, %d, %s)',
$post->ID,
$rt['query'],
$rt['rank'],
$rt['page'],
gmdate( 'c', $rt['time'] )
)
);
}
/**
* Extracts the values from the _rt URL parameter.
*
* @param string $rt The URL parameter.
*
* @return array|WP_Error An array of values: 'rank', 'page', 'query', and
* 'time'. Returns a WP_Error if the value doesn't explode into right number of
* parts.
*/
function relevanssi_extract_rt( string $rt ) {
$rt_values = explode( '|', $rt );
if ( count( $rt_values ) !== 4 ) {
return new WP_Error( 'invalid-rt', __( 'Invalid click tracking value format.', 'relevanssi' ) );
}
$rank = intval( $rt_values[0] );
$page = intval( $rt_values[1] );
$time = intval( $rt_values[3] );
if ( 0 === $rank || 0 === $page || 0 === $time ) {
return new WP_Error( 'invalid-rt', __( 'Invalid click tracking value format.', 'relevanssi' ) );
}
return array(
'rank' => $rank,
'page' => $page,
'query' => $rt_values[2],
'time' => $time,
);
}
/**
* Adds tracking information to a permalink.
*
* Called from the `relevanssi_permalink` filter function to add the tracking
* data to the link.
*
* @param string $permalink The permalink to modify.
* @param object $link_post A post object, default null in which case the global
* $post is used.
*
* @global $relevanssi_tracking_positions An array of post ID => rank pairs used
* to get the post rankings. If a post does not appear in this array, the
* tracking data is not added to the permalink.
* @global $relevanssi_tracking_permalink A cache of permalinks to avoid doing
* work that is already done.
*
* @return string The modified permalink.
*/
function relevanssi_add_tracking( string $permalink, $link_post = null ): string {
if ( 'on' !== get_option( 'relevanssi_click_tracking', 'off' ) ) {
return $permalink;
}
if ( empty( get_search_query() ) ) {
return $permalink;
}
if ( ! relevanssi_is_ok_to_log() ) {
return $permalink;
}
if ( ! $link_post ) {
global $post;
$link_post = $post;
}
$id = relevanssi_get_post_identifier( $link_post );
if ( ! isset( $link_post->blog_id ) || get_current_blog_id() === $link_post->blog_id ) {
if ( relevanssi_is_front_page_id( $link_post->ID ) ) {
return $permalink;
}
}
global $relevanssi_tracking_positions, $relevanssi_tracking_permalink;
$position = $relevanssi_tracking_positions[ $id ] ?? null;
if ( ! $position ) {
return $permalink;
}
if ( isset( $relevanssi_tracking_permalink[ $id ] ) ) {
return $relevanssi_tracking_permalink[ $id ];
}
$page = get_query_var( 'paged' ) > 0 ? get_query_var( 'paged' ) : 1;
$nonce = wp_create_nonce( 'relevanssi_click_tracking_' . $id );
$query = relevanssi_strtolower( str_replace( '|', ' ', get_search_query() ) );
$time = time();
$value = "$position|$page|$query|$time";
$permalink = esc_attr(
add_query_arg(
array(
'_rt' => relevanssi_base64url_encode( $value ),
'_rt_nonce' => $nonce,
),
$permalink
)
);
$relevanssi_tracking_permalink[ $id ] = $permalink;
return $permalink;
}
/**
* URL-friendly base64 encode.
*
* @param string $data String to encode.
* @return string Encoded string.
*/
function relevanssi_base64url_encode( string $data ): string {
return rtrim(
strtr(
base64_encode( $data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
'+/',
'-_'
),
'='
);
}
/**
* URL-friendly base64 decode.
*
* @param string $data String to decode.
* @return string Decoded string.
*/
function relevanssi_base64url_decode( string $data ): string {
return base64_decode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
strtr( $data, '-_', '+/' )
);
}
/**
* Records the ranking positions for the posts found.
*
* Runs as the last thing (at PHP_INT_MAX) on the `relevanssi_hits_filter` hook
* to record the ranking positions of each post.
*
* @global array $relevanssi_tracking_positions An array of post ID => rank
* pairs.
*
* @param array $hits The hits found.
*
* @return array The hits found, unmodified.
*/
function relevanssi_record_positions( array $hits ): array {
global $relevanssi_tracking_positions;
$position = 0;
foreach ( $hits[0] as $hit ) {
++$position;
$hit = relevanssi_get_an_object( $hit )['object'];
if ( ! $hit ) {
continue;
}
if ( $hit->ID > 0 ) {
$id = relevanssi_get_post_identifier( $hit );
$relevanssi_tracking_positions[ $id ] = $position;
} elseif ( isset( $hit->term_id ) ) {
$relevanssi_tracking_positions[ $hit->post_type . '_' . $hit->term_id ] = $position;
} elseif ( isset( $hit->user_id ) ) {
$relevanssi_tracking_positions[ 'user_' . $hit->user_id ] = $position;
}
}
return $hits;
}
/**
* Removes the undisplayed posts from the $relevanssi_tracking_positions array.
*
* Goes through the $relevanssi_tracking_positions array and only keeps the
* posts that appear on the current page of results.
*
* @global array $relevanssi_tracking_positions An array of post ID => rank
* pairs.
*
* @param array $hits The hits displayed.
*
* @return array The hits displayed, unmodified.
*/
function relevanssi_current_page_hits( array $hits ): array {
global $relevanssi_tracking_positions;
$all_positions = $relevanssi_tracking_positions;
$relevanssi_tracking_positions = array();
foreach ( $hits as $hit ) {
$hit = relevanssi_get_an_object( $hit )['object'];
$id = relevanssi_get_post_identifier( $hit );
if ( $hit->ID > 0 ) {
$relevanssi_tracking_positions[ $id ] = $all_positions[ $id ];
} elseif ( isset( $hit->term_id ) ) {
$id = $hit->post_type . '_' . $hit->term_id;
$relevanssi_tracking_positions[ $id ] = $all_positions[ $id ];
} elseif ( isset( $hit->user_id ) ) {
$id = 'user_' . $hit->user_id;
$relevanssi_tracking_positions[ $id ] = $all_positions[ $id ];
}
}
return $hits;
}
/**
* Creates the tracking table.
*
* @param string $charset_collate Character set collation.
*
* @return void
*/
function relevanssi_create_tracking_table( string $charset_collate ) {
global $wpdb;
$sql = 'CREATE TABLE ' . $wpdb->prefix . 'relevanssi_tracking ' .
"(`id` int(11) NOT NULL AUTO_INCREMENT,
`post_id` int(11) NOT NULL DEFAULT '0',
`query` varchar(200) NOT NULL,
`rank` int(11) NOT NULL DEFAULT '0',
`page` int(11) NOT NULL DEFAULT '0',
`timestamp` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY id (id),
UNIQUE INDEX post_id_timestamp (post_id, timestamp)) $charset_collate";
dbDelta( $sql );
}
/**
* Generates an array with date indices and 0 values for each date.
*
* Uses the `relevanssi_trim_click_logs` option to determine the length of the
* date range.
*
* @param string $type The type of date count: 'clicks', 'log' or 'both'.
*
* @return array An array of 'Y-m-d' date indices.
*/
function relevanssi_default_date_count( string $type ): array {
global $wpdb, $relevanssi_variables;
if ( 'clicks' === $type ) {
$amount_of_days = get_option( 'relevanssi_trim_click_logs', 90 );
}
if ( 'log' === $type ) {
$amount_of_days = get_option( 'relevanssi_trim_logs', 30 );
if ( 0 === $amount_of_days ) {
$amount_of_days = abs( $wpdb->get_var( "SELECT TIMESTAMPDIFF(DAY, NOW(), time) FROM {$relevanssi_variables['log_table']} ORDER BY time ASC LIMIT 1" ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared.
}
}
if ( 'both' === $type ) {
$click_days = get_option( 'relevanssi_trim_click_logs', 90 );
$log_days = get_option( 'relevanssi_trim_logs', 30 );
if ( '0' === $log_days ) {
$log_days = abs( $wpdb->get_var( "SELECT TIMESTAMPDIFF(DAY, NOW(), time) FROM {$relevanssi_variables['log_table']} ORDER BY time ASC LIMIT 1" ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared.
}
$amount_of_days = max( $click_days, $log_days );
}
$date_counts = array();
$start_date = gmdate( 'Y-m-d', strtotime( intval( $amount_of_days ) . ' days ago' ) );
$end_date = gmdate( 'Y-m-d' );
while ( strtotime( $start_date ) <= strtotime( $end_date ) ) {
$date_counts[ $start_date ] = 0;
$start_date = gmdate(
'Y-m-d',
strtotime( '+1 days', strtotime( $start_date ) )
);
}
return $date_counts;
}
/**
* Determines what happens when a request for a post insights screen is made.
*
* @param array $request The $_REQUEST array to dig for parameters.
*
* @return bool True, if a screen was displayed and false if not.
*/
function relevanssi_handle_insights_screens( array $request ): bool {
if ( isset( $request['insights'] ) ) {
if ( isset( $request['action'] ) && isset( $request['query'] ) && 'delete_query' === $request['action'] ) {
check_admin_referer( 'relevanssi_delete_query' );
relevanssi_delete_query( $request['query'] );
}
if ( isset( $request['action'] ) && isset( $request['query'] ) && 'delete_query_from_log' === $request['action'] ) {
check_admin_referer( 'relevanssi_delete_query' );
relevanssi_delete_query_from_log( $request['query'] );
}
relevanssi_show_insights( stripslashes( $request['insights'] ) );
return true;
}
if ( isset( $request['post_insights'] ) ) {
relevanssi_show_post_insights( $request['post_insights'] );
return true;
}
return false;
}
/**
* Deletes a query from the click tracking database.
*
* @param string $query The query to delete.
*/
function relevanssi_delete_query( string $query ) {
global $wpdb, $relevanssi_variables;
$deleted = $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$relevanssi_variables['tracking_table']} WHERE query = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
stripslashes( $query )
)
);
if ( $deleted ) {
printf(
"
",
sprintf(
// Translators: %s is the stopword.
esc_html__(
"The query '%s' deleted from the click tracking log.",
'relevanssi'
),
esc_html( stripslashes( $query ) )
)
);
} else {
printf(
"",
sprintf(
// Translators: %s is the stopword.
esc_html__(
"Couldn't remove the query '%s' from the click tracking log.",
'relevanssi'
),
esc_html( stripslashes( $query ) )
)
);
}
}
/**
* Displays the search query insights screen.
*
* Prints out the display for a single search query insights screen.
*
* @param string $query The search query.
*/
function relevanssi_show_insights( string $query ) {
global $wpdb, $relevanssi_variables;
?>
' .
// Translators: %s is the search query string.
esc_html__( 'Search insights for %s', 'relevanssi' ) .
'',
esc_html( '"' . $query . '"' )
);
$results = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM ' . $relevanssi_variables['tracking_table'] // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
. ' WHERE query = %s',
$query
)
);
$posts = array();
$post_average_rank = array();
$post_average_page = array();
$date_counts = relevanssi_default_date_count( 'both' );
$oldest_date = array_keys( $date_counts )[0];
foreach ( $results as $row ) {
if ( $row->timestamp < $oldest_date ) {
continue;
}
relevanssi_increase_value( $posts[ $row->post_id ] );
relevanssi_increase_value( $post_average_rank[ $row->post_id ], $row->rank );
relevanssi_increase_value( $post_average_page[ $row->post_id ], $row->page );
relevanssi_increase_value( $date_counts[ gmdate( 'Y-m-d', strtotime( $row->timestamp ) ) ] );
}
relevanssi_average_array( $post_average_rank, $posts );
relevanssi_average_array( $post_average_page, $posts );
$results = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM ' . $relevanssi_variables['log_table'] // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
. ' WHERE query = %s',
$query
)
);
$date_numbers = relevanssi_default_date_count( 'both' );
foreach ( $results as $row ) {
relevanssi_increase_value( $date_numbers[ gmdate( 'Y-m-d', strtotime( $row->time ) ) ] );
}
ksort( $date_numbers );
arsort( $posts );
$dates_array = $date_counts + $date_numbers;
ksort( $dates_array );
$dates = array_map(
function ( $v ) {
return gmdate( 'M j', strtotime( $v ) );
},
array_keys( $dates_array )
);
relevanssi_create_line_chart(
$dates,
array(
__( '# of Searches', 'relevanssi' ) => array_values( $date_numbers ),
__( '# of Clicks', 'relevanssi' ) => array_values( $date_counts ),
)
);
if ( count( $posts ) > 0 ) {
?>
|
|
|
|
$count ) {
$insights_url = relevanssi_get_insights_url( intval( $post_id ) );
$insights = sprintf( "%s", esc_url( $insights_url ), get_the_title( $post_id ) );
$link = get_permalink( $post_id );
$edit = get_edit_post_link( $post_id );
?>
|
(
| ) |
|
|
|
' .
// Translators: %s is the search query string.
esc_html__( 'Search insights for %s', 'relevanssi' ) .
'',
esc_html( '"' . $title . '"' )
);
$link = get_permalink( $post_id );
$edit = get_edit_post_link( $post_id );
?>
|
get_results(
$wpdb->prepare(
'SELECT * FROM ' . $relevanssi_variables['tracking_table'] // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
. ' WHERE post_id = %d',
$post_id
)
);
$queries = array();
$query_average_rank = array();
$query_average_page = array();
$date_counts = relevanssi_default_date_count( 'clicks' );
foreach ( $results as $row ) {
relevanssi_increase_value( $queries[ $row->query ] );
relevanssi_increase_value( $query_average_rank[ $row->query ], $row->rank );
relevanssi_increase_value( $query_average_page[ $row->query ], $row->page );
relevanssi_increase_value( $date_counts[ gmdate( 'Y-m-d', strtotime( $row->timestamp ) ) ] );
}
relevanssi_average_array( $query_average_rank, $queries );
relevanssi_average_array( $query_average_page, $queries );
arsort( $queries );
$dates = array_map(
function ( $v ) {
return gmdate( 'M j', strtotime( $v ) );
},
array_keys( $date_counts )
);
relevanssi_create_line_chart(
$dates,
array(
__( '# of Clicks', 'relevanssi' ) => array_values( $date_counts ),
)
);
?>
|
|
|
|
$count ) {
$insights_url = relevanssi_get_insights_url( $query );
$insights = sprintf( "%s", esc_url( $insights_url ), $query );
?>
|
|
|
|
get_results(
'SELECT LOWER(query) AS query, COUNT(*) AS count '
. "FROM {$relevanssi_variables['tracking_table']} " // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
. 'GROUP BY query'
);
$relevanssi_variables['query_clicks'] = array_combine(
wp_list_pluck( $data, 'query' ),
wp_list_pluck( $data, 'count' )
);
return $relevanssi_variables['query_clicks'][ $query ] ?? 0;
}
/**
* Prints out the user interface for setting the click tracking options.
*/
function relevanssi_click_tracking_interface() {
$click_tracking = relevanssi_check( get_option( 'relevanssi_click_tracking' ) );
$trim_click_logs = get_option( 'relevanssi_trim_click_logs' );
?>
get_var(
$wpdb->prepare(
'SELECT COUNT(*) FROM ' . $click_table . ' ' . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
'WHERE timestamp >= %s AND timestamp <= %s',
$from . ' 00:00:00',
$to . ' 23:59:59'
)
);
$click_ratio = 0;
if ( $total > 0 ) {
$click_ratio = round( 100 * $total_clicks / $total, 1 );
}
?>
get_results(
$wpdb->prepare(
"SELECT post_id, COUNT(*) AS hits, AVG(`rank`) AS average
FROM $click_table " . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
'WHERE timestamp >= %s AND timestamp <= %s
GROUP BY post_id ORDER BY hits DESC',
$from . ' 00:00:00',
$to . ' 23:59:59'
)
);
$top_ten = array_slice( $results, 0, 10 );
$list = array();
foreach ( $top_ten as $result ) {
$title = get_the_title( $result->post_id );
$insights_url = relevanssi_get_insights_url( intval( $result->post_id ) );
$list[] = ''
. wp_kses_post( $title ) . ' (' . intval( $result->hits )
. ')';
}
if ( count( $list ) > 0 ) {
?>
' . implode( "\n", $list ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$something_printed = true;
?>
average - $a->average;
}
);
$top_ten = array_slice( $results, 0, 10 );
$list = array();
foreach ( $top_ten as $result ) {
$title = get_the_title( $result->post_id );
$insights_url = relevanssi_get_insights_url( intval( $result->post_id ) );
$list[] = '' . wp_kses_post( $title ) . ' ('
. round( $result->average, 0 ) . ')';
}
if ( count( $list ) > 0 ) {
?>
' . implode( "\n", $list ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$something_printed = true;
?>
get_results(
$wpdb->prepare(
"SELECT query, COUNT(DISTINCT(post_id)) AS posts
FROM $click_table " . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
'WHERE timestamp >= %s AND timestamp <= %s
GROUP BY query ORDER BY posts DESC LIMIT 10',
$from . ' 00:00:00',
$to . ' 23:59:59'
)
);
$list = array();
foreach ( $results as $result ) {
if ( $result->posts < 3 ) {
continue;
}
$insights_url = relevanssi_get_insights_url( $result->query );
$list[] = ''
. esc_html( $result->query ) . ' ('
// Translators: %1$s is the number of posts.
. sprintf( __( '%1$s posts', 'relevanssi' ), intval( $result->posts ) )
. ')';
}
if ( count( $list ) > 0 ) {
?>
' . implode( "\n", $list ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$something_printed = true;
}
if ( ! $something_printed ) {
?>
query.
*
* @return string The HTML link tag to link to the insights page.
*/
function relevanssi_insights_link( $query ): string {
global $relevanssi_variables;
$insights_url = admin_url( 'admin.php?page=' . rawurlencode( $relevanssi_variables['plugin_basename'] ) )
. '&insights=' . rawurlencode( $query->query );
$insights = sprintf( "%s", esc_url( $insights_url ), esc_html( relevanssi_hyphenate( $query->query ) ) );
return $insights;
}
/**
* Trims Relevanssi click tracking table.
*
* Trims Relevanssi click tracking table, using the day interval setting from
* 'relevanssi_trim_click_logs'.
*
* @global object $wpdb The WordPress database interface.
* @global array $relevanssi_variables The global Relevanssi variables, used
* for database table names.
*
* @return int|bool Number of rows deleted, or false on error.
*/
function relevanssi_trim_click_logs() {
global $wpdb, $relevanssi_variables;
$interval = intval( get_option( 'relevanssi_trim_click_logs' ) );
return $wpdb->query(
$wpdb->prepare(
'DELETE FROM ' . $relevanssi_variables['tracking_table'] . // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
' WHERE timestamp < TIMESTAMP(DATE_SUB(NOW(), INTERVAL %d DAY))',
$interval
)
);
}
/**
* Sets up the Relevanssi click tracking log trimming action.
*/
function relevanssi_schedule_click_tracking_trim() {
if ( get_option( 'relevanssi_trim_click_logs' ) > 0 ) {
if ( ! wp_next_scheduled( 'relevanssi_trim_click_logs' ) ) {
wp_schedule_event( time(), 'daily', 'relevanssi_trim_click_logs' );
}
} elseif ( wp_next_scheduled( 'relevanssi_trim_click_logs' ) ) {
wp_clear_scheduled_hook( 'relevanssi_trim_click_logs' );
}
}
/**
* Prints out the Relevanssi click tracking log as a CSV file.
*
* Exports the whole Relevanssi click tracking log as a CSV file.
*
* @uses relevanssi_output_exported_log
*/
function relevanssi_export_click_log() {
global $wpdb, $relevanssi_variables;
$data = $wpdb->get_results( 'SELECT * FROM ' . $relevanssi_variables['tracking_table'], ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
relevanssi_output_exported_log(
'relevanssi_click_log.csv',
$data,
__( 'No search clicks logged.', 'relevanssi' )
);
}
/**
* Returns the post ID prefixed with the blog ID.
*
* @param object $post_object The post object.
*
* @return string Post ID or "blog ID-post ID".
*/
function relevanssi_get_post_identifier( $post_object ) {
if ( is_multisite() ) {
if ( isset( $post_object->blog_id ) ) {
return $post_object->blog_id . '-' . $post_object->ID;
} else {
return get_current_blog_id() . '-' . $post_object->ID;
}
} else {
return $post_object->ID;
}
}