PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / 1.7.4
Post Views Counter v1.7.4
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
303 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_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'render_traffic_signal_column' ], 10, 2 );
79 }
80 }
81 }
82 }
83
84 /**
85 * Add traffic signals column to post list.
86 *
87 * @param array $columns Existing columns
88 * @return array Modified columns
89 */
90 public function add_traffic_signal_column( $columns ) {
91 // find position of views column
92 $views_position = array_search( 'post_views', array_keys( $columns ), true );
93
94 $signal_column = [
95 '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>'
96 ];
97
98 if ( $views_position !== false ) {
99 // insert after views column
100 $before = array_slice( $columns, 0, $views_position + 1, true );
101 $after = array_slice( $columns, $views_position + 1, null, true );
102
103 $columns = array_merge( $before, $signal_column, $after );
104 }
105
106 return $columns;
107 }
108
109 /**
110 * Render traffic signal column content.
111 *
112 * @param string $column_name Column name
113 * @param int $post_id Post ID
114 * @return void
115 */
116 public function render_traffic_signal_column( $column_name, $post_id ) {
117 if ( $column_name !== 'traffic_signal' ) {
118 return;
119 }
120
121 // check if user can see stats
122 if ( apply_filters( 'pvc_admin_display_post_views', true, $post_id ) === false ) {
123 return;
124 }
125
126 // get signal status
127 $signal = $this->detect_signal( $post_id );
128
129 if ( $signal === null ) {
130 // smart silence - no unusual activity
131 printf(
132 '<span class="pvc-signal" role="tooltip" aria-label="%s" data-microtip-position="top"><span class="pvc-insight pvc-insight-silence"></span></span>',
133 esc_attr__( 'No unusual activity detected.', 'post-views-counter' )
134 );
135 } else {
136 // anomaly detected - generic signal
137 printf(
138 '<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>',
139 esc_attr__( 'Unusual traffic pattern detected. More insights available in Post Views Counter Pro.', 'post-views-counter' )
140 );
141 }
142 }
143
144 /**
145 * Detect traffic signal using simple Month-over-Month comparison.
146 *
147 * @param int $post_id Post ID
148 * @return array|null Signal data or null if no anomaly
149 */
150 private function detect_signal( $post_id ) {
151 // check cache first
152 $cache_key = 'pvc_signal_' . $post_id;
153 $cache_group = 'post_views_counter';
154 $cached = wp_cache_get( $cache_key, $cache_group );
155
156 if ( $cached !== false ) {
157 return $cached;
158 }
159
160 // get current month views
161 $current_date = new DateTime();
162 $current_year = $current_date->format( 'Y' );
163 $current_month = $current_date->format( 'm' );
164
165 $current_views = pvc_get_views( [
166 'post_id' => $post_id,
167 'post_type' => get_post_type( $post_id ),
168 'fields' => 'date=>views',
169 'views_query' => [
170 'year' => $current_year,
171 'month' => $current_month
172 ]
173 ] );
174
175 $current_total = is_array( $current_views ) ? array_sum( $current_views ) : 0;
176
177 // minimum views threshold
178 if ( $current_total < 10 ) {
179 wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
180 return null;
181 }
182
183 // get previous month views
184 $prev_date = clone $current_date;
185 $prev_date->modify( '-1 month' );
186 $prev_year = $prev_date->format( 'Y' );
187 $prev_month = $prev_date->format( 'm' );
188
189 $prev_views = pvc_get_views( [
190 'post_id' => $post_id,
191 'post_type' => get_post_type( $post_id ),
192 'fields' => 'date=>views',
193 'views_query' => [
194 'year' => $prev_year,
195 'month' => $prev_month
196 ]
197 ] );
198
199 $prev_total = is_array( $prev_views ) ? array_sum( $prev_views ) : 0;
200
201 // need both periods to compare
202 if ( $prev_total < 10 ) {
203 wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
204 return null;
205 }
206
207 // calculate change percentage
208 $change_percent = ( ( $current_total - $prev_total ) / $prev_total ) * 100;
209
210 // threshold: 25% change (up or down)
211 if ( abs( $change_percent ) > 25 ) {
212 $result = [
213 'anomaly' => true,
214 'change_percent' => round( $change_percent )
215 ];
216 } else {
217 $result = null;
218 }
219
220 // cache for 1 hour
221 wp_cache_set( $cache_key, $result, $cache_group, HOUR_IN_SECONDS );
222
223 return $result;
224 }
225
226 /**
227 * Enqueue traffic signals assets on post list screens.
228 *
229 * @param string $hook Current admin page hook
230 * @return void
231 */
232 public function enqueue_signals_assets( $hook ) {
233 // check if traffic signals are enabled (Pro may disable this)
234 if ( ! $this->is_enabled() )
235 return;
236
237 // only on post list screens
238 if ( ! in_array( $hook, [ 'edit.php', 'upload.php' ], true ) )
239 return;
240
241 $screen = get_current_screen();
242 $pvc = Post_Views_Counter();
243
244 // check if traffic signals should be displayed
245 if ( ! $screen || ! $screen->post_type )
246 return;
247
248 // check if this post type has view counting enabled
249 if ( ! in_array( $screen->post_type, $pvc->options['general']['post_types_count'], true ) )
250 return;
251
252 // enqueue Microtip CSS for tooltips
253 wp_enqueue_style( 'pvc-microtip', POST_VIEWS_COUNTER_URL . '/assets/microtip/microtip.min.css', [], '1.0.0' );
254
255 // enqueue admin-columns CSS for signal icons (if not already enqueued by modal)
256 if ( ! wp_style_is( 'pvc-admin-columns', 'enqueued' ) ) {
257 wp_enqueue_style( 'pvc-admin-columns', POST_VIEWS_COUNTER_URL . '/css/admin-columns.css', [], $pvc->defaults['version'] );
258 }
259 }
260
261 /**
262 * Invalidate signal cache when view counts update.
263 *
264 * @param int $post_id Post ID
265 * @return void
266 */
267 public function invalidate_signal_cache( $post_id ) {
268 $cache_key = 'pvc_signal_' . $post_id;
269 $cache_group = 'post_views_counter';
270 wp_cache_delete( $cache_key, $cache_group );
271 }
272
273 /**
274 * Flush all signals cache (called on period resets, cache flushes).
275 *
276 * @return void
277 */
278 public function flush_all_signals_cache() {
279 // WordPress doesn't support wildcard cache deletion
280 // We rely on natural cache expiration (1 hour) for bulk invalidation
281
282 // For persistent object caches (Redis, Memcached), implement via plugin-specific flush
283 if ( wp_using_ext_object_cache() ) {
284 do_action( 'pvc_flush_signals_cache' );
285 }
286 }
287
288 /**
289 * Invalidate signal cache when post status changes.
290 *
291 * @param string $new_status New post status
292 * @param string $old_status Old post status
293 * @param WP_Post $post Post object
294 * @return void
295 */
296 public function invalidate_signal_cache_on_status_change( $new_status, $old_status, $post ) {
297 // only invalidate if status actually changed
298 if ( $new_status !== $old_status ) {
299 $this->invalidate_signal_cache( $post->ID );
300 }
301 }
302 }
303