PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / woocommerce-analytics / src / class-pixel-builder.php
jetpack / jetpack_vendor / automattic / woocommerce-analytics / src Last commit date
API 8 months ago mu-plugin 5 months ago class-consent-manager.php 8 months ago class-features.php 5 months ago class-my-account.php 8 months ago class-pixel-builder.php 4 months ago class-universal.php 3 weeks ago class-wc-analytics-tracking.php 3 weeks ago class-woo-analytics-trait.php 3 weeks ago class-woocommerce-analytics.php 3 weeks ago
class-pixel-builder.php
340 lines
1 <?php
2 /**
3 * Pixel Builder for WooCommerce Analytics
4 *
5 * @package automattic/woocommerce-analytics
6 */
7
8 namespace Automattic\Woocommerce_Analytics;
9
10 use WP_Error;
11
12 /**
13 * Pixel Builder class - handles pixel URL construction.
14 */
15 class Pixel_Builder {
16
17 /**
18 * Tracks pixel URL.
19 *
20 * @var string
21 */
22 const TRACKS_PIXEL_URL = 'https://pixel.wp.com/t.gif';
23
24 /**
25 * ClickHouse pixel URL.
26 *
27 * @var string
28 */
29 const CH_PIXEL_URL = 'https://pixel.wp.com/w.gif';
30
31 /**
32 * Browser type identifier for server-side tracking.
33 *
34 * @var string
35 */
36 const BROWSER_TYPE = 'php-agent';
37
38 /**
39 * Event name regex pattern.
40 * Format: prefix_eventname (e.g., woocommerceanalytics_checkout_started)
41 *
42 * @var string
43 */
44 const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){1}([a-z0-9_]+)$/';
45
46 /**
47 * Property name regex pattern.
48 * Format: lowercase letters/underscores, starting with letter or underscore.
49 *
50 * @var string
51 */
52 const PROP_NAME_REGEX = '/^[a-z_][a-z0-9_]*$/';
53
54 /**
55 * Build a timestamp representing milliseconds since 1970-01-01.
56 *
57 * @return string A string representing a timestamp.
58 */
59 public static function build_timestamp() {
60 $ts = round( microtime( true ) * 1000 );
61 return number_format( $ts, 0, '', '' );
62 }
63
64 /**
65 * Add request timestamp and nocache parameter to pixel URL.
66 * Should be called just before the HTTP request.
67 *
68 * @param string $pixel Pixel URL.
69 * @return string Pixel URL with request timestamp and URL terminator.
70 */
71 public static function add_request_timestamp_and_nocache( $pixel ) {
72 return $pixel . '&_rt=' . self::build_timestamp() . '&_=_';
73 }
74
75 /**
76 * Build a Tracks pixel URL from properties.
77 *
78 * @param array $properties Event properties.
79 * @return string|WP_Error Pixel URL on success, WP_Error on failure.
80 */
81 public static function build_tracks_url( $properties ) {
82 $validated = self::validate_and_sanitize( $properties );
83
84 if ( is_wp_error( $validated ) ) {
85 return $validated;
86 }
87
88 return self::TRACKS_PIXEL_URL . '?' . http_build_query( $validated );
89 }
90
91 /**
92 * Build a ClickHouse pixel URL from properties.
93 *
94 * @param array $properties Event properties.
95 * @return string|WP_Error Pixel URL on success, WP_Error on failure.
96 */
97 public static function build_ch_url( $properties ) {
98 $validated = self::validate_and_sanitize( $properties );
99
100 if ( is_wp_error( $validated ) ) {
101 return $validated;
102 }
103
104 return self::CH_PIXEL_URL . '?' . http_build_query( $validated );
105 }
106
107 /**
108 * Validate and sanitize event properties.
109 *
110 * @param array $properties Event properties.
111 * @return array|WP_Error Validated properties on success, WP_Error on failure.
112 */
113 public static function validate_and_sanitize( $properties ) {
114 // Required: event name.
115 if ( empty( $properties['_en'] ) ) {
116 return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 );
117 }
118
119 // Validate event name format.
120 if ( ! self::event_name_is_valid( $properties['_en'] ) ) {
121 return new WP_Error( 'invalid_event_name', 'A valid event name must be specified.' );
122 }
123
124 // Delete non-routable IP addresses (geoip would discard these anyway).
125 if ( isset( $properties['_via_ip'] ) && preg_match( '/^192\.168|^10\./', $properties['_via_ip'] ) ) {
126 unset( $properties['_via_ip'] );
127 }
128
129 // Add browser type for server-side tracking.
130 $properties['browser_type'] = self::BROWSER_TYPE;
131
132 // Ensure timestamp exists.
133 if ( ! isset( $properties['_ts'] ) ) {
134 $properties['_ts'] = self::build_timestamp();
135 }
136
137 // Validate property names.
138 foreach ( array_keys( $properties ) as $key ) {
139 if ( '_en' === $key ) {
140 continue;
141 }
142 if ( ! self::prop_name_is_valid( $key ) ) {
143 return new WP_Error( 'invalid_prop_name', 'A valid prop name must be specified: ' . $key );
144 }
145 }
146
147 // Sanitize array values to prevent bracket notation in URL serialization.
148 return self::sanitize_property_values( $properties );
149 }
150
151 /**
152 * Check if event name is valid.
153 *
154 * @param string $name Event name.
155 * @return bool True if valid, false otherwise.
156 */
157 public static function event_name_is_valid( $name ) {
158 return (bool) preg_match( self::EVENT_NAME_REGEX, $name );
159 }
160
161 /**
162 * Check if a property name is valid.
163 *
164 * @param string $name Property name.
165 * @return bool True if valid, false otherwise.
166 */
167 public static function prop_name_is_valid( $name ) {
168 return (bool) preg_match( self::PROP_NAME_REGEX, $name );
169 }
170
171 /**
172 * Sanitize property values for URL serialization.
173 *
174 * Converts array values to appropriate formats to prevent http_build_query()
175 * from creating bracket notation (e.g., prop[0], prop[1]) which violates
176 * the property name regex.
177 *
178 * @param array $properties Event properties.
179 * @return array Sanitized properties.
180 */
181 private static function sanitize_property_values( $properties ) {
182 foreach ( $properties as $key => $value ) {
183 if ( ! is_array( $value ) ) {
184 continue;
185 }
186
187 if ( empty( $value ) ) {
188 // Empty array becomes empty string.
189 $properties[ $key ] = '';
190 continue;
191 }
192
193 // Check if array is indexed (not associative) and contains only scalar values.
194 $is_indexed_array = array_keys( $value ) === range( 0, count( $value ) - 1 );
195 $has_scalar_only = ! array_filter(
196 $value,
197 function ( $item ) {
198 return is_array( $item ) || is_object( $item );
199 }
200 );
201
202 if ( $is_indexed_array && $has_scalar_only ) {
203 // Indexed arrays with scalar values: join as comma string.
204 $properties[ $key ] = implode( ',', array_map( 'strval', $value ) );
205 continue;
206 }
207
208 // Associative arrays or nested arrays become JSON strings.
209 $encoded = wp_json_encode( $value, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES );
210 $properties[ $key ] = ( false === $encoded ) ? '' : $encoded;
211 }
212
213 return $properties;
214 }
215
216 /**
217 * Check if a SOCKS proxy is configured.
218 *
219 * The Requests library doesn't support SOCKS proxies, so we need to fall back
220 * to wp_remote_get() which respects WordPress proxy settings.
221 *
222 * @return bool True if a SOCKS proxy is configured.
223 */
224 private static function is_socks_proxy_configured() {
225 if ( ! defined( 'WP_PROXY_HOST' ) || ! is_string( WP_PROXY_HOST ) || '' === WP_PROXY_HOST ) {
226 return false;
227 }
228
229 return self::is_socks_proxy_host( (string) WP_PROXY_HOST );
230 }
231
232 /**
233 * Check if a proxy host string indicates a SOCKS proxy.
234 *
235 * @param string $proxy_host The proxy host value.
236 * @return bool True if the host indicates a SOCKS proxy.
237 */
238 public static function is_socks_proxy_host( $proxy_host ) {
239 $proxy_host = strtolower( $proxy_host );
240
241 return 0 === strpos( $proxy_host, 'socks5://' )
242 || 0 === strpos( $proxy_host, 'socks4://' )
243 || 0 === strpos( $proxy_host, 'socks://' );
244 }
245
246 /**
247 * Send pixel requests using batched non-blocking HTTP calls.
248 *
249 * Uses Requests library's request_multiple() for parallel execution via curl_multi.
250 *
251 * @param array $pixels Array of pixel URLs to send.
252 * @return bool True on success.
253 */
254 public static function send_pixels_batched( $pixels ) {
255 if ( empty( $pixels ) ) {
256 return true;
257 }
258
259 // Check if batching is supported.
260 // Note: WpOrg\Requests\Requests doesn't support SOCKS proxies, so we fall back
261 // to individual wp_remote_get() requests which respect WP_PROXY_* settings.
262 $can_batch = ( class_exists( 'WpOrg\Requests\Requests' ) && method_exists( 'WpOrg\Requests\Requests', 'request_multiple' ) )
263 || ( class_exists( 'Requests' ) && method_exists( 'Requests', 'request_multiple' ) );
264 $can_batch = $can_batch && ! self::is_socks_proxy_configured();
265
266 if ( ! $can_batch ) {
267 // Fallback to individual requests.
268 foreach ( $pixels as $pixel ) {
269 self::send_pixel( $pixel );
270 }
271 return true;
272 }
273
274 // Add timestamp and nocache to all pixels.
275 $pixels_to_send = array();
276 foreach ( $pixels as $pixel ) {
277 $pixels_to_send[] = self::add_request_timestamp_and_nocache( $pixel );
278 }
279
280 // Build request array for batch sending.
281 $requests = array();
282 $options = array(
283 'blocking' => false, // Non-blocking mode.
284 'timeout' => 1,
285 );
286
287 foreach ( $pixels_to_send as $pixel ) {
288 $requests[] = array(
289 'url' => $pixel,
290 'headers' => array(),
291 'data' => array(),
292 'type' => 'GET',
293 );
294 }
295
296 try {
297 if ( class_exists( 'WpOrg\Requests\Requests' ) ) {
298 \WpOrg\Requests\Requests::request_multiple( $requests, $options );
299 } elseif ( class_exists( 'Requests' ) ) {
300 \Requests::request_multiple( $requests, $options ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.requestsDeprecated
301 }
302 } catch ( \Exception $e ) {
303 // Log error but don't break the site - tracking pixels should fail gracefully.
304 $error_message = 'WooCommerce Analytics: Batch pixel request failed - ' . $e->getMessage();
305 if ( function_exists( 'wc_get_logger' ) ) {
306 wc_get_logger()->error( $error_message, array( 'source' => 'woocommerce-analytics' ) );
307 } else {
308 // Fallback for MU-plugin stage when WooCommerce logger is not available.
309 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
310 error_log( $error_message );
311 }
312 return false;
313 }
314
315 return true;
316 }
317
318 /**
319 * Send a single pixel request.
320 *
321 * @param string $pixel Pixel URL.
322 * @return bool True on success.
323 */
324 public static function send_pixel( $pixel ) {
325 $pixel = self::add_request_timestamp_and_nocache( $pixel );
326
327 wp_remote_get(
328 $pixel,
329 array(
330 'blocking' => false,
331 'redirection' => 2,
332 'httpversion' => '1.1',
333 'timeout' => 1,
334 )
335 );
336
337 return true;
338 }
339 }
340