PluginProbe ʕ •ᴥ•ʔ
Post Views Counter / trunk
Post Views Counter vtrunk
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 4 months ago class-columns-modal.php 1 month ago class-columns.php 3 weeks ago class-counter.php 2 months ago class-crawler-detect.php 7 months ago class-cron.php 1 month ago class-dashboard.php 1 month ago class-emails-mailer.php 1 month ago class-emails-period.php 1 month ago class-emails-query.php 1 month ago class-emails-scheduler.php 1 month ago class-emails-template.php 1 month ago class-emails.php 1 month ago class-frontend.php 3 weeks ago class-functions.php 1 year ago class-import.php 2 months ago class-integration-gutenberg.php 6 months ago class-integrations.php 2 months ago class-query.php 2 months ago class-settings-api.php 1 month ago class-settings-display.php 4 months ago class-settings-emails.php 1 month ago class-settings-general.php 1 month ago class-settings-integrations.php 5 months ago class-settings-other.php 1 month ago class-settings-reports.php 1 month ago class-settings.php 1 month ago class-toolbar.php 4 months ago class-traffic-signals.php 2 months ago class-update.php 1 month ago class-widgets.php 1 month ago functions.php 1 month 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
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 filters to disable traffic signals when needed.
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 * Filters can use this to disable traffic signals and prevent duplicates.
46 *
47 * @param bool $enabled Whether traffic signals are enabled (default: true)
48 */
49 return apply_filters( 'pvc_enable_traffic_signals', true );
50 }
51
52 /**
53 * Register traffic signals column for post types.
54 *
55 * @return void
56 */
57 public function register_signals_column() {
58 // check if traffic signals are enabled
59 if ( ! $this->is_enabled() )
60 return;
61
62 // get main instance
63 $pvc = Post_Views_Counter();
64
65 // is posts views column active?
66 if ( ! $pvc->options['display']['post_views_column'] )
67 return;
68
69 // get post types
70 $post_types = $pvc->options['general']['post_types_count'];
71
72 // any post types?
73 if ( ! empty( $post_types ) ) {
74 foreach ( $post_types as $post_type ) {
75 if ( $post_type !== 'attachment' ) {
76 add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_traffic_signal_column' ], 10, 1 );
77 add_filter( 'manage_edit-' . $post_type . '_columns', [ $this, 'add_traffic_signal_column' ], 20 );
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_period = $current_date->format( 'Ym' );
163
164 $current_total = (int) pvc_get_post_views( $post_id, $current_period );
165
166 // minimum views threshold
167 if ( $current_total < 10 ) {
168 wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
169 return null;
170 }
171
172 // get previous month views (same number of days as current month-to-date)
173 $prev_date = clone $current_date;
174 $prev_date->modify( '-1 month' );
175 $prev_year = $prev_date->format( 'Y' );
176 $prev_month = $prev_date->format( 'm' );
177 $prev_last_day = (int) $prev_date->format( 't' );
178 $current_day = (int) $current_date->format( 'd' );
179 // compare against the same number of days from last month (clamp to last day)
180 $end_day = min( $current_day, $prev_last_day );
181
182 $prev_start = (int) ( $prev_year . $prev_month . '01' );
183 $prev_end = (int) ( $prev_year . $prev_month . str_pad( (string) $end_day, 2, '0', STR_PAD_LEFT ) );
184
185 $prev_total = (int) pvc_get_post_views(
186 $post_id,
187 '',
188 [
189 'type' => 0,
190 'period_range' => [ $prev_start, $prev_end ]
191 ]
192 );
193
194 // need both periods to compare
195 if ( $prev_total < 10 ) {
196 wp_cache_set( $cache_key, null, $cache_group, HOUR_IN_SECONDS );
197 return null;
198 }
199
200 // calculate change percentage
201 $change_percent = ( ( $current_total - $prev_total ) / $prev_total ) * 100;
202
203 // threshold: 25% change (up or down)
204 if ( abs( $change_percent ) > 25 ) {
205 $result = [
206 'anomaly' => true,
207 'change_percent' => round( $change_percent )
208 ];
209 } else {
210 $result = null;
211 }
212
213 // cache for 1 hour
214 wp_cache_set( $cache_key, $result, $cache_group, HOUR_IN_SECONDS );
215
216 return $result;
217 }
218
219 /**
220 * Enqueue traffic signals assets on post list screens.
221 *
222 * @param string $hook Current admin page hook
223 * @return void
224 */
225 public function enqueue_signals_assets( $hook ) {
226 // check if traffic signals are enabled
227 if ( ! $this->is_enabled() )
228 return;
229
230 // only on post list screens
231 if ( ! in_array( $hook, [ 'edit.php', 'upload.php' ], true ) )
232 return;
233
234 $screen = get_current_screen();
235 $pvc = Post_Views_Counter();
236 $post_types = (array) $pvc->options['general']['post_types_count'];
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, $post_types, 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