PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.2.5
ShopBuilder – WooCommerce Builder For Elementor v3.2.5
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 +1555 -154 2.1.63.2.5 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,8 +36,22 @@
28 36 /**
29 37 * @var array
30 38 */
31 39 private static $cache = [];
40 + /**
41 + * Check if the stored license is valid.
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 /**
34 56 * Verify nonce.
35 57 *
@@ -45,12 +67,118 @@
45 67 return false;
46 68 }
47 69
48 70 /**
49 - * @param $plugin_slug
71 + * Get nonce.
50 72 *
73 + * @return string|null
74 + */
75 + public static function enable_loader() {
76 + return 'on' !== self::get_option( 'general', 'optimization', 'remove_pre_loader', '' );
77 + }
78 + /**
79 + * Get nonce.
80 + *
81 + * @return string|null
82 + */
83 + public static function content_invisible() {
84 + $wp_rocket_compatibility = 'on' !== self::get_option( 'general', 'optimization', 'wp_rocket_compatibility', '' );
85 + if ( $wp_rocket_compatibility ) {
86 + return true;
87 + }
88 + if ( defined( 'WP_ROCKET_VERSION' ) ) {
89 + return false;
90 + }
91 + return true;
92 + }
93 + /**
94 + * Get nonce.
95 + *
96 + * @return string|null
97 + */
98 + public static function get_nonce() {
99 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
100 + return isset( $_REQUEST[ rtsb()->nonceId ] ) ? sanitize_text_field( $_REQUEST[ rtsb()->nonceId ] ) : null;
101 + }
102 +
103 + /**
104 + * Set Session
105 + *
106 + * @param string $name Name.
107 + * @param mixed $value Value.
108 + */
109 + public static function setSession( $name, $value ) {
110 + if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
111 + session_start();
112 + $_SESSION[ $name ] = $value;
113 + } else {
114 + $_SESSION[ $name ] = $value;
115 + }
116 + }
117 +
118 + /**
119 + * Get Cookie or Session
120 + *
121 + * @param string $name Name.
122 + *
51 123 * @return bool
52 124 */
125 + public static function getSession( $name ) {
126 + if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
127 + session_start();
128 + }
129 + return $_SESSION[ $name ] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
130 + }
131 + /**
132 + * Remove Session Variable
133 + *
134 + * @param string $name The name of the session variable to remove.
135 + */
136 + public static function removeSession( $name ) {
137 + if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
138 + session_start();
139 + unset( $_SESSION[ $name ] );
140 + } else {
141 + unset( $_SESSION[ $name ] );
142 + }
143 + }
144 + /**
145 + * Gets the current timestamp in WordPress timezone.
146 + *
147 + * @return int Current timestamp.
148 + */
149 + public static function currentTimestampUTC() {
150 + $wp_timezone = wp_timezone();
151 + $now = new DateTime( 'now', $wp_timezone ); // Current time in WP timezone.
152 + return $now->getTimestamp();
153 + }
154 + /**
155 + * Action.
156 + *
157 + * @param string $action action.
158 + * @return void
159 + */
160 + public static function add_to_scheduled_hook_list( $action ) {
161 + if ( empty( $action ) ) {
162 + return;
163 + }
164 + $schedule = get_option( 'rtsb_cron_schedule_free', [] );
165 + $schedule[] = $action;
166 + update_option( 'rtsb_cron_schedule_free', array_unique( $schedule ) );
167 + }
168 + /**
169 + * @return DB
170 + */
171 + public static function DB() {
172 + return new DB( 'wpdb' );
173 + }
174 + /**
175 + * Check if a plugin is installed.
176 + *
177 + * @param string $plugin_slug Plugin slug.
178 + *
179 + * @return bool
180 + */
53 181 public static function check_plugin_installed( $plugin_slug ): bool {
54 182 $installed_plugins = get_plugins();
55 183
56 184 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
@@ -56,10 +184,12 @@
56 184 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
57 185 }
58 186
59 187 /**
60 - * @param $plugin_slug
188 + * Check if a plugin is active.
61 189 *
190 + * @param string $plugin_slug Plugin slug.
191 + *
62 192 * @return bool
63 193 */
64 194 public static function check_plugin_active( $plugin_slug ): bool {
65 195 if ( is_plugin_active( $plugin_slug ) ) {
@@ -67,21 +197,71 @@
67 197 }
68 198
69 199 return false;
70 200 }
201 + /**
202 + * Get all user roles.
203 + *
204 + * @return array
205 + */
206 + public static function get_current_user_roles() {
207 + $current_user = wp_get_current_user();
208 + return $current_user->roles;
209 + }
210 + /**
211 + * Get all user roles.
212 + *
213 + * @return array|false|mixed
214 + */
215 + public static function get_all_user_roles() {
216 + $cache_key = 'rtsb_get_user_roles';
217 + $roles = wp_cache_get( $cache_key, 'shopbuilder' );
71 218
219 + if ( empty( $roles ) ) {
220 + if ( ! function_exists( 'get_editable_roles' ) ) {
221 + require_once ABSPATH . 'wp-admin/includes/user.php';
222 + }
223 +
224 + $user_roles = \get_editable_roles();
225 + $roles = [];
226 +
227 + foreach ( $user_roles as $key => $role ) {
228 + if ( empty( $role['capabilities'] ) ) {
229 + continue;
230 + }
231 + $roles[ $key ] = $role['name'];
232 + }
233 + }
234 + wp_cache_set( $cache_key, $roles, 'shopbuilder' );
235 + Cache::set_data_cache_key( $cache_key );
236 + return $roles;
237 + }
238 +
72 239 /**
73 - * @param $data
240 + * Stripslashes value.
74 241 *
242 + * @param mixed $data Data.
243 + *
75 244 * @return mixed|string
76 245 */
77 246 public static function stripslashes_value( $data ) {
78 - if ( is_string( $data ) && strpos( $data, '\\' ) !== false ) {
79 - return stripslashes( $data );
80 - } elseif ( is_array( $data ) ) {
247 + if ( is_array( $data ) ) {
81 248 foreach ( $data as $key => $value ) {
82 249 $data[ $key ] = self::stripslashes_value( $value );
83 250 }
251 + } elseif ( is_string( $data ) ) {
252 + $trimmed = trim( $data );
253 + // Try to decode JSON.
254 + $json = json_decode( $trimmed, true );
255 + if ( json_last_error() === JSON_ERROR_NONE && is_array( $json ) ) {
256 + // Recursively stripslashes on decoded array.
257 + $json = self::stripslashes_value( $json );
258 + return wp_json_encode( $json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
259 + }
260 + // Not valid JSON, just stripslashes if needed.
261 + if ( strpos( $data, '\\' ) !== false ) {
262 + return stripslashes( $data );
263 + }
84 264 }
85 265
86 266 return $data;
87 267 }
@@ -86,10 +266,12 @@
86 266 return $data;
87 267 }
88 268
89 269 /**
90 - * @param $string
270 + * Multiselect settings field value
91 271 *
272 + * @param string $string String.
273 + *
92 274 * @return array
93 275 */
94 276 public static function multiselect_settings_field_value( $string ) {
95 277
@@ -109,8 +291,10 @@
109 291 return $values;
110 292 }
111 293
112 294 /**
295 + * Check if is block theme
296 + *
113 297 * @return bool
114 298 */
115 299 public static function check_is_block_theme(): bool {
116 300 global $wp_version;
@@ -118,16 +302,19 @@
118 302 return version_compare( $wp_version, '5.9', '>=' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
119 303 }
120 304
121 305 /**
306 + * Locate template
307 + *
122 308 * @param string $template_name Template name.
123 309 * @param string $template_path Template path. (default: '').
124 - * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin
310 + * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin.
125 311 *
126 312 * @return mixed|void
127 313 */
128 314 public static function locate_template( $template_name, $template_path = '', $plugin_path = '' ) {
129 - $template_name = $template_name . '.php';
315 + $template_name = self::sanitize_file_name( $template_name ) . '.php';
316 +
130 317 if ( ! $template_path ) {
131 318 $template_path = rtsb()->get_template_path();
132 319 }
133 320 if ( ! $plugin_path ) {
@@ -149,12 +336,12 @@
149 336 )
150 337 );
151 338
152 339 if ( ! $located ) {
153 - if ( file_exists( $plugin_path ) ) {
340 + if ( $pro_plugin_path && rtsb()->has_pro() && file_exists( $pro_plugin_path ) ) {
341 + return apply_filters( 'rtsb/core/locate_template', $pro_plugin_path, $template_name );
342 + } elseif ( file_exists( $plugin_path ) ) {
154 343 return apply_filters( 'rtsb/core/locate_template', $plugin_path, $template_name );
155 - } elseif ( $pro_plugin_path && rtsb()->has_pro() && file_exists( $pro_plugin_path ) ) {
156 - return apply_filters( 'rtsb/core/locate_template', $pro_plugin_path, $template_name );
157 344 }
158 345 }
159 346
160 347 /**
@@ -170,8 +357,19 @@
170 357 return apply_filters( 'rtsb/core/locate_template', $located, $template_name );
171 358 }
172 359
173 360 /**
361 + * Remove any character that is not alphanumeric, /, _, or -.
362 + *
363 + * @param string $name Name to sanitize.
364 + *
365 + * @return array|string|string[]|null
366 + */
367 + private static function sanitize_file_name( $name ) {
368 + return preg_replace( '/[^a-zA-Z0-9\/_\-]/', '', $name );
369 + }
370 +
371 + /**
174 372 * Template Content
175 373 *
176 374 * @param string $template_name Template name.
177 375 * @param array $args Arguments. (default: array).
@@ -180,11 +378,20 @@
180 378 * @param string $plugin_path Fallback path from where file will load if fail to load from template. (default: '').
181 379 *
182 380 * @return false|string|void
183 381 */
184 - public static function load_template( $template_name, array $args = null, $return = false, $template_path = '', $plugin_path = '' ) {
185 - $located = self::locate_template( $template_name, $template_path, $plugin_path );
186 -
382 + public static function load_template( $template_name, $args = null, $return = false, $template_path = '', $plugin_path = '' ) {
383 + $cache_key = sanitize_key( implode( '-', [ 'template', $template_name, $template_path ] ) );
384 + $located = (string) wp_cache_get( $cache_key, 'shopbuilder' );
385 + if ( ! $located ) {
386 + $located = self::locate_template( $template_name, $template_path, $plugin_path );
387 + // Don't cache the absolute path so that it can be shared between web servers with different paths.
388 + $cache_path = wc_tokenize_path( $located, wc_get_path_define_tokens() );
389 + Cache::set_template_cache( $cache_key, $cache_path );
390 + } else {
391 + // Make sure that the absolute path to the template is resolved.
392 + $located = wc_untokenize_path( $located, wc_get_path_define_tokens() );
393 + }
187 394 if ( ! file_exists( $located ) ) {
188 395 // translators: %s template.
189 396 self::doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'shopbuilder' ), '<code>' . $located . '</code>' ), '1.0' );
190 397
@@ -192,9 +399,9 @@
192 399 }
193 400
194 401 if ( ! empty( $args ) && is_array( $args ) ) {
195 402 $atts = $args;
196 - extract( $args ); // @codingStandardsIgnoreLine
403 + extract( $args ); // @codingStandardsIgnoreLine
197 404 }
198 405
199 406 // Allow 3rd party plugin filter template file from their plugin.
200 407 $located = apply_filters( 'rtsb/core/get_template', $located, $template_name, $args );
@@ -309,9 +516,9 @@
309 516 // Return the Builder Template id.
310 517
311 518 if ( get_post_type( get_the_ID() ) == BuilderFns::$post_type_tb ) {
312 519 $product_id = get_post_meta( get_the_ID(), BuilderFns::$product_template_meta, true );
313 - if ( $product_id ) {
520 + if ( $product_id && 'product' === get_post_type( $product_id ) && get_post_status( $product_id ) ) {
314 521 return $product_id;
315 522 }
316 523 }
317 524
@@ -316,15 +523,16 @@
316 523 }
317 524
318 525 global $wpdb;
319 526 $cache_key = 'rtsb_prepared_product_id';
320 - $_post_id = wp_cache_get( $cache_key );
321 -
527 + $_post_id = wp_cache_get( $cache_key, 'shopbuilder' );
322 528 if ( false === $_post_id || 'publish' !== get_post_status( $_post_id ) ) {
529 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
323 530 $_post_id = $wpdb->get_var(
324 531 $wpdb->prepare( "SELECT MAX(ID) FROM {$wpdb->prefix}posts WHERE post_type = %s AND post_status IN('publish', 'draft')", 'product' )
325 532 );
326 - wp_cache_set( $cache_key, $_post_id, '', 12 * HOUR_IN_SECONDS );
533 + wp_cache_set( $cache_key, $_post_id, 'shopbuilder', 12 * HOUR_IN_SECONDS );
534 + Cache::set_data_cache_key( $cache_key );
327 535 }
328 536
329 537 return $_post_id;
330 538 }
@@ -337,9 +545,8 @@
337 545 public static function get_product() {
338 546 global $product;
339 547
340 548 if ( is_singular( 'product' ) && $product instanceof WC_Product ) {
341 - // do_action( 'rtsb_before_product_template_render' );.
342 549 return $product;
343 550 }
344 551 $cache_key = 'prepared_product_for_preview';
345 552 if ( isset( self::$cache[ $cache_key ] ) ) {
@@ -360,10 +567,11 @@
360 567 *
361 568 * @return void
362 569 */
363 570 public static function doing_it_wrong( $function, $message, $version ) {
571 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
364 572 $message .= ' Backtrace: ' . wp_debug_backtrace_summary();
365 - _doing_it_wrong( $function, $message, $version );
573 + _doing_it_wrong( esc_html( $function ), wp_kses_post( $message ), esc_html( $version ) );
366 574 }
367 575
368 576 /**
369 577 * @return array
@@ -379,8 +587,15 @@
379 587
380 588 return $pages;
381 589 }
382 590
591 + /**
592 + * Get section items
593 + *
594 + * @param string $section_id Section ID.
595 + *
596 + * @return array
597 + */
383 598 public static function get_section_items( $section_id ) {
384 599 if ( ! $section_id ) {
385 600 return [];
386 601 }
@@ -387,8 +602,16 @@
387 602
388 603 return DataModel::source()->get_option( $section_id, [] );
389 604 }
390 605
606 + /**
607 + * Get options items
608 + *
609 + * @param string $section_id Section ID.
610 + * @param string $item_id Item ID.
611 + *
612 + * @return array
613 + */
391 614 public static function get_options( $section_id, $item_id ) {
392 615 if ( ! $section_id || ! $item_id ) {
393 616 return [];
394 617 }
@@ -399,11 +622,11 @@
399 622
400 623 /**
401 624 * Get options value with default value if options doesn't exist.
402 625 *
403 - * @param $group
404 - * @param $option_key
405 - * @param $default_value
626 + * @param array $group Group.
627 + * @param string $option_key Option key.
628 + * @param string $default_value Default value.
406 629 *
407 630 * @return mixed|string
408 631 */
409 632 public static function get_options_by_default_val( $group, $option_key, $default_value = '' ) {
@@ -415,32 +638,34 @@
415 638 return $group[ $option_key ];
416 639 }
417 640
418 641 /**
419 - * @param string $section_id
420 - * @param string $item_id
421 - * @param string $option_id
422 - * @param null $default EXCEPT multi_checkbox you can provide default value if given option does not set any value
423 - * @param null $type checkbox, multi_checkbox, number
642 + * Get option.
424 643 *
644 + * @param string $section_id Section ID.
645 + * @param string $item_id Item ID.
646 + * @param string $option_id Option ID.
647 + * @param null $default EXCEPT multi_checkbox you can provide default value if given option does not set any value.
648 + * @param null $type checkbox, multi_checkbox, number.
649 + *
425 650 * @return bool|int|mixed|null
426 651 */
427 652 public static function get_option( $section_id, $item_id, $option_id, $default = null, $type = null ) {
428 653 $options = self::get_options( $section_id, $item_id );
429 654
430 - if ( $type === 'checkbox' ) {
655 + if ( 'checkbox' === $type ) {
431 656 if ( isset( $options[ $option_id ] ) ) {
432 - return $options[ $option_id ] === 'on';
657 + return 'on' === $options[ $option_id ];
433 658 }
434 659
435 660 return $default;
436 - } elseif ( $type === 'multi_checkbox' ) {
437 - return isset( $options[ $option_id ] ) && is_array( $options[ $option_id ] ) && in_array( $default, $options[ $option_id ] );
438 - } elseif ( $type === 'number' ) {
661 + } elseif ( 'multi_checkbox' === $type ) {
662 + return isset( $options[ $option_id ] ) && is_array( $options[ $option_id ] ) && in_array( $default, $options[ $option_id ] ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
663 + } elseif ( 'number' === $type ) {
439 664 return isset( $options[ $option_id ] ) ? absint( $options[ $option_id ] ) : absint( $default );
440 665 }
441 666
442 - return isset( $options[ $option_id ] ) && ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
667 + return ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
443 668 }
444 669
445 670
446 671 /**
@@ -494,13 +719,14 @@
494 719 }
495 720
496 721 if ( strlen( $page_content ) > 0 ) {
497 722 // Search for an existing page with the specified page content (typically a shortcode).
498 - $shortcode = str_replace( [ '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ], '', $page_content );
723 + $shortcode = str_replace( [ '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ], '', $page_content );
724 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
499 725 $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}%" ) );
500 726 } else {
501 727 // Search for an existing page with the specified page slug.
502 - $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 ) );
728 + $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
503 729 }
504 730
505 731 $valid_page_found = apply_filters( 'rtsb/core/create_page_id', $valid_page_found, $slug, $page_content, $options );
506 732
@@ -522,12 +748,12 @@
522 748
523 749 // Search for a matching valid trashed page.
524 750 if ( strlen( $page_content ) > 0 ) {
525 751 // Search for an existing page with the specified page content (typically a shortcode).
526 - $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_content LIKE %s LIMIT 1;", "%{$page_content}%" ) );
752 + $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_content LIKE %s LIMIT 1;", "%{$page_content}%" ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
527 753 } else {
528 754 // Search for an existing page with the specified page slug.
529 - $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_name = %s LIMIT 1;", $slug ) );
755 + $trashed_page_found = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_type='page' AND post_status = 'trash' AND post_name = %s LIMIT 1;", $slug ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
530 756 }
531 757
532 758 if ( $trashed_page_found ) {
533 759 $page_id = $trashed_page_found;
@@ -566,8 +792,13 @@
566 792
567 793 return $page_id;
568 794 }
569 795
796 + /**
797 + * Get allowed HTML tags.
798 + *
799 + * @return array
800 + */
570 801 public static function get_kses_array() {
571 802 return [
572 803 'a' => [
573 804 'class' => [],
@@ -666,13 +897,14 @@
666 897 ];
667 898 }
668 899
669 900
670 - /*
901 + /**
671 902 * Escape output of wishlist icon
672 903 *
673 904 * @param string $data Data to escape.
674 - * @return string Escaped data
905 + *
906 + * @return void
675 907 */
676 908 public static function print_icon( $data ) {
677 909 /**
678 910 * APPLY_FILTERS: rtsb/core/allowed_icon_html
@@ -756,9 +988,9 @@
756 988 * @return mixed
757 989 */
758 990 public static function allowedHtml( $level = 'basic' ) {
759 991 $allowed_html = [];
760 - // TODO:: Need Optimize.
992 +
761 993 switch ( $level ) {
762 994 case 'basic':
763 995 $allowed_html = [
764 996 'b' => [
@@ -898,13 +1130,15 @@
898 1130 return $allowed_html;
899 1131 }
900 1132
901 1133 /**
902 - * Safe print a validated HTML tag.
1134 + * Safe get a validated HTML tag.
903 1135 *
904 - * @param string $tag
1136 + * @param string $tag HTML tag.
1137 + *
1138 + * @return string
905 1139 */
906 - public static function print_validated_html_tag( $tag ) {
1140 + public static function get_validated_html_tag( $tag ) {
907 1141 $allowed_html_wrapper_tags = [
908 1142 'a',
909 1143 'article',
910 1144 'aside',
@@ -924,12 +1158,23 @@
924 1158 'section',
925 1159 'span',
926 1160 ];
927 1161
928 - self::print_html( in_array( strtolower( $tag ), $allowed_html_wrapper_tags, true ) ? $tag : 'div' );
1162 + return in_array( strtolower( $tag ), $allowed_html_wrapper_tags, true ) ? $tag : 'div';
929 1163 }
930 1164
931 1165 /**
1166 + * Safe print a validated HTML tag.
1167 + *
1168 + * @param string $tag HTML tag.
1169 + *
1170 + * @return void
1171 + */
1172 + public static function print_validated_html_tag( $tag ) {
1173 + self::print_html( self::get_validated_html_tag( $tag ) );
1174 + }
1175 +
1176 + /**
932 1177 * Insert Some array element
933 1178 *
934 1179 * @param [type] $key The elements will insert nearby this key.
935 1180 * @param [type] $main_array Original array.
@@ -992,11 +1237,17 @@
992 1237
993 1238 /**
994 1239 * Free Layouts
995 1240 *
1241 + * @param string $layout Layout.
1242 + *
996 1243 * @return array
997 1244 */
998 - public static function free_layouts() {
1245 + public static function free_layouts( $layout = '' ) {
1246 + if ( ! empty( $layout ) && false !== strpos( $layout, 'rtsb-' ) ) {
1247 + return [ $layout => esc_html__( 'Addon Layout', 'shopbuilder' ) ];
1248 + }
1249 +
999 1250 $layouts = [
1000 1251 'grid-layout1' => esc_html__( 'Grid Layout 1', 'shopbuilder' ),
1001 1252 'grid-layout2' => esc_html__( 'Grid Layout 2', 'shopbuilder' ),
1002 1253 'list-layout1' => esc_html__( 'List Layout 1', 'shopbuilder' ),
@@ -1029,9 +1280,14 @@
1029 1280 if ( isset( self::$cache[ $cache_key ] ) ) {
1030 1281 return self::$cache[ $cache_key ];
1031 1282 }
1032 1283
1033 - $termList = get_terms( [ $taxonomy ], [ 'hide_empty' => 0 ] );
1284 + $termList = get_terms(
1285 + [
1286 + 'taxonomy' => $taxonomy,
1287 + 'hide_empty' => false,
1288 + ]
1289 + );
1034 1290
1035 1291 if ( is_array( $termList ) && ! empty( $termList ) && empty( $termList['errors'] ) ) {
1036 1292 if ( $placeholder ) {
1037 1293 $terms = [
@@ -1045,8 +1301,25 @@
1045 1301 }
1046 1302 self::$cache[ $cache_key ] = $terms;
1047 1303 return $terms;
1048 1304 }
1305 + /**
1306 + * Get all product attribute taxonomy by id
1307 + *
1308 + * @param array $term_ids Term id.
1309 + *
1310 + * @return array
1311 + */
1312 + public static function tax_filter_attr_taxonomy( $term_ids ) {
1313 + $taxonomies = [];
1314 + foreach ( $term_ids as $id ) {
1315 + $term = get_term( $id );
1316 + if ( ! is_wp_error( $term ) && $term ) {
1317 + $taxonomies[] = $term->taxonomy;
1318 + }
1319 + }
1320 + return array_unique( $taxonomies );
1321 + }
1049 1322
1050 1323 /**
1051 1324 * Get all terms by attributes
1052 1325 *
@@ -1060,9 +1333,9 @@
1060 1333
1061 1334 if ( $attributes ) {
1062 1335 foreach ( $attributes as $tax ) {
1063 1336 if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
1064 - $termList[ $tax->attribute_name ] = get_terms( wc_attribute_taxonomy_name( $tax->attribute_name ), 'orderby=name&hide_empty=0' );
1337 + $termList[ $tax->attribute_name ] = get_terms( wc_attribute_taxonomy_name( $tax->attribute_name ) );
1065 1338 }
1066 1339 }
1067 1340 }
1068 1341
@@ -1158,9 +1431,8 @@
1158 1431 if ( ! is_admin() ) {
1159 1432 return [];
1160 1433 }
1161 1434
1162 - // TODO:: Return Slug always true for all settings.
1163 1435 $term_list = [];
1164 1436 $args = [
1165 1437 'taxonomy' => $taxonomy,
1166 1438 'hide_empty' => false,
@@ -1230,9 +1502,9 @@
1230 1502
1231 1503 if ( $args['show_rating_count'] ) {
1232 1504 $count .= '<div class="rtsb-count">';
1233 1505 $count .= sprintf(
1234 - /* translators: %s is the number of reviews */
1506 + /* translators: %s is the number of reviews */
1235 1507 _n( '%s Review', '%s Reviews', $rating_count, 'shopbuilder' ),
1236 1508 $rating_count
1237 1509 );
1238 1510 $count .= '</div>';
@@ -1258,8 +1530,28 @@
1258 1530 self::print_html( $html, true );
1259 1531 }
1260 1532
1261 1533 /**
1534 + * Get product simple rating.
1535 + *
1536 + * @return void
1537 + */
1538 + public static function get_product_simple_rating_html() {
1539 + global $product;
1540 + $html = null;
1541 + $rating_count = $product->get_rating_count();
1542 + $average_rating = $product->get_average_rating();
1543 + $html .= '<div class="product-rating">';
1544 + $html .= wc_get_rating_html( $average_rating, $rating_count );
1545 + $html .= ! empty( $html ) ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1546 + $html .= '</div>';
1547 +
1548 + self::print_html( $html, true );
1549 + }
1550 +
1551 +
1552 +
1553 + /**
1262 1554 * Text truncation.
1263 1555 *
1264 1556 * @param string $text_to_truncate Text.
1265 1557 * @param int $limit Limit.
@@ -1355,9 +1647,42 @@
1355 1647
1356 1648 <?php
1357 1649 self::print_html( ob_get_clean() );
1358 1650 }
1651 + /**
1652 + * Brands HTML
1653 + *
1654 + * @param int $id Product ID.
1655 + * @param string $class Custom class.
1656 + *
1657 + * @return void|string
1658 + */
1659 + public static function get_brands_list( $id, $class = 'rtsb-brand-outline' ) {
1660 + $terms = get_the_terms( $id, 'product_brand' );
1661 + if ( empty( $id ) || empty( $terms ) || is_wp_error( $terms ) ) {
1662 + return '';
1663 + }
1664 + ob_start();
1665 + ?>
1359 1666
1667 + <ul class="rtsb-brand-list <?php echo esc_attr( $class ); ?>">
1668 + <?php
1669 + $brand_list = get_the_term_list(
1670 + $id,
1671 + 'product_brand',
1672 + '<li class="rtsb-brand-list-item">',
1673 + '</li><li class="rtsb-brand-list-item">',
1674 + '</li>'
1675 + );
1676 +
1677 + self::print_html( $brand_list );
1678 + ?>
1679 + </ul>
1680 +
1681 + <?php
1682 + self::print_html( ob_get_clean() );
1683 + }
1684 +
1360 1685 /**
1361 1686 * Get Featured Image HTML.
1362 1687 *
1363 1688 * @param string $type Image type.
@@ -1460,9 +1785,9 @@
1460 1785
1461 1786 if ( ! $img_html ) {
1462 1787 $hwstring = image_hwstring( 160, 160 );
1463 1788 $attr = isset( $attr['src'] ) ? apply_filters( 'wp_get_attachment_image_attributes', $attr, false, $f_img_size ) : [];
1464 - $attr['class'] = 'default-img';
1789 + $attr['class'] = 'default-img ' . $hover_class;
1465 1790 $attr['src'] = esc_url( rtsb()->get_assets_uri( 'images/demo.png' ) );
1466 1791 $attr['alt'] = esc_html__( 'Default Image', 'shopbuilder' );
1467 1792 $img_html = rtrim( "<img $hwstring" );
1468 1793
@@ -1553,9 +1878,9 @@
1553 1878 if ( empty( $control['value'] ) ) {
1554 1879 return '';
1555 1880 }
1556 1881 if ( is_array( $control['value'] ) ) {
1557 - $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) );
1882 + $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1558 1883 } else {
1559 1884 $cache_key = 'icons_managet_' . str_replace( ' ', '', $control['value'] );
1560 1885 }
1561 1886 if ( isset( self::$cache[ $cache_key ] ) ) {
@@ -1591,9 +1916,13 @@
1591 1916 public static function get_product_swatches( $type ) {
1592 1917 ?>
1593 1918 <div class="rtsb-swatches <?php echo esc_attr( $type ); ?>-layout">
1594 1919 <?php
1595 - do_action( 'rtwpvs_show_archive_variation' );
1920 + if ( class_exists( 'Rtwpvsp' ) ) {
1921 + do_action( 'rtwpvs_show_archive_variation' );
1922 + } else {
1923 + do_action( 'rtsb/vs/showcase/variation' );
1924 + }
1596 1925 ?>
1597 1926 </div>
1598 1927 <?php
1599 1928 }
@@ -1690,47 +2019,35 @@
1690 2019 public static function calculate_sale_percentage( $product = null ) {
1691 2020 if ( is_null( $product ) ) {
1692 2021 global $product;
1693 2022 }
1694 -
1695 2023 if ( ! $product instanceof WC_Product ) {
1696 2024 return '';
1697 2025 }
1698 -
1699 2026 if ( ! $product->is_on_sale() ) {
1700 2027 return '';
1701 2028 }
1702 -
1703 - $percentage = null;
1704 - $max_percentage = '';
1705 -
2029 + $max_percentage = 0;
1706 2030 if ( $product->is_type( 'simple' ) ) {
1707 - $max_percentage = ( ( $product->get_regular_price() - $product->get_sale_price() ) / $product->get_regular_price() ) * 100;
2031 + $regular_price = (float) $product->get_regular_price();
2032 + $sale_price = (float) $product->get_sale_price();
2033 + if ( $regular_price > 0 && $sale_price > 0 ) {
2034 + $max_percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2035 + }
1708 2036 } elseif ( $product->is_type( 'variable' ) ) {
1709 - $max_percentage = 0;
2037 + $prices = $product->get_variation_prices( true );
2038 + if ( ! empty( $prices['regular_price'] ) && ! empty( $prices['sale_price'] ) ) {
2039 + foreach ( $prices['regular_price'] as $key => $regular_price ) {
2040 + $sale_price = $prices['sale_price'][ $key ];
1710 2041
1711 - foreach ( $product->get_children() as $child_id ) {
1712 -
1713 - $variation = wc_get_product( $child_id );
1714 -
1715 - $price = $variation->get_regular_price();
1716 - $sale = $variation->get_sale_price();
1717 -
1718 - if ( 0 !== $price && ! empty( $sale ) ) {
1719 - $percentage = ( $price - $sale ) / $price * 100;
2042 + if ( $regular_price > 0 && $sale_price > 0 && $regular_price > $sale_price ) {
2043 + $percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2044 + $max_percentage = max( $max_percentage, $percentage );
2045 + }
1720 2046 }
1721 -
1722 - if ( $percentage > $max_percentage ) {
1723 - $max_percentage = $percentage;
1724 - }
1725 2047 }
1726 2048 }
1727 -
1728 - if ( $max_percentage > 0 ) {
1729 - return round( $max_percentage );
1730 - }
1731 -
1732 - return 0;
2049 + return $max_percentage > 0 ? round( $max_percentage ) : 0;
1733 2050 }
1734 2051
1735 2052 /**
1736 2053 * Pagination
@@ -1741,21 +2058,21 @@
1741 2058 *
1742 2059 * @return string
1743 2060 */
1744 2061 public static function custom_pagination( $pages = '', $range = 4, $ajax = false ) {
1745 - $html = null;
1746 - $showitems = ( $range * 2 ) + 1;
2062 + $html = null;
2063 + $visible_range = apply_filters( 'rtsb/elements/pagination/visible_range', ( $range * 2 ) + 1 );
1747 2064
1748 2065 global $paged;
1749 2066
1750 2067 if ( is_front_page() ) {
1751 - $paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1;
2068 + $paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1752 2069 } else {
1753 - $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
2070 + $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1754 2071 }
1755 2072
1756 2073 if ( empty( $paged ) ) {
1757 - $paged = 1;
2074 + $paged = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1758 2075 }
1759 2076
1760 2077 if ( '' === $pages ) {
1761 2078 global $wp_query;
@@ -1772,8 +2089,9 @@
1772 2089
1773 2090 if ( $ajax ) {
1774 2091 $ajaxClass = ' rtsb-pagination-ajax';
1775 2092 $dataAttr = "data-paged='1'";
2093 + $dataAttr .= ' data-visible-range="' . esc_attr( $visible_range ) . '"';
1776 2094 }
1777 2095
1778 2096 if ( 1 !== $pages ) {
1779 2097 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
@@ -1778,29 +2096,29 @@
1778 2096 if ( 1 !== $pages ) {
1779 2097 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
1780 2098 $html .= '<ul class="pagination-list">';
1781 2099
1782 - if ( $paged > 2 && $paged > $range + 1 && $showitems < $pages ) {
2100 + if ( $paged > 2 && $paged > $visible_range + 1 && $visible_range < $pages ) {
1783 2101 $html .= "<li><a data-paged='1' href='" . get_pagenum_link( 1 ) . "' aria-label='First'>&laquo;</a></li>";
1784 2102 }
1785 2103
1786 - if ( $paged > 1 && $showitems < $pages ) {
2104 + if ( $paged > 1 && $visible_range < $pages ) {
1787 2105 $p = $paged - 1;
1788 2106 $html .= "<li><a data-paged='{$p}' href='" . get_pagenum_link( $p ) . "' aria-label='Previous'>&lsaquo;</a></li>";
1789 2107 }
1790 2108
1791 2109 for ( $i = 1; $i <= $pages; $i++ ) {
1792 - if ( 1 != $pages && ( ! ( $i >= $paged + $range + 1 || $i <= $paged - $range - 1 ) || $pages <= $showitems ) ) {
2110 + if ( 1 != $pages && ( ! ( $i >= $paged + $visible_range + 1 || $i <= $paged - $visible_range - 1 ) || $pages <= $visible_range ) ) {
1793 2111 $html .= ( $paged == $i ) ? '<li class="active"><span>' . $i . '</span></li>' : "<li><a data-paged='{$i}' href='" . get_pagenum_link( $i ) . "'>" . $i . '</a></li>';
1794 2112 }
1795 2113 }
1796 2114
1797 - if ( $paged < $pages && $showitems < $pages ) {
2115 + if ( $paged < $pages && $visible_range < $pages ) {
1798 2116 $p = $paged + 1;
1799 2117 $html .= "<li><a data-paged='{$p}' href=\"" . get_pagenum_link( $paged + 1 ) . "\" aria-label='Next'>&rsaquo;</a></li>";
1800 2118 }
1801 2119
1802 - if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $showitems < $pages ) {
2120 + if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $visible_range < $pages ) {
1803 2121 $html .= "<li><a data-paged='{$pages}' href='" . get_pagenum_link( $pages ) . "' aria-label='Last'>&raquo;</a></li>";
1804 2122 }
1805 2123
1806 2124 $html .= '</ul>';
@@ -1912,10 +2230,8 @@
1912 2230 } else {
1913 2231 $class .= ' rtsb-' . esc_attr( str_replace( '_', '-', $type ) );
1914 2232 }
1915 2233
1916 - $html .= '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">';
1917 -
1918 2234 if ( ( 'add_to_cart' === $type ) && ( ! empty( $cart_html ) ) ) {
1919 2235 $html .= $cart_html;
1920 2236 } else {
1921 2237 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
@@ -1920,9 +2236,11 @@
1920 2236 } else {
1921 2237 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
1922 2238 }
1923 2239
1924 - $html .= '</' . esc_attr( $wrapper ) . '>';
2240 + if ( ! empty( $html ) ) {
2241 + $html = '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">' . $html . '</' . esc_attr( $wrapper ) . '>';
2242 + }
1925 2243
1926 2244 return apply_filters( 'rtsb/elements/elementor/get_action_button_by_type', $html );
1927 2245 }
1928 2246
@@ -2023,9 +2341,9 @@
2023 2341 $link['social_network'] = 'Facebook';
2024 2342 $link['social_action'] = 'Share';
2025 2343 break;
2026 2344 case 'twitter':
2027 - $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'] );
2345 + $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'] );
2028 2346 $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>';
2029 2347 $link['attr_title'] = esc_html__( 'Share on Twitter', 'shopbuilder' );
2030 2348 $link['social_network'] = 'Twitter';
2031 2349 $link['social_action'] = 'Tweet';
@@ -2059,9 +2377,9 @@
2059 2377 $link['social_action'] = 'Share';
2060 2378 break;
2061 2379 case 'reddit':
2062 2380 $link['link'] = esc_url( 'https://reddit.com/submit?url=' . $link['url'] . '&title=' . $link['title'] );
2063 - $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>';
2381 + $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>';
2064 2382 $link['attr_title'] = esc_html__( 'Share on Reddit', 'shopbuilder' );
2065 2383 $link['social_network'] = 'Reddit';
2066 2384 $link['social_action'] = 'Share';
2067 2385 break;
@@ -2149,33 +2467,65 @@
2149 2467
2150 2468 /**
2151 2469 * Social Share Platforms.
2152 2470 *
2153 - * @return mixed|null
2471 + * @param string $module Module.
2472 + *
2473 + * @return true|void
2154 2474 */
2155 2475 public static function is_module_active( $module ) {
2156 - $modulelist = ModuleList::instance()->get_data();
2476 + $modulelist = self::get_modules_list();
2477 +
2157 2478 if ( ! empty( $modulelist[ $module ]['active'] ) ) {
2158 2479 return true;
2159 2480 }
2160 2481 }
2482 + /**
2483 + * Checks if catalog pro is active
2484 + *
2485 + * @return boolean
2486 + */
2487 + public static function is_catalog_mode() {
2488 + return function_exists( 'rtsbpro' ) && self::is_module_active( 'catalog_mode' );
2489 + }
2490 + /**
2491 + * Checks if back in stock notifier is active
2492 + *
2493 + * @return boolean
2494 + */
2495 + public static function is_back_in_stock_notifier_enable() {
2496 + return function_exists( 'rtsbpro' ) && self::is_module_active( 'back_in_stock_notifier' );
2497 + }
2161 2498
2499 + /**
2500 + * Elementor Widget Active.
2501 + *
2502 + * @param string $widget Elementor Widget.
2503 + *
2504 + * @return true|void
2505 + */
2506 + public static function is_elementor_widget_active( $widget ) {
2507 + $element_list = self::get_widgets_list();
2162 2508
2509 + if ( ! empty( $element_list[ $widget ]['active'] ) ) {
2510 + return true;
2511 + }
2512 + }
2513 +
2163 2514 /***
2164 2515 * Save Settings data
2165 2516 *
2166 - * @param $section_id
2167 - * @param $block_id
2168 - * @param $rawOptions
2517 + * @param string $section_id Section ID.
2518 + * @param string $block_id Block ID.
2519 + * @param array $rawOptions Raw Options.
2169 2520 *
2170 2521 * @return array
2171 2522 */
2172 - public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) {
2523 + public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) { // phpcs:ignore Generic.Metrics.NestingLevel.TooHigh
2173 2524 $section_id = ! empty( $section_id ) ? sanitize_text_field( wp_unslash( $section_id ) ) : '';
2174 2525 $block_id = ! empty( $block_id ) ? sanitize_text_field( wp_unslash( $block_id ) ) : '';
2175 2526 $rawOptions = ! empty( $rawOptions ) ? $rawOptions : [];
2176 -
2177 - $results = [
2527 + $results = [
2178 2528 'status' => true,
2179 2529 'message' => '',
2180 2530 ];
2181 2531 if ( ! $section_id || ! $block_id ) {
@@ -2197,9 +2547,9 @@
2197 2547 $fields = [];
2198 2548 if ( isset( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) && ! empty( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) ) {
2199 2549 $fields = $sections[ $section_id ]['list'][ $block_id ]['fields'];
2200 2550 }
2201 -
2551 + do_action( 'rtsb/before/save/options', $section_id, $block_id, $rawOptions );
2202 2552 if ( empty( $fields ) ) {
2203 2553 if ( isset( $rawOptions['active'] ) ) {
2204 2554 $changed = true;
2205 2555 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
@@ -2205,21 +2555,49 @@
2205 2555 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
2206 2556 }
2207 2557 } else {
2208 2558 foreach ( $rawOptions as $raw_option_key => $raw_value ) {
2209 - if ( $raw_option_key === 'active' ) {
2559 + if ( 'active' === $raw_option_key ) {
2210 2560 $changed = true;
2211 - $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : '';
2561 + $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : ''; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
2212 2562 } else {
2213 2563 if ( isset( $fields[ $raw_option_key ] ) ) {
2214 2564 $field = $fields[ $raw_option_key ];
2215 - if ( $field['type'] === 'switch' ) {
2565 + if ( 'switch' === $field['type'] ) {
2216 2566 $value = 'on' === $raw_value ? 'on' : '';
2217 2567 } elseif ( 'repeaters' === $field['type'] ) {
2218 - $value = stripslashes_deep( $raw_value );
2568 + $the_value = [];
2569 + $rep_title = [];
2570 + if ( ! empty( $raw_value ) && is_array( $raw_value ) ) {
2571 + foreach ( $raw_value as $key => $value ) {
2572 + if ( is_string( $value ) ) {
2573 + $the_value_decoded = json_decode( stripslashes_deep( $value ) );
2574 + } else {
2575 + $the_value_decoded = $value;
2576 + }
2577 + $cr = $the_value_decoded->title ?? '';
2578 + if ( in_array( $cr, $rep_title, true ) ) {
2579 + continue;
2580 + }
2581 + $rep_title[] = $cr;
2582 + $the_value[] = $the_value_decoded;
2583 + }
2584 + } elseif ( ! empty( $raw_value ) && is_string( $raw_value ) ) {
2585 + $the_value = $raw_value;
2586 + }
2587 + $value = wp_json_encode( $the_value, JSON_UNESCAPED_UNICODE );
2588 + } elseif ( in_array( $field['type'], [ 'product_addons_special_settings', 'checkout_fields' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2589 + $manual_field_value = [];
2590 + if ( is_array( $raw_value ) ) {
2591 + foreach ( $raw_value as $key => $value ) {
2592 + $manual_field_value[] = json_decode( stripslashes( $value ), true );
2593 + }
2594 + $value = wp_json_encode( $manual_field_value, JSON_UNESCAPED_UNICODE );
2595 + } elseif ( is_string( $raw_value ) ) {
2596 + $value = $raw_value;
2597 + }
2219 2598 } else {
2220 - if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) {
2221 -
2599 + if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2222 2600 if ( isset( $raw_value ) && is_array( $raw_value ) ) {
2223 2601 if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2224 2602 $value = array_map( $field['sanitize_fn'], $raw_value );
2225 2603 } else {
@@ -2228,16 +2606,22 @@
2228 2606 } else {
2229 2607 $value = [];
2230 2608 }
2231 2609 } else {
2232 - if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2233 - $value = $field['sanitize_fn']( $raw_value );
2610 + if ( ! empty( $field['sanitize_fn'] ) ) {
2611 + if ( is_callable( $field['sanitize_fn'] ) ) {
2612 + $value = $field['sanitize_fn']( $raw_value );
2613 + } elseif ( 'pass_all' === $field['sanitize_fn'] ) {
2614 + $value = $raw_value;
2615 + } else {
2616 + $value = $field['sanitize_fn']( $raw_value );
2617 + }
2234 2618 } else {
2235 2619 $value = sanitize_text_field( $raw_value );
2236 2620 }
2237 2621 }
2238 2622 }
2239 - $options[ $block_id ][ $raw_option_key ] = $sections[ $section_id ]['list'][ $block_id ]['fields'][ $raw_option_key ]['value'] = $value ?? null;
2623 + $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
2240 2624 $changed = true;
2241 2625 }
2242 2626 }
2243 2627 }
@@ -2257,27 +2641,15 @@
2257 2641
2258 2642 return $results;
2259 2643 }
2260 2644
2261 - /***
2262 - * @param $meta_key
2263 - * @param $meta_value
2645 + /**
2646 + * Count products by taxonomies.
2264 2647 *
2265 - * @return mixed|void
2648 + * @param array $terms Terms.
2649 + * @param string $taxonomy Taxonomy.
2266 2650 *
2267 - *
2268 - * public static function get_post_id_by_meta_key_and_value( $meta_key, $meta_value ) {
2269 - * if( ! $meta_key || ! $meta_value ){
2270 - * return;
2271 - * }
2272 - * global $wpdb;
2273 - * $prepare_guery = $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '%s' and meta_value = '%d'", $meta_key, $meta_value );
2274 - * $the_products = $wpdb->get_col( $prepare_guery );
2275 - * if( count( $the_products ) > 0 ){
2276 - * return $the_products[0];
2277 - * }
2278 - * return;
2279 - * }
2651 + * @return int|void
2280 2652 */
2281 2653 public static function count_products_by_taxonomies( $terms, $taxonomy = 'product_cat' ) {
2282 2654 if ( empty( $terms ) || ! is_array( $terms ) ) {
2283 2655 return;
@@ -2289,8 +2661,10 @@
2289 2661 ];
2290 2662
2291 2663 if ( 'product_cat' === $taxonomy ) {
2292 2664 $args['product_category_id'] = $terms;
2665 + } elseif ( 'product_brand' === $taxonomy ) {
2666 + $args['product_brand_id'] = $terms;
2293 2667 } else {
2294 2668 $args['product_tag_id'] = $terms;
2295 2669 }
2296 2670
@@ -2299,8 +2673,61 @@
2299 2673 return ! empty( $query->get_products() ) ? count( $query->get_products() ) : 0;
2300 2674 }
2301 2675
2302 2676 /**
2677 + * Count products by attribute terms.
2678 + *
2679 + * @param array $term_ids Term IDs.
2680 + * @param string $relation Relation.
2681 + *
2682 + * @return int
2683 + */
2684 + public static function count_products_by_attribute_terms( $term_ids, $relation = 'AND' ) {
2685 + if ( empty( $term_ids ) || ! is_array( $term_ids ) ) {
2686 + return 0;
2687 + }
2688 +
2689 + $terms_by_taxonomy = [];
2690 +
2691 + foreach ( $term_ids as $term_id ) {
2692 + $term = get_term( $term_id );
2693 + if ( ! is_wp_error( $term ) && $term ) {
2694 + if ( ! isset( $terms_by_taxonomy[ $term->taxonomy ] ) ) {
2695 + $terms_by_taxonomy[ $term->taxonomy ] = [];
2696 + }
2697 + $terms_by_taxonomy[ $term->taxonomy ][] = $term_id;
2698 + }
2699 + }
2700 +
2701 + if ( empty( $terms_by_taxonomy ) ) {
2702 + return 0;
2703 + }
2704 +
2705 + $args = [
2706 + 'status' => 'publish',
2707 + 'limit' => -1,
2708 + 'return' => 'ids',
2709 + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2710 + 'relation' => strtoupper( $relation ),
2711 + ],
2712 + ];
2713 +
2714 + foreach ( $terms_by_taxonomy as $taxonomy => $terms ) {
2715 + $args['tax_query'][] = [
2716 + 'taxonomy' => $taxonomy,
2717 + 'field' => 'term_id',
2718 + 'terms' => $terms,
2719 + 'operator' => 'IN',
2720 + ];
2721 + }
2722 +
2723 + $query = new WC_Product_Query( $args );
2724 + $products = $query->get_products();
2725 +
2726 + return count( $products );
2727 + }
2728 +
2729 + /**
2303 2730 * Check if product filters widget has ajax.
2304 2731 *
2305 2732 * @param string $page Page name.
2306 2733 *
@@ -2328,8 +2755,9 @@
2328 2755
2329 2756 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
2330 2757 $ajax[] = isset( $data['settings']['ajax_mode'] ) ? false : true;
2331 2758 }
2759 +
2332 2760 self::$cache[ $cache_key ] = ! empty( $ajax[0] );
2333 2761 return ! empty( $ajax[0] );
2334 2762 }
2335 2763
@@ -2370,33 +2798,88 @@
2370 2798 if ( ! is_admin() ) {
2371 2799 return [];
2372 2800 }
2373 2801
2374 - $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) );
2802 + $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2375 2803
2376 2804 if ( isset( self::$cache[ $cache_key ] ) ) {
2377 2805 return self::$cache[ $cache_key ];
2378 2806 }
2807 + $results = wp_cache_get( $cache_key, 'shopbuilder' );
2808 + if ( ! $results ) {
2809 + global $wpdb;
2810 + $sql = "SELECT t.term_id, t.name
2811 + FROM {$wpdb->terms} t
2812 + JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
2813 + WHERE tt.taxonomy = 'product_cat'";
2379 2814
2380 - $results = wp_cache_get( $cache_key );
2815 + if ( $search_query ) {
2816 + $sql .= ' AND t.name LIKE %s';
2817 + $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2818 + } else {
2819 + $prepared_sql = $sql; // No need for placeholders.
2820 + }
2381 2821
2822 + $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
2823 + // Cache the results in the object cache for future use.
2824 + wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
2825 + Cache::set_data_cache_key( $cache_key );
2826 + }
2827 +
2828 + $cats = [];
2829 + if ( ! is_array( $results ) && ! count( $results ) ) {
2830 + return $cats;
2831 + }
2832 + foreach ( $results as $row ) {
2833 + $category_id = $row->term_id;
2834 + $category_title = $row->name;
2835 +
2836 + $cats[] = [
2837 + 'value' => $category_id,
2838 + 'label' => $category_title,
2839 + ];
2840 + }
2841 +
2842 + // Cache the results in a static variable for the next call.
2843 + self::$cache[ $cache_key ] = $cats;
2844 + return $cats;
2845 + }
2846 + /**
2847 + * Get WooCommerce product categories.
2848 + *
2849 + * @param string|null $search_query The search string for category names.
2850 + *
2851 + * @return array An array of product categories with 'value' and 'label' keys.
2852 + */
2853 + public static function products_tags_query( $search_query = null ) {
2854 + if ( ! is_admin() ) {
2855 + return [];
2856 + }
2857 +
2858 + $cache_key = 'rtsb_product_tags_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2859 +
2860 + if ( isset( self::$cache[ $cache_key ] ) ) {
2861 + return self::$cache[ $cache_key ];
2862 + }
2863 + $results = wp_cache_get( $cache_key, 'shopbuilder' );
2382 2864 if ( ! $results ) {
2383 2865 global $wpdb;
2384 2866 $sql = "SELECT t.term_id, t.name
2385 2867 FROM {$wpdb->terms} t
2386 2868 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
2387 - WHERE tt.taxonomy = 'product_cat'";
2869 + WHERE tt.taxonomy = 'product_tag'";
2388 2870
2389 2871 if ( $search_query ) {
2390 2872 $sql .= ' AND t.name LIKE %s';
2391 - $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // Escaping and wildcard
2873 + $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2392 2874 } else {
2393 2875 $prepared_sql = $sql; // No need for placeholders.
2394 2876 }
2395 2877
2396 - $results = $wpdb->get_results( $prepared_sql );
2878 + $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
2397 2879 // Cache the results in the object cache for future use.
2398 - wp_cache_set( $cache_key, $results ); // Adjust the expiration time as needed.
2880 + wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
2881 + Cache::set_data_cache_key( $cache_key );
2399 2882 }
2400 2883
2401 2884 $cats = [];
2402 2885 if ( ! is_array( $results ) && ! count( $results ) ) {
@@ -2475,8 +2958,9 @@
2475 2958 /**
2476 2959 * Get Post Types.
2477 2960 *
2478 2961 * @param string $search_query Search query.
2962 + * @param array $args Arguments.
2479 2963 *
2480 2964 * @return array
2481 2965 */
2482 2966 public static function get_post_types( $search_query = null, $args = [] ) {
@@ -2499,11 +2983,12 @@
2499 2983 $query_results = get_posts( $args );
2500 2984 foreach ( $query_results as $post ) {
2501 2985 $posts[] = [
2502 2986 'value' => $post->ID,
2503 - 'label' => $post->post_title,
2987 + 'label' => $post->post_title . ' (ID#' . $post->ID . ')',
2504 2988 ];
2505 2989 }
2990 +
2506 2991 if ( $search_query ) {
2507 2992 if ( strpos( '-error 404', $search_query ) ) {
2508 2993 $posts[] = [
2509 2994 'value' => 'error',
@@ -2510,12 +2995,14 @@
2510 2995 'label' => __( '404 Error', 'shopbuilder' ),
2511 2996 ];
2512 2997 }
2513 2998 } else {
2514 - $posts[] = [
2515 - 'value' => 'error',
2516 - 'label' => __( '404 Error', 'shopbuilder' ),
2517 - ];
2999 + if ( 'page' === $args['post_type'] ) {
3000 + $posts[] = [
3001 + 'value' => 'error',
3002 + 'label' => __( '404 Error', 'shopbuilder' ),
3003 + ];
3004 + }
2518 3005 }
2519 3006 } else {
2520 3007 $posts = self::get_registered_post_types( $search_query );
2521 3008 }
@@ -2543,9 +3030,9 @@
2543 3030 public static function renderView( $viewName, $args = [], $return = false ) {
2544 3031 $viewName = str_replace( '.', '/', $viewName );
2545 3032
2546 3033 if ( ! empty( $args ) && is_array( $args ) ) {
2547 - extract( $args );
3034 + extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
2548 3035 }
2549 3036
2550 3037 $view_file = rtsb()->plugin_path() . '/resources/' . $viewName . '.php';
2551 3038
@@ -2567,9 +3054,9 @@
2567 3054
2568 3055 /**
2569 3056 * Best selling product query.
2570 3057 *
2571 - * @param int $minimum_sale Minimum sale/
3058 + * @param int $minimum_sale Minimum sale.
2572 3059 * @param int $limit Total limit.
2573 3060 *
2574 3061 * @return array|mixed
2575 3062 */
@@ -2599,9 +3086,9 @@
2599 3086 ",
2600 3087 $minimum_sale,
2601 3088 $limit
2602 3089 );
2603 - $best_selling = $wpdb->get_col( $sql );
3090 + $best_selling = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
2604 3091 // Cache the result for future use.
2605 3092 set_transient( $cache_key, $best_selling, DAY_IN_SECONDS );
2606 3093 self::$cache[ $cache_key ] = $best_selling;
2607 3094 return $best_selling;
@@ -2627,12 +3114,14 @@
2627 3114
2628 3115 if ( isset( self::$cache[ $cache_key ] ) ) {
2629 3116 return self::$cache[ $cache_key ];
2630 3117 }
3118 + $date_created = $product->get_date_created();
3119 + if ( ! $date_created ) {
3120 + return false;
3121 + }
3122 + $publish_timestamp = strtotime( $date_created->date_i18n( 'Y-m-d H:i:s' ) );
2631 3123
2632 - // Get the product publish date.
2633 - $publish_timestamp = strtotime( $product->get_date_created()->date_i18n( 'Y-m-d H:i:s' ) );
2634 -
2635 3124 // Calculate the difference in days.
2636 3125 $days_difference = abs( time() - $publish_timestamp ) / ( 60 * 60 * 24 );
2637 3126
2638 3127 // Check if the product is considered new.
@@ -2660,12 +3149,9 @@
2660 3149
2661 3150 /**
2662 3151 * Checks if a feature is disabled for guest users based on the provided option.
2663 3152 *
2664 - * @param string $option The option to retrieve from the item.
2665 - * @param mixed $default The default value if the option is not found.
2666 - *
2667 - * @return bool
3153 + * @return void
2668 3154 */
2669 3155 public static function woocommerce_output_all_notices() {
2670 3156 if ( function_exists( 'wc_print_notices' ) ) :
2671 3157 echo '<div class="rtsb-notice">';
@@ -2671,6 +3157,921 @@
2671 3157 echo '<div class="rtsb-notice">';
2672 3158 woocommerce_output_all_notices();
2673 3159 echo '</div>';
2674 3160 endif;
3161 + }
3162 +
3163 + /**
3164 + * @param object $product Product.
3165 + * @return bool
3166 + */
3167 + public static function is_visible_qty_input( $product = null ) {
3168 + $is_visible_qty = true;
3169 + if ( $product instanceof \WC_Product ) {
3170 + if ( $product->is_sold_individually() ) {
3171 + $is_visible_qty = false;
3172 + } elseif ( $product->managing_stock() ) {
3173 + if ( in_array( $product->get_backorders(), [ 'notify' , 'yes' ], true ) ) {
3174 + $is_visible_qty = true;
3175 + } elseif ( $product->get_stock_quantity() < 2 ) {
3176 + $is_visible_qty = false;
3177 + }
3178 + }
3179 + }
3180 + return $is_visible_qty;
3181 + }
3182 +
3183 + /**
3184 + * @param int $object_id Object id.
3185 + * @param string $element_type element type.
3186 + * @param string|null $current_lang null for current language, default for default languages.
3187 + * @return false|mixed|null
3188 + */
3189 + public static function wpml_object_id( $object_id, $element_type, $current_lang = 'default' ) {
3190 + if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) {
3191 + return $object_id;
3192 + }
3193 + if ( ! $object_id || ! $element_type ) {
3194 + return $object_id;
3195 + }
3196 + if ( 'default' === $current_lang ) {
3197 + $lang = apply_filters( 'wpml_default_language', null );
3198 + } else {
3199 + $lang = null;
3200 + }
3201 + return apply_filters( 'wpml_object_id', $object_id, $element_type, false, $lang );
3202 + }
3203 +
3204 + /**
3205 + * @return string
3206 + */
3207 + public static function wpml_current_language() {
3208 + $current = apply_filters( 'wpml_current_language', null );
3209 + $default = apply_filters( 'wpml_default_language', null );
3210 + return $default !== $current ? $current : null;
3211 + }
3212 +
3213 + /**
3214 + * Custom icons.
3215 + *
3216 + * @return string[]
3217 + */
3218 + public static function get_custom_icon_names() {
3219 + return [
3220 + 'heart-empty',
3221 + 'heart',
3222 + 'eye',
3223 + 'exchange',
3224 + 'plus',
3225 + 'minus',
3226 + 'avatar',
3227 + 'pay',
3228 + 'share',
3229 + 'clock',
3230 + 'check-alt',
3231 + 'check',
3232 + 'delete',
3233 + 'marker',
3234 + 'list',
3235 + 'list-2',
3236 + 'power',
3237 + 'cart',
3238 + 'cart-2',
3239 + 'cart-3',
3240 + 'downloads',
3241 + 'zoom',
3242 + 'user-edit',
3243 + 'grid',
3244 + 'filter',
3245 + 'billing',
3246 + 'login',
3247 + 'payment',
3248 + 'search',
3249 + 'edit',
3250 + 'coupon',
3251 + 'arrows-cw',
3252 + 'trash-empty',
3253 + ];
3254 + }
3255 +
3256 + /**
3257 + * Retrieve an array of custom icons with their HTML representation.
3258 + *
3259 + * @return array
3260 + */
3261 + public static function get_icons() {
3262 + $icon = self::get_custom_icon_names();
3263 + $icons_array = [];
3264 + foreach ( $icon as $value ) {
3265 + $icons_array[ $value ] = '<i class="rtsb-icon rtsb-icon-' . $value . '"></i> <span class="icon-name">' . ucfirst( str_replace( '-', ' ', $value ) ) . '</span>';
3266 + }
3267 + return $icons_array;
3268 + }
3269 +
3270 + /**
3271 + * Generates HTML markup for displaying an endpoint icon.
3272 + *
3273 + * @param array $data The endpoint data containing icon information.
3274 + * @param boolean $wrapper Whether to wrap the icon in a span element.
3275 + *
3276 + * @return string
3277 + */
3278 + public static function get_icon_html( $data, $wrapper = true ) {
3279 + if ( empty( $data ) || 'none' === $data['icon_source'] ) {
3280 + return '';
3281 + }
3282 + $endpoint = '';
3283 + $html = $wrapper ? '<span class="icon ' . esc_attr( $endpoint ) . '_icon">' : '';
3284 +
3285 + if ( 'select_icon' === $data['icon_source'] ) {
3286 + $html .= '<i class="rtsb-icon rtsb-icon-' . esc_attr( $data['custom_icon'] ) . '"></i>';
3287 + } else {
3288 + $icon_html = '';
3289 + $icon_url = $data['image_icon']['source'] ?? '';
3290 + $icon_id = $data['image_icon']['id'] ?? 0;
3291 + $extension = ! empty( $icon_url ) ? strtolower( substr( strrchr( $data['image_icon']['source'], '.' ), 1 ) ) : '';
3292 +
3293 + if ( 'svg' === $extension ) {
3294 + $content = file_get_contents( $icon_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
3295 + $is_svg = strpos( $content, '<svg' );
3296 +
3297 + if ( false !== $is_svg ) {
3298 + $icon_html = substr( $content, $is_svg );
3299 + }
3300 + } else {
3301 + $attr = [
3302 + 'class' => 'rtsb-icon-img',
3303 + 'alt' => esc_html( ucfirst( $endpoint ) ) . esc_html__( ' Icon', 'shopbuilder' ),
3304 + ];
3305 +
3306 + $icon_html = wp_get_attachment_image( absint( $icon_id ), 'full', true, $attr );
3307 + }
3308 +
3309 + $html .= $icon_html;
3310 + }
3311 +
3312 + $html .= $wrapper ? '</span>' : '';
3313 +
3314 + return $html;
3315 + }
3316 + /**
3317 + * Flushes rewrite rules.
3318 + *
3319 + * @return void
3320 + */
3321 + public static function maybe_flush_rewrite_rules() {
3322 + add_action( 'wp_loaded', [ __CLASS__, 'flush_rewrite_rules' ] );
3323 + add_action( 'shutdown', [ __CLASS__, 'flush_rewrite_rules_once_more' ] );
3324 + }
3325 +
3326 + /**
3327 + * Flushes rewrite rules if needed.
3328 + *
3329 + * @return void
3330 + */
3331 + public static function flush_rewrite_rules() {
3332 + if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3333 + flush_rewrite_rules();
3334 + }
3335 +
3336 + if ( get_option( 'rtsb_permalinks_flushed' ) === 'yes' ) {
3337 + flush_rewrite_rules();
3338 + delete_option( 'rtsb_permalinks_flushed' );
3339 + }
3340 + }
3341 +
3342 + /**
3343 + * Flushes rewrite rules if needed for second time.
3344 + *
3345 + * @return void
3346 + */
3347 + public static function flush_rewrite_rules_once_more() {
3348 + if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3349 + flush_rewrite_rules();
3350 + update_option( 'rtsb_permalinks_flushed', 'yes' );
3351 + delete_option( 'rtsb_permalinks_need_flush' );
3352 + }
3353 + }
3354 +
3355 + /**
3356 + * Social Share Platforms.
3357 + *
3358 + * @param string $section_id Section ID.
3359 + * @param string $block_id Block ID.
3360 + * @param string $option_key Option key.
3361 + * @param string $compare_key Compare key.
3362 + * @param string $compare_value Compare value.
3363 + * @param array $values Values.
3364 + *
3365 + * @return void
3366 + */
3367 + public static function set_repeater_options( $section_id, $block_id, $option_key, $compare_key, $compare_value, $values ) {
3368 + $modules = DataModel::source()->get_option( $section_id, [], false );
3369 + if ( empty( $modules[ $block_id ] ) ) {
3370 + return;
3371 + }
3372 + $options = $modules[ $block_id ];
3373 + if ( empty( $options[ $option_key ] ) ) {
3374 + return;
3375 + }
3376 + $repeater_options = json_decode( $options[ $option_key ], true );
3377 + // Check if options are not empty and are in array format.
3378 + if ( ! is_array( $repeater_options ) ) {
3379 + return;
3380 + }
3381 + // Use array_column to get an array of values from $compare_key.
3382 + $column_values = array_column( $repeater_options, $compare_key );
3383 + // Use array_search to find the index of the $compare_value.
3384 + $index = array_search( $compare_value, $column_values, true );
3385 + if ( false === $index ) {
3386 + return;
3387 + }
3388 + $repeater_options[ $index ] = wp_parse_args( $values, $repeater_options[ $index ] );
3389 + $options[ $option_key ] = wp_json_encode( $repeater_options );
3390 + $modules[ $block_id ] = $options;
3391 + DataModel::source()->set_option( $section_id, $modules );
3392 + }
3393 +
3394 +
3395 + /**
3396 + * @param array $ids products id.
3397 + * @return array
3398 + */
3399 + public static function get_available_products_by_ids( $ids = [] ) {
3400 + $ids = array_filter(
3401 + $ids,
3402 + function ( $id ) {
3403 + return 'product' === get_post_type( $id ) && 'publish' === get_post_status( $id );
3404 + }
3405 + );
3406 + return $ids;
3407 + }
3408 +
3409 + /**
3410 + * Generate and cache dynamic CSS styles.
3411 + *
3412 + * @param array $options CSS options to apply (e.g. padding, margin).
3413 + * @param string $cache_key Cache key for the CSS.
3414 + * @param array $css_properties An array of CSS properties with selectors and properties.
3415 + *
3416 + * @return void
3417 + */
3418 + public static function dynamic_styles( $options, $cache_key, $css_properties ) {
3419 + $cached_css = wp_cache_get( $cache_key, 'shopbuilder' );
3420 + $style_handle = self::optimized_handle( 'rtsb-frontend' );
3421 +
3422 + if ( false !== $cached_css ) {
3423 + wp_add_inline_style( $style_handle, $cached_css );
3424 + return;
3425 + }
3426 +
3427 + $css_rules = [];
3428 + $grouped_css_rules = [];
3429 +
3430 + foreach ( $css_properties as $option => $details ) {
3431 + if ( ! empty( $options[ $option ] ) ) {
3432 + $selector = $details['selector'] ?? '';
3433 + $property = $details['property'] ?? '';
3434 + $unit = $details['unit'] ?? '';
3435 + $value = $options[ $option ];
3436 +
3437 + if ( empty( $grouped_css_rules[ $selector ] ) ) {
3438 + $grouped_css_rules[ $selector ] = [];
3439 + }
3440 +
3441 + if ( is_array( $property ) ) {
3442 + foreach ( $property as $prop ) {
3443 + $grouped_css_rules[ $selector ][] = $prop . ': ' . $value . $unit . ' !important';
3444 + }
3445 + } else {
3446 + $grouped_css_rules[ $selector ][] = $property . ': ' . $value . $unit . ' !important';
3447 + }
3448 + }
3449 + }
3450 +
3451 + foreach ( $grouped_css_rules as $selector => $properties ) {
3452 + $css_rules[] = $selector . ' {' . implode( '; ', $properties ) . '}';
3453 + }
3454 +
3455 + $dynamic_css = implode( ' ', $css_rules );
3456 +
3457 + wp_cache_set( $cache_key, $dynamic_css, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3458 + Cache::set_data_cache_key( $cache_key );
3459 +
3460 + if ( ! empty( $dynamic_css ) ) {
3461 + wp_add_inline_style( $style_handle, $dynamic_css );
3462 + }
3463 + }
3464 +
3465 + /**
3466 + * Pro version notice.
3467 + *
3468 + * @param string $ver Pro Version.
3469 + * @param string $tab Tab.
3470 + * @param string $name Name.
3471 + * @param bool $enable_free_Link Enable free link.
3472 + *
3473 + * @return array[]
3474 + */
3475 + public static function pro_version_notice( $ver, $tab = 'billing_fields', $name = 'ShopBuilder', $enable_free_Link = true ) {
3476 + return [
3477 + 'version_check' => [
3478 + 'id' => 'version_check',
3479 + 'type' => 'title',
3480 + 'label' => sprintf(
3481 + /* translators: 1: required version, 2: link to the Plugins page */
3482 + esc_html__(
3483 + 'To access all features and settings of this module, please ensure that "%1$s" is updated to version %2$s. %3$s',
3484 + 'shopbuilder'
3485 + ),
3486 + $name,
3487 + sprintf(
3488 + '<br /><u><b>%s</b></u>',
3489 + esc_html( $ver ?? '1.5.0' ) . ' or higher'
3490 + ),
3491 + $enable_free_Link
3492 + ? sprintf(
3493 + '%s <a class="pro-update-required" href="%s" target="_blank" title="%s">%s</a>',
3494 + esc_attr__( 'Update to the latest version', 'shopbuilder' ),
3495 + esc_url( admin_url( 'plugins.php' ) ),
3496 + esc_attr__( 'Go to the Plugin Page', 'shopbuilder' ),
3497 + esc_html__( 'from the Plugins page', 'shopbuilder' )
3498 + )
3499 + : ''
3500 + ),
3501 + 'tab' => $tab,
3502 + 'customClass' => 'checkout-notice',
3503 + ],
3504 + ];
3505 + }
3506 +
3507 + /**
3508 + * Get product gallery ids.
3509 + *
3510 + * @param object $product Product object.
3511 + *
3512 + * @return mixed
3513 + */
3514 + public static function get_cached_gallery_ids( $product ) {
3515 + $product_id = $product->get_id();
3516 + $cache_key = 'rtsb_product_gallery_ids_' . $product_id;
3517 +
3518 + if ( isset( self::$cache[ $cache_key ] ) ) {
3519 + return self::$cache[ $cache_key ];
3520 + }
3521 +
3522 + $cached_result = wp_cache_get( $cache_key, 'shopbuilder' );
3523 +
3524 + if ( false !== $cached_result ) {
3525 + self::$cache[ $cache_key ] = $cached_result;
3526 +
3527 + return $cached_result;
3528 + }
3529 +
3530 + $gallery_ids = $product->get_gallery_image_ids();
3531 + self::$cache[ $cache_key ] = $gallery_ids;
3532 +
3533 + wp_cache_set( $cache_key, $gallery_ids, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3534 + Cache::set_data_cache_key( $cache_key );
3535 +
3536 + return $gallery_ids;
3537 + }
3538 +
3539 + /**
3540 + * Check if optimization settings are enabled.
3541 + *
3542 + * @return bool
3543 + */
3544 + public static function is_optimization_enabled() {
3545 + $has_pro = rtsb()->has_pro();
3546 + $pro_ver = defined( 'RTSBPRO_VERSION' ) ? RTSBPRO_VERSION : 0;
3547 +
3548 + if ( $has_pro && version_compare( $pro_ver, '2.0.0', '<' ) ) {
3549 + return false;
3550 + }
3551 +
3552 + $generalList = GeneralList::instance()->get_data();
3553 +
3554 + return 'on' === ( $generalList['optimization']['enable_optimization'] ?? '' );
3555 + }
3556 +
3557 + /**
3558 + * Get the optimized handle.
3559 + *
3560 + * @param string $handle Base handle used for script/style IDs.
3561 + *
3562 + * @return string
3563 + */
3564 + public static function optimized_handle( $handle ) {
3565 + $use_optimization = self::is_optimization_enabled();
3566 +
3567 + if ( $use_optimization ) {
3568 + $handle = self::is_contextual_loading() ? self::get_optimized_handle_by_context() : 'rtsb-bundled';
3569 + }
3570 +
3571 + return $handle;
3572 + }
3573 +
3574 + /**
3575 + * Get the optimized handle by context.
3576 + *
3577 + * @return string
3578 + */
3579 + public static function get_optimized_handle_by_context() {
3580 + $context = self::detect_context();
3581 +
3582 + if ( 'account' === $context ) {
3583 + return 'rtsb-bundled-global';
3584 + }
3585 +
3586 + return "rtsb-bundled-$context";
3587 + }
3588 +
3589 + /**
3590 + * Check if Elementor page.
3591 + *
3592 + * @param int $post_id Post ID.
3593 + *
3594 + * @return bool
3595 + */
3596 + public static function is_elementor_page( $post_id = null ) {
3597 + if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
3598 + return false;
3599 + }
3600 +
3601 + $post_id = $post_id ?: get_the_ID();
3602 +
3603 + if ( ! $post_id ) {
3604 + return true;
3605 + }
3606 +
3607 + return Plugin::$instance->documents->get( $post_id )->is_built_with_elementor();
3608 + }
3609 +
3610 + /**
3611 + * Check if shop or archive.
3612 + *
3613 + * @return bool
3614 + */
3615 + public static function is_shop_or_archive() {
3616 + return is_shop() || is_product_category() || is_product_tag() || is_post_type_archive( 'product' ) || BuilderFns::is_archive() || BuilderFns::is_shop() || is_tax( 'product_brand' );
3617 + }
3618 +
3619 + /**
3620 + * Detect context.
3621 + *
3622 + * @return string
3623 + */
3624 + public static function detect_context() {
3625 + switch ( true ) {
3626 + case is_product() || BuilderFns::is_product():
3627 + $context = 'product';
3628 + break;
3629 +
3630 + case is_cart() || BuilderFns::is_cart():
3631 + $context = 'cart';
3632 + break;
3633 +
3634 + case is_checkout() || BuilderFns::is_checkout():
3635 + $context = 'checkout';
3636 + break;
3637 +
3638 + case self::is_shop_or_archive():
3639 + $context = 'shop';
3640 + break;
3641 +
3642 + default:
3643 + $context = 'global';
3644 + break;
3645 + }
3646 +
3647 + return apply_filters( 'rtsb/optimizer/context_detect', $context );
3648 + }
3649 +
3650 + /**
3651 + * Enqueue optimized assets.
3652 + *
3653 + * @return void
3654 + */
3655 + public static function enqueue_optimized_assets() {
3656 + $asset_registry = AssetRegistry::instance();
3657 + $bundled_assets = $asset_registry->get_bundled_assets();
3658 + $context = self::detect_context();
3659 + $contextual_loading = self::is_contextual_loading();
3660 + $theme_handle = self::find_theme_stylesheet_handle();
3661 +
3662 + if ( $contextual_loading ) {
3663 + // Enqueue JS.
3664 + if ( ! empty( $bundled_assets['js'] ) ) {
3665 + $js_context = isset( $bundled_assets['js'][ $context ] ) ? $context : 'global';
3666 +
3667 + if ( $js_context ) {
3668 + wp_enqueue_script( $bundled_assets['js'][ $js_context ]['handle'] );
3669 + }
3670 + }
3671 +
3672 + // Enqueue CSS.
3673 + if ( ! empty( $bundled_assets['css'] ) ) {
3674 + $css_context = isset( $bundled_assets['css'][ $context ] ) ? $context : 'global';
3675 +
3676 + if ( $css_context ) {
3677 + $handle = $bundled_assets['css'][ $css_context ]['handle'];
3678 +
3679 + wp_enqueue_style( $handle );
3680 +
3681 + if ( ! empty( $theme_handle ) ) {
3682 + self::set_theme_dependency( $theme_handle, $handle );
3683 + }
3684 + }
3685 + }
3686 + } else {
3687 + // Enqueue JS.
3688 + if ( ! empty( $bundled_assets['js'] ) ) {
3689 + wp_enqueue_script( $bundled_assets['js']['handle'] );
3690 + }
3691 +
3692 + // Enqueue CSS.
3693 + if ( ! empty( $bundled_assets['css'] ) ) {
3694 + $handle = $bundled_assets['css']['handle'];
3695 +
3696 + wp_enqueue_style( $handle );
3697 +
3698 + if ( ! empty( $theme_handle ) ) {
3699 + self::set_theme_dependency( $theme_handle, $handle );
3700 + }
3701 + }
3702 + }
3703 + }
3704 +
3705 + /**
3706 + * Find theme stylesheet handle.
3707 + *
3708 + * @return string|false
3709 + */
3710 + private static function find_theme_stylesheet_handle() {
3711 + static $cached_handle = null;
3712 +
3713 + if ( null !== $cached_handle ) {
3714 + return $cached_handle;
3715 + }
3716 +
3717 + global $wp_styles;
3718 +
3719 + if ( ! $wp_styles instanceof WP_Styles ) {
3720 + return false;
3721 + }
3722 +
3723 + $stylesheet_uri = get_stylesheet_uri();
3724 + $theme_directory_uri = get_stylesheet_directory_uri();
3725 +
3726 + $hash = md5( $stylesheet_uri );
3727 + $transient_key = 'rtsb_theme_css_handle_' . $hash;
3728 +
3729 + $transient = get_transient( $transient_key );
3730 +
3731 + if ( false !== $transient ) {
3732 + $cached_handle = $transient;
3733 +
3734 + return $transient;
3735 + }
3736 +
3737 + foreach ( $wp_styles->registered as $handle => $style ) {
3738 + $src = $style->src;
3739 +
3740 + if (
3741 + ( $src === $stylesheet_uri || strpos( $src, $theme_directory_uri ) !== false ) &&
3742 + strpos( $src, 'style.css' ) !== false
3743 + ) {
3744 + set_transient( $transient_key, $handle, DAY_IN_SECONDS );
3745 + Cache::set_transient_cache_key( $transient_key );
3746 + $cached_handle = $handle;
3747 +
3748 + return $handle;
3749 + }
3750 + }
3751 +
3752 + $filtered_handle = apply_filters( 'rtsb/optimizer/theme_stylesheet_handle', false );
3753 +
3754 + if ( is_string( $filtered_handle ) && ! empty( $filtered_handle ) ) {
3755 + set_transient( $transient_key, $filtered_handle, DAY_IN_SECONDS );
3756 + Cache::set_transient_cache_key( $transient_key );
3757 + $cached_handle = $filtered_handle;
3758 +
3759 + return $filtered_handle;
3760 + }
3761 +
3762 + set_transient( $transient_key, false, DAY_IN_SECONDS );
3763 + Cache::set_transient_cache_key( $transient_key );
3764 + $cached_handle = false;
3765 +
3766 + return false;
3767 + }
3768 +
3769 + /**
3770 + * Set theme dependency.
3771 + *
3772 + * @param string $theme_handle Theme handle.
3773 + * @param string $our_handle Our handle.
3774 + *
3775 + * @return void
3776 + */
3777 + private static function set_theme_dependency( $theme_handle, $our_handle ) {
3778 + global $wp_styles;
3779 +
3780 + if ( ! isset( $wp_styles->registered[ $theme_handle ] ) ) {
3781 + return;
3782 + }
3783 +
3784 + $theme_style = $wp_styles->registered[ $theme_handle ];
3785 +
3786 + // Add our handle as a dependency if it's not already there.
3787 + if ( ! in_array( $our_handle, $theme_style->deps, true ) ) {
3788 + $theme_style->deps[] = $our_handle;
3789 + }
3790 + }
3791 +
3792 + /**
3793 + * Check if Elementor scripts should be loaded.
3794 + *
3795 + * @return bool
3796 + */
3797 + public static function should_load_elementor_scripts() {
3798 + $data = GeneralList::instance()->get_data()['optimization'] ?? [];
3799 +
3800 + $enable_optimization = $data['enable_optimization'] ?? 'on';
3801 + $load_elementor_scripts = $data['load_elementor_scripts'] ?? 'on';
3802 +
3803 + if ( 'on' !== $enable_optimization ) {
3804 + return true;
3805 + }
3806 +
3807 + return 'on' === $load_elementor_scripts;
3808 + }
3809 +
3810 + /**
3811 + * Locate asset.
3812 + *
3813 + * @param string $relative_path Relative path.
3814 + *
3815 + * @return string|null
3816 + */
3817 + public static function locate_asset( $relative_path ) {
3818 + $free_context = rtsb();
3819 + $pro_context = function_exists( 'rtsbpro' ) && rtsb()->has_pro() ? rtsbpro() : null;
3820 +
3821 + $paths = [];
3822 +
3823 + if ( $pro_context ) {
3824 + $paths[] = $pro_context->get_assets_path( $relative_path );
3825 + }
3826 +
3827 + $paths[] = $free_context->get_assets_path( $relative_path );
3828 +
3829 + foreach ( $paths as $path ) {
3830 + if ( file_exists( $path ) ) {
3831 + return $path;
3832 + }
3833 + }
3834 +
3835 + return null;
3836 + }
3837 +
3838 + /**
3839 + * Enqueue module assets.
3840 + *
3841 + * @param string $handle Asset handle.
3842 + * @param string $module_name Module name.
3843 + * @param array $options Options array: ['type' => 'css|js|both', 'deps' => [], 'context' => null].
3844 + *
3845 + * @return string
3846 + */
3847 + public static function enqueue_module_assets( $handle, $module_name, $options = [] ) {
3848 + $use_optimization = self::is_optimization_enabled();
3849 + $handle = self::optimized_handle( $handle );
3850 +
3851 + if ( $use_optimization ) {
3852 + return $handle;
3853 + }
3854 +
3855 + $defaults = [
3856 + 'type' => 'both',
3857 + 'deps' => [ 'jquery', 'rtsb-public' ],
3858 + 'context' => null,
3859 + 'version' => RTSB_VERSION,
3860 + ];
3861 +
3862 + $config = array_merge( $defaults, $options );
3863 + $rtl_suffix = is_rtl() ? '-rtl' : '';
3864 + $rtl_dir = is_rtl() ? trailingslashit( 'rtl' ) : trailingslashit( 'css' );
3865 + $context = $config['context'] ?: rtsb();
3866 + $load_css = in_array( $config['type'], [ 'css', 'both' ], true );
3867 + $load_js = in_array( $config['type'], [ 'js', 'both' ], true );
3868 +
3869 + // Register CSS if enabled.
3870 + if ( $load_css ) {
3871 + wp_register_style(
3872 + $handle,
3873 + $context->get_assets_uri( $rtl_dir . 'modules/' . $module_name . $rtl_suffix . '.css' ),
3874 + [],
3875 + $config['version']
3876 + );
3877 + }
3878 +
3879 + // Register JS if enabled.
3880 + if ( $load_js ) {
3881 + wp_register_script(
3882 + $handle,
3883 + $context->get_assets_uri( 'js/modules/' . $module_name . '.js' ),
3884 + $config['deps'],
3885 + $config['version'],
3886 + true
3887 + );
3888 + }
3889 +
3890 + if ( $load_css ) {
3891 + wp_enqueue_style( $handle );
3892 +
3893 + $theme_handle = self::find_theme_stylesheet_handle();
3894 +
3895 + if ( ! empty( $theme_handle ) ) {
3896 + self::set_theme_dependency( $theme_handle, $handle );
3897 + }
3898 + }
3899 +
3900 + if ( $load_js ) {
3901 + wp_enqueue_script( $handle );
3902 + }
3903 +
3904 + return $handle;
3905 + }
3906 +
3907 + /**
3908 + * Check if contextual loading is enabled.
3909 + *
3910 + * @return bool
3911 + */
3912 + public static function is_contextual_loading() {
3913 + $data = GeneralList::instance()->get_data()['optimization'] ?? [];
3914 +
3915 + return ! empty( $data['context_asset_loading'] ) && 'on' === $data['context_asset_loading'];
3916 + }
3917 +
3918 + /**
3919 + * Get Modules list with cache support.
3920 + *
3921 + * @return array
3922 + */
3923 + public static function get_modules_list() {
3924 + static $cached_modules = null;
3925 +
3926 + if ( null !== $cached_modules ) {
3927 + return $cached_modules;
3928 + }
3929 +
3930 + $cache_key = 'rtsb_module_list';
3931 + $cache_group = 'shopbuilder';
3932 +
3933 + $cached = wp_cache_get( $cache_key, $cache_group );
3934 +
3935 + if ( false !== $cached ) {
3936 + $cached_modules = $cached;
3937 +
3938 + return $cached;
3939 + }
3940 +
3941 + $modules = ModuleList::instance()->get_data();
3942 +
3943 + wp_cache_set( $cache_key, $modules, $cache_group, 12 * HOUR_IN_SECONDS );
3944 + Cache::set_data_cache_key( $cache_key );
3945 +
3946 + $cached_modules = $modules;
3947 +
3948 + return $modules;
3949 + }
3950 +
3951 + /**
3952 + * Get Elementor widgets list with cache support.
3953 + *
3954 + * @return array
3955 + */
3956 + public static function get_widgets_list() {
3957 + static $cached_widgets = null;
3958 +
3959 + if ( null !== $cached_widgets ) {
3960 + return $cached_widgets;
3961 + }
3962 +
3963 + $cache_key = 'rtsb_elementor_widget_list';
3964 + $cache_group = 'shopbuilder';
3965 +
3966 + $cached = wp_cache_get( $cache_key, $cache_group );
3967 +
3968 + if ( false !== $cached ) {
3969 + $cached_widgets = $cached;
3970 +
3971 + return $cached;
3972 + }
3973 +
3974 + $widgets = ElementList::instance()->get_list();
3975 +
3976 + wp_cache_set( $cache_key, $widgets, $cache_group, 12 * HOUR_IN_SECONDS );
3977 + Cache::set_data_cache_key( $cache_key );
3978 +
3979 + $cached_widgets = $widgets;
3980 +
3981 + return $widgets;
3982 + }
3983 +
3984 + /**
3985 + * Convert the given price to the active currency.
3986 + *
3987 + * @param float $price The original price.
3988 + * @param Object||null $product Product.
3989 + *
3990 + * @return float
3991 + */
3992 + public static function get_currency_base_price( $price, $product = null ) {
3993 + return apply_filters( 'rtsb/convert/currency/price', $price, $product );
3994 + }
3995 + /**
3996 + * Generate a signed URL with a payload.
3997 + *
3998 + * @param array $payload Associative data to encode.
3999 + * @param string $base_url Base URL to append ?key=... (e.g. wc_get_checkout_url()).
4000 + * @param string $key Key name to use in the URL.
4001 + * @return string Signed URL with key parameter.
4002 + */
4003 + public static function generate_signed_url( array $payload, string $base_url, $key = 'key' ) {
4004 + if ( empty( $key ) ) {
4005 + $key = 'key';
4006 + }
4007 + // Convert payload to JSON.
4008 + $data = wp_json_encode( $payload );
4009 + // Create signature.
4010 + $signature = wp_hash( $data );
4011 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
4012 + $encode_key = base64_encode( $data . '::' . $signature );
4013 + // Return full URL with key param.
4014 + return add_query_arg( [ $key => rawurlencode( $encode_key ) ], $base_url );
4015 + }
4016 +
4017 + /**
4018 + * Decode and verify a signed URL key.
4019 + *
4020 + * @param string $key Encoded key from URL.
4021 + * @return array|false Decoded payload array on success, false on failure.
4022 + */
4023 + public static function decode_signed_key( string $key ) {
4024 + if ( empty( $key ) ) {
4025 + return false;
4026 + }
4027 + $key = rawurldecode( wp_unslash( $key ) );
4028 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
4029 + $raw = base64_decode( sanitize_text_field( $key ) );
4030 + if ( ! $raw ) {
4031 + return false;
4032 + }
4033 + $parts = explode( '::', $raw, 2 );
4034 + if ( count( $parts ) !== 2 ) {
4035 + return false;
4036 + }
4037 + list( $data_json, $signature ) = $parts;
4038 + // Validate signature.
4039 + if ( ! hash_equals( wp_hash( $data_json ), $signature ) ) {
4040 + return false;
4041 + }
4042 + $payload = json_decode( $data_json, true );
4043 + return is_array( $payload ) ? $payload : false;
4044 + }
4045 +
4046 + /**
4047 + * Get Loco Translate MO file path for a plugin.
4048 + *
4049 + * @param string $textdomain Plugin textdomain.
4050 + * @return void|false
4051 + */
4052 + public static function load_loco_textdomain( $textdomain ) {
4053 + if ( ! function_exists( 'loco_plugin_version' ) ) {
4054 + return false;
4055 + }
4056 + $lang = WP_LANG_DIR;
4057 + $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4058 + if ( ! file_exists( $path ) && defined( 'LOCO_LANG_DIR' ) ) {
4059 + $lang = LOCO_LANG_DIR;
4060 + $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4061 + }
4062 + if ( ! file_exists( $path ) ) {
4063 + if ( 'shopbuilder' === $textdomain ) {
4064 + $plugin_root = dirname( RTSB_FILE );
4065 + } elseif ( 'shopbuilder-pro' === $textdomain ) {
4066 + $plugin_root = dirname( RTSBPRO_FILE );
4067 + } else {
4068 + return false;
4069 + }
4070 + $path = $plugin_root . '/languages/' . $textdomain . '-' . get_locale() . '.mo';
4071 + }
4072 + if ( ! file_exists( $path ) ) {
4073 + return false;
4074 + }
4075 + load_textdomain( $textdomain, $path );
2675 4076 }
2676 4077 }