PluginProbe
Lasso Lite – Affiliate Link Manager & Product Displays / 156
Lasso Lite – Affiliate Link Manager & Product Displays v156
158 157 155 156 154 153 152 151 150 149 148 trunk 0.9.9 104 105 106 107 108 109 110 111 112 113 114 115 All 57 releases
← All changes | classes/class-helper.php +960 -62 110156 View file →
@@ -6,14 +6,20 @@
6 6 */
7 7
8 8 namespace LassoLite\Classes;
9 9
10 +use LassoLite\Classes\Helper\Url_Format;
11 +
10 12 use LassoLite\Admin\Constant;
11 13
14 +use LassoLite\Classes\Affiliate_Link;
12 15 use LassoLite\Classes\Amazon_Api;
16 +use LassoLite\Classes\Cache_Per_Process;
13 17 use LassoLite\Classes\Enum;
14 -use LassoLite\Classes\Cache_Per_Process;
18 +use LassoLite\Classes\Import;
19 +use LassoLite\Classes\License;
15 20 use LassoLite\Classes\Setting;
21 +use LassoLite\Classes\SURL;
16 22
17 23 use LassoLite\Models\Model;
18 24 use LassoLite\Models\Url_Details;
19 25
@@ -23,9 +29,8 @@
23 29 /**
24 30 * Lasso_Helper
25 31 */
26 32 class Helper {
27 -
28 33 /**
29 34 * User agent
30 35 *
31 36 * @var string $user_agent
@@ -37,9 +42,11 @@
37 42 *
38 43 * @return array|string
39 44 */
40 45 public static function POST() { // phpcs:ignore
41 - return wp_unslash( $_POST ); // phpcs:ignore
46 + $post = wp_unslash( $_POST ); // phpcs:ignore
47 +
48 + return $post;
42 49 }
43 50
44 51 /**
45 52 * GET PHP GET
@@ -46,9 +53,11 @@
46 53 *
47 54 * @return array|string
48 55 */
49 56 public static function GET() { // phpcs:ignore
50 - return wp_unslash( $_GET ); // phpcs:ignore
57 + $get = wp_unslash( $_GET ); // phpcs:ignore
58 +
59 + return $get;
51 60 }
52 61
53 62 /**
54 63 * Include variables
@@ -207,12 +216,9 @@
207 216 * @param string $url URL.
208 217 * @return bool
209 218 */
210 219 public static function has_protocol( $url ) {
211 - if ( strpos( $url, 'http' ) === 0 || strpos( $url, 'https' ) === 0 ) {
212 - return true;
213 - }
214 - return false;
220 + return Url_Format::has_protocol( $url );
215 221 }
216 222
217 223 /**
218 224 * Check if Classic Editor plugin is active.
@@ -223,8 +229,21 @@
223 229 return self::get_is_plugin_active( 'classic-editor/classic-editor.php' );
224 230 }
225 231
226 232 /**
233 + * Check if Disable Gutenberg plugin is active.
234 + *
235 + * @return bool
236 + */
237 + public static function is_disable_gutenberg_plugin_active() {
238 + if ( ! function_exists( 'is_plugin_active' ) ) {
239 + include_once ABSPATH . 'wp-admin/includes/plugin.php';
240 + }
241 +
242 + return self::get_is_plugin_active( 'disable-gutenberg/disable-gutenberg.php' );
243 + }
244 +
245 + /**
227 246 * If the Lasso Pro plugin is installed, return true. Otherwise, return false
228 247 */
229 248 public static function is_lasso_pro_installed() {
230 249 return self::get_is_plugin_active( 'lasso/affiliate-plugin.php' );
@@ -240,9 +259,9 @@
240 259 /**
241 260 * Get license status in DB
242 261 */
243 262 public static function get_license_status() {
244 - $db_status = get_option( 'lasso_license_status', '' );
263 + $db_status = get_option( 'lasso_lite_license_status', '' );
245 264 $active_license = boolval( $db_status );
246 265
247 266 return $active_license;
248 267 }
@@ -253,8 +272,11 @@
253 272 * @param string $post_name Post name.
254 273 * @param int $post_id Post id. Default to 0.
255 274 */
256 275 public static function the_slug_exists( $post_name, $post_id = 0 ) {
276 + if ( empty( $post_name ) ) {
277 + return false;
278 + }
257 279
258 280 $posts_tbl = Model::get_wp_table_name( 'posts' );
259 281 $sql = '
260 282 SELECT
@@ -266,12 +288,13 @@
266 288 WHERE
267 289 post_name = %s
268 290 AND ID != %d
269 291 AND post_status <> "trash"
292 + AND post_type = %s
270 293 LIMIT 1
271 294 ';
272 295
273 - $prepare = Model::prepare( $sql, $post_name, $post_id ); // phpcs:ignore
296 + $prepare = Model::prepare( $sql, $post_name, $post_id, Constant::LASSO_POST_TYPE ); // phpcs:ignore
274 297 $row = Model::get_row( $prepare, 'ARRAY_A' ); // phpcs:ignore
275 298
276 299 return $row ? $row : false;
277 300 }
@@ -281,42 +304,9 @@
281 304 *
282 305 * @param string $url URL.
283 306 */
284 307 public static function add_https( $url ) {
285 - $invalid_url = array(
286 - 'https://%20https:/',
287 - 'https://xhttps://',
288 - 'http:/https://',
289 - 'http://https://',
290 - 'https://https://',
291 - 'https://hhttps://',
292 - 'https://]https://',
293 - 'https://&quot;https://',
294 - '[gift_item link=&quot;https://',
295 - ']https://',
296 - );
297 - $url = trim( $url );
298 - $url = str_replace( $invalid_url, 'https://', $url );
299 -
300 - // ? fix mailto in <a> href
301 - if ( strpos( $url, 'mailto:' ) !== false || filter_var( $url, FILTER_VALIDATE_EMAIL ) ) {
302 - $email = explode( 'mailto:', $url )[1] ?? '';
303 - if ( filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
304 - $url = 'mailto:' . $email;
305 - }
306 -
307 - return $url;
308 - }
309 -
310 - if ( '' === $url || is_null( $url ) || strpos( $url, '[' ) === 0 ) {
311 - return $url;
312 - }
313 -
314 - if ( strpos( $url, 'http://' ) !== 0 && strpos( $url, 'https://' ) !== 0 && strpos( $url, '.' ) !== false && '#' !== $url ) {
315 - $url = 'https://' . $url;
316 - }
317 -
318 - return $url;
308 + return Url_Format::add_https( $url );
319 309 }
320 310
321 311 /**
322 312 * Format URL before sending request
@@ -452,9 +442,9 @@
452 442 $setup_amz_tracking_id = boolval( get_option( Enum::SETUP_AMZ_TRACKING_ID ) ) ? 15 : 0;
453 443 $follow_on_twitter = boolval( get_option( Enum::FOLLOW_ON_TWITTER ) ) ? 10 : 0;
454 444 $share_on_twitter = boolval( get_option( Enum::SHARE_ON_TWITTER ) ) ? 10 : 0;
455 445 $leave_a_review = boolval( get_option( Enum::LEAVE_A_REVIEW ) ) ? 5 : 0;
456 - $is_show_review_note = ! $leave_a_review && $total_links >= 20 && $enable_support && $setup_amz_tracking_id && $follow_on_twitter && $share_on_twitter ? 1 : 0;
446 + $is_show_review_note = ! $leave_a_review && $total_links >= 5 && $enable_support && $setup_amz_tracking_id && $follow_on_twitter && $share_on_twitter ? 1 : 0;
457 447 $progress = $enable_support + $setup_amz_tracking_id + $follow_on_twitter + $share_on_twitter + ( $links * 2 ) + $leave_a_review;
458 448 $progress = $progress ? $progress / 100 : 0;
459 449 $open_modal_add_link = $links < 20 ? 'btn-add-20-links' : '';
460 450
@@ -479,8 +469,74 @@
479 469 return $data;
480 470 }
481 471
482 472 /**
473 + * Whitelist landing-cookie attribution keys for Lite plugin signup (#788).
474 + *
475 + * @param mixed $raw POST attribution object or JSON string.
476 + * @return array|null Sanitized payload or null when no signal.
477 + */
478 + public static function sanitize_signup_attribution( $raw ) {
479 + $allowed_keys = array(
480 + 'url',
481 + 'ref',
482 + 'utm_source',
483 + 'utm_medium',
484 + 'utm_campaign',
485 + 'utm_content',
486 + 'ref_code',
487 + 'dt',
488 + );
489 + $signal_keys = array(
490 + 'utm_source',
491 + 'utm_medium',
492 + 'utm_campaign',
493 + 'utm_content',
494 + 'ref_code',
495 + );
496 +
497 + if ( is_string( $raw ) ) {
498 + $raw = json_decode( $raw, true );
499 + }
500 + if ( ! is_array( $raw ) || empty( $raw ) ) {
501 + return null;
502 + }
503 +
504 + $payload = array();
505 + foreach ( $allowed_keys as $key ) {
506 + if ( ! isset( $raw[ $key ] ) || ! is_scalar( $raw[ $key ] ) ) {
507 + continue;
508 + }
509 + $text = trim( (string) $raw[ $key ] );
510 + if ( '' === $text ) {
511 + continue;
512 + }
513 + if ( in_array( $key, array( 'url', 'ref' ), true ) ) {
514 + $payload[ $key ] = substr( $text, 0, 2048 );
515 + } else {
516 + $payload[ $key ] = substr( $text, 0, 500 );
517 + }
518 + }
519 +
520 + if ( empty( $payload ) ) {
521 + return null;
522 + }
523 +
524 + $has_signal = false;
525 + foreach ( $signal_keys as $key ) {
526 + if ( ! empty( $payload[ $key ] ) ) {
527 + $has_signal = true;
528 + break;
529 + }
530 + }
531 + if ( ! $has_signal && empty( $payload['url'] ) ) {
532 + return null;
533 + }
534 +
535 + return $payload;
536 + }
537 +
538 + /**
483 539 * Send request
484 540 *
485 541 * @param string $method Method (get or post). Default to get.
486 542 * @param string $url URL. Default to empty.
@@ -510,11 +566,19 @@
510 566 $res = wp_remote_request( $url, $request_options );
511 567 }
512 568
513 569 if ( is_wp_error( $res ) ) {
570 + // Return structured info so callers can surface what failed (e.g. cURL error).
571 + $error_payload = array(
572 + 'error' => array(
573 + 'code' => $res->get_error_code(),
574 + 'message' => $res->get_error_message(),
575 + 'data' => $res->get_error_data(),
576 + ),
577 + );
514 578 return array(
515 579 'status_code' => 500,
516 - 'response' => array(),
580 + 'response' => json_decode( wp_json_encode( $error_payload ) ),
517 581 );
518 582 }
519 583
520 584 $body = wp_remote_retrieve_body( $res );
@@ -526,8 +590,38 @@
526 590 );
527 591 }
528 592
529 593 /**
594 + * Plain-text error from send_request() output (WP_Error payload or API JSON body).
595 + *
596 + * @param array $response Return value from send_request().
597 + * @param string $default Default message.
598 + * @return string
599 + */
600 + public static function hub_request_error_message( $response, $default = 'Request failed.' ) {
601 + $default = (string) $default;
602 + if ( empty( $response ) || ! is_array( $response ) ) {
603 + return $default;
604 + }
605 + $r = $response['response'] ?? null;
606 + if ( empty( $r ) || ! is_object( $r ) ) {
607 + return $default;
608 + }
609 + if ( isset( $r->error ) ) {
610 + if ( is_object( $r->error ) && isset( $r->error->message ) ) {
611 + return (string) $r->error->message;
612 + }
613 + if ( is_string( $r->error ) && $r->error !== '' ) {
614 + return $r->error;
615 + }
616 + }
617 + if ( isset( $r->message ) && is_string( $r->message ) && $r->message !== '' ) {
618 + return $r->message;
619 + }
620 + return $default;
621 + }
622 +
623 + /**
530 624 * Get Lasso Lite - WP option
531 625 *
532 626 * @param string $option_name Option name.
533 627 * @param mixed $default Default value.
@@ -554,17 +648,45 @@
554 648 *
555 649 * @param string $url URL.
556 650 */
557 651 public static function validate_url( $url ) {
558 - if ( ! is_string( $url ) ) {
559 - return false;
652 + return Url_Format::validate_url( $url );
653 + }
654 +
655 + /**
656 + * Remove specific parameters from URL
657 + *
658 + * @param string $url URL to clean.
659 + * @param array|string $params Parameter(s) to remove.
660 + * @return string Cleaned URL
661 + */
662 + public static function remove_url_params( $url, $params ) {
663 + $parsed_url = wp_parse_url( $url );
664 +
665 + if ( ! isset( $parsed_url['query'] ) ) {
666 + return $url;
560 667 }
561 668
562 - $url = str_replace( ' ', '%20', $url );
563 - $url = preg_replace( '/[^\00-\255]+/u', '', $url );
669 + parse_str( $parsed_url['query'], $query_params );
564 670
565 - return ( ( strpos( $url, 'http://' ) === 0 || strpos( $url, 'https://' ) === 0 ) &&
566 - filter_var( $url, FILTER_VALIDATE_URL ) !== false );
671 + // ? Handle both array and single parameter
672 + $params = (array) $params;
673 + foreach ( $params as $param ) {
674 + unset( $query_params[ $param ] );
675 + }
676 +
677 + // ? Rebuild URL
678 + $clean_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'];
679 + if ( ! empty( $query_params ) ) {
680 + $clean_url .= '?' . http_build_query( $query_params );
681 + }
682 +
683 + // ? Add fragment if exists
684 + if ( isset( $parsed_url['fragment'] ) ) {
685 + $clean_url .= '#' . $parsed_url['fragment'];
686 + }
687 +
688 + return $clean_url;
567 689 }
568 690
569 691 /**
570 692 * Get argument from url
@@ -754,9 +876,9 @@
754 876 $p->shortcode = $shortcode;
755 877 }
756 878 }
757 879 } elseif ( 'EasyAzon' === $p->import_source ) {
758 - $product = get_option( $p->id, true );
880 + $product = Lasso_DB::get_easyazon_option( $p->post_title );
759 881 $p->post_title = $product['title'];
760 882 $p->id = $product['identifier'];
761 883 $p->import_permalink = $product['url'];
762 884 $p->post_name = strtolower( $product['identifier'] );
@@ -764,10 +886,9 @@
764 886 $p->shortcode = $shortcode;
765 887
766 888 $revert = $lasso_db->is_easyazon_product_imported( $product['identifier'] );
767 889 if ( $revert ) {
768 - $p->check_status = 'checked';
769 - $p->id = $revert->lasso_id;
890 + $p->id = $revert->lasso_id;
770 891 }
771 892 } elseif ( 'AmaLinks Pro' === $p->import_source ) {
772 893 $shortcode = $p->post_name;
773 894 $attributes = $lasso_helper->get_attributes( $shortcode );
@@ -783,10 +904,20 @@
783 904 }
784 905 if ( empty( $p->import_permalink ) ) {
785 906 $p->import_permalink = $lasso_amazon_api->get_amazon_link_by_product_id( $p->id );
786 907 }
908 + } elseif ( 'Lasso Pro' === $p->import_source ) {
909 + $target_url = Import::get_lasso_pro_target_url( $p->id );
910 + $p->import_permalink = $target_url;
911 + $p->shortcode = '[lasso rel="' . $p->post_name . '" id="' . $p->id . '"]';
787 912 }
788 913
914 + $p->post_title_attr = esc_attr( $p->post_title ?? '' );
915 + $p->import_permalink_attr = esc_attr( $p->import_permalink ?? '' );
916 + if ( ! empty( $p->shortcode ) ) {
917 + $p->shortcode_attr = esc_attr( $p->shortcode );
918 + }
919 +
789 920 return $p;
790 921 }
791 922
792 923 /**
@@ -917,10 +1048,10 @@
917 1048 * @param string $post_name Post name.
918 1049 */
919 1050 public static function lasso_unique_post_name( $post_id, $post_name ) {
920 1051 if ( intval( $post_id ) > 0 && ! empty( $post_name ) && self::the_slug_exists( $post_name, $post_id ) ) {
921 - $post_name .= '-link';
922 - $post_name = wp_unique_post_slug( $post_name, $post_id, 'publish', Constant::LASSO_POST_TYPE, 0 );
1052 + $post_name = rtrim( $post_name, '-link' ); // ? Fix the issue adding multiple "-link" string to the end.
1053 + $post_name = wp_unique_post_slug( $post_name, $post_id, 'publish', Constant::LASSO_POST_TYPE, 0 );
923 1054 }
924 1055
925 1056 return $post_name;
926 1057 }
@@ -1016,9 +1147,9 @@
1016 1147 */
1017 1148 public static function is_wordpress_post() {
1018 1149 global $pagenow;
1019 1150
1020 - $get = wp_unslash( $_GET ); // phpcs:ignore
1151 + $get = self::GET(); // phpcs:ignore
1021 1152 $action = $get['action'] ?? '';
1022 1153 $add_new_page = 'post-new.php' === $pagenow;
1023 1154 $edit_page = 'post.php' === $pagenow && 'edit' === $action;
1024 1155 $post_type = $get['post_type'] ?? '';
@@ -1191,8 +1322,87 @@
1191 1322 return ! empty( ( new Lasso_DB() )->get_import_plugins( true ) ) ? true : false;
1192 1323 }
1193 1324
1194 1325 /**
1326 + * Ordered onboarding step ids (tab-item data-step values).
1327 + *
1328 + * @return string[]
1329 + */
1330 + public static function get_onboarding_step_ids() {
1331 + return array( 'welcome', 'display', 'amazon', 'connect-lasso', 'import' );
1332 + }
1333 +
1334 + /**
1335 + * @param string $step Step id.
1336 + * @return bool
1337 + */
1338 + public static function is_valid_onboarding_step( $step ) {
1339 + return in_array( $step, self::get_onboarding_step_ids(), true );
1340 + }
1341 +
1342 + /**
1343 + * Last saved onboarding tab for in-progress FTUE.
1344 + *
1345 + * @param bool $include_import Whether the import step is available for this install.
1346 + * @return string
1347 + */
1348 + public static function get_onboarding_current_step( $include_import = true ) {
1349 + $step = (string) self::get_option( Enum::ONBOARDING_CURRENT_STEP, '' );
1350 + if ( ! self::is_valid_onboarding_step( $step ) ) {
1351 + return 'welcome';
1352 + }
1353 + if ( 'import' === $step && ! $include_import ) {
1354 + return 'connect-lasso';
1355 + }
1356 + return $step;
1357 + }
1358 +
1359 + /**
1360 + * @param string $step Step id.
1361 + * @return bool
1362 + */
1363 + public static function save_onboarding_current_step( $step ) {
1364 + if ( ! self::is_valid_onboarding_step( $step ) ) {
1365 + return false;
1366 + }
1367 + return self::update_option( Enum::ONBOARDING_CURRENT_STEP, $step );
1368 + }
1369 +
1370 + /**
1371 + * @return bool
1372 + */
1373 + public static function clear_onboarding_current_step() {
1374 + return self::update_option( Enum::ONBOARDING_CURRENT_STEP, '' );
1375 + }
1376 +
1377 + /**
1378 + * FTUE gate complete: stop redirecting to onboarding and drop saved step.
1379 + *
1380 + * Cleared after first link creation or an explicit Hub Connect skip.
1381 + *
1382 + * @return void
1383 + */
1384 + public static function mark_onboarding_welcome_complete() {
1385 + self::update_option( Enum::IS_VISITED_WELCOME_PAGE, 1 );
1386 + self::clear_onboarding_current_step();
1387 + }
1388 +
1389 + /**
1390 + * Reset FTUE onboarding state for QA (`reset-onboarding=1`).
1391 + *
1392 + * @return void
1393 + */
1394 + public static function reset_onboarding_for_testing() {
1395 + self::update_option( Enum::IS_VISITED_WELCOME_PAGE, 0 );
1396 + self::clear_onboarding_current_step();
1397 + update_option( Enum::LASSO_LITE_ACTIVE, 1 );
1398 + self::update_option( Constant::LASSO_ACCOUNT_EMAIL, '' );
1399 + self::update_option( Constant::LASSO_ACCOUNT_API_KEY, '' );
1400 + self::update_option( Constant::LASSO_ACCOUNT_USER_ID, 0 );
1401 + self::update_option( Constant::LASSO_OPTION_IS_CONNECTED_AFFILIATE, '0' );
1402 + }
1403 +
1404 + /**
1195 1405 * Get brag icon
1196 1406 *
1197 1407 * @param bool $force_to_show Force to show the brag. Default to false.
1198 1408 */
@@ -1205,9 +1415,10 @@
1205 1415 }
1206 1416
1207 1417 $lasso_settings = Setting::get_settings();
1208 1418
1209 - $enable_brag_mode = $lasso_settings['enable_brag_mode'] ?? false;
1419 + // Brag mode is always enabled in Lite.
1420 + $enable_brag_mode = true;
1210 1421 $lasso_url = $lasso_settings['lasso_affiliate_URL'] ?? false;
1211 1422
1212 1423 if ( $lasso_url && ( $force_to_show || $enable_brag_mode ) ) {
1213 1424 $icon_brag = esc_url( SIMPLE_URLS_URL . '/admin/assets/images/lasso-icon-brag.svg' );
@@ -1213,10 +1424,10 @@
1213 1424 $icon_brag = esc_url( SIMPLE_URLS_URL . '/admin/assets/images/lasso-icon-brag.svg' );
1214 1425 $lasso_affiliate_url = self::add_params_to_url( $lasso_url, array( 'utm_source' => 'brag' ) );
1215 1426 $img_attr = self::build_img_lazyload_attributes();
1216 1427 $icon = '
1217 - <a class="lasso-brag" href="' . $lasso_affiliate_url . '" target="_blank" rel="nofollow noindex">
1218 - <img src="' . $icon_brag . '" ' . $img_attr . ' alt="Lasso Brag" width="30" height="30">
1428 + <a class="lasso-brag" href="' . esc_url( $lasso_affiliate_url ) . '" target="_blank" rel="nofollow noindex">
1429 + <img src="' . esc_url( $icon_brag ) . '" ' . $img_attr . ' alt="Lasso Brag" width="30" height="30">
1219 1430 </a>
1220 1431 ';
1221 1432
1222 1433 Cache_Per_Process::get_instance()->set_cache( $cache_key, $icon );
@@ -1242,9 +1453,8 @@
1242 1453 $query = self::get_query_from_array( $query );
1243 1454 $parse['query'] = $query;
1244 1455
1245 1456 return self::get_url_from_parse( $parse );
1246 -
1247 1457 }
1248 1458
1249 1459 /**
1250 1460 * Get final url in the url
@@ -1329,6 +1539,694 @@
1329 1539 return 'checked' !== $post->check_status;
1330 1540 }
1331 1541
1332 1542 return false;
1543 + }
1544 +
1545 + /**
1546 + * Check whether Lite is using new or old UI
1547 + *
1548 + * @return bool true: new UI. false: old UI.
1549 + */
1550 + public static function is_lite_using_new_ui() {
1551 + $new_ui = get_option( Enum::SWITCH_TO_NEW_UI );
1552 + $new_ui = self::cast_to_boolean( $new_ui );
1553 +
1554 + if ( ! $new_ui ) {
1555 + return false;
1556 + }
1557 +
1558 + return true;
1559 + }
1560 +
1561 + /**
1562 + * CSS custom properties for Lasso display colors (shared admin + block editor iframe).
1563 + *
1564 + * @param bool $important Append `!important` to each variable value.
1565 + * @return string
1566 + */
1567 + public static function get_lasso_display_css_variables( $important = false ) {
1568 + $settings = Setting::get_settings();
1569 + $suffix = $important ? ' !important' : '';
1570 +
1571 + // @codingStandardsIgnoreStart
1572 + return ':root{
1573 + --lasso-main: ' . $settings['display_color_main'] . $suffix . ';
1574 + --lasso-title: ' . $settings['display_color_title'] . $suffix . ';
1575 + --lasso-button: ' . $settings['display_color_button'] . $suffix . ';
1576 + --lasso-secondary-button: ' . $settings['display_color_secondary_button'] . $suffix . ';
1577 + --lasso-button-text: ' . $settings['display_color_button_text'] . $suffix . ';
1578 + --lasso-background: ' . $settings['display_color_background'] . $suffix . ';
1579 + --lasso-pros: ' . $settings['display_color_pros'] . $suffix . ';
1580 + --lasso-cons: ' . $settings['display_color_cons'] . $suffix . ';
1581 + }';
1582 + // @codingStandardsIgnoreEnd
1583 + }
1584 +
1585 + /**
1586 + * Whether show Request Review at the top of the page
1587 + */
1588 + public static function show_request_review() {
1589 + $link_count = SURL::total();
1590 +
1591 + $lasso_review_allow = self::cast_to_boolean( self::get_option( Constant::LASSO_OPTION_REVIEW_ALLOW, '1' ) );
1592 + $lasso_review_snooze = self::cast_to_boolean( self::get_option( Constant::LASSO_OPTION_REVIEW_SNOOZE, '0' ) );
1593 + $lasso_review_link_count = intval( self::get_option( Constant::LASSO_OPTION_REVIEW_LINK_COUNT, $link_count ) );
1594 +
1595 + // Ask after early success (enough links to be real usage), not after a large catalog.
1596 + $show = ! $lasso_review_snooze && $link_count >= 5;
1597 + $snooze_but_show = $lasso_review_snooze && $link_count - $lasso_review_link_count >= 5;
1598 +
1599 + if ( ! $lasso_review_allow ) {
1600 + return false;
1601 + }
1602 +
1603 + if ( $show || $snooze_but_show ) {
1604 + return true;
1605 + }
1606 +
1607 + return false;
1608 + }
1609 +
1610 + /**
1611 + * Whether to show the Amazon Creators API migration notice (legacy PA-API keys not required).
1612 + *
1613 + * @return bool
1614 + */
1615 + public static function show_amazon_credentials_notice() {
1616 + $dismissed = self::cast_to_boolean( self::get_option( Constant::LASSO_OPTION_AMAZON_CREDENTIALS_NOTICE_DISMISSED, '0' ) );
1617 + $dismissed_final = self::cast_to_boolean( self::get_option( Constant::LASSO_OPTION_AMAZON_CREDENTIALS_NOTICE_DISMISSED_FINAL, '0' ) );
1618 + $updated = self::cast_to_boolean( self::get_option( Constant::LASSO_OPTION_AMAZON_CREDENTIALS_UPDATED, '0' ) );
1619 +
1620 + $settings = Setting::get_settings();
1621 + $creators_credential_id = trim( (string) ( $settings['amazon_creators_credential_id'] ?? '' ) );
1622 + $creators_secret = trim( (string) ( $settings['amazon_creators_secret'] ?? '' ) );
1623 + $creators_version = trim( (string) ( $settings['amazon_creators_version'] ?? '' ) );
1624 + $creators_partner_tag = trim( (string) ( $settings['amazon_creators_partner_tag'] ?? '' ) );
1625 + $has_creators_credentials = '' !== $creators_credential_id
1626 + && '' !== $creators_secret
1627 + && '' !== $creators_version
1628 + && '' !== $creators_partner_tag;
1629 +
1630 + $dismissed_fully = $dismissed && $dismissed_final;
1631 +
1632 + return ! $dismissed_fully && ! $updated && ! $has_creators_credentials;
1633 + }
1634 +
1635 + /**
1636 + * Whether the stored thumbnail is still the default placeholder.
1637 + *
1638 + * @param string $stored_thumbnail Post meta thumbnail.
1639 + * @return bool
1640 + */
1641 + private static function uses_default_thumbnail( $stored_thumbnail ) {
1642 + $stored_thumbnail = (string) $stored_thumbnail;
1643 +
1644 + if ( '' === $stored_thumbnail ) {
1645 + return true;
1646 + }
1647 +
1648 + if ( false !== strpos( $stored_thumbnail, Constant::DEFAULT_THUMBNAIL ) ) {
1649 + return true;
1650 + }
1651 +
1652 + return false !== strpos( $stored_thumbnail, 'lasso-no-thumbnail.jpg' );
1653 + }
1654 +
1655 + /**
1656 + * Whether the footer credentials banner should block the upsell CTA.
1657 + *
1658 + * The upsell may show after the customer dismisses the banner once, even if the
1659 + * banner reappears on a later page load until the second dismiss is recorded.
1660 + *
1661 + * @return bool
1662 + */
1663 + private static function amazon_credentials_banner_blocks_upsell() {
1664 + if ( ! self::show_amazon_credentials_notice() ) {
1665 + return false;
1666 + }
1667 +
1668 + $dismissed_once = self::cast_to_boolean(
1669 + self::get_option( Constant::LASSO_OPTION_AMAZON_CREDENTIALS_NOTICE_DISMISSED, '0' )
1670 + );
1671 +
1672 + return ! $dismissed_once;
1673 + }
1674 +
1675 + /**
1676 + * Whether to show the Get Amazon Images upsell on URL Details.
1677 + *
1678 + * @param bool $is_amazon_link Whether the link is an Amazon URL.
1679 + * @param string $stored_thumbnail Post meta thumbnail (before Amazon DB enrichment).
1680 + * @param bool $is_amazon_configured Whether valid Amazon API credentials exist.
1681 + * @return bool
1682 + */
1683 + public static function should_show_get_amazon_images_upsell( $is_amazon_link, $stored_thumbnail, $is_amazon_configured ) {
1684 + if ( ! $is_amazon_link || $is_amazon_configured ) {
1685 + return false;
1686 + }
1687 +
1688 + if ( ! self::uses_default_thumbnail( $stored_thumbnail ) ) {
1689 + return false;
1690 + }
1691 +
1692 + return ! self::amazon_credentials_banner_blocks_upsell();
1693 + }
1694 +
1695 + /**
1696 + * Whether the URL Details upsell should render hidden until the credentials banner is dismissed.
1697 + *
1698 + * @param bool $is_amazon_link Whether the link is an Amazon URL.
1699 + * @param string $stored_thumbnail Post meta thumbnail (before Amazon DB enrichment).
1700 + * @param bool $is_amazon_configured Whether valid Amazon API credentials exist.
1701 + * @return bool
1702 + */
1703 + public static function should_defer_get_amazon_images_upsell( $is_amazon_link, $stored_thumbnail, $is_amazon_configured ) {
1704 + if ( ! $is_amazon_link || $is_amazon_configured || ! self::uses_default_thumbnail( $stored_thumbnail ) ) {
1705 + return false;
1706 + }
1707 +
1708 + return self::amazon_credentials_banner_blocks_upsell();
1709 + }
1710 +
1711 + /**
1712 + * Get Ajax URL
1713 + */
1714 + public static function get_ajax_url() {
1715 + return admin_url( 'admin-ajax.php' );
1716 + }
1717 +
1718 + /**
1719 + * Get price value from price text including currency symbol.
1720 + *
1721 + * @param string $price_text Price text.
1722 + * @param string $price_symbol Price symbol.
1723 + * @return mixed|string
1724 + */
1725 + public static function get_price_value_from_price_text( $price_text, $price_symbol = '' ) {
1726 + if ( preg_match( '/[€]|R\$|TL|kr|zł/', $price_text ) || in_array( $price_symbol, array( '€', 'R$', 'TL', 'kr', 'zł' ), true ) ) {
1727 + // ? For price use , as decimal separator and . as thousands separator.
1728 + $replace_character = '.';
1729 + $reg_pattern = '/\d+,?\d*/';
1730 + } else {
1731 + // ? For price use . as decimal separator and , as thousands separator.
1732 + $replace_character = ',';
1733 + $reg_pattern = '/\d+\.?\d*/';
1734 + }
1735 +
1736 + $price_without_thousands_separator = str_replace( $replace_character, '', $price_text );
1737 + preg_match( $reg_pattern, $price_without_thousands_separator, $matches );
1738 +
1739 + // ? Final format to general float number by replace ',' to '.'.
1740 + return isset( $matches[0] ) ? str_replace( ',', '.', $matches[0] ) : '';
1741 + }
1742 +
1743 + /**
1744 + * Get status from BLS
1745 + *
1746 + * @param string $url URL.
1747 + * @param bool $get_res Get response or not. Default to false.
1748 + * @param bool $is_lasso_save Is Lasso save data action. Default to false.
1749 + * @param bool $url_version_param Is add version param to request api url to ignore cache. Default to false.
1750 + * @param bool $force_bls Force fetch product from BLS or not. Default to false.
1751 + * @param bool $refresh_image Bypass cache and fetch fresh product image/metadata. Default to false.
1752 + */
1753 + public static function get_url_status_code_by_broken_link_service( $url, $get_res = false, $is_lasso_save = false, $url_version_param = false, $force_bls = false, $refresh_image = false ) {
1754 + $status = 200;
1755 + $url = Amazon_Api::get_amazon_product_url( $url, false );
1756 + $url = self::format_url_before_requesting( $url );
1757 +
1758 + $headers = self::get_headers();
1759 + $query = array(
1760 + 'url' => $url,
1761 + );
1762 +
1763 + if ( $url_version_param ) {
1764 + $query['ver'] = time();
1765 + }
1766 +
1767 + if ( $force_bls ) {
1768 + $query['force_bls'] = 1;
1769 + }
1770 +
1771 + if ( $refresh_image ) {
1772 + $query['refresh_image'] = 1;
1773 + }
1774 +
1775 + $request_url = Constant::LASSO_LINK . '/link/status/?' . http_build_query( $query, '', '&', PHP_QUERY_RFC3986 );
1776 + if ( ! $is_lasso_save && defined( 'DOING_CRON' ) && DOING_CRON && ! Cron::should_send_scheduled_data_request( $url ) ) {
1777 + return $get_res ? array(
1778 + 'status_code' => 200,
1779 + 'response' => array(),
1780 + ) : 200;
1781 + }
1782 + Cron::maybe_pace_background_request( $url, $is_lasso_save );
1783 + $res = self::send_request( 'get', $request_url, array(), $headers );
1784 +
1785 + // phpcs:ignore
1786 + // $res = self::send_request( 'get', LASSO_LINK . '/link/status/?' . $encrypted_base64, array(), $headers );
1787 + if ( $get_res ) {
1788 + return $res;
1789 + }
1790 +
1791 + $bls_response = $res['response'] ?? null;
1792 + if ( ! is_object( $bls_response ) ) {
1793 + return 500;
1794 + }
1795 +
1796 + return intval( $bls_response->status ?? $status );
1797 + }
1798 +
1799 + /**
1800 + * Get currency symbol from ISO currency code
1801 + *
1802 + * @param string $iso Iso currency code.
1803 + * @return string
1804 + */
1805 + public static function get_currency_symbol_from_iso_code( $iso ) {
1806 + $iso = strtoupper( $iso );
1807 + $result = '$';
1808 +
1809 + $currencies = array(
1810 + 'USD' => '$',
1811 + 'AUD' => '$',
1812 + 'CAD' => '$',
1813 + 'EUR' => '€',
1814 + 'MXN' => '$',
1815 + 'CNY' => '¥',
1816 + 'JPY' => '¥',
1817 + 'INR' => '₹',
1818 + 'SEK' => 'kr',
1819 + 'BRL' => 'R$',
1820 + 'TRY' => 'TL',
1821 + 'GBP' => '£',
1822 + 'PLN' => 'zł',
1823 + 'EGP' => 'E£',
1824 + 'SGD' => 'S$',
1825 + 'AED' => 'AED',
1826 + );
1827 +
1828 + return isset( $currencies[ $iso ] ) ? $currencies[ $iso ] : $result;
1829 + }
1830 +
1831 + /**
1832 + * Check using WP classic editor
1833 + *
1834 + * @return bool
1835 + */
1836 + public static function is_classic_editor() {
1837 + return self::is_classic_editor_plugin_active() || self::is_disable_gutenberg_plugin_active();
1838 + }
1839 +
1840 + /**
1841 + * Escape string prevent SQL injection
1842 + *
1843 + * @param string $keyword Keyword.
1844 + * @param bool $search_from_after Option to check search string from end of string.
1845 + * @return string
1846 + */
1847 + public static function esc_like_query( $keyword, $search_from_after = false ) {
1848 + global $wpdb;
1849 +
1850 + $wild = '%';
1851 + if ( $search_from_after ) {
1852 + $query = $wpdb->esc_like( $keyword ) . $wild;
1853 + } else {
1854 + $query = $wild . $wpdb->esc_like( $keyword ) . $wild;
1855 + }
1856 +
1857 + $query = str_replace( ' ', '%', $query );
1858 + return $query;
1859 + }
1860 +
1861 + /**
1862 + * Sanitize HTML for safe display (formerly regex-based script strip).
1863 + * Now delegates to wp_kses_post so event handlers and javascript: URLs are removed.
1864 + *
1865 + * Falsy input (null, false, empty string) is returned unchanged.
1866 + *
1867 + * @param string|null|false $html HTML code.
1868 + * @return string|null|false Sanitized HTML, or the original falsy value.
1869 + */
1870 + public static function sanitize_script( $html ) {
1871 + if ( ! $html ) {
1872 + return $html;
1873 + }
1874 +
1875 + return wp_kses_post( (string) $html );
1876 + }
1877 +
1878 + /**
1879 + * Verify access and nonce, then return wp_send_json_error if unverified.
1880 + *
1881 + * @param bool $allow_edit_post_access Allow access for editor, author, contributor.
1882 + * @return void
1883 + */
1884 + public static function verify_access_and_nonce( $allow_edit_post_access = false ) {
1885 + try {
1886 + // ? Verify access token.
1887 + if ( ! current_user_can( 'manage_options' ) ) {
1888 + if ( $allow_edit_post_access ) {
1889 + // ? Allow access for editor, author, contributor.
1890 + // ? WP User Roles: https://wordpress.com/support/invite-people/user-roles/#:~:text=Editor%3A%20Has%20access%20to%20all,posts%20until%20they%20are%20published.
1891 + $current_user_role = self::get_current_user_role();
1892 + if ( ! in_array( $current_user_role, array( 'editor', 'author', 'contributor' ), true ) ) {
1893 + wp_send_json_error( 'Access denied.' );
1894 + }
1895 + } else {
1896 + wp_send_json_error( 'Access denied.' );
1897 + }
1898 + }
1899 +
1900 + // ? Verify nonce.
1901 + $data = array();
1902 + $server = wp_unslash( $_SERVER );
1903 + $method = $server['REQUEST_METHOD'] ?? '';
1904 + if ( 'POST' === $method ) {
1905 + $data = self::POST();
1906 + } elseif ( 'GET' === $method ) {
1907 + $data = self::GET();
1908 + }
1909 +
1910 + $nonce = $data['nonce'] ?? '';
1911 + if ( false === wp_verify_nonce( $nonce, Constant::LASSO_LITE_NONCE . wp_salt() ) ) {
1912 + wp_send_json_error( 'Nonce not verified.' );
1913 + }
1914 + } catch ( WPAjaxDieStopException $e ) {
1915 + throw $e;
1916 + } catch ( \Exception $e ) {
1917 + wp_send_json_error( 'Verify access and nonce error.' );
1918 + }
1919 + }
1920 +
1921 + /**
1922 + * Get current user role
1923 + *
1924 + * @return string
1925 + */
1926 + public static function get_current_user_role() {
1927 + if ( is_user_logged_in() ) {
1928 + $user = wp_get_current_user();
1929 + $roles = (array) $user->roles;
1930 +
1931 + return $roles[0];
1932 + } else {
1933 + return 'guest';
1934 + }
1935 + }
1936 +
1937 + /**
1938 + * Build headers for Lasso API
1939 + *
1940 + * @param string $license_id License ID. Default to null.
1941 + * @return array
1942 + */
1943 + public static function get_headers( $license_id = null ) {
1944 + $license = $license_id ? $license_id : License::get_license();
1945 + if ( ! is_string( $license ) && ! is_numeric( $license ) ) {
1946 + $license = '';
1947 + }
1948 + $license = (string) $license;
1949 +
1950 + $site_id = License::get_site_id();
1951 + if ( ! is_string( $site_id ) && ! is_numeric( $site_id ) ) {
1952 + $site_id = '';
1953 + }
1954 + $site_id = (string) $site_id;
1955 +
1956 + $headers = array(
1957 + 'Content-Type' => 'application/json',
1958 + 'license' => $license,
1959 + 'site_id' => $site_id,
1960 + 'site_url' => rawurlencode( site_url() ),
1961 + 'is_lasso_lite' => '1',
1962 + 'email' => (string) get_option( 'admin_email', '' ),
1963 + );
1964 +
1965 + return $headers;
1966 + }
1967 +
1968 + /**
1969 + * Build lsid
1970 + *
1971 + * @return false|string
1972 + */
1973 + public static function build_lsid() {
1974 + $lsid = session_create_id( 'ls-' );
1975 + if ( ! $lsid ) {
1976 + $lsid = 'ls-' . md5( uniqid( wp_rand(), true ) );
1977 + }
1978 +
1979 + return $lsid;
1980 + }
1981 +
1982 + /**
1983 + * Check if WP Elementor plugin is active.
1984 + *
1985 + * @return bool
1986 + */
1987 + public static function is_wp_elementor_plugin_actived() {
1988 + return self::get_is_plugin_active( 'elementor/elementor.php' );
1989 + }
1990 +
1991 + /**
1992 + * FOLLOW ALL REDIRECTS
1993 + * This makes multiple requests, following each redirect until it reaches the final destination.
1994 + *
1995 + * @param string $url URL.
1996 + * @param bool $is_lasso_save Is Lasso save data action. Default to false.
1997 + * @param bool $get_page_title Get page title or not. Default to false.
1998 + */
1999 + public static function get_redirect_final_target( $url, $is_lasso_save = false, $get_page_title = false ) {
2000 + // ? Get final url for amazon shortlink from cache
2001 + $amazon_shortlink_final_url_cached = Amazon_Api::get_shortlink_final_url_cached( $url );
2002 + if ( $amazon_shortlink_final_url_cached ) {
2003 + return $get_page_title ? array( $amazon_shortlink_final_url_cached, get_option( Amazon_Api::build_shortlink_cache_key( $url ) . '_page_title' ) ) : $amazon_shortlink_final_url_cached;
2004 + }
2005 +
2006 + $origin_url = $url;
2007 + $url = Amazon_Api::get_amazon_product_url( $url, $is_lasso_save ? true : false );
2008 + $url = self::format_url_before_requesting( $url );
2009 + $base_domain = self::get_base_domain( $url );
2010 + $browser = self::get_server_param( 'HTTP_USER_AGENT' );
2011 + $browser = '' !== $browser ? $browser : self::$user_agent;
2012 + $use_bls = apply_filters( 'get_final_url_domain_bls', false, $url );
2013 +
2014 + $cache_prefix = 'get_final_url_';
2015 + $page_title = '';
2016 +
2017 + // ? check result in cache first, it may be use before in the same request.
2018 + $final_url_cache = Cache_Per_Process::get_instance()->get_cache( $cache_prefix . md5( $url ) . $get_page_title );
2019 + if ( $final_url_cache ) {
2020 + return $final_url_cache;
2021 + }
2022 + if ( Amazon_Api::is_amazon_url( $url ) && Amazon_Api::get_product_id_by_url( $url ) ) {
2023 + return $get_page_title ? array( $url, $page_title ) : $url;
2024 + }
2025 +
2026 + $res = wp_remote_get(
2027 + $url,
2028 + array(
2029 + 'headers' => array(
2030 + 'user-agent' => $browser,
2031 + ),
2032 + )
2033 + );
2034 +
2035 + $is_amazon_shortened_url = Amazon_Api::is_amazon_shortened_url( $url );
2036 +
2037 + $status_code = is_wp_error( $res ) ? 500 : $res['response']['code'] ?? '';
2038 + if ( 200 === $status_code || 429 === $status_code ) {
2039 + $new_url = $res['http_response']->get_response_object()->url;
2040 + $use_bls = apply_filters( 'get_final_url_domain_bls', false, $new_url );
2041 + }
2042 +
2043 + if ( $is_amazon_shortened_url ) {
2044 + $use_bls = true;
2045 + }
2046 +
2047 + if ( is_wp_error( $res ) || $use_bls || 403 === $status_code ) {
2048 + $allow_bls = $is_lasso_save
2049 + || ! defined( 'DOING_CRON' )
2050 + || ! DOING_CRON
2051 + || Cron::should_send_scheduled_data_request( $url );
2052 + if ( ! $allow_bls ) {
2053 + $result = $get_page_title ? array( $url, $page_title ) : $url;
2054 + Cache_Per_Process::get_instance()->set_cache( $cache_prefix . md5( $url ) . $get_page_title, $result );
2055 + return $result;
2056 + }
2057 + $headers = self::get_headers();
2058 + $data = array(
2059 + 'url' => $url,
2060 + );
2061 + $encrypted_base64 = http_build_query( $data );
2062 + Cron::maybe_pace_background_request( $url, $is_lasso_save );
2063 + $res = self::send_request( 'get', Constant::LASSO_LINK . '/link/final-url/?' . $encrypted_base64, array(), $headers );
2064 +
2065 + $bls_response = ( isset( $res['response'] ) && is_object( $res['response'] ) ) ? $res['response'] : null;
2066 + $final_url = ( null !== $bls_response ) ? ( $bls_response->finalUrl ?? $url ) : $url;
2067 + $page_title = ( null !== $bls_response ) ? ( $bls_response->pageTitle ?? '' ) : '';
2068 +
2069 + $bls_status = ( null !== $bls_response && isset( $bls_response->status ) ) ? intval( $bls_response->status ) : 0;
2070 + if ( $bls_status ) {
2071 + $response_status = $bls_status;
2072 + } elseif ( 200 === intval( $res['status_code'] ?? 0 ) ) {
2073 + $response_status = 500;
2074 + } else {
2075 + $response_status = intval( $res['status_code'] ?? 500 );
2076 + }
2077 +
2078 + // ? Set the response status code for add new link process
2079 + Cache_Per_Process::get_instance()->set_cache( Affiliate_Link::ADD_NEW_LINK_RESPONSE_STATUS . md5( $origin_url ), $response_status );
2080 +
2081 + $tmp_url = self::get_final_url_from_url_param( $final_url );
2082 + if ( $tmp_url ) {
2083 + $page_title = self::get_title_by_url( $tmp_url );
2084 + $final_url = $tmp_url;
2085 + }
2086 +
2087 + // ? cache result
2088 + $result = $get_page_title ? array( $final_url, $page_title ) : $final_url;
2089 + Cache_Per_Process::get_instance()->set_cache( $cache_prefix . md5( $url ) . $get_page_title, $result );
2090 +
2091 + // ? Cache the final url of amazon shortlink
2092 + if ( $is_amazon_shortened_url ) {
2093 + $shortlink_cache_key = Amazon_Api::build_shortlink_cache_key( $url );
2094 + update_option( $shortlink_cache_key, $final_url );
2095 + // ? Cache the page title of amazon shortlink
2096 + update_option( $shortlink_cache_key . '_page_title', $page_title );
2097 + }
2098 +
2099 + return $result;
2100 + }
2101 +
2102 + $http_response = $res['http_response']->get_response_object();
2103 + $status = wp_remote_retrieve_response_code( $res );
2104 +
2105 + // ? Set the response status code for add new link process
2106 + Cache_Per_Process::get_instance()->set_cache( Affiliate_Link::ADD_NEW_LINK_RESPONSE_STATUS . md5( $origin_url ), $status );
2107 +
2108 + $final_url = $http_response->url;
2109 + $page_title = self::get_page_title( $http_response->body );
2110 + if ( strpos( $page_title, 'Please Wait...' ) !== false
2111 + || strpos( $page_title, 'Cloudflare' ) !== false
2112 + || strpos( $page_title, 'Access Denied' ) !== false
2113 + || strpos( $page_title, 'Just a moment...' ) !== false
2114 + ) {
2115 + $page_title = self::get_title_by_url( $final_url );
2116 + }
2117 +
2118 + $tmp_url = self::get_final_url_from_url_param( $final_url );
2119 + if ( $tmp_url ) {
2120 + $page_title = self::get_title_by_url( $tmp_url );
2121 + $final_url = $tmp_url;
2122 + }
2123 +
2124 + // ? Cache the final url of amazon shortlink
2125 + if ( $is_amazon_shortened_url ) {
2126 + $shortlink_cache_key = Amazon_Api::build_shortlink_cache_key( $url );
2127 + update_option( $shortlink_cache_key, $final_url );
2128 + // ? Cache the page title of amazon shortlink
2129 + update_option( $shortlink_cache_key . '_page_title', $page_title );
2130 + }
2131 +
2132 + if ( ! $page_title || Affiliate_Link::DEFAULT_TITLE === $page_title ) {
2133 + $page_title = self::get_title_by_url( $final_url );
2134 + }
2135 +
2136 + // ? cache result
2137 + $result = $get_page_title ? array( $final_url, $page_title ) : $final_url;
2138 + Cache_Per_Process::get_instance()->set_cache( $cache_prefix . md5( $url ) . $get_page_title, $result );
2139 +
2140 + return $result;
2141 + }
2142 +
2143 + /**
2144 + * Get page title from HTML
2145 + *
2146 + * @param string $html HTML string.
2147 + */
2148 + public static function get_page_title( $html ) {
2149 + $temp = explode( '<title', $html )[1] ?? '';
2150 + $html = $temp ? '<title' . $temp : $html;
2151 + $temp = explode( '</title>', $html )[0] ?? '';
2152 + $html = $temp ? $temp . '</title>' : $html;
2153 + $res = preg_match( '/<title\s*(.*?)>(.*?)<\/title>/siU', $html, $title_matches );
2154 + if ( ! $res ) {
2155 + return '';
2156 + }
2157 +
2158 + // ? Clean up title: remove EOL's and excessive whitespace.
2159 + $title = preg_replace( '/\s+/', ' ', $title_matches[2] ?? '' );
2160 + // ? String – to UTF8 is \xe2\x80\x93, replace by -
2161 + $title = str_replace( '–', '-', $title );
2162 + // ? Remove all non-US-ASCII (i.e. outside 0x0-0x7F) characters
2163 + $title = preg_replace( '/[^\x00-\x7F]/', '', $title );
2164 + $title = trim( $title );
2165 + $title = self::format_post_title( $title );
2166 +
2167 + return $title;
2168 + }
2169 +
2170 + /**
2171 + * Build the dynamic query variable name for serving the performance snippet.
2172 + *
2173 + * Derive a domain-specific key to reduce collisions and support per-domain routing.
2174 + *
2175 + * @return string Query var name (md5 hash of the base domain)
2176 + */
2177 + public static function get_snippet_query() {
2178 + $domain = self::get_base_domain( site_url() );
2179 + if ( empty( $domain ) ) {
2180 + return 'lasso_connect_snippet_lite';
2181 + }
2182 + return md5( $domain );
2183 + }
2184 +
2185 + /**
2186 + * Derive Intercom user_id from email with optional JWT override.
2187 + * If a JWT with a non-empty `user_id` claim is present, that value is used;
2188 + * otherwise the md5 hash of the lowercased+trimmed email is returned.
2189 + * Callers may pass raw or pre-normalized email; the function normalizes
2190 + * defensively to keep the derived user_id deterministic.
2191 + *
2192 + * @param string $user_email Email (raw or pre-normalized); trimmed and lowercased internally.
2193 + * @param string $intercom_user_jwt Optional Intercom JWT token used to override
2194 + * the email-based user_id when valid.
2195 + *
2196 + * @return string Intercom user_id derived from JWT or email hash.
2197 + */
2198 + public static function get_intercom_user_id( $user_email, $intercom_user_jwt ) {
2199 + // MD5 is used only for a deterministic Intercom identifier; not for secrets.
2200 + // Normalize email casing to keep intercom user_id deterministic.
2201 + $normalized_email = is_string( $user_email ) ? strtolower( trim( $user_email ) ) : '';
2202 + $intercom_user_id = md5( $normalized_email );
2203 + if ( ! is_string( $intercom_user_jwt ) || '' === trim( $intercom_user_jwt ) ) {
2204 + return $intercom_user_id;
2205 + }
2206 +
2207 + // JWT is expected to be issued by our backend; no signature verification here.
2208 + $jwt_parts = explode( '.', $intercom_user_jwt );
2209 + if ( 3 === count( $jwt_parts ) ) {
2210 + $payload = $jwt_parts[1];
2211 + $payload = strtr( $payload, '-_', '+/' );
2212 + $payload_length = strlen( $payload );
2213 + $payload_padding = $payload_length % 4;
2214 + // Pad base64 payload to the next multiple of 4 so decode succeeds.
2215 + if ( 0 !== $payload_padding ) {
2216 + $payload = str_pad( $payload, $payload_length + 4 - $payload_padding, '=', STR_PAD_RIGHT );
2217 + }
2218 + $payload_decoded = base64_decode( $payload, true );
2219 + if ( false !== $payload_decoded ) {
2220 + $decoded = json_decode( $payload_decoded );
2221 + if ( JSON_ERROR_NONE === json_last_error() && is_object( $decoded ) && isset( $decoded->user_id ) && is_scalar( $decoded->user_id ) ) {
2222 + $user_id_claim = (string) $decoded->user_id;
2223 + if ( '' !== trim( $user_id_claim ) ) {
2224 + $intercom_user_id = $user_id_claim;
2225 + }
2226 + }
2227 + }
2228 + }
2229 +
2230 + return $intercom_user_id;
1333 2231 }
1334 2232 }