PluginProbe
WebberZone Top 10 — Popular Posts / trunk
WebberZone Top 10 — Popular Posts vtrunk
4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 1.6.2 All 116 releases
top-10 / includes / class-tracker.php

class-tracker.php in WebberZone Top 10 — Popular Posts trunk, at includes/class-tracker.php

484 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Tracker class.
4 *
5 * @package WebberZone\Top_Ten
6 */
7
8 namespace WebberZone\Top_Ten;
9
10 use WebberZone\Top_Ten\Database;
11 use WebberZone\Top_Ten\Util\Helpers;
12 use WebberZone\Top_Ten\Util\Hook_Registry;
13
14 if ( ! defined( 'WPINC' ) ) {
15 exit;
16 }
17
18 /**
19 * Tracks post views via the tracker endpoint and its AJAX handlers.
20 *
21 * @since 3.3.0
22 */
23 class Tracker {
24
25 /**
26 * Constructor class.
27 *
28 * @since 3.3.0
29 */
30 public function __construct() {
31 Hook_Registry::add_action( 'parse_request', array( $this, 'parse_request' ) );
32 Hook_Registry::add_filter( 'query_vars', array( $this, 'query_vars' ) );
33 Hook_Registry::add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
34 Hook_Registry::add_action( 'wp_ajax_nopriv_tptn_tracker', array( $this, 'tracker_parser' ) );
35 Hook_Registry::add_action( 'wp_ajax_tptn_tracker', array( $this, 'tracker_parser' ) );
36 }
37
38 /**
39 * Enqueues the scripts needed by Top 10.
40 *
41 * @since 1.9.7
42 * @return void
43 */
44 public static function enqueue_scripts() {
45 global $post, $ajax_tptn_tracker;
46
47 $is_singular = is_singular();
48 $tracker_all_pages = (bool) \tptn_get_option( 'tracker_all_pages' );
49 /**
50 * Filters the site-wide context key tracked for the current request.
51 *
52 * @since 4.5.0
53 *
54 * @param string $sitewide_context Context key, or an empty string when the request is not tracked site-wide.
55 */
56 $sitewide_context = (string) apply_filters( 'tptn_tracker_sitewide_context', '' );
57 $has_sitewide_context = '' !== $sitewide_context;
58
59 if ( ! is_object( $post ) && ! $tracker_all_pages && ! $has_sitewide_context ) {
60 return;
61 }
62 if ( ( is_object( $post ) && 'draft' === $post->post_status ) || is_customize_preview() ) {
63 return;
64 }
65
66 $track_users = wp_parse_list( \tptn_get_option( 'track_users' ) );
67 $trackers = wp_parse_list( \tptn_get_option( 'trackers' ) );
68
69 if ( $is_singular || $tracker_all_pages || $has_sitewide_context ) {
70
71 $current_user = wp_get_current_user(); // Let's get the current user.
72 $post_author = is_object( $post ) && ( (int) $current_user->ID === (int) $post->post_author ); // Is the current user the post author?
73 $current_user_admin = ( current_user_can( 'manage_options' ) ) ? true : false; // Is the current user an admin?
74 $current_user_editor = ( ( current_user_can( 'edit_others_posts' ) ) && ( ! current_user_can( 'manage_options' ) ) ) ? true : false; // Is the current user an editor?
75 $is_bot = Helpers::is_bot();
76
77 $include_code = true;
78 if ( ( $post_author ) && ( ! in_array( 'authors', $track_users, true ) ) ) {
79 $include_code = false;
80 }
81 if ( ( $current_user_admin ) && ( ! in_array( 'admins', $track_users, true ) ) ) {
82 $include_code = false;
83 }
84 if ( ( $current_user_editor ) && ( ! in_array( 'editors', $track_users, true ) ) ) {
85 $include_code = false;
86 }
87 if ( ( $current_user->exists() ) && ( ! \tptn_get_option( 'logged_in' ) ) ) {
88 $include_code = false;
89 }
90 if ( $is_bot && \tptn_get_option( 'no_bots' ) ) {
91 $include_code = false;
92 }
93
94 if ( $include_code ) {
95
96 $id = $is_singular && is_object( $post ) ? absint( $post->ID ) : 0;
97 $blog_id = get_current_blog_id();
98 $activate_counter = in_array( 'overall', $trackers, true ) ? 1 : 0; // It's 1 if we're updating the overall count.
99 $activate_counter = $activate_counter + ( in_array( 'daily', $trackers, true ) ? 10 : 0 ); // It's 10 if we're updating the daily count.
100 $top_ten_debug = absint( \tptn_get_option( 'debug_mode' ) );
101 $tracker_type = \tptn_get_option( 'tracker_type' );
102
103 switch ( $tracker_type ) {
104 case 'query_based':
105 $home_url = home_url( '/' );
106 break;
107
108 case 'ajaxurl':
109 $home_url = admin_url( 'admin-ajax.php' );
110 break;
111
112 case 'rest_based':
113 $home_url = rest_url( 'top-10/v1/tracker' );
114 break;
115
116 default:
117 $home_url = rest_url( 'top-10/v1/tracker' );
118 break;
119 }
120
121 /**
122 * Filter the URL of the tracker.
123 *
124 * Other tracker types can override the URL processed by the jQuery.post request
125 * The corresponding tracker can use the below variables or append their own to $ajax_tptn_tracker
126 *
127 * @since 2.0
128 *
129 * @param string $home_url URL of the tracker.
130 */
131 $home_url = apply_filters( 'tptn_add_counter_script_url', $home_url );
132
133 // Strip any query strings since we don't need them.
134 $home_url = strtok( $home_url, '?' );
135
136 $ajax_tptn_tracker = array(
137 'ajax_url' => $home_url,
138 'top_ten_id' => $id,
139 'top_ten_sitewide_context' => $sitewide_context,
140 'top_ten_blog_id' => $blog_id,
141 'activate_counter' => $activate_counter,
142 'top_ten_debug' => $top_ten_debug,
143 'tracker_type' => $tracker_type,
144 'tptn_rnd' => wp_rand( 1, time() ),
145 );
146
147 /**
148 * Filter the localize script arguments for the Top 10 tracker.
149 *
150 * @since 2.4.0
151 */
152 $ajax_tptn_tracker = apply_filters( 'tptn_tracker_script_args', $ajax_tptn_tracker );
153
154 wp_enqueue_script(
155 'tptn_tracker',
156 plugins_url( 'includes/js/top-10.min.js', TOP_TEN_PLUGIN_FILE ),
157 array(),
158 TOP_TEN_VERSION,
159 true
160 );
161
162 wp_localize_script( 'tptn_tracker', 'ajax_tptn_tracker', $ajax_tptn_tracker );
163
164 }
165 }
166 }
167
168 /**
169 * Function to add additional queries to query_vars.
170 *
171 * @since 2.0.0
172 *
173 * @param array $vars Query variables array.
174 * @return array Query variables array with Top 10 parameters appended
175 */
176 public static function query_vars( $vars ) {
177 // Add these to the list of queryvars that WP gathers.
178 $vars[] = 'top_ten_id';
179 $vars[] = 'top_ten_sitewide_context';
180 $vars[] = 'top_ten_blog_id';
181 $vars[] = 'activate_counter';
182 $vars[] = 'view_counter';
183 $vars[] = 'top_ten_debug';
184 $vars[] = 'tptn_feed';
185
186 /**
187 * Function to add additional queries to query_vars.
188 *
189 * @since 2.6.0
190 *
191 * @param array $vars Updated Query variables array with Top 10 queries added.
192 */
193 return apply_filters( 'tptn_query_vars', $vars );
194 }
195
196 /**
197 * Parses the WordPress object to update/display the count.
198 *
199 * @since 2.0.0
200 *
201 * @param \WP $wp Current WordPress environment instance.
202 */
203 public static function parse_request( $wp ) {
204
205 if ( empty( $wp->query_vars['top_ten_id'] ) && empty( $wp->query_vars['top_ten_sitewide_context'] ) ) {
206 return;
207 }
208
209 if (
210 array_key_exists( 'activate_counter', $wp->query_vars )
211 && ( array_key_exists( 'top_ten_id', $wp->query_vars ) || array_key_exists( 'top_ten_sitewide_context', $wp->query_vars ) )
212 ) {
213 if ( ! self::is_tracking_request_allowed() ) {
214 if ( array_key_exists( 'top_ten_debug', $wp->query_vars ) && 1 === absint( $wp->query_vars['top_ten_debug'] ) ) {
215 header( 'content-type: application/x-javascript' );
216 wp_send_json( 'blocked' );
217 } else {
218 header( 'HTTP/1.0 204 No Content' );
219 header( 'Cache-Control: max-age=15, s-maxage=0' );
220 }
221 exit;
222 }
223
224 $id = absint( $wp->query_vars['top_ten_id'] ?? 0 );
225 $sitewide_context = sanitize_text_field( $wp->query_vars['top_ten_sitewide_context'] ?? '' );
226 $blog_id = absint( $wp->query_vars['top_ten_blog_id'] ?? 0 );
227 $activate_counter = absint( $wp->query_vars['activate_counter'] );
228
229 $is_feed = ! empty( $wp->query_vars['tptn_feed'] );
230 $source = $is_feed ? 1 : 0;
231
232 $str = self::update_count( $id, $blog_id, $activate_counter, $source );
233 if ( '' !== $sitewide_context ) {
234 $str .= self::update_sitewide_count( $sitewide_context, $blog_id, $activate_counter, $source );
235 }
236
237 if ( $is_feed ) {
238 self::output_tracking_pixel(); // Sends GIF and exits.
239 } else {
240 // If the debug parameter is set then we output $str else we send a No Content header.
241 if ( array_key_exists( 'top_ten_debug', $wp->query_vars ) && 1 === absint( $wp->query_vars['top_ten_debug'] ) ) {
242 header( 'content-type: application/x-javascript' );
243 wp_send_json( $str );
244 } else {
245 header( 'HTTP/1.0 204 No Content' );
246 header( 'Cache-Control: max-age=15, s-maxage=0' );
247 }
248
249 // Stop anything else from loading as it is not needed.
250 exit;
251 }
252 } elseif ( array_key_exists( 'top_ten_id', $wp->query_vars ) && array_key_exists( 'view_counter', $wp->query_vars ) ) {
253
254 $id = absint( $wp->query_vars['top_ten_id'] );
255
256 if ( $id > 0 ) {
257
258 $output = Counter::get_post_count( $id );
259
260 nocache_headers();
261 wp_send_json( array( 'count' => $output ) );
262 }
263 } else {
264 return;
265 }
266 }
267
268 /**
269 * Add a tracking pixel to feed content.
270 *
271 * Feed readers that block remote images by default will not trigger the count.
272 *
273 * @since 4.3.0
274 *
275 * @param string $content Feed content.
276 * @return string Feed content with the tracker image appended.
277 */
278 public static function add_feed_tracker( $content ) {
279 global $post;
280
281 if (
282 ! \tptn_get_option( 'track_feed_views' ) ||
283 ! is_feed() ||
284 ! is_object( $post ) ||
285 empty( $post->ID )
286 ) {
287 return $content;
288 }
289
290 /*
291 * In full-text mode WordPress fires both the_excerpt_rss (for <description>)
292 * and the_content_feed (for <content:encoded>) per item. Skip the excerpt hook
293 * in that case so we don't double-count; the_content_feed will add the pixel.
294 * In excerpt-only mode the_content_feed never fires, so the_excerpt_rss handles it.
295 */
296 if ( 'the_excerpt_rss' === current_filter() && ! get_option( 'rss_use_excerpt' ) ) {
297 return $content;
298 }
299
300 $trackers = wp_parse_list( \tptn_get_option( 'trackers' ) );
301 $activate_counter = in_array( 'overall', $trackers, true ) ? 1 : 0;
302 $activate_counter = $activate_counter + ( in_array( 'daily', $trackers, true ) ? 10 : 0 );
303
304 if ( 0 === $activate_counter ) {
305 return $content;
306 }
307
308 $tracker_url = add_query_arg(
309 array(
310 'top_ten_id' => absint( $post->ID ),
311 'top_ten_blog_id' => get_current_blog_id(),
312 'activate_counter' => $activate_counter,
313 'tptn_feed' => 1,
314 ),
315 home_url( '/' )
316 );
317
318 $tracker = sprintf(
319 '<img src="%1$s" width="1" height="1" alt="" style="display:none" />',
320 esc_url( $tracker_url )
321 );
322
323 return $content . $tracker;
324 }
325
326 /**
327 * Output a transparent GIF for feed view tracking requests.
328 *
329 * @since 4.3.0
330 *
331 * @return void
332 */
333 protected static function output_tracking_pixel() {
334 $pixel = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
335
336 header( 'Content-Type: image/gif' );
337 header( 'Cache-Control: no-cache, no-store, must-revalidate, max-age=0' );
338 echo base64_decode( $pixel ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode, WordPress.Security.EscapeOutput.OutputNotEscaped
339 exit;
340 }
341
342 /**
343 * Parse the ajax response.
344 *
345 * @since 2.4.0
346 */
347 public static function tracker_parser() {
348 $top_ten_debug = isset( $_POST['top_ten_debug'] ) ? absint( sanitize_text_field( wp_unslash( $_POST['top_ten_debug'] ) ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
349
350 if ( ! self::is_tracking_request_allowed() ) {
351 if ( 1 === $top_ten_debug ) {
352 echo esc_html( 'blocked' );
353 wp_die();
354 }
355
356 header( 'HTTP/1.0 204 No Content' );
357 header( 'Cache-Control: max-age=15, s-maxage=0' );
358 wp_die( '', '', array( 'response' => 204 ) );
359 }
360
361 $id = isset( $_POST['top_ten_id'] ) ? absint( sanitize_text_field( wp_unslash( $_POST['top_ten_id'] ) ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
362 $sitewide_context = isset( $_POST['top_ten_sitewide_context'] ) ? sanitize_text_field( wp_unslash( $_POST['top_ten_sitewide_context'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
363 $blog_id = isset( $_POST['top_ten_blog_id'] ) ? absint( sanitize_text_field( wp_unslash( $_POST['top_ten_blog_id'] ) ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
364 $activate_counter = isset( $_POST['activate_counter'] ) ? absint( sanitize_text_field( wp_unslash( $_POST['activate_counter'] ) ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
365
366 $str = self::update_count( $id, $blog_id, $activate_counter, 0 );
367 if ( '' !== $sitewide_context ) {
368 $str .= self::update_sitewide_count( $sitewide_context, $blog_id, $activate_counter, 0 );
369 }
370
371 // If the debug parameter is set then we output $str else we send a No Content header.
372 if ( 1 === $top_ten_debug ) {
373 echo esc_html( $str );
374 } else {
375 header( 'HTTP/1.0 204 No Content' );
376 header( 'Cache-Control: max-age=15, s-maxage=0' );
377 }
378
379 wp_die();
380 }
381
382 /**
383 * Check whether a tracker request should be processed.
384 *
385 * Cached pages can enqueue the tracker for a bot, so bot detection must also run
386 * at the endpoint. Browser prefetches and direct navigations must not create views.
387 *
388 * @since 4.5.0
389 * @return bool True when the request may be tracked.
390 */
391 public static function is_tracking_request_allowed() {
392 foreach ( array( 'HTTP_SEC_PURPOSE', 'HTTP_PURPOSE' ) as $header ) {
393 $value = isset( $_SERVER[ $header ] ) && is_string( $_SERVER[ $header ] )
394 ? sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) )
395 : '';
396
397 if ( preg_match( '/(?:^|[\s,;])(?:prefetch|prerender)(?:$|[\s,;])/i', $value ) ) {
398 return false;
399 }
400 }
401
402 $fetch_mode = isset( $_SERVER['HTTP_SEC_FETCH_MODE'] ) && is_string( $_SERVER['HTTP_SEC_FETCH_MODE'] )
403 ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_SEC_FETCH_MODE'] ) )
404 : '';
405 if ( 'navigate' === strtolower( $fetch_mode ) ) {
406 return false;
407 }
408
409 return ! ( \tptn_get_option( 'no_bots' ) && Helpers::is_bot() );
410 }
411
412 /**
413 * Function to update the count in the database.
414 *
415 * @since 2.6.0
416 *
417 * @param int $id Post ID.
418 * @param int $blog_id Blog ID.
419 * @param int $activate_counter Activate counter flag.
420 * @param int $source Traffic source: 0 = web, 1 = feed.
421 *
422 * @return string Response on database update.
423 */
424 public static function update_count( $id, $blog_id, $activate_counter, $source = 0 ) {
425
426 $str = '';
427
428 /**
429 * Filter the flag to confirm that counts should be updated in the database.
430 *
431 * @since 4.0.0
432 *
433 * @param bool $flag Flag to confirm that counts should be updated in the database.
434 * @param int $id Post ID.
435 * @param int $blog_id Blog ID.
436 * @param int $activate_counter Activate counter flag.
437 * @param int $source Traffic source: 0 = web, 1 = feed.
438 */
439 $before_update_count = apply_filters( 'tptn_before_update_count', true, $id, $blog_id, $activate_counter, $source );
440
441 if ( $id > 0 && $activate_counter > 0 && $before_update_count ) {
442 $result = Database::record_view( $id, $blog_id, $activate_counter, $source );
443 $str .= ( false === $result ) ? 'loge' : 'log' . $result;
444 }
445
446 /**
447 * Filter the response on database update.
448 *
449 * @since 2.6.0
450 *
451 * @param string $str Response string.
452 * @param int $id Post ID.
453 * @param int $blog_id Blog ID.
454 * @param int $activate_counter Activate counter flag.
455 * @param int $source Traffic source: 0 = web, 1 = feed.
456 */
457 return apply_filters( 'tptn_update_count', $str, $id, $blog_id, $activate_counter, $source );
458 }
459
460 /**
461 * Update a site-wide context count.
462 *
463 * @param string $context Context key.
464 * @param int $blog_id Blog ID.
465 * @param int $activate_counter Counter flag.
466 * @param int $source Traffic source.
467 * @return string Response on database update.
468 */
469 public static function update_sitewide_count( $context, $blog_id, $activate_counter, $source = 0 ) {
470 /**
471 * Filters the response returned after recording a site-wide view.
472 *
473 * @since 4.5.0
474 *
475 * @param string $response Response text.
476 * @param string $context Site-wide context key.
477 * @param int $blog_id Blog ID.
478 * @param int $activate_counter Which counters are active.
479 * @param int $source Traffic source.
480 */
481 return (string) apply_filters( 'tptn_tracker_sitewide_count', '', $context, $blog_id, $activate_counter, $source );
482 }
483 }
484