| @@ -15,9 +15,9 @@ | ||
| 15 | 15 | exit; |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | - * Admin Columns Class. | |
| 19 | + * Tracks post views via the tracker endpoint and its AJAX handlers. | |
| 20 | 20 | * |
| 21 | 21 | * @since 3.3.0 |
| 22 | 22 | */ |
| 23 | 23 | class Tracker { |
| @@ -43,12 +43,24 @@ | ||
| 43 | 43 | */ |
| 44 | 44 | public static function enqueue_scripts() { |
| 45 | 45 | global $post, $ajax_tptn_tracker; |
| 46 | 46 | |
| 47 | - if ( ! is_object( $post ) ) { | |
| 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 ) { | |
| 48 | 60 | return; |
| 49 | 61 | } |
| 50 | - if ( 'draft' === $post->post_status || is_customize_preview() ) { | |
| 62 | + if ( ( is_object( $post ) && 'draft' === $post->post_status ) || is_customize_preview() ) { | |
| 51 | 63 | return; |
| 52 | 64 | } |
| 53 | 65 | |
| 54 | 66 | $track_users = wp_parse_list( \tptn_get_option( 'track_users' ) ); |
| @@ -53,12 +65,12 @@ | ||
| 53 | 65 | |
| 54 | 66 | $track_users = wp_parse_list( \tptn_get_option( 'track_users' ) ); |
| 55 | 67 | $trackers = wp_parse_list( \tptn_get_option( 'trackers' ) ); |
| 56 | 68 | |
| 57 | - if ( is_singular() || \tptn_get_option( 'tracker_all_pages' ) ) { | |
| 69 | + if ( $is_singular || $tracker_all_pages || $has_sitewide_context ) { | |
| 58 | 70 | |
| 59 | 71 | $current_user = wp_get_current_user(); // Let's get the current user. |
| 60 | - $post_author = ( (int) $current_user->ID === (int) $post->post_author ) ? true : false; // Is the current user the post author? | |
| 72 | + $post_author = is_object( $post ) && ( (int) $current_user->ID === (int) $post->post_author ); // Is the current user the post author? | |
| 61 | 73 | $current_user_admin = ( current_user_can( 'manage_options' ) ) ? true : false; // Is the current user an admin? |
| 62 | 74 | $current_user_editor = ( ( current_user_can( 'edit_others_posts' ) ) && ( ! current_user_can( 'manage_options' ) ) ) ? true : false; // Is the current user an editor? |
| 63 | 75 | $is_bot = Helpers::is_bot(); |
| 64 | 76 | |
| @@ -80,9 +92,9 @@ | ||
| 80 | 92 | } |
| 81 | 93 | |
| 82 | 94 | if ( $include_code ) { |
| 83 | 95 | |
| 84 | - $id = is_singular() ? absint( $post->ID ) : 0; | |
| 96 | + $id = $is_singular && is_object( $post ) ? absint( $post->ID ) : 0; | |
| 85 | 97 | $blog_id = get_current_blog_id(); |
| 86 | 98 | $activate_counter = in_array( 'overall', $trackers, true ) ? 1 : 0; // It's 1 if we're updating the overall count. |
| 87 | 99 | $activate_counter = $activate_counter + ( in_array( 'daily', $trackers, true ) ? 10 : 0 ); // It's 10 if we're updating the daily count. |
| 88 | 100 | $top_ten_debug = absint( \tptn_get_option( 'debug_mode' ) ); |
| @@ -121,14 +133,16 @@ | ||
| 121 | 133 | // Strip any query strings since we don't need them. |
| 122 | 134 | $home_url = strtok( $home_url, '?' ); |
| 123 | 135 | |
| 124 | 136 | $ajax_tptn_tracker = array( |
| 125 | - 'ajax_url' => $home_url, | |
| 126 | - 'top_ten_id' => $id, | |
| 127 | - 'top_ten_blog_id' => $blog_id, | |
| 128 | - 'activate_counter' => $activate_counter, | |
| 129 | - 'top_ten_debug' => $top_ten_debug, | |
| 130 | - 'tptn_rnd' => wp_rand( 1, time() ), | |
| 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() ), | |
| 131 | 145 | ); |
| 132 | 146 | |
| 133 | 147 | /** |
| 134 | 148 | * Filter the localize script arguments for the Top 10 tracker. |
| @@ -138,9 +152,9 @@ | ||
| 138 | 152 | $ajax_tptn_tracker = apply_filters( 'tptn_tracker_script_args', $ajax_tptn_tracker ); |
| 139 | 153 | |
| 140 | 154 | wp_enqueue_script( |
| 141 | 155 | 'tptn_tracker', |
| 142 | - plugins_url( 'includes/js/top-10-tracker.min.js', TOP_TEN_PLUGIN_FILE ), | |
| 156 | + plugins_url( 'includes/js/top-10.min.js', TOP_TEN_PLUGIN_FILE ), | |
| 143 | 157 | array(), |
| 144 | 158 | TOP_TEN_VERSION, |
| 145 | 159 | true |
| 146 | 160 | ); |
| @@ -161,8 +175,9 @@ | ||
| 161 | 175 | */ |
| 162 | 176 | public static function query_vars( $vars ) { |
| 163 | 177 | // Add these to the list of queryvars that WP gathers. |
| 164 | 178 | $vars[] = 'top_ten_id'; |
| 179 | + $vars[] = 'top_ten_sitewide_context'; | |
| 165 | 180 | $vars[] = 'top_ten_blog_id'; |
| 166 | 181 | $vars[] = 'activate_counter'; |
| 167 | 182 | $vars[] = 'view_counter'; |
| 168 | 183 | $vars[] = 'top_ten_debug'; |
| @@ -186,16 +201,30 @@ | ||
| 186 | 201 | * @param \WP $wp Current WordPress environment instance. |
| 187 | 202 | */ |
| 188 | 203 | public static function parse_request( $wp ) { |
| 189 | 204 | |
| 190 | - if ( empty( $wp->query_vars['top_ten_id'] ) ) { | |
| 205 | + if ( empty( $wp->query_vars['top_ten_id'] ) && empty( $wp->query_vars['top_ten_sitewide_context'] ) ) { | |
| 191 | 206 | return; |
| 192 | 207 | } |
| 193 | 208 | |
| 194 | - if ( array_key_exists( 'top_ten_id', $wp->query_vars ) && array_key_exists( 'activate_counter', $wp->query_vars ) ) { | |
| 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 | + } | |
| 195 | 223 | |
| 196 | - $id = absint( $wp->query_vars['top_ten_id'] ); | |
| 197 | - $blog_id = absint( $wp->query_vars['top_ten_blog_id'] ); | |
| 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 ); | |
| 198 | 227 | $activate_counter = absint( $wp->query_vars['activate_counter'] ); |
| 199 | 228 | |
| 200 | 229 | $is_feed = ! empty( $wp->query_vars['tptn_feed'] ); |
| 201 | 230 | $source = $is_feed ? 1 : 0; |
| @@ -200,8 +229,11 @@ | ||
| 200 | 229 | $is_feed = ! empty( $wp->query_vars['tptn_feed'] ); |
| 201 | 230 | $source = $is_feed ? 1 : 0; |
| 202 | 231 | |
| 203 | 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 | + } | |
| 204 | 236 | |
| 205 | 237 | if ( $is_feed ) { |
| 206 | 238 | self::output_tracking_pixel(); // Sends GIF and exits. |
| 207 | 239 | } else { |
| @@ -235,16 +267,10 @@ | ||
| 235 | 267 | |
| 236 | 268 | /** |
| 237 | 269 | * Add a tracking pixel to feed content. |
| 238 | 270 | * |
| 239 | - * Appends a 1×1 transparent GIF to each feed item. When a feed reader | |
| 240 | - * loads the image, parse_request() intercepts the request, increments | |
| 241 | - * the view count, and serves the GIF. Views are merged into the same | |
| 242 | - * tables as regular web views. | |
| 271 | + * Feed readers that block remote images by default will not trigger the count. | |
| 243 | 272 | * |
| 244 | - * Note: feed readers that block remote images by default will not trigger | |
| 245 | - * the pixel. The count only increments when the reader actually loads images. | |
| 246 | - * | |
| 247 | 273 | * @since 4.3.0 |
| 248 | 274 | * |
| 249 | 275 | * @param string $content Feed content. |
| 250 | 276 | * @return string Feed content with the tracker image appended. |
| @@ -318,15 +344,30 @@ | ||
| 318 | 344 | * |
| 319 | 345 | * @since 2.4.0 |
| 320 | 346 | */ |
| 321 | 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 | |
| 322 | 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 | + | |
| 323 | 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 | |
| 324 | 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 |
| 325 | 364 | $activate_counter = isset( $_POST['activate_counter'] ) ? absint( sanitize_text_field( wp_unslash( $_POST['activate_counter'] ) ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 326 | - $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 | |
| 327 | 365 | |
| 328 | 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 | + } | |
| 329 | 370 | |
| 330 | 371 | // If the debug parameter is set then we output $str else we send a No Content header. |
| 331 | 372 | if ( 1 === $top_ten_debug ) { |
| 332 | 373 | echo esc_html( $str ); |
| @@ -338,8 +379,38 @@ | ||
| 338 | 379 | wp_die(); |
| 339 | 380 | } |
| 340 | 381 | |
| 341 | 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 | + /** | |
| 342 | 413 | * Function to update the count in the database. |
| 343 | 414 | * |
| 344 | 415 | * @since 2.6.0 |
| 345 | 416 | * |
| @@ -367,9 +438,9 @@ | ||
| 367 | 438 | */ |
| 368 | 439 | $before_update_count = apply_filters( 'tptn_before_update_count', true, $id, $blog_id, $activate_counter, $source ); |
| 369 | 440 | |
| 370 | 441 | if ( $id > 0 && $activate_counter > 0 && $before_update_count ) { |
| 371 | - $result = Database::append_to_funnel( $id, $blog_id, $activate_counter, $source ); | |
| 442 | + $result = Database::record_view( $id, $blog_id, $activate_counter, $source ); | |
| 372 | 443 | $str .= ( false === $result ) ? 'loge' : 'log' . $result; |
| 373 | 444 | } |
| 374 | 445 | |
| 375 | 446 | /** |
| @@ -383,6 +454,30 @@ | ||
| 383 | 454 | * @param int $activate_counter Activate counter flag. |
| 384 | 455 | * @param int $source Traffic source: 0 = web, 1 = feed. |
| 385 | 456 | */ |
| 386 | 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 ); | |
| 387 | 482 | } |
| 388 | 483 | } |