| Server IP : 207.135.97.11 / Your IP : 172.19.0.1 Web Server : LiteSpeed System : Linux 6d372a2d2e33 5.14.0-611.24.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Jan 23 11:42:43 UTC 2026 x86_64 User : nobody ( 65534) PHP Version : 8.3.30 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/html/ed.net.au/wp-content/plugins/post-views-counter/includes/ |
Upload File : |
<?php
// exit if accessed directly
if ( ! defined( 'ABSPATH' ) )
exit;
/**
* Post_Views_Counter_Traffic_Signals class.
*
* @class Post_Views_Counter_Traffic_Signals
*/
class Post_Views_Counter_Traffic_Signals {
/**
* Class constructor.
*
* @return void
*/
public function __construct() {
// early exit if traffic signals are disabled
if ( ! $this->is_enabled() )
return;
// actions
add_action( 'admin_init', [ $this, 'register_signals_column' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_signals_assets' ] );
add_action( 'pvc_after_update_post_views_count', [ $this, 'invalidate_signal_cache' ], 10, 1 );
add_action( 'pvc_flush_cached_counts', [ $this, 'flush_all_signals_cache' ] );
add_action( 'pvc_reset_counts', [ $this, 'flush_all_signals_cache' ] );
add_action( 'transition_post_status', [ $this, 'invalidate_signal_cache_on_status_change' ], 10, 3 );
add_action( 'deleted_post', [ $this, 'invalidate_signal_cache' ], 10, 1 );
}
/**
* Check if traffic signals are enabled.
*
* Allows filters to disable traffic signals when needed.
* Example: add_filter( 'pvc_enable_traffic_signals', '__return_false' );
*
* @return bool True if enabled, false otherwise
*/
private function is_enabled() {
/**
* Filter whether traffic signals should be enabled.
*
* Filters can use this to disable traffic signals and prevent duplicates.
*
* @param bool $enabled Whether traffic signals are enabled (default: true)
*/
return apply_filters( 'pvc_enable_traffic_signals', true );
}
/**
* Register traffic signals column for post types.
*
* @return void
*/
public function register_signals_column() {
// check if traffic signals are enabled
if ( ! $this->is_enabled() )
return;
// get main instance
$pvc = Post_Views_Counter();
// is posts views column active?
if ( ! $pvc->options['display']['post_views_column'] )
return;
// get post types
$post_types = $pvc->options['general']['post_types_count'];
// any post types?
if ( ! empty( $post_types ) ) {
foreach ( $post_types as $post_type ) {
if ( $post_type !== 'attachment' ) {
add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_traffic_signal_column' ], 10, 1 );
add_filter( 'manage_edit-' . $post_type . '_columns', [ $this, 'add_traffic_signal_column' ], 20 );
add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'render_traffic_signal_column' ], 10, 2 );
}
}
}
}
/**
* Add traffic signals column to post list.
*
* @param array $columns Existing columns
* @return array Modified columns
*/
public function add_traffic_signal_column( $columns ) {
// find position of views column
$views_position = array_search( 'post_views', array_keys( $columns ), true );
$signal_column = [
'traffic_signal' => '<span class="pvc-signal-header" title="' . esc_attr__( 'Traffic Signals', 'post-views-counter' ) . '"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M14.828 14.828 21 21"/><path d="M21 16v5h-5"/><path d="m21 3-9 9-4-4-6 6"/><path d="M21 8V3h-5"/></svg><span class="screen-reader-text">' . esc_html__( 'Traffic Signals', 'post-views-counter' ) . '</span></span>'
];
if ( $views_position !== false ) {
// insert after views column
$before = array_slice( $columns, 0, $views_position + 1, true );
$after = array_slice( $columns, $views_position + 1, null, true );
$columns = array_merge( $before, $signal_column, $after );
}
return $columns;
}
/**
* Render traffic signal column content.
*
* @param string $column_name Column name
* @param int $post_id Post ID
* @return void
*/
public function render_traffic_signal_column( $column_name, $post_id ) {
if ( $column_name !== 'traffic_signal' ) {
return;
}
// check if user can see stats
if ( apply_filters( 'pvc_admin_display_post_views', true, $post_id ) === false ) {
return;
}
// get signal status
$signal = $this->detect_signal( $post_id );
if ( $signal === null ) {
// smart silence - no unusual activity
printf(
'<span class="pvc-signal" role="tooltip" aria-label="%s" data-microtip-position="top"><span class="pvc-insight pvc-insight-silence"></span></span>',
esc_attr__( 'No unusual activity detected.', 'post-views-counter' )
);
} else {
// anomaly detected - generic signal
printf(
'<span class="pvc-signal" role="tooltip" aria-label="%s" data-microtip-position="top" data-microtip-size="medium"><span class="pvc-insight pvc-insight-anomaly"></span></span>',
esc_attr__( 'Unusual traffic pattern detected. More insights available in Post Views Counter Pro.', 'post-views-counter' )
);
}
}
/**
* Detect traffic signal using simple Month-over-Month comparison.
*
* @param int $post_id Post ID
* @return array|null Signal data or null if no anomaly
*/
private function detect_signal( $post_id ) {
// check cache first
$cache_key = 'pvc_signal_' . $post_id;
$cache_group = 'post_views_counter';
$cached = wp_cache_get( $cache_key, $cache_group );
if ( $cached !== false ) {
return $cached;
}
// get current month views
$current_date = new DateTime();
$current_period = $current_date->format( 'Ym' );
$current_total = (int) pvc_get_post_views( $post_id, $current_period );
// minimum views threshold
if ( $current_total < 10 ) {
wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
return null;
}
// get previous month views (same number of days as current month-to-date)
$prev_date = clone $current_date;
$prev_date->modify( '-1 month' );
$prev_year = $prev_date->format( 'Y' );
$prev_month = $prev_date->format( 'm' );
$prev_last_day = (int) $prev_date->format( 't' );
$current_day = (int) $current_date->format( 'd' );
// compare against the same number of days from last month (clamp to last day)
$end_day = min( $current_day, $prev_last_day );
$prev_start = (int) ( $prev_year . $prev_month . '01' );
$prev_end = (int) ( $prev_year . $prev_month . str_pad( (string) $end_day, 2, '0', STR_PAD_LEFT ) );
$prev_total = (int) pvc_get_post_views(
$post_id,
'',
[
'type' => 0,
'period_range' => [ $prev_start, $prev_end ]
]
);
// need both periods to compare
if ( $prev_total < 10 ) {
wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
return null;
}
// calculate change percentage
$change_percent = ( ( $current_total - $prev_total ) / $prev_total ) * 100;
// threshold: 25% change (up or down)
if ( abs( $change_percent ) > 25 ) {
$result = [
'anomaly' => true,
'change_percent' => round( $change_percent )
];
} else {
$result = null;
}
// cache for 1 hour
wp_cache_set( $cache_key, $result, $cache_group, HOUR_IN_SECONDS );
return $result;
}
/**
* Enqueue traffic signals assets on post list screens.
*
* @param string $hook Current admin page hook
* @return void
*/
public function enqueue_signals_assets( $hook ) {
// check if traffic signals are enabled
if ( ! $this->is_enabled() )
return;
// only on post list screens
if ( ! in_array( $hook, [ 'edit.php', 'upload.php' ], true ) )
return;
$screen = get_current_screen();
$pvc = Post_Views_Counter();
$post_types = (array) $pvc->options['general']['post_types_count'];
// check if traffic signals should be displayed
if ( ! $screen || ! $screen->post_type )
return;
// check if this post type has view counting enabled
if ( ! in_array( $screen->post_type, $post_types, true ) )
return;
// enqueue Microtip CSS for tooltips
wp_enqueue_style( 'pvc-microtip', POST_VIEWS_COUNTER_URL . '/assets/microtip/microtip.min.css', [], '1.0.0' );
// enqueue admin-columns CSS for signal icons (if not already enqueued by modal)
if ( ! wp_style_is( 'pvc-admin-columns', 'enqueued' ) ) {
wp_enqueue_style( 'pvc-admin-columns', POST_VIEWS_COUNTER_URL . '/css/admin-columns.css', [], $pvc->defaults['version'] );
}
}
/**
* Invalidate signal cache when view counts update.
*
* @param int $post_id Post ID
* @return void
*/
public function invalidate_signal_cache( $post_id ) {
$cache_key = 'pvc_signal_' . $post_id;
$cache_group = 'post_views_counter';
wp_cache_delete( $cache_key, $cache_group );
}
/**
* Flush all signals cache (called on period resets, cache flushes).
*
* @return void
*/
public function flush_all_signals_cache() {
// WordPress doesn't support wildcard cache deletion
// We rely on natural cache expiration (1 hour) for bulk invalidation
// For persistent object caches (Redis, Memcached), implement via plugin-specific flush
if ( wp_using_ext_object_cache() ) {
do_action( 'pvc_flush_signals_cache' );
}
}
/**
* Invalidate signal cache when post status changes.
*
* @param string $new_status New post status
* @param string $old_status Old post status
* @param WP_Post $post Post object
* @return void
*/
public function invalidate_signal_cache_on_status_change( $new_status, $old_status, $post ) {
// only invalidate if status actually changed
if ( $new_status !== $old_status ) {
$this->invalidate_signal_cache( $post->ID );
}
}
}