PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.0
ShopBuilder – WooCommerce Builder For Elementor v3.4.0
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 +1995 -216 2.1.23.4.0 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 RadiusTheme\SB\Dependencies\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 */
@@ -45,12 +98,118 @@
45 98 return false;
46 99 }
47 100
48 101 /**
49 - * @param $plugin_slug
102 + * Get nonce.
50 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 + */
129 + public static function get_nonce() {
130 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
131 + return isset( $_REQUEST[ rtsb()->nonceId ] ) ? sanitize_text_field( $_REQUEST[ rtsb()->nonceId ] ) : null;
132 + }
133 +
134 + /**
135 + * Set Session
136 + *
137 + * @param string $name Name.
138 + * @param mixed $value Value.
139 + */
140 + public static function setSession( $name, $value ) {
141 + if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
142 + session_start();
143 + $_SESSION[ $name ] = $value;
144 + } else {
145 + $_SESSION[ $name ] = $value;
146 + }
147 + }
148 +
149 + /**
150 + * Get Cookie or Session
151 + *
152 + * @param string $name Name.
153 + *
51 154 * @return bool
52 155 */
156 + public static function getSession( $name ) {
157 + if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
158 + session_start();
159 + }
160 + return $_SESSION[ $name ] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
161 + }
162 + /**
163 + * Remove Session Variable
164 + *
165 + * @param string $name The name of the session variable to remove.
166 + */
167 + public static function removeSession( $name ) {
168 + if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
169 + session_start();
170 + unset( $_SESSION[ $name ] );
171 + } else {
172 + unset( $_SESSION[ $name ] );
173 + }
174 + }
175 + /**
176 + * Gets the current timestamp in WordPress timezone.
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 + *
210 + * @return bool
211 + */
53 212 public static function check_plugin_installed( $plugin_slug ): bool {
54 213 $installed_plugins = get_plugins();
55 214
56 215 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
@@ -56,10 +215,12 @@
56 215 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
57 216 }
58 217
59 218 /**
60 - * @param $plugin_slug
219 + * Check if a plugin is active.
61 220 *
221 + * @param string $plugin_slug Plugin slug.
222 + *
62 223 * @return bool
63 224 */
64 225 public static function check_plugin_active( $plugin_slug ): bool {
65 226 if ( is_plugin_active( $plugin_slug ) ) {
@@ -67,21 +228,71 @@
67 228 }
68 229
69 230 return false;
70 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' );
71 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 +
72 270 /**
73 - * @param $data
271 + * Stripslashes value.
74 272 *
273 + * @param mixed $data Data.
274 + *
75 275 * @return mixed|string
76 276 */
77 277 public static function stripslashes_value( $data ) {
78 - if ( is_string( $data ) && strpos( $data, '\\' ) !== false ) {
79 - return stripslashes( $data );
80 - } elseif ( is_array( $data ) ) {
278 + if ( is_array( $data ) ) {
81 279 foreach ( $data as $key => $value ) {
82 280 $data[ $key ] = self::stripslashes_value( $value );
83 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 + }
84 295 }
85 296
86 297 return $data;
87 298 }
@@ -86,10 +297,12 @@
86 297 return $data;
87 298 }
88 299
89 300 /**
90 - * @param $string
301 + * Multiselect settings field value
91 302 *
303 + * @param string $string String.
304 + *
92 305 * @return array
93 306 */
94 307 public static function multiselect_settings_field_value( $string ) {
95 308
@@ -109,8 +322,10 @@
109 322 return $values;
110 323 }
111 324
112 325 /**
326 + * Check if is block theme
327 + *
113 328 * @return bool
114 329 */
115 330 public static function check_is_block_theme(): bool {
116 331 global $wp_version;
@@ -118,16 +333,19 @@
118 333 return version_compare( $wp_version, '5.9', '>=' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
119 334 }
120 335
121 336 /**
337 + * Locate template
338 + *
122 339 * @param string $template_name Template name.
123 340 * @param string $template_path Template path. (default: '').
124 - * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin
341 + * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin.
125 342 *
126 343 * @return mixed|void
127 344 */
128 345 public static function locate_template( $template_name, $template_path = '', $plugin_path = '' ) {
129 - $template_name = $template_name . '.php';
346 + $template_name = self::sanitize_file_name( $template_name ) . '.php';
347 +
130 348 if ( ! $template_path ) {
131 349 $template_path = rtsb()->get_template_path();
132 350 }
133 351 if ( ! $plugin_path ) {
@@ -149,12 +367,12 @@
149 367 )
150 368 );
151 369
152 370 if ( ! $located ) {
153 - 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 ) ) {
154 374 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 375 }
158 376 }
159 377
160 378 /**
@@ -170,8 +388,19 @@
170 388 return apply_filters( 'rtsb/core/locate_template', $located, $template_name );
171 389 }
172 390
173 391 /**
392 + * Remove any character that is not alphanumeric, /, _, or -.
393 + *
394 + * @param string $name Name to sanitize.
395 + *
396 + * @return array|string|string[]|null
397 + */
398 + private static function sanitize_file_name( $name ) {
399 + return preg_replace( '/[^a-zA-Z0-9\/_\-]/', '', $name );
400 + }
401 +
402 + /**
174 403 * Template Content
175 404 *
176 405 * @param string $template_name Template name.
177 406 * @param array $args Arguments. (default: array).
@@ -180,11 +409,20 @@
180 409 * @param string $plugin_path Fallback path from where file will load if fail to load from template. (default: '').
181 410 *
182 411 * @return false|string|void
183 412 */
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 -
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 + }
187 425 if ( ! file_exists( $located ) ) {
188 426 // translators: %s template.
189 427 self::doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'shopbuilder' ), '<code>' . $located . '</code>' ), '1.0' );
190 428
@@ -192,9 +430,9 @@
192 430 }
193 431
194 432 if ( ! empty( $args ) && is_array( $args ) ) {
195 433 $atts = $args;
196 - extract( $args ); // @codingStandardsIgnoreLine
434 + extract( $args ); // @codingStandardsIgnoreLine
197 435 }
198 436
199 437 // Allow 3rd party plugin filter template file from their plugin.
200 438 $located = apply_filters( 'rtsb/core/get_template', $located, $template_name, $args );
@@ -309,9 +547,9 @@
309 547 // Return the Builder Template id.
310 548
311 549 if ( get_post_type( get_the_ID() ) == BuilderFns::$post_type_tb ) {
312 550 $product_id = get_post_meta( get_the_ID(), BuilderFns::$product_template_meta, true );
313 - if ( $product_id ) {
551 + if ( $product_id && 'product' === get_post_type( $product_id ) && get_post_status( $product_id ) ) {
314 552 return $product_id;
315 553 }
316 554 }
317 555
@@ -316,15 +554,16 @@
316 554 }
317 555
318 556 global $wpdb;
319 557 $cache_key = 'rtsb_prepared_product_id';
320 - $_post_id = wp_cache_get( $cache_key );
321 -
558 + $_post_id = wp_cache_get( $cache_key, 'shopbuilder' );
322 559 if ( false === $_post_id || 'publish' !== get_post_status( $_post_id ) ) {
560 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
323 561 $_post_id = $wpdb->get_var(
324 562 $wpdb->prepare( "SELECT MAX(ID) FROM {$wpdb->prefix}posts WHERE post_type = %s AND post_status IN('publish', 'draft')", 'product' )
325 563 );
326 - 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 );
327 566 }
328 567
329 568 return $_post_id;
330 569 }
@@ -337,9 +576,8 @@
337 576 public static function get_product() {
338 577 global $product;
339 578
340 579 if ( is_singular( 'product' ) && $product instanceof WC_Product ) {
341 - do_action( 'rtsb_before_product_template_render' );
342 580 return $product;
343 581 }
344 582 $cache_key = 'prepared_product_for_preview';
345 583 if ( isset( self::$cache[ $cache_key ] ) ) {
@@ -344,9 +582,8 @@
344 582 $cache_key = 'prepared_product_for_preview';
345 583 if ( isset( self::$cache[ $cache_key ] ) ) {
346 584 return self::$cache[ $cache_key ];
347 585 }
348 -
349 586 $product = wc_get_product( self::get_prepared_product_id() );
350 587 self::$cache[ $cache_key ] = $product;
351 588 do_action( 'rtsb_before_product_template_render' );
352 589 return $product;
@@ -361,10 +598,11 @@
361 598 *
362 599 * @return void
363 600 */
364 601 public static function doing_it_wrong( $function, $message, $version ) {
602 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
365 603 $message .= ' Backtrace: ' . wp_debug_backtrace_summary();
366 - _doing_it_wrong( $function, $message, $version );
604 + _doing_it_wrong( esc_html( $function ), wp_kses_post( $message ), esc_html( $version ) );
367 605 }
368 606
369 607 /**
370 608 * @return array
@@ -380,8 +618,15 @@
380 618
381 619 return $pages;
382 620 }
383 621
622 + /**
623 + * Get section items
624 + *
625 + * @param string $section_id Section ID.
626 + *
627 + * @return array
628 + */
384 629 public static function get_section_items( $section_id ) {
385 630 if ( ! $section_id ) {
386 631 return [];
387 632 }
@@ -388,8 +633,16 @@
388 633
389 634 return DataModel::source()->get_option( $section_id, [] );
390 635 }
391 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 + */
392 645 public static function get_options( $section_id, $item_id ) {
393 646 if ( ! $section_id || ! $item_id ) {
394 647 return [];
395 648 }
@@ -400,11 +653,11 @@
400 653
401 654 /**
402 655 * Get options value with default value if options doesn't exist.
403 656 *
404 - * @param $group
405 - * @param $option_key
406 - * @param $default_value
657 + * @param array $group Group.
658 + * @param string $option_key Option key.
659 + * @param string $default_value Default value.
407 660 *
408 661 * @return mixed|string
409 662 */
410 663 public static function get_options_by_default_val( $group, $option_key, $default_value = '' ) {
@@ -416,32 +669,34 @@
416 669 return $group[ $option_key ];
417 670 }
418 671
419 672 /**
420 - * @param string $section_id
421 - * @param string $item_id
422 - * @param string $option_id
423 - * @param null $default EXCEPT multi_checkbox you can provide default value if given option does not set any value
424 - * @param null $type checkbox, multi_checkbox, number
673 + * Get option.
425 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 + *
426 681 * @return bool|int|mixed|null
427 682 */
428 683 public static function get_option( $section_id, $item_id, $option_id, $default = null, $type = null ) {
429 684 $options = self::get_options( $section_id, $item_id );
430 685
431 - if ( $type === 'checkbox' ) {
686 + if ( 'checkbox' === $type ) {
432 687 if ( isset( $options[ $option_id ] ) ) {
433 - return $options[ $option_id ] === 'on';
688 + return 'on' === $options[ $option_id ];
434 689 }
435 690
436 691 return $default;
437 - } elseif ( $type === 'multi_checkbox' ) {
438 - return isset( $options[ $option_id ] ) && is_array( $options[ $option_id ] ) && in_array( $default, $options[ $option_id ] );
439 - } 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 ) {
440 695 return isset( $options[ $option_id ] ) ? absint( $options[ $option_id ] ) : absint( $default );
441 696 }
442 697
443 - return isset( $options[ $option_id ] ) && ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
698 + return ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
444 699 }
445 700
446 701
447 702 /**
@@ -495,13 +750,14 @@
495 750 }
496 751
497 752 if ( strlen( $page_content ) > 0 ) {
498 753 // Search for an existing page with the specified page content (typically a shortcode).
499 - $shortcode = str_replace( [ '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ], '', $page_content );
754 + $shortcode = str_replace( [ '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ], '', $page_content );
755 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
500 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}%" ) );
501 757 } else {
502 758 // Search for an existing page with the specified page slug.
503 - $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 ) );
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
504 760 }
505 761
506 762 $valid_page_found = apply_filters( 'rtsb/core/create_page_id', $valid_page_found, $slug, $page_content, $options );
507 763
@@ -523,12 +779,12 @@
523 779
524 780 // Search for a matching valid trashed page.
525 781 if ( strlen( $page_content ) > 0 ) {
526 782 // Search for an existing page with the specified page content (typically a shortcode).
527 - $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}%" ) );
783 + $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
528 784 } else {
529 785 // Search for an existing page with the specified page slug.
530 - $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 ) );
786 + $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
531 787 }
532 788
533 789 if ( $trashed_page_found ) {
534 790 $page_id = $trashed_page_found;
@@ -567,8 +823,13 @@
567 823
568 824 return $page_id;
569 825 }
570 826
827 + /**
828 + * Get allowed HTML tags.
829 + *
830 + * @return array
831 + */
571 832 public static function get_kses_array() {
572 833 return [
573 834 'a' => [
574 835 'class' => [],
@@ -667,13 +928,14 @@
667 928 ];
668 929 }
669 930
670 931
671 - /*
932 + /**
672 933 * Escape output of wishlist icon
673 934 *
674 935 * @param string $data Data to escape.
675 - * @return string Escaped data
936 + *
937 + * @return void
676 938 */
677 939 public static function print_icon( $data ) {
678 940 /**
679 941 * APPLY_FILTERS: rtsb/core/allowed_icon_html
@@ -757,9 +1019,9 @@
757 1019 * @return mixed
758 1020 */
759 1021 public static function allowedHtml( $level = 'basic' ) {
760 1022 $allowed_html = [];
761 - // TODO:: Need Optimize.
1023 +
762 1024 switch ( $level ) {
763 1025 case 'basic':
764 1026 $allowed_html = [
765 1027 'b' => [
@@ -899,13 +1161,15 @@
899 1161 return $allowed_html;
900 1162 }
901 1163
902 1164 /**
903 - * Safe print a validated HTML tag.
1165 + * Safe get a validated HTML tag.
904 1166 *
905 - * @param string $tag
1167 + * @param string $tag HTML tag.
1168 + *
1169 + * @return string
906 1170 */
907 - public static function print_validated_html_tag( $tag ) {
1171 + public static function get_validated_html_tag( $tag ) {
908 1172 $allowed_html_wrapper_tags = [
909 1173 'a',
910 1174 'article',
911 1175 'aside',
@@ -925,12 +1189,23 @@
925 1189 'section',
926 1190 'span',
927 1191 ];
928 1192
929 - 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';
930 1194 }
931 1195
932 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 + /**
933 1208 * Insert Some array element
934 1209 *
935 1210 * @param [type] $key The elements will insert nearby this key.
936 1211 * @param [type] $main_array Original array.
@@ -981,9 +1256,9 @@
981 1256
982 1257 $imgSize = [];
983 1258
984 1259 foreach ( $sizes as $key => $img ) {
985 - $imgSize[ $key ] = ucfirst( $key ) . " ({$img['width']}*{$img['height']})";
1260 + $imgSize[ $key ] = esc_html( $key ) . " ({$img['width']}*{$img['height']})";
986 1261 }
987 1262
988 1263 $imgSize['full'] = esc_html__( 'Full size', 'shopbuilder' );
989 1264 $imgSize['rtsb_custom'] = esc_html__( 'Custom image size', 'shopbuilder' );
@@ -993,11 +1268,17 @@
993 1268
994 1269 /**
995 1270 * Free Layouts
996 1271 *
1272 + * @param string $layout Layout.
1273 + *
997 1274 * @return array
998 1275 */
999 - 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 +
1000 1281 $layouts = [
1001 1282 'grid-layout1' => esc_html__( 'Grid Layout 1', 'shopbuilder' ),
1002 1283 'grid-layout2' => esc_html__( 'Grid Layout 2', 'shopbuilder' ),
1003 1284 'list-layout1' => esc_html__( 'List Layout 1', 'shopbuilder' ),
@@ -1030,9 +1311,14 @@
1030 1311 if ( isset( self::$cache[ $cache_key ] ) ) {
1031 1312 return self::$cache[ $cache_key ];
1032 1313 }
1033 1314
1034 - $termList = get_terms( [ $taxonomy ], [ 'hide_empty' => 0 ] );
1315 + $termList = get_terms(
1316 + [
1317 + 'taxonomy' => $taxonomy,
1318 + 'hide_empty' => false,
1319 + ]
1320 + );
1035 1321
1036 1322 if ( is_array( $termList ) && ! empty( $termList ) && empty( $termList['errors'] ) ) {
1037 1323 if ( $placeholder ) {
1038 1324 $terms = [
@@ -1046,8 +1332,25 @@
1046 1332 }
1047 1333 self::$cache[ $cache_key ] = $terms;
1048 1334 return $terms;
1049 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 + }
1050 1353
1051 1354 /**
1052 1355 * Get all terms by attributes
1053 1356 *
@@ -1053,8 +1356,17 @@
1053 1356 *
1054 1357 * @return array
1055 1358 */
1056 1359 public static function get_all_attributes() {
1360 + // Memoize per request: this runs once per product widget while Elementor
1361 + // builds the editor control config, firing a get_terms() query per
1362 + // attribute taxonomy each time. Caching avoids the repeated N queries.
1363 + static $cached = null;
1364 +
1365 + if ( null !== $cached ) {
1366 + return $cached;
1367 + }
1368 +
1057 1369 $terms = [];
1058 1370 $termList = [];
1059 1371
1060 1372 $attributes = wc_get_attribute_taxonomies();
@@ -1061,9 +1373,9 @@
1061 1373
1062 1374 if ( $attributes ) {
1063 1375 foreach ( $attributes as $tax ) {
1064 1376 if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
1065 - $termList[ $tax->attribute_name ] = get_terms( wc_attribute_taxonomy_name( $tax->attribute_name ), 'orderby=name&hide_empty=0' );
1377 + $termList[ $tax->attribute_name ] = get_terms( wc_attribute_taxonomy_name( $tax->attribute_name ) );
1066 1378 }
1067 1379 }
1068 1380 }
1069 1381
@@ -1074,8 +1386,10 @@
1074 1386 }
1075 1387 }
1076 1388 }
1077 1389
1390 + $cached = $terms;
1391 +
1078 1392 return $terms;
1079 1393 }
1080 1394
1081 1395 /**
@@ -1159,9 +1473,8 @@
1159 1473 if ( ! is_admin() ) {
1160 1474 return [];
1161 1475 }
1162 1476
1163 - // TODO:: Return Slug always true for all settings.
1164 1477 $term_list = [];
1165 1478 $args = [
1166 1479 'taxonomy' => $taxonomy,
1167 1480 'hide_empty' => false,
@@ -1231,9 +1544,9 @@
1231 1544
1232 1545 if ( $args['show_rating_count'] ) {
1233 1546 $count .= '<div class="rtsb-count">';
1234 1547 $count .= sprintf(
1235 - /* translators: %s is the number of reviews */
1548 + /* translators: %s is the number of reviews */
1236 1549 _n( '%s Review', '%s Reviews', $rating_count, 'shopbuilder' ),
1237 1550 $rating_count
1238 1551 );
1239 1552 $count .= '</div>';
@@ -1259,8 +1572,28 @@
1259 1572 self::print_html( $html, true );
1260 1573 }
1261 1574
1262 1575 /**
1576 + * Get product simple rating.
1577 + *
1578 + * @return void
1579 + */
1580 + public static function get_product_simple_rating_html() {
1581 + global $product;
1582 + $html = null;
1583 + $rating_count = $product->get_rating_count();
1584 + $average_rating = $product->get_average_rating();
1585 + $html .= '<div class="product-rating">';
1586 + $html .= wc_get_rating_html( $average_rating, $rating_count );
1587 + $html .= ! empty( $html ) ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1588 + $html .= '</div>';
1589 +
1590 + self::print_html( $html, true );
1591 + }
1592 +
1593 +
1594 +
1595 + /**
1263 1596 * Text truncation.
1264 1597 *
1265 1598 * @param string $text_to_truncate Text.
1266 1599 * @param int $limit Limit.
@@ -1302,8 +1635,14 @@
1302 1635 *
1303 1636 * @return void
1304 1637 */
1305 1638 public static function get_badge_html( $text, $class = 'fill' ) {
1639 + if ( self::is_module_active( 'product_badges' ) && 'rtsb_yes' === $text ) {
1640 + do_action( 'rtsb/modules/product_badges/frontend/display' );
1641 +
1642 + return;
1643 + }
1644 +
1306 1645 if ( empty( $text ) ) {
1307 1646 return;
1308 1647 }
1309 1648 ob_start();
@@ -1310,10 +1649,9 @@
1310 1649 ?>
1311 1650
1312 1651 <ul class="rtsb-promotion-list">
1313 1652 <li class="rtsb-promotion-list-item">
1314 - <span
1315 - class="rtsb-tag-<?php echo ! empty( $class ) ? esc_attr( $class ) : ''; ?>"><?php echo esc_html( $text ); ?></span>
1653 + <span class="rtsb-tag-<?php echo ! empty( $class ) ? esc_attr( $class ) : ''; ?>"><?php echo esc_html( $text ); ?></span>
1316 1654 </li>
1317 1655 </ul>
1318 1656
1319 1657 <?php
@@ -1351,10 +1689,42 @@
1351 1689
1352 1690 <?php
1353 1691 self::print_html( ob_get_clean() );
1354 1692 }
1693 + /**
1694 + * Brands HTML
1695 + *
1696 + * @param int $id Product ID.
1697 + * @param string $class Custom class.
1698 + *
1699 + * @return void|string
1700 + */
1701 + public static function get_brands_list( $id, $class = 'rtsb-brand-outline' ) {
1702 + $terms = get_the_terms( $id, 'product_brand' );
1703 + if ( empty( $id ) || empty( $terms ) || is_wp_error( $terms ) ) {
1704 + return '';
1705 + }
1706 + ob_start();
1707 + ?>
1355 1708
1709 + <ul class="rtsb-brand-list <?php echo esc_attr( $class ); ?>">
1710 + <?php
1711 + $brand_list = get_the_term_list(
1712 + $id,
1713 + 'product_brand',
1714 + '<li class="rtsb-brand-list-item">',
1715 + '</li><li class="rtsb-brand-list-item">',
1716 + '</li>'
1717 + );
1356 1718
1719 + self::print_html( $brand_list );
1720 + ?>
1721 + </ul>
1722 +
1723 + <?php
1724 + self::print_html( ob_get_clean() );
1725 + }
1726 +
1357 1727 /**
1358 1728 * Get Featured Image HTML.
1359 1729 *
1360 1730 * @param string $type Image type.
@@ -1364,12 +1734,13 @@
1364 1734 * @param array $custom_img_size Custom image size.
1365 1735 * @param bool $lazy Lazy load check.
1366 1736 * @param bool $hover Hover image.
1367 1737 * @param bool $gallery Gallery image.
1738 + * @param bool $custom_image_id Custom category image ID.
1368 1739 *
1369 1740 * @return string|null
1370 1741 */
1371 - public static function get_product_image_html( $type = 'product', $post_id = null, $f_img_size = 'medium', $default_img_id = null, $custom_img_size = [], $lazy = false, $hover = false, $gallery = false ) {
1742 + public static function get_product_image_html( $type = 'product', $post_id = null, $f_img_size = 'medium', $default_img_id = null, $custom_img_size = [], $lazy = false, $hover = false, $gallery = false, $custom_image_id = 0 ) {
1372 1743 $img_html = null;
1373 1744 $attachment_id = null;
1374 1745 $c_size = false;
1375 1746 $hover_class = '';
@@ -1393,9 +1764,9 @@
1393 1764 if ( 'product' === $type ) {
1394 1765 $a_id = get_post_thumbnail_id( $post_id );
1395 1766 $post_title = get_the_title( $post_id );
1396 1767 } elseif ( 'category' === $type ) {
1397 - $a_id = get_term_meta( $post_id, 'thumbnail_id', true );
1768 + $a_id = $custom_image_id ? $custom_image_id : get_term_meta( $post_id, 'thumbnail_id', true );
1398 1769 $post_title = get_term( $post_id )->name;
1399 1770 } else {
1400 1771 $a_id = $default_img_id;
1401 1772 }
@@ -1456,9 +1827,9 @@
1456 1827
1457 1828 if ( ! $img_html ) {
1458 1829 $hwstring = image_hwstring( 160, 160 );
1459 1830 $attr = isset( $attr['src'] ) ? apply_filters( 'wp_get_attachment_image_attributes', $attr, false, $f_img_size ) : [];
1460 - $attr['class'] = 'default-img';
1831 + $attr['class'] = 'default-img ' . $hover_class;
1461 1832 $attr['src'] = esc_url( rtsb()->get_assets_uri( 'images/demo.png' ) );
1462 1833 $attr['alt'] = esc_html__( 'Default Image', 'shopbuilder' );
1463 1834 $img_html = rtrim( "<img $hwstring" );
1464 1835
@@ -1549,9 +1920,9 @@
1549 1920 if ( empty( $control['value'] ) ) {
1550 1921 return '';
1551 1922 }
1552 1923 if ( is_array( $control['value'] ) ) {
1553 - $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) );
1924 + $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1554 1925 } else {
1555 1926 $cache_key = 'icons_managet_' . str_replace( ' ', '', $control['value'] );
1556 1927 }
1557 1928 if ( isset( self::$cache[ $cache_key ] ) ) {
@@ -1587,9 +1958,13 @@
1587 1958 public static function get_product_swatches( $type ) {
1588 1959 ?>
1589 1960 <div class="rtsb-swatches <?php echo esc_attr( $type ); ?>-layout">
1590 1961 <?php
1591 - do_action( 'rtwpvs_show_archive_variation' );
1962 + if ( class_exists( 'Rtwpvsp' ) ) {
1963 + do_action( 'rtwpvs_show_archive_variation' );
1964 + } else {
1965 + do_action( 'rtsb/vs/showcase/variation' );
1966 + }
1592 1967 ?>
1593 1968 </div>
1594 1969 <?php
1595 1970 }
@@ -1613,20 +1988,18 @@
1613 1988 if ( ! $product->is_on_sale() ) {
1614 1989 return '';
1615 1990 }
1616 1991
1617 - $badge_text = '';
1618 - $percentage = self::calculate_sale_percentage( $product );
1619 -
1620 - if ( $percentage > 0 ) {
1621 - $badge_text = '-' . round( $percentage ) . '%';
1992 + if ( 'text' === $type ) {
1993 + return $text;
1622 1994 }
1623 1995
1624 - if ( 'text' === $type ) {
1625 - $badge_text = $text;
1626 - }
1996 + $percentage = self::calculate_sale_percentage( $product );
1627 1997
1628 - return $badge_text;
1998 + // The product is on sale (checked above). When a percentage can be derived show
1999 + // it; otherwise (e.g. some variable products where variation prices aren't
2000 + // primed) fall back to the sale text so an on-sale product always shows a badge.
2001 + return $percentage > 0 ? '-' . round( $percentage ) . '%' : $text;
1629 2002 }
1630 2003
1631 2004 /**
1632 2005 * Get sale badge
@@ -1638,8 +2011,15 @@
1638 2011 *
1639 2012 * @return string
1640 2013 */
1641 2014 public static function get_promo_badge( $product, $type, $text, $out_of_stock_text ) {
2015 + $disable_badges = self::get_option( 'general', 'guest_user', 'hide_badges', '' );
2016 + $badges_visibility = rtsb()->has_pro() && ! is_user_logged_in() && $disable_badges;
2017 +
2018 + if ( $badges_visibility ) {
2019 + return '';
2020 + }
2021 +
1642 2022 if ( is_null( $product ) ) {
1643 2023 global $product;
1644 2024 }
1645 2025
@@ -1654,20 +2034,18 @@
1654 2034 if ( ! $product->is_on_sale() ) {
1655 2035 return '';
1656 2036 }
1657 2037
1658 - $badge_text = '';
1659 - $percentage = self::calculate_sale_percentage( $product );
1660 -
1661 - if ( $percentage > 0 ) {
1662 - $badge_text = '-' . round( $percentage ) . '%';
2038 + if ( 'text' === $type ) {
2039 + return $text;
1663 2040 }
1664 2041
1665 - if ( 'text' === $type ) {
1666 - $badge_text = $text;
1667 - }
2042 + $percentage = self::calculate_sale_percentage( $product );
1668 2043
1669 - return $badge_text;
2044 + // The product is on sale (checked above). When a percentage can be derived show
2045 + // it; otherwise (e.g. some variable products where variation prices aren't
2046 + // primed) fall back to the sale text so an on-sale product always shows a badge.
2047 + return $percentage > 0 ? '-' . round( $percentage ) . '%' : $text;
1670 2048 }
1671 2049
1672 2050 /**
1673 2051 * Calculate Sale Percentage
@@ -1679,43 +2057,35 @@
1679 2057 public static function calculate_sale_percentage( $product = null ) {
1680 2058 if ( is_null( $product ) ) {
1681 2059 global $product;
1682 2060 }
1683 -
1684 2061 if ( ! $product instanceof WC_Product ) {
1685 2062 return '';
1686 2063 }
1687 -
1688 - $percentage = null;
1689 - $max_percentage = '';
1690 -
2064 + if ( ! $product->is_on_sale() ) {
2065 + return '';
2066 + }
2067 + $max_percentage = 0;
1691 2068 if ( $product->is_type( 'simple' ) ) {
1692 - $max_percentage = ( ( $product->get_regular_price() - $product->get_sale_price() ) / $product->get_regular_price() ) * 100;
2069 + $regular_price = (float) $product->get_regular_price();
2070 + $sale_price = (float) $product->get_sale_price();
2071 + if ( $regular_price > 0 && $sale_price > 0 ) {
2072 + $max_percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2073 + }
1693 2074 } elseif ( $product->is_type( 'variable' ) ) {
1694 - $max_percentage = 0;
2075 + $prices = $product->get_variation_prices( true );
2076 + if ( ! empty( $prices['regular_price'] ) && ! empty( $prices['sale_price'] ) ) {
2077 + foreach ( $prices['regular_price'] as $key => $regular_price ) {
2078 + $sale_price = $prices['sale_price'][ $key ];
1695 2079
1696 - foreach ( $product->get_children() as $child_id ) {
1697 -
1698 - $variation = wc_get_product( $child_id );
1699 -
1700 - $price = $variation->get_regular_price();
1701 - $sale = $variation->get_sale_price();
1702 -
1703 - if ( 0 !== $price && ! empty( $sale ) ) {
1704 - $percentage = ( $price - $sale ) / $price * 100;
2080 + if ( $regular_price > 0 && $sale_price > 0 && $regular_price > $sale_price ) {
2081 + $percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2082 + $max_percentage = max( $max_percentage, $percentage );
2083 + }
1705 2084 }
1706 -
1707 - if ( $percentage > $max_percentage ) {
1708 - $max_percentage = $percentage;
1709 - }
1710 2085 }
1711 2086 }
1712 -
1713 - if ( $max_percentage > 0 ) {
1714 - return round( $max_percentage );
1715 - }
1716 -
1717 - return 0;
2087 + return $max_percentage > 0 ? round( $max_percentage ) : 0;
1718 2088 }
1719 2089
1720 2090 /**
1721 2091 * Pagination
@@ -1726,21 +2096,22 @@
1726 2096 *
1727 2097 * @return string
1728 2098 */
1729 2099 public static function custom_pagination( $pages = '', $range = 4, $ajax = false ) {
1730 - $html = null;
1731 - $showitems = ( $range * 2 ) + 1;
2100 + $html = null;
2101 + $visible_range = apply_filters( 'rtsb/elements/pagination/visible_range', ( $range * 2 ) + 1 );
1732 2102
1733 2103 global $paged;
1734 2104
1735 - if ( is_front_page() ) {
1736 - $paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1;
1737 - } else {
1738 - $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
2105 + // Prefer `paged` (archive pagination), fall back to `page` (paginated singular posts).
2106 + // This handles the case where the shop/archive is set as the front page — `is_front_page()`
2107 + // alone would force `page` and miss the archive `paged` value.
2108 + $paged = get_query_var( 'paged' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2109 + if ( ! $paged ) {
2110 + $paged = get_query_var( 'page' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1739 2111 }
1740 -
1741 - if ( empty( $paged ) ) {
1742 - $paged = 1;
2112 + if ( ! $paged ) {
2113 + $paged = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1743 2114 }
1744 2115
1745 2116 if ( '' === $pages ) {
1746 2117 global $wp_query;
@@ -1757,8 +2128,9 @@
1757 2128
1758 2129 if ( $ajax ) {
1759 2130 $ajaxClass = ' rtsb-pagination-ajax';
1760 2131 $dataAttr = "data-paged='1'";
2132 + $dataAttr .= ' data-visible-range="' . esc_attr( $visible_range ) . '"';
1761 2133 }
1762 2134
1763 2135 if ( 1 !== $pages ) {
1764 2136 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
@@ -1763,29 +2135,29 @@
1763 2135 if ( 1 !== $pages ) {
1764 2136 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
1765 2137 $html .= '<ul class="pagination-list">';
1766 2138
1767 - if ( $paged > 2 && $paged > $range + 1 && $showitems < $pages ) {
2139 + if ( $paged > 2 && $paged > $visible_range + 1 && $visible_range < $pages ) {
1768 2140 $html .= "<li><a data-paged='1' href='" . get_pagenum_link( 1 ) . "' aria-label='First'>&laquo;</a></li>";
1769 2141 }
1770 2142
1771 - if ( $paged > 1 && $showitems < $pages ) {
2143 + if ( $paged > 1 && $visible_range < $pages ) {
1772 2144 $p = $paged - 1;
1773 2145 $html .= "<li><a data-paged='{$p}' href='" . get_pagenum_link( $p ) . "' aria-label='Previous'>&lsaquo;</a></li>";
1774 2146 }
1775 2147
1776 2148 for ( $i = 1; $i <= $pages; $i++ ) {
1777 - if ( 1 != $pages && ( ! ( $i >= $paged + $range + 1 || $i <= $paged - $range - 1 ) || $pages <= $showitems ) ) {
2149 + if ( 1 != $pages && ( ! ( $i >= $paged + $visible_range + 1 || $i <= $paged - $visible_range - 1 ) || $pages <= $visible_range ) ) {
1778 2150 $html .= ( $paged == $i ) ? '<li class="active"><span>' . $i . '</span></li>' : "<li><a data-paged='{$i}' href='" . get_pagenum_link( $i ) . "'>" . $i . '</a></li>';
1779 2151 }
1780 2152 }
1781 2153
1782 - if ( $paged < $pages && $showitems < $pages ) {
2154 + if ( $paged < $pages && $visible_range < $pages ) {
1783 2155 $p = $paged + 1;
1784 2156 $html .= "<li><a data-paged='{$p}' href=\"" . get_pagenum_link( $paged + 1 ) . "\" aria-label='Next'>&rsaquo;</a></li>";
1785 2157 }
1786 2158
1787 - if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $showitems < $pages ) {
2159 + if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $visible_range < $pages ) {
1788 2160 $html .= "<li><a data-paged='{$pages}' href='" . get_pagenum_link( $pages ) . "' aria-label='Last'>&raquo;</a></li>";
1789 2161 }
1790 2162
1791 2163 $html .= '</ul>';
@@ -1897,10 +2269,8 @@
1897 2269 } else {
1898 2270 $class .= ' rtsb-' . esc_attr( str_replace( '_', '-', $type ) );
1899 2271 }
1900 2272
1901 - $html .= '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">';
1902 -
1903 2273 if ( ( 'add_to_cart' === $type ) && ( ! empty( $cart_html ) ) ) {
1904 2274 $html .= $cart_html;
1905 2275 } else {
1906 2276 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
@@ -1905,9 +2275,11 @@
1905 2275 } else {
1906 2276 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
1907 2277 }
1908 2278
1909 - $html .= '</' . esc_attr( $wrapper ) . '>';
2279 + if ( ! empty( $html ) ) {
2280 + $html = '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">' . $html . '</' . esc_attr( $wrapper ) . '>';
2281 + }
1910 2282
1911 2283 return apply_filters( 'rtsb/elements/elementor/get_action_button_by_type', $html );
1912 2284 }
1913 2285
@@ -2008,9 +2380,9 @@
2008 2380 $link['social_network'] = 'Facebook';
2009 2381 $link['social_action'] = 'Share';
2010 2382 break;
2011 2383 case 'twitter':
2012 - $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'] );
2384 + $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'] );
2013 2385 $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>';
2014 2386 $link['attr_title'] = esc_html__( 'Share on Twitter', 'shopbuilder' );
2015 2387 $link['social_network'] = 'Twitter';
2016 2388 $link['social_action'] = 'Tweet';
@@ -2044,9 +2416,9 @@
2044 2416 $link['social_action'] = 'Share';
2045 2417 break;
2046 2418 case 'reddit':
2047 2419 $link['link'] = esc_url( 'https://reddit.com/submit?url=' . $link['url'] . '&title=' . $link['title'] );
2048 - $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>';
2420 + $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>';
2049 2421 $link['attr_title'] = esc_html__( 'Share on Reddit', 'shopbuilder' );
2050 2422 $link['social_network'] = 'Reddit';
2051 2423 $link['social_action'] = 'Share';
2052 2424 break;
@@ -2134,33 +2506,87 @@
2134 2506
2135 2507 /**
2136 2508 * Social Share Platforms.
2137 2509 *
2138 - * @return mixed|null
2510 + * @param string $module Module.
2511 + *
2512 + * @return true|void
2139 2513 */
2140 2514 public static function is_module_active( $module ) {
2141 - $modulelist = ModuleList::instance()->get_data();
2515 + $modulelist = self::get_modules_list();
2516 +
2142 2517 if ( ! empty( $modulelist[ $module ]['active'] ) ) {
2143 2518 return true;
2144 2519 }
2145 2520 }
2521 + /**
2522 + * Checks if catalog pro is active
2523 + *
2524 + * @return boolean
2525 + */
2526 + public static function is_catalog_mode() {
2527 + return function_exists( 'rtsbpro' ) && self::is_module_active( 'catalog_mode' );
2528 + }
2529 + /**
2530 + * Checks if back in stock notifier is active
2531 + *
2532 + * @return boolean
2533 + */
2534 + public static function is_back_in_stock_notifier_enable() {
2535 + return function_exists( 'rtsbpro' ) && self::is_module_active( 'back_in_stock_notifier' );
2536 + }
2146 2537
2538 + /**
2539 + * Elementor Widget Active.
2540 + *
2541 + * @param string $widget Elementor Widget.
2542 + *
2543 + * @return true|void
2544 + */
2545 + public static function is_elementor_widget_active( $widget ) {
2546 + $element_list = self::get_widgets_list();
2147 2547
2548 + if ( ! empty( $element_list[ $widget ]['active'] ) ) {
2549 + return true;
2550 + }
2551 + }
2552 +
2553 + /**
2554 + * Sanitize a media (fileupload) control value into an { id, source } array.
2555 + *
2556 + * The control value is posted as an array (or a JSON string). Using it as a
2557 + * `sanitize_fn` avoids it being flattened to an empty string by the default
2558 + * sanitize_text_field() path in set_options().
2559 + *
2560 + * @param mixed $raw_value Raw posted value.
2561 + *
2562 + * @return array|string
2563 + */
2564 + public static function sanitize_fileupload_value( $raw_value ) {
2565 + $decoded = is_string( $raw_value ) && '' !== $raw_value ? json_decode( wp_unslash( $raw_value ), true ) : $raw_value;
2566 + if ( ! is_array( $decoded ) ) {
2567 + return '';
2568 + }
2569 + return [
2570 + 'id' => isset( $decoded['id'] ) ? absint( $decoded['id'] ) : 0,
2571 + 'source' => isset( $decoded['source'] ) ? esc_url_raw( $decoded['source'] ) : '',
2572 + ];
2573 + }
2574 +
2148 2575 /***
2149 2576 * Save Settings data
2150 2577 *
2151 - * @param $section_id
2152 - * @param $block_id
2153 - * @param $rawOptions
2578 + * @param string $section_id Section ID.
2579 + * @param string $block_id Block ID.
2580 + * @param array $rawOptions Raw Options.
2154 2581 *
2155 2582 * @return array
2156 2583 */
2157 - public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) {
2584 + public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) { // phpcs:ignore Generic.Metrics.NestingLevel.TooHigh
2158 2585 $section_id = ! empty( $section_id ) ? sanitize_text_field( wp_unslash( $section_id ) ) : '';
2159 2586 $block_id = ! empty( $block_id ) ? sanitize_text_field( wp_unslash( $block_id ) ) : '';
2160 2587 $rawOptions = ! empty( $rawOptions ) ? $rawOptions : [];
2161 -
2162 - $results = [
2588 + $results = [
2163 2589 'status' => true,
2164 2590 'message' => '',
2165 2591 ];
2166 2592 if ( ! $section_id || ! $block_id ) {
@@ -2176,15 +2602,15 @@
2176 2602
2177 2603 return $results;
2178 2604 }
2179 2605
2180 - $options = DataModel::source()->get_option( $section_id, [] );
2606 + $options = DataModel::source()->get_option( $section_id, [], false );
2181 2607 $changed = false;
2182 2608 $fields = [];
2183 2609 if ( isset( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) && ! empty( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) ) {
2184 2610 $fields = $sections[ $section_id ]['list'][ $block_id ]['fields'];
2185 2611 }
2186 -
2612 + do_action( 'rtsb/before/save/options', $section_id, $block_id, $rawOptions );
2187 2613 if ( empty( $fields ) ) {
2188 2614 if ( isset( $rawOptions['active'] ) ) {
2189 2615 $changed = true;
2190 2616 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
@@ -2190,21 +2616,49 @@
2190 2616 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
2191 2617 }
2192 2618 } else {
2193 2619 foreach ( $rawOptions as $raw_option_key => $raw_value ) {
2194 - if ( $raw_option_key === 'active' ) {
2620 + if ( 'active' === $raw_option_key ) {
2195 2621 $changed = true;
2196 - $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : '';
2622 + $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : ''; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
2197 2623 } else {
2198 2624 if ( isset( $fields[ $raw_option_key ] ) ) {
2199 2625 $field = $fields[ $raw_option_key ];
2200 - if ( $field['type'] === 'switch' ) {
2626 + if ( 'switch' === $field['type'] ) {
2201 2627 $value = 'on' === $raw_value ? 'on' : '';
2202 2628 } elseif ( 'repeaters' === $field['type'] ) {
2203 - $value = stripslashes_deep( $raw_value );
2629 + $the_value = [];
2630 + $rep_title = [];
2631 + if ( ! empty( $raw_value ) && is_array( $raw_value ) ) {
2632 + foreach ( $raw_value as $key => $value ) {
2633 + if ( is_string( $value ) ) {
2634 + $the_value_decoded = json_decode( stripslashes_deep( $value ) );
2635 + } else {
2636 + $the_value_decoded = $value;
2637 + }
2638 + $cr = $the_value_decoded->title ?? '';
2639 + if ( in_array( $cr, $rep_title, true ) ) {
2640 + continue;
2641 + }
2642 + $rep_title[] = $cr;
2643 + $the_value[] = $the_value_decoded;
2644 + }
2645 + } elseif ( ! empty( $raw_value ) && is_string( $raw_value ) ) {
2646 + $the_value = $raw_value;
2647 + }
2648 + $value = wp_json_encode( $the_value, JSON_UNESCAPED_UNICODE );
2649 + } elseif ( in_array( $field['type'], [ 'product_addons_special_settings', 'checkout_fields' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2650 + $manual_field_value = [];
2651 + if ( is_array( $raw_value ) ) {
2652 + foreach ( $raw_value as $key => $value ) {
2653 + $manual_field_value[] = json_decode( stripslashes( $value ), true );
2654 + }
2655 + $value = wp_json_encode( $manual_field_value, JSON_UNESCAPED_UNICODE );
2656 + } elseif ( is_string( $raw_value ) ) {
2657 + $value = $raw_value;
2658 + }
2204 2659 } else {
2205 - if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) {
2206 -
2660 + if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2207 2661 if ( isset( $raw_value ) && is_array( $raw_value ) ) {
2208 2662 if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2209 2663 $value = array_map( $field['sanitize_fn'], $raw_value );
2210 2664 } else {
@@ -2213,16 +2667,22 @@
2213 2667 } else {
2214 2668 $value = [];
2215 2669 }
2216 2670 } else {
2217 - if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2218 - $value = $field['sanitize_fn']( $raw_value );
2671 + if ( ! empty( $field['sanitize_fn'] ) ) {
2672 + if ( is_callable( $field['sanitize_fn'] ) ) {
2673 + $value = $field['sanitize_fn']( $raw_value );
2674 + } elseif ( 'pass_all' === $field['sanitize_fn'] ) {
2675 + $value = $raw_value;
2676 + } else {
2677 + $value = $field['sanitize_fn']( $raw_value );
2678 + }
2219 2679 } else {
2220 2680 $value = sanitize_text_field( $raw_value );
2221 2681 }
2222 2682 }
2223 2683 }
2224 - $options[ $block_id ][ $raw_option_key ] = $sections[ $section_id ]['list'][ $block_id ]['fields'][ $raw_option_key ]['value'] = $value ?? null;
2684 + $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
2225 2685 $changed = true;
2226 2686 }
2227 2687 }
2228 2688 }
@@ -2242,29 +2702,16 @@
2242 2702
2243 2703 return $results;
2244 2704 }
2245 2705
2246 - /***
2247 - * @param $meta_key
2248 - * @param $meta_value
2706 + /**
2707 + * Count products by taxonomies.
2249 2708 *
2250 - * @return mixed|void
2709 + * @param array $terms Terms.
2710 + * @param string $taxonomy Taxonomy.
2251 2711 *
2252 - *
2253 - * public static function get_post_id_by_meta_key_and_value( $meta_key, $meta_value ) {
2254 - * if( ! $meta_key || ! $meta_value ){
2255 - * return;
2256 - * }
2257 - * global $wpdb;
2258 - * $prepare_guery = $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '%s' and meta_value = '%d'", $meta_key, $meta_value );
2259 - * $the_products = $wpdb->get_col( $prepare_guery );
2260 - * if( count( $the_products ) > 0 ){
2261 - * return $the_products[0];
2262 - * }
2263 - * return;
2264 - * }
2712 + * @return int|void
2265 2713 */
2266 -
2267 2714 public static function count_products_by_taxonomies( $terms, $taxonomy = 'product_cat' ) {
2268 2715 if ( empty( $terms ) || ! is_array( $terms ) ) {
2269 2716 return;
2270 2717 }
@@ -2275,8 +2722,10 @@
2275 2722 ];
2276 2723
2277 2724 if ( 'product_cat' === $taxonomy ) {
2278 2725 $args['product_category_id'] = $terms;
2726 + } elseif ( 'product_brand' === $taxonomy ) {
2727 + $args['product_brand_id'] = $terms;
2279 2728 } else {
2280 2729 $args['product_tag_id'] = $terms;
2281 2730 }
2282 2731
@@ -2285,8 +2734,61 @@
2285 2734 return ! empty( $query->get_products() ) ? count( $query->get_products() ) : 0;
2286 2735 }
2287 2736
2288 2737 /**
2738 + * Count products by attribute terms.
2739 + *
2740 + * @param array $term_ids Term IDs.
2741 + * @param string $relation Relation.
2742 + *
2743 + * @return int
2744 + */
2745 + public static function count_products_by_attribute_terms( $term_ids, $relation = 'AND' ) {
2746 + if ( empty( $term_ids ) || ! is_array( $term_ids ) ) {
2747 + return 0;
2748 + }
2749 +
2750 + $terms_by_taxonomy = [];
2751 +
2752 + foreach ( $term_ids as $term_id ) {
2753 + $term = get_term( $term_id );
2754 + if ( ! is_wp_error( $term ) && $term ) {
2755 + if ( ! isset( $terms_by_taxonomy[ $term->taxonomy ] ) ) {
2756 + $terms_by_taxonomy[ $term->taxonomy ] = [];
2757 + }
2758 + $terms_by_taxonomy[ $term->taxonomy ][] = $term_id;
2759 + }
2760 + }
2761 +
2762 + if ( empty( $terms_by_taxonomy ) ) {
2763 + return 0;
2764 + }
2765 +
2766 + $args = [
2767 + 'status' => 'publish',
2768 + 'limit' => -1,
2769 + 'return' => 'ids',
2770 + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2771 + 'relation' => strtoupper( $relation ),
2772 + ],
2773 + ];
2774 +
2775 + foreach ( $terms_by_taxonomy as $taxonomy => $terms ) {
2776 + $args['tax_query'][] = [
2777 + 'taxonomy' => $taxonomy,
2778 + 'field' => 'term_id',
2779 + 'terms' => $terms,
2780 + 'operator' => 'IN',
2781 + ];
2782 + }
2783 +
2784 + $query = new WC_Product_Query( $args );
2785 + $products = $query->get_products();
2786 +
2787 + return count( $products );
2788 + }
2789 +
2790 + /**
2289 2791 * Check if product filters widget has ajax.
2290 2792 *
2291 2793 * @param string $page Page name.
2292 2794 *
@@ -2300,9 +2802,12 @@
2300 2802 if ( 'page' === get_post_type() ) {
2301 2803 return false;
2302 2804 }
2303 2805
2304 - $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2806 + $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2807 + if ( ! $id ) {
2808 + return false;
2809 + }
2305 2810 $cache_key = 'product_filters_has_ajax_' . $id;
2306 2811 if ( isset( self::$cache[ $cache_key ] ) ) {
2307 2812 return self::$cache[ $cache_key ];
2308 2813 }
@@ -2311,13 +2816,239 @@
2311 2816
2312 2817 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
2313 2818 $ajax[] = isset( $data['settings']['ajax_mode'] ) ? false : true;
2314 2819 }
2820 +
2315 2821 self::$cache[ $cache_key ] = ! empty( $ajax[0] );
2316 2822 return ! empty( $ajax[0] );
2317 2823 }
2318 2824
2319 2825 /**
2826 + * Build the active filter chips HTML from the current request query string.
2827 + *
2828 + * Mirrors the markup produced by the JS `displayAppliedFilter()` method so
2829 + * the chips are visible on initial page load (before AJAX hydration).
2830 + *
2831 + * @return string HTML for the active filter chips, or empty string if none.
2832 + */
2833 + public static function get_active_filters_html() {
2834 + // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only query string parsing for display.
2835 + if ( empty( $_GET ) ) {
2836 + return '';
2837 + }
2838 +
2839 + $skip_keys = [
2840 + 'displayview',
2841 + 'orderby',
2842 + 'paged',
2843 + 'page',
2844 + 'product-page',
2845 + 'rtsb_archive_page',
2846 + 'rtsb_grid_columns',
2847 + 'min_price',
2848 + 'max_price',
2849 + '_pjax',
2850 + ];
2851 +
2852 + // When the "Product Categories" widget is already showing the active
2853 + // category navigation, suppress the duplicate product_cat chip group.
2854 + if ( self::is_product_categories_widget_active() ) {
2855 + $skip_keys[] = 'product_cat';
2856 + }
2857 +
2858 + $groups = [];
2859 +
2860 + foreach ( $_GET as $raw_key => $raw_value ) {
2861 + $key = sanitize_key( $raw_key );
2862 +
2863 + if ( '' === $key || in_array( $key, $skip_keys, true ) ) {
2864 + continue;
2865 + }
2866 +
2867 + if ( 0 === strpos( $key, 'query_type_' ) ) {
2868 + continue;
2869 + }
2870 +
2871 + if ( is_array( $raw_value ) ) {
2872 + continue;
2873 + }
2874 +
2875 + $value = sanitize_text_field( wp_unslash( $raw_value ) );
2876 +
2877 + if ( '' === $value ) {
2878 + continue;
2879 + }
2880 +
2881 + $taxonomy = $key;
2882 + if ( 0 === strpos( $taxonomy, 'filter_' ) ) {
2883 + $taxonomy = 'pa_' . substr( $taxonomy, 7 );
2884 + }
2885 +
2886 + $items = [];
2887 + foreach ( explode( ',', $value ) as $slug ) {
2888 + $slug = trim( $slug );
2889 + if ( '' === $slug ) {
2890 + continue;
2891 + }
2892 + $items[] = [
2893 + 'slug' => $slug,
2894 + 'name' => self::get_active_filter_term_label( $taxonomy, $slug, $key ),
2895 + ];
2896 + }
2897 +
2898 + if ( empty( $items ) ) {
2899 + continue;
2900 + }
2901 +
2902 + $groups[ $key ] = [
2903 + 'label' => self::get_active_filter_group_label( $taxonomy, $key ),
2904 + 'items' => $items,
2905 + 'class' => $key,
2906 + ];
2907 + }
2908 +
2909 + // Price range as a single combined chip.
2910 + if ( isset( $_GET['min_price'] ) || isset( $_GET['max_price'] ) ) {
2911 + $min = isset( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : '';
2912 + $max = isset( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : '';
2913 +
2914 + if ( '' !== $min || '' !== $max ) {
2915 + $symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '';
2916 + $groups['price_filter'] = [
2917 + 'label' => esc_html__( 'Price', 'shopbuilder' ),
2918 + 'class' => 'price_filter',
2919 + 'items' => [
2920 + [
2921 + 'slug' => $min . ',' . $max,
2922 + 'name' => $symbol . $min . ' - ' . $symbol . $max,
2923 + 'extra_class' => ' remove-price',
2924 + ],
2925 + ],
2926 + ];
2927 + }
2928 + }
2929 +
2930 + if ( empty( $groups ) ) {
2931 + return '';
2932 + }
2933 +
2934 + $html = '<div class="rtsb-active-filters">';
2935 +
2936 + foreach ( $groups as $key => $group ) {
2937 + $html .= '<div class="active-filter ' . esc_attr( $group['class'] ) . '">';
2938 +
2939 + if ( ! empty( $group['label'] ) ) {
2940 + $html .= '<div class="filter-name">' . esc_html( $group['label'] ) . ': </div>';
2941 + }
2942 +
2943 + $html .= '<div class="filter-item-container">';
2944 +
2945 + foreach ( $group['items'] as $item ) {
2946 + $extra = isset( $item['extra_class'] ) ? $item['extra_class'] : '';
2947 + $html .= '<div class="filter-item">' . esc_html( $item['name'] );
2948 + $html .= '<span class="remove-filter' . esc_attr( $extra ) . '"';
2949 + $html .= ' title="' . esc_attr( sprintf( /* translators: %s: filter term name */ __( 'Remove %s', 'shopbuilder' ), $item['name'] ) ) . '"';
2950 + $html .= ' data-filter-name="' . esc_attr( $key ) . '"';
2951 + $html .= ' data-filter-value="' . esc_attr( $item['slug'] ) . '">';
2952 + $html .= '<i class="rtsb-icon rtsb-icon-delete"></i></span>';
2953 + $html .= '</div>';
2954 + }
2955 +
2956 + $html .= '</div></div>';
2957 + }
2958 +
2959 + $html .= '<a href="#" class="rtsb-clear-filters"><span class="icon-wrap"><i class="eicon-trash-o"></i></span><span>' . esc_html__( 'Reset Filters', 'shopbuilder' ) . '</span></a>';
2960 + $html .= '</div>';
2961 +
2962 + return $html;
2963 + // phpcs:enable
2964 + }
2965 +
2966 + /**
2967 + * Determine whether the "Product Categories" Elementor widget is present
2968 + * on the current shop/archive layout. Used to avoid duplicating the
2969 + * product_cat chip group when the widget already shows the active term.
2970 + *
2971 + * @return bool
2972 + */
2973 + private static function is_product_categories_widget_active() {
2974 + $elmap = ElementorDataMap::instance();
2975 +
2976 + foreach ( [ 'shop', 'archive' ] as $page ) {
2977 + $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2978 +
2979 + if ( ! $id ) {
2980 + continue;
2981 + }
2982 +
2983 + $widgets = $elmap->get_widget_data( 'rtsb-product-categories-general', [], $id );
2984 +
2985 + if ( ! empty( $widgets ) ) {
2986 + return true;
2987 + }
2988 + }
2989 +
2990 + return false;
2991 + }
2992 +
2993 + /**
2994 + * Resolve the display label for an active filter group (taxonomy heading).
2995 + *
2996 + * @param string $taxonomy Resolved taxonomy slug (e.g. product_cat, pa_color).
2997 + * @param string $raw_key Original URL key (e.g. product_cat, filter_color).
2998 + *
2999 + * @return string
3000 + */
3001 + private static function get_active_filter_group_label( $taxonomy, $raw_key ) {
3002 + if ( 's' === $raw_key || 'search' === $raw_key ) {
3003 + return esc_html__( 'Search', 'shopbuilder' );
3004 + }
3005 + if ( 'rating_filter' === $raw_key ) {
3006 + return esc_html__( 'Ratings', 'shopbuilder' );
3007 + }
3008 + if ( 'sale_filter' === $raw_key ) {
3009 + return esc_html__( 'Sale Filter', 'shopbuilder' );
3010 + }
3011 +
3012 + if ( 0 === strpos( $taxonomy, 'pa_' ) && function_exists( 'wc_attribute_label' ) ) {
3013 + return wc_attribute_label( $taxonomy );
3014 + }
3015 +
3016 + if ( taxonomy_exists( $taxonomy ) ) {
3017 + $tax_obj = get_taxonomy( $taxonomy );
3018 + if ( $tax_obj && ! empty( $tax_obj->labels->name ) ) {
3019 + return $tax_obj->labels->name;
3020 + }
3021 + }
3022 +
3023 + return ucwords( str_replace( [ '_', '-' ], ' ', $raw_key ) );
3024 + }
3025 +
3026 + /**
3027 + * Resolve a term slug to its display name; fall back to a humanised slug.
3028 + *
3029 + * @param string $taxonomy Resolved taxonomy slug.
3030 + * @param string $slug Raw slug from query string.
3031 + * @param string $raw_key Original URL key.
3032 + *
3033 + * @return string
3034 + */
3035 + private static function get_active_filter_term_label( $taxonomy, $slug, $raw_key ) {
3036 + if ( 's' === $raw_key || 'search' === $raw_key || 'rating_filter' === $raw_key ) {
3037 + return rawurldecode( $slug );
3038 + }
3039 +
3040 + if ( taxonomy_exists( $taxonomy ) ) {
3041 + $term = get_term_by( 'slug', $slug, $taxonomy );
3042 + if ( $term && ! is_wp_error( $term ) ) {
3043 + return $term->name;
3044 + }
3045 + }
3046 +
3047 + return ucwords( str_replace( [ '-', '_' ], ' ', rawurldecode( $slug ) ) );
3048 + }
3049 +
3050 + /**
2320 3051 * Check if product has applied filter.
2321 3052 *
2322 3053 * @param string $page Page name.
2323 3054 *
@@ -2330,14 +3061,23 @@
2330 3061
2331 3062 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2332 3063 $elmap = ElementorDataMap::instance();
2333 3064 $ajax = [];
2334 -
3065 + if ( ! $id ) {
3066 + return false;
3067 + }
2335 3068 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
2336 3069 $ajax[] = ! isset( $data['settings']['active_filter'] );
2337 3070
2338 3071 }
2339 3072
3073 + // Also account for the free, non-AJAX "Product Filters" widget so that
3074 + // the active filter chips wrapper renders for it on initial page load.
3075 + foreach ( $elmap->get_widget_data( 'rtsb-product-filters', [], $id ) as $data ) {
3076 + $ajax[] = true;
3077 + unset( $data );
3078 + }
3079 +
2340 3080 return ! empty( $ajax[0] );
2341 3081 }
2342 3082
2343 3083 /**
@@ -2351,31 +3091,88 @@
2351 3091 if ( ! is_admin() ) {
2352 3092 return [];
2353 3093 }
2354 3094
2355 - $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) );
3095 + $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
2356 3096
2357 3097 if ( isset( self::$cache[ $cache_key ] ) ) {
2358 3098 return self::$cache[ $cache_key ];
2359 3099 }
3100 + $results = wp_cache_get( $cache_key, 'shopbuilder' );
3101 + if ( ! $results ) {
3102 + global $wpdb;
3103 + $sql = "SELECT t.term_id, t.name
3104 + FROM {$wpdb->terms} t
3105 + JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
3106 + WHERE tt.taxonomy = 'product_cat'";
2360 3107
2361 - $results = wp_cache_get( $cache_key );
3108 + if ( $search_query ) {
3109 + $sql .= ' AND t.name LIKE %s';
3110 + $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3111 + } else {
3112 + $prepared_sql = $sql; // No need for placeholders.
3113 + }
2362 3114
3115 + $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
3116 + // Cache the results in the object cache for future use.
3117 + wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
3118 + Cache::set_data_cache_key( $cache_key );
3119 + }
3120 +
3121 + $cats = [];
3122 + if ( ! is_array( $results ) && ! count( $results ) ) {
3123 + return $cats;
3124 + }
3125 + foreach ( $results as $row ) {
3126 + $category_id = $row->term_id;
3127 + $category_title = $row->name;
3128 +
3129 + $cats[] = [
3130 + 'value' => $category_id,
3131 + 'label' => $category_title,
3132 + ];
3133 + }
3134 +
3135 + // Cache the results in a static variable for the next call.
3136 + self::$cache[ $cache_key ] = $cats;
3137 + return $cats;
3138 + }
3139 + /**
3140 + * Get WooCommerce product categories.
3141 + *
3142 + * @param string|null $search_query The search string for category names.
3143 + *
3144 + * @return array An array of product categories with 'value' and 'label' keys.
3145 + */
3146 + public static function products_tags_query( $search_query = null ) {
3147 + if ( ! is_admin() ) {
3148 + return [];
3149 + }
3150 +
3151 + $cache_key = 'rtsb_product_tags_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
3152 +
3153 + if ( isset( self::$cache[ $cache_key ] ) ) {
3154 + return self::$cache[ $cache_key ];
3155 + }
3156 + $results = wp_cache_get( $cache_key, 'shopbuilder' );
2363 3157 if ( ! $results ) {
2364 3158 global $wpdb;
2365 3159 $sql = "SELECT t.term_id, t.name
2366 3160 FROM {$wpdb->terms} t
2367 3161 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
2368 - WHERE tt.taxonomy = 'product_cat'";
3162 + WHERE tt.taxonomy = 'product_tag'";
3163 +
2369 3164 if ( $search_query ) {
2370 3165 $sql .= ' AND t.name LIKE %s';
2371 - $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // Escaping and wildcard
3166 + $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
2372 3167 } else {
2373 - $prepared_sql = $sql; // No need for placeholders
3168 + $prepared_sql = $sql; // No need for placeholders.
2374 3169 }
2375 - $results = $wpdb->get_results( $prepared_sql );
2376 - // Cache the results in the object cache for future use
2377 - wp_cache_set( $cache_key, $results ); // Adjust the expiration time as needed
3170 +
3171 + $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
3172 + // Cache the results in the object cache for future use.
3173 + wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
3174 + Cache::set_data_cache_key( $cache_key );
2378 3175 }
2379 3176
2380 3177 $cats = [];
2381 3178 if ( ! is_array( $results ) && ! count( $results ) ) {
@@ -2390,20 +3187,20 @@
2390 3187 'label' => $category_title,
2391 3188 ];
2392 3189 }
2393 3190
2394 - // Cache the results in a static variable for the next call
3191 + // Cache the results in a static variable for the next call.
2395 3192 self::$cache[ $cache_key ] = $cats;
2396 3193 return $cats;
2397 3194 }
2398 3195
2399 3196 /**
2400 - * @param $search_query
3197 + * @param string $search_query Search query.
2401 3198 *
2402 3199 * @return array
2403 3200 */
2404 3201 public static function get_registered_post_types( $search_query = null ) {
2405 - // Get all registered post types
3202 + // Get all registered post types.
2406 3203 $registered_post_types = get_post_types(
2407 3204 [
2408 3205 'public' => true,
2409 3206 '_builtin' => false,
@@ -2416,11 +3213,11 @@
2416 3213 unset( $registered_post_types['rtsb_builder'] );
2417 3214
2418 3215 $post_types = [];
2419 3216
2420 - // Check if a search query is provided
3217 + // Check if a search query is provided.
2421 3218 if ( ! empty( $search_query ) ) {
2422 - // Filter post types based on the search query
3219 + // Filter post types based on the search query.
2423 3220 $registered_post_types = array_filter(
2424 3221 $registered_post_types,
2425 3222 function ( $post_type ) use ( $search_query ) {
2426 3223 return stripos( $post_type->labels->singular_name, $search_query ) !== false;
@@ -2425,9 +3222,9 @@
2425 3222 function ( $post_type ) use ( $search_query ) {
2426 3223 return stripos( $post_type->labels->singular_name, $search_query ) !== false;
2427 3224 }
2428 3225 );
2429 - // Check if 'post' match the search query and include them if they do
3226 + // Check if 'post' match the search query and include them if they do.
2430 3227 if ( stripos( 'post', $search_query ) !== false ) {
2431 3228 $post_types[] = [
2432 3229 'value' => 'post',
2433 3230 'label' => 'Post',
@@ -2439,9 +3236,9 @@
2439 3236 'label' => 'Post',
2440 3237 ];
2441 3238 }
2442 3239
2443 - // Convert the filtered post types to an array
3240 + // Convert the filtered post types to an array.
2444 3241 foreach ( $registered_post_types as $post_type ) {
2445 3242 $post_types[] = [
2446 3243 'value' => $post_type->name,
2447 3244 'label' => $post_type->labels->singular_name,
@@ -2453,9 +3250,10 @@
2453 3250
2454 3251 /**
2455 3252 * Get Post Types.
2456 3253 *
2457 - * @param $search_query
3254 + * @param string $search_query Search query.
3255 + * @param array $args Arguments.
2458 3256 *
2459 3257 * @return array
2460 3258 */
2461 3259 public static function get_post_types( $search_query = null, $args = [] ) {
@@ -2478,11 +3276,12 @@
2478 3276 $query_results = get_posts( $args );
2479 3277 foreach ( $query_results as $post ) {
2480 3278 $posts[] = [
2481 3279 'value' => $post->ID,
2482 - 'label' => $post->post_title,
3280 + 'label' => $post->post_title . ' (ID#' . $post->ID . ')',
2483 3281 ];
2484 3282 }
3283 +
2485 3284 if ( $search_query ) {
2486 3285 if ( strpos( '-error 404', $search_query ) ) {
2487 3286 $posts[] = [
2488 3287 'value' => 'error',
@@ -2489,12 +3288,14 @@
2489 3288 'label' => __( '404 Error', 'shopbuilder' ),
2490 3289 ];
2491 3290 }
2492 3291 } else {
2493 - $posts[] = [
2494 - 'value' => 'error',
2495 - 'label' => __( '404 Error', 'shopbuilder' ),
2496 - ];
3292 + if ( 'page' === $args['post_type'] ) {
3293 + $posts[] = [
3294 + 'value' => 'error',
3295 + 'label' => __( '404 Error', 'shopbuilder' ),
3296 + ];
3297 + }
2497 3298 }
2498 3299 } else {
2499 3300 $posts = self::get_registered_post_types( $search_query );
2500 3301 }
@@ -2522,9 +3323,9 @@
2522 3323 public static function renderView( $viewName, $args = [], $return = false ) {
2523 3324 $viewName = str_replace( '.', '/', $viewName );
2524 3325
2525 3326 if ( ! empty( $args ) && is_array( $args ) ) {
2526 - extract( $args );
3327 + extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
2527 3328 }
2528 3329
2529 3330 $view_file = rtsb()->plugin_path() . '/resources/' . $viewName . '.php';
2530 3331
@@ -2542,37 +3343,48 @@
2542 3343 } else {
2543 3344 include $view_file;
2544 3345 }
2545 3346 }
2546 - /**
2547 - * @param int $limit Best-selling Base on product Limit.
2548 - * @param string $return_type id|Object
2549 - *
2550 - * @return int
2551 - */
2552 - public static function best_selling_products( $limit = 10, $return_type = 'id' ) {
2553 - $cache_key = 'best_selling_products' . $return_type . $limit;
2554 - // if ( isset( self::$cache[ $cache_key ] ) ) {
2555 - // return self::$cache[ $cache_key ];
2556 - // }
2557 - // Check if the data is in the cache
3347 +
3348 + /**
3349 + * Best selling product query.
3350 + *
3351 + * @param int $minimum_sale Minimum sale.
3352 + * @param int $limit Total limit.
3353 + *
3354 + * @return array|mixed
3355 + */
3356 + public static function best_selling_products_ids( $minimum_sale = 1, $limit = 10 ) {
3357 + $cache_key = 'rtsb_best_selling_products_' . $minimum_sale . '_' . $limit;
3358 + // Check if the data is in the cache.
3359 + if ( isset( self::$cache[ $cache_key ] ) ) {
3360 + return self::$cache[ $cache_key ];
3361 + }
2558 3362 $cached_data = get_transient( $cache_key );
2559 3363 if ( $cached_data ) {
3364 + self::$cache[ $cache_key ] = $cached_data;
2560 3365 return $cached_data;
2561 3366 }
2562 -
2563 - $args = [
2564 - 'limit' => $limit,
2565 - 'orderby' => 'total_sales',
2566 - ];
2567 - if ( 'id' === strtolower( $return_type ) ) {
2568 - $args['return'] = 'ids';
2569 - }
2570 - $best_selling = wc_get_products( $args );
2571 - // self::$cache[ $cache_key ] = $best_selling;
2572 - // Cache the result for future use
3367 + global $wpdb;
3368 + $sql = $wpdb->prepare(
3369 + "
3370 + SELECT ID
3371 + FROM {$wpdb->posts} AS p
3372 + LEFT JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id
3373 + WHERE p.post_type = 'product'
3374 + AND p.post_status = 'publish'
3375 + AND pm.meta_key = 'total_sales'
3376 + AND pm.meta_value >= %d
3377 + ORDER BY pm.meta_value + 0 DESC
3378 + LIMIT %d
3379 + ",
3380 + $minimum_sale,
3381 + $limit
3382 + );
3383 + $best_selling = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
3384 + // Cache the result for future use.
2573 3385 set_transient( $cache_key, $best_selling, DAY_IN_SECONDS );
2574 -
3386 + self::$cache[ $cache_key ] = $best_selling;
2575 3387 return $best_selling;
2576 3388 }
2577 3389
2578 3390 /**
@@ -2595,20 +3407,987 @@
2595 3407
2596 3408 if ( isset( self::$cache[ $cache_key ] ) ) {
2597 3409 return self::$cache[ $cache_key ];
2598 3410 }
3411 + $date_created = $product->get_date_created();
3412 + if ( ! $date_created ) {
3413 + return false;
3414 + }
3415 + $publish_timestamp = strtotime( $date_created->date_i18n( 'Y-m-d H:i:s' ) );
2599 3416
2600 - // Get the product publish date
2601 - $publish_timestamp = strtotime( $product->get_date_created()->date_i18n( 'Y-m-d H:i:s' ) );
2602 -
2603 - // Calculate the difference in days
3417 + // Calculate the difference in days.
2604 3418 $days_difference = abs( time() - $publish_timestamp ) / ( 60 * 60 * 24 );
2605 3419
2606 - // Check if the product is considered new
3420 + // Check if the product is considered new.
2607 3421 $is_new = $days_difference <= $days_threshold;
2608 3422
2609 - // Cache the result for a day
3423 + // Cache the result for a day.
2610 3424 self::$cache[ $cache_key ] = $is_new;
2611 3425
2612 3426 return $is_new;
3427 + }
3428 +
3429 + /**
3430 + * Checks if a feature is disabled for guest users based on the provided option.
3431 + *
3432 + * @param string $option The option to retrieve from the item.
3433 + * @param mixed $default The default value if the option is not found.
3434 + *
3435 + * @return bool
3436 + */
3437 + public static function is_guest_feature_disabled( $option, $default ) {
3438 + $disabled = self::get_option( 'general', 'guest_user', $option, $default );
3439 +
3440 + return ! is_user_logged_in() && $disabled;
3441 + }
3442 +
3443 + /**
3444 + * Checks if a feature is disabled for guest users based on the provided option.
3445 + *
3446 + * @return void
3447 + */
3448 + public static function woocommerce_output_all_notices() {
3449 + if ( function_exists( 'wc_print_notices' ) ) :
3450 + echo '<div class="rtsb-notice">';
3451 + woocommerce_output_all_notices();
3452 + echo '</div>';
3453 + endif;
3454 + }
3455 +
3456 + /**
3457 + * @param object $product Product.
3458 + * @return bool
3459 + */
3460 + public static function is_visible_qty_input( $product = null ) {
3461 + $is_visible_qty = true;
3462 + if ( $product instanceof \WC_Product ) {
3463 + if ( $product->is_sold_individually() ) {
3464 + $is_visible_qty = false;
3465 + } elseif ( $product->managing_stock() ) {
3466 + if ( in_array( $product->get_backorders(), [ 'notify' , 'yes' ], true ) ) {
3467 + $is_visible_qty = true;
3468 + } elseif ( $product->get_stock_quantity() < 2 ) {
3469 + $is_visible_qty = false;
3470 + }
3471 + }
3472 + }
3473 + return $is_visible_qty;
3474 + }
3475 +
3476 + /**
3477 + * @param int $object_id Object id.
3478 + * @param string $element_type element type.
3479 + * @param string|null $current_lang null for current language, default for default languages.
3480 + * @return false|mixed|null
3481 + */
3482 + public static function wpml_object_id( $object_id, $element_type, $current_lang = 'default' ) {
3483 + if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) {
3484 + return $object_id;
3485 + }
3486 + if ( ! $object_id || ! $element_type ) {
3487 + return $object_id;
3488 + }
3489 + if ( 'default' === $current_lang ) {
3490 + $lang = apply_filters( 'wpml_default_language', null );
3491 + } else {
3492 + $lang = null;
3493 + }
3494 + return apply_filters( 'wpml_object_id', $object_id, $element_type, false, $lang );
3495 + }
3496 +
3497 + /**
3498 + * @return string
3499 + */
3500 + public static function wpml_current_language() {
3501 + $current = apply_filters( 'wpml_current_language', null );
3502 + $default = apply_filters( 'wpml_default_language', null );
3503 + return $default !== $current ? $current : null;
3504 + }
3505 +
3506 + /**
3507 + * Custom icons.
3508 + *
3509 + * @return string[]
3510 + */
3511 + public static function get_custom_icon_names() {
3512 + return [
3513 + 'heart-empty',
3514 + 'heart',
3515 + 'eye',
3516 + 'exchange',
3517 + 'plus',
3518 + 'minus',
3519 + 'avatar',
3520 + 'pay',
3521 + 'share',
3522 + 'clock',
3523 + 'check-alt',
3524 + 'check',
3525 + 'delete',
3526 + 'marker',
3527 + 'list',
3528 + 'list-2',
3529 + 'power',
3530 + 'cart',
3531 + 'cart-2',
3532 + 'cart-3',
3533 + 'downloads',
3534 + 'zoom',
3535 + 'user-edit',
3536 + 'grid',
3537 + 'filter',
3538 + 'billing',
3539 + 'login',
3540 + 'payment',
3541 + 'search',
3542 + 'edit',
3543 + 'coupon',
3544 + 'arrows-cw',
3545 + 'trash-empty',
3546 + ];
3547 + }
3548 +
3549 + /**
3550 + * Retrieve an array of custom icons with their HTML representation.
3551 + *
3552 + * @return array
3553 + */
3554 + public static function get_icons() {
3555 + $icon = self::get_custom_icon_names();
3556 + $icons_array = [];
3557 + foreach ( $icon as $value ) {
3558 + $icons_array[ $value ] = '<i class="rtsb-icon rtsb-icon-' . $value . '"></i> <span class="icon-name">' . ucfirst( str_replace( '-', ' ', $value ) ) . '</span>';
3559 + }
3560 + return $icons_array;
3561 + }
3562 +
3563 + /**
3564 + * Generates HTML markup for displaying an endpoint icon.
3565 + *
3566 + * @param array $data The endpoint data containing icon information.
3567 + * @param boolean $wrapper Whether to wrap the icon in a span element.
3568 + *
3569 + * @return string
3570 + */
3571 + public static function get_icon_html( $data, $wrapper = true ) {
3572 + if ( empty( $data ) || 'none' === $data['icon_source'] ) {
3573 + return '';
3574 + }
3575 + $endpoint = '';
3576 + $html = $wrapper ? '<span class="icon ' . esc_attr( $endpoint ) . '_icon">' : '';
3577 +
3578 + if ( 'select_icon' === $data['icon_source'] ) {
3579 + $html .= '<i class="rtsb-icon rtsb-icon-' . esc_attr( $data['custom_icon'] ) . '"></i>';
3580 + } else {
3581 + $icon_html = '';
3582 + $icon_url = $data['image_icon']['source'] ?? '';
3583 + $icon_id = $data['image_icon']['id'] ?? 0;
3584 + $extension = ! empty( $icon_url ) ? strtolower( substr( strrchr( $data['image_icon']['source'], '.' ), 1 ) ) : '';
3585 +
3586 + if ( 'svg' === $extension ) {
3587 + $content = file_get_contents( $icon_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
3588 + $is_svg = strpos( $content, '<svg' );
3589 +
3590 + if ( false !== $is_svg ) {
3591 + $icon_html = substr( $content, $is_svg );
3592 + }
3593 + } else {
3594 + $attr = [
3595 + 'class' => 'rtsb-icon-img',
3596 + 'alt' => esc_html( ucfirst( $endpoint ) ) . esc_html__( ' Icon', 'shopbuilder' ),
3597 + ];
3598 +
3599 + $icon_html = wp_get_attachment_image( absint( $icon_id ), 'full', true, $attr );
3600 + }
3601 +
3602 + $html .= $icon_html;
3603 + }
3604 +
3605 + $html .= $wrapper ? '</span>' : '';
3606 +
3607 + return $html;
3608 + }
3609 + /**
3610 + * Flushes rewrite rules.
3611 + *
3612 + * @return void
3613 + */
3614 + public static function maybe_flush_rewrite_rules() {
3615 + add_action( 'wp_loaded', [ __CLASS__, 'flush_rewrite_rules' ] );
3616 + add_action( 'shutdown', [ __CLASS__, 'flush_rewrite_rules_once_more' ] );
3617 + }
3618 +
3619 + /**
3620 + * Flushes rewrite rules if needed.
3621 + *
3622 + * @return void
3623 + */
3624 + public static function flush_rewrite_rules() {
3625 + if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3626 + flush_rewrite_rules();
3627 + }
3628 +
3629 + if ( get_option( 'rtsb_permalinks_flushed' ) === 'yes' ) {
3630 + flush_rewrite_rules();
3631 + delete_option( 'rtsb_permalinks_flushed' );
3632 + }
3633 + }
3634 +
3635 + /**
3636 + * Flushes rewrite rules if needed for second time.
3637 + *
3638 + * @return void
3639 + */
3640 + public static function flush_rewrite_rules_once_more() {
3641 + if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3642 + flush_rewrite_rules();
3643 + update_option( 'rtsb_permalinks_flushed', 'yes' );
3644 + delete_option( 'rtsb_permalinks_need_flush' );
3645 + }
3646 + }
3647 +
3648 + /**
3649 + * Social Share Platforms.
3650 + *
3651 + * @param string $section_id Section ID.
3652 + * @param string $block_id Block ID.
3653 + * @param string $option_key Option key.
3654 + * @param string $compare_key Compare key.
3655 + * @param string $compare_value Compare value.
3656 + * @param array $values Values.
3657 + *
3658 + * @return void
3659 + */
3660 + public static function set_repeater_options( $section_id, $block_id, $option_key, $compare_key, $compare_value, $values ) {
3661 + $modules = DataModel::source()->get_option( $section_id, [], false );
3662 + if ( empty( $modules[ $block_id ] ) ) {
3663 + return;
3664 + }
3665 + $options = $modules[ $block_id ];
3666 + if ( empty( $options[ $option_key ] ) ) {
3667 + return;
3668 + }
3669 + $repeater_options = json_decode( $options[ $option_key ], true );
3670 + // Check if options are not empty and are in array format.
3671 + if ( ! is_array( $repeater_options ) ) {
3672 + return;
3673 + }
3674 + // Use array_column to get an array of values from $compare_key.
3675 + $column_values = array_column( $repeater_options, $compare_key );
3676 + // Use array_search to find the index of the $compare_value.
3677 + $index = array_search( $compare_value, $column_values, true );
3678 + if ( false === $index ) {
3679 + return;
3680 + }
3681 + $repeater_options[ $index ] = wp_parse_args( $values, $repeater_options[ $index ] );
3682 + $options[ $option_key ] = wp_json_encode( $repeater_options );
3683 + $modules[ $block_id ] = $options;
3684 + DataModel::source()->set_option( $section_id, $modules );
3685 + }
3686 +
3687 +
3688 + /**
3689 + * @param array $ids products id.
3690 + * @return array
3691 + */
3692 + public static function get_available_products_by_ids( $ids = [] ) {
3693 + $ids = array_filter(
3694 + $ids,
3695 + function ( $id ) {
3696 + return 'product' === get_post_type( $id ) && 'publish' === get_post_status( $id );
3697 + }
3698 + );
3699 + return $ids;
3700 + }
3701 +
3702 + /**
3703 + * Generate and cache dynamic CSS styles.
3704 + *
3705 + * @param array $options CSS options to apply (e.g. padding, margin).
3706 + * @param string $cache_key Cache key for the CSS.
3707 + * @param array $css_properties An array of CSS properties with selectors and properties.
3708 + *
3709 + * @return void
3710 + */
3711 + public static function dynamic_styles( $options, $cache_key, $css_properties ) {
3712 + $cached_css = wp_cache_get( $cache_key, 'shopbuilder' );
3713 + $style_handle = self::optimized_handle( 'rtsb-frontend' );
3714 +
3715 + if ( false !== $cached_css ) {
3716 + wp_add_inline_style( $style_handle, $cached_css );
3717 + return;
3718 + }
3719 +
3720 + $css_rules = [];
3721 + $grouped_css_rules = [];
3722 +
3723 + foreach ( $css_properties as $option => $details ) {
3724 + if ( ! empty( $options[ $option ] ) ) {
3725 + $selector = $details['selector'] ?? '';
3726 + $property = $details['property'] ?? '';
3727 + $unit = $details['unit'] ?? '';
3728 + $value = $options[ $option ];
3729 +
3730 + if ( empty( $grouped_css_rules[ $selector ] ) ) {
3731 + $grouped_css_rules[ $selector ] = [];
3732 + }
3733 +
3734 + if ( is_array( $property ) ) {
3735 + foreach ( $property as $prop ) {
3736 + $grouped_css_rules[ $selector ][] = $prop . ': ' . $value . $unit . ' !important';
3737 + }
3738 + } else {
3739 + $grouped_css_rules[ $selector ][] = $property . ': ' . $value . $unit . ' !important';
3740 + }
3741 + }
3742 + }
3743 +
3744 + foreach ( $grouped_css_rules as $selector => $properties ) {
3745 + $css_rules[] = $selector . ' {' . implode( '; ', $properties ) . '}';
3746 + }
3747 +
3748 + $dynamic_css = implode( ' ', $css_rules );
3749 +
3750 + wp_cache_set( $cache_key, $dynamic_css, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3751 + Cache::set_data_cache_key( $cache_key );
3752 +
3753 + if ( ! empty( $dynamic_css ) ) {
3754 + wp_add_inline_style( $style_handle, $dynamic_css );
3755 + }
3756 + }
3757 +
3758 + /**
3759 + * Pro version notice.
3760 + *
3761 + * @param string $ver Pro Version.
3762 + * @param string $tab Tab.
3763 + * @param string $name Name.
3764 + * @param bool $enable_free_Link Enable free link.
3765 + *
3766 + * @return array[]
3767 + */
3768 + public static function pro_version_notice( $ver, $tab = 'billing_fields', $name = 'ShopBuilder', $enable_free_Link = true ) {
3769 + return [
3770 + 'version_check' => [
3771 + 'id' => 'version_check',
3772 + 'type' => 'title',
3773 + 'label' => sprintf(
3774 + /* translators: 1: required version, 2: link to the Plugins page */
3775 + esc_html__(
3776 + 'To access all features and settings of this module, please ensure that "%1$s" is updated to version %2$s. %3$s',
3777 + 'shopbuilder'
3778 + ),
3779 + $name,
3780 + sprintf(
3781 + '<br /><u><b>%s</b></u>',
3782 + esc_html( $ver ?? '1.5.0' ) . ' or higher'
3783 + ),
3784 + $enable_free_Link
3785 + ? sprintf(
3786 + '%s <a class="pro-update-required" href="%s" target="_blank" title="%s">%s</a>',
3787 + esc_attr__( 'Update to the latest version', 'shopbuilder' ),
3788 + esc_url( admin_url( 'plugins.php' ) ),
3789 + esc_attr__( 'Go to the Plugin Page', 'shopbuilder' ),
3790 + esc_html__( 'from the Plugins page', 'shopbuilder' )
3791 + )
3792 + : ''
3793 + ),
3794 + 'tab' => $tab,
3795 + 'customClass' => 'checkout-notice',
3796 + ],
3797 + ];
3798 + }
3799 +
3800 + /**
3801 + * Get product gallery ids.
3802 + *
3803 + * @param object $product Product object.
3804 + *
3805 + * @return mixed
3806 + */
3807 + public static function get_cached_gallery_ids( $product ) {
3808 + $product_id = $product->get_id();
3809 + $cache_key = 'rtsb_product_gallery_ids_' . $product_id;
3810 +
3811 + if ( isset( self::$cache[ $cache_key ] ) ) {
3812 + return self::$cache[ $cache_key ];
3813 + }
3814 +
3815 + $cached_result = wp_cache_get( $cache_key, 'shopbuilder' );
3816 +
3817 + if ( false !== $cached_result ) {
3818 + self::$cache[ $cache_key ] = $cached_result;
3819 +
3820 + return $cached_result;
3821 + }
3822 +
3823 + $gallery_ids = $product->get_gallery_image_ids();
3824 + self::$cache[ $cache_key ] = $gallery_ids;
3825 +
3826 + wp_cache_set( $cache_key, $gallery_ids, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3827 + Cache::set_data_cache_key( $cache_key );
3828 +
3829 + return $gallery_ids;
3830 + }
3831 +
3832 + /**
3833 + * Check if optimization setting is turned on in the database.
3834 + *
3835 + * This only checks the user's setting value, without validating
3836 + * whether the cache directory is writable.
3837 + *
3838 + * @return bool
3839 + */
3840 + public static function is_optimization_setting_on() {
3841 + $has_pro = rtsb()->has_pro();
3842 + $pro_ver = defined( 'RTSBPRO_VERSION' ) ? RTSBPRO_VERSION : 0;
3843 +
3844 + if ( $has_pro && version_compare( $pro_ver, '2.0.0', '<' ) ) {
3845 + return false;
3846 + }
3847 +
3848 + $generalList = GeneralList::instance()->get_data();
3849 +
3850 + return 'on' === ( $generalList['optimization']['enable_optimization'] ?? '' );
3851 + }
3852 +
3853 + /**
3854 + * Check if optimization is enabled and the cache directory is writable.
3855 + *
3856 + * @return bool
3857 + */
3858 + public static function is_optimization_enabled() {
3859 + if ( ! self::is_optimization_setting_on() ) {
3860 + return false;
3861 + }
3862 +
3863 + $upload = wp_upload_dir();
3864 + $cache_dir = trailingslashit( $upload['basedir'] ) . 'shopbuilder_uploads/cache/';
3865 +
3866 + if ( ! wp_is_writable( $cache_dir ) ) {
3867 + return false;
3868 + }
3869 +
3870 + return true;
3871 + }
3872 +
3873 + /**
3874 + * Get the optimized handle.
3875 + *
3876 + * @param string $handle Base handle used for script/style IDs.
3877 + *
3878 + * @return string
3879 + */
3880 + public static function optimized_handle( $handle ) {
3881 + $use_optimization = self::is_optimization_enabled();
3882 +
3883 + if ( $use_optimization ) {
3884 + $handle = self::is_contextual_loading() ? self::get_optimized_handle_by_context() : 'rtsb-bundled';
3885 + }
3886 +
3887 + return $handle;
3888 + }
3889 +
3890 + /**
3891 + * Get the optimized handle by context.
3892 + *
3893 + * @return string
3894 + */
3895 + public static function get_optimized_handle_by_context() {
3896 + $context = self::detect_context();
3897 +
3898 + if ( 'account' === $context ) {
3899 + return 'rtsb-bundled-global';
3900 + }
3901 +
3902 + return "rtsb-bundled-$context";
3903 + }
3904 +
3905 + /**
3906 + * Check if Elementor page.
3907 + *
3908 + * @param int $post_id Post ID.
3909 + *
3910 + * @return bool
3911 + */
3912 + public static function is_elementor_page( $post_id = null ) {
3913 + if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
3914 + return false;
3915 + }
3916 +
3917 + $post_id = $post_id ?: get_the_ID();
3918 +
3919 + if ( ! $post_id ) {
3920 + return true;
3921 + }
3922 +
3923 + return Plugin::$instance->documents->get( $post_id )->is_built_with_elementor();
3924 + }
3925 +
3926 + /**
3927 + * Check if shop or archive.
3928 + *
3929 + * @return bool
3930 + */
3931 + public static function is_shop_or_archive() {
3932 + return is_shop() || is_product_category() || is_product_tag() || is_post_type_archive( 'product' ) || BuilderFns::is_archive() || BuilderFns::is_shop() || is_tax( 'product_brand' );
3933 + }
3934 +
3935 + /**
3936 + * Detect context.
3937 + *
3938 + * @return string
3939 + */
3940 + public static function detect_context() {
3941 + switch ( true ) {
3942 + case is_product() || BuilderFns::is_product():
3943 + $context = 'product';
3944 + break;
3945 +
3946 + case is_cart() || BuilderFns::is_cart():
3947 + $context = 'cart';
3948 + break;
3949 +
3950 + case is_checkout() || BuilderFns::is_checkout():
3951 + $context = 'checkout';
3952 + break;
3953 +
3954 + case self::is_shop_or_archive():
3955 + $context = 'shop';
3956 + break;
3957 +
3958 + default:
3959 + $context = 'global';
3960 + break;
3961 + }
3962 +
3963 + return apply_filters( 'rtsb/optimizer/context_detect', $context );
3964 + }
3965 +
3966 + /**
3967 + * Enqueue optimized assets.
3968 + *
3969 + * @return void
3970 + */
3971 + public static function enqueue_optimized_assets() {
3972 + $asset_registry = AssetRegistry::instance();
3973 + $bundled_assets = $asset_registry->get_bundled_assets();
3974 + $context = self::detect_context();
3975 + $contextual_loading = self::is_contextual_loading();
3976 + $theme_handle = self::find_theme_stylesheet_handle();
3977 +
3978 + if ( $contextual_loading ) {
3979 + // Enqueue JS.
3980 + if ( ! empty( $bundled_assets['js'] ) ) {
3981 + $js_context = isset( $bundled_assets['js'][ $context ] ) ? $context : 'global';
3982 +
3983 + if ( $js_context ) {
3984 + wp_enqueue_script( $bundled_assets['js'][ $js_context ]['handle'] );
3985 + }
3986 + }
3987 +
3988 + // Enqueue CSS.
3989 + if ( ! empty( $bundled_assets['css'] ) ) {
3990 + $css_context = isset( $bundled_assets['css'][ $context ] ) ? $context : 'global';
3991 +
3992 + if ( $css_context ) {
3993 + $handle = $bundled_assets['css'][ $css_context ]['handle'];
3994 +
3995 + wp_enqueue_style( $handle );
3996 +
3997 + if ( ! empty( $theme_handle ) ) {
3998 + self::set_theme_dependency( $theme_handle, $handle );
3999 + }
4000 + }
4001 + }
4002 + } else {
4003 + // Enqueue JS.
4004 + if ( ! empty( $bundled_assets['js'] ) ) {
4005 + wp_enqueue_script( $bundled_assets['js']['handle'] );
4006 + }
4007 +
4008 + // Enqueue CSS.
4009 + if ( ! empty( $bundled_assets['css'] ) ) {
4010 + $handle = $bundled_assets['css']['handle'];
4011 +
4012 + wp_enqueue_style( $handle );
4013 +
4014 + if ( ! empty( $theme_handle ) ) {
4015 + self::set_theme_dependency( $theme_handle, $handle );
4016 + }
4017 + }
4018 + }
4019 + }
4020 +
4021 + /**
4022 + * Find theme stylesheet handle.
4023 + *
4024 + * @return string|false
4025 + */
4026 + private static function find_theme_stylesheet_handle() {
4027 + static $cached_handle = null;
4028 +
4029 + if ( null !== $cached_handle ) {
4030 + return $cached_handle;
4031 + }
4032 +
4033 + global $wp_styles;
4034 +
4035 + if ( ! $wp_styles instanceof WP_Styles ) {
4036 + return false;
4037 + }
4038 +
4039 + $stylesheet_uri = get_stylesheet_uri();
4040 + $theme_directory_uri = get_stylesheet_directory_uri();
4041 +
4042 + $hash = md5( $stylesheet_uri );
4043 + $transient_key = 'rtsb_theme_css_handle_' . $hash;
4044 +
4045 + $transient = get_transient( $transient_key );
4046 +
4047 + if ( false !== $transient ) {
4048 + $cached_handle = $transient;
4049 +
4050 + return $transient;
4051 + }
4052 +
4053 + foreach ( $wp_styles->registered as $handle => $style ) {
4054 + $src = $style->src;
4055 +
4056 + if (
4057 + ( $src === $stylesheet_uri || strpos( $src, $theme_directory_uri ) !== false ) &&
4058 + strpos( $src, 'style.css' ) !== false
4059 + ) {
4060 + set_transient( $transient_key, $handle, DAY_IN_SECONDS );
4061 + Cache::set_transient_cache_key( $transient_key );
4062 + $cached_handle = $handle;
4063 +
4064 + return $handle;
4065 + }
4066 + }
4067 +
4068 + $filtered_handle = apply_filters( 'rtsb/optimizer/theme_stylesheet_handle', false );
4069 +
4070 + if ( is_string( $filtered_handle ) && ! empty( $filtered_handle ) ) {
4071 + set_transient( $transient_key, $filtered_handle, DAY_IN_SECONDS );
4072 + Cache::set_transient_cache_key( $transient_key );
4073 + $cached_handle = $filtered_handle;
4074 +
4075 + return $filtered_handle;
4076 + }
4077 +
4078 + set_transient( $transient_key, false, DAY_IN_SECONDS );
4079 + Cache::set_transient_cache_key( $transient_key );
4080 + $cached_handle = false;
4081 +
4082 + return false;
4083 + }
4084 +
4085 + /**
4086 + * Set theme dependency.
4087 + *
4088 + * @param string $theme_handle Theme handle.
4089 + * @param string $our_handle Our handle.
4090 + *
4091 + * @return void
4092 + */
4093 + private static function set_theme_dependency( $theme_handle, $our_handle ) {
4094 + global $wp_styles;
4095 +
4096 + if ( ! isset( $wp_styles->registered[ $theme_handle ] ) ) {
4097 + return;
4098 + }
4099 +
4100 + $theme_style = $wp_styles->registered[ $theme_handle ];
4101 +
4102 + // Add our handle as a dependency if it's not already there.
4103 + if ( ! in_array( $our_handle, $theme_style->deps, true ) ) {
4104 + $theme_style->deps[] = $our_handle;
4105 + }
4106 + }
4107 +
4108 + /**
4109 + * Check if Elementor scripts should be loaded.
4110 + *
4111 + * @return bool
4112 + */
4113 + public static function should_load_elementor_scripts() {
4114 + $data = GeneralList::instance()->get_data()['optimization'] ?? [];
4115 +
4116 + $enable_optimization = $data['enable_optimization'] ?? 'on';
4117 + $load_elementor_scripts = $data['load_elementor_scripts'] ?? 'on';
4118 +
4119 + if ( 'on' !== $enable_optimization ) {
4120 + return true;
4121 + }
4122 +
4123 + return 'on' === $load_elementor_scripts;
4124 + }
4125 +
4126 + /**
4127 + * Locate asset.
4128 + *
4129 + * @param string $relative_path Relative path.
4130 + *
4131 + * @return string|null
4132 + */
4133 + public static function locate_asset( $relative_path ) {
4134 + $free_context = rtsb();
4135 + $pro_context = function_exists( 'rtsbpro' ) && rtsb()->has_pro() ? rtsbpro() : null;
4136 +
4137 + $paths = [];
4138 +
4139 + if ( $pro_context ) {
4140 + $paths[] = $pro_context->get_assets_path( $relative_path );
4141 + }
4142 +
4143 + $paths[] = $free_context->get_assets_path( $relative_path );
4144 +
4145 + foreach ( $paths as $path ) {
4146 + if ( file_exists( $path ) ) {
4147 + return $path;
4148 + }
4149 + }
4150 +
4151 + return null;
4152 + }
4153 +
4154 + /**
4155 + * Enqueue module assets.
4156 + *
4157 + * @param string $handle Asset handle.
4158 + * @param string $module_name Module name.
4159 + * @param array $options Options array: ['type' => 'css|js|both', 'deps' => [], 'context' => null].
4160 + *
4161 + * @return string
4162 + */
4163 + public static function enqueue_module_assets( $handle, $module_name, $options = [] ) {
4164 + $use_optimization = self::is_optimization_enabled();
4165 + $handle = self::optimized_handle( $handle );
4166 +
4167 + if ( $use_optimization ) {
4168 + return $handle;
4169 + }
4170 +
4171 + $defaults = [
4172 + 'type' => 'both',
4173 + 'deps' => [ 'jquery', 'rtsb-public' ],
4174 + 'context' => null,
4175 + 'version' => RTSB_VERSION,
4176 + ];
4177 +
4178 + $config = array_merge( $defaults, $options );
4179 + $rtl_suffix = is_rtl() ? '-rtl' : '';
4180 + $rtl_dir = is_rtl() ? trailingslashit( 'rtl' ) : trailingslashit( 'css' );
4181 + $context = $config['context'] ?: rtsb();
4182 + $load_css = in_array( $config['type'], [ 'css', 'both' ], true );
4183 + $load_js = in_array( $config['type'], [ 'js', 'both' ], true );
4184 +
4185 + // Register CSS if enabled.
4186 + if ( $load_css ) {
4187 + wp_register_style(
4188 + $handle,
4189 + $context->get_assets_uri( $rtl_dir . 'modules/' . $module_name . $rtl_suffix . '.css' ),
4190 + [],
4191 + $config['version']
4192 + );
4193 + }
4194 +
4195 + // Register JS if enabled.
4196 + if ( $load_js ) {
4197 + wp_register_script(
4198 + $handle,
4199 + $context->get_assets_uri( 'js/modules/' . $module_name . '.js' ),
4200 + $config['deps'],
4201 + $config['version'],
4202 + true
4203 + );
4204 + }
4205 +
4206 + if ( $load_css ) {
4207 + wp_enqueue_style( $handle );
4208 +
4209 + $theme_handle = self::find_theme_stylesheet_handle();
4210 +
4211 + if ( ! empty( $theme_handle ) ) {
4212 + self::set_theme_dependency( $theme_handle, $handle );
4213 + }
4214 + }
4215 +
4216 + if ( $load_js ) {
4217 + wp_enqueue_script( $handle );
4218 + }
4219 +
4220 + return $handle;
4221 + }
4222 +
4223 + /**
4224 + * Check if contextual loading is enabled.
4225 + *
4226 + * @return bool
4227 + */
4228 + public static function is_contextual_loading() {
4229 + $data = GeneralList::instance()->get_data()['optimization'] ?? [];
4230 +
4231 + return ! empty( $data['context_asset_loading'] ) && 'on' === $data['context_asset_loading'];
4232 + }
4233 +
4234 + /**
4235 + * Get Modules list with cache support.
4236 + *
4237 + * @return array
4238 + */
4239 + public static function get_modules_list() {
4240 + static $cached_modules = null;
4241 +
4242 + if ( null !== $cached_modules ) {
4243 + return $cached_modules;
4244 + }
4245 +
4246 + $cache_key = 'rtsb_module_list';
4247 + $cache_group = 'shopbuilder';
4248 +
4249 + $cached = wp_cache_get( $cache_key, $cache_group );
4250 +
4251 + if ( false !== $cached ) {
4252 + $cached_modules = $cached;
4253 +
4254 + return $cached;
4255 + }
4256 +
4257 + $modules = ModuleList::instance()->get_data();
4258 +
4259 + wp_cache_set( $cache_key, $modules, $cache_group, 12 * HOUR_IN_SECONDS );
4260 + Cache::set_data_cache_key( $cache_key );
4261 +
4262 + $cached_modules = $modules;
4263 +
4264 + return $modules;
4265 + }
4266 +
4267 + /**
4268 + * Get Elementor widgets list with cache support.
4269 + *
4270 + * @return array
4271 + */
4272 + public static function get_widgets_list() {
4273 + static $cached_widgets = null;
4274 +
4275 + if ( null !== $cached_widgets ) {
4276 + return $cached_widgets;
4277 + }
4278 +
4279 + $cache_key = 'rtsb_elementor_widget_list';
4280 + $cache_group = 'shopbuilder';
4281 +
4282 + $cached = wp_cache_get( $cache_key, $cache_group );
4283 +
4284 + if ( false !== $cached ) {
4285 + $cached_widgets = $cached;
4286 +
4287 + return $cached;
4288 + }
4289 +
4290 + $widgets = ElementList::instance()->get_list();
4291 +
4292 + wp_cache_set( $cache_key, $widgets, $cache_group, 12 * HOUR_IN_SECONDS );
4293 + Cache::set_data_cache_key( $cache_key );
4294 +
4295 + $cached_widgets = $widgets;
4296 +
4297 + return $widgets;
4298 + }
4299 +
4300 + /**
4301 + * Convert the given price to the active currency.
4302 + *
4303 + * @param float $price The original price.
4304 + * @param Object||null $product Product.
4305 + *
4306 + * @return float
4307 + */
4308 + public static function get_currency_base_price( $price, $product = null ) {
4309 + return apply_filters( 'rtsb/convert/currency/price', $price, $product );
4310 + }
4311 + /**
4312 + * Generate a signed URL with a payload.
4313 + *
4314 + * @param array $payload Associative data to encode.
4315 + * @param string $base_url Base URL to append ?key=... (e.g. wc_get_checkout_url()).
4316 + * @param string $key Key name to use in the URL.
4317 + * @return string Signed URL with key parameter.
4318 + */
4319 + public static function generate_signed_url( array $payload, string $base_url, $key = 'key' ) {
4320 + if ( empty( $key ) ) {
4321 + $key = 'key';
4322 + }
4323 + // Convert payload to JSON.
4324 + $data = wp_json_encode( $payload );
4325 + // Create signature.
4326 + $signature = wp_hash( $data );
4327 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
4328 + $encode_key = base64_encode( $data . '::' . $signature );
4329 + // Return full URL with key param.
4330 + return add_query_arg( [ $key => rawurlencode( $encode_key ) ], $base_url );
4331 + }
4332 +
4333 + /**
4334 + * Decode and verify a signed URL key.
4335 + *
4336 + * @param string $key Encoded key from URL.
4337 + * @return array|false Decoded payload array on success, false on failure.
4338 + */
4339 + public static function decode_signed_key( string $key ) {
4340 + if ( empty( $key ) ) {
4341 + return false;
4342 + }
4343 + $key = rawurldecode( wp_unslash( $key ) );
4344 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
4345 + $raw = base64_decode( sanitize_text_field( $key ) );
4346 + if ( ! $raw ) {
4347 + return false;
4348 + }
4349 + $parts = explode( '::', $raw, 2 );
4350 + if ( count( $parts ) !== 2 ) {
4351 + return false;
4352 + }
4353 + list( $data_json, $signature ) = $parts;
4354 + // Validate signature.
4355 + if ( ! hash_equals( wp_hash( $data_json ), $signature ) ) {
4356 + return false;
4357 + }
4358 + $payload = json_decode( $data_json, true );
4359 + return is_array( $payload ) ? $payload : false;
4360 + }
4361 +
4362 + /**
4363 + * Get Loco Translate MO file path for a plugin.
4364 + *
4365 + * @param string $textdomain Plugin textdomain.
4366 + * @return void|false
4367 + */
4368 + public static function load_loco_textdomain( $textdomain ) {
4369 + if ( ! function_exists( 'loco_plugin_version' ) ) {
4370 + return false;
4371 + }
4372 + $lang = WP_LANG_DIR;
4373 + $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4374 + if ( ! file_exists( $path ) && defined( 'LOCO_LANG_DIR' ) ) {
4375 + $lang = LOCO_LANG_DIR;
4376 + $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4377 + }
4378 + if ( ! file_exists( $path ) ) {
4379 + if ( 'shopbuilder' === $textdomain ) {
4380 + $plugin_root = dirname( RTSB_FILE );
4381 + } elseif ( 'shopbuilder-pro' === $textdomain ) {
4382 + $plugin_root = dirname( RTSBPRO_FILE );
4383 + } else {
4384 + return false;
4385 + }
4386 + $path = $plugin_root . '/languages/' . $textdomain . '-' . get_locale() . '.mo';
4387 + }
4388 + if ( ! file_exists( $path ) ) {
4389 + return false;
4390 + }
4391 + load_textdomain( $textdomain, $path );
2613 4392 }
2614 4393 }