| 1 |
<?php |
| 2 |
/** |
| 3 |
* Keep xSpeed's own requests out of the site's analytics. |
| 4 |
* |
| 5 |
* The cache warmer, the benchmark, the optimize verifier, the health |
| 6 |
* probe and the dashboard's server checks fetch the site's pages |
| 7 |
* server-side. No JavaScript runs, but WP |
| 8 |
* Statistics and Slimstat can record hits in PHP, and neither recognises |
| 9 |
* our user agents, so every warm was counted as a visitor. |
| 10 |
* |
| 11 |
* Both plugins offer a record-time filter, which is what this uses. It must |
| 12 |
* stay at record time: the warmed page is cached and served to real |
| 13 |
* visitors, so anything that stopped a plugin printing its tracking snippet |
| 14 |
* on a warm would switch analytics off for everyone who gets that copy. |
| 15 |
* |
| 16 |
* @package XSpeed |
| 17 |
*/ |
| 18 |
|
| 19 |
declare(strict_types=1); |
| 20 |
|
| 21 |
namespace XSpeed; |
| 22 |
|
| 23 |
defined( 'ABSPATH' ) || exit; |
| 24 |
|
| 25 |
/** |
| 26 |
* Recognises xSpeed's own requests and tells analytics plugins to skip them. |
| 27 |
*/ |
| 28 |
final class Self_Traffic { |
| 29 |
|
| 30 |
/** |
| 31 |
* User-agent fragments xSpeed's own requests carry. `xSpeed-Preloader` is |
| 32 |
* the warmer's name before 1.3.5; it can still arrive from a warm queued |
| 33 |
* before an update. |
| 34 |
*/ |
| 35 |
public const AGENTS = array( 'xSpeed-Warmer', 'xSpeed-Preloader', 'xSpeed Benchmark', 'xSpeed-Verifier', 'xSpeed Health Probe' ); |
| 36 |
|
| 37 |
/** |
| 38 |
* Request header every xSpeed loopback sends, so a request is recognised |
| 39 |
* by what it is rather than by what it calls itself. The user agent is |
| 40 |
* not enough on its own: the warmer's is filterable, the documented |
| 41 |
* firewall remedy is to set it to a real browser's, and matching that |
| 42 |
* string would drop every real visitor on the same browser. Some probes |
| 43 |
* also have to send a real browser's or WordPress's own UA. |
| 44 |
*/ |
| 45 |
public const HEADER = 'X-XSpeed-Self'; |
| 46 |
|
| 47 |
/** $_SERVER key the header arrives under. */ |
| 48 |
public const SERVER_KEY = 'HTTP_X_XSPEED_SELF'; |
| 49 |
|
| 50 |
/** Register the analytics filters. Safe when neither plugin is active. */ |
| 51 |
public static function boot(): void { |
| 52 |
add_filter( 'wp_statistics_exclusion_robots', array( __CLASS__, 'add_to_robot_list' ) ); |
| 53 |
add_filter( 'slimstat_filter_pageview_stat_init', array( __CLASS__, 'drop_slimstat_hit' ) ); |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* Add the marker header to a loopback request's headers. |
| 58 |
* |
| 59 |
* @param array<string,string> $headers Headers the request already sends. |
| 60 |
* @return array<string,string> |
| 61 |
*/ |
| 62 |
public static function headers( array $headers = array() ): array { |
| 63 |
$headers[ self::HEADER ] = '1'; |
| 64 |
return $headers; |
| 65 |
} |
| 66 |
|
| 67 |
/** |
| 68 |
* The fragments, filterable so an add-on can name its own requests. |
| 69 |
* |
| 70 |
* @return string[] |
| 71 |
*/ |
| 72 |
public static function agents(): array { |
| 73 |
/** |
| 74 |
* Filter the user-agent fragments that mark a request as xSpeed's own, |
| 75 |
* so analytics plugins don't count it as a visitor. |
| 76 |
* |
| 77 |
* @param string[] $agents Case-insensitive substrings, longer than 3 characters. |
| 78 |
*/ |
| 79 |
$agents = function_exists( 'apply_filters' ) ? apply_filters( 'xspeed_self_user_agents', self::AGENTS ) : self::AGENTS; |
| 80 |
if ( ! is_array( $agents ) ) { |
| 81 |
return array(); |
| 82 |
} |
| 83 |
// WP Statistics ignores fragments of 3 characters or fewer, and so |
| 84 |
// does every other consumer, so the paths agree on what matches. |
| 85 |
$agents = array_filter( |
| 86 |
array_map( static fn ( $agent ): string => trim( (string) $agent ), $agents ), |
| 87 |
static fn ( string $agent ): bool => strlen( $agent ) > 3 |
| 88 |
); |
| 89 |
return array_values( $agents ); |
| 90 |
} |
| 91 |
|
| 92 |
/** Is this user agent one of ours? */ |
| 93 |
public static function is_self( string $ua ): bool { |
| 94 |
if ( '' === $ua ) { |
| 95 |
return false; |
| 96 |
} |
| 97 |
foreach ( self::agents() as $fragment ) { |
| 98 |
if ( false !== stripos( $ua, $fragment ) ) { |
| 99 |
return true; |
| 100 |
} |
| 101 |
} |
| 102 |
return false; |
| 103 |
} |
| 104 |
|
| 105 |
/** Does the current request carry the marker header? */ |
| 106 |
public static function request_is_marked(): bool { |
| 107 |
return isset( $_SERVER[ self::SERVER_KEY ] ) && '' !== $_SERVER[ self::SERVER_KEY ]; |
| 108 |
} |
| 109 |
|
| 110 |
/** Is the current request one of ours, by header or by user agent? */ |
| 111 |
public static function is_self_request(): bool { |
| 112 |
return self::request_is_marked() || self::is_self( self::request_ua() ); |
| 113 |
} |
| 114 |
|
| 115 |
/** The current request's user agent, sanitised. */ |
| 116 |
private static function request_ua(): string { |
| 117 |
return isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) : ''; |
| 118 |
} |
| 119 |
|
| 120 |
/** |
| 121 |
* WP Statistics: add our fragments to its robot list, which it matches |
| 122 |
* by substring. It has no hook that sees request headers, so a marked |
| 123 |
* request whose UA is not one of ours (a renamed warmer, a probe) gets |
| 124 |
* its own UA added, for this request only. A real visitor on the same |
| 125 |
* browser doesn't carry the header and is still counted. |
| 126 |
* |
| 127 |
* @param mixed $robots The list so far. |
| 128 |
* @return mixed |
| 129 |
*/ |
| 130 |
public static function add_to_robot_list( $robots ) { |
| 131 |
if ( ! is_array( $robots ) ) { |
| 132 |
return $robots; |
| 133 |
} |
| 134 |
$robots = array_merge( $robots, self::agents() ); |
| 135 |
$ua = self::request_ua(); |
| 136 |
if ( self::request_is_marked() && strlen( $ua ) > 3 && ! self::is_self( $ua ) ) { |
| 137 |
$robots[] = $ua; |
| 138 |
} |
| 139 |
return $robots; |
| 140 |
} |
| 141 |
|
| 142 |
/** |
| 143 |
* Slimstat: an empty stat aborts the pageview (its error e-302). |
| 144 |
* |
| 145 |
* @param mixed $stat The pageview being recorded. |
| 146 |
* @return mixed |
| 147 |
*/ |
| 148 |
public static function drop_slimstat_hit( $stat ) { |
| 149 |
return self::is_self_request() ? array() : $stat; |
| 150 |
} |
| 151 |
} |
| 152 |
|