PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 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 All 33 releases
xspeed / includes / class-self-traffic.php

class-self-traffic.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.7, at includes/class-self-traffic.php

152 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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