PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.2.6
ShopBuilder – WooCommerce Builder For Elementor v3.2.6
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
← All changes | app/Helpers/Fns.php +1311 -148 2.1.143.2.6 View file →
@@ -11,15 +11,23 @@
11 11 if ( ! defined( 'ABSPATH' ) ) {
12 12 exit( 'This script cannot be accessed directly.' );
13 13 }
14 14
15 +
16 +use CodesVault\Howdyqb\DB;
17 +use DateTime;
18 +use WP_Styles;
15 19 use WC_Product;
20 +use Elementor\Plugin;
16 21 use WC_Product_Query;
17 22 use Elementor\Icons_Manager;
18 23 use RadiusTheme\SB\Models\ReSizer;
24 +use RadiusTheme\SB\Models\Settings;
19 25 use RadiusTheme\SB\Models\DataModel;
20 26 use RadiusTheme\SB\Models\ModuleList;
21 -use RadiusTheme\SB\Models\Settings;
27 +use RadiusTheme\SB\Models\GeneralList;
28 +use RadiusTheme\SB\Models\ElementList;
29 +use RadiusTheme\SB\Controllers\AssetRegistry;
22 30
23 31 /**
24 32 * Fns class
25 33 */
@@ -28,10 +36,55 @@
28 36 /**
29 37 * @var array
30 38 */
31 39 private static $cache = [];
40 + /**
41 + * Check if the stored license is valid. Its Only Show Pro Label.
42 + *
43 + * @return bool
44 + */
45 + public static function has_valid_license() {
46 + static $cached_result = null;
47 + // Return cached result if already calculated in this request.
48 + if ( null !== $cached_result ) {
49 + return $cached_result;
50 + }
51 + $license_status = rtsb()->has_pro() ? rtsbpro()->get_license( 'license_status' ) : '';
52 + return 'valid' === $license_status;
53 + }
32 54
33 55 /**
56 + * Check if license is valid or bypassed by theme,
57 + *
58 + * @return bool
59 + */
60 + public static function is_pro_authorized() {
61 + static $cached_result = null;
62 + if ( null !== $cached_result ) {
63 + return $cached_result;
64 + }
65 + // ❗ Step 2: If Pro not installed → no access.
66 + if ( ! function_exists( 'rtsbpro' ) ) {
67 + $cached_result = false;
68 + return $cached_result;
69 + }
70 + $current_theme = wp_get_theme()->get_stylesheet(); // Child theme And Parent Theme Need Check work or not.
71 + /**
72 + * Allowed themes (bypass license)
73 + *
74 + * @param array $themes
75 + */
76 + $allowed_themes = apply_filters( 'rt_pro_access_allowed_themes', [] );
77 + // ✅ Step 1: Theme bypass (no license check).
78 + if ( in_array( $current_theme, $allowed_themes, true ) ) {
79 + $cached_result = true;
80 + return true;
81 + }
82 + // ✅ Step 3: Normal license validation
83 + $license_status = rtsbpro()->get_license( 'license_status' );
84 + return ( 'valid' === $license_status );
85 + }
86 + /**
34 87 * Verify nonce.
35 88 *
36 89 * @return bool
37 90 */
@@ -44,33 +97,60 @@
44 97
45 98 return false;
46 99 }
47 100
101 + /**
102 + * Get nonce.
103 + *
104 + * @return string|null
105 + */
106 + public static function enable_loader() {
107 + return 'on' !== self::get_option( 'general', 'optimization', 'remove_pre_loader', '' );
108 + }
109 + /**
110 + * Get nonce.
111 + *
112 + * @return string|null
113 + */
114 + public static function content_invisible() {
115 + $wp_rocket_compatibility = 'on' !== self::get_option( 'general', 'optimization', 'wp_rocket_compatibility', '' );
116 + if ( $wp_rocket_compatibility ) {
117 + return true;
118 + }
119 + if ( defined( 'WP_ROCKET_VERSION' ) ) {
120 + return false;
121 + }
122 + return true;
123 + }
124 + /**
125 + * Get nonce.
126 + *
127 + * @return string|null
128 + */
48 129 public static function get_nonce() {
49 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended
130 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
50 131 return isset( $_REQUEST[ rtsb()->nonceId ] ) ? sanitize_text_field( $_REQUEST[ rtsb()->nonceId ] ) : null;
51 132 }
52 133
53 -
54 134 /**
55 - * Set Cookie or Session
135 + * Set Session
56 136 *
57 - * @param $name
58 - * @param $value
137 + * @param string $name Name.
138 + * @param mixed $value Value.
59 139 */
60 140 public static function setSession( $name, $value ) {
61 141 if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
62 142 session_start();
63 143 $_SESSION[ $name ] = $value;
64 - // session_write_close();
65 144 } else {
66 145 $_SESSION[ $name ] = $value;
67 146 }
68 147 }
148 +
69 149 /**
70 150 * Get Cookie or Session
71 151 *
72 - * @param $name
152 + * @param string $name Name.
73 153 *
74 154 * @return bool
75 155 */
76 156 public static function getSession( $name ) {
@@ -76,9 +156,9 @@
76 156 public static function getSession( $name ) {
77 157 if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
78 158 session_start();
79 159 }
80 - return $_SESSION[ $name ] ?? null;
160 + return $_SESSION[ $name ] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
81 161 }
82 162 /**
83 163 * Remove Session Variable
84 164 *
@@ -87,17 +167,47 @@
87 167 public static function removeSession( $name ) {
88 168 if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
89 169 session_start();
90 170 unset( $_SESSION[ $name ] );
91 - // session_write_close();
92 171 } else {
93 172 unset( $_SESSION[ $name ] );
94 173 }
95 174 }
96 -
97 175 /**
98 - * @param $plugin_slug
176 + * Gets the current timestamp in WordPress timezone.
99 177 *
178 + * @return int Current timestamp.
179 + */
180 + public static function currentTimestampUTC() {
181 + $wp_timezone = wp_timezone();
182 + $now = new DateTime( 'now', $wp_timezone ); // Current time in WP timezone.
183 + return $now->getTimestamp();
184 + }
185 + /**
186 + * Action.
187 + *
188 + * @param string $action action.
189 + * @return void
190 + */
191 + public static function add_to_scheduled_hook_list( $action ) {
192 + if ( empty( $action ) ) {
193 + return;
194 + }
195 + $schedule = get_option( 'rtsb_cron_schedule_free', [] );
196 + $schedule[] = $action;
197 + update_option( 'rtsb_cron_schedule_free', array_unique( $schedule ) );
198 + }
199 + /**
200 + * @return DB
201 + */
202 + public static function DB() {
203 + return new DB( 'wpdb' );
204 + }
205 + /**
206 + * Check if a plugin is installed.
207 + *
208 + * @param string $plugin_slug Plugin slug.
209 + *
100 210 * @return bool
101 211 */
102 212 public static function check_plugin_installed( $plugin_slug ): bool {
103 213 $installed_plugins = get_plugins();
@@ -105,10 +215,12 @@
105 215 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
106 216 }
107 217
108 218 /**
109 - * @param $plugin_slug
219 + * Check if a plugin is active.
110 220 *
221 + * @param string $plugin_slug Plugin slug.
222 + *
111 223 * @return bool
112 224 */
113 225 public static function check_plugin_active( $plugin_slug ): bool {
114 226 if ( is_plugin_active( $plugin_slug ) ) {
@@ -116,21 +228,71 @@
116 228 }
117 229
118 230 return false;
119 231 }
232 + /**
233 + * Get all user roles.
234 + *
235 + * @return array
236 + */
237 + public static function get_current_user_roles() {
238 + $current_user = wp_get_current_user();
239 + return $current_user->roles;
240 + }
241 + /**
242 + * Get all user roles.
243 + *
244 + * @return array|false|mixed
245 + */
246 + public static function get_all_user_roles() {
247 + $cache_key = 'rtsb_get_user_roles';
248 + $roles = wp_cache_get( $cache_key, 'shopbuilder' );
120 249
250 + if ( empty( $roles ) ) {
251 + if ( ! function_exists( 'get_editable_roles' ) ) {
252 + require_once ABSPATH . 'wp-admin/includes/user.php';
253 + }
254 +
255 + $user_roles = \get_editable_roles();
256 + $roles = [];
257 +
258 + foreach ( $user_roles as $key => $role ) {
259 + if ( empty( $role['capabilities'] ) ) {
260 + continue;
261 + }
262 + $roles[ $key ] = $role['name'];
263 + }
264 + }
265 + wp_cache_set( $cache_key, $roles, 'shopbuilder' );
266 + Cache::set_data_cache_key( $cache_key );
267 + return $roles;
268 + }
269 +
121 270 /**
122 - * @param $data
271 + * Stripslashes value.
123 272 *
273 + * @param mixed $data Data.
274 + *
124 275 * @return mixed|string
125 276 */
126 277 public static function stripslashes_value( $data ) {
127 - if ( is_string( $data ) && strpos( $data, '\\' ) !== false ) {
128 - return stripslashes( $data );
129 - } elseif ( is_array( $data ) ) {
278 + if ( is_array( $data ) ) {
130 279 foreach ( $data as $key => $value ) {
131 280 $data[ $key ] = self::stripslashes_value( $value );
132 281 }
282 + } elseif ( is_string( $data ) ) {
283 + $trimmed = trim( $data );
284 + // Try to decode JSON.
285 + $json = json_decode( $trimmed, true );
286 + if ( json_last_error() === JSON_ERROR_NONE && is_array( $json ) ) {
287 + // Recursively stripslashes on decoded array.
288 + $json = self::stripslashes_value( $json );
289 + return wp_json_encode( $json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
290 + }
291 + // Not valid JSON, just stripslashes if needed.
292 + if ( strpos( $data, '\\' ) !== false ) {
293 + return stripslashes( $data );
294 + }
133 295 }
134 296
135 297 return $data;
136 298 }
@@ -135,10 +297,12 @@
135 297 return $data;
136 298 }
137 299
138 300 /**
139 - * @param $string
301 + * Multiselect settings field value
140 302 *
303 + * @param string $string String.
304 + *
141 305 * @return array
142 306 */
143 307 public static function multiselect_settings_field_value( $string ) {
144 308
@@ -158,8 +322,10 @@
158 322 return $values;
159 323 }
160 324
161 325 /**
326 + * Check if is block theme
327 + *
162 328 * @return bool
163 329 */
164 330 public static function check_is_block_theme(): bool {
165 331 global $wp_version;
@@ -167,8 +333,10 @@
167 333 return version_compare( $wp_version, '5.9', '>=' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
168 334 }
169 335
170 336 /**
337 + * Locate template
338 + *
171 339 * @param string $template_name Template name.
172 340 * @param string $template_path Template path. (default: '').
173 341 * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin.
174 342 *
@@ -199,12 +367,12 @@
199 367 )
200 368 );
201 369
202 370 if ( ! $located ) {
203 - if ( file_exists( $plugin_path ) ) {
371 + if ( $pro_plugin_path && rtsb()->has_pro() && file_exists( $pro_plugin_path ) ) {
372 + return apply_filters( 'rtsb/core/locate_template', $pro_plugin_path, $template_name );
373 + } elseif ( file_exists( $plugin_path ) ) {
204 374 return apply_filters( 'rtsb/core/locate_template', $plugin_path, $template_name );
205 - } elseif ( $pro_plugin_path && rtsb()->has_pro() && file_exists( $pro_plugin_path ) ) {
206 - return apply_filters( 'rtsb/core/locate_template', $pro_plugin_path, $template_name );
207 375 }
208 376 }
209 377
210 378 /**
@@ -241,11 +409,20 @@
241 409 * @param string $plugin_path Fallback path from where file will load if fail to load from template. (default: '').
242 410 *
243 411 * @return false|string|void
244 412 */
245 - public static function load_template( $template_name, array $args = null, $return = false, $template_path = '', $plugin_path = '' ) {
246 - $located = self::locate_template( $template_name, $template_path, $plugin_path );
247 -
413 + public static function load_template( $template_name, $args = null, $return = false, $template_path = '', $plugin_path = '' ) {
414 + $cache_key = sanitize_key( implode( '-', [ 'template', $template_name, $template_path ] ) );
415 + $located = (string) wp_cache_get( $cache_key, 'shopbuilder' );
416 + if ( ! $located ) {
417 + $located = self::locate_template( $template_name, $template_path, $plugin_path );
418 + // Don't cache the absolute path so that it can be shared between web servers with different paths.
419 + $cache_path = wc_tokenize_path( $located, wc_get_path_define_tokens() );
420 + Cache::set_template_cache( $cache_key, $cache_path );
421 + } else {
422 + // Make sure that the absolute path to the template is resolved.
423 + $located = wc_untokenize_path( $located, wc_get_path_define_tokens() );
424 + }
248 425 if ( ! file_exists( $located ) ) {
249 426 // translators: %s template.
250 427 self::doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'shopbuilder' ), '<code>' . $located . '</code>' ), '1.0' );
251 428
@@ -253,9 +430,9 @@
253 430 }
254 431
255 432 if ( ! empty( $args ) && is_array( $args ) ) {
256 433 $atts = $args;
257 - extract( $args ); // @codingStandardsIgnoreLine
434 + extract( $args ); // @codingStandardsIgnoreLine
258 435 }
259 436
260 437 // Allow 3rd party plugin filter template file from their plugin.
261 438 $located = apply_filters( 'rtsb/core/get_template', $located, $template_name, $args );
@@ -370,9 +547,9 @@
370 547 // Return the Builder Template id.
371 548
372 549 if ( get_post_type( get_the_ID() ) == BuilderFns::$post_type_tb ) {
373 550 $product_id = get_post_meta( get_the_ID(), BuilderFns::$product_template_meta, true );
374 - if ( $product_id ) {
551 + if ( $product_id && 'product' === get_post_type( $product_id ) && get_post_status( $product_id ) ) {
375 552 return $product_id;
376 553 }
377 554 }
378 555
@@ -377,16 +554,16 @@
377 554 }
378 555
379 556 global $wpdb;
380 557 $cache_key = 'rtsb_prepared_product_id';
381 - $_post_id = wp_cache_get( $cache_key );
382 -
558 + $_post_id = wp_cache_get( $cache_key, 'shopbuilder' );
383 559 if ( false === $_post_id || 'publish' !== get_post_status( $_post_id ) ) {
384 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
560 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
385 561 $_post_id = $wpdb->get_var(
386 562 $wpdb->prepare( "SELECT MAX(ID) FROM {$wpdb->prefix}posts WHERE post_type = %s AND post_status IN('publish', 'draft')", 'product' )
387 563 );
388 - wp_cache_set( $cache_key, $_post_id, '', 12 * HOUR_IN_SECONDS );
564 + wp_cache_set( $cache_key, $_post_id, 'shopbuilder', 12 * HOUR_IN_SECONDS );
565 + Cache::set_data_cache_key( $cache_key );
389 566 }
390 567
391 568 return $_post_id;
392 569 }
@@ -399,9 +576,8 @@
399 576 public static function get_product() {
400 577 global $product;
401 578
402 579 if ( is_singular( 'product' ) && $product instanceof WC_Product ) {
403 - // do_action( 'rtsb_before_product_template_render' );.
404 580 return $product;
405 581 }
406 582 $cache_key = 'prepared_product_for_preview';
407 583 if ( isset( self::$cache[ $cache_key ] ) ) {
@@ -422,8 +598,9 @@
422 598 *
423 599 * @return void
424 600 */
425 601 public static function doing_it_wrong( $function, $message, $version ) {
602 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
426 603 $message .= ' Backtrace: ' . wp_debug_backtrace_summary();
427 604 _doing_it_wrong( esc_html( $function ), wp_kses_post( $message ), esc_html( $version ) );
428 605 }
429 606
@@ -441,8 +618,15 @@
441 618
442 619 return $pages;
443 620 }
444 621
622 + /**
623 + * Get section items
624 + *
625 + * @param string $section_id Section ID.
626 + *
627 + * @return array
628 + */
445 629 public static function get_section_items( $section_id ) {
446 630 if ( ! $section_id ) {
447 631 return [];
448 632 }
@@ -449,8 +633,16 @@
449 633
450 634 return DataModel::source()->get_option( $section_id, [] );
451 635 }
452 636
637 + /**
638 + * Get options items
639 + *
640 + * @param string $section_id Section ID.
641 + * @param string $item_id Item ID.
642 + *
643 + * @return array
644 + */
453 645 public static function get_options( $section_id, $item_id ) {
454 646 if ( ! $section_id || ! $item_id ) {
455 647 return [];
456 648 }
@@ -461,11 +653,11 @@
461 653
462 654 /**
463 655 * Get options value with default value if options doesn't exist.
464 656 *
465 - * @param $group
466 - * @param $option_key
467 - * @param $default_value
657 + * @param array $group Group.
658 + * @param string $option_key Option key.
659 + * @param string $default_value Default value.
468 660 *
469 661 * @return mixed|string
470 662 */
471 663 public static function get_options_by_default_val( $group, $option_key, $default_value = '' ) {
@@ -477,32 +669,34 @@
477 669 return $group[ $option_key ];
478 670 }
479 671
480 672 /**
481 - * @param string $section_id
482 - * @param string $item_id
483 - * @param string $option_id
484 - * @param null $default EXCEPT multi_checkbox you can provide default value if given option does not set any value
485 - * @param null $type checkbox, multi_checkbox, number
673 + * Get option.
486 674 *
675 + * @param string $section_id Section ID.
676 + * @param string $item_id Item ID.
677 + * @param string $option_id Option ID.
678 + * @param null $default EXCEPT multi_checkbox you can provide default value if given option does not set any value.
679 + * @param null $type checkbox, multi_checkbox, number.
680 + *
487 681 * @return bool|int|mixed|null
488 682 */
489 683 public static function get_option( $section_id, $item_id, $option_id, $default = null, $type = null ) {
490 684 $options = self::get_options( $section_id, $item_id );
491 685
492 - if ( $type === 'checkbox' ) {
686 + if ( 'checkbox' === $type ) {
493 687 if ( isset( $options[ $option_id ] ) ) {
494 - return $options[ $option_id ] === 'on';
688 + return 'on' === $options[ $option_id ];
495 689 }
496 690
497 691 return $default;
498 - } elseif ( $type === 'multi_checkbox' ) {
499 - return isset( $options[ $option_id ] ) && is_array( $options[ $option_id ] ) && in_array( $default, $options[ $option_id ] );
500 - } elseif ( $type === 'number' ) {
692 + } elseif ( 'multi_checkbox' === $type ) {
693 + return isset( $options[ $option_id ] ) && is_array( $options[ $option_id ] ) && in_array( $default, $options[ $option_id ] ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
694 + } elseif ( 'number' === $type ) {
501 695 return isset( $options[ $option_id ] ) ? absint( $options[ $option_id ] ) : absint( $default );
502 696 }
503 697
504 - return isset( $options[ $option_id ] ) && ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
698 + return ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
505 699 }
506 700
507 701
508 702 /**
@@ -557,9 +751,9 @@
557 751
558 752 if ( strlen( $page_content ) > 0 ) {
559 753 // Search for an existing page with the specified page content (typically a shortcode).
560 754 $shortcode = str_replace( [ '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ], '', $page_content );
561 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
755 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
562 756 $valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_content LIKE %s LIMIT 1;", "%{$shortcode}%" ) );
563 757 } else {
564 758 // Search for an existing page with the specified page slug.
565 759 $valid_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status NOT IN ( 'pending', 'trash', 'future', 'auto-draft' ) AND post_name = %s LIMIT 1;", $slug ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
@@ -629,8 +823,13 @@
629 823
630 824 return $page_id;
631 825 }
632 826
827 + /**
828 + * Get allowed HTML tags.
829 + *
830 + * @return array
831 + */
633 832 public static function get_kses_array() {
634 833 return [
635 834 'a' => [
636 835 'class' => [],
@@ -729,13 +928,14 @@
729 928 ];
730 929 }
731 930
732 931
733 - /*
932 + /**
734 933 * Escape output of wishlist icon
735 934 *
736 935 * @param string $data Data to escape.
737 - * @return string Escaped data
936 + *
937 + * @return void
738 938 */
739 939 public static function print_icon( $data ) {
740 940 /**
741 941 * APPLY_FILTERS: rtsb/core/allowed_icon_html
@@ -819,9 +1019,9 @@
819 1019 * @return mixed
820 1020 */
821 1021 public static function allowedHtml( $level = 'basic' ) {
822 1022 $allowed_html = [];
823 - // TODO:: Need Optimize.
1023 +
824 1024 switch ( $level ) {
825 1025 case 'basic':
826 1026 $allowed_html = [
827 1027 'b' => [
@@ -961,13 +1161,15 @@
961 1161 return $allowed_html;
962 1162 }
963 1163
964 1164 /**
965 - * Safe print a validated HTML tag.
1165 + * Safe get a validated HTML tag.
966 1166 *
967 - * @param string $tag
1167 + * @param string $tag HTML tag.
1168 + *
1169 + * @return string
968 1170 */
969 - public static function print_validated_html_tag( $tag ) {
1171 + public static function get_validated_html_tag( $tag ) {
970 1172 $allowed_html_wrapper_tags = [
971 1173 'a',
972 1174 'article',
973 1175 'aside',
@@ -987,12 +1189,23 @@
987 1189 'section',
988 1190 'span',
989 1191 ];
990 1192
991 - self::print_html( in_array( strtolower( $tag ), $allowed_html_wrapper_tags, true ) ? $tag : 'div' );
1193 + return in_array( strtolower( $tag ), $allowed_html_wrapper_tags, true ) ? $tag : 'div';
992 1194 }
993 1195
994 1196 /**
1197 + * Safe print a validated HTML tag.
1198 + *
1199 + * @param string $tag HTML tag.
1200 + *
1201 + * @return void
1202 + */
1203 + public static function print_validated_html_tag( $tag ) {
1204 + self::print_html( self::get_validated_html_tag( $tag ) );
1205 + }
1206 +
1207 + /**
995 1208 * Insert Some array element
996 1209 *
997 1210 * @param [type] $key The elements will insert nearby this key.
998 1211 * @param [type] $main_array Original array.
@@ -1043,9 +1256,9 @@
1043 1256
1044 1257 $imgSize = [];
1045 1258
1046 1259 foreach ( $sizes as $key => $img ) {
1047 - $imgSize[ $key ] = ucfirst( $key ) . " ({$img['width']}*{$img['height']})";
1260 + $imgSize[ $key ] = esc_html( $key ) . " ({$img['width']}*{$img['height']})";
1048 1261 }
1049 1262
1050 1263 $imgSize['full'] = esc_html__( 'Full size', 'shopbuilder' );
1051 1264 $imgSize['rtsb_custom'] = esc_html__( 'Custom image size', 'shopbuilder' );
@@ -1055,11 +1268,17 @@
1055 1268
1056 1269 /**
1057 1270 * Free Layouts
1058 1271 *
1272 + * @param string $layout Layout.
1273 + *
1059 1274 * @return array
1060 1275 */
1061 - public static function free_layouts() {
1276 + public static function free_layouts( $layout = '' ) {
1277 + if ( ! empty( $layout ) && false !== strpos( $layout, 'rtsb-' ) ) {
1278 + return [ $layout => esc_html__( 'Addon Layout', 'shopbuilder' ) ];
1279 + }
1280 +
1062 1281 $layouts = [
1063 1282 'grid-layout1' => esc_html__( 'Grid Layout 1', 'shopbuilder' ),
1064 1283 'grid-layout2' => esc_html__( 'Grid Layout 2', 'shopbuilder' ),
1065 1284 'list-layout1' => esc_html__( 'List Layout 1', 'shopbuilder' ),
@@ -1113,8 +1332,25 @@
1113 1332 }
1114 1333 self::$cache[ $cache_key ] = $terms;
1115 1334 return $terms;
1116 1335 }
1336 + /**
1337 + * Get all product attribute taxonomy by id
1338 + *
1339 + * @param array $term_ids Term id.
1340 + *
1341 + * @return array
1342 + */
1343 + public static function tax_filter_attr_taxonomy( $term_ids ) {
1344 + $taxonomies = [];
1345 + foreach ( $term_ids as $id ) {
1346 + $term = get_term( $id );
1347 + if ( ! is_wp_error( $term ) && $term ) {
1348 + $taxonomies[] = $term->taxonomy;
1349 + }
1350 + }
1351 + return array_unique( $taxonomies );
1352 + }
1117 1353
1118 1354 /**
1119 1355 * Get all terms by attributes
1120 1356 *
@@ -1226,9 +1462,8 @@
1226 1462 if ( ! is_admin() ) {
1227 1463 return [];
1228 1464 }
1229 1465
1230 - // TODO:: Return Slug always true for all settings.
1231 1466 $term_list = [];
1232 1467 $args = [
1233 1468 'taxonomy' => $taxonomy,
1234 1469 'hide_empty' => false,
@@ -1298,9 +1533,9 @@
1298 1533
1299 1534 if ( $args['show_rating_count'] ) {
1300 1535 $count .= '<div class="rtsb-count">';
1301 1536 $count .= sprintf(
1302 - /* translators: %s is the number of reviews */
1537 + /* translators: %s is the number of reviews */
1303 1538 _n( '%s Review', '%s Reviews', $rating_count, 'shopbuilder' ),
1304 1539 $rating_count
1305 1540 );
1306 1541 $count .= '</div>';
@@ -1325,8 +1560,13 @@
1325 1560
1326 1561 self::print_html( $html, true );
1327 1562 }
1328 1563
1564 + /**
1565 + * Get product simple rating.
1566 + *
1567 + * @return void
1568 + */
1329 1569 public static function get_product_simple_rating_html() {
1330 1570 global $product;
1331 1571 $html = null;
1332 1572 $rating_count = $product->get_rating_count();
@@ -1438,9 +1678,42 @@
1438 1678
1439 1679 <?php
1440 1680 self::print_html( ob_get_clean() );
1441 1681 }
1682 + /**
1683 + * Brands HTML
1684 + *
1685 + * @param int $id Product ID.
1686 + * @param string $class Custom class.
1687 + *
1688 + * @return void|string
1689 + */
1690 + public static function get_brands_list( $id, $class = 'rtsb-brand-outline' ) {
1691 + $terms = get_the_terms( $id, 'product_brand' );
1692 + if ( empty( $id ) || empty( $terms ) || is_wp_error( $terms ) ) {
1693 + return '';
1694 + }
1695 + ob_start();
1696 + ?>
1442 1697
1698 + <ul class="rtsb-brand-list <?php echo esc_attr( $class ); ?>">
1699 + <?php
1700 + $brand_list = get_the_term_list(
1701 + $id,
1702 + 'product_brand',
1703 + '<li class="rtsb-brand-list-item">',
1704 + '</li><li class="rtsb-brand-list-item">',
1705 + '</li>'
1706 + );
1707 +
1708 + self::print_html( $brand_list );
1709 + ?>
1710 + </ul>
1711 +
1712 + <?php
1713 + self::print_html( ob_get_clean() );
1714 + }
1715 +
1443 1716 /**
1444 1717 * Get Featured Image HTML.
1445 1718 *
1446 1719 * @param string $type Image type.
@@ -1543,9 +1816,9 @@
1543 1816
1544 1817 if ( ! $img_html ) {
1545 1818 $hwstring = image_hwstring( 160, 160 );
1546 1819 $attr = isset( $attr['src'] ) ? apply_filters( 'wp_get_attachment_image_attributes', $attr, false, $f_img_size ) : [];
1547 - $attr['class'] = 'default-img';
1820 + $attr['class'] = 'default-img ' . $hover_class;
1548 1821 $attr['src'] = esc_url( rtsb()->get_assets_uri( 'images/demo.png' ) );
1549 1822 $attr['alt'] = esc_html__( 'Default Image', 'shopbuilder' );
1550 1823 $img_html = rtrim( "<img $hwstring" );
1551 1824
@@ -1636,9 +1909,9 @@
1636 1909 if ( empty( $control['value'] ) ) {
1637 1910 return '';
1638 1911 }
1639 1912 if ( is_array( $control['value'] ) ) {
1640 - $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) );
1913 + $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1641 1914 } else {
1642 1915 $cache_key = 'icons_managet_' . str_replace( ' ', '', $control['value'] );
1643 1916 }
1644 1917 if ( isset( self::$cache[ $cache_key ] ) ) {
@@ -1674,9 +1947,13 @@
1674 1947 public static function get_product_swatches( $type ) {
1675 1948 ?>
1676 1949 <div class="rtsb-swatches <?php echo esc_attr( $type ); ?>-layout">
1677 1950 <?php
1678 - do_action( 'rtwpvs_show_archive_variation' );
1951 + if ( class_exists( 'Rtwpvsp' ) ) {
1952 + do_action( 'rtwpvs_show_archive_variation' );
1953 + } else {
1954 + do_action( 'rtsb/vs/showcase/variation' );
1955 + }
1679 1956 ?>
1680 1957 </div>
1681 1958 <?php
1682 1959 }
@@ -1773,47 +2050,35 @@
1773 2050 public static function calculate_sale_percentage( $product = null ) {
1774 2051 if ( is_null( $product ) ) {
1775 2052 global $product;
1776 2053 }
1777 -
1778 2054 if ( ! $product instanceof WC_Product ) {
1779 2055 return '';
1780 2056 }
1781 -
1782 2057 if ( ! $product->is_on_sale() ) {
1783 2058 return '';
1784 2059 }
1785 -
1786 - $percentage = null;
1787 - $max_percentage = '';
1788 -
2060 + $max_percentage = 0;
1789 2061 if ( $product->is_type( 'simple' ) ) {
1790 - $max_percentage = ( ( $product->get_regular_price() - $product->get_sale_price() ) / $product->get_regular_price() ) * 100;
2062 + $regular_price = (float) $product->get_regular_price();
2063 + $sale_price = (float) $product->get_sale_price();
2064 + if ( $regular_price > 0 && $sale_price > 0 ) {
2065 + $max_percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2066 + }
1791 2067 } elseif ( $product->is_type( 'variable' ) ) {
1792 - $max_percentage = 0;
2068 + $prices = $product->get_variation_prices( true );
2069 + if ( ! empty( $prices['regular_price'] ) && ! empty( $prices['sale_price'] ) ) {
2070 + foreach ( $prices['regular_price'] as $key => $regular_price ) {
2071 + $sale_price = $prices['sale_price'][ $key ];
1793 2072
1794 - foreach ( $product->get_children() as $child_id ) {
1795 -
1796 - $variation = wc_get_product( $child_id );
1797 -
1798 - $price = $variation->get_regular_price();
1799 - $sale = $variation->get_sale_price();
1800 -
1801 - if ( 0 !== $price && ! empty( $sale ) ) {
1802 - $percentage = ( $price - $sale ) / $price * 100;
2073 + if ( $regular_price > 0 && $sale_price > 0 && $regular_price > $sale_price ) {
2074 + $percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2075 + $max_percentage = max( $max_percentage, $percentage );
2076 + }
1803 2077 }
1804 -
1805 - if ( $percentage > $max_percentage ) {
1806 - $max_percentage = $percentage;
1807 - }
1808 2078 }
1809 2079 }
1810 -
1811 - if ( $max_percentage > 0 ) {
1812 - return round( $max_percentage );
1813 - }
1814 -
1815 - return 0;
2080 + return $max_percentage > 0 ? round( $max_percentage ) : 0;
1816 2081 }
1817 2082
1818 2083 /**
1819 2084 * Pagination
@@ -1830,15 +2095,15 @@
1830 2095
1831 2096 global $paged;
1832 2097
1833 2098 if ( is_front_page() ) {
1834 - $paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1;
2099 + $paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1835 2100 } else {
1836 - $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
2101 + $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1837 2102 }
1838 2103
1839 2104 if ( empty( $paged ) ) {
1840 - $paged = 1;
2105 + $paged = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1841 2106 }
1842 2107
1843 2108 if ( '' === $pages ) {
1844 2109 global $wp_query;
@@ -1996,10 +2261,8 @@
1996 2261 } else {
1997 2262 $class .= ' rtsb-' . esc_attr( str_replace( '_', '-', $type ) );
1998 2263 }
1999 2264
2000 - $html .= '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">';
2001 -
2002 2265 if ( ( 'add_to_cart' === $type ) && ( ! empty( $cart_html ) ) ) {
2003 2266 $html .= $cart_html;
2004 2267 } else {
2005 2268 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
@@ -2004,9 +2267,11 @@
2004 2267 } else {
2005 2268 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
2006 2269 }
2007 2270
2008 - $html .= '</' . esc_attr( $wrapper ) . '>';
2271 + if ( ! empty( $html ) ) {
2272 + $html = '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">' . $html . '</' . esc_attr( $wrapper ) . '>';
2273 + }
2009 2274
2010 2275 return apply_filters( 'rtsb/elements/elementor/get_action_button_by_type', $html );
2011 2276 }
2012 2277
@@ -2107,9 +2372,9 @@
2107 2372 $link['social_network'] = 'Facebook';
2108 2373 $link['social_action'] = 'Share';
2109 2374 break;
2110 2375 case 'twitter':
2111 - $link['link'] = esc_url( 'https://twitter.com/intent/tweet?text=' . htmlspecialchars( rawurlencode( html_entity_decode( $link['title'], ENT_COMPAT, 'UTF-8' ) ), ENT_COMPAT, 'UTF-8' ) . '&url=' . $link['url'] );
2376 + $link['link'] = esc_url( 'https://x.com/intent/tweet?text=' . htmlspecialchars( rawurlencode( html_entity_decode( $link['title'], ENT_COMPAT, 'UTF-8' ) ), ENT_COMPAT, 'UTF-8' ) . '&url=' . $link['url'] );
2112 2377 $link['icon'] = '<svg width="24" height="24" viewBox="0 0 1200 1227" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" /></svg>';
2113 2378 $link['attr_title'] = esc_html__( 'Share on Twitter', 'shopbuilder' );
2114 2379 $link['social_network'] = 'Twitter';
2115 2380 $link['social_action'] = 'Tweet';
@@ -2143,9 +2408,9 @@
2143 2408 $link['social_action'] = 'Share';
2144 2409 break;
2145 2410 case 'reddit':
2146 2411 $link['link'] = esc_url( 'https://reddit.com/submit?url=' . $link['url'] . '&title=' . $link['title'] );
2147 - $link['icon'] = '<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title>ionicons-v5_logos</title><path d="M324,256a36,36,0,1,0,36,36A36,36,0,0,0,324,256Z"/><circle cx="188" cy="292" r="36" transform="translate(-97.43 94.17) rotate(-22.5)"/><path d="M496,253.77c0-31.19-25.14-56.56-56-56.56a55.72,55.72,0,0,0-35.61,12.86c-35-23.77-80.78-38.32-129.65-41.27l22-79L363.15,103c1.9,26.48,24,47.49,50.65,47.49,28,0,50.78-23,50.78-51.21S441,48,413,48c-19.53,0-36.31,11.19-44.85,28.77l-90-17.89L247.05,168.4l-4.63.13c-50.63,2.21-98.34,16.93-134.77,41.53A55.38,55.38,0,0,0,72,197.21c-30.89,0-56,25.37-56,56.56a56.43,56.43,0,0,0,28.11,49.06,98.65,98.65,0,0,0-.89,13.34c.11,39.74,22.49,77,63,105C146.36,448.77,199.51,464,256,464s109.76-15.23,149.83-42.89c40.53-28,62.85-65.27,62.85-105.06a109.32,109.32,0,0,0-.84-13.3A56.32,56.32,0,0,0,496,253.77ZM414,75a24,24,0,1,1-24,24A24,24,0,0,1,414,75ZM42.72,253.77a29.6,29.6,0,0,1,29.42-29.71,29,29,0,0,1,13.62,3.43c-15.5,14.41-26.93,30.41-34.07,47.68A30.23,30.23,0,0,1,42.72,253.77ZM390.82,399c-35.74,24.59-83.6,38.14-134.77,38.14S157,423.61,121.29,399c-33-22.79-51.24-52.26-51.24-83A78.5,78.5,0,0,1,75,288.72c5.68-15.74,16.16-30.48,31.15-43.79a155.17,155.17,0,0,1,14.76-11.53l.3-.21,0,0,.24-.17c35.72-24.52,83.52-38,134.61-38s98.9,13.51,134.62,38l.23.17.34.25A156.57,156.57,0,0,1,406,244.92c15,13.32,25.48,28.05,31.16,43.81a85.44,85.44,0,0,1,4.31,17.67,77.29,77.29,0,0,1,.6,9.65C442.06,346.77,423.86,376.24,390.82,399Zm69.6-123.92c-7.13-17.28-18.56-33.29-34.07-47.72A29.09,29.09,0,0,1,440,224a29.59,29.59,0,0,1,29.41,29.71A30.07,30.07,0,0,1,460.42,275.1Z"/><path d="M323.23,362.22c-.25.25-25.56,26.07-67.15,26.27-42-.2-66.28-25.23-67.31-26.27h0a4.14,4.14,0,0,0-5.83,0l-13.7,13.47a4.15,4.15,0,0,0,0,5.89h0c3.4,3.4,34.7,34.23,86.78,34.45,51.94-.22,83.38-31.05,86.78-34.45h0a4.16,4.16,0,0,0,0-5.9l-13.71-13.47a4.13,4.13,0,0,0-5.81,0Z"/></svg>';
2412 + $link['icon'] = '<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><path d="M324,256a36,36,0,1,0,36,36A36,36,0,0,0,324,256Z"/><circle cx="188" cy="292" r="36" transform="translate(-97.43 94.17) rotate(-22.5)"/><path d="M496,253.77c0-31.19-25.14-56.56-56-56.56a55.72,55.72,0,0,0-35.61,12.86c-35-23.77-80.78-38.32-129.65-41.27l22-79L363.15,103c1.9,26.48,24,47.49,50.65,47.49,28,0,50.78-23,50.78-51.21S441,48,413,48c-19.53,0-36.31,11.19-44.85,28.77l-90-17.89L247.05,168.4l-4.63.13c-50.63,2.21-98.34,16.93-134.77,41.53A55.38,55.38,0,0,0,72,197.21c-30.89,0-56,25.37-56,56.56a56.43,56.43,0,0,0,28.11,49.06,98.65,98.65,0,0,0-.89,13.34c.11,39.74,22.49,77,63,105C146.36,448.77,199.51,464,256,464s109.76-15.23,149.83-42.89c40.53-28,62.85-65.27,62.85-105.06a109.32,109.32,0,0,0-.84-13.3A56.32,56.32,0,0,0,496,253.77ZM414,75a24,24,0,1,1-24,24A24,24,0,0,1,414,75ZM42.72,253.77a29.6,29.6,0,0,1,29.42-29.71,29,29,0,0,1,13.62,3.43c-15.5,14.41-26.93,30.41-34.07,47.68A30.23,30.23,0,0,1,42.72,253.77ZM390.82,399c-35.74,24.59-83.6,38.14-134.77,38.14S157,423.61,121.29,399c-33-22.79-51.24-52.26-51.24-83A78.5,78.5,0,0,1,75,288.72c5.68-15.74,16.16-30.48,31.15-43.79a155.17,155.17,0,0,1,14.76-11.53l.3-.21,0,0,.24-.17c35.72-24.52,83.52-38,134.61-38s98.9,13.51,134.62,38l.23.17.34.25A156.57,156.57,0,0,1,406,244.92c15,13.32,25.48,28.05,31.16,43.81a85.44,85.44,0,0,1,4.31,17.67,77.29,77.29,0,0,1,.6,9.65C442.06,346.77,423.86,376.24,390.82,399Zm69.6-123.92c-7.13-17.28-18.56-33.29-34.07-47.72A29.09,29.09,0,0,1,440,224a29.59,29.59,0,0,1,29.41,29.71A30.07,30.07,0,0,1,460.42,275.1Z"/><path d="M323.23,362.22c-.25.25-25.56,26.07-67.15,26.27-42-.2-66.28-25.23-67.31-26.27h0a4.14,4.14,0,0,0-5.83,0l-13.7,13.47a4.15,4.15,0,0,0,0,5.89h0c3.4,3.4,34.7,34.23,86.78,34.45,51.94-.22,83.38-31.05,86.78-34.45h0a4.16,4.16,0,0,0,0-5.9l-13.71-13.47a4.13,4.13,0,0,0-5.81,0Z"/></svg>';
2148 2413 $link['attr_title'] = esc_html__( 'Share on Reddit', 'shopbuilder' );
2149 2414 $link['social_network'] = 'Reddit';
2150 2415 $link['social_action'] = 'Share';
2151 2416 break;
@@ -2233,27 +2498,61 @@
2233 2498
2234 2499 /**
2235 2500 * Social Share Platforms.
2236 2501 *
2237 - * @return mixed|null
2502 + * @param string $module Module.
2503 + *
2504 + * @return true|void
2238 2505 */
2239 2506 public static function is_module_active( $module ) {
2240 - $modulelist = ModuleList::instance()->get_data();
2507 + $modulelist = self::get_modules_list();
2508 +
2241 2509 if ( ! empty( $modulelist[ $module ]['active'] ) ) {
2242 2510 return true;
2243 2511 }
2244 2512 }
2513 + /**
2514 + * Checks if catalog pro is active
2515 + *
2516 + * @return boolean
2517 + */
2518 + public static function is_catalog_mode() {
2519 + return function_exists( 'rtsbpro' ) && self::is_module_active( 'catalog_mode' );
2520 + }
2521 + /**
2522 + * Checks if back in stock notifier is active
2523 + *
2524 + * @return boolean
2525 + */
2526 + public static function is_back_in_stock_notifier_enable() {
2527 + return function_exists( 'rtsbpro' ) && self::is_module_active( 'back_in_stock_notifier' );
2528 + }
2245 2529
2530 + /**
2531 + * Elementor Widget Active.
2532 + *
2533 + * @param string $widget Elementor Widget.
2534 + *
2535 + * @return true|void
2536 + */
2537 + public static function is_elementor_widget_active( $widget ) {
2538 + $element_list = self::get_widgets_list();
2539 +
2540 + if ( ! empty( $element_list[ $widget ]['active'] ) ) {
2541 + return true;
2542 + }
2543 + }
2544 +
2246 2545 /***
2247 2546 * Save Settings data
2248 2547 *
2249 - * @param $section_id
2250 - * @param $block_id
2251 - * @param $rawOptions
2548 + * @param string $section_id Section ID.
2549 + * @param string $block_id Block ID.
2550 + * @param array $rawOptions Raw Options.
2252 2551 *
2253 2552 * @return array
2254 2553 */
2255 - public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) {
2554 + public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) { // phpcs:ignore Generic.Metrics.NestingLevel.TooHigh
2256 2555 $section_id = ! empty( $section_id ) ? sanitize_text_field( wp_unslash( $section_id ) ) : '';
2257 2556 $block_id = ! empty( $block_id ) ? sanitize_text_field( wp_unslash( $block_id ) ) : '';
2258 2557 $rawOptions = ! empty( $rawOptions ) ? $rawOptions : [];
2259 2558 $results = [
@@ -2279,9 +2578,9 @@
2279 2578 $fields = [];
2280 2579 if ( isset( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) && ! empty( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) ) {
2281 2580 $fields = $sections[ $section_id ]['list'][ $block_id ]['fields'];
2282 2581 }
2283 -
2582 + do_action( 'rtsb/before/save/options', $section_id, $block_id, $rawOptions );
2284 2583 if ( empty( $fields ) ) {
2285 2584 if ( isset( $rawOptions['active'] ) ) {
2286 2585 $changed = true;
2287 2586 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
@@ -2287,21 +2586,49 @@
2287 2586 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
2288 2587 }
2289 2588 } else {
2290 2589 foreach ( $rawOptions as $raw_option_key => $raw_value ) {
2291 - if ( $raw_option_key === 'active' ) {
2590 + if ( 'active' === $raw_option_key ) {
2292 2591 $changed = true;
2293 - $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : '';
2592 + $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : ''; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
2294 2593 } else {
2295 2594 if ( isset( $fields[ $raw_option_key ] ) ) {
2296 2595 $field = $fields[ $raw_option_key ];
2297 - if ( $field['type'] === 'switch' ) {
2596 + if ( 'switch' === $field['type'] ) {
2298 2597 $value = 'on' === $raw_value ? 'on' : '';
2299 2598 } elseif ( 'repeaters' === $field['type'] ) {
2300 - $value = stripslashes_deep( $raw_value );
2599 + $the_value = [];
2600 + $rep_title = [];
2601 + if ( ! empty( $raw_value ) && is_array( $raw_value ) ) {
2602 + foreach ( $raw_value as $key => $value ) {
2603 + if ( is_string( $value ) ) {
2604 + $the_value_decoded = json_decode( stripslashes_deep( $value ) );
2605 + } else {
2606 + $the_value_decoded = $value;
2607 + }
2608 + $cr = $the_value_decoded->title ?? '';
2609 + if ( in_array( $cr, $rep_title, true ) ) {
2610 + continue;
2611 + }
2612 + $rep_title[] = $cr;
2613 + $the_value[] = $the_value_decoded;
2614 + }
2615 + } elseif ( ! empty( $raw_value ) && is_string( $raw_value ) ) {
2616 + $the_value = $raw_value;
2617 + }
2618 + $value = wp_json_encode( $the_value, JSON_UNESCAPED_UNICODE );
2619 + } elseif ( in_array( $field['type'], [ 'product_addons_special_settings', 'checkout_fields' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2620 + $manual_field_value = [];
2621 + if ( is_array( $raw_value ) ) {
2622 + foreach ( $raw_value as $key => $value ) {
2623 + $manual_field_value[] = json_decode( stripslashes( $value ), true );
2624 + }
2625 + $value = wp_json_encode( $manual_field_value, JSON_UNESCAPED_UNICODE );
2626 + } elseif ( is_string( $raw_value ) ) {
2627 + $value = $raw_value;
2628 + }
2301 2629 } else {
2302 - if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) {
2303 -
2630 + if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2304 2631 if ( isset( $raw_value ) && is_array( $raw_value ) ) {
2305 2632 if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2306 2633 $value = array_map( $field['sanitize_fn'], $raw_value );
2307 2634 } else {
@@ -2323,9 +2650,9 @@
2323 2650 $value = sanitize_text_field( $raw_value );
2324 2651 }
2325 2652 }
2326 2653 }
2327 - $options[ $block_id ][ $raw_option_key ] = $sections[ $section_id ]['list'][ $block_id ]['fields'][ $raw_option_key ]['value'] = $value ?? null;
2654 + $options[ $block_id ][ $raw_option_key ] = $sections[ $section_id ]['list'][ $block_id ]['fields'][ $raw_option_key ]['value'] = $value ?? null; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
2328 2655 $changed = true;
2329 2656 }
2330 2657 }
2331 2658 }
@@ -2345,27 +2672,15 @@
2345 2672
2346 2673 return $results;
2347 2674 }
2348 2675
2349 - /***
2350 - * @param $meta_key
2351 - * @param $meta_value
2676 + /**
2677 + * Count products by taxonomies.
2352 2678 *
2353 - * @return mixed|void
2679 + * @param array $terms Terms.
2680 + * @param string $taxonomy Taxonomy.
2354 2681 *
2355 - *
2356 - * public static function get_post_id_by_meta_key_and_value( $meta_key, $meta_value ) {
2357 - * if( ! $meta_key || ! $meta_value ){
2358 - * return;
2359 - * }
2360 - * global $wpdb;
2361 - * $prepare_guery = $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '%s' and meta_value = '%d'", $meta_key, $meta_value );
2362 - * $the_products = $wpdb->get_col( $prepare_guery );
2363 - * if( count( $the_products ) > 0 ){
2364 - * return $the_products[0];
2365 - * }
2366 - * return;
2367 - * }
2682 + * @return int|void
2368 2683 */
2369 2684 public static function count_products_by_taxonomies( $terms, $taxonomy = 'product_cat' ) {
2370 2685 if ( empty( $terms ) || ! is_array( $terms ) ) {
2371 2686 return;
@@ -2377,8 +2692,10 @@
2377 2692 ];
2378 2693
2379 2694 if ( 'product_cat' === $taxonomy ) {
2380 2695 $args['product_category_id'] = $terms;
2696 + } elseif ( 'product_brand' === $taxonomy ) {
2697 + $args['product_brand_id'] = $terms;
2381 2698 } else {
2382 2699 $args['product_tag_id'] = $terms;
2383 2700 }
2384 2701
@@ -2387,8 +2704,61 @@
2387 2704 return ! empty( $query->get_products() ) ? count( $query->get_products() ) : 0;
2388 2705 }
2389 2706
2390 2707 /**
2708 + * Count products by attribute terms.
2709 + *
2710 + * @param array $term_ids Term IDs.
2711 + * @param string $relation Relation.
2712 + *
2713 + * @return int
2714 + */
2715 + public static function count_products_by_attribute_terms( $term_ids, $relation = 'AND' ) {
2716 + if ( empty( $term_ids ) || ! is_array( $term_ids ) ) {
2717 + return 0;
2718 + }
2719 +
2720 + $terms_by_taxonomy = [];
2721 +
2722 + foreach ( $term_ids as $term_id ) {
2723 + $term = get_term( $term_id );
2724 + if ( ! is_wp_error( $term ) && $term ) {
2725 + if ( ! isset( $terms_by_taxonomy[ $term->taxonomy ] ) ) {
2726 + $terms_by_taxonomy[ $term->taxonomy ] = [];
2727 + }
2728 + $terms_by_taxonomy[ $term->taxonomy ][] = $term_id;
2729 + }
2730 + }
2731 +
2732 + if ( empty( $terms_by_taxonomy ) ) {
2733 + return 0;
2734 + }
2735 +
2736 + $args = [
2737 + 'status' => 'publish',
2738 + 'limit' => -1,
2739 + 'return' => 'ids',
2740 + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2741 + 'relation' => strtoupper( $relation ),
2742 + ],
2743 + ];
2744 +
2745 + foreach ( $terms_by_taxonomy as $taxonomy => $terms ) {
2746 + $args['tax_query'][] = [
2747 + 'taxonomy' => $taxonomy,
2748 + 'field' => 'term_id',
2749 + 'terms' => $terms,
2750 + 'operator' => 'IN',
2751 + ];
2752 + }
2753 +
2754 + $query = new WC_Product_Query( $args );
2755 + $products = $query->get_products();
2756 +
2757 + return count( $products );
2758 + }
2759 +
2760 + /**
2391 2761 * Check if product filters widget has ajax.
2392 2762 *
2393 2763 * @param string $page Page name.
2394 2764 *
@@ -2459,22 +2829,76 @@
2459 2829 if ( ! is_admin() ) {
2460 2830 return [];
2461 2831 }
2462 2832
2463 - $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) );
2833 + $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2464 2834
2465 2835 if ( isset( self::$cache[ $cache_key ] ) ) {
2466 2836 return self::$cache[ $cache_key ];
2467 2837 }
2838 + $results = wp_cache_get( $cache_key, 'shopbuilder' );
2839 + if ( ! $results ) {
2840 + global $wpdb;
2841 + $sql = "SELECT t.term_id, t.name
2842 + FROM {$wpdb->terms} t
2843 + JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
2844 + WHERE tt.taxonomy = 'product_cat'";
2468 2845
2469 - $results = wp_cache_get( $cache_key );
2846 + if ( $search_query ) {
2847 + $sql .= ' AND t.name LIKE %s';
2848 + $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2849 + } else {
2850 + $prepared_sql = $sql; // No need for placeholders.
2851 + }
2470 2852
2853 + $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
2854 + // Cache the results in the object cache for future use.
2855 + wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
2856 + Cache::set_data_cache_key( $cache_key );
2857 + }
2858 +
2859 + $cats = [];
2860 + if ( ! is_array( $results ) && ! count( $results ) ) {
2861 + return $cats;
2862 + }
2863 + foreach ( $results as $row ) {
2864 + $category_id = $row->term_id;
2865 + $category_title = $row->name;
2866 +
2867 + $cats[] = [
2868 + 'value' => $category_id,
2869 + 'label' => $category_title,
2870 + ];
2871 + }
2872 +
2873 + // Cache the results in a static variable for the next call.
2874 + self::$cache[ $cache_key ] = $cats;
2875 + return $cats;
2876 + }
2877 + /**
2878 + * Get WooCommerce product categories.
2879 + *
2880 + * @param string|null $search_query The search string for category names.
2881 + *
2882 + * @return array An array of product categories with 'value' and 'label' keys.
2883 + */
2884 + public static function products_tags_query( $search_query = null ) {
2885 + if ( ! is_admin() ) {
2886 + return [];
2887 + }
2888 +
2889 + $cache_key = 'rtsb_product_tags_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2890 +
2891 + if ( isset( self::$cache[ $cache_key ] ) ) {
2892 + return self::$cache[ $cache_key ];
2893 + }
2894 + $results = wp_cache_get( $cache_key, 'shopbuilder' );
2471 2895 if ( ! $results ) {
2472 2896 global $wpdb;
2473 2897 $sql = "SELECT t.term_id, t.name
2474 2898 FROM {$wpdb->terms} t
2475 2899 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
2476 - WHERE tt.taxonomy = 'product_cat'";
2900 + WHERE tt.taxonomy = 'product_tag'";
2477 2901
2478 2902 if ( $search_query ) {
2479 2903 $sql .= ' AND t.name LIKE %s';
2480 2904 $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -2483,9 +2907,10 @@
2483 2907 }
2484 2908
2485 2909 $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
2486 2910 // Cache the results in the object cache for future use.
2487 - wp_cache_set( $cache_key, $results ); // Adjust the expiration time as needed.
2911 + wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
2912 + Cache::set_data_cache_key( $cache_key );
2488 2913 }
2489 2914
2490 2915 $cats = [];
2491 2916 if ( ! is_array( $results ) && ! count( $results ) ) {
@@ -2564,8 +2989,9 @@
2564 2989 /**
2565 2990 * Get Post Types.
2566 2991 *
2567 2992 * @param string $search_query Search query.
2993 + * @param array $args Arguments.
2568 2994 *
2569 2995 * @return array
2570 2996 */
2571 2997 public static function get_post_types( $search_query = null, $args = [] ) {
@@ -2576,9 +3002,8 @@
2576 3002 'post_type' => 'any',
2577 3003 'posts_per_page' => 20,
2578 3004 'orderby' => 'ID',
2579 3005 'order' => 'DESC',
2580 - // 'suppress_filters' => false, // WPML Support, Remove Others Languages.
2581 3006 ]
2582 3007 );
2583 3008
2584 3009 if ( 'archive' !== $args['post_type'] ) {
@@ -2589,11 +3014,12 @@
2589 3014 $query_results = get_posts( $args );
2590 3015 foreach ( $query_results as $post ) {
2591 3016 $posts[] = [
2592 3017 'value' => $post->ID,
2593 - 'label' => $post->post_title . ' (' . $post->ID . ')',
3018 + 'label' => $post->post_title . ' (ID#' . $post->ID . ')',
2594 3019 ];
2595 3020 }
3021 +
2596 3022 if ( $search_query ) {
2597 3023 if ( strpos( '-error 404', $search_query ) ) {
2598 3024 $posts[] = [
2599 3025 'value' => 'error',
@@ -2600,12 +3026,14 @@
2600 3026 'label' => __( '404 Error', 'shopbuilder' ),
2601 3027 ];
2602 3028 }
2603 3029 } else {
2604 - $posts[] = [
2605 - 'value' => 'error',
2606 - 'label' => __( '404 Error', 'shopbuilder' ),
2607 - ];
3030 + if ( 'page' === $args['post_type'] ) {
3031 + $posts[] = [
3032 + 'value' => 'error',
3033 + 'label' => __( '404 Error', 'shopbuilder' ),
3034 + ];
3035 + }
2608 3036 }
2609 3037 } else {
2610 3038 $posts = self::get_registered_post_types( $search_query );
2611 3039 }
@@ -2633,9 +3061,9 @@
2633 3061 public static function renderView( $viewName, $args = [], $return = false ) {
2634 3062 $viewName = str_replace( '.', '/', $viewName );
2635 3063
2636 3064 if ( ! empty( $args ) && is_array( $args ) ) {
2637 - extract( $args );
3065 + extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
2638 3066 }
2639 3067
2640 3068 $view_file = rtsb()->plugin_path() . '/resources/' . $viewName . '.php';
2641 3069
@@ -2657,9 +3085,9 @@
2657 3085
2658 3086 /**
2659 3087 * Best selling product query.
2660 3088 *
2661 - * @param int $minimum_sale Minimum sale/
3089 + * @param int $minimum_sale Minimum sale.
2662 3090 * @param int $limit Total limit.
2663 3091 *
2664 3092 * @return array|mixed
2665 3093 */
@@ -2717,12 +3145,14 @@
2717 3145
2718 3146 if ( isset( self::$cache[ $cache_key ] ) ) {
2719 3147 return self::$cache[ $cache_key ];
2720 3148 }
3149 + $date_created = $product->get_date_created();
3150 + if ( ! $date_created ) {
3151 + return false;
3152 + }
3153 + $publish_timestamp = strtotime( $date_created->date_i18n( 'Y-m-d H:i:s' ) );
2721 3154
2722 - // Get the product publish date.
2723 - $publish_timestamp = strtotime( $product->get_date_created()->date_i18n( 'Y-m-d H:i:s' ) );
2724 -
2725 3155 // Calculate the difference in days.
2726 3156 $days_difference = abs( time() - $publish_timestamp ) / ( 60 * 60 * 24 );
2727 3157
2728 3158 // Check if the product is considered new.
@@ -2750,12 +3180,9 @@
2750 3180
2751 3181 /**
2752 3182 * Checks if a feature is disabled for guest users based on the provided option.
2753 3183 *
2754 - * @param string $option The option to retrieve from the item.
2755 - * @param mixed $default The default value if the option is not found.
2756 - *
2757 - * @return bool
3184 + * @return void
2758 3185 */
2759 3186 public static function woocommerce_output_all_notices() {
2760 3187 if ( function_exists( 'wc_print_notices' ) ) :
2761 3188 echo '<div class="rtsb-notice">';
@@ -2857,8 +3284,68 @@
2857 3284 ];
2858 3285 }
2859 3286
2860 3287 /**
3288 + * Retrieve an array of custom icons with their HTML representation.
3289 + *
3290 + * @return array
3291 + */
3292 + public static function get_icons() {
3293 + $icon = self::get_custom_icon_names();
3294 + $icons_array = [];
3295 + foreach ( $icon as $value ) {
3296 + $icons_array[ $value ] = '<i class="rtsb-icon rtsb-icon-' . $value . '"></i> <span class="icon-name">' . ucfirst( str_replace( '-', ' ', $value ) ) . '</span>';
3297 + }
3298 + return $icons_array;
3299 + }
3300 +
3301 + /**
3302 + * Generates HTML markup for displaying an endpoint icon.
3303 + *
3304 + * @param array $data The endpoint data containing icon information.
3305 + * @param boolean $wrapper Whether to wrap the icon in a span element.
3306 + *
3307 + * @return string
3308 + */
3309 + public static function get_icon_html( $data, $wrapper = true ) {
3310 + if ( empty( $data ) || 'none' === $data['icon_source'] ) {
3311 + return '';
3312 + }
3313 + $endpoint = '';
3314 + $html = $wrapper ? '<span class="icon ' . esc_attr( $endpoint ) . '_icon">' : '';
3315 +
3316 + if ( 'select_icon' === $data['icon_source'] ) {
3317 + $html .= '<i class="rtsb-icon rtsb-icon-' . esc_attr( $data['custom_icon'] ) . '"></i>';
3318 + } else {
3319 + $icon_html = '';
3320 + $icon_url = $data['image_icon']['source'] ?? '';
3321 + $icon_id = $data['image_icon']['id'] ?? 0;
3322 + $extension = ! empty( $icon_url ) ? strtolower( substr( strrchr( $data['image_icon']['source'], '.' ), 1 ) ) : '';
3323 +
3324 + if ( 'svg' === $extension ) {
3325 + $content = file_get_contents( $icon_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
3326 + $is_svg = strpos( $content, '<svg' );
3327 +
3328 + if ( false !== $is_svg ) {
3329 + $icon_html = substr( $content, $is_svg );
3330 + }
3331 + } else {
3332 + $attr = [
3333 + 'class' => 'rtsb-icon-img',
3334 + 'alt' => esc_html( ucfirst( $endpoint ) ) . esc_html__( ' Icon', 'shopbuilder' ),
3335 + ];
3336 +
3337 + $icon_html = wp_get_attachment_image( absint( $icon_id ), 'full', true, $attr );
3338 + }
3339 +
3340 + $html .= $icon_html;
3341 + }
3342 +
3343 + $html .= $wrapper ? '</span>' : '';
3344 +
3345 + return $html;
3346 + }
3347 + /**
2861 3348 * Flushes rewrite rules.
2862 3349 *
2863 3350 * @return void
2864 3351 */
@@ -2898,12 +3385,18 @@
2898 3385
2899 3386 /**
2900 3387 * Social Share Platforms.
2901 3388 *
2902 - * @return mixed|null
3389 + * @param string $section_id Section ID.
3390 + * @param string $block_id Block ID.
3391 + * @param string $option_key Option key.
3392 + * @param string $compare_key Compare key.
3393 + * @param string $compare_value Compare value.
3394 + * @param array $values Values.
3395 + *
3396 + * @return void
2903 3397 */
2904 3398 public static function set_repeater_options( $section_id, $block_id, $option_key, $compare_key, $compare_value, $values ) {
2905 - // $options = self::get_options( $section_id, $block_id );
2906 3399 $modules = DataModel::source()->get_option( $section_id, [], false );
2907 3400 if ( empty( $modules[ $block_id ] ) ) {
2908 3401 return;
2909 3402 }
@@ -2924,9 +3417,10 @@
2924 3417 return;
2925 3418 }
2926 3419 $repeater_options[ $index ] = wp_parse_args( $values, $repeater_options[ $index ] );
2927 3420 $options[ $option_key ] = wp_json_encode( $repeater_options );
2928 - self::set_options( $section_id, $block_id, $options );
3421 + $modules[ $block_id ] = $options;
3422 + DataModel::source()->set_option( $section_id, $modules );
2929 3423 }
2930 3424
2931 3425
2932 3426 /**
@@ -2940,6 +3434,675 @@
2940 3434 return 'product' === get_post_type( $id ) && 'publish' === get_post_status( $id );
2941 3435 }
2942 3436 );
2943 3437 return $ids;
3438 + }
3439 +
3440 + /**
3441 + * Generate and cache dynamic CSS styles.
3442 + *
3443 + * @param array $options CSS options to apply (e.g. padding, margin).
3444 + * @param string $cache_key Cache key for the CSS.
3445 + * @param array $css_properties An array of CSS properties with selectors and properties.
3446 + *
3447 + * @return void
3448 + */
3449 + public static function dynamic_styles( $options, $cache_key, $css_properties ) {
3450 + $cached_css = wp_cache_get( $cache_key, 'shopbuilder' );
3451 + $style_handle = self::optimized_handle( 'rtsb-frontend' );
3452 +
3453 + if ( false !== $cached_css ) {
3454 + wp_add_inline_style( $style_handle, $cached_css );
3455 + return;
3456 + }
3457 +
3458 + $css_rules = [];
3459 + $grouped_css_rules = [];
3460 +
3461 + foreach ( $css_properties as $option => $details ) {
3462 + if ( ! empty( $options[ $option ] ) ) {
3463 + $selector = $details['selector'] ?? '';
3464 + $property = $details['property'] ?? '';
3465 + $unit = $details['unit'] ?? '';
3466 + $value = $options[ $option ];
3467 +
3468 + if ( empty( $grouped_css_rules[ $selector ] ) ) {
3469 + $grouped_css_rules[ $selector ] = [];
3470 + }
3471 +
3472 + if ( is_array( $property ) ) {
3473 + foreach ( $property as $prop ) {
3474 + $grouped_css_rules[ $selector ][] = $prop . ': ' . $value . $unit . ' !important';
3475 + }
3476 + } else {
3477 + $grouped_css_rules[ $selector ][] = $property . ': ' . $value . $unit . ' !important';
3478 + }
3479 + }
3480 + }
3481 +
3482 + foreach ( $grouped_css_rules as $selector => $properties ) {
3483 + $css_rules[] = $selector . ' {' . implode( '; ', $properties ) . '}';
3484 + }
3485 +
3486 + $dynamic_css = implode( ' ', $css_rules );
3487 +
3488 + wp_cache_set( $cache_key, $dynamic_css, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3489 + Cache::set_data_cache_key( $cache_key );
3490 +
3491 + if ( ! empty( $dynamic_css ) ) {
3492 + wp_add_inline_style( $style_handle, $dynamic_css );
3493 + }
3494 + }
3495 +
3496 + /**
3497 + * Pro version notice.
3498 + *
3499 + * @param string $ver Pro Version.
3500 + * @param string $tab Tab.
3501 + * @param string $name Name.
3502 + * @param bool $enable_free_Link Enable free link.
3503 + *
3504 + * @return array[]
3505 + */
3506 + public static function pro_version_notice( $ver, $tab = 'billing_fields', $name = 'ShopBuilder', $enable_free_Link = true ) {
3507 + return [
3508 + 'version_check' => [
3509 + 'id' => 'version_check',
3510 + 'type' => 'title',
3511 + 'label' => sprintf(
3512 + /* translators: 1: required version, 2: link to the Plugins page */
3513 + esc_html__(
3514 + 'To access all features and settings of this module, please ensure that "%1$s" is updated to version %2$s. %3$s',
3515 + 'shopbuilder'
3516 + ),
3517 + $name,
3518 + sprintf(
3519 + '<br /><u><b>%s</b></u>',
3520 + esc_html( $ver ?? '1.5.0' ) . ' or higher'
3521 + ),
3522 + $enable_free_Link
3523 + ? sprintf(
3524 + '%s <a class="pro-update-required" href="%s" target="_blank" title="%s">%s</a>',
3525 + esc_attr__( 'Update to the latest version', 'shopbuilder' ),
3526 + esc_url( admin_url( 'plugins.php' ) ),
3527 + esc_attr__( 'Go to the Plugin Page', 'shopbuilder' ),
3528 + esc_html__( 'from the Plugins page', 'shopbuilder' )
3529 + )
3530 + : ''
3531 + ),
3532 + 'tab' => $tab,
3533 + 'customClass' => 'checkout-notice',
3534 + ],
3535 + ];
3536 + }
3537 +
3538 + /**
3539 + * Get product gallery ids.
3540 + *
3541 + * @param object $product Product object.
3542 + *
3543 + * @return mixed
3544 + */
3545 + public static function get_cached_gallery_ids( $product ) {
3546 + $product_id = $product->get_id();
3547 + $cache_key = 'rtsb_product_gallery_ids_' . $product_id;
3548 +
3549 + if ( isset( self::$cache[ $cache_key ] ) ) {
3550 + return self::$cache[ $cache_key ];
3551 + }
3552 +
3553 + $cached_result = wp_cache_get( $cache_key, 'shopbuilder' );
3554 +
3555 + if ( false !== $cached_result ) {
3556 + self::$cache[ $cache_key ] = $cached_result;
3557 +
3558 + return $cached_result;
3559 + }
3560 +
3561 + $gallery_ids = $product->get_gallery_image_ids();
3562 + self::$cache[ $cache_key ] = $gallery_ids;
3563 +
3564 + wp_cache_set( $cache_key, $gallery_ids, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3565 + Cache::set_data_cache_key( $cache_key );
3566 +
3567 + return $gallery_ids;
3568 + }
3569 +
3570 + /**
3571 + * Check if optimization settings are enabled.
3572 + *
3573 + * @return bool
3574 + */
3575 + public static function is_optimization_enabled() {
3576 + $has_pro = rtsb()->has_pro();
3577 + $pro_ver = defined( 'RTSBPRO_VERSION' ) ? RTSBPRO_VERSION : 0;
3578 +
3579 + if ( $has_pro && version_compare( $pro_ver, '2.0.0', '<' ) ) {
3580 + return false;
3581 + }
3582 +
3583 + $generalList = GeneralList::instance()->get_data();
3584 +
3585 + return 'on' === ( $generalList['optimization']['enable_optimization'] ?? '' );
3586 + }
3587 +
3588 + /**
3589 + * Get the optimized handle.
3590 + *
3591 + * @param string $handle Base handle used for script/style IDs.
3592 + *
3593 + * @return string
3594 + */
3595 + public static function optimized_handle( $handle ) {
3596 + $use_optimization = self::is_optimization_enabled();
3597 +
3598 + if ( $use_optimization ) {
3599 + $handle = self::is_contextual_loading() ? self::get_optimized_handle_by_context() : 'rtsb-bundled';
3600 + }
3601 +
3602 + return $handle;
3603 + }
3604 +
3605 + /**
3606 + * Get the optimized handle by context.
3607 + *
3608 + * @return string
3609 + */
3610 + public static function get_optimized_handle_by_context() {
3611 + $context = self::detect_context();
3612 +
3613 + if ( 'account' === $context ) {
3614 + return 'rtsb-bundled-global';
3615 + }
3616 +
3617 + return "rtsb-bundled-$context";
3618 + }
3619 +
3620 + /**
3621 + * Check if Elementor page.
3622 + *
3623 + * @param int $post_id Post ID.
3624 + *
3625 + * @return bool
3626 + */
3627 + public static function is_elementor_page( $post_id = null ) {
3628 + if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
3629 + return false;
3630 + }
3631 +
3632 + $post_id = $post_id ?: get_the_ID();
3633 +
3634 + if ( ! $post_id ) {
3635 + return true;
3636 + }
3637 +
3638 + return Plugin::$instance->documents->get( $post_id )->is_built_with_elementor();
3639 + }
3640 +
3641 + /**
3642 + * Check if shop or archive.
3643 + *
3644 + * @return bool
3645 + */
3646 + public static function is_shop_or_archive() {
3647 + return is_shop() || is_product_category() || is_product_tag() || is_post_type_archive( 'product' ) || BuilderFns::is_archive() || BuilderFns::is_shop() || is_tax( 'product_brand' );
3648 + }
3649 +
3650 + /**
3651 + * Detect context.
3652 + *
3653 + * @return string
3654 + */
3655 + public static function detect_context() {
3656 + switch ( true ) {
3657 + case is_product() || BuilderFns::is_product():
3658 + $context = 'product';
3659 + break;
3660 +
3661 + case is_cart() || BuilderFns::is_cart():
3662 + $context = 'cart';
3663 + break;
3664 +
3665 + case is_checkout() || BuilderFns::is_checkout():
3666 + $context = 'checkout';
3667 + break;
3668 +
3669 + case self::is_shop_or_archive():
3670 + $context = 'shop';
3671 + break;
3672 +
3673 + default:
3674 + $context = 'global';
3675 + break;
3676 + }
3677 +
3678 + return apply_filters( 'rtsb/optimizer/context_detect', $context );
3679 + }
3680 +
3681 + /**
3682 + * Enqueue optimized assets.
3683 + *
3684 + * @return void
3685 + */
3686 + public static function enqueue_optimized_assets() {
3687 + $asset_registry = AssetRegistry::instance();
3688 + $bundled_assets = $asset_registry->get_bundled_assets();
3689 + $context = self::detect_context();
3690 + $contextual_loading = self::is_contextual_loading();
3691 + $theme_handle = self::find_theme_stylesheet_handle();
3692 +
3693 + if ( $contextual_loading ) {
3694 + // Enqueue JS.
3695 + if ( ! empty( $bundled_assets['js'] ) ) {
3696 + $js_context = isset( $bundled_assets['js'][ $context ] ) ? $context : 'global';
3697 +
3698 + if ( $js_context ) {
3699 + wp_enqueue_script( $bundled_assets['js'][ $js_context ]['handle'] );
3700 + }
3701 + }
3702 +
3703 + // Enqueue CSS.
3704 + if ( ! empty( $bundled_assets['css'] ) ) {
3705 + $css_context = isset( $bundled_assets['css'][ $context ] ) ? $context : 'global';
3706 +
3707 + if ( $css_context ) {
3708 + $handle = $bundled_assets['css'][ $css_context ]['handle'];
3709 +
3710 + wp_enqueue_style( $handle );
3711 +
3712 + if ( ! empty( $theme_handle ) ) {
3713 + self::set_theme_dependency( $theme_handle, $handle );
3714 + }
3715 + }
3716 + }
3717 + } else {
3718 + // Enqueue JS.
3719 + if ( ! empty( $bundled_assets['js'] ) ) {
3720 + wp_enqueue_script( $bundled_assets['js']['handle'] );
3721 + }
3722 +
3723 + // Enqueue CSS.
3724 + if ( ! empty( $bundled_assets['css'] ) ) {
3725 + $handle = $bundled_assets['css']['handle'];
3726 +
3727 + wp_enqueue_style( $handle );
3728 +
3729 + if ( ! empty( $theme_handle ) ) {
3730 + self::set_theme_dependency( $theme_handle, $handle );
3731 + }
3732 + }
3733 + }
3734 + }
3735 +
3736 + /**
3737 + * Find theme stylesheet handle.
3738 + *
3739 + * @return string|false
3740 + */
3741 + private static function find_theme_stylesheet_handle() {
3742 + static $cached_handle = null;
3743 +
3744 + if ( null !== $cached_handle ) {
3745 + return $cached_handle;
3746 + }
3747 +
3748 + global $wp_styles;
3749 +
3750 + if ( ! $wp_styles instanceof WP_Styles ) {
3751 + return false;
3752 + }
3753 +
3754 + $stylesheet_uri = get_stylesheet_uri();
3755 + $theme_directory_uri = get_stylesheet_directory_uri();
3756 +
3757 + $hash = md5( $stylesheet_uri );
3758 + $transient_key = 'rtsb_theme_css_handle_' . $hash;
3759 +
3760 + $transient = get_transient( $transient_key );
3761 +
3762 + if ( false !== $transient ) {
3763 + $cached_handle = $transient;
3764 +
3765 + return $transient;
3766 + }
3767 +
3768 + foreach ( $wp_styles->registered as $handle => $style ) {
3769 + $src = $style->src;
3770 +
3771 + if (
3772 + ( $src === $stylesheet_uri || strpos( $src, $theme_directory_uri ) !== false ) &&
3773 + strpos( $src, 'style.css' ) !== false
3774 + ) {
3775 + set_transient( $transient_key, $handle, DAY_IN_SECONDS );
3776 + Cache::set_transient_cache_key( $transient_key );
3777 + $cached_handle = $handle;
3778 +
3779 + return $handle;
3780 + }
3781 + }
3782 +
3783 + $filtered_handle = apply_filters( 'rtsb/optimizer/theme_stylesheet_handle', false );
3784 +
3785 + if ( is_string( $filtered_handle ) && ! empty( $filtered_handle ) ) {
3786 + set_transient( $transient_key, $filtered_handle, DAY_IN_SECONDS );
3787 + Cache::set_transient_cache_key( $transient_key );
3788 + $cached_handle = $filtered_handle;
3789 +
3790 + return $filtered_handle;
3791 + }
3792 +
3793 + set_transient( $transient_key, false, DAY_IN_SECONDS );
3794 + Cache::set_transient_cache_key( $transient_key );
3795 + $cached_handle = false;
3796 +
3797 + return false;
3798 + }
3799 +
3800 + /**
3801 + * Set theme dependency.
3802 + *
3803 + * @param string $theme_handle Theme handle.
3804 + * @param string $our_handle Our handle.
3805 + *
3806 + * @return void
3807 + */
3808 + private static function set_theme_dependency( $theme_handle, $our_handle ) {
3809 + global $wp_styles;
3810 +
3811 + if ( ! isset( $wp_styles->registered[ $theme_handle ] ) ) {
3812 + return;
3813 + }
3814 +
3815 + $theme_style = $wp_styles->registered[ $theme_handle ];
3816 +
3817 + // Add our handle as a dependency if it's not already there.
3818 + if ( ! in_array( $our_handle, $theme_style->deps, true ) ) {
3819 + $theme_style->deps[] = $our_handle;
3820 + }
3821 + }
3822 +
3823 + /**
3824 + * Check if Elementor scripts should be loaded.
3825 + *
3826 + * @return bool
3827 + */
3828 + public static function should_load_elementor_scripts() {
3829 + $data = GeneralList::instance()->get_data()['optimization'] ?? [];
3830 +
3831 + $enable_optimization = $data['enable_optimization'] ?? 'on';
3832 + $load_elementor_scripts = $data['load_elementor_scripts'] ?? 'on';
3833 +
3834 + if ( 'on' !== $enable_optimization ) {
3835 + return true;
3836 + }
3837 +
3838 + return 'on' === $load_elementor_scripts;
3839 + }
3840 +
3841 + /**
3842 + * Locate asset.
3843 + *
3844 + * @param string $relative_path Relative path.
3845 + *
3846 + * @return string|null
3847 + */
3848 + public static function locate_asset( $relative_path ) {
3849 + $free_context = rtsb();
3850 + $pro_context = function_exists( 'rtsbpro' ) && rtsb()->has_pro() ? rtsbpro() : null;
3851 +
3852 + $paths = [];
3853 +
3854 + if ( $pro_context ) {
3855 + $paths[] = $pro_context->get_assets_path( $relative_path );
3856 + }
3857 +
3858 + $paths[] = $free_context->get_assets_path( $relative_path );
3859 +
3860 + foreach ( $paths as $path ) {
3861 + if ( file_exists( $path ) ) {
3862 + return $path;
3863 + }
3864 + }
3865 +
3866 + return null;
3867 + }
3868 +
3869 + /**
3870 + * Enqueue module assets.
3871 + *
3872 + * @param string $handle Asset handle.
3873 + * @param string $module_name Module name.
3874 + * @param array $options Options array: ['type' => 'css|js|both', 'deps' => [], 'context' => null].
3875 + *
3876 + * @return string
3877 + */
3878 + public static function enqueue_module_assets( $handle, $module_name, $options = [] ) {
3879 + $use_optimization = self::is_optimization_enabled();
3880 + $handle = self::optimized_handle( $handle );
3881 +
3882 + if ( $use_optimization ) {
3883 + return $handle;
3884 + }
3885 +
3886 + $defaults = [
3887 + 'type' => 'both',
3888 + 'deps' => [ 'jquery', 'rtsb-public' ],
3889 + 'context' => null,
3890 + 'version' => RTSB_VERSION,
3891 + ];
3892 +
3893 + $config = array_merge( $defaults, $options );
3894 + $rtl_suffix = is_rtl() ? '-rtl' : '';
3895 + $rtl_dir = is_rtl() ? trailingslashit( 'rtl' ) : trailingslashit( 'css' );
3896 + $context = $config['context'] ?: rtsb();
3897 + $load_css = in_array( $config['type'], [ 'css', 'both' ], true );
3898 + $load_js = in_array( $config['type'], [ 'js', 'both' ], true );
3899 +
3900 + // Register CSS if enabled.
3901 + if ( $load_css ) {
3902 + wp_register_style(
3903 + $handle,
3904 + $context->get_assets_uri( $rtl_dir . 'modules/' . $module_name . $rtl_suffix . '.css' ),
3905 + [],
3906 + $config['version']
3907 + );
3908 + }
3909 +
3910 + // Register JS if enabled.
3911 + if ( $load_js ) {
3912 + wp_register_script(
3913 + $handle,
3914 + $context->get_assets_uri( 'js/modules/' . $module_name . '.js' ),
3915 + $config['deps'],
3916 + $config['version'],
3917 + true
3918 + );
3919 + }
3920 +
3921 + if ( $load_css ) {
3922 + wp_enqueue_style( $handle );
3923 +
3924 + $theme_handle = self::find_theme_stylesheet_handle();
3925 +
3926 + if ( ! empty( $theme_handle ) ) {
3927 + self::set_theme_dependency( $theme_handle, $handle );
3928 + }
3929 + }
3930 +
3931 + if ( $load_js ) {
3932 + wp_enqueue_script( $handle );
3933 + }
3934 +
3935 + return $handle;
3936 + }
3937 +
3938 + /**
3939 + * Check if contextual loading is enabled.
3940 + *
3941 + * @return bool
3942 + */
3943 + public static function is_contextual_loading() {
3944 + $data = GeneralList::instance()->get_data()['optimization'] ?? [];
3945 +
3946 + return ! empty( $data['context_asset_loading'] ) && 'on' === $data['context_asset_loading'];
3947 + }
3948 +
3949 + /**
3950 + * Get Modules list with cache support.
3951 + *
3952 + * @return array
3953 + */
3954 + public static function get_modules_list() {
3955 + static $cached_modules = null;
3956 +
3957 + if ( null !== $cached_modules ) {
3958 + return $cached_modules;
3959 + }
3960 +
3961 + $cache_key = 'rtsb_module_list';
3962 + $cache_group = 'shopbuilder';
3963 +
3964 + $cached = wp_cache_get( $cache_key, $cache_group );
3965 +
3966 + if ( false !== $cached ) {
3967 + $cached_modules = $cached;
3968 +
3969 + return $cached;
3970 + }
3971 +
3972 + $modules = ModuleList::instance()->get_data();
3973 +
3974 + wp_cache_set( $cache_key, $modules, $cache_group, 12 * HOUR_IN_SECONDS );
3975 + Cache::set_data_cache_key( $cache_key );
3976 +
3977 + $cached_modules = $modules;
3978 +
3979 + return $modules;
3980 + }
3981 +
3982 + /**
3983 + * Get Elementor widgets list with cache support.
3984 + *
3985 + * @return array
3986 + */
3987 + public static function get_widgets_list() {
3988 + static $cached_widgets = null;
3989 +
3990 + if ( null !== $cached_widgets ) {
3991 + return $cached_widgets;
3992 + }
3993 +
3994 + $cache_key = 'rtsb_elementor_widget_list';
3995 + $cache_group = 'shopbuilder';
3996 +
3997 + $cached = wp_cache_get( $cache_key, $cache_group );
3998 +
3999 + if ( false !== $cached ) {
4000 + $cached_widgets = $cached;
4001 +
4002 + return $cached;
4003 + }
4004 +
4005 + $widgets = ElementList::instance()->get_list();
4006 +
4007 + wp_cache_set( $cache_key, $widgets, $cache_group, 12 * HOUR_IN_SECONDS );
4008 + Cache::set_data_cache_key( $cache_key );
4009 +
4010 + $cached_widgets = $widgets;
4011 +
4012 + return $widgets;
4013 + }
4014 +
4015 + /**
4016 + * Convert the given price to the active currency.
4017 + *
4018 + * @param float $price The original price.
4019 + * @param Object||null $product Product.
4020 + *
4021 + * @return float
4022 + */
4023 + public static function get_currency_base_price( $price, $product = null ) {
4024 + return apply_filters( 'rtsb/convert/currency/price', $price, $product );
4025 + }
4026 + /**
4027 + * Generate a signed URL with a payload.
4028 + *
4029 + * @param array $payload Associative data to encode.
4030 + * @param string $base_url Base URL to append ?key=... (e.g. wc_get_checkout_url()).
4031 + * @param string $key Key name to use in the URL.
4032 + * @return string Signed URL with key parameter.
4033 + */
4034 + public static function generate_signed_url( array $payload, string $base_url, $key = 'key' ) {
4035 + if ( empty( $key ) ) {
4036 + $key = 'key';
4037 + }
4038 + // Convert payload to JSON.
4039 + $data = wp_json_encode( $payload );
4040 + // Create signature.
4041 + $signature = wp_hash( $data );
4042 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
4043 + $encode_key = base64_encode( $data . '::' . $signature );
4044 + // Return full URL with key param.
4045 + return add_query_arg( [ $key => rawurlencode( $encode_key ) ], $base_url );
4046 + }
4047 +
4048 + /**
4049 + * Decode and verify a signed URL key.
4050 + *
4051 + * @param string $key Encoded key from URL.
4052 + * @return array|false Decoded payload array on success, false on failure.
4053 + */
4054 + public static function decode_signed_key( string $key ) {
4055 + if ( empty( $key ) ) {
4056 + return false;
4057 + }
4058 + $key = rawurldecode( wp_unslash( $key ) );
4059 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
4060 + $raw = base64_decode( sanitize_text_field( $key ) );
4061 + if ( ! $raw ) {
4062 + return false;
4063 + }
4064 + $parts = explode( '::', $raw, 2 );
4065 + if ( count( $parts ) !== 2 ) {
4066 + return false;
4067 + }
4068 + list( $data_json, $signature ) = $parts;
4069 + // Validate signature.
4070 + if ( ! hash_equals( wp_hash( $data_json ), $signature ) ) {
4071 + return false;
4072 + }
4073 + $payload = json_decode( $data_json, true );
4074 + return is_array( $payload ) ? $payload : false;
4075 + }
4076 +
4077 + /**
4078 + * Get Loco Translate MO file path for a plugin.
4079 + *
4080 + * @param string $textdomain Plugin textdomain.
4081 + * @return void|false
4082 + */
4083 + public static function load_loco_textdomain( $textdomain ) {
4084 + if ( ! function_exists( 'loco_plugin_version' ) ) {
4085 + return false;
4086 + }
4087 + $lang = WP_LANG_DIR;
4088 + $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4089 + if ( ! file_exists( $path ) && defined( 'LOCO_LANG_DIR' ) ) {
4090 + $lang = LOCO_LANG_DIR;
4091 + $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4092 + }
4093 + if ( ! file_exists( $path ) ) {
4094 + if ( 'shopbuilder' === $textdomain ) {
4095 + $plugin_root = dirname( RTSB_FILE );
4096 + } elseif ( 'shopbuilder-pro' === $textdomain ) {
4097 + $plugin_root = dirname( RTSBPRO_FILE );
4098 + } else {
4099 + return false;
4100 + }
4101 + $path = $plugin_root . '/languages/' . $textdomain . '-' . get_locale() . '.mo';
4102 + }
4103 + if ( ! file_exists( $path ) ) {
4104 + return false;
4105 + }
4106 + load_textdomain( $textdomain, $path );
2944 4107 }
2945 4108 }