PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.7.6
Post Views Counter v1.7.6
1.7.13 1.7.12 1.7.11 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.2 1.3.2.1 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.7.0 1.7.1 1.7.10 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9
post-views-counter / includes / class-traffic-signals.php
post-views-counter / includes Last commit date
class-admin.php 5 months ago class-columns-modal.php 5 months ago class-columns.php 5 months ago class-counter.php 5 months ago class-crawler-detect.php 5 months ago class-cron.php 5 months ago class-dashboard.php 5 months ago class-frontend.php 5 months ago class-functions.php 5 months ago class-import.php 5 months ago class-integration-gutenberg.php 5 months ago class-integrations.php 5 months ago class-query.php 5 months ago class-settings-api.php 5 months ago class-settings-display.php 5 months ago class-settings-general.php 5 months ago class-settings-integrations.php 5 months ago class-settings-other.php 5 months ago class-settings-reports.php 5 months ago class-settings.php 5 months ago class-toolbar.php 5 months ago class-traffic-signals.php 5 months ago class-update.php 5 months ago class-widgets.php 5 months ago functions.php 5 months ago
class-traffic-signals.php
297 lines
1 <?php
2 // exit if accessed directly
3 if ( ! defined( 'ABSPATH' ) )
4 exit;
5
6 /**
7 * Post_Views_Counter_Traffic_Signals class.
8 *
9 * @class Post_Views_Counter_Traffic_Signals
10 */
11 class Post_Views_Counter_Traffic_Signals {
12
13 /**
14 * Class constructor.
15 *
16 * @return void
17 */
18 public function __construct() {
19 // early exit if traffic signals are disabled (e.g., by Pro plugin)
20 if ( ! $this->is_enabled() )
21 return;
22
23 // actions
24 add_action( 'admin_init', [ $this, 'register_signals_column' ] );
25 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_signals_assets' ] );
26 add_action( 'pvc_after_update_post_views_count', [ $this, 'invalidate_signal_cache' ], 10, 1 );
27 add_action( 'pvc_flush_cached_counts', [ $this, 'flush_all_signals_cache' ] );
28 add_action( 'pvc_reset_counts', [ $this, 'flush_all_signals_cache' ] );
29 add_action( 'transition_post_status', [ $this, 'invalidate_signal_cache_on_status_change' ], 10, 3 );
30 add_action( 'deleted_post', [ $this, 'invalidate_signal_cache' ], 10, 1 );
31 }
32
33 /**
34 * Check if traffic signals are enabled.
35 *
36 * Allows Pro plugin (or other extensions) to disable Free signals via filter.
37 * Example: add_filter( 'pvc_enable_traffic_signals', '__return_false' );
38 *
39 * @return bool True if enabled, false otherwise
40 */
41 private function is_enabled() {
42 /**
43 * Filter whether traffic signals should be enabled.
44 *
45 * Pro plugin uses this to disable Free signals and prevent duplicates.
46 * Site owners can override by removing the Pro filter if they want both.
47 *
48 * @param bool $enabled Whether traffic signals are enabled (default: true)
49 */
50 return apply_filters( 'pvc_enable_traffic_signals', true );
51 }
52
53 /**
54 * Register traffic signals column for post types.
55 *
56 * @return void
57 */
58 public function register_signals_column() {
59 // check if traffic signals are enabled (Pro may disable this)
60 if ( ! $this->is_enabled() )
61 return;
62
63 // get main instance
64 $pvc = Post_Views_Counter();
65
66 // is posts views column active?
67 if ( ! $pvc->options['display']['post_views_column'] )
68 return;
69
70 // get post types
71 $post_types = $pvc->options['general']['post_types_count'];
72
73 // any post types?
74 if ( ! empty( $post_types ) ) {
75 foreach ( $post_types as $post_type ) {
76 if ( $post_type !== 'attachment' ) {
77 add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_traffic_signal_column' ], 10, 1 );
78 add_filter( 'manage_edit-' . $post_type . '_columns', [ $this, 'add_traffic_signal_column' ], 20 );
79 add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'render_traffic_signal_column' ], 10, 2 );
80 }
81 }
82 }
83 }
84
85 /**
86 * Add traffic signals column to post list.
87 *
88 * @param array $columns Existing columns
89 * @return array Modified columns
90 */
91 public function add_traffic_signal_column( $columns ) {
92 // find position of views column
93 $views_position = array_search( 'post_views', array_keys( $columns ), true );
94
95 $signal_column = [
96 '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>'
97 ];
98
99 if ( $views_position !== false ) {
100 // insert after views column
101 $before = array_slice( $columns, 0, $views_position + 1, true );
102 $after = array_slice( $columns, $views_position + 1, null, true );
103
104 $columns = array_merge( $before, $signal_column, $after );
105 }
106
107 return $columns;
108 }
109
110 /**
111 * Render traffic signal column content.
112 *
113 * @param string $column_name Column name
114 * @param int $post_id Post ID
115 * @return void
116 */
117 public function render_traffic_signal_column( $column_name, $post_id ) {
118 if ( $column_name !== 'traffic_signal' ) {
119 return;
120 }
121
122 // check if user can see stats
123 if ( apply_filters( 'pvc_admin_display_post_views', true, $post_id ) === false ) {
124 return;
125 }
126
127 // get signal status
128 $signal = $this->detect_signal( $post_id );
129
130 if ( $signal === null ) {
131 // smart silence - no unusual activity
132 printf(
133 '<span class="pvc-signal" role="tooltip" aria-label="%s" data-microtip-position="top"><span class="pvc-insight pvc-insight-silence"></span></span>',
134 esc_attr__( 'No unusual activity detected.', 'post-views-counter' )
135 );
136 } else {
137 // anomaly detected - generic signal
138 printf(
139 '<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>',
140 esc_attr__( 'Unusual traffic pattern detected. More insights available in Post Views Counter Pro.', 'post-views-counter' )
141 );
142 }
143 }
144
145 /**
146 * Detect traffic signal using simple Month-over-Month comparison.
147 *
148 * @param int $post_id Post ID
149 * @return array|null Signal data or null if no anomaly
150 */
151 private function detect_signal( $post_id ) {
152 // check cache first
153 $cache_key = 'pvc_signal_' . $post_id;
154 $cache_group = 'post_views_counter';
155 $cached = wp_cache_get( $cache_key, $cache_group );
156
157 if ( $cached !== false ) {
158 return $cached;
159 }
160
161 // get current month views
162 $current_date = new DateTime();
163 $current_period = $current_date->format( 'Ym' );
164
165 $current_total = (int) pvc_get_post_views( $post_id, $current_period );
166
167 // minimum views threshold
168 if ( $current_total < 10 ) {
169 wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
170 return null;
171 }
172
173 // get previous month views (same number of days as current month-to-date)
174 $prev_date = clone $current_date;
175 $prev_date->modify( '-1 month' );
176 $prev_year = $prev_date->format( 'Y' );
177 $prev_month = $prev_date->format( 'm' );
178 $prev_last_day = (int) $prev_date->format( 't' );
179 $current_day = (int) $current_date->format( 'd' );
180 // compare against the same number of days from last month (clamp to last day)
181 $end_day = min( $current_day, $prev_last_day );
182
183 $prev_start = (int) ( $prev_year . $prev_month . '01' );
184 $prev_end = (int) ( $prev_year . $prev_month . str_pad( (string) $end_day, 2, '0', STR_PAD_LEFT ) );
185
186 $prev_total = (int) pvc_get_post_views(
187 $post_id,
188 '',
189 [
190 'type' => 0,
191 'period_range' => [ $prev_start, $prev_end ]
192 ]
193 );
194
195 // need both periods to compare
196 if ( $prev_total < 10 ) {
197 wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
198 return null;
199 }
200
201 // calculate change percentage
202 $change_percent = ( ( $current_total - $prev_total ) / $prev_total ) * 100;
203
204 // threshold: 25% change (up or down)
205 if ( abs( $change_percent ) > 25 ) {
206 $result = [
207 'anomaly' => true,
208 'change_percent' => round( $change_percent )
209 ];
210 } else {
211 $result = null;
212 }
213
214 // cache for 1 hour
215 wp_cache_set( $cache_key, $result, $cache_group, HOUR_IN_SECONDS );
216
217 return $result;
218 }
219
220 /**
221 * Enqueue traffic signals assets on post list screens.
222 *
223 * @param string $hook Current admin page hook
224 * @return void
225 */
226 public function enqueue_signals_assets( $hook ) {
227 // check if traffic signals are enabled (Pro may disable this)
228 if ( ! $this->is_enabled() )
229 return;
230
231 // only on post list screens
232 if ( ! in_array( $hook, [ 'edit.php', 'upload.php' ], true ) )
233 return;
234
235 $screen = get_current_screen();
236 $pvc = Post_Views_Counter();
237
238 // check if traffic signals should be displayed
239 if ( ! $screen || ! $screen->post_type )
240 return;
241
242 // check if this post type has view counting enabled
243 if ( ! in_array( $screen->post_type, $pvc->options['general']['post_types_count'], true ) )
244 return;
245
246 // enqueue Microtip CSS for tooltips
247 wp_enqueue_style( 'pvc-microtip', POST_VIEWS_COUNTER_URL . '/assets/microtip/microtip.min.css', [], '1.0.0' );
248
249 // enqueue admin-columns CSS for signal icons (if not already enqueued by modal)
250 if ( ! wp_style_is( 'pvc-admin-columns', 'enqueued' ) ) {
251 wp_enqueue_style( 'pvc-admin-columns', POST_VIEWS_COUNTER_URL . '/css/admin-columns.css', [], $pvc->defaults['version'] );
252 }
253 }
254
255 /**
256 * Invalidate signal cache when view counts update.
257 *
258 * @param int $post_id Post ID
259 * @return void
260 */
261 public function invalidate_signal_cache( $post_id ) {
262 $cache_key = 'pvc_signal_' . $post_id;
263 $cache_group = 'post_views_counter';
264 wp_cache_delete( $cache_key, $cache_group );
265 }
266
267 /**
268 * Flush all signals cache (called on period resets, cache flushes).
269 *
270 * @return void
271 */
272 public function flush_all_signals_cache() {
273 // WordPress doesn't support wildcard cache deletion
274 // We rely on natural cache expiration (1 hour) for bulk invalidation
275
276 // For persistent object caches (Redis, Memcached), implement via plugin-specific flush
277 if ( wp_using_ext_object_cache() ) {
278 do_action( 'pvc_flush_signals_cache' );
279 }
280 }
281
282 /**
283 * Invalidate signal cache when post status changes.
284 *
285 * @param string $new_status New post status
286 * @param string $old_status Old post status
287 * @param WP_Post $post Post object
288 * @return void
289 */
290 public function invalidate_signal_cache_on_status_change( $new_status, $old_status, $post ) {
291 // only invalidate if status actually changed
292 if ( $new_status !== $old_status ) {
293 $this->invalidate_signal_cache( $post->ID );
294 }
295 }
296 }
297