PluginProbe ʕ •ᴥ•ʔ
Microsoft Clarity / 0.10.28
Microsoft Clarity v0.10.28
0.10.29 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 / clarity.php
microsoft-clarity Last commit date
includes 1 week ago js 1 week ago LICENSE.txt 5 years ago clarity-collect-batch.php 5 months ago clarity-collect-storage.php 5 months ago clarity-hooks.php 6 months ago clarity-page.php 1 week ago clarity-server-analytics.php 2 months ago clarity.php 1 week ago index.php 5 years ago readme.txt 1 week ago
clarity.php
738 lines
1 <?php
2
3 /**
4 * Plugin Name: Microsoft Clarity
5 * Plugin URI: https://clarity.microsoft.com/
6 * Description: With data and session replay from Clarity, you'll see how people are using your site — where they get stuck and what they love.
7 * Version: 0.10.28
8 * Author: Microsoft
9 * Author URI: https://www.microsoft.com/en-us/
10 * License: MIT
11 * License URI: https://docs.opensource.microsoft.com/content/releasing/license.html
12 */
13
14 require_once plugin_dir_path(__FILE__) . '/includes/brandagent-config.php';
15 require_once plugin_dir_path(__FILE__) . '/includes/brandagent-webhooks.php';
16 require_once plugin_dir_path(__FILE__) . '/includes/brandagent-custom-webhooks.php';
17 require_once plugin_dir_path(__FILE__) . '/includes/brandagent-rest-api.php';
18 require_once plugin_dir_path(__FILE__) . '/includes/brandagent-wordpress.php';
19 require_once plugin_dir_path(__FILE__) . '/includes/brandagent-content-webhooks.php';
20 require_once plugin_dir_path(__FILE__) . '/clarity-page.php';
21 require_once plugin_dir_path(__FILE__) . '/clarity-hooks.php';
22 require_once plugin_dir_path(__FILE__) . '/clarity-server-analytics.php';
23
24 /**
25 * Runs when Clarity Plugin is activated.
26 */
27 register_activation_hook(__FILE__, 'clarity_on_activation');
28 add_action('admin_init', 'clarity_activation_redirect');
29
30 /**
31 * Whether WooCommerce is active for the blog that is currently switched in.
32 *
33 * class_exists( 'WooCommerce' ) cannot answer this inside a switch_to_blog() loop: switching swaps
34 * DB and global context but never loads or unloads plugin code, so the class is present or absent
35 * for the whole request based on whichever blog bootstrapped it, and every iteration would get the
36 * same answer. The active_plugins option is per-blog and is re-read after each switch, and
37 * is_plugin_active() also covers a network-activated WooCommerce.
38 *
39 * @return bool True when WooCommerce is active for the current blog.
40 */
41 function clarity_is_woocommerce_active_for_current_blog()
42 {
43 if ( ! function_exists( 'is_plugin_active' ) ) {
44 require_once ABSPATH . 'wp-admin/includes/plugin.php';
45 }
46
47 return is_plugin_active( 'woocommerce/woocommerce.php' );
48 }
49
50 /**
51 * Fire a non-blocking Brand Agent lifecycle notification, retrying once on transport failure.
52 *
53 * Even with blocking=false, wp_remote_post() can fail immediately (e.g. invalid URL, transport
54 * error), so every lifecycle call shares the same single retry and the same log shape.
55 *
56 * @param string $endpoint Absolute endpoint URL.
57 * @param array $request_args Arguments passed straight to wp_remote_post().
58 * @param string $log_label Endpoint name used in the log message.
59 * @return array|WP_Error Response from the final attempt.
60 */
61 function brandagent_notify_lifecycle_endpoint( $endpoint, $request_args, $log_label )
62 {
63 $response = wp_remote_post( $endpoint, $request_args );
64 if ( is_wp_error( $response ) ) {
65 brandagent_log(
66 'BrandAgent Lifecycle: ' . $log_label . ' call failed; retrying once',
67 array(
68 'endpoint' => $endpoint,
69 'error' => $response->get_error_message(),
70 )
71 );
72
73 $response = wp_remote_post( $endpoint, $request_args );
74 if ( is_wp_error( $response ) ) {
75 brandagent_log(
76 'BrandAgent Lifecycle: ' . $log_label . ' call failed after retry',
77 array(
78 'endpoint' => $endpoint,
79 'error' => $response->get_error_message(),
80 )
81 );
82 }
83 }
84
85 return $response;
86 }
87
88 /**
89 * Plugin activation callback. Registers option to redirect on next admin load.
90 */
91 function clarity_on_activation($network_wide)
92 {
93 // update activate option
94 clrt_update_clarity_options('activate', $network_wide);
95
96 // Register Brand Agent routes and flush rewrite rules
97 brandagent_register_routes();
98 flush_rewrite_rules();
99
100 // Notify the BA server that the plugin was installed and trigger upsell product ingest
101 // (fire-and-forget, non-blocking). On multisite network activation, notify once per site
102 // so backend state is consistent for each subsite's home_url().
103 $clarity_server_url = BrandAgent_Config::get_clarity_server_url();
104 if ( ! empty( $clarity_server_url ) ) {
105 // WooCommerce activation is per-site on multisite, so the check has to happen inside the
106 // switch_to_blog() loop rather than once globally - otherwise a network activation would
107 // route every subsite down whichever branch the current blog happens to match. The check
108 // reads the per-blog active-plugins option because class_exists() is not switch-aware.
109 $store_urls = array();
110
111 if ( is_multisite() && $network_wide ) {
112 foreach ( get_sites() as $site ) {
113 switch_to_blog( (int) $site->blog_id );
114 $store_urls[] = array(
115 'url' => home_url(),
116 'has_woo' => clarity_is_woocommerce_active_for_current_blog(),
117 );
118 restore_current_blog();
119 }
120 } else {
121 $store_urls[] = array(
122 'url' => home_url(),
123 'has_woo' => clarity_is_woocommerce_active_for_current_blog(),
124 );
125 }
126
127 $base_url = trailingslashit( $clarity_server_url );
128 $plugin_installed_endpoint = $base_url . 'woocommerce/plugin-installed';
129 $upsell_ingest_endpoint = $base_url . 'woocommerce/upsell-ingest';
130 $wordpress_plugin_installed_endpoint = $base_url . 'wordpress/plugin-installed';
131
132 foreach ( $store_urls as $store ) {
133 $store_url = $store['url'];
134 $request_args = array(
135 'blocking' => false,
136 'timeout' => 3,
137 'headers' => array( 'Content-Type' => 'application/json' ),
138 'body' => wp_json_encode( array( 'storeUrl' => $store_url ) ),
139 );
140
141 if ( ! $store['has_woo'] ) {
142 // Plain WordPress: create AdvertiserMetadata stamped platform=WordPress. There is no
143 // companion upsell-ingest call because a content site has no catalogue to pre-index.
144 brandagent_notify_lifecycle_endpoint( $wordpress_plugin_installed_endpoint, $request_args, 'wordpress plugin-installed' );
145
146 continue;
147 }
148
149 // plugin-installed: BA server creates AdvertiserMetadata for this store.
150 brandagent_notify_lifecycle_endpoint( $plugin_installed_endpoint, $request_args, 'plugin-installed' );
151
152 // upsell-ingest: pre-index products so they are ready before the merchant reaches publish.
153 brandagent_notify_lifecycle_endpoint( $upsell_ingest_endpoint, $request_args, 'upsell-ingest' );
154 }
155 }
156
157 // Don't do redirects when multiple plugins are bulk activated
158 if (
159 (isset($_REQUEST['action']) && 'activate-selected' === $_REQUEST['action']) &&
160 (isset($_POST['checked']) && count($_POST['checked']) > 1)
161 ) {
162 return;
163 }
164 add_option('clarity_activation_redirect', wp_get_current_user()->ID);
165 }
166
167 /**
168 * Redirects the user after plugin activation
169 */
170 function clarity_activation_redirect()
171 {
172 // Make sure it is the user that activated the plugin
173 if (is_user_logged_in() && intval(get_option('clarity_activation_redirect', false)) === wp_get_current_user()->ID) {
174 // Make sure we don't redirect again
175 delete_option('clarity_activation_redirect');
176 wp_safe_redirect(admin_url('admin.php?page=microsoft-clarity'));
177 exit;
178 }
179 }
180
181 /**
182 * Runs when Clarity Plugin is deactivated.
183 */
184 register_deactivation_hook(__FILE__, 'clarity_on_deactivation');
185 function clarity_on_deactivation($network_wide)
186 {
187 clrt_update_clarity_options('deactivate', $network_wide);
188 flush_rewrite_rules();
189 }
190
191 /**
192 * Runs when Clarity Plugin is uninstalled.
193 */
194 register_uninstall_hook(__FILE__, 'clarity_on_uninstall');
195 function clarity_on_uninstall()
196 {
197 // Uninstall hook doesn't pass $network_wide flag.
198 // Set it to true to delete options for all the sites in a multisite setup (in a single site setup, the flag is irrelevant).
199
200 clrt_update_clarity_options('uninstall', true);
201 }
202
203 /**
204 * Updates clarity options based on the plugin's action and WordPress installation type.
205 *
206 * @since 0.10.1
207 *
208 * @param string $action activate, deactivate or uninstall.
209 * @param bool $network_wide In case of a multisite installation, should the action be performed on all the sites or not.
210 */
211 function clrt_update_clarity_options($action, $network_wide)
212 {
213 if (is_multisite() && $network_wide) {
214 $sites = get_sites();
215 foreach ($sites as $site) {
216 switch_to_blog($site->blog_id);
217
218 clrt_update_clarity_options_handler($action, $network_wide);
219
220 restore_current_blog();
221 }
222 } else {
223 clrt_update_clarity_options_handler($action, $network_wide);
224 }
225 }
226
227 /**
228 * @since 0.10.1
229 */
230 function clrt_update_clarity_options_handler($action, $network_wide)
231 {
232 switch ($action) {
233 case 'activate':
234 $id = get_option('clarity_wordpress_site_id');
235
236 if (! $id) {
237 update_option('clarity_wordpress_site_id', wp_generate_uuid4());
238 }
239
240 // Clear the cached version flag so a reinstall re-evaluates instead of inheriting a stale banner.
241 delete_transient('clarity_is_latest_plugin_version');
242
243 // Drop the cached BrandAgent backend URL (24h TTL). Content webhooks post to it on the hot path
244 // and read it from this cache, so a plugin update/reactivate is the recovery lever when the
245 // backend moves — without this the store keeps posting to the old host until the TTL lapses.
246 BrandAgent_Config::clear_cache();
247
248 // Initialize BAInjectFrontendScript with default value
249 if ( get_option( 'BAInjectFrontendScript' ) === false ) {
250 add_option( 'BAInjectFrontendScript', 'false' );
251 brandagent_log( 'BrandAgent Lifecycle: Initialized BAInjectFrontendScript option', array( 'value' => 'false' ) );
252 }
253
254 // Resume all BrandAgent webhooks
255 if ( class_exists( 'BrandAgent_Webhooks' ) ) {
256 $resumed_count = BrandAgent_Webhooks::resume_all_brandagent_webhooks();
257 brandagent_log( 'BrandAgent Lifecycle: Plugin activated; resumed webhooks', array( 'resumed_count' => $resumed_count ) );
258 } else {
259 brandagent_log( 'BrandAgent Lifecycle: Plugin activated; BrandAgent_Webhooks class not available for resume' );
260 }
261
262 break;
263 case 'deactivate':
264 // Plugin activation/deactivation is handled differently in the database for site-level and network-wide activation.
265 // Ensure a complete deactivation if the plugin was activated per site before network-wide activation.
266
267 $plugin_name = plugin_basename(__FILE__);
268 if ($network_wide && in_array($plugin_name, (array) get_option('active_plugins', array()), true)) {
269 deactivate_plugins($plugin_name, true, false);
270 }
271
272 update_option('clarity_wordpress_site_id', '');
273 update_option('clarity_project_id', '');
274 clarity_flush_and_clear_collect_recurring();
275
276 // Pause all BrandAgent webhooks
277 if ( class_exists( 'BrandAgent_Webhooks' ) ) {
278 $paused_count = BrandAgent_Webhooks::pause_all_brandagent_webhooks();
279 brandagent_log( 'BrandAgent Lifecycle: Plugin deactivated; paused webhooks', array( 'paused_count' => $paused_count ) );
280 } else {
281 brandagent_log( 'BrandAgent Lifecycle: Plugin deactivated; BrandAgent_Webhooks class not available for pause' );
282 }
283
284 break;
285 case 'uninstall':
286 brandagent_log( 'BrandAgent Lifecycle: Plugin uninstall started' );
287 handle_brandagent_uninstall();
288
289 delete_option('clarity_wordpress_site_id');
290 delete_option('clarity_project_id');
291 delete_option( 'BAOauthSuccess' );
292 delete_option( 'BAInjectFrontendScript' );
293 delete_option( 'BAOauthRepairDone' );
294 delete_option( 'BAWebhooksCreated' );
295 delete_option( 'BAWebhooksBackfillDone' );
296 delete_option( 'clarity_ba_eligible_triggered' );
297 // Plain-WordPress connect bookkeeping. Without this a reinstall still looks opted in, and
298 // brandagent_wordpress_maybe_resume_connect() silently reconnects a site the merchant just
299 // removed the plugin from.
300 delete_option( 'brandagent_wp_connect_optin' );
301 delete_option( 'brandagent_wp_connect_attempts' );
302 delete_transient( 'brandagent_wp_connect_throttle' );
303 delete_transient( 'brandagent_connect_nonce' );
304 // Cleanup for the option used up to version 0.10.16. Should remove this after users migrate to 0.10.17+ where this option is no longer used.
305 delete_option('clarity_collect_batch');
306 // Remove the cached banner flag so it can't linger and resurface on reinstall.
307 delete_transient('clarity_is_latest_plugin_version');
308 delete_option('clarity_dismissed_update_version');
309 clarity_flush_and_clear_collect_recurring();
310 clarity_drop_collect_events_table();
311
312 // Delete all BrandAgent webhooks
313 if ( class_exists( 'BrandAgent_Webhooks' ) ) {
314 $deleted_count = BrandAgent_Webhooks::delete_all_brandagent_webhooks();
315 brandagent_log( 'BrandAgent Lifecycle: Plugin uninstall deleted webhooks', array( 'deleted_count' => $deleted_count ) );
316 } else {
317 brandagent_log( 'BrandAgent Lifecycle: Plugin uninstall; BrandAgent_Webhooks class not available for deletion' );
318 }
319
320 // Delete stored HMAC secret for this store
321 if ( function_exists( 'brandagent_delete_hmac_secret' ) ) {
322 brandagent_delete_hmac_secret();
323 } else {
324 brandagent_log( 'BrandAgent Lifecycle: Plugin uninstall; HMAC delete helper not available' );
325 }
326
327 brandagent_log( 'BrandAgent Lifecycle: Plugin uninstall completed' );
328 break;
329 }
330 }
331
332 /**
333 * Escapes the plugin id characters.
334 */
335 function escape_value_for_script($value)
336 {
337 return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
338 }
339
340 /**
341 * Adds the script to run clarity.
342 */
343 add_action('wp_head', 'clarity_add_script_to_header');
344 function clarity_add_script_to_header()
345 {
346 $clarity_project_id = get_option('clarity_project_id');
347 if (! empty($clarity_project_id)) {
348 ?>
349 <script type="text/javascript">
350 (function(c, l, a, r, i, t, y) {
351 c[a] = c[a] || function() {
352 (c[a].q = c[a].q || []).push(arguments)
353 };
354 t = l.createElement(r);
355 t.async = 1;
356 t.src = "https://www.clarity.ms/tag/" + i + "?ref=wordpress";
357 y = l.getElementsByTagName(r)[0];
358 y.parentNode.insertBefore(t, y);
359 })(window, document, "clarity", "script", "<?php echo escape_value_for_script($clarity_project_id); ?>");
360 </script>
361 <?php
362 }
363 }
364
365 /**
366 * Determines whether the Brand Agent frontend loader should run on the current
367 * request: OAuth must be connected and the current page must be an allowed
368 * (non-admin / non-login) context with BAInjectFrontendScript enabled.
369 */
370 function brand_agent_should_inject_frontend_script()
371 {
372 // Inject if: oauth succeeded AND (WooCommerce page OR BAInjectFrontendScript=true)
373 return get_option( 'BAOauthSuccess' ) == 1 && should_inject_brand_agents_script();
374 }
375
376 /**
377 * Enqueue the Brand Agent loader through the WordPress Script Modules API
378 * (WordPress 6.5+). Letting core emit the module tag guarantees it is printed
379 * AFTER the import map (in <head> for block themes, in the footer for classic
380 * themes), so the import map is always registered first. That keeps the
381 * WooCommerce Interactivity API (mini-cart, add-to-cart, product collections,
382 * checkout) working in browsers that reject import maps registered after a
383 * module load has started (Firefox, Chrome < 133, Android WebView).
384 */
385 add_action('wp_enqueue_scripts', 'brand_agent_enqueue_frontend_module');
386 function brand_agent_enqueue_frontend_module()
387 {
388 // Only handled here on WordPress 6.5+; older versions use the inline fallback.
389 if ( ! function_exists( 'wp_enqueue_script_module' ) ) {
390 return;
391 }
392
393 if ( ! brand_agent_should_inject_frontend_script() ) {
394 return;
395 }
396
397 wp_enqueue_script_module(
398 'brand-agent-frontend',
399 BrandAgent_Config::get_frontend_injection_url(),
400 array(),
401 null
402 );
403 }
404
405 /**
406 * Fallback loader for WordPress versions without the Script Modules API (< 6.5).
407 * Those versions predate the block Interactivity API import map, so injecting the
408 * module from an inline script is safe. Insertion is still deferred until the
409 * document has parsed as a defensive measure.
410 */
411 add_action('wp_head', 'brand_agent_add_script_to_header');
412 function brand_agent_add_script_to_header()
413 {
414 // WordPress 6.5+ is handled by the Script Modules API path above.
415 if ( function_exists( 'wp_enqueue_script_module' ) ) {
416 return;
417 }
418
419 if ( ! brand_agent_should_inject_frontend_script() ) {
420 return;
421 }
422
423 $frontend_injection_url = BrandAgent_Config::get_frontend_injection_url();
424 ?>
425 <script>
426 (function() {
427 var injectBrandAgentLoader = function() {
428 var script = document.createElement('script');
429 script.src = '<?php echo esc_js($frontend_injection_url); ?>';
430 script.type = 'module';
431 document.head.appendChild(script);
432 };
433 if (document.readyState === 'loading') {
434 document.addEventListener('DOMContentLoaded', injectBrandAgentLoader);
435 } else {
436 injectBrandAgentLoader();
437 }
438 })();
439 </script>
440 <?php
441 }
442
443 /**
444 * Adds the page link to the Microsoft Clarity block on installed plugin page.
445 */
446 add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'clarity_page_link');
447 function clarity_page_link($links)
448 {
449 $url = get_admin_url() . 'admin.php?page=microsoft-clarity';
450 $clarity_link = "<a href='$url'>" . __('Clarity Dashboard') . '</a>';
451 array_unshift($links, $clarity_link);
452 return $links;
453 }
454
455 /**
456 * Retrieving the currently installed plugin version
457 */
458 function get_installed_plugin_version()
459 {
460 if (! function_exists('get_plugin_data')) {
461 require_once(ABSPATH . 'wp-admin/includes/plugin.php');
462 }
463
464 $plugin_data = get_plugin_data(plugin_dir_path(__FILE__) . 'clarity.php');
465
466 return $plugin_data['Version'];
467 }
468
469 /**
470 * One-time migration: repair BAOauthSuccess if it was cleared by the
471 * register_setting('general') bug (fixed in 0.10.23) while
472 * BAInjectFrontendScript remained true and the HMAC secret is still present.
473 */
474 add_action('admin_init', 'clarity_repair_oauth_status');
475 function clarity_repair_oauth_status() {
476 if ( get_option('BAOauthRepairDone') ) {
477 return;
478 }
479
480 if ( get_option('BAInjectFrontendScript') === 'true'
481 && brandagent_get_hmac_secret()
482 && get_option('BAOauthSuccess') != 1 ) {
483 update_option('BAOauthSuccess', true);
484 brandagent_log( 'BrandAgent OAuth Repair: Restored BAOauthSuccess from existing Brand Agent state' );
485 }
486
487 update_option('BAOauthRepairDone', true);
488 }
489
490 /**
491 * One-time migration: backfill BAWebhooksCreated for stores that already have
492 * BrandAgent webhooks set up before this flag existed. Without this, a future
493 * config/update would unnecessarily re-trigger complete-onboarding +
494 * create_webhooks() for healthy stores. Stores that completed OAuth but never
495 * got webhooks (the bug we're fixing) are intentionally left without the flag,
496 * so the next config/update will recover them.
497 */
498 add_action('admin_init', 'clarity_backfill_webhooks_created');
499 function clarity_backfill_webhooks_created() {
500 if ( get_option('BAWebhooksBackfillDone') ) {
501 return;
502 }
503
504 // Defer if WooCommerce isn't ready yet — without WC_Webhook, has_any_brandagent_webhook()
505 // would return false for healthy stores and we'd mark the backfill done with the flag
506 // unset, causing one unnecessary complete-onboarding round-trip on the next config/update.
507 if ( ! class_exists('BrandAgent_Webhooks') || ! class_exists('WC_Webhook') ) {
508 return;
509 }
510
511 if ( get_option('BAOauthSuccess') == 1
512 && get_option('BAInjectFrontendScript') === 'true'
513 && BrandAgent_Webhooks::has_any_brandagent_webhook() ) {
514 update_option('BAWebhooksCreated', true);
515 }
516
517 update_option('BAWebhooksBackfillDone', true);
518 }
519
520 /**
521 * Refresh the cached "update available" flag used by the admin banner.
522 * Gated on core's update_plugins->response (the same source the Update button
523 * installs from), so the banner never offers an update that can't yet be installed.
524 */
525 add_action('admin_init', 'check_if_installed_plugin_version_is_latest');
526 function check_if_installed_plugin_version_is_latest()
527 {
528 // Skip for ajax requests.
529 if (wp_doing_ajax()) {
530 return;
531 }
532
533 // Throttle the check to every 5 minutes.
534 if (get_transient('clarity_is_latest_plugin_version') !== false) {
535 return;
536 }
537
538 // Refresh core's update data (self-throttled internally) and read its verdict.
539 wp_update_plugins();
540
541 $plugin_file = plugin_basename(__FILE__);
542 $updates = get_site_transient('update_plugins');
543 $update_available = isset($updates->response[$plugin_file]->new_version);
544
545 set_transient('clarity_is_latest_plugin_version', $update_available ? '0' : '1', 5 * 60); // 5 minutes cache
546 }
547
548 /**
549 * Clear cached plugin-version status after this plugin is installed or updated.
550 * This is needed to avoid showing a banner after update due to stale cache.
551 */
552 add_action('upgrader_process_complete', 'clarity_invalidate_latest_version_transient_on_update', 10, 2);
553 function clarity_invalidate_latest_version_transient_on_update($upgrader_object, $options)
554 {
555 if (! is_array($options) || ($options['type'] ?? '') !== 'plugin') {
556 return;
557 }
558
559 $action = $options['action'] ?? '';
560 if ($action !== 'update' && $action !== 'install') {
561 return;
562 }
563
564 // Bulk updates pass a 'plugins' array; single update/install passes a 'plugin' string.
565 $affected_plugins = (array) ($options['plugins'] ?? array());
566 if (! empty($options['plugin'])) {
567 $affected_plugins[] = $options['plugin'];
568 }
569
570 // On overwrite-install the affected plugin isn't always reported; clear anyway to be safe.
571 if (! empty($affected_plugins) && ! in_array(plugin_basename(__FILE__), $affected_plugins, true)) {
572 return;
573 }
574
575 delete_transient('clarity_is_latest_plugin_version');
576
577 // A plugin update is also the recovery lever for a moved BrandAgent backend: drop the cached backend
578 // URL (24h TTL) that content webhooks post to, so the new version re-fetches instead of posting to a
579 // stale host until the TTL lapses.
580 BrandAgent_Config::clear_cache();
581 }
582
583 /**
584 * Check if script should be injected on current page
585 */
586 function should_inject_brand_agents_script() {
587 if ( get_option( 'BAInjectFrontendScript', 'false' ) !== 'true' ) {
588 return false;
589 }
590
591 // Don't inject on admin pages
592 if ( is_admin() ) {
593 return false;
594 }
595
596 // Don't inject on login/register pages
597 if ( function_exists( 'is_login' ) && is_login() ) {
598 return false;
599 }
600
601 // Don't inject on wp-login.php
602 if ( $GLOBALS['pagenow'] === 'wp-login.php' ) {
603 return false;
604 }
605
606 // Inject on all other pages
607 return true;
608 }
609
610 /**
611 * Brand Agent Proxy Endpoints
612 * Rewrite rules and handlers for proxying requests from the frontend to the
613 * BrandAgent backend server. HMAC helper functions are defined in
614 * includes/brandagent-config.php (loaded first to be available during OAuth callback).
615 */
616
617 /**
618 * Register rewrite rules for Brand Agent proxy endpoints
619 */
620 function brandagent_register_routes() {
621 add_rewrite_rule(
622 '^a/msba/(.*)',
623 'index.php?brandagent_api=1&brandagent_path=$matches[1]',
624 'top'
625 );
626 }
627 add_action( 'init', 'brandagent_register_routes' );
628
629 /**
630 * Register query vars for Brand Agent endpoints
631 */
632 function brandagent_register_query_vars( $vars ) {
633 $vars[] = 'brandagent_api';
634 $vars[] = 'brandagent_path';
635 return $vars;
636 }
637 add_filter( 'query_vars', 'brandagent_register_query_vars' );
638
639 /**
640 * Handle Brand Agent custom endpoint requests
641 */
642 function brandagent_handle_custom_endpoint() {
643 if ( intval( get_query_var( 'brandagent_api' ) ) === 1 ) {
644 require_once plugin_dir_path( __FILE__ ) . 'includes/brandagent-endpoint.php';
645 exit;
646 }
647 }
648 add_action( 'template_redirect', 'brandagent_handle_custom_endpoint' );
649
650 /**
651 * Register Brand Agent REST API endpoints
652 */
653 function brandagent_register_rest_api() {
654 $rest_api = new BrandAgent_REST_API();
655 $rest_api->register_routes();
656 }
657 add_action( 'rest_api_init', 'brandagent_register_rest_api' );
658
659 /**
660 * Check for pending webhook deletion after WordPress is initialized
661 * This handles the case where webhook deletion was requested but WooCommerce wasn't loaded yet
662 * Uses 'init' hook with priority 20 to ensure WooCommerce is available
663 */
664 add_action( 'init', 'brandagent_process_pending_webhook_deletion', 20 );
665 function brandagent_process_pending_webhook_deletion() {
666 $pending = get_transient( 'brandagent_pending_webhook_deletion' );
667
668 if ( ! $pending ) {
669 return;
670 }
671
672 // Check if WooCommerce is available
673 if ( ! class_exists( 'WooCommerce' ) || ! class_exists( 'WC_Webhook' ) ) {
674 brandagent_log( 'BrandAgent Webhooks: WooCommerce not available yet for pending webhook deletion' );
675 return;
676 }
677
678 // Delete the transient first to prevent multiple attempts
679 delete_transient( 'brandagent_pending_webhook_deletion' );
680
681 if ( class_exists( 'BrandAgent_Webhooks' ) ) {
682 $deleted_count = BrandAgent_Webhooks::delete_all_brandagent_webhooks();
683 brandagent_log( 'BrandAgent Webhooks: Deleted webhooks via pending deletion', array( 'deleted_count' => $deleted_count ) );
684 } else {
685 brandagent_log( 'BrandAgent Webhooks: BrandAgent_Webhooks class not available for pending deletion' );
686 }
687 }
688
689 /**
690 * Call the Clarity dashboard uninstall endpoint so the backend drops this site's Brand Agent data.
691 * Invoked during plugin uninstall, before the local HMAC secret is deleted.
692 *
693 * Which endpoint depends on how the site onboarded, and the two are not interchangeable. A
694 * WooCommerce store is registered as Platform=WooCommerce with credentials and webhooks the
695 * WooCommerce teardown unwinds, and its secret is filed in Key Vault under a woocommerce-* name.
696 * A plain site is Platform=WordPress with a wordpress-* secret and a different signing scheme.
697 * Sending either one down the other's path fails signature verification, and because uninstall is
698 * fire-and-forget the merchant's data would be left behind with nothing to retry it.
699 */
700 function handle_brandagent_uninstall() {
701 if ( get_option( 'BAOauthSuccess' ) != 1 ) {
702 brandagent_log( 'BrandAgent Uninstall: Skipping backend uninstall because OAuth is not marked successful' );
703 return;
704 }
705
706 // Follows the credential this site actually holds, which is what "how it onboarded" means once
707 // the secret is filed under a platform-specific Key Vault name. Live activation state is the
708 // wrong signal here for the same reason it is wrong for signing: a store that deactivates
709 // WooCommerce before deleting the plugin still holds a woocommerce-* secret, so the WordPress
710 // teardown would fail verification and strand its data with nothing to retry the call.
711 if ( brandagent_get_hmac_platform() !== 'woocommerce' ) {
712 if ( function_exists( 'brandagent_wordpress_notify_uninstall' ) ) {
713 brandagent_wordpress_notify_uninstall();
714 } else {
715 brandagent_log( 'BrandAgent Uninstall: WordPress uninstall helper not available' );
716 }
717
718 return;
719 }
720
721 $site_url = home_url();
722
723 // HMAC-signed request through Clarity proxy
724 $clarity_domain = BrandAgent_Config::get_clarity_server_url();
725 $uninstall_endpoint = $clarity_domain . '/woocommerce/uninstall';
726 brandagent_log( 'BrandAgent Uninstall: Calling backend uninstall endpoint', array( 'store_url' => $site_url, 'endpoint' => $uninstall_endpoint ) );
727 $response = brandagent_sign_outbound_request( $uninstall_endpoint, wp_json_encode( array( 'storeUrl' => $site_url ) ), 'POST', 15 );
728
729 // Log error if call fails, but continue with local cleanup
730 if ( is_wp_error( $response ) ) {
731 brandagent_log( 'BrandAgent Uninstall: Failed to call backend uninstall endpoint', array( 'error' => $response->get_error_message() ) );
732 return;
733 }
734
735 $status_code = wp_remote_retrieve_response_code( $response );
736 brandagent_log( 'BrandAgent Uninstall: Backend uninstall endpoint returned', array( 'status_code' => $status_code ) );
737 }
738