PluginProbe ʕ •ᴥ•ʔ
Microsoft Clarity / 0.10.29
Microsoft Clarity v0.10.29
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-page.php
microsoft-clarity Last commit date
includes 12 hours 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 12 hours ago index.php 5 years ago readme.txt 12 hours ago
clarity-page.php
781 lines
1 <?php
2
3 /*******************************************************************************
4 * File with Clarity page
5 *******************************************************************************/
6
7 // Handle Brand Agent remove from waitlist success callback - use add_action to ensure WordPress is loaded
8 add_action( 'init', 'brandagent_handle_remove_from_waitlist_success_callback', 1 );
9 function brandagent_handle_remove_from_waitlist_success_callback() {
10 if ( isset( $_GET['brandagent_remove_from_waitlist_success'] ) && $_GET['brandagent_remove_from_waitlist_success'] == '1' ) {
11 brandagent_log( 'BrandAgent: Received remove from waitlist success callback' );
12
13 // Verify HMAC signature from request headers
14 $client_id = isset( $_SERVER['HTTP_X_WOOCOMMERCE_CLIENT_ID'] )
15 ? sanitize_text_field( $_SERVER['HTTP_X_WOOCOMMERCE_CLIENT_ID'] )
16 : '';
17 $timestamp = isset( $_SERVER['HTTP_X_WOOCOMMERCE_TIMESTAMP'] )
18 ? sanitize_text_field( $_SERVER['HTTP_X_WOOCOMMERCE_TIMESTAMP'] )
19 : '';
20 $signature = isset( $_SERVER['HTTP_X_WOOCOMMERCE_SIGNATURE'] )
21 ? sanitize_text_field( $_SERVER['HTTP_X_WOOCOMMERCE_SIGNATURE'] )
22 : '';
23
24 // Validate required headers are present
25 if ( empty( $client_id ) || empty( $timestamp ) || empty( $signature ) ) {
26 brandagent_log( 'BrandAgent: Remove from waitlist callback missing required HMAC headers' );
27 header( 'Content-Type: application/json' );
28 http_response_code( 401 );
29 echo json_encode( array( 'success' => false, 'error' => 'Missing authentication headers' ) );
30 exit;
31 }
32
33 // Validate timestamp (5-minute window for replay attack prevention)
34 $time_difference = abs( time() - intval( $timestamp ) );
35 if ( $time_difference > 300 ) {
36 brandagent_log( 'BrandAgent: Remove from waitlist callback timestamp too old: ' . $time_difference . ' seconds' );
37 header( 'Content-Type: application/json' );
38 http_response_code( 401 );
39 echo json_encode( array( 'success' => false, 'error' => 'Request timestamp expired' ) );
40 exit;
41 }
42
43 // Get the stored HMAC secret and verify signature
44 $secret_key = brandagent_get_hmac_secret();
45 if ( ! $secret_key ) {
46 brandagent_log( 'BrandAgent: Remove from waitlist callback - no HMAC secret stored' );
47 header( 'Content-Type: application/json' );
48 http_response_code( 401 );
49 echo json_encode( array( 'success' => false, 'error' => 'HMAC secret not found' ) );
50 exit;
51 }
52
53 // Compute expected signature: message = clientId + timestamp
54 $expected_signature = brandagent_generate_hmac_signature( $client_id, $timestamp, $secret_key );
55
56 // Constant-time comparison to prevent timing attacks
57 if ( ! hash_equals( $expected_signature, $signature ) ) {
58 brandagent_log( 'BrandAgent: Remove from waitlist callback HMAC signature verification failed' );
59 header( 'Content-Type: application/json' );
60 http_response_code( 401 );
61 echo json_encode( array( 'success' => false, 'error' => 'Invalid signature' ) );
62 exit;
63 }
64
65 brandagent_log( 'BrandAgent: Remove from waitlist callback HMAC signature verified successfully' );
66
67 delete_option( 'BAOauthSuccess' );
68 delete_option( 'BAInjectFrontendScript' );
69 delete_option( 'BAOauthRepairDone' );
70 delete_option( 'BAWebhooksCreated' );
71 delete_option( 'BAWebhooksBackfillDone' );
72 delete_option( 'clarity_ba_eligible_triggered' );
73 delete_option( 'brandagent_wp_connect_optin' );
74 delete_option( 'brandagent_wp_connect_attempts' );
75 delete_transient( 'brandagent_wp_connect_throttle' );
76 brandagent_delete_hmac_secret();
77
78 // Try to delete webhooks immediately if WooCommerce is available
79 $deleted_immediately = false;
80 if ( class_exists( 'WooCommerce' ) && class_exists( 'BrandAgent_Webhooks' ) ) {
81 $deleted_count = BrandAgent_Webhooks::delete_all_brandagent_webhooks();
82 brandagent_log( 'BrandAgent: Deleted ' . $deleted_count . ' webhook(s) immediately' );
83 $deleted_immediately = true;
84 }
85
86 // If WooCommerce not loaded, set transient for later deletion
87 if ( ! $deleted_immediately ) {
88 set_transient( 'brandagent_pending_webhook_deletion', true, 3600 );
89 brandagent_log( 'BrandAgent: Set pending webhook deletion transient (WooCommerce not loaded)' );
90 }
91
92 header( 'Content-Type: application/json' );
93 echo json_encode( array( 'success' => true ) );
94 exit;
95 }
96 }
97
98 // Handle WooCommerce OAuth return URL (browser redirect after user authorizes).
99 // Deferred to 'init' so that wp_remote_post() and the HTTP API are fully loaded.
100 add_action( 'init', 'brandagent_handle_oauth_callback', 1 );
101 function brandagent_handle_oauth_callback() {
102 if ( ! isset($_GET['brandagent_callback']) || $_GET['brandagent_callback'] != '1' ) {
103 return;
104 }
105
106 $oauth_token = isset($_GET['oauth_token']) ? sanitize_text_field($_GET['oauth_token']) : '';
107
108 // WooCommerce adds success=1 to the return URL when the callback was successful
109 $wc_success = isset($_GET['success']) && $_GET['success'] == '1';
110 $success = false;
111 brandagent_log( 'BrandAgent OAuth: Callback received', array(
112 'wc_success' => $wc_success,
113 'oauth_present' => ! empty( $oauth_token ),
114 ) );
115
116 if ($wc_success && !empty($oauth_token)) {
117 // Pull the HMAC secret from Clarity server (server-to-server, secret in response body)
118 if ( ! class_exists( 'BrandAgent_Config' ) ) {
119 $config_path = plugin_dir_path( __FILE__ ) . 'includes/brandagent-config.php';
120 if ( file_exists( $config_path ) ) {
121 require_once $config_path;
122 } else {
123 brandagent_log( 'BrandAgent OAuth: ERROR - Config file not found at ' . $config_path );
124 }
125 }
126
127 $clarity_server_url = BrandAgent_Config::get_clarity_server_url();
128 $fetch_url = $clarity_server_url . '/woocommerce/fetch-secret';
129
130 $request_body = wp_json_encode( array( 'oauth_token' => $oauth_token ) );
131
132 $response = wp_remote_post( $fetch_url, array(
133 'timeout' => 15,
134 'headers' => array( 'Content-Type' => 'application/json' ),
135 'body' => $request_body,
136 ) );
137
138 if ( is_wp_error( $response ) ) {
139 brandagent_log( 'BrandAgent OAuth: ERROR - wp_remote_post failed: ' . $response->get_error_message() );
140 } else {
141 $status_code = wp_remote_retrieve_response_code( $response );
142 $response_body = wp_remote_retrieve_body( $response );
143
144 if ( $status_code === 200 ) {
145 $body = json_decode( $response_body, true );
146 if ( json_last_error() !== JSON_ERROR_NONE ) {
147 brandagent_log( 'BrandAgent OAuth: ERROR - JSON parse failed: ' . json_last_error_msg() );
148 } elseif ( isset( $body['success'] ) && $body['success'] === true && ! empty( $body['hmac_secret'] ) ) {
149 $hmac_secret_stored = brandagent_store_hmac_secret( $body['hmac_secret'], 'woocommerce' );
150 update_option( 'BAOauthSuccess', true );
151 $success = true;
152 brandagent_log( 'BrandAgent OAuth: SUCCESS - HMAC secret handled, BAOauthSuccess set.', array( 'hmac_stored' => $hmac_secret_stored ) );
153 } else {
154 brandagent_log( 'BrandAgent OAuth: ERROR - Unexpected response from fetch-secret', array(
155 'status_code' => $status_code,
156 'success_present' => isset( $body['success'] ),
157 'hmac_present' => isset( $body['hmac_secret'] ) && ! empty( $body['hmac_secret'] ),
158 ) );
159 }
160 } else {
161 brandagent_log( 'BrandAgent OAuth: ERROR - fetch-secret returned non-success status', array( 'status_code' => $status_code ) );
162 }
163 }
164 } else {
165 brandagent_log( 'BrandAgent OAuth: SKIPPED - WooCommerce success or OAuth token missing', array(
166 'wc_success' => $wc_success,
167 'oauth_present' => ! empty( $oauth_token ),
168 ) );
169 }
170
171 ?>
172 <!DOCTYPE html>
173 <html>
174 <body>
175 <script>
176 if (window.opener) {
177 <?php if ($success): ?>
178 window.opener.postMessage({
179 type: 'WOOCOMMERCE_OAUTH_SUCCESS'
180 }, '*');
181 <?php else: ?>
182 window.opener.postMessage({
183 type: 'WOOCOMMERCE_OAUTH_FAILURE'
184 }, '*');
185 <?php endif; ?>
186
187 setTimeout(function() {
188 window.close();
189 }, 100);
190 }
191 </script>
192 </body>
193 </html>
194 <?php
195 exit;
196 }
197
198 // Handle Brand Agent refresh credentials callback
199 add_action( 'init', 'brandagent_handle_refresh_credentials_callback', 1 );
200 function brandagent_handle_refresh_credentials_callback() {
201 if ( ! isset( $_GET['brandagent_refresh_credentials'] ) || $_GET['brandagent_refresh_credentials'] != '1' ) {
202 return;
203 }
204
205 $oauth_token = isset( $_GET['oauth_token'] ) ? sanitize_text_field( $_GET['oauth_token'] ) : '';
206 $success = false;
207 brandagent_log( 'BrandAgent Refresh: Callback received', array( 'oauth_present' => ! empty( $oauth_token ) ) );
208
209 if ( ! empty( $oauth_token ) ) {
210 // Ensure BrandAgent_Config is loaded
211 if ( ! class_exists( 'BrandAgent_Config' ) ) {
212 $config_path = plugin_dir_path( __FILE__ ) . 'includes/brandagent-config.php';
213 if ( file_exists( $config_path ) ) {
214 require_once $config_path;
215 } else {
216 brandagent_log( 'BrandAgent Refresh: ERROR - Config file not found at ' . $config_path );
217 }
218 }
219
220 // Fetch the new HMAC secret from Clarity server using the opaque token
221 $clarity_server_url = BrandAgent_Config::get_clarity_server_url();
222 $fetch_url = $clarity_server_url . '/woocommerce/fetch-secret';
223
224 $request_body = wp_json_encode( array( 'oauth_token' => $oauth_token ) );
225
226 $response = wp_remote_post( $fetch_url, array(
227 'timeout' => 15,
228 'headers' => array( 'Content-Type' => 'application/json' ),
229 'body' => $request_body,
230 ) );
231
232 if ( is_wp_error( $response ) ) {
233 brandagent_log( 'BrandAgent Refresh: ERROR - wp_remote_post failed: ' . $response->get_error_message() );
234 } else {
235 $status_code = wp_remote_retrieve_response_code( $response );
236 $response_body = wp_remote_retrieve_body( $response );
237
238 if ( $status_code === 200 ) {
239 $body = json_decode( $response_body, true );
240 if ( json_last_error() !== JSON_ERROR_NONE ) {
241 brandagent_log( 'BrandAgent Refresh: ERROR - JSON parse failed: ' . json_last_error_msg() );
242 } elseif ( isset( $body['success'] ) && $body['success'] === true && ! empty( $body['hmac_secret'] ) ) {
243 $hmac_secret_stored = brandagent_store_hmac_secret( $body['hmac_secret'], 'woocommerce' );
244 $success = true;
245 brandagent_log( 'BrandAgent Refresh: SUCCESS - New HMAC secret handled.', array( 'hmac_stored' => $hmac_secret_stored ) );
246 } else {
247 brandagent_log( 'BrandAgent Refresh: ERROR - Unexpected response from fetch-secret', array(
248 'status_code' => $status_code,
249 'success_present' => isset( $body['success'] ),
250 'hmac_present' => isset( $body['hmac_secret'] ) && ! empty( $body['hmac_secret'] ),
251 ) );
252 }
253 } else {
254 brandagent_log( 'BrandAgent Refresh: ERROR - fetch-secret returned non-success status', array( 'status_code' => $status_code ) );
255 }
256 }
257 } else {
258 brandagent_log( 'BrandAgent Refresh: ERROR - Missing oauth_token parameter' );
259 }
260
261 header( 'Content-Type: application/json' );
262 if ( $success ) {
263 echo json_encode( array( 'success' => true ) );
264 } else {
265 http_response_code( 500 );
266 echo json_encode( array( 'success' => false, 'error' => 'Failed to refresh credentials' ) );
267 }
268 exit;
269 }
270
271 function generate_wordpress_id_option_if_empty()
272 {
273 $clarity_wp_site = get_option('clarity_wordpress_site_id');
274 if (empty($clarity_wp_site)) {
275 update_option('clarity_wordpress_site_id', wp_generate_uuid4());
276 };
277 }
278
279 /**
280 * generate a guid identifier for this wordpress site
281 * runs in the callback of register_activation_hook, rerunning here for existing plugin which updated
282 **/
283 function refresh_wordpress_id_option()
284 {
285 update_option('clarity_wordpress_site_id', wp_generate_uuid4());
286 }
287
288 /**
289 * Detects whether this site is hosted on WordPress.com.
290 **/
291 function clarity_is_wordpress_com_hosted()
292 {
293 return defined('IS_WPCOM') && IS_WPCOM;
294 }
295
296 /**
297 * Displays the embedded iframe in Clarity settings
298 **/
299 function clarity_section_iframe_callback()
300 {
301 $nonce = wp_create_nonce('wp_ajax_edit_clarity_project_id');
302
303 $clarity_project_id_option = get_option(
304 'clarity_project_id', /* option */
305 clarity_project_id_default_value() /* default */
306 );
307 $clarity_wp_site = get_option(
308 'clarity_wordpress_site_id' /* option */
309 /* default */
310 );
311
312 $site_url = home_url();
313 $hosting_type = clarity_is_wordpress_com_hosted() ? 'wpcom' : 'selfhosted';
314
315 $clarity_domain = clarity_get_embed_base_url();
316
317 $query_params = "?nonce=$nonce&integration=Wordpress&wpsite=$clarity_wp_site&siteurl=$site_url&hostingtype=$hosting_type";
318
319 // set a QP if user is admin
320 if (current_user_can('manage_options')) {
321 $query_params = $query_params . "&WPAdmin=1";
322 }
323
324 // set a QP if user is WooCommerce plugin is active
325 if (class_exists('woocommerce')) {
326 $query_params = $query_params . "&WooCommerce=1";
327 }
328
329 // set a QP if permalink structure is plain (required for Brand Agent rewrite rules)
330 if (get_option('permalink_structure') === '') {
331 $query_params = $query_params . "&PlainPermalink=1";
332 }
333
334 // Add flag to indicate Brand Agent integration is supported (0.10.21+)
335 // If this flag is missing, iframe knows user is on an older version
336 $query_params = $query_params . "&BrandAgentSupported=1";
337
338 // Plain WordPress Brand Agent requires the 0.10.28+ connect bridge, WordPress HMAC
339 // runtime proxying, and content sync contract. Keep this separate from the legacy
340 // BrandAgentSupported marker, which is also emitted by WooCommerce-capable 0.10.27.
341 $query_params = $query_params . "&WordPressBrandAgentSupported=1";
342
343 // initially set iframe src to the new users path
344 $iframe_src = $clarity_domain . $query_params;
345
346 // clarity project exist
347 if (!empty($clarity_project_id_option)) {
348 $iframe_src = $iframe_src . "&project=" . $clarity_project_id_option;
349 }
350
351 // Support deep-linking to specific pages in the embedded Clarity dashboard
352 if (isset($_GET['iframeRedirect']) && !empty($_GET['iframeRedirect'])) {
353 $iframe_redirect = sanitize_text_field($_GET['iframeRedirect']);
354 $iframe_src = $iframe_src . "&iframeRedirect=" . rawurlencode($iframe_redirect);
355 }
356
357 ?>
358 <div style="width:100%;height:100vh;padding-right:15px;margin-top:0px;box-sizing:border-box;">
359 <iframe sandbox="allow-modals allow-forms allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" src="<?php echo $iframe_src ?>" width="100%" height="100%" title="Microsoft Clarity" />
360 </div>
361 <?php
362 }
363
364 /**
365 * clarity project id default value is empty string
366 **/
367 function clarity_project_id_default_value()
368 {
369 return '';
370 }
371
372 /**
373 * Generates a menu page
374 **/
375
376 add_action('admin_menu', 'clarity_page_generation');
377 function clarity_page_generation()
378 {
379 add_menu_page(
380 'microsoft-clarity', /* $page_title */
381 'Clarity', /* menu_title */
382 'edit_posts', /* capability */
383 'microsoft-clarity', /* menu_slug */
384 'clarity_section_iframe_callback', /* callback */
385 'https://claritystatic.blob.core.windows.net/images/logo.svg', /* icon_url */
386 99 /* position */
387 );
388 }
389
390 /**
391 * Register Plugin settings
392 * clarity_project_id: option for currently integrated Clarity project id
393 **/
394 add_action('admin_init', 'clarity_register_settings');
395 function clarity_register_settings()
396 {
397 register_setting(
398 'clarity_settings_fields', /* $option_group */
399 'clarity_project_id' /* option_name */
400 /* args */
401 );
402 }
403
404 /**
405 * Notice for when wordpress admins did not finish intalling Clarity
406 * did not integrate a project
407 */
408 add_action('admin_notices', 'setup_clarity_notice__info');
409 function setup_clarity_notice__info()
410 {
411 global $pagenow;
412 $url = get_admin_url() . 'admin.php?page=microsoft-clarity';
413
414 $learnMoreUrl = 'https://wordpress.org/plugins/microsoft-clarity/';
415
416 $clarity_project_id_option = get_option(
417 'clarity_project_id', /* option */
418 clarity_project_id_default_value() /* default */
419 );
420 $pageQPExists = isset($_GET['page']);
421 if ($pageQPExists) {
422 $pageQP = $_GET['page'];
423 } else {
424 $pageQP = "";
425 }
426
427
428 if (empty($clarity_project_id_option) && $pageQP !== "microsoft-clarity" && current_user_can("manage_options")) {
429 echo
430 '<div class="notice notice-info is-dismissible">
431 <p style="font-weight:700">
432 Unlock User Insights with Microsoft Clarity!
433 </p>
434 <p style="font-weight:500">
435 Almost there! Start tracking user behavior on your site with Microsoft Clarity. See exactly where on your site users click, scroll, and get stuck. It takes just a few moments to set up.
436 </p>
437 <p>
438 <a class="button-primary" href="' . $url . '">
439 Setup Clarity
440 </a>
441 <a class="button-primary" style="margin-left:10px" href="' . $learnMoreUrl . '">
442 Learn more
443 </a>
444 </p>
445 </div>';
446 }
447 }
448
449 /**
450 * Add js function to listen to message on all admin pages
451 * These message contain changes to integrated Clarity project
452 * remove - change - add new
453 */
454 add_action('admin_enqueue_scripts', 'add_event_listeners');
455 function add_event_listeners($hook)
456 {
457 $pageQPExists = isset($_GET['page']);
458 if ($pageQPExists) {
459 $pageQP = $_GET['page'];
460 } else {
461 $pageQP = "";
462 }
463
464 if ($pageQP !== "microsoft-clarity") {
465 return;
466 }
467
468 if (!current_user_can("edit_posts")) {
469 return;
470 }
471
472 wp_register_script(
473 'window_listeners_js', /* handle */
474 plugins_url('js\add_window_listeners.js', __FILE__), /* src */
475 array(), /* deps */
476 false, /* ver */
477 false /* in_footer */
478 );
479 wp_enqueue_script(
480 'window_listeners_js' /* handle */
481 /* src */
482 /* deps */
483 /* ver */
484 /* in_footer */
485 );
486
487 // Inject the trusted Clarity dashboard origin (derived from the embed iframe URL) so the
488 // postMessage listeners accept and reply to the actual iframe origin in every environment
489 // (local dev host during testing, https://clarity.microsoft.com in production).
490 $embed_origin = clarity_get_embed_origin();
491 if (!empty($embed_origin)) {
492 wp_localize_script(
493 'window_listeners_js',
494 'clarityBrandAgentConfig',
495 array('trustedOrigin' => $embed_origin)
496 );
497 }
498 }
499
500 /**
501 * Base URL (origin + "/embed" path) of the embedded Clarity dashboard.
502 *
503 * Single source of truth for the wp-admin iframe src and the postMessage origin allow-list so
504 * they never drift apart across environments.
505 *
506 * @return string Embed base URL.
507 */
508 function clarity_get_embed_base_url()
509 {
510 // Local/dev override: define CLARITY_EMBED_BASE_URL in wp-config.php to point the admin iframe
511 // at a local dashboard build. The shipped default must stay production - this single value
512 // feeds both the iframe src and, via clarity_get_embed_origin() -> wp_localize_script(), the
513 // TRUSTED_CLARITY_ORIGIN postMessage allow-list in js/add_window_listeners.js, so a dev host
514 // baked in here would break the iframe and stop the listeners trusting clarity.microsoft.com
515 // on every merchant site.
516 if (defined('CLARITY_EMBED_BASE_URL') && CLARITY_EMBED_BASE_URL) {
517 return untrailingslashit(CLARITY_EMBED_BASE_URL);
518 }
519
520 return "https://clarity.microsoft.com/embed";
521 }
522
523 /**
524 * Origin (scheme://host[:port]) of the embedded Clarity dashboard, derived from the embed base URL.
525 *
526 * @return string Embed origin, or empty string if it cannot be parsed.
527 */
528 function clarity_get_embed_origin()
529 {
530 $parts = wp_parse_url(clarity_get_embed_base_url());
531 if (empty($parts['scheme']) || empty($parts['host'])) {
532 return "";
533 }
534 $origin = $parts['scheme'] . '://' . $parts['host'];
535 if (!empty($parts['port'])) {
536 $origin .= ':' . $parts['port'];
537 }
538 return $origin;
539 }
540
541 /**
542 * Add callback triggered when a new message is received
543 * Edits the clarity project id option respectively
544 */
545 add_action('wp_ajax_edit_clarity_project_id', "edit_clarity_project_id");
546 function edit_clarity_project_id()
547 {
548 $new_value = $_POST['new_value'];
549 $nonce = $_POST['nonce'];
550 if (!wp_verify_nonce($nonce, "wp_ajax_edit_clarity_project_id")) {
551 die(json_encode(
552 array(
553 'success' => false,
554 'message' => 'Invalid nonce.',
555 )
556 ));
557 }
558 // only admins are allowed to edit the Clarity project id
559 if (!current_user_can('manage_options')) {
560 die(json_encode(
561 array(
562 'success' => false,
563 'message' => 'User must be WordPress admin.'
564 )
565 ));
566 } else {
567 update_option(
568 'clarity_project_id', /* option */
569 $new_value /* value */
570 /* autoload */
571 );
572 die(json_encode(
573 array(
574 'success' => true,
575 'message' => 'Clarity project updated successfully.'
576 )
577 ));
578 }
579 }
580
581 /**
582 * Add callback triggered when a new message is received
583 * Edits the agent enabled status option respectively
584 */
585 add_action('wp_ajax_edit_agent_enabled_status', "edit_agent_enabled_status");
586 function edit_agent_enabled_status()
587 {
588 $new_value = $_POST['new_value'];
589 $nonce = $_POST['nonce'];
590 if (!wp_verify_nonce($nonce, "wp_ajax_edit_clarity_project_id")) {
591 die(json_encode(
592 array(
593 'success' => false,
594 'message' => 'Invalid nonce.',
595 )
596 ));
597 }
598 // only admins are allowed to edit the Clarity project id
599 if (!current_user_can('manage_options')) {
600 die(json_encode(
601 array(
602 'success' => false,
603 'message' => 'User must be WordPress admin.'
604 )
605 ));
606 } else {
607 update_option(
608 'BAOauthSuccess', /* option */
609 $new_value /* value */
610 /* autoload */
611 );
612 die(json_encode(
613 array(
614 'success' => true,
615 'message' => 'Agent enabled status updated successfully.'
616 )
617 ));
618 }
619 }
620
621 /**
622 * Displays an admin notice if the plugin version installed is not the latest
623 */
624 add_action('admin_notices', 'plugin_update_notice');
625 function plugin_update_notice()
626 {
627 // Only show the notice to users who can update plugins
628 if (! current_user_can('update_plugins')) {
629 return;
630 }
631
632 $is_latest_version = get_transient('clarity_is_latest_plugin_version');
633 if ($is_latest_version !== '0') {
634 return;
635 }
636
637 $plugin_slug = 'microsoft-clarity/clarity.php';
638
639 // Suppress the banner if it was already dismissed for this exact version.
640 $updates = get_site_transient('update_plugins');
641 $new_version = isset($updates->response[$plugin_slug]->new_version) ? $updates->response[$plugin_slug]->new_version : '';
642 if ($new_version !== '' && $new_version === get_option('clarity_dismissed_update_version')) {
643 return;
644 }
645
646 $update_url = wp_nonce_url(
647 add_query_arg(
648 array(
649 'action' => 'trigger_plugin_update',
650 'plugin' => urlencode($plugin_slug),
651 ),
652 admin_url('admin.php')
653 ),
654 'plugin_update_nonce'
655 );
656 $dismiss_nonce = wp_create_nonce('clarity_dismiss_update_notice');
657
658 ?>
659 <div class="notice notice-warning is-dismissible clarity-update-notice">
660 <p style="font-weight:700">
661 <?php _e('A new version of Microsoft Clarity is available.', 'text-domain'); ?>
662 </p>
663 <p>
664 <a href="<?php echo esc_url($update_url); ?>" class="button button-primary">
665 <?php _e('Update Now', 'text-domain'); ?>
666 </a>
667 </p>
668 </div>
669 <script>
670 // Persist the dismissal so the banner stays hidden for this version on future page loads.
671 document.addEventListener('click', function (e) {
672 if (e.target.classList.contains('notice-dismiss') && e.target.closest('.clarity-update-notice')) {
673 fetch(ajaxurl, {
674 method: 'POST',
675 credentials: 'same-origin',
676 body: new URLSearchParams({ action: 'clarity_dismiss_update_notice', nonce: '<?php echo esc_js($dismiss_nonce); ?>' })
677 });
678 }
679 });
680 </script>
681 <?php
682 }
683
684 /**
685 * Updates the plugin to the latest version programmatically
686 * The upgrade function deactives the plugin by default before the upgrade, hence the need to reactivate it
687 */
688 add_action('admin_action_trigger_plugin_update', 'plugin_perform_update');
689 function plugin_perform_update()
690 {
691 if (! current_user_can('update_plugins')) {
692 wp_die(__('You do not have sufficient permissions to update plugins.', 'text-domain'));
693 }
694
695 if (! isset($_GET['plugin']) || ! isset($_GET['_wpnonce'])) {
696 return;
697 }
698
699 $plugin_slug = sanitize_text_field(urldecode($_GET['plugin']));
700
701 if (! wp_verify_nonce($_GET['_wpnonce'], 'plugin_update_nonce')) {
702 wp_die(__('Nonce verification failed.', 'text-domain'));
703 }
704
705 include_once(ABSPATH . 'wp-admin/includes/class-wp-upgrader.php');
706 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
707
708 // Refresh core's update data so Plugin_Upgrader has a real package to download.
709 wp_clean_plugins_cache(true);
710 wp_update_plugins();
711
712 $update_plugins = get_site_transient('update_plugins');
713 $has_pending_update = isset($update_plugins->response[$plugin_slug]);
714
715 if (! $has_pending_update) {
716 // Already on the latest version: clear the stale flag and report success, not failure.
717 set_transient('clarity_is_latest_plugin_version', '1', 24 * 60 * 60);
718 $redirect_url = add_query_arg('plugin_updated', '1', admin_url('admin.php?page=microsoft-clarity'));
719 wp_redirect(esc_url($redirect_url));
720 exit;
721 }
722
723 // Create a custom skin to handle output and redirection
724 $upgrader_skin = new Automatic_Upgrader_Skin();
725 $upgrader = new Plugin_Upgrader($upgrader_skin);
726
727 // Perform the update
728 $updated = $upgrader->upgrade($plugin_slug);
729
730 if (is_wp_error($updated) || ! $updated) {
731 // Handle error: redirect back to admin page with an error notice
732 $redirect_url = add_query_arg('plugin_update_error', '1', admin_url('plugins.php'));
733 wp_redirect(esc_url($redirect_url));
734 exit;
735 } else {
736 // Success: redirect back to the plugin page with a success notice
737 activate_plugin($plugin_slug);
738 $redirect_url = add_query_arg('plugin_updated', '1', admin_url('admin.php?page=microsoft-clarity'));
739 wp_redirect(esc_url($redirect_url));
740 exit;
741 }
742 }
743
744 /**
745 * Persist dismissal of the update banner site-wide for the currently available version
746 */
747 add_action('wp_ajax_clarity_dismiss_update_notice', 'clarity_dismiss_update_notice');
748 function clarity_dismiss_update_notice()
749 {
750 if (! current_user_can('update_plugins') || ! isset($_POST['nonce']) || ! wp_verify_nonce($_POST['nonce'], 'clarity_dismiss_update_notice')) {
751 wp_die('', '', array('response' => 403));
752 }
753
754 $plugin_slug = 'microsoft-clarity/clarity.php';
755 $updates = get_site_transient('update_plugins');
756 $new_version = isset($updates->response[$plugin_slug]->new_version) ? $updates->response[$plugin_slug]->new_version : '';
757 if ($new_version !== '') {
758 update_option('clarity_dismissed_update_version', $new_version);
759 }
760 wp_die();
761 }
762
763 /**
764 * Display an admin notice with the status of the plugin update
765 */
766 add_action('admin_notices', 'plugin_admin_notices');
767 function plugin_admin_notices()
768 {
769 if (isset($_GET['plugin_updated']) && '1' === $_GET['plugin_updated']) {
770 echo
771 '<div class="notice notice-success is-dismissible">
772 <p><strong>Microsoft Clarity plugin has been updated successfully.</strong></p>
773 </div>';
774 } else if (isset($_GET['plugin_update_error']) && '1' === $_GET['plugin_update_error']) {
775 echo
776 '<div class="notice notice-error is-dismissible">
777 <p><strong>Microsoft Clarity plugin update failed.</strong></p>
778 </div>';
779 }
780 }
781