PluginProbe
Lasso Lite – Affiliate Link Manager & Product Displays / 154
Lasso Lite – Affiliate Link Manager & Product Displays v154
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 116 All 56 releases
simple-urls / classes / class-helper.php

class-helper.php in Lasso Lite – Affiliate Link Manager & Product Displays 154, at classes/class-helper.php

2,216 lines 67.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Declare class Helper
4 *
5 * @package Helper
6 */
7
8 namespace LassoLite\Classes;
9
10 use LassoLite\Classes\Helper\Url_Format;
11
12 use LassoLite\Admin\Constant;
13
14 use LassoLite\Classes\Affiliate_Link;
15 use LassoLite\Classes\Amazon_Api;
16 use LassoLite\Classes\Cache_Per_Process;
17 use LassoLite\Classes\Enum;
18 use LassoLite\Classes\Import;
19 use LassoLite\Classes\License;
20 use LassoLite\Classes\Setting;
21 use LassoLite\Classes\SURL;
22
23 use LassoLite\Models\Model;
24 use LassoLite\Models\Url_Details;
25
26
27 use Exception;
28
29 /**
30 * Lasso_Helper
31 */
32 class Helper {
33 /**
34 * User agent
35 *
36 * @var string $user_agent
37 */
38 public static $user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0';
39
40 /**
41 * GET PHP POST
42 *
43 * @return array|string
44 */
45 public static function POST() { // phpcs:ignore
46 $post = wp_unslash( $_POST ); // phpcs:ignore
47
48 return $post;
49 }
50
51 /**
52 * GET PHP GET
53 *
54 * @return array|string
55 */
56 public static function GET() { // phpcs:ignore
57 $get = wp_unslash( $_GET ); // phpcs:ignore
58
59 return $get;
60 }
61
62 /**
63 * Include variables
64 *
65 * @param string $file_path File path.
66 * @param array $variables List of variables.
67 * @param bool $output_ajax_html Output a ajax html string or not.
68 */
69 public static function include_with_variables( $file_path, $variables = array(), $output_ajax_html = true ) {
70 $output = null;
71 if ( file_exists( $file_path ) ) {
72 extract( $variables ); // phpcs:ignore
73 if ( $output_ajax_html ) {
74 ob_start();
75 include $file_path;
76 $output = ob_get_clean();
77 return $output;
78 } else {
79 require $file_path;
80 }
81 }
82
83 return $output;
84 }
85
86 /**
87 * Get path to views folder
88 *
89 * @return string
90 */
91 public static function get_path_views_folder() {
92 return SIMPLE_URLS_DIR . 'admin/views/';
93 }
94
95 /**
96 * Enqueue a Lasso script.
97 *
98 * @param string $handle Name of the script. Should be unique.
99 * @param string $file_name Lasso script file name.
100 * @param array $deps Optional. An array of registered script handles this script depends on. Default empty array.
101 * @param bool $in_footer Optional. Whether to enqueue the script before </body> instead of in the <head>. Default 'false'.
102 */
103 public static function enqueue_script( $handle, $file_name, $deps = array(), $in_footer = false ) {
104 $handle = SIMPLE_URLS_SLUG . '-' . $handle;
105 $file_path = SIMPLE_URLS_DIR . '/admin/assets/js/' . $file_name;
106
107 if ( file_exists( $file_path ) ) {
108 $src = SIMPLE_URLS_URL . '/admin/assets/js/' . $file_name;
109 $ver = strval( @filemtime( $file_path ) ); // phpcs:ignore
110
111 wp_enqueue_script( $handle, $src, $deps, $ver, $in_footer );
112 }
113 }
114
115 /**
116 * Enqueue a Lasso CSS stylesheet.
117 *
118 * @param string $handle Name of the stylesheet. Should be unique.
119 * @param string $file_name Lasso stylesheet file name.
120 * @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
121 * @param string $media Optional. The media for which this stylesheet has been defined.
122 * Default 'all'. Accepts media types like 'all', 'print' and 'screen', or media queries like
123 * '(orientation: portrait)' and '(max-width: 640px)'.
124 * @param bool $apply_handle_prefix Is apply prefix for handle. Default to true.
125 */
126 public static function enqueue_style( $handle, $file_name, $deps = array(), $media = 'all', $apply_handle_prefix = true ) {
127 $handle = $apply_handle_prefix ? SIMPLE_URLS_SLUG . '-' . $handle : $handle;
128 $file_path = SIMPLE_URLS_DIR . '/admin/assets/css/' . $file_name;
129
130 if ( file_exists( $file_path ) ) {
131 $src = SIMPLE_URLS_URL . '/admin/assets/css/' . $file_name;
132 $ver = strval( @filemtime( $file_path ) ); // phpcs:ignore
133
134 wp_enqueue_style( $handle, $src, $deps, $ver, $media );
135 }
136 }
137
138 /**
139 * Get list page
140 *
141 * @return Page[]
142 */
143 public static function available_pages() {
144 $pages[ Enum::PAGE_DASHBOARD ] = new Page( 'Dashboard', Enum::PAGE_DASHBOARD, 'dashboard/index.php' );
145 $pages[ Enum::PAGE_OPPORTUNITIES ] = new Page( 'Opportunities', Enum::PAGE_OPPORTUNITIES, 'opportunities/index.php' );
146 $pages[ Enum::PAGE_IMPORT ] = new Page( 'Import', Enum::PAGE_IMPORT, 'import/index.php' );
147 $pages[ Enum::PAGE_TABLES ] = new Page( 'Tables', Enum::PAGE_TABLES, 'tables/index.php' );
148 $pages[ Enum::PAGE_URL_DETAILS ] = new Page( 'Link Details', Enum::PAGE_URL_DETAILS, '/dashboard/url-details.php' );
149
150 $pages[ Enum::PAGE_SETTINGS_GENERAL ] = new Page( 'General', Enum::PAGE_SETTINGS_GENERAL, 'settings/general.php' );
151 $pages[ Enum::PAGE_SETTINGS_DISPLAY ] = new Page( 'Display', Enum::PAGE_SETTINGS_DISPLAY, 'settings/display.php' );
152 $pages[ Enum::PAGE_SETTINGS_AMAZON ] = new Page( 'Amazon', Enum::PAGE_SETTINGS_AMAZON, 'settings/amazon.php' );
153 $pages[ Enum::PAGE_GROUPS ] = new Page( 'Groups', Enum::PAGE_GROUPS, '/groups/index.php' );
154 $pages[ Enum::PAGE_GROUP_DETAIL ] = new Page( 'Group Detail', Enum::PAGE_GROUP_DETAIL, '/groups/detail.php' );
155
156 if ( get_option( Enum::LASSO_LITE_ACTIVE ) && ! self::get_option( Enum::IS_VISITED_WELCOME_PAGE ) ) {
157 $pages[ Enum::PAGE_ONBOARDING ] = new Page( 'Onboarding', Enum::PAGE_ONBOARDING, 'onboarding/index.php' );
158 }
159
160 return $pages;
161 }
162
163 /**
164 * Convert date time to WordPress format
165 *
166 * @param string $datetime Date time. Format must be 'Y-m-d H:i:s' (example: 2018-09-14 10:34:54).
167 * @param bool $time Is it time. Default to true.
168 */
169 public static function convert_datetime_format( $datetime, $time = true ) {
170 if ( ! $datetime ) {
171 return $datetime;
172 }
173
174 $date_format = get_option( 'date_format' );
175 $time_format = 'g:i a T';
176 $datetime_format = $date_format . ' ' . $time_format;
177 $format = ( $time ) ? $datetime_format : $date_format;
178
179 try {
180 $result = date_create_from_format( 'Y-m-d H:i:s', $datetime );
181 $result = $result->format( $format );
182 } catch ( \Exception $e ) {
183 $result = $datetime;
184 }
185
186 return $result;
187 }
188
189 /**
190 * Add surl prefix
191 *
192 * @param string $page Page name.
193 * @return string
194 */
195 public static function add_prefix_page( $page ) {
196 return SIMPLE_URLS_SLUG . '-' . $page;
197 }
198
199 /**
200 * Print a wrapper for js render library
201 *
202 * @param string $html_id_selector Html id selector.
203 * @param string $file_path Absolute path file.
204 * @return string|null
205 */
206 public static function wrapper_js_render( $html_id_selector, $file_path ) {
207 $output = '<script id="' . $html_id_selector . '" type="text/x-jsrender">';
208 $output .= self::include_with_variables( $file_path, array(), true );
209 $output .= '</script>';
210 return $output;
211 }
212
213 /**
214 * Check a url has protocol or not
215 *
216 * @param string $url URL.
217 * @return bool
218 */
219 public static function has_protocol( $url ) {
220 return Url_Format::has_protocol( $url );
221 }
222
223 /**
224 * Check if Classic Editor plugin is active.
225 *
226 * @return bool
227 */
228 public static function is_classic_editor_plugin_active() {
229 return self::get_is_plugin_active( 'classic-editor/classic-editor.php' );
230 }
231
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 /**
246 * If the Lasso Pro plugin is installed, return true. Otherwise, return false
247 */
248 public static function is_lasso_pro_installed() {
249 return self::get_is_plugin_active( 'lasso/affiliate-plugin.php' );
250 }
251
252 /**
253 * If the Lasso Pro plugin is active, return true. Otherwise, return false
254 */
255 public static function is_lasso_pro_plugin_active() {
256 return self::is_lasso_pro_installed() && self::get_license_status();
257 }
258
259 /**
260 * Get license status in DB
261 */
262 public static function get_license_status() {
263 $db_status = get_option( 'lasso_lite_license_status', '' );
264 $active_license = boolval( $db_status );
265
266 return $active_license;
267 }
268
269 /**
270 * Check whether slug exists or not
271 *
272 * @param string $post_name Post name.
273 * @param int $post_id Post id. Default to 0.
274 */
275 public static function the_slug_exists( $post_name, $post_id = 0 ) {
276 if ( empty( $post_name ) ) {
277 return false;
278 }
279
280 $posts_tbl = Model::get_wp_table_name( 'posts' );
281 $sql = '
282 SELECT
283 ID,
284 post_name,
285 post_type
286 FROM '
287 . $posts_tbl . '
288 WHERE
289 post_name = %s
290 AND ID != %d
291 AND post_status <> "trash"
292 AND post_type = %s
293 LIMIT 1
294 ';
295
296 $prepare = Model::prepare( $sql, $post_name, $post_id, Constant::LASSO_POST_TYPE ); // phpcs:ignore
297 $row = Model::get_row( $prepare, 'ARRAY_A' ); // phpcs:ignore
298
299 return $row ? $row : false;
300 }
301
302 /**
303 * Add https to the url
304 *
305 * @param string $url URL.
306 */
307 public static function add_https( $url ) {
308 return Url_Format::add_https( $url );
309 }
310
311 /**
312 * Format URL before sending request
313 *
314 * @param string $url URL.
315 * @param bool $encode Encode url or not. Default to false.
316 */
317 public static function format_url_before_requesting( $url, $encode = false ) {
318 $url = trim( $url );
319 $url = $encode ? rawurlencode( $url ) : $url;
320
321 return $url;
322 }
323
324 /**
325 * Get title by url
326 *
327 * @param string $url URL.
328 */
329 public static function get_title_by_url( $url ) {
330 $url = self::add_https( $url );
331 $parse = wp_parse_url( $url );
332 $host = $parse['host'] ?? '';
333 $host = str_replace( 'www.', '', $host );
334 $host = explode( '.', $host );
335 $host = $host[ count( $host ) - 2 ] ?? '';
336 $host = str_replace( '-', ' ', $host );
337 $host = ucwords( $host );
338
339 return $host;
340 }
341
342 /**
343 * Remove a action out of WordPress hook
344 *
345 * Example 1: Lasso_Helper::remove_action('admin_print_footer_scripts', 'register_tinymce_quicktags'); $callback is a function name.
346 * Example 2: Lasso_Helper::remove_action('admin_print_footer_scripts', array('EarnistProductPicker', 'register_tinymce_quicktags')); $callback is array.
347 *
348 * @param string $hook_name Hook name.
349 * @param array|string $callback Callback function. $callback[0] is a class name, $callback[1] is a function name.
350 * @param int $priority Priority.
351 */
352 public static function remove_action( $hook_name, $callback, $priority = 10 ) {
353 global $wp_filter;
354
355 if ( ! isset( $wp_filter[ $hook_name ]->callbacks[ $priority ] ) ) {
356 return;
357 }
358
359 foreach ( $wp_filter[ $hook_name ]->callbacks[ $priority ] as $key_function_name_wp => $data ) {
360 $should_remove = false;
361 $obj_name_wp = null;
362 $function_name_wp = is_array( $data['function'] ) ? $data['function'][1] : $data['function'];
363 if ( ! is_array( $callback ) ) {
364 $function_name = $callback;
365 if ( $function_name_wp === $function_name ) {
366 $should_remove = true;
367 }
368 } else {
369 list( $object_name, $function_name ) = $callback;
370 if ( gettype( $object_name ) === 'object' ) {
371 $object_name = get_class( $object_name );
372 }
373
374 if ( ! $data['function'] instanceof \Closure ) {
375 $obj_name_wp = $data['function'][0];
376 if ( gettype( $obj_name_wp ) === 'object' ) {
377 $obj_name_wp = get_class( $obj_name_wp );
378 if ( $obj_name_wp === $object_name && $function_name_wp === $function_name ) {
379 $should_remove = true;
380 }
381 }
382 }
383 }
384
385 if ( $should_remove ) {
386 unset( $wp_filter[ $hook_name ]->callbacks[ $priority ][ $key_function_name_wp ] );
387 break;
388 }
389 }
390 }
391
392 /**
393 * It includes the HTML for the display modal dialogs
394 *
395 * @return string the html for the modal.
396 */
397 public static function get_display_modal_html() {
398 $html = self::include_with_variables( Helper::get_path_views_folder() . 'modals/display-add.php' ); // phpcs:ignore
399 $html .= self::include_with_variables( Helper::get_path_views_folder() . 'modals/url-add.php', array( 'is_from_editor' => true, ) ); // phpcs:ignore
400 $html .= self::include_with_variables( Helper::get_path_views_folder() . 'modals/url-quick-detail.php' ); // phpcs:ignore
401 $html .= self::wrapper_js_render( 'single-list', Helper::get_path_views_folder() . 'modals/single-jsrender.html' ); // phpcs:ignore
402 $html .= self::wrapper_js_render( 'url-quick-detail-jsrender', Helper::get_path_views_folder() . 'components/url-quick-detail-jsrender.html' ); // phpcs:ignore
403
404 return $html;
405 }
406
407 /**
408 * Get countries of Amazon
409 *
410 * @param string $selected_country Country code.
411 */
412 public static function get_countries_dd( $selected_country ) {
413 $countries = Amazon_Api::get_amazon_api_countries();
414
415 $countries_dd = '<select id="amazon_default_tracking_country" name="amazon_default_tracking_country" class="form-control">';
416 foreach ( $countries as $key => $country ) {
417 if ( strlen( $key ) !== 2 ) {
418 continue;
419 }
420
421 $selected = '';
422 if ( $selected_country === $key ) {
423 $selected = 'selected';
424 }
425 $countries_dd .= '<option value="' . $key . '" ' . $selected . ' >' . $country['name'] . '</option>';
426 }
427 $countries_dd .= '</select>';
428
429 return $countries_dd;
430 }
431
432 /**
433 * Get setup progress information
434 *
435 * @return array
436 */
437 public static function get_setup_progress_information() {
438 $enable_support = boolval( Setting::get_setting( Enum::SUPPORT_ENABLED ) ) ? 20 : 0;
439 $total_links = SURL::total();
440 $links = $total_links > 20 ? 20 : $total_links;
441 $links_percent = 20 === $links ? 100 : ( $links / 20 ) * 100;
442 $setup_amz_tracking_id = boolval( get_option( Enum::SETUP_AMZ_TRACKING_ID ) ) ? 15 : 0;
443 $follow_on_twitter = boolval( get_option( Enum::FOLLOW_ON_TWITTER ) ) ? 10 : 0;
444 $share_on_twitter = boolval( get_option( Enum::SHARE_ON_TWITTER ) ) ? 10 : 0;
445 $leave_a_review = boolval( get_option( Enum::LEAVE_A_REVIEW ) ) ? 5 : 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;
447 $progress = $enable_support + $setup_amz_tracking_id + $follow_on_twitter + $share_on_twitter + ( $links * 2 ) + $leave_a_review;
448 $progress = $progress ? $progress / 100 : 0;
449 $open_modal_add_link = $links < 20 ? 'btn-add-20-links' : '';
450
451 $data = array(
452 'progress' => round( $progress, 2 ),
453 'progress_percent' => round( $progress * 100 ),
454 'links' => $links,
455 'links_percent' => $links_percent,
456 'setup_amz_tracking_id' => $setup_amz_tracking_id,
457 'follow_on_twitter' => $follow_on_twitter,
458 'share_on_twitter' => $share_on_twitter,
459 'leave_a_review' => $leave_a_review,
460 'is_show_review_note' => $is_show_review_note,
461 'enable_support' => $enable_support,
462 'setting_amz_url' => Page::get_lite_page_url( Enum::PAGE_SETTINGS_AMAZON ),
463 'follow_twitter_url' => home_url() . '?' . Enum::SLUG_CLOAK_FOLLOW_TWITTER,
464 'share_twitter_url' => home_url() . '?' . Enum::SLUG_CLOAK_SHARE_TWITTER,
465 'review_url' => home_url() . '?' . Enum::SLUG_CLOAK_LASSO_REVIEW_URL,
466 'open_modal_add_link' => $open_modal_add_link,
467 );
468
469 return $data;
470 }
471
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 /**
539 * Send request
540 *
541 * @param string $method Method (get or post). Default to get.
542 * @param string $url URL. Default to empty.
543 * @param array $data Post data. Default to empty array.
544 * @param array $headers Headers. Default to empty array.
545 * @param bool $is_lasso_save Is Lasso save data action. Default to false.
546 */
547 public static function send_request( $method = 'get', $url = '', $data = array(), $headers = array(), $is_lasso_save = false ) {
548 $method = strtolower( $method );
549 $request_options = array(
550 'headers' => $headers,
551 'timeout' => Constant::TIME_OUT,
552 'sslverify' => Constant::SSL_VERIFY,
553 );
554 $body = wp_json_encode( $data );
555 $headers_expect = ! empty( $body ) && strlen( $body ) > 1048576 ? '100-Continue' : '';
556 if ( 'get' === $method ) {
557 $res = wp_remote_get( $url, $request_options );
558 } elseif ( 'post' === $method ) {
559 $request_options['headers']['expect'] = $headers_expect;
560 $request_options['body'] = $body;
561 $res = wp_remote_post( $url, $request_options );
562 } elseif ( 'put' === $method ) {
563 $request_options['headers']['expect'] = $headers_expect;
564 $request_options['body'] = $body;
565 $request_options['method'] = 'PUT';
566 $res = wp_remote_request( $url, $request_options );
567 }
568
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 );
578 return array(
579 'status_code' => 500,
580 'response' => json_decode( wp_json_encode( $error_payload ) ),
581 );
582 }
583
584 $body = wp_remote_retrieve_body( $res );
585 $status = wp_remote_retrieve_response_code( $res );
586
587 return array(
588 'status_code' => $status,
589 'response' => json_decode( $body ),
590 );
591 }
592
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 /**
624 * Get Lasso Lite - WP option
625 *
626 * @param string $option_name Option name.
627 * @param mixed $default Default value.
628 * @return mixed|void
629 */
630 public static function get_option( $option_name, $default = false ) {
631 return get_option( SIMPLE_URLS_SLUG . '_' . $option_name, $default );
632 }
633
634 /**
635 * Update Lasso Lite - WP option
636 *
637 * @param string $option_name Option name.
638 * @param mixed $option_value Option value.
639 * @param bool $autoload Autoload.
640 * @return bool
641 */
642 public static function update_option( $option_name, $option_value, $autoload = null ) {
643 return update_option( SIMPLE_URLS_SLUG . '_' . $option_name, $option_value, $autoload );
644 }
645
646 /**
647 * Validate URL
648 *
649 * @param string $url URL.
650 */
651 public static function validate_url( $url ) {
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;
667 }
668
669 parse_str( $parsed_url['query'], $query_params );
670
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;
689 }
690
691 /**
692 * Get argument from url
693 *
694 * @param string $link Amazon link.
695 * @param string $argument URL argument.
696 * @return string
697 */
698 public static function get_argument_from_url( $link, $argument ) {
699 if ( ! $argument ) {
700 return '';
701 }
702
703 $link = str_replace( '&amp;', '&', $link );
704 $parse = wp_parse_url( $link );
705 $queries = array();
706 parse_str( $parse['query'] ?? '', $queries );
707
708 return $queries[ $argument ] ?? '';
709 }
710
711 /**
712 * Build url parameter string
713 * Parameter example: array( 'rel_=abc', 'maas=def' );
714 *
715 * @param array $parameters Parameter key=value array.
716 * @return string|mixed
717 */
718 public static function build_url_parameter_string( $parameters = array() ) {
719 $result = implode( '&', $parameters );
720 $result = preg_replace( '!\&+!', '&', $result );
721 $result = trim( $result, '&' );
722
723 return $result;
724 }
725
726 /**
727 * Convert query array to a string (in a url)
728 *
729 * @param array $query Query.
730 */
731 public static function get_query_from_array( $query ) {
732 $result = array();
733 foreach ( $query as $key => $value ) {
734 $result[] = $key . '=' . rawurlencode( $value );
735 }
736 $result = implode( '&', $result );
737 return $result;
738 }
739
740 /**
741 * Convert parse_url() to a url
742 *
743 * @param array $parse Parse data from a URL.
744 * @param bool $host Is it host. Default to false.
745 */
746 public static function get_url_from_parse( $parse, $host = false ) {
747 $parse['host'] = $parse['host'] ?? '';
748 $host = false !== $host ? $host : $parse['host'];
749 $parse['host'] = '://' . $host;
750 $parse['query'] = isset( $parse['query'] ) ? '?' . $parse['query'] : '';
751
752 return implode( '', $parse );
753 }
754
755 /**
756 * Get param of $_SERVER
757 *
758 * @param string $name Name of param.
759 */
760 public static function get_server_param( $name ) {
761 return wp_unslash( $_SERVER[ $name ] ?? '' ); // phpcs:ignore
762 }
763
764 /**
765 * Check if Lasso URL description is empty.
766 *
767 * @param string $description Lasso URL description.
768 * @return bool
769 */
770 public static function is_description_empty( $description ) {
771 if ( empty( $description ) || '<p><br></p>' === $description ) {
772 return true;
773 }
774
775 return false;
776 }
777
778 /**
779 * Get base domain
780 * Ex: "http://domain.com" would return "domain.com"
781 *
782 * @param string $domain Domain. It must be passed WITH protocol. Default to empty.
783 */
784 public static function get_base_domain( $domain = '' ) {
785 $domain = self::add_https( $domain );
786 if ( ! self::validate_url( $domain ) ) {
787 return '';
788 }
789
790 $url = @wp_parse_url( $domain ); // phpcs:ignore
791 $host = $url['host'] ?? '';
792 $host = str_replace( 'www.', '', $host );
793 $host = trim( $host );
794
795 return $host ? $host : '';
796 }
797
798 /**
799 * Convert stdclass object to array
800 *
801 * @param bool $obj StdClass object.
802 */
803 public static function convert_stdclass_to_array( $obj ) {
804 $array = json_decode( wp_json_encode( $obj ), true );
805
806 return $array;
807 }
808
809 /**
810 * Check if mysql error relative to Lasso's table does not exist
811 *
812 * @param string $mysql_error mysql error.
813 * @return boolean
814 */
815 public static function is_lasso_tables_does_not_exist_error( $mysql_error ) {
816 return (bool) preg_match( "/(\.)(.)*lasso_(.)*doesn\'t(\s)exist/", $mysql_error );
817 }
818
819 /**
820 * If the AAWP plugin is active, return true. Otherwise, return false
821 */
822 public static function is_aawp_active() {
823 return is_plugin_active( 'aawp/aawp.php' );
824 }
825
826 /**
827 * If the Amalinks Pro plugin is active, return true. Otherwise, return false
828 */
829 public static function is_amalinks_pro_active() {
830 return is_plugin_active( 'amalinkspro/amalinkspro.php' );
831 }
832
833 /**
834 * Format importable data before showing/importing/reverting
835 *
836 * @param object $p Importable post.
837 */
838 public static function format_importable_data( $p ) {
839 $lasso_db = new Lasso_DB();
840 $lasso_helper = new Helper();
841 $lasso_amazon_api = new Amazon_Api();
842
843 $home_url = home_url();
844 $p->import_permalink = get_permalink( $p->id );
845
846 if ( 'Pretty Links' === $p->import_source ) {
847 $pretty_link_data = $lasso_db->get_pretty_link_by_id( $p->id );
848 $defaul_permalink = $home_url . '/' . $pretty_link_data->slug . '/';
849
850 $prlipro = get_option( 'prlipro_options', array() );
851 $prlipro = is_array( $prlipro ) ? $prlipro : array();
852 $base_slug_prefix = $prlipro['base_slug_prefix'] ?? '';
853 $p->import_permalink = '' !== $base_slug_prefix && strpos( $p->post_name, $base_slug_prefix ) === false
854 ? $home_url . '/' . $base_slug_prefix . '/' . $p->post_name . '/'
855 : $defaul_permalink;
856 } elseif ( 'AAWP' === $p->import_source ) {
857 $aawp_row = $lasso_db->get_aawp_product( $p->id );
858 $p->import_permalink = $aawp_row->url ?? '';
859 $shortcode = '[amazon link="' . $p->post_name . '"]';
860 $p->shortcode = $shortcode;
861
862 // ? AAWP list
863 if ( 'aawp_list' === $p->post_type ) {
864 $p->import_permalink = 'https://amazon.com/s?k=' . $p->post_title;
865 $p->check_status = self::check_aawp_list_is_imported( $p->id ) ? 'checked' : '';
866
867 $cat = term_exists( $p->post_title, Constant::LASSO_CATEGORY );
868 $p->check_status = $cat ? 'checked' : $p->check_status;
869
870 $aawp_list = $lasso_db->get_aawp_list( $p->id );
871 if ( $aawp_list ) {
872 $items_count = $aawp_list->items_count ?? 0;
873 $attr_type = 'bestseller' === $aawp_list->type ? 'bestseller' : 'link';
874 $attr_type = 'new_releases' === $aawp_list->type ? 'new' : $attr_type;
875 $shortcode = '[amazon ' . $attr_type . '="' . $aawp_list->keywords . '" items="' . $items_count . '"]';
876 $p->shortcode = $shortcode;
877 }
878 }
879 } elseif ( 'EasyAzon' === $p->import_source ) {
880 $product = Lasso_DB::get_easyazon_option( $p->post_title );
881 $p->post_title = $product['title'];
882 $p->id = $product['identifier'];
883 $p->import_permalink = $product['url'];
884 $p->post_name = strtolower( $product['identifier'] );
885 $shortcode = '[easyazon_link identifier="' . $p->id . '"]' . $p->post_title . '[/easyazon_link]';
886 $p->shortcode = $shortcode;
887
888 $revert = $lasso_db->is_easyazon_product_imported( $product['identifier'] );
889 if ( $revert ) {
890 $p->id = $revert->lasso_id;
891 }
892 } elseif ( 'AmaLinks Pro' === $p->import_source ) {
893 $shortcode = $p->post_name;
894 $attributes = $lasso_helper->get_attributes( $shortcode );
895 $p->shortcode = $shortcode;
896 $p->import_permalink = $attributes['apilink'] ?? '';
897 if ( empty( $p->id ) ) {
898 $p->id = $attributes['asin'] ?? $p->id;
899
900 $url_details = $lasso_db->get_url_details_by_product_id( $p->id, Amazon_Api::PRODUCT_TYPE );
901 $lasso_id = $url_details->lasso_id ?? 0;
902 $p->check_status = $lasso_id > 0 ? 'checked' : '';
903
904 }
905 if ( empty( $p->import_permalink ) ) {
906 $p->import_permalink = $lasso_amazon_api->get_amazon_link_by_product_id( $p->id );
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 . '"]';
912 }
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
920 return $p;
921 }
922
923 /**
924 * Get attributes of shortcode
925 *
926 * @param string $tags_data Shortcode string.
927 */
928 public static function get_attributes( $tags_data ) {
929 // ? fix shortcode contains content: [shortcode]content[/shortcode]
930 preg_match_all(
931 '/' . get_shortcode_regex() . '/',
932 $tags_data,
933 $matches,
934 PREG_SET_ORDER
935 );
936 $content_between = $matches[0][5] ?? '';
937 $shortcode_name = $matches[0][2] ?? '';
938 if ( ! empty( $shortcode_name ) ) {
939 // ? remove content between content/anchor text
940 $tags_data = str_replace( ']' . $content_between . '[', '][', $tags_data );
941 // ? remove the end shortcode
942 $tags_data = str_replace( '[/' . $shortcode_name . ']', '', $tags_data );
943 }
944 $tags_data = str_replace( '/]', ']', $tags_data );
945
946 $attributes = array();
947 $parse = shortcode_parse_atts( esc_html( $tags_data ) );
948
949 $temp_key = 'temp';
950 foreach ( $parse as $key => $value ) {
951 if ( ! is_integer( $key ) ) {
952 $temp_key = $key;
953 $attributes[ $key ] = self::remove_special_character_in_attributes( $value );
954 } else {
955 // ? join data with old key.
956 $attributes[ $temp_key ] = trim( ( $attributes[ $temp_key ] ?? '' ) . ' ' . self::remove_special_character_in_attributes( $value ) );
957 }
958 }
959 unset( $attributes['temp'] );
960
961 return $attributes;
962 }
963
964 /**
965 * Remove special character in attributes
966 *
967 * @param string $text Text string.
968 */
969 public static function remove_special_character_in_attributes( $text ) {
970 $text = str_replace( 'u0026', '&', $text );
971 $text = str_replace( array( '\\', 'u0022', '&quot;', 'u003c', 'u003e', '&lt;', '&gt;' ), '', $text );
972 return htmlspecialchars_decode( trim( $text, ']' ), ENT_QUOTES );
973 }
974
975 /**
976 * Check whether aawp list id is imported to Lasso or not
977 *
978 * @param int $id Aawp list id.
979 */
980 public static function check_aawp_list_is_imported( $id ) {
981 $lasso_db = new Lasso_DB();
982
983 $sql = '
984 SELECT lud.product_id
985 FROM ' . ( new Url_Details() )->get_table_name() . ' AS lud
986 LEFT JOIN ' . $lasso_db->posts . ' AS p
987 ON lud.lasso_id = p.ID
988 WHERE lud.product_id != \'\'
989 AND p.ID is not null
990 AND lud.product_type = \'' . Amazon_Api::PRODUCT_TYPE . '\'
991 ';
992 $row = $lasso_db->get_col( $sql );
993 $amazon_ids = $row ? $row : array();
994
995 $aawp_list = $lasso_db->get_aawp_list( $id );
996 $aawp_amazon_ids = $aawp_list->product_asins ?? '';
997 $aawp_amazon_ids = '' !== $aawp_amazon_ids ? explode( ',', $aawp_amazon_ids ) : array();
998
999 $same_elements = array_intersect( $aawp_amazon_ids, $amazon_ids );
1000
1001 return $aawp_amazon_ids === $same_elements;
1002 }
1003
1004 /**
1005 * Paginate items by a sql query
1006 * Reset page number if results is empty.
1007 *
1008 * @param string $sql Sql query.
1009 * @param int $page Number of page.
1010 * @param int $limit Number of results. Default to 10.
1011 */
1012 public static function paginate( $sql, &$page, $limit = 10 ) {
1013 $start_index = ( $page - 1 ) * $limit;
1014 $pagination_sql = $sql . ' LIMIT ' . $start_index . ', ' . $limit;
1015
1016 if ( $page > 1 ) {
1017 $result = Model::get_row( $pagination_sql );
1018
1019 if ( ! $result ) {
1020 $page = 1;
1021 $pagination_sql = $sql . ' LIMIT 0, ' . $limit;
1022 }
1023 }
1024
1025 return $pagination_sql;
1026 }
1027
1028 /**
1029 * Format "post title" to escape html and apply limit length.
1030 *
1031 * @param Title $title lasso urls title.
1032 * @param int $limit_length limit length of title.
1033 */
1034 public static function format_post_title( $title, $limit_length = 200 ) {
1035 $title = esc_html( $title );
1036
1037 if ( strlen( $title ) > $limit_length ) {
1038 $title = substr( $title, 0, $limit_length ) . '...';
1039 }
1040
1041 return $title;
1042 }
1043
1044 /**
1045 * Get unique post name of lasso post
1046 *
1047 * @param int $post_id Post id.
1048 * @param string $post_name Post name.
1049 */
1050 public static function lasso_unique_post_name( $post_id, $post_name ) {
1051 if ( intval( $post_id ) > 0 && ! empty( $post_name ) && self::the_slug_exists( $post_name, $post_id ) ) {
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 );
1054 }
1055
1056 return $post_name;
1057 }
1058
1059 /**
1060 * Get plugin status result
1061 *
1062 * @param string $plugin Plugin key.
1063 * @return bool
1064 */
1065 public static function get_is_plugin_active( $plugin ) {
1066 $cache_result = Cache_Per_Process::get_instance()->get_cache( 'is_plugin_active_' . md5( $plugin ), null );
1067 if ( null !== $cache_result ) {
1068 return $cache_result;
1069 }
1070
1071 if ( ! function_exists( 'is_plugin_active' ) ) {
1072 include_once ABSPATH . 'wp-admin/includes/plugin.php';
1073 }
1074
1075 $result = \is_plugin_active( $plugin );
1076 Cache_Per_Process::get_instance()->set_cache( 'is_plugin_active_' . md5( $plugin ), $result );
1077
1078 return $result;
1079 }
1080
1081 /**
1082 *
1083 * Check plugin earnist is loaded https://www.getearnist.com.
1084 *
1085 * @return bool
1086 */
1087 public static function is_earnist_plugin_loaded() {
1088 return self::get_is_plugin_active( 'earnist/earnist.php' );
1089 }
1090
1091 /**
1092 *
1093 * Check plugin Shortcode Star Rating is loaded https://github.com/modshrink/shortcode-star-rating.
1094 *
1095 * @return bool
1096 */
1097 public static function is_shortcode_start_rating_plugin_loaded() {
1098 return self::get_is_plugin_active( 'shortcode-star-rating/shortcode-star-rating.php' );
1099 }
1100
1101 /**
1102 * Check plugin "Easy Table of Contents" is activated
1103 *
1104 * @return bool
1105 */
1106 public static function is_plugin_easy_table_of_contents_activated() {
1107 return self::get_is_plugin_active( 'easy-table-of-contents/easy-table-of-contents.php' );
1108 }
1109
1110 /**
1111 * Check if Gravity Perks plugin is active.
1112 *
1113 * @return bool
1114 */
1115 public static function is_gravity_perks_plugin_active() {
1116 return self::get_is_plugin_active( 'gravityperks/gravityperks.php' );
1117 }
1118
1119 /**
1120 * Check if Ezoic plugin is active.
1121 *
1122 * @return bool
1123 */
1124 public static function is_ezoic_plugin_active() {
1125 return self::get_is_plugin_active( 'ezoic-integration/ezoic-integration.php' );
1126 }
1127
1128 /**
1129 * Check if WP Rocket - Lazyload is enabled.
1130 *
1131 * @return bool
1132 */
1133 public static function is_wp_rocket_lazyload_image_enabled() {
1134 if ( self::get_is_plugin_active( 'wp-rocket/wp-rocket.php' ) ) {
1135 // ? Check if layzyload enabled
1136 $wp_rocket_settings = get_option( 'wp_rocket_settings', array() );
1137 if ( $wp_rocket_settings['lazyload'] ?? false ) {
1138 return true;
1139 }
1140 }
1141
1142 return false;
1143 }
1144
1145 /**
1146 * Check whether current page is WP post page
1147 */
1148 public static function is_wordpress_post() {
1149 global $pagenow;
1150
1151 $get = self::GET(); // phpcs:ignore
1152 $action = $get['action'] ?? '';
1153 $add_new_page = 'post-new.php' === $pagenow;
1154 $edit_page = 'post.php' === $pagenow && 'edit' === $action;
1155 $post_type = $get['post_type'] ?? '';
1156
1157 if ( ( 'edit.php' === $pagenow || $add_new_page ) && '' === $post_type ) {
1158 $post_type = 'post';
1159 } elseif ( $add_new_page ) {
1160 $post_type = $get['post_type'] ?? $post_type;
1161 } elseif ( $edit_page ) {
1162 $post_id = intval( $get['post'] ?? 0 );
1163 $post_type = $post_id > 0 ? get_post_type( $post_id ) : $post_type;
1164 }
1165
1166 if ( 'term.php' === $pagenow ) {
1167 $post_type = '';
1168 }
1169
1170 return 'post' === $post_type || 'page' === $post_type;
1171 }
1172
1173 /**
1174 * Cast the value to boolean
1175 *
1176 * @param bool|string $value A string boolean like "true" or "false".
1177 *
1178 * @return bool
1179 */
1180 public static function cast_to_boolean( $value ) {
1181 return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1182 }
1183
1184 /**
1185 * Get CPU load of server/hosting
1186 */
1187 public static function get_cpu_load() {
1188 $load = null;
1189
1190 if ( stristr( PHP_OS, 'win' ) ) {
1191 $cmd = 'wmic cpu get loadpercentage /all';
1192 @exec( $cmd, $output ); // phpcs:ignore
1193
1194 if ( $output ) {
1195 foreach ( $output as $line ) {
1196 if ( $line && preg_match( '/^[0-9]+$/', $line ) ) {
1197 $load = $line;
1198 break;
1199 }
1200 }
1201 }
1202 } else {
1203 try {
1204 if ( @is_readable( '/proc/stat' ) ) { // phpcs:ignore
1205 $cached_cpu_load = Cache_Per_Process::get_instance()->get_cache( 'cpu_load', null );
1206 if ( $cached_cpu_load ) {
1207 $stat_data1 = $cached_cpu_load;
1208 } else {
1209 $stat_data1 = self::get_server_load_linux_data();
1210 }
1211
1212 // ? Collect 2 samples - each with 1 second period
1213 // ? See: https://de.wikipedia.org/wiki/Load#Der_Load_Average_auf_Unix-Systemen
1214 sleep( 1 );
1215
1216 $stat_data2 = self::get_server_load_linux_data();
1217 Cache_Per_Process::get_instance()->set_cache( 'cpu_load', $stat_data2 );
1218
1219 if ( ( ! is_null( $stat_data1 ) ) && ( ! is_null( $stat_data2 ) ) ) {
1220 // ? Get difference
1221 $stat_data2[0] -= $stat_data1[0];
1222 $stat_data2[1] -= $stat_data1[1];
1223 $stat_data2[2] -= $stat_data1[2];
1224 $stat_data2[3] -= $stat_data1[3];
1225
1226 // ? Sum up the 4 values for User, Nice, System and Idle and calculate
1227 // ? the percentage of idle time (which is part of the 4 values!)
1228 $cpu_time = $stat_data2[0] + $stat_data2[1] + $stat_data2[2] + $stat_data2[3];
1229
1230 // ? Invert percentage to get CPU time, not idle time
1231 $load = 100 - ( $stat_data2[3] * 100 / max( $cpu_time, 1 ) );
1232 }
1233 }
1234 } catch ( Exception $e ) {
1235 $load = 0; // Just run because we can't detect CPU load.
1236 }
1237 }
1238
1239 return round( $load, 2 );
1240 }
1241
1242 /**
1243 * Get server load linux data
1244 */
1245 private static function get_server_load_linux_data() {
1246 if ( @is_readable( '/proc/stat' ) ) { // phpcs:ignore
1247 $stats = @file_get_contents( '/proc/stat' ); // phpcs:ignore
1248
1249 if ( false !== $stats ) {
1250 // ? Remove double spaces to make it easier to extract values with explode()
1251 $stats = preg_replace( '/[[:blank:]]+/', ' ', $stats );
1252
1253 // ? Separate lines
1254 $stats = str_replace( array( "\r\n", "\n\r", "\r" ), "\n", $stats );
1255 $stats = explode( "\n", $stats );
1256
1257 // ? Separate values and find line for main CPU load
1258 foreach ( $stats as $stat_line ) {
1259 $stat_line_data = explode( ' ', trim( $stat_line ) );
1260
1261 // ? Found
1262 if ( count( $stat_line_data ) >= 5 && 'cpu' === $stat_line_data[0] ) {
1263 return array(
1264 $stat_line_data[1],
1265 $stat_line_data[2],
1266 $stat_line_data[3],
1267 $stat_line_data[4],
1268 );
1269 }
1270 }
1271 }
1272 }
1273
1274 return null;
1275 }
1276
1277 /**
1278 * Remove unexpected character from post title
1279 *
1280 * @param string $post_title Post title.
1281 * @return string
1282 */
1283 public static function remove_unexpected_characters_from_post_title( $post_title ) {
1284 // ? Remove unexpected character.
1285 $post_title = preg_replace( "/[^A-Za-z0-9\s`~!@#$%^&:;\/\?\"\'\+\=\.\,\-\_\*\(\)\|\[\]\<\>\{\}\\\]/", ' ', $post_title );
1286 // ? Remove duplicated space.
1287 $post_title = preg_replace( '/\s\s+/', ' ', $post_title );
1288
1289 return $post_title;
1290 }
1291
1292 /**
1293 * Check whether this install is new
1294 */
1295 public static function is_new_install() {
1296 return boolval( get_option( Enum::LASSO_LITE_ACTIVE ) );
1297 }
1298
1299 /**
1300 * Build image lazyload attributes
1301 *
1302 * @return string
1303 */
1304 public static function build_img_lazyload_attributes() {
1305 $result = 'loading="lazy"'; // ? WP default lazyload.
1306
1307 if ( self::is_ezoic_plugin_active() ) {
1308 $result = 'class="ezlazyload"';
1309 } elseif ( self::is_wp_rocket_lazyload_image_enabled() ) {
1310 $result = 'class="rocket-lazyload"';
1311 }
1312
1313 return $result;
1314 }
1315
1316 /**
1317 * Check whether import page should display
1318 *
1319 * @return bool
1320 */
1321 public static function should_show_import_page() {
1322 return ! empty( ( new Lasso_DB() )->get_import_plugins( true ) ) ? true : false;
1323 }
1324
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 /**
1405 * Get brag icon
1406 *
1407 * @param bool $force_to_show Force to show the brag. Default to false.
1408 */
1409 public static function get_brag_icon( $force_to_show = false ) {
1410 $cache_key = 'lasso_lite_brag_icon';
1411 $brag_cache = Cache_Per_Process::get_instance()->get_cache( $cache_key, '' );
1412
1413 if ( $brag_cache ) {
1414 return $brag_cache;
1415 }
1416
1417 $lasso_settings = Setting::get_settings();
1418
1419 // Brag mode is always enabled in Lite.
1420 $enable_brag_mode = true;
1421 $lasso_url = $lasso_settings['lasso_affiliate_URL'] ?? false;
1422
1423 if ( $lasso_url && ( $force_to_show || $enable_brag_mode ) ) {
1424 $icon_brag = esc_url( SIMPLE_URLS_URL . '/admin/assets/images/lasso-icon-brag.svg' );
1425 $lasso_affiliate_url = self::add_params_to_url( $lasso_url, array( 'utm_source' => 'brag' ) );
1426 $img_attr = self::build_img_lazyload_attributes();
1427 $icon = '
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">
1430 </a>
1431 ';
1432
1433 Cache_Per_Process::get_instance()->set_cache( $cache_key, $icon );
1434
1435 return $icon;
1436 }
1437
1438 return '';
1439 }
1440
1441 /**
1442 * Add params to URL
1443 *
1444 * @param string $url URL.
1445 * @param array $params Params.
1446 */
1447 public static function add_params_to_url( $url, $params ) {
1448 // ? parse url
1449 $parse = wp_parse_url( $url );
1450 parse_str( $parse['query'] ?? '', $query );
1451
1452 $query = array_merge( $query, $params );
1453 $query = self::get_query_from_array( $query );
1454 $parse['query'] = $query;
1455
1456 return self::get_url_from_parse( $parse );
1457 }
1458
1459 /**
1460 * Get final url in the url
1461 * Example: https://affiliate.com/redirect?url=https://getlasso.co
1462 *
1463 * @param string $url URL.
1464 */
1465 public static function get_final_url_from_url_param( $url ) {
1466 if ( ! self::validate_url( $url ) ) {
1467 return false;
1468 }
1469
1470 $final_url = false;
1471 $base_domain = self::get_base_domain( $url );
1472
1473 if ( self::is_shareasale_url( $url ) ) {
1474 $final_url = self::get_argument_from_url( $url, 'urllink' );
1475 } elseif ( 'pntra.com' === $base_domain ) {
1476 $final_url = self::get_argument_from_url( $url, 'url' );
1477 } elseif ( 'wordseed.com' === $base_domain ) {
1478 $final_url = self::get_argument_from_url( $url, 'url' );
1479 } elseif ( 'titan.fitness' === $base_domain ) {
1480 $final_url = 'https://www.titan.fitness' . self::get_argument_from_url( $url, 'redirect' );
1481 } else { // ? other urls
1482 $parse = wp_parse_url( $url );
1483 $queries = array();
1484 parse_str( $parse['query'] ?? '', $queries );
1485 foreach ( $queries as $key => $param ) {
1486 if ( 'referrer' === $key ) {
1487 continue;
1488 }
1489
1490 $param = str_replace( ' ', '%20', $param );
1491 if ( self::validate_url( $param ) ) {
1492 $final_url = $param;
1493 break;
1494 }
1495 }
1496 }
1497 $final_url = self::add_https( $final_url );
1498
1499 if ( empty( $final_url ) || ! self::validate_url( $final_url ) ) {
1500 $final_url = false;
1501 } else {
1502 $final_url = trim( $final_url, '/' );
1503 }
1504
1505 return $final_url;
1506 }
1507
1508 /**
1509 * Check whether url is shareasale domain or not
1510 *
1511 * @param string $url URL.
1512 * @return bool
1513 */
1514 public static function is_shareasale_url( $url ) {
1515 $allow_domains = array( 'shareasale.com', 'shareasale-analytics.com' );
1516 $domain = self::get_base_domain( $url );
1517
1518 return in_array( $domain, $allow_domains, true );
1519 }
1520
1521 /**
1522 * Check importable
1523 *
1524 * @return bool
1525 */
1526 public static function is_importable() {
1527 $lasso_lite_db = new Lasso_DB();
1528 $sql = $lasso_lite_db->get_importable_urls_query( true );
1529 $post = Model::get_row( $sql );
1530
1531 if ( ! empty( $post ) ) {
1532 $post->post_title = self::format_post_title( $post->post_title ?? '' );
1533 $post->shortcode = '';
1534
1535 // ? Get import target permalinks
1536 $post = self::format_importable_data( $post );
1537
1538 // ? Check first record from list import
1539 return 'checked' !== $post->check_status;
1540 }
1541
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', '' ), 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 $res = self::send_request( 'get', $request_url, array(), $headers );
1777
1778 // phpcs:ignore
1779 // $res = self::send_request( 'get', LASSO_LINK . '/link/status/?' . $encrypted_base64, array(), $headers );
1780 if ( $get_res ) {
1781 return $res;
1782 }
1783
1784 $bls_response = $res['response'] ?? null;
1785 if ( ! is_object( $bls_response ) ) {
1786 return 500;
1787 }
1788
1789 return intval( $bls_response->status ?? $status );
1790 }
1791
1792 /**
1793 * Get currency symbol from ISO currency code
1794 *
1795 * @param string $iso Iso currency code.
1796 * @return string
1797 */
1798 public static function get_currency_symbol_from_iso_code( $iso ) {
1799 $iso = strtoupper( $iso );
1800 $result = '$';
1801
1802 $currencies = array(
1803 'USD' => '$',
1804 'AUD' => '$',
1805 'CAD' => '$',
1806 'EUR' => '',
1807 'MXN' => '$',
1808 'CNY' => '¥',
1809 'JPY' => '¥',
1810 'INR' => '',
1811 'SEK' => 'kr',
1812 'BRL' => 'R$',
1813 'TRY' => 'TL',
1814 'GBP' => '£',
1815 'PLN' => '',
1816 'EGP' => '',
1817 'SGD' => 'S$',
1818 'AED' => 'AED',
1819 );
1820
1821 return isset( $currencies[ $iso ] ) ? $currencies[ $iso ] : $result;
1822 }
1823
1824 /**
1825 * Check using WP classic editor
1826 *
1827 * @return bool
1828 */
1829 public static function is_classic_editor() {
1830 return self::is_classic_editor_plugin_active() || self::is_disable_gutenberg_plugin_active();
1831 }
1832
1833 /**
1834 * Escape string prevent SQL injection
1835 *
1836 * @param string $keyword Keyword.
1837 * @param bool $search_from_after Option to check search string from end of string.
1838 * @return string
1839 */
1840 public static function esc_like_query( $keyword, $search_from_after = false ) {
1841 global $wpdb;
1842
1843 $wild = '%';
1844 if ( $search_from_after ) {
1845 $query = $wpdb->esc_like( $keyword ) . $wild;
1846 } else {
1847 $query = $wild . $wpdb->esc_like( $keyword ) . $wild;
1848 }
1849
1850 $query = str_replace( ' ', '%', $query );
1851 return $query;
1852 }
1853
1854 /**
1855 * Sanitize HTML for safe display (formerly regex-based script strip).
1856 * Now delegates to wp_kses_post so event handlers and javascript: URLs are removed.
1857 *
1858 * Falsy input (null, false, empty string) is returned unchanged.
1859 *
1860 * @param string|null|false $html HTML code.
1861 * @return string|null|false Sanitized HTML, or the original falsy value.
1862 */
1863 public static function sanitize_script( $html ) {
1864 if ( ! $html ) {
1865 return $html;
1866 }
1867
1868 return wp_kses_post( (string) $html );
1869 }
1870
1871 /**
1872 * Verify access and nonce, then return wp_send_json_error if unverified.
1873 *
1874 * @param bool $allow_edit_post_access Allow access for editor, author, contributor.
1875 * @return void
1876 */
1877 public static function verify_access_and_nonce( $allow_edit_post_access = false ) {
1878 try {
1879 // ? Verify access token.
1880 if ( ! current_user_can( 'manage_options' ) ) {
1881 if ( $allow_edit_post_access ) {
1882 // ? Allow access for editor, author, contributor.
1883 // ? WP User Roles: https://wordpress.com/support/invite-people/user-roles/#:~:text=Editor%3A%20Has%20access%20to%20all,posts%20until%20they%20are%20published.
1884 $current_user_role = self::get_current_user_role();
1885 if ( ! in_array( $current_user_role, array( 'editor', 'author', 'contributor' ), true ) ) {
1886 wp_send_json_error( 'Access denied.' );
1887 }
1888 } else {
1889 wp_send_json_error( 'Access denied.' );
1890 }
1891 }
1892
1893 // ? Verify nonce.
1894 $data = array();
1895 $server = wp_unslash( $_SERVER );
1896 $method = $server['REQUEST_METHOD'] ?? '';
1897 if ( 'POST' === $method ) {
1898 $data = self::POST();
1899 } elseif ( 'GET' === $method ) {
1900 $data = self::GET();
1901 }
1902
1903 $nonce = $data['nonce'] ?? '';
1904 if ( false === wp_verify_nonce( $nonce, Constant::LASSO_LITE_NONCE . wp_salt() ) ) {
1905 wp_send_json_error( 'Nonce not verified.' );
1906 }
1907 } catch ( WPAjaxDieStopException $e ) {
1908 throw $e;
1909 } catch ( \Exception $e ) {
1910 wp_send_json_error( 'Verify access and nonce error.' );
1911 }
1912 }
1913
1914 /**
1915 * Get current user role
1916 *
1917 * @return string
1918 */
1919 public static function get_current_user_role() {
1920 if ( is_user_logged_in() ) {
1921 $user = wp_get_current_user();
1922 $roles = (array) $user->roles;
1923
1924 return $roles[0];
1925 } else {
1926 return 'guest';
1927 }
1928 }
1929
1930 /**
1931 * Build headers for Lasso API
1932 *
1933 * @param string $license_id License ID. Default to null.
1934 * @return array
1935 */
1936 public static function get_headers( $license_id = null ) {
1937 $license = $license_id ? $license_id : License::get_license();
1938 if ( ! is_string( $license ) && ! is_numeric( $license ) ) {
1939 $license = '';
1940 }
1941 $license = (string) $license;
1942
1943 $site_id = License::get_site_id();
1944 if ( ! is_string( $site_id ) && ! is_numeric( $site_id ) ) {
1945 $site_id = '';
1946 }
1947 $site_id = (string) $site_id;
1948
1949 $headers = array(
1950 'Content-Type' => 'application/json',
1951 'license' => $license,
1952 'site_id' => $site_id,
1953 'site_url' => rawurlencode( site_url() ),
1954 'is_lasso_lite' => '1',
1955 'email' => (string) get_option( 'admin_email', '' ),
1956 );
1957
1958 return $headers;
1959 }
1960
1961 /**
1962 * Build lsid
1963 *
1964 * @return false|string
1965 */
1966 public static function build_lsid() {
1967 $lsid = session_create_id( 'ls-' );
1968 if ( ! $lsid ) {
1969 $lsid = 'ls-' . md5( uniqid( wp_rand(), true ) );
1970 }
1971
1972 return $lsid;
1973 }
1974
1975 /**
1976 * Check if WP Elementor plugin is active.
1977 *
1978 * @return bool
1979 */
1980 public static function is_wp_elementor_plugin_actived() {
1981 return self::get_is_plugin_active( 'elementor/elementor.php' );
1982 }
1983
1984 /**
1985 * FOLLOW ALL REDIRECTS
1986 * This makes multiple requests, following each redirect until it reaches the final destination.
1987 *
1988 * @param string $url URL.
1989 * @param bool $is_lasso_save Is Lasso save data action. Default to false.
1990 * @param bool $get_page_title Get page title or not. Default to false.
1991 */
1992 public static function get_redirect_final_target( $url, $is_lasso_save = false, $get_page_title = false ) {
1993 // ? Get final url for amazon shortlink from cache
1994 $amazon_shortlink_final_url_cached = Amazon_Api::get_shortlink_final_url_cached( $url );
1995 if ( $amazon_shortlink_final_url_cached ) {
1996 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;
1997 }
1998
1999 $origin_url = $url;
2000 $url = Amazon_Api::get_amazon_product_url( $url, $is_lasso_save ? true : false );
2001 $url = self::format_url_before_requesting( $url );
2002 $base_domain = self::get_base_domain( $url );
2003 $browser = self::get_server_param( 'HTTP_USER_AGENT' );
2004 $browser = '' !== $browser ? $browser : self::$user_agent;
2005 $use_bls = apply_filters( 'get_final_url_domain_bls', false, $url );
2006
2007 $cache_prefix = 'get_final_url_';
2008 $page_title = '';
2009
2010 // ? check result in cache first, it may be use before in the same request.
2011 $final_url_cache = Cache_Per_Process::get_instance()->get_cache( $cache_prefix . md5( $url ) . $get_page_title );
2012 if ( $final_url_cache ) {
2013 return $final_url_cache;
2014 }
2015 if ( Amazon_Api::is_amazon_url( $url ) && Amazon_Api::get_product_id_by_url( $url ) ) {
2016 return $get_page_title ? array( $url, $page_title ) : $url;
2017 }
2018
2019 $res = wp_remote_get(
2020 $url,
2021 array(
2022 'headers' => array(
2023 'user-agent' => $browser,
2024 ),
2025 )
2026 );
2027
2028 $is_amazon_shortened_url = Amazon_Api::is_amazon_shortened_url( $url );
2029
2030 $status_code = is_wp_error( $res ) ? 500 : $res['response']['code'] ?? '';
2031 if ( 200 === $status_code || 429 === $status_code ) {
2032 $new_url = $res['http_response']->get_response_object()->url;
2033 $use_bls = apply_filters( 'get_final_url_domain_bls', false, $new_url );
2034 }
2035
2036 if ( $is_amazon_shortened_url ) {
2037 $use_bls = true;
2038 }
2039
2040 if ( is_wp_error( $res ) || $use_bls || 403 === $status_code ) {
2041 $headers = self::get_headers();
2042 $data = array(
2043 'url' => $url,
2044 );
2045 $encrypted_base64 = http_build_query( $data );
2046 $res = self::send_request( 'get', Constant::LASSO_LINK . '/link/final-url/?' . $encrypted_base64, array(), $headers );
2047
2048 $bls_response = ( isset( $res['response'] ) && is_object( $res['response'] ) ) ? $res['response'] : null;
2049 $final_url = ( null !== $bls_response ) ? ( $bls_response->finalUrl ?? $url ) : $url;
2050 $page_title = ( null !== $bls_response ) ? ( $bls_response->pageTitle ?? '' ) : '';
2051
2052 $bls_status = ( null !== $bls_response && isset( $bls_response->status ) ) ? intval( $bls_response->status ) : 0;
2053 if ( $bls_status ) {
2054 $response_status = $bls_status;
2055 } elseif ( 200 === intval( $res['status_code'] ?? 0 ) ) {
2056 $response_status = 500;
2057 } else {
2058 $response_status = intval( $res['status_code'] ?? 500 );
2059 }
2060
2061 // ? Set the response status code for add new link process
2062 Cache_Per_Process::get_instance()->set_cache( Affiliate_Link::ADD_NEW_LINK_RESPONSE_STATUS . md5( $origin_url ), $response_status );
2063
2064 $tmp_url = self::get_final_url_from_url_param( $final_url );
2065 if ( $tmp_url ) {
2066 $page_title = self::get_title_by_url( $tmp_url );
2067 $final_url = $tmp_url;
2068 }
2069
2070 // ? cache result
2071 $result = $get_page_title ? array( $final_url, $page_title ) : $final_url;
2072 Cache_Per_Process::get_instance()->set_cache( $cache_prefix . md5( $url ) . $get_page_title, $result );
2073
2074 // ? Cache the final url of amazon shortlink
2075 if ( $is_amazon_shortened_url ) {
2076 $shortlink_cache_key = Amazon_Api::build_shortlink_cache_key( $url );
2077 update_option( $shortlink_cache_key, $final_url );
2078 // ? Cache the page title of amazon shortlink
2079 update_option( $shortlink_cache_key . '_page_title', $page_title );
2080 }
2081
2082 return $result;
2083 }
2084
2085 $http_response = $res['http_response']->get_response_object();
2086 $status = wp_remote_retrieve_response_code( $res );
2087
2088 // ? Set the response status code for add new link process
2089 Cache_Per_Process::get_instance()->set_cache( Affiliate_Link::ADD_NEW_LINK_RESPONSE_STATUS . md5( $origin_url ), $status );
2090
2091 $final_url = $http_response->url;
2092 $page_title = self::get_page_title( $http_response->body );
2093 if ( strpos( $page_title, 'Please Wait...' ) !== false
2094 || strpos( $page_title, 'Cloudflare' ) !== false
2095 || strpos( $page_title, 'Access Denied' ) !== false
2096 || strpos( $page_title, 'Just a moment...' ) !== false
2097 ) {
2098 $page_title = self::get_title_by_url( $final_url );
2099 }
2100
2101 $tmp_url = self::get_final_url_from_url_param( $final_url );
2102 if ( $tmp_url ) {
2103 $page_title = self::get_title_by_url( $tmp_url );
2104 $final_url = $tmp_url;
2105 }
2106
2107 // ? Cache the final url of amazon shortlink
2108 if ( $is_amazon_shortened_url ) {
2109 $shortlink_cache_key = Amazon_Api::build_shortlink_cache_key( $url );
2110 update_option( $shortlink_cache_key, $final_url );
2111 // ? Cache the page title of amazon shortlink
2112 update_option( $shortlink_cache_key . '_page_title', $page_title );
2113 }
2114
2115 if ( ! $page_title || Affiliate_Link::DEFAULT_TITLE === $page_title ) {
2116 $page_title = self::get_title_by_url( $final_url );
2117 }
2118
2119 // ? cache result
2120 $result = $get_page_title ? array( $final_url, $page_title ) : $final_url;
2121 Cache_Per_Process::get_instance()->set_cache( $cache_prefix . md5( $url ) . $get_page_title, $result );
2122
2123 return $result;
2124 }
2125
2126 /**
2127 * Get page title from HTML
2128 *
2129 * @param string $html HTML string.
2130 */
2131 public static function get_page_title( $html ) {
2132 $temp = explode( '<title', $html )[1] ?? '';
2133 $html = $temp ? '<title' . $temp : $html;
2134 $temp = explode( '</title>', $html )[0] ?? '';
2135 $html = $temp ? $temp . '</title>' : $html;
2136 $res = preg_match( '/<title\s*(.*?)>(.*?)<\/title>/siU', $html, $title_matches );
2137 if ( ! $res ) {
2138 return '';
2139 }
2140
2141 // ? Clean up title: remove EOL's and excessive whitespace.
2142 $title = preg_replace( '/\s+/', ' ', $title_matches[2] ?? '' );
2143 // ? String – to UTF8 is \xe2\x80\x93, replace by -
2144 $title = str_replace( '', '-', $title );
2145 // ? Remove all non-US-ASCII (i.e. outside 0x0-0x7F) characters
2146 $title = preg_replace( '/[^\x00-\x7F]/', '', $title );
2147 $title = trim( $title );
2148 $title = self::format_post_title( $title );
2149
2150 return $title;
2151 }
2152
2153 /**
2154 * Build the dynamic query variable name for serving the performance snippet.
2155 *
2156 * Derive a domain-specific key to reduce collisions and support per-domain routing.
2157 *
2158 * @return string Query var name (md5 hash of the base domain)
2159 */
2160 public static function get_snippet_query() {
2161 $domain = self::get_base_domain( site_url() );
2162 if ( empty( $domain ) ) {
2163 return 'lasso_connect_snippet_lite';
2164 }
2165 return md5( $domain );
2166 }
2167
2168 /**
2169 * Derive Intercom user_id from email with optional JWT override.
2170 * If a JWT with a non-empty `user_id` claim is present, that value is used;
2171 * otherwise the md5 hash of the lowercased+trimmed email is returned.
2172 * Callers may pass raw or pre-normalized email; the function normalizes
2173 * defensively to keep the derived user_id deterministic.
2174 *
2175 * @param string $user_email Email (raw or pre-normalized); trimmed and lowercased internally.
2176 * @param string $intercom_user_jwt Optional Intercom JWT token used to override
2177 * the email-based user_id when valid.
2178 *
2179 * @return string Intercom user_id derived from JWT or email hash.
2180 */
2181 public static function get_intercom_user_id( $user_email, $intercom_user_jwt ) {
2182 // MD5 is used only for a deterministic Intercom identifier; not for secrets.
2183 // Normalize email casing to keep intercom user_id deterministic.
2184 $normalized_email = is_string( $user_email ) ? strtolower( trim( $user_email ) ) : '';
2185 $intercom_user_id = md5( $normalized_email );
2186 if ( ! is_string( $intercom_user_jwt ) || '' === trim( $intercom_user_jwt ) ) {
2187 return $intercom_user_id;
2188 }
2189
2190 // JWT is expected to be issued by our backend; no signature verification here.
2191 $jwt_parts = explode( '.', $intercom_user_jwt );
2192 if ( 3 === count( $jwt_parts ) ) {
2193 $payload = $jwt_parts[1];
2194 $payload = strtr( $payload, '-_', '+/' );
2195 $payload_length = strlen( $payload );
2196 $payload_padding = $payload_length % 4;
2197 // Pad base64 payload to the next multiple of 4 so decode succeeds.
2198 if ( 0 !== $payload_padding ) {
2199 $payload = str_pad( $payload, $payload_length + 4 - $payload_padding, '=', STR_PAD_RIGHT );
2200 }
2201 $payload_decoded = base64_decode( $payload, true );
2202 if ( false !== $payload_decoded ) {
2203 $decoded = json_decode( $payload_decoded );
2204 if ( JSON_ERROR_NONE === json_last_error() && is_object( $decoded ) && isset( $decoded->user_id ) && is_scalar( $decoded->user_id ) ) {
2205 $user_id_claim = (string) $decoded->user_id;
2206 if ( '' !== trim( $user_id_claim ) ) {
2207 $intercom_user_id = $user_id_claim;
2208 }
2209 }
2210 }
2211 }
2212
2213 return $intercom_user_id;
2214 }
2215 }
2216