PluginProbe ʕ •ᴥ•ʔ
Microsoft Clarity / trunk
Microsoft Clarity vtrunk
0.10.28 0.10.27 0.10.26 0.10.25 0.10.24 0.8.0 0.9.0 0.9.1 0.9.2 0.9.3 0.9.4 trunk 0.10.0 0.10.1 0.10.10 0.10.11 0.10.12 0.10.13 0.10.14 0.10.15 0.10.16 0.10.17 0.10.18 0.10.19 0.10.2 0.10.20 0.10.21 0.10.22 0.10.23 0.10.3 0.10.4 0.10.5 0.10.6 0.10.7 0.10.8 0.10.9 0.2 0.4 0.5 0.6 0.6.1 0.7 0.7.1 0.7.2 0.7.3 0.7.4 0.7.5
microsoft-clarity / includes / brandagent-config.php
microsoft-clarity / includes Last commit date
webhooks 5 months ago brandagent-config.php 4 days ago brandagent-content-webhooks.php 4 days ago brandagent-custom-webhooks.php 3 months ago brandagent-endpoint.php 4 days ago brandagent-rest-api.php 3 months ago brandagent-webhooks.php 3 months ago brandagent-wordpress.php 4 days ago
brandagent-config.php
616 lines
1 <?php
2 /**
3 * Brand Agent Configuration
4 *
5 * @package MicrosoftClarity
6 * @since 0.10.21
7 */
8
9 // Exit if accessed directly
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Base URL path for all BrandAgent webhooks
14 */
15 if ( ! defined( 'BRANDAGENT_WEBHOOK_BASE_URL' ) ) {
16 define( 'BRANDAGENT_WEBHOOK_BASE_URL', '/api/v1/woocommerce/webhooks/' );
17 }
18
19 /**
20 * Base URL path for BrandAgent WordPress content webhooks (plain-WordPress content sync).
21 * Distinct from BRANDAGENT_WEBHOOK_BASE_URL (WooCommerce): content webhooks are core-hook driven and
22 * do not require WooCommerce. See includes/brandagent-content-webhooks.php.
23 */
24 if ( ! defined( 'BRANDAGENT_CONTENT_WEBHOOK_BASE_URL' ) ) {
25 define( 'BRANDAGENT_CONTENT_WEBHOOK_BASE_URL', '/api/v1/wordpress/webhooks/' );
26 }
27
28 /**
29 * HMAC timestamp validation window in seconds (5 minutes)
30 * Used for replay attack prevention
31 */
32 if ( ! defined( 'BRANDAGENT_HMAC_TIMESTAMP_WINDOW' ) ) {
33 define( 'BRANDAGENT_HMAC_TIMESTAMP_WINDOW', 300 );
34 }
35
36 /**
37 * Cookie name used to store BrandAgent cart attributes in the browser.
38 */
39 if ( ! defined( 'BRANDAGENT_ATTRS_COOKIE_NAME' ) ) {
40 define( 'BRANDAGENT_ATTRS_COOKIE_NAME', 'brandagent_attrs' );
41 }
42
43 /**
44 * Lifetime of the BrandAgent attributes cookie in seconds (2 hours).
45 */
46 if ( ! defined( 'BRANDAGENT_ATTRS_COOKIE_TTL' ) ) {
47 define( 'BRANDAGENT_ATTRS_COOKIE_TTL', 2 * HOUR_IN_SECONDS );
48 }
49
50 /**
51 * Logging helper — writes to wp-content/debug.log and Query Monitor.
52 * Only logs when WP_DEBUG is enabled to avoid information disclosure in production.
53 *
54 * @param string $message The message to log.
55 * @param array $context Optional structured context to append.
56 */
57 function brandagent_log( $message, $context = array() ) {
58 if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
59 return;
60 }
61
62 if ( ! empty( $context ) && is_array( $context ) ) {
63 $sanitized_context = brandagent_sanitize_log_context( $context );
64 $encoded_context = function_exists( 'wp_json_encode' )
65 ? wp_json_encode( $sanitized_context )
66 : json_encode( $sanitized_context );
67
68 if ( $encoded_context ) {
69 $message .= ' ' . $encoded_context;
70 }
71 }
72
73 $path = WP_CONTENT_DIR . '/debug.log';
74 $line = '[' . date( 'c' ) . '] ' . $message . PHP_EOL;
75 @file_put_contents( $path, $line, FILE_APPEND );
76 }
77
78 /**
79 * Sanitize structured Brand Agent log context before writing to disk.
80 *
81 * @param array $context Context values.
82 * @return array Sanitized context.
83 */
84 function brandagent_sanitize_log_context( $context ) {
85 $sanitized = array();
86
87 foreach ( $context as $key => $value ) {
88 $sanitized[ $key ] = brandagent_sanitize_log_value( $key, $value );
89 }
90
91 return $sanitized;
92 }
93
94 /**
95 * Sanitize a single Brand Agent log value.
96 *
97 * @param string $key Context key.
98 * @param mixed $value Context value.
99 * @return mixed Sanitized value.
100 */
101 function brandagent_sanitize_log_value( $key, $value ) {
102 $sensitive_key_fragments = array(
103 'authorization',
104 'billing',
105 'body',
106 'clientinformation',
107 'cookie',
108 'customer',
109 'email',
110 'payload',
111 'phone',
112 'secret',
113 'shipping',
114 'signature',
115 'token',
116 );
117
118 $key_lower = strtolower( (string) $key );
119 foreach ( $sensitive_key_fragments as $fragment ) {
120 if ( strpos( $key_lower, $fragment ) !== false ) {
121 return '[redacted]';
122 }
123 }
124
125 if ( is_array( $value ) ) {
126 return brandagent_sanitize_log_context( $value );
127 }
128
129 if ( is_object( $value ) ) {
130 return '[object ' . get_class( $value ) . ']';
131 }
132
133 if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) || $value === null ) {
134 return $value;
135 }
136
137 $string_value = (string) $value;
138 if ( strlen( $string_value ) > 200 ) {
139 return substr( $string_value, 0, 200 ) . '...';
140 }
141
142 return $string_value;
143 }
144
145 /**
146 * Brand Agent Configuration Class
147 */
148 class BrandAgent_Config {
149
150 /**
151 * Clarity server base URL
152 *
153 * @var string
154 */
155 private static $clarity_server_url = 'https://clarity.microsoft.com';
156
157 /**
158 * Cache key for backend URL
159 *
160 * @var string
161 */
162 private static $cache_key = 'brandagent_backend_url';
163
164 /**
165 * Cache duration (24 hours)
166 *
167 * @var int
168 */
169 private static $cache_duration = 86400;
170
171 /**
172 * Fetch backend base URL from Clarity server
173 *
174 * @return string|false Backend base URL or false on failure
175 */
176 private static function fetch_backend_url_from_clarity() {
177 $config_endpoint = self::get_clarity_server_url() . '/woocommerce/brandagent/config';
178
179 $response = wp_remote_get( $config_endpoint, array(
180 'timeout' => 10,
181 'headers' => array(
182 'Accept' => 'application/json',
183 ),
184 ) );
185
186 if ( is_wp_error( $response ) ) {
187 brandagent_log( 'BrandAgent Config: Failed to fetch from Clarity server: ' . $response->get_error_message() );
188 return false;
189 }
190
191 $status_code = wp_remote_retrieve_response_code( $response );
192 if ( $status_code !== 200 ) {
193 brandagent_log( 'BrandAgent Config: Clarity server returned status ' . $status_code );
194 return false;
195 }
196
197 $body = wp_remote_retrieve_body( $response );
198 $data = json_decode( $body, true );
199
200 if ( ! isset( $data['backendBaseUrl'] ) ) {
201 brandagent_log( 'BrandAgent Config: Invalid response from Clarity server' );
202 return false;
203 }
204
205 brandagent_log( 'BrandAgent Config: Successfully fetched backend URL from Clarity server: ' . $data['backendBaseUrl'] );
206 return $data['backendBaseUrl'];
207 }
208
209 /**
210 * Get backend base URL
211 * Fetches from Clarity server and caches for 24 hours
212 *
213 * @return string Backend base URL
214 */
215 public static function get_backend_base_url() {
216 // Local/dev override: when BRANDAGENT_BACKEND_BASE_URL is defined (e.g. via wp-config
217 // for a local wp-env store), use it directly and skip the dashboard config round-trip.
218 if ( defined( 'BRANDAGENT_BACKEND_BASE_URL' ) && BRANDAGENT_BACKEND_BASE_URL ) {
219 return rtrim( BRANDAGENT_BACKEND_BASE_URL, '/' );
220 }
221
222 // Try to get from cache first. This read is essential: get_backend_base_url() is called on the
223 // hot path of every content webhook (each post publish/update/delete), and without it every call
224 // falls through to fetch_backend_url_from_clarity() — a blocking 10s-timeout HTTP GET — on the
225 // editor's save request. The cache is invalidated by clear_cache(), which runs on plugin
226 // activation and on plugin update (see clarity.php), so a plugin update is the recovery lever
227 // when the backend URL moves.
228 $cached_url = get_transient( self::$cache_key );
229 if ( $cached_url !== false ) {
230 return $cached_url;
231 }
232
233 // Fetch from Clarity server
234 $backend_url = self::fetch_backend_url_from_clarity();
235
236 if ( $backend_url === false ) {
237 brandagent_log( 'BrandAgent Config: ERROR - Could not fetch backend URL from Clarity server' );
238 // Return empty string - plugin cannot function without this
239 return '';
240 }
241
242 // Cache the result
243 set_transient( self::$cache_key, $backend_url, self::$cache_duration );
244
245 return $backend_url;
246 }
247
248 /**
249 * Clear the cached backend URL
250 * Useful for testing or forcing a refresh
251 *
252 * @return void
253 */
254 public static function clear_cache() {
255 delete_transient( self::$cache_key );
256 }
257
258 /**
259 * Get Clarity server URL
260 *
261 * @return string Clarity server URL
262 */
263 public static function get_clarity_server_url() {
264 // Local/dev override: BRANDAGENT_CLARITY_SERVER_URL (e.g. via wp-config for a local
265 // wp-env store pointing at a dashboard on host.docker.internal). No-op in production.
266 if ( defined( 'BRANDAGENT_CLARITY_SERVER_URL' ) && BRANDAGENT_CLARITY_SERVER_URL ) {
267 return rtrim( BRANDAGENT_CLARITY_SERVER_URL, '/' );
268 }
269 return self::$clarity_server_url;
270 }
271
272 /**
273 * URL of the frontend widget loader injected into store pages.
274 *
275 * @return string Frontend injection script URL.
276 */
277 public static function get_frontend_injection_url() {
278 // Local/dev override: BRANDAGENT_FRONTEND_INJECTION_URL points the injected loader at a
279 // self-hosted build (e.g. served same-origin from the store) instead of the CDN default.
280 if ( defined( 'BRANDAGENT_FRONTEND_INJECTION_URL' ) && BRANDAGENT_FRONTEND_INJECTION_URL ) {
281 return BRANDAGENT_FRONTEND_INJECTION_URL;
282 }
283 return 'https://adsagentclientafd-b7hqhjdrf3fpeqh2.b01.azurefd.net/frontendInjection.js';
284 }
285 }
286
287 /**
288 * ============================================================================
289 * Brand Agent Encryption Helper Functions
290 * ============================================================================
291 * Used to encrypt the HMAC secret at rest in wp_options (AES-256-CBC).
292 * The encryption key is derived from wp_salt('auth'), which is defined in
293 * wp-config.php and never stored in the database.
294 */
295
296 /**
297 * Derive a 256-bit encryption key from WordPress auth salt.
298 *
299 * @return string 32-byte binary encryption key
300 */
301 function brandagent_get_encryption_key() {
302 return hash( 'sha256', wp_salt( 'auth' ), true );
303 }
304
305 /**
306 * Encrypt a value using AES-256-CBC.
307 *
308 * @param string $plaintext The value to encrypt
309 * @return string|false Encrypted value as "iv_base64:ciphertext_base64", or false on failure
310 */
311 function brandagent_encrypt( $plaintext ) {
312 $key = brandagent_get_encryption_key();
313 $iv = openssl_random_pseudo_bytes( 16 );
314 if ( $iv === false ) {
315 brandagent_log( 'BrandAgent Encrypt: ERROR - openssl_random_pseudo_bytes failed' );
316 return false;
317 }
318 $ciphertext = openssl_encrypt( $plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
319 if ( $ciphertext === false ) {
320 brandagent_log( 'BrandAgent Encrypt: ERROR - openssl_encrypt failed: ' . openssl_error_string() );
321 return false;
322 }
323 return base64_encode( $iv ) . ':' . base64_encode( $ciphertext );
324 }
325
326 /**
327 * Decrypt a value encrypted by brandagent_encrypt().
328 *
329 * @param string $encrypted The encrypted value in "iv_base64:ciphertext_base64" format
330 * @return string|false The decrypted plaintext, or false on failure
331 */
332 function brandagent_decrypt( $encrypted ) {
333 $key = brandagent_get_encryption_key();
334 $parts = explode( ':', $encrypted, 2 );
335 if ( count( $parts ) !== 2 ) {
336 brandagent_log( 'BrandAgent Decrypt: ERROR - Invalid encrypted format' );
337 return false;
338 }
339 $iv = base64_decode( $parts[0] );
340 $ciphertext = base64_decode( $parts[1] );
341 if ( $iv === false || $ciphertext === false ) {
342 brandagent_log( 'BrandAgent Decrypt: ERROR - Base64 decoding failed' );
343 return false;
344 }
345 $plaintext = openssl_decrypt( $ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv );
346 if ( $plaintext === false ) {
347 brandagent_log( 'BrandAgent Decrypt: ERROR - openssl_decrypt failed: ' . openssl_error_string() );
348 return false;
349 }
350 return $plaintext;
351 }
352
353 /**
354 * ============================================================================
355 * Brand Agent HMAC Helper Functions
356 * ============================================================================
357 * These functions must be defined here (before clarity-page.php is loaded)
358 * because they are called during the OAuth callback which runs at file load time.
359 */
360
361 /**
362 * Generate HMAC signature for WordPress requests
363 * Message format: clientId + timestamp, signed with SHA256
364 *
365 * @param string $client_id The client ID
366 * @param int $timestamp Unix timestamp
367 * @param string $secret_key The HMAC secret key
368 * @return string Base64-encoded HMAC signature
369 */
370 function brandagent_generate_hmac_signature( $client_id, $timestamp, $secret_key ) {
371 $message = $client_id . $timestamp;
372 return base64_encode( hash_hmac( 'sha256', $message, $secret_key, true ) );
373 }
374
375 /**
376 * Whether this store is a WooCommerce store (vs a plain WordPress content store).
377 *
378 * Selects which outbound HMAC scheme the plugin uses when calling the BrandAgent backend proxy
379 * endpoints (api/config/read, api/v1/init): the backend routes on which header family is present
380 * (X-WooCommerce-* vs X-WordPress-*) and each store's merchant secret is keyed by platform, so the
381 * scheme must match how the store was provisioned or auth fails.
382 *
383 * The decision follows the platform recorded alongside the credential itself — brandagent_store_hmac_secret()
384 * writes brandagent_hmac_platform in the same path as the secret, so the two can never disagree — rather than
385 * which plugins happen to be active now. The merchant secret is provisioned once per platform and never
386 * re-keyed, so a runtime plugin change (a plain-WordPress store later activating WooCommerce — the expected
387 * blog-adds-a-shop growth path — or a WooCommerce store deactivating it) must NOT flip the scheme: doing so
388 * would send the other platform's header family and 401 against a secret key that was never written for this
389 * store, silently killing the widget and content sync.
390 *
391 * Delegates to brandagent_get_hmac_platform() rather than reading a separate option: that is the single
392 * source of truth backend routing (brandagent-endpoint.php) already uses, it deliberately does NOT infer
393 * from class_exists( 'woocommerce' ), and it carries its own pre-option backfill. With no credential stored
394 * it returns '' → false, which is safe: brandagent_content_webhooks_enabled() then fails on the missing
395 * secret immediately after.
396 *
397 * @return bool True when this store holds a WooCommerce-issued credential.
398 */
399 function brandagent_is_woocommerce_store() {
400 return 'woocommerce' === brandagent_get_hmac_platform();
401 }
402
403 /**
404 * Normalize store URL for consistent formatting
405 * Matches C# backend normalization logic
406 *
407 * @param string $store_url The store URL to normalize
408 * @return string Normalized store URL
409 */
410 function brandagent_normalize_store_url( $store_url ) {
411 $normalized = strtolower( str_replace( array( 'https://', 'http://' ), '', rtrim( $store_url, '/' ) ) );
412 // Replace dots, slashes, and colons with hyphens to produce a valid Key Vault / Azure Search
413 // identifier and HMAC client id. Must stay identical to the backend C# NormalizeStoreUrl so
414 // plugin-signed requests verify; the colon only affects host:port dev stores (production
415 // home_url() has no port).
416 return str_replace( array( '.', '/', ':' ), '-', $normalized );
417 }
418
419 /**
420 * Get client ID from request URL query parameter
421 *
422 * @return string|false Client ID or false if not found
423 */
424 function brandagent_get_client_id() {
425 $client_id = isset( $_GET['clientId'] ) ? sanitize_text_field( $_GET['clientId'] ) : '';
426 if ( empty( $client_id ) ) {
427 return false;
428 }
429 return $client_id;
430 }
431
432 /**
433 * Store HMAC secret received during OAuth.
434 * The secret is encrypted with AES-256-CBC before being stored in wp_options.
435 *
436 * @param string $hmac_secret The HMAC secret
437 * @param string $platform Flow that issued this secret: 'woocommerce' or 'wordpress'.
438 * @return bool True on success
439 */
440 function brandagent_store_hmac_secret( $hmac_secret, $platform ) {
441 $store_url = home_url();
442 $normalized_store_url = brandagent_normalize_store_url( $store_url );
443 $option_key = 'brandagent_secret_key_' . $normalized_store_url;
444
445 // Clean the HMAC secret
446 $hmac_secret_clean = trim( $hmac_secret );
447 $hmac_secret_clean = str_replace( array( "\r", "\n", " " ), '', $hmac_secret_clean );
448
449 // Encrypt before storing
450 $encrypted = brandagent_encrypt( $hmac_secret_clean );
451 if ( $encrypted === false ) {
452 brandagent_log( 'BrandAgent: ERROR - HMAC secret encryption failed before storage', array( 'store_url' => $store_url ) );
453 return false;
454 }
455
456 update_option( $option_key, $encrypted );
457
458 // Record which flow issued this credential, in the same write path as the credential itself so
459 // the two can never disagree. Signing has to follow the secret we hold, not the plugins that
460 // happen to be active later.
461 update_option( 'brandagent_hmac_platform', $platform );
462
463 brandagent_log( 'BrandAgent: HMAC secret stored successfully for ' . $store_url );
464 return true;
465 }
466
467 /**
468 * Get the stored HMAC secret for this store.
469 * Decrypts the AES-256-CBC encrypted value from wp_options.
470 *
471 * @return string|false The HMAC secret key or false if not found
472 */
473 function brandagent_get_hmac_secret() {
474 $store_url = home_url();
475 $normalized_store_url = brandagent_normalize_store_url( $store_url );
476 $option_key = 'brandagent_secret_key_' . $normalized_store_url;
477
478 $stored_value = get_option( $option_key, false );
479 if ( $stored_value === false ) {
480 return false;
481 }
482
483 $decrypted = brandagent_decrypt( $stored_value );
484 if ( $decrypted === false ) {
485 brandagent_log( 'BrandAgent: ERROR - Decryption failed for HMAC secret' );
486 return false;
487 }
488 return $decrypted;
489 }
490
491 /**
492 * Get the flow that issued the HMAC secret currently stored for this store.
493 *
494 * The signing scheme must follow the stored credential rather than the current plugin load state.
495 * A store that onboarded through WooCommerce and later deactivates WooCommerce still holds a
496 * WooCommerce-issued secret, and signing that with the WordPress scheme would fail verification.
497 *
498 * @return string 'woocommerce', 'wordpress', or '' when this store has no credential yet.
499 */
500 function brandagent_get_hmac_platform() {
501 $platform = get_option( 'brandagent_hmac_platform', '' );
502 if ( $platform === 'woocommerce' || $platform === 'wordpress' ) {
503 return $platform;
504 }
505
506 // No platform recorded yet. WordPress connect ships in the same release that introduced this
507 // option and records both together, so any credential predating the option was necessarily issued
508 // by the WooCommerce flow. The WordPress opt-in marker is not credential provenance: it is written
509 // before the network connect starts and may coexist with a legacy WooCommerce secret after a failed
510 // attempt. Deliberately NOT inferred from current plugin state for the same reason.
511 if ( brandagent_get_hmac_secret() !== false ) {
512 $platform = 'woocommerce';
513 update_option( 'brandagent_hmac_platform', $platform );
514 brandagent_log( 'BrandAgent: HMAC platform backfilled as ' . $platform . ' for pre-existing credential' );
515 return $platform;
516 }
517
518 // No credential stored, so nothing can be signed yet and there is no flow to infer. Onboarding
519 // records the authoritative value; until then the caller fails closed on the missing secret.
520 return '';
521 }
522
523 /**
524 * Delete the stored HMAC secret for this store.
525 * Removes the encrypted HMAC secret from wp_options.
526 *
527 * @return bool True on success, false on failure
528 */
529 function brandagent_delete_hmac_secret() {
530 $store_url = home_url();
531 $normalized_store_url = brandagent_normalize_store_url( $store_url );
532 $option_key = 'brandagent_secret_key_' . $normalized_store_url;
533
534 $result = delete_option( $option_key );
535 delete_option( 'brandagent_hmac_platform' );
536 if ( $result ) {
537 brandagent_log( 'BrandAgent: HMAC secret deleted successfully for ' . $store_url );
538 } else {
539 brandagent_log( 'BrandAgent: HMAC secret delete skipped or failed', array( 'store_url' => $store_url ) );
540 }
541 return $result;
542 }
543
544 /**
545 * Verify HMAC signature from incoming backend request
546 * Used to authenticate requests FROM the BA server TO the WordPress plugin
547 *
548 * @param string $received_signature The signature from the request header
549 * @param string $timestamp Unix timestamp from the request
550 * @param string $request_body The raw request body
551 * @return bool True if signature is valid
552 */
553 function brandagent_verify_incoming_hmac_signature( $received_signature, $timestamp, $request_body = '' ) {
554 $secret_key = brandagent_get_hmac_secret();
555 if ( ! $secret_key ) {
556 brandagent_log( 'BrandAgent: Cannot verify signature - no HMAC secret stored' );
557 return false;
558 }
559
560 // Validate timestamp (5-minute window for replay attack prevention)
561 $time_difference = abs( time() - intval( $timestamp ) );
562 if ( $time_difference > BRANDAGENT_HMAC_TIMESTAMP_WINDOW ) {
563 brandagent_log( 'BrandAgent: Request timestamp too old: ' . $time_difference . ' seconds' );
564 return false;
565 }
566
567 // Message: store_url + timestamp + sha256(body)
568 $store_url = home_url();
569 $body_hash = hash( 'sha256', $request_body );
570 $message = $store_url . $timestamp . $body_hash;
571
572 $expected_signature = base64_encode( hash_hmac( 'sha256', $message, $secret_key, true ) );
573
574 // Constant-time comparison to prevent timing attacks
575 return hash_equals( $expected_signature, $received_signature );
576 }
577
578 /**
579 * Sign and send an outbound HTTP request to the BrandAgent backend with HMAC authentication
580 *
581 * @param string $url The full URL to send the request to
582 * @param string $body The JSON request body (optional)
583 * @param string $method HTTP method: 'POST' or 'GET' (default: 'POST')
584 * @param int $timeout Request timeout in seconds (default: 30)
585 * @return array|WP_Error Response array or WP_Error on failure
586 */
587 function brandagent_sign_outbound_request( $url, $body = '', $method = 'POST', $timeout = 30 ) {
588 $store_url = home_url();
589 $secret_key = brandagent_get_hmac_secret();
590 if ( ! $secret_key ) {
591 brandagent_log( 'BrandAgent: Cannot sign request - no HMAC secret available' );
592 return new WP_Error( 'hmac_missing', 'HMAC secret not available' );
593 }
594
595 $client_id = brandagent_normalize_store_url( $store_url );
596 $timestamp = time();
597 $signature = brandagent_generate_hmac_signature( $client_id, $timestamp, $secret_key );
598
599 $headers = array(
600 'Content-Type' => 'application/json',
601 'X-WooCommerce-Client-Id' => $client_id,
602 'X-WooCommerce-Store-Url' => $store_url,
603 'X-WooCommerce-Signature' => $signature,
604 'X-WooCommerce-Timestamp' => (string) $timestamp,
605 );
606
607 $args = array( 'timeout' => $timeout, 'headers' => $headers );
608 if ( ! empty( $body ) ) {
609 $args['body'] = $body;
610 }
611
612 return ( $method === 'GET' )
613 ? wp_remote_get( $url, $args )
614 : wp_remote_post( $url, $args );
615 }
616