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 +2259 -191 2.0.23.4.0 View file →
@@ -11,14 +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;
19 +use WC_Product;
20 +use Elementor\Plugin;
21 +use WC_Product_Query;
15 22 use Elementor\Icons_Manager;
16 23 use RadiusTheme\SB\Models\ReSizer;
24 +use RadiusTheme\SB\Models\Settings;
17 25 use RadiusTheme\SB\Models\DataModel;
18 26 use RadiusTheme\SB\Models\ModuleList;
19 -use RadiusTheme\SB\Models\Settings;
20 -use WC_Product;
27 +use RadiusTheme\SB\Models\GeneralList;
28 +use RadiusTheme\SB\Models\ElementList;
29 +use RadiusTheme\SB\Controllers\AssetRegistry;
21 30
22 31 /**
23 32 * Fns class
24 33 */
@@ -24,8 +33,58 @@
24 33 */
25 34 class Fns {
26 35
27 36 /**
37 + * @var array
38 + */
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 + }
54 +
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 + /**
28 87 * Verify nonce.
29 88 *
30 89 * @return bool
31 90 */
@@ -39,12 +98,118 @@
39 98 return false;
40 99 }
41 100
42 101 /**
43 - * @param $plugin_slug
102 + * Get nonce.
44 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 + *
45 154 * @return bool
46 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 + */
47 212 public static function check_plugin_installed( $plugin_slug ): bool {
48 213 $installed_plugins = get_plugins();
49 214
50 215 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
@@ -50,10 +215,12 @@
50 215 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
51 216 }
52 217
53 218 /**
54 - * @param $plugin_slug
219 + * Check if a plugin is active.
55 220 *
221 + * @param string $plugin_slug Plugin slug.
222 + *
56 223 * @return bool
57 224 */
58 225 public static function check_plugin_active( $plugin_slug ): bool {
59 226 if ( is_plugin_active( $plugin_slug ) ) {
@@ -61,21 +228,71 @@
61 228 }
62 229
63 230 return false;
64 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' );
65 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 +
66 270 /**
67 - * @param $data
271 + * Stripslashes value.
68 272 *
273 + * @param mixed $data Data.
274 + *
69 275 * @return mixed|string
70 276 */
71 277 public static function stripslashes_value( $data ) {
72 - if ( is_string( $data ) && strpos( $data, '\\' ) !== false ) {
73 - return stripslashes( $data );
74 - } elseif ( is_array( $data ) ) {
278 + if ( is_array( $data ) ) {
75 279 foreach ( $data as $key => $value ) {
76 280 $data[ $key ] = self::stripslashes_value( $value );
77 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 + }
78 295 }
79 296
80 297 return $data;
81 298 }
@@ -80,10 +297,12 @@
80 297 return $data;
81 298 }
82 299
83 300 /**
84 - * @param $string
301 + * Multiselect settings field value
85 302 *
303 + * @param string $string String.
304 + *
86 305 * @return array
87 306 */
88 307 public static function multiselect_settings_field_value( $string ) {
89 308
@@ -103,8 +322,10 @@
103 322 return $values;
104 323 }
105 324
106 325 /**
326 + * Check if is block theme
327 + *
107 328 * @return bool
108 329 */
109 330 public static function check_is_block_theme(): bool {
110 331 global $wp_version;
@@ -112,16 +333,19 @@
112 333 return version_compare( $wp_version, '5.9', '>=' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
113 334 }
114 335
115 336 /**
337 + * Locate template
338 + *
116 339 * @param string $template_name Template name.
117 340 * @param string $template_path Template path. (default: '').
118 - * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin
341 + * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin.
119 342 *
120 343 * @return mixed|void
121 344 */
122 345 public static function locate_template( $template_name, $template_path = '', $plugin_path = '' ) {
123 - $template_name = $template_name . '.php';
346 + $template_name = self::sanitize_file_name( $template_name ) . '.php';
347 +
124 348 if ( ! $template_path ) {
125 349 $template_path = rtsb()->get_template_path();
126 350 }
127 351 if ( ! $plugin_path ) {
@@ -143,12 +367,12 @@
143 367 )
144 368 );
145 369
146 370 if ( ! $located ) {
147 - 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 ) ) {
148 374 return apply_filters( 'rtsb/core/locate_template', $plugin_path, $template_name );
149 - } elseif ( $pro_plugin_path && rtsb()->has_pro() && file_exists( $pro_plugin_path ) ) {
150 - return apply_filters( 'rtsb/core/locate_template', $pro_plugin_path, $template_name );
151 375 }
152 376 }
153 377
154 378 /**
@@ -164,8 +388,19 @@
164 388 return apply_filters( 'rtsb/core/locate_template', $located, $template_name );
165 389 }
166 390
167 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 + /**
168 403 * Template Content
169 404 *
170 405 * @param string $template_name Template name.
171 406 * @param array $args Arguments. (default: array).
@@ -174,11 +409,20 @@
174 409 * @param string $plugin_path Fallback path from where file will load if fail to load from template. (default: '').
175 410 *
176 411 * @return false|string|void
177 412 */
178 - public static function load_template( $template_name, array $args = null, $return = false, $template_path = '', $plugin_path = '' ) {
179 - $located = self::locate_template( $template_name, $template_path, $plugin_path );
180 -
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 + }
181 425 if ( ! file_exists( $located ) ) {
182 426 // translators: %s template.
183 427 self::doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'shopbuilder' ), '<code>' . $located . '</code>' ), '1.0' );
184 428
@@ -186,9 +430,9 @@
186 430 }
187 431
188 432 if ( ! empty( $args ) && is_array( $args ) ) {
189 433 $atts = $args;
190 - extract( $args ); // @codingStandardsIgnoreLine
434 + extract( $args ); // @codingStandardsIgnoreLine
191 435 }
192 436
193 437 // Allow 3rd party plugin filter template file from their plugin.
194 438 $located = apply_filters( 'rtsb/core/get_template', $located, $template_name, $args );
@@ -198,8 +442,9 @@
198 442 }
199 443
200 444 do_action( 'rtsb/core/before_template_part', $template_name, $located, $args );
201 445 include $located;
446 +
202 447 do_action( 'rtsb/core/after_template_part', $template_name, $located, $args );
203 448
204 449 if ( $return ) {
205 450 return ob_get_clean();
@@ -302,9 +547,9 @@
302 547 // Return the Builder Template id.
303 548
304 549 if ( get_post_type( get_the_ID() ) == BuilderFns::$post_type_tb ) {
305 550 $product_id = get_post_meta( get_the_ID(), BuilderFns::$product_template_meta, true );
306 - if ( $product_id ) {
551 + if ( $product_id && 'product' === get_post_type( $product_id ) && get_post_status( $product_id ) ) {
307 552 return $product_id;
308 553 }
309 554 }
310 555
@@ -309,15 +554,16 @@
309 554 }
310 555
311 556 global $wpdb;
312 557 $cache_key = 'rtsb_prepared_product_id';
313 - $_post_id = wp_cache_get( $cache_key );
314 -
558 + $_post_id = wp_cache_get( $cache_key, 'shopbuilder' );
315 559 if ( false === $_post_id || 'publish' !== get_post_status( $_post_id ) ) {
560 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
316 561 $_post_id = $wpdb->get_var(
317 - $wpdb->prepare( "SELECT MAX(ID) FROM {$wpdb->prefix}posts WHERE post_type = %s AND post_status = %s", 'product', 'publish' )
562 + $wpdb->prepare( "SELECT MAX(ID) FROM {$wpdb->prefix}posts WHERE post_type = %s AND post_status IN('publish', 'draft')", 'product' )
318 563 );
319 - 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 );
320 566 }
321 567
322 568 return $_post_id;
323 569 }
@@ -322,19 +568,26 @@
322 568 return $_post_id;
323 569 }
324 570
325 571 /**
326 - * Get the product function.
572 + * Get the product function. Only used in single page widgets.
327 573 *
328 574 * @return object
329 575 */
330 576 public static function get_product() {
331 - global $post, $product;
577 + global $product;
578 +
332 579 if ( is_singular( 'product' ) && $product instanceof WC_Product ) {
333 580 return $product;
334 581 }
335 -
336 - return wc_get_product( self::get_prepared_product_id() );
582 + $cache_key = 'prepared_product_for_preview';
583 + if ( isset( self::$cache[ $cache_key ] ) ) {
584 + return self::$cache[ $cache_key ];
585 + }
586 + $product = wc_get_product( self::get_prepared_product_id() );
587 + self::$cache[ $cache_key ] = $product;
588 + do_action( 'rtsb_before_product_template_render' );
589 + return $product;
337 590 }
338 591
339 592 /**
340 593 * Error function
@@ -345,10 +598,11 @@
345 598 *
346 599 * @return void
347 600 */
348 601 public static function doing_it_wrong( $function, $message, $version ) {
602 + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
349 603 $message .= ' Backtrace: ' . wp_debug_backtrace_summary();
350 - _doing_it_wrong( $function, $message, $version );
604 + _doing_it_wrong( esc_html( $function ), wp_kses_post( $message ), esc_html( $version ) );
351 605 }
352 606
353 607 /**
354 608 * @return array
@@ -364,8 +618,15 @@
364 618
365 619 return $pages;
366 620 }
367 621
622 + /**
623 + * Get section items
624 + *
625 + * @param string $section_id Section ID.
626 + *
627 + * @return array
628 + */
368 629 public static function get_section_items( $section_id ) {
369 630 if ( ! $section_id ) {
370 631 return [];
371 632 }
@@ -372,8 +633,16 @@
372 633
373 634 return DataModel::source()->get_option( $section_id, [] );
374 635 }
375 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 + */
376 645 public static function get_options( $section_id, $item_id ) {
377 646 if ( ! $section_id || ! $item_id ) {
378 647 return [];
379 648 }
@@ -384,11 +653,11 @@
384 653
385 654 /**
386 655 * Get options value with default value if options doesn't exist.
387 656 *
388 - * @param $group
389 - * @param $option_key
390 - * @param $default_value
657 + * @param array $group Group.
658 + * @param string $option_key Option key.
659 + * @param string $default_value Default value.
391 660 *
392 661 * @return mixed|string
393 662 */
394 663 public static function get_options_by_default_val( $group, $option_key, $default_value = '' ) {
@@ -400,32 +669,34 @@
400 669 return $group[ $option_key ];
401 670 }
402 671
403 672 /**
404 - * @param string $section_id
405 - * @param string $item_id
406 - * @param string $option_id
407 - * @param null $default EXCEPT multi_checkbox you can provide default value if given option does not set any value
408 - * @param null $type checkbox, multi_checkbox, number
673 + * Get option.
409 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 + *
410 681 * @return bool|int|mixed|null
411 682 */
412 683 public static function get_option( $section_id, $item_id, $option_id, $default = null, $type = null ) {
413 684 $options = self::get_options( $section_id, $item_id );
414 685
415 - if ( $type === 'checkbox' ) {
686 + if ( 'checkbox' === $type ) {
416 687 if ( isset( $options[ $option_id ] ) ) {
417 - return $options[ $option_id ] === 'on';
688 + return 'on' === $options[ $option_id ];
418 689 }
419 690
420 691 return $default;
421 - } elseif ( $type === 'multi_checkbox' ) {
422 - return isset( $options[ $option_id ] ) && is_array( $options[ $option_id ] ) && in_array( $default, $options[ $option_id ] );
423 - } 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 ) {
424 695 return isset( $options[ $option_id ] ) ? absint( $options[ $option_id ] ) : absint( $default );
425 696 }
426 697
427 - return isset( $options[ $option_id ] ) && ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
698 + return ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
428 699 }
429 700
430 701
431 702 /**
@@ -479,13 +750,14 @@
479 750 }
480 751
481 752 if ( strlen( $page_content ) > 0 ) {
482 753 // Search for an existing page with the specified page content (typically a shortcode).
483 - $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
484 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}%" ) );
485 757 } else {
486 758 // Search for an existing page with the specified page slug.
487 - $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
488 760 }
489 761
490 762 $valid_page_found = apply_filters( 'rtsb/core/create_page_id', $valid_page_found, $slug, $page_content, $options );
491 763
@@ -507,12 +779,12 @@
507 779
508 780 // Search for a matching valid trashed page.
509 781 if ( strlen( $page_content ) > 0 ) {
510 782 // Search for an existing page with the specified page content (typically a shortcode).
511 - $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
512 784 } else {
513 785 // Search for an existing page with the specified page slug.
514 - $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
515 787 }
516 788
517 789 if ( $trashed_page_found ) {
518 790 $page_id = $trashed_page_found;
@@ -551,8 +823,13 @@
551 823
552 824 return $page_id;
553 825 }
554 826
827 + /**
828 + * Get allowed HTML tags.
829 + *
830 + * @return array
831 + */
555 832 public static function get_kses_array() {
556 833 return [
557 834 'a' => [
558 835 'class' => [],
@@ -651,13 +928,14 @@
651 928 ];
652 929 }
653 930
654 931
655 - /*
932 + /**
656 933 * Escape output of wishlist icon
657 934 *
658 935 * @param string $data Data to escape.
659 - * @return string Escaped data
936 + *
937 + * @return void
660 938 */
661 939 public static function print_icon( $data ) {
662 940 /**
663 941 * APPLY_FILTERS: rtsb/core/allowed_icon_html
@@ -741,9 +1019,9 @@
741 1019 * @return mixed
742 1020 */
743 1021 public static function allowedHtml( $level = 'basic' ) {
744 1022 $allowed_html = [];
745 - // TODO:: Need Optimize.
1023 +
746 1024 switch ( $level ) {
747 1025 case 'basic':
748 1026 $allowed_html = [
749 1027 'b' => [
@@ -883,8 +1161,51 @@
883 1161 return $allowed_html;
884 1162 }
885 1163
886 1164 /**
1165 + * Safe get a validated HTML tag.
1166 + *
1167 + * @param string $tag HTML tag.
1168 + *
1169 + * @return string
1170 + */
1171 + public static function get_validated_html_tag( $tag ) {
1172 + $allowed_html_wrapper_tags = [
1173 + 'a',
1174 + 'article',
1175 + 'aside',
1176 + 'button',
1177 + 'div',
1178 + 'footer',
1179 + 'h1',
1180 + 'h2',
1181 + 'h3',
1182 + 'h4',
1183 + 'h5',
1184 + 'h6',
1185 + 'header',
1186 + 'main',
1187 + 'nav',
1188 + 'p',
1189 + 'section',
1190 + 'span',
1191 + ];
1192 +
1193 + return in_array( strtolower( $tag ), $allowed_html_wrapper_tags, true ) ? $tag : 'div';
1194 + }
1195 +
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 + /**
887 1208 * Insert Some array element
888 1209 *
889 1210 * @param [type] $key The elements will insert nearby this key.
890 1211 * @param [type] $main_array Original array.
@@ -935,9 +1256,9 @@
935 1256
936 1257 $imgSize = [];
937 1258
938 1259 foreach ( $sizes as $key => $img ) {
939 - $imgSize[ $key ] = ucfirst( $key ) . " ({$img['width']}*{$img['height']})";
1260 + $imgSize[ $key ] = esc_html( $key ) . " ({$img['width']}*{$img['height']})";
940 1261 }
941 1262
942 1263 $imgSize['full'] = esc_html__( 'Full size', 'shopbuilder' );
943 1264 $imgSize['rtsb_custom'] = esc_html__( 'Custom image size', 'shopbuilder' );
@@ -947,11 +1268,17 @@
947 1268
948 1269 /**
949 1270 * Free Layouts
950 1271 *
1272 + * @param string $layout Layout.
1273 + *
951 1274 * @return array
952 1275 */
953 - 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 +
954 1281 $layouts = [
955 1282 'grid-layout1' => esc_html__( 'Grid Layout 1', 'shopbuilder' ),
956 1283 'grid-layout2' => esc_html__( 'Grid Layout 2', 'shopbuilder' ),
957 1284 'list-layout1' => esc_html__( 'List Layout 1', 'shopbuilder' ),
@@ -960,9 +1287,8 @@
960 1287 'slider-layout2' => esc_html__( 'Slider Layout 2', 'shopbuilder' ),
961 1288 'category-single-layout1' => esc_html__( 'Category Single Layout 1', 'shopbuilder' ),
962 1289 'category-layout1' => esc_html__( 'Category Layout 1', 'shopbuilder' ),
963 1290 'category-layout2' => esc_html__( 'Category Layout 2', 'shopbuilder' ),
964 - 'category-layout3' => esc_html__( 'Category Layout 2', 'shopbuilder' ),
965 1291 ];
966 1292
967 1293 return apply_filters( 'rtsb/elements/elementor/free_layouts', $layouts );
968 1294 }
@@ -980,10 +1306,19 @@
980 1306
981 1307 if ( empty( $taxonomy ) ) {
982 1308 return $terms;
983 1309 }
1310 + $cache_key = 'rtsb_get_all_terms_by_tax_name_' . $taxonomy;
1311 + if ( isset( self::$cache[ $cache_key ] ) ) {
1312 + return self::$cache[ $cache_key ];
1313 + }
984 1314
985 - $termList = get_terms( [ $taxonomy ], [ 'hide_empty' => 0 ] );
1315 + $termList = get_terms(
1316 + [
1317 + 'taxonomy' => $taxonomy,
1318 + 'hide_empty' => false,
1319 + ]
1320 + );
986 1321
987 1322 if ( is_array( $termList ) && ! empty( $termList ) && empty( $termList['errors'] ) ) {
988 1323 if ( $placeholder ) {
989 1324 $terms = [
@@ -994,11 +1329,28 @@
994 1329 foreach ( $termList as $term ) {
995 1330 $terms[ $term->term_id ] = esc_html( $term->name );
996 1331 }
997 1332 }
998 -
1333 + self::$cache[ $cache_key ] = $terms;
999 1334 return $terms;
1000 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 + }
1001 1353
1002 1354 /**
1003 1355 * Get all terms by attributes
1004 1356 *
@@ -1004,8 +1356,17 @@
1004 1356 *
1005 1357 * @return array
1006 1358 */
1007 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 +
1008 1369 $terms = [];
1009 1370 $termList = [];
1010 1371
1011 1372 $attributes = wc_get_attribute_taxonomies();
@@ -1012,9 +1373,9 @@
1012 1373
1013 1374 if ( $attributes ) {
1014 1375 foreach ( $attributes as $tax ) {
1015 1376 if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
1016 - $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 ) );
1017 1378 }
1018 1379 }
1019 1380 }
1020 1381
@@ -1025,8 +1386,10 @@
1025 1386 }
1026 1387 }
1027 1388 }
1028 1389
1390 + $cached = $terms;
1391 +
1029 1392 return $terms;
1030 1393 }
1031 1394
1032 1395 /**
@@ -1106,9 +1469,12 @@
1106 1469 *
1107 1470 * @return int|array
1108 1471 */
1109 1472 public static function get_terms( $taxonomy, $first_term = false, $only_parents = false, $return_slug = false ) {
1110 - // TODO:: Return Slug always true for all settings.
1473 + if ( ! is_admin() ) {
1474 + return [];
1475 + }
1476 +
1111 1477 $term_list = [];
1112 1478 $args = [
1113 1479 'taxonomy' => $taxonomy,
1114 1480 'hide_empty' => false,
@@ -1141,25 +1507,67 @@
1141 1507
1142 1508 /**
1143 1509 * Get product rating.
1144 1510 *
1511 + * @param array $args Arguments.
1512 + *
1145 1513 * @return string|void
1146 1514 */
1147 - public static function get_product_rating_html() {
1515 + public static function get_product_rating_html( $args = [] ) {
1148 1516 global $product;
1149 1517
1150 - $html = '';
1151 - $rating_count = $product->get_rating_count();
1152 - $average = $product->get_average_rating();
1518 + $html = '';
1519 + $rating_count = $product->get_rating_count();
1520 + $average_rating = $product->get_average_rating();
1153 1521
1154 - if ( ! $rating_count || empty( wc_get_rating_html( $average, $rating_count ) ) ) {
1155 - return $html;
1522 + if ( ! rtsb()->has_pro() || empty( $args ) ) {
1523 + if ( ! $rating_count || empty( wc_get_rating_html( $average_rating, $rating_count ) ) ) {
1524 + return $html;
1525 + }
1526 +
1527 + $html .= '<div class="product-rating">';
1528 + $html .= wc_get_rating_html( $average_rating, $rating_count );
1529 + $html .= ! empty( $html ) ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1530 + $html .= '</div>';
1531 +
1532 + self::print_html( $html, true );
1533 +
1534 + return;
1156 1535 }
1157 1536
1158 - $html .= '<div class="product-rating">';
1159 - $html .= wc_get_rating_html( $average, $rating_count );
1160 - // TODO:: Disable Rating Count option need to apply.
1161 - $html .= ! empty( $html ) ? '<span class="count">(' . $average . ')</span>' : '';
1537 + if ( empty( $rating_count ) && ! $args['show_empty_rating'] ) {
1538 + return '';
1539 + }
1540 +
1541 + $preset = ! empty( $args['preset'] ) ? $args['preset'] : 'preset1';
1542 + $average = $args['show_average_rating'] ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1543 + $count = '';
1544 +
1545 + if ( $args['show_rating_count'] ) {
1546 + $count .= '<div class="rtsb-count">';
1547 + $count .= sprintf(
1548 + /* translators: %s is the number of reviews */
1549 + _n( '%s Review', '%s Reviews', $rating_count, 'shopbuilder' ),
1550 + $rating_count
1551 + );
1552 + $count .= '</div>';
1553 + }
1554 +
1555 + if ( 'preset2' === $preset ) {
1556 + $average = $args['show_average_rating'] ? '<div class="inner-wrapper"><span class="star-icon"></span><span class="average-rating">' . $average_rating . '</span></div>' : '';
1557 + }
1558 +
1559 + $html .= '<div class="product-rating ' . esc_attr( $preset ) . '">';
1560 +
1561 + if ( 'preset1' === $preset ) {
1562 + $html .= wc_get_rating_html( $average_rating, $rating_count );
1563 + $html .= $average;
1564 + $html .= $count;
1565 + } elseif ( 'preset2' === $preset ) {
1566 + $html .= $average;
1567 + $html .= $count;
1568 + }
1569 +
1162 1570 $html .= '</div>';
1163 1571
1164 1572 self::print_html( $html, true );
1165 1573 }
@@ -1164,8 +1572,28 @@
1164 1572 self::print_html( $html, true );
1165 1573 }
1166 1574
1167 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 + /**
1168 1596 * Text truncation.
1169 1597 *
1170 1598 * @param string $text_to_truncate Text.
1171 1599 * @param int $limit Limit.
@@ -1207,8 +1635,14 @@
1207 1635 *
1208 1636 * @return void
1209 1637 */
1210 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 +
1211 1645 if ( empty( $text ) ) {
1212 1646 return;
1213 1647 }
1214 1648 ob_start();
@@ -1215,10 +1649,9 @@
1215 1649 ?>
1216 1650
1217 1651 <ul class="rtsb-promotion-list">
1218 1652 <li class="rtsb-promotion-list-item">
1219 - <span
1220 - 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>
1221 1654 </li>
1222 1655 </ul>
1223 1656
1224 1657 <?php
@@ -1256,10 +1689,42 @@
1256 1689
1257 1690 <?php
1258 1691 self::print_html( ob_get_clean() );
1259 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 + ?>
1260 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 + );
1261 1718
1719 + self::print_html( $brand_list );
1720 + ?>
1721 + </ul>
1722 +
1723 + <?php
1724 + self::print_html( ob_get_clean() );
1725 + }
1726 +
1262 1727 /**
1263 1728 * Get Featured Image HTML.
1264 1729 *
1265 1730 * @param string $type Image type.
@@ -1269,12 +1734,13 @@
1269 1734 * @param array $custom_img_size Custom image size.
1270 1735 * @param bool $lazy Lazy load check.
1271 1736 * @param bool $hover Hover image.
1272 1737 * @param bool $gallery Gallery image.
1738 + * @param bool $custom_image_id Custom category image ID.
1273 1739 *
1274 1740 * @return string|null
1275 1741 */
1276 - 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 ) {
1277 1743 $img_html = null;
1278 1744 $attachment_id = null;
1279 1745 $c_size = false;
1280 1746 $hover_class = '';
@@ -1298,9 +1764,9 @@
1298 1764 if ( 'product' === $type ) {
1299 1765 $a_id = get_post_thumbnail_id( $post_id );
1300 1766 $post_title = get_the_title( $post_id );
1301 1767 } elseif ( 'category' === $type ) {
1302 - $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 );
1303 1769 $post_title = get_term( $post_id )->name;
1304 1770 } else {
1305 1771 $a_id = $default_img_id;
1306 1772 }
@@ -1361,9 +1827,9 @@
1361 1827
1362 1828 if ( ! $img_html ) {
1363 1829 $hwstring = image_hwstring( 160, 160 );
1364 1830 $attr = isset( $attr['src'] ) ? apply_filters( 'wp_get_attachment_image_attributes', $attr, false, $f_img_size ) : [];
1365 - $attr['class'] = 'default-img';
1831 + $attr['class'] = 'default-img ' . $hover_class;
1366 1832 $attr['src'] = esc_url( rtsb()->get_assets_uri( 'images/demo.png' ) );
1367 1833 $attr['alt'] = esc_html__( 'Default Image', 'shopbuilder' );
1368 1834 $img_html = rtrim( "<img $hwstring" );
1369 1835
@@ -1450,8 +1916,20 @@
1450 1916 *
1451 1917 * @return string
1452 1918 */
1453 1919 public static function icons_manager( $control, $class = '', $builder = 'elementor' ): string {
1920 + if ( empty( $control['value'] ) ) {
1921 + return '';
1922 + }
1923 + if ( is_array( $control['value'] ) ) {
1924 + $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1925 + } else {
1926 + $cache_key = 'icons_managet_' . str_replace( ' ', '', $control['value'] );
1927 + }
1928 + if ( isset( self::$cache[ $cache_key ] ) ) {
1929 + return self::$cache[ $cache_key ];
1930 + }
1931 +
1454 1932 $attributes = [
1455 1933 'aria-hidden' => 'true',
1456 1934 ];
1457 1935
@@ -1464,9 +1942,11 @@
1464 1942 if ( defined( 'ELEMENTOR_VERSION' ) && ! empty( $control ) && 'elementor' === $builder ) {
1465 1943 Icons_Manager::render_icon( $control, $attributes );
1466 1944 }
1467 1945
1468 - return ob_get_clean();
1946 + $icons = ob_get_clean();
1947 + self::$cache[ $cache_key ] = $icons;
1948 + return $icons;
1469 1949 }
1470 1950
1471 1951 /**
1472 1952 * Get product swatches.
@@ -1478,9 +1958,13 @@
1478 1958 public static function get_product_swatches( $type ) {
1479 1959 ?>
1480 1960 <div class="rtsb-swatches <?php echo esc_attr( $type ); ?>-layout">
1481 1961 <?php
1482 - 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 + }
1483 1967 ?>
1484 1968 </div>
1485 1969 <?php
1486 1970 }
@@ -1504,46 +1988,107 @@
1504 1988 if ( ! $product->is_on_sale() ) {
1505 1989 return '';
1506 1990 }
1507 1991
1508 - $badge_text = '';
1509 - $percentage = null;
1510 - $max_percentage = '';
1992 + if ( 'text' === $type ) {
1993 + return $text;
1994 + }
1511 1995
1512 - if ( $product->is_type( 'simple' ) ) {
1513 - $max_percentage = ( ( $product->get_regular_price() - $product->get_sale_price() ) / $product->get_regular_price() ) * 100;
1514 - } elseif ( $product->is_type( 'variable' ) ) {
1515 - $max_percentage = 0;
1996 + $percentage = self::calculate_sale_percentage( $product );
1516 1997
1517 - foreach ( $product->get_children() as $child_id ) {
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;
2002 + }
1518 2003
1519 - $variation = wc_get_product( $child_id );
2004 + /**
2005 + * Get sale badge
2006 + *
2007 + * @param object $product Product Object.
2008 + * @param string $type Badge type.
2009 + * @param string $text Badge text.
2010 + * @param string $out_of_stock_text Out of stock text.
2011 + *
2012 + * @return string
2013 + */
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;
1520 2017
1521 - $price = $variation->get_regular_price();
1522 - $sale = $variation->get_sale_price();
2018 + if ( $badges_visibility ) {
2019 + return '';
2020 + }
1523 2021
1524 - if ( 0 !== $price && ! empty( $sale ) ) {
1525 - $percentage = ( $price - $sale ) / $price * 100;
1526 - }
2022 + if ( is_null( $product ) ) {
2023 + global $product;
2024 + }
1527 2025
1528 - if ( $percentage > $max_percentage ) {
1529 - $max_percentage = $percentage;
1530 - }
1531 - }
2026 + if ( ! $product instanceof WC_Product ) {
2027 + return '';
1532 2028 }
1533 2029
1534 - if ( $max_percentage > 0 ) {
1535 - $badge_text = '-' . round( $max_percentage ) . '%';
2030 + if ( ! $product->is_in_stock() ) {
2031 + return $out_of_stock_text;
1536 2032 }
1537 2033
2034 + if ( ! $product->is_on_sale() ) {
2035 + return '';
2036 + }
2037 +
1538 2038 if ( 'text' === $type ) {
1539 - $badge_text = $text;
2039 + return $text;
1540 2040 }
1541 2041
1542 - return $badge_text;
2042 + $percentage = self::calculate_sale_percentage( $product );
2043 +
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;
1543 2048 }
1544 2049
1545 2050 /**
2051 + * Calculate Sale Percentage
2052 + *
2053 + * @param object $product Product object.
2054 + *
2055 + * @return float|int|string
2056 + */
2057 + public static function calculate_sale_percentage( $product = null ) {
2058 + if ( is_null( $product ) ) {
2059 + global $product;
2060 + }
2061 + if ( ! $product instanceof WC_Product ) {
2062 + return '';
2063 + }
2064 + if ( ! $product->is_on_sale() ) {
2065 + return '';
2066 + }
2067 + $max_percentage = 0;
2068 + if ( $product->is_type( 'simple' ) ) {
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 + }
2074 + } elseif ( $product->is_type( 'variable' ) ) {
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 ];
2079 +
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 + }
2084 + }
2085 + }
2086 + }
2087 + return $max_percentage > 0 ? round( $max_percentage ) : 0;
2088 + }
2089 +
2090 + /**
1546 2091 * Pagination
1547 2092 *
1548 2093 * @param string $pages Pages.
1549 2094 * @param integer $range Range.
@@ -1551,21 +2096,22 @@
1551 2096 *
1552 2097 * @return string
1553 2098 */
1554 2099 public static function custom_pagination( $pages = '', $range = 4, $ajax = false ) {
1555 - $html = null;
1556 - $showitems = ( $range * 2 ) + 1;
2100 + $html = null;
2101 + $visible_range = apply_filters( 'rtsb/elements/pagination/visible_range', ( $range * 2 ) + 1 );
1557 2102
1558 2103 global $paged;
1559 2104
1560 - if ( is_front_page() ) {
1561 - $paged = ( get_query_var( 'page' ) ) ? get_query_var( 'page' ) : 1;
1562 - } else {
1563 - $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
1564 2111 }
1565 -
1566 - if ( empty( $paged ) ) {
1567 - $paged = 1;
2112 + if ( ! $paged ) {
2113 + $paged = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1568 2114 }
1569 2115
1570 2116 if ( '' === $pages ) {
1571 2117 global $wp_query;
@@ -1582,8 +2128,9 @@
1582 2128
1583 2129 if ( $ajax ) {
1584 2130 $ajaxClass = ' rtsb-pagination-ajax';
1585 2131 $dataAttr = "data-paged='1'";
2132 + $dataAttr .= ' data-visible-range="' . esc_attr( $visible_range ) . '"';
1586 2133 }
1587 2134
1588 2135 if ( 1 !== $pages ) {
1589 2136 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
@@ -1588,29 +2135,29 @@
1588 2135 if ( 1 !== $pages ) {
1589 2136 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
1590 2137 $html .= '<ul class="pagination-list">';
1591 2138
1592 - if ( $paged > 2 && $paged > $range + 1 && $showitems < $pages ) {
2139 + if ( $paged > 2 && $paged > $visible_range + 1 && $visible_range < $pages ) {
1593 2140 $html .= "<li><a data-paged='1' href='" . get_pagenum_link( 1 ) . "' aria-label='First'>&laquo;</a></li>";
1594 2141 }
1595 2142
1596 - if ( $paged > 1 && $showitems < $pages ) {
2143 + if ( $paged > 1 && $visible_range < $pages ) {
1597 2144 $p = $paged - 1;
1598 2145 $html .= "<li><a data-paged='{$p}' href='" . get_pagenum_link( $p ) . "' aria-label='Previous'>&lsaquo;</a></li>";
1599 2146 }
1600 2147
1601 2148 for ( $i = 1; $i <= $pages; $i++ ) {
1602 - 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 ) ) {
1603 2150 $html .= ( $paged == $i ) ? '<li class="active"><span>' . $i . '</span></li>' : "<li><a data-paged='{$i}' href='" . get_pagenum_link( $i ) . "'>" . $i . '</a></li>';
1604 2151 }
1605 2152 }
1606 2153
1607 - if ( $paged < $pages && $showitems < $pages ) {
2154 + if ( $paged < $pages && $visible_range < $pages ) {
1608 2155 $p = $paged + 1;
1609 2156 $html .= "<li><a data-paged='{$p}' href=\"" . get_pagenum_link( $paged + 1 ) . "\" aria-label='Next'>&rsaquo;</a></li>";
1610 2157 }
1611 2158
1612 - if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $showitems < $pages ) {
2159 + if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $visible_range < $pages ) {
1613 2160 $html .= "<li><a data-paged='{$pages}' href='" . get_pagenum_link( $pages ) . "' aria-label='Last'>&raquo;</a></li>";
1614 2161 }
1615 2162
1616 2163 $html .= '</ul>';
@@ -1666,9 +2213,8 @@
1666 2213 */
1667 2214 do_action( 'rtsb/elements/elementor/additional_action_buttons', $items );
1668 2215 $html .= ob_get_clean();
1669 2216
1670 - // $html .= self::get_action_button_by_type( $items, 'quick_checkout' );
1671 2217 $html .= self::get_action_button_by_type( $items, 'wishlist' );
1672 2218 $html .= self::get_action_button_by_type( $items, 'compare' );
1673 2219 $html .= self::get_action_button_by_type( $items, 'quick_view' );
1674 2220 $html .= '</ul>';
@@ -1681,9 +2227,8 @@
1681 2227 } elseif ( 'preset1' === $preset || 'preset2' === $preset || 'preset4' === $preset ) {
1682 2228 $html .= '<div class="rtsb-action-buttons ' . esc_attr( $class ) . '">';
1683 2229 $html .= '<ul class="rtsb-action-button-list">';
1684 2230 $html .= self::get_action_button_by_type( $items, 'add_to_cart', $ajax_cart );
1685 - // $html .= self::get_action_button_by_type( $items, 'quick_checkout' );
1686 2231
1687 2232 ob_start();
1688 2233 /**
1689 2234 * Additional formatted action buttons hook.
@@ -1724,10 +2269,8 @@
1724 2269 } else {
1725 2270 $class .= ' rtsb-' . esc_attr( str_replace( '_', '-', $type ) );
1726 2271 }
1727 2272
1728 - $html .= '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">';
1729 -
1730 2273 if ( ( 'add_to_cart' === $type ) && ( ! empty( $cart_html ) ) ) {
1731 2274 $html .= $cart_html;
1732 2275 } else {
1733 2276 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
@@ -1732,9 +2275,11 @@
1732 2275 } else {
1733 2276 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
1734 2277 }
1735 2278
1736 - $html .= '</' . esc_attr( $wrapper ) . '>';
2279 + if ( ! empty( $html ) ) {
2280 + $html = '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">' . $html . '</' . esc_attr( $wrapper ) . '>';
2281 + }
1737 2282
1738 2283 return apply_filters( 'rtsb/elements/elementor/get_action_button_by_type', $html );
1739 2284 }
1740 2285
@@ -1835,9 +2380,9 @@
1835 2380 $link['social_network'] = 'Facebook';
1836 2381 $link['social_action'] = 'Share';
1837 2382 break;
1838 2383 case 'twitter':
1839 - $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'] );
1840 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>';
1841 2386 $link['attr_title'] = esc_html__( 'Share on Twitter', 'shopbuilder' );
1842 2387 $link['social_network'] = 'Twitter';
1843 2388 $link['social_action'] = 'Tweet';
@@ -1871,9 +2416,9 @@
1871 2416 $link['social_action'] = 'Share';
1872 2417 break;
1873 2418 case 'reddit':
1874 2419 $link['link'] = esc_url( 'https://reddit.com/submit?url=' . $link['url'] . '&title=' . $link['title'] );
1875 - $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>';
1876 2421 $link['attr_title'] = esc_html__( 'Share on Reddit', 'shopbuilder' );
1877 2422 $link['social_network'] = 'Reddit';
1878 2423 $link['social_action'] = 'Share';
1879 2424 break;
@@ -1961,33 +2506,87 @@
1961 2506
1962 2507 /**
1963 2508 * Social Share Platforms.
1964 2509 *
1965 - * @return mixed|null
2510 + * @param string $module Module.
2511 + *
2512 + * @return true|void
1966 2513 */
1967 2514 public static function is_module_active( $module ) {
1968 - $modulelist = ModuleList::instance()->get_data();
2515 + $modulelist = self::get_modules_list();
2516 +
1969 2517 if ( ! empty( $modulelist[ $module ]['active'] ) ) {
1970 2518 return true;
1971 2519 }
1972 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 + }
1973 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();
1974 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 +
1975 2575 /***
1976 2576 * Save Settings data
1977 2577 *
1978 - * @param $section_id
1979 - * @param $block_id
1980 - * @param $rawOptions
2578 + * @param string $section_id Section ID.
2579 + * @param string $block_id Block ID.
2580 + * @param array $rawOptions Raw Options.
1981 2581 *
1982 2582 * @return array
1983 2583 */
1984 - 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
1985 2585 $section_id = ! empty( $section_id ) ? sanitize_text_field( wp_unslash( $section_id ) ) : '';
1986 2586 $block_id = ! empty( $block_id ) ? sanitize_text_field( wp_unslash( $block_id ) ) : '';
1987 2587 $rawOptions = ! empty( $rawOptions ) ? $rawOptions : [];
1988 -
1989 - $results = [
2588 + $results = [
1990 2589 'status' => true,
1991 2590 'message' => '',
1992 2591 ];
1993 2592 if ( ! $section_id || ! $block_id ) {
@@ -2003,15 +2602,15 @@
2003 2602
2004 2603 return $results;
2005 2604 }
2006 2605
2007 - $options = DataModel::source()->get_option( $section_id, [] );
2606 + $options = DataModel::source()->get_option( $section_id, [], false );
2008 2607 $changed = false;
2009 2608 $fields = [];
2010 2609 if ( isset( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) && ! empty( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) ) {
2011 2610 $fields = $sections[ $section_id ]['list'][ $block_id ]['fields'];
2012 2611 }
2013 -
2612 + do_action( 'rtsb/before/save/options', $section_id, $block_id, $rawOptions );
2014 2613 if ( empty( $fields ) ) {
2015 2614 if ( isset( $rawOptions['active'] ) ) {
2016 2615 $changed = true;
2017 2616 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
@@ -2017,21 +2616,49 @@
2017 2616 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
2018 2617 }
2019 2618 } else {
2020 2619 foreach ( $rawOptions as $raw_option_key => $raw_value ) {
2021 - if ( $raw_option_key === 'active' ) {
2620 + if ( 'active' === $raw_option_key ) {
2022 2621 $changed = true;
2023 - $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
2024 2623 } else {
2025 2624 if ( isset( $fields[ $raw_option_key ] ) ) {
2026 2625 $field = $fields[ $raw_option_key ];
2027 - if ( $field['type'] === 'switch' ) {
2626 + if ( 'switch' === $field['type'] ) {
2028 2627 $value = 'on' === $raw_value ? 'on' : '';
2029 2628 } elseif ( 'repeaters' === $field['type'] ) {
2030 - $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 + }
2031 2659 } else {
2032 - if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) {
2033 -
2660 + if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2034 2661 if ( isset( $raw_value ) && is_array( $raw_value ) ) {
2035 2662 if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2036 2663 $value = array_map( $field['sanitize_fn'], $raw_value );
2037 2664 } else {
@@ -2040,16 +2667,22 @@
2040 2667 } else {
2041 2668 $value = [];
2042 2669 }
2043 2670 } else {
2044 - if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2045 - $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 + }
2046 2679 } else {
2047 2680 $value = sanitize_text_field( $raw_value );
2048 2681 }
2049 2682 }
2050 2683 }
2051 - $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
2052 2685 $changed = true;
2053 2686 }
2054 2687 }
2055 2688 }
@@ -2069,56 +2702,90 @@
2069 2702
2070 2703 return $results;
2071 2704 }
2072 2705
2073 - /***
2074 - * @param $meta_key
2075 - * @param $meta_value
2706 + /**
2707 + * Count products by taxonomies.
2076 2708 *
2077 - * @return mixed|void
2709 + * @param array $terms Terms.
2710 + * @param string $taxonomy Taxonomy.
2078 2711 *
2079 - *
2080 - * public static function get_post_id_by_meta_key_and_value( $meta_key, $meta_value ) {
2081 - * if( ! $meta_key || ! $meta_value ){
2082 - * return;
2083 - * }
2084 - * global $wpdb;
2085 - * $prepare_guery = $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '%s' and meta_value = '%d'", $meta_key, $meta_value );
2086 - * $the_products = $wpdb->get_col( $prepare_guery );
2087 - * if( count( $the_products ) > 0 ){
2088 - * return $the_products[0];
2089 - * }
2090 - * return;
2091 - * }
2712 + * @return int|void
2092 2713 */
2093 -
2094 2714 public static function count_products_by_taxonomies( $terms, $taxonomy = 'product_cat' ) {
2095 2715 if ( empty( $terms ) || ! is_array( $terms ) ) {
2096 2716 return;
2097 2717 }
2098 2718
2099 - $category_ids = array_map(
2100 - function ( $name ) {
2101 - return sanitize_title( $name );
2102 - },
2103 - $terms
2104 - );
2719 + $args = [
2720 + 'limit' => -1,
2721 + 'return' => 'ids',
2722 + ];
2105 2723
2724 + if ( 'product_cat' === $taxonomy ) {
2725 + $args['product_category_id'] = $terms;
2726 + } elseif ( 'product_brand' === $taxonomy ) {
2727 + $args['product_brand_id'] = $terms;
2728 + } else {
2729 + $args['product_tag_id'] = $terms;
2730 + }
2731 +
2732 + $query = new WC_Product_Query( $args );
2733 +
2734 + return ! empty( $query->get_products() ) ? count( $query->get_products() ) : 0;
2735 + }
2736 +
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 +
2106 2766 $args = [
2107 - 'post_type' => 'product',
2108 - 'posts_per_page' => - 1,
2109 - 'tax_query' => [
2110 - [
2111 - 'taxonomy' => $taxonomy,
2112 - 'field' => 'term_id',
2113 - 'terms' => $category_ids,
2114 - ],
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 ),
2115 2772 ],
2116 2773 ];
2117 2774
2118 - $query = new \WP_Query( $args );
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 + }
2119 2783
2120 - return $query->found_posts;
2784 + $query = new WC_Product_Query( $args );
2785 + $products = $query->get_products();
2786 +
2787 + return count( $products );
2121 2788 }
2122 2789
2123 2790 /**
2124 2791 * Check if product filters widget has ajax.
@@ -2131,9 +2798,20 @@
2131 2798 if ( empty( $page ) ) {
2132 2799 return false;
2133 2800 }
2134 2801
2135 - $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2802 + if ( 'page' === get_post_type() ) {
2803 + return false;
2804 + }
2805 +
2806 + $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2807 + if ( ! $id ) {
2808 + return false;
2809 + }
2810 + $cache_key = 'product_filters_has_ajax_' . $id;
2811 + if ( isset( self::$cache[ $cache_key ] ) ) {
2812 + return self::$cache[ $cache_key ];
2813 + }
2136 2814 $elmap = ElementorDataMap::instance();
2137 2815 $ajax = [];
2138 2816
2139 2817 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
@@ -2139,12 +2817,238 @@
2139 2817 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
2140 2818 $ajax[] = isset( $data['settings']['ajax_mode'] ) ? false : true;
2141 2819 }
2142 2820
2821 + self::$cache[ $cache_key ] = ! empty( $ajax[0] );
2143 2822 return ! empty( $ajax[0] );
2144 2823 }
2145 2824
2146 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 + /**
2147 3051 * Check if product has applied filter.
2148 3052 *
2149 3053 * @param string $page Page name.
2150 3054 *
@@ -2157,14 +3061,23 @@
2157 3061
2158 3062 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2159 3063 $elmap = ElementorDataMap::instance();
2160 3064 $ajax = [];
2161 -
3065 + if ( ! $id ) {
3066 + return false;
3067 + }
2162 3068 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
2163 3069 $ajax[] = ! isset( $data['settings']['active_filter'] );
2164 3070
2165 3071 }
2166 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 +
2167 3080 return ! empty( $ajax[0] );
2168 3081 }
2169 3082
2170 3083 /**
@@ -2174,24 +3087,94 @@
2174 3087 *
2175 3088 * @return array An array of product categories with 'value' and 'label' keys.
2176 3089 */
2177 3090 public static function products_category_query( $search_query = null ) {
2178 - global $wpdb;
3091 + if ( ! is_admin() ) {
3092 + return [];
3093 + }
2179 3094
2180 - $sql = "SELECT t.term_id, t.name
3095 + $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
3096 +
3097 + if ( isset( self::$cache[ $cache_key ] ) ) {
3098 + return self::$cache[ $cache_key ];
3099 + }
3100 + $results = wp_cache_get( $cache_key, 'shopbuilder' );
3101 + if ( ! $results ) {
3102 + global $wpdb;
3103 + $sql = "SELECT t.term_id, t.name
2181 3104 FROM {$wpdb->terms} t
2182 3105 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
2183 3106 WHERE tt.taxonomy = 'product_cat'";
2184 3107
2185 - if ( $search_query ) {
2186 - $sql .= ' AND t.name LIKE %s';
2187 - $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // Escaping and wildcard
2188 - } else {
2189 - $prepared_sql = $sql; // No need for placeholders
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 + }
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 );
2190 3119 }
2191 3120
2192 - $results = $wpdb->get_results( $prepared_sql );
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;
2193 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' );
3157 + if ( ! $results ) {
3158 + global $wpdb;
3159 + $sql = "SELECT t.term_id, t.name
3160 + FROM {$wpdb->terms} t
3161 + JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
3162 + WHERE tt.taxonomy = 'product_tag'";
3163 +
3164 + if ( $search_query ) {
3165 + $sql .= ' AND t.name LIKE %s';
3166 + $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3167 + } else {
3168 + $prepared_sql = $sql; // No need for placeholders.
3169 + }
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 );
3175 + }
3176 +
2194 3177 $cats = [];
2195 3178 if ( ! is_array( $results ) && ! count( $results ) ) {
2196 3179 return $cats;
2197 3180 }
@@ -2204,18 +3187,20 @@
2204 3187 'label' => $category_title,
2205 3188 ];
2206 3189 }
2207 3190
3191 + // Cache the results in a static variable for the next call.
3192 + self::$cache[ $cache_key ] = $cats;
2208 3193 return $cats;
2209 3194 }
2210 3195
2211 3196 /**
2212 - * @param $search_query
3197 + * @param string $search_query Search query.
2213 3198 *
2214 3199 * @return array
2215 3200 */
2216 3201 public static function get_registered_post_types( $search_query = null ) {
2217 - // Get all registered post types
3202 + // Get all registered post types.
2218 3203 $registered_post_types = get_post_types(
2219 3204 [
2220 3205 'public' => true,
2221 3206 '_builtin' => false,
@@ -2228,11 +3213,11 @@
2228 3213 unset( $registered_post_types['rtsb_builder'] );
2229 3214
2230 3215 $post_types = [];
2231 3216
2232 - // Check if a search query is provided
3217 + // Check if a search query is provided.
2233 3218 if ( ! empty( $search_query ) ) {
2234 - // Filter post types based on the search query
3219 + // Filter post types based on the search query.
2235 3220 $registered_post_types = array_filter(
2236 3221 $registered_post_types,
2237 3222 function ( $post_type ) use ( $search_query ) {
2238 3223 return stripos( $post_type->labels->singular_name, $search_query ) !== false;
@@ -2237,9 +3222,9 @@
2237 3222 function ( $post_type ) use ( $search_query ) {
2238 3223 return stripos( $post_type->labels->singular_name, $search_query ) !== false;
2239 3224 }
2240 3225 );
2241 - // 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.
2242 3227 if ( stripos( 'post', $search_query ) !== false ) {
2243 3228 $post_types[] = [
2244 3229 'value' => 'post',
2245 3230 'label' => 'Post',
@@ -2251,9 +3236,9 @@
2251 3236 'label' => 'Post',
2252 3237 ];
2253 3238 }
2254 3239
2255 - // Convert the filtered post types to an array
3240 + // Convert the filtered post types to an array.
2256 3241 foreach ( $registered_post_types as $post_type ) {
2257 3242 $post_types[] = [
2258 3243 'value' => $post_type->name,
2259 3244 'label' => $post_type->labels->singular_name,
@@ -2265,9 +3250,10 @@
2265 3250
2266 3251 /**
2267 3252 * Get Post Types.
2268 3253 *
2269 - * @param $search_query
3254 + * @param string $search_query Search query.
3255 + * @param array $args Arguments.
2270 3256 *
2271 3257 * @return array
2272 3258 */
2273 3259 public static function get_post_types( $search_query = null, $args = [] ) {
@@ -2290,11 +3276,12 @@
2290 3276 $query_results = get_posts( $args );
2291 3277 foreach ( $query_results as $post ) {
2292 3278 $posts[] = [
2293 3279 'value' => $post->ID,
2294 - 'label' => $post->post_title,
3280 + 'label' => $post->post_title . ' (ID#' . $post->ID . ')',
2295 3281 ];
2296 3282 }
3283 +
2297 3284 if ( $search_query ) {
2298 3285 if ( strpos( '-error 404', $search_query ) ) {
2299 3286 $posts[] = [
2300 3287 'value' => 'error',
@@ -2301,12 +3288,14 @@
2301 3288 'label' => __( '404 Error', 'shopbuilder' ),
2302 3289 ];
2303 3290 }
2304 3291 } else {
2305 - $posts[] = [
2306 - 'value' => 'error',
2307 - 'label' => __( '404 Error', 'shopbuilder' ),
2308 - ];
3292 + if ( 'page' === $args['post_type'] ) {
3293 + $posts[] = [
3294 + 'value' => 'error',
3295 + 'label' => __( '404 Error', 'shopbuilder' ),
3296 + ];
3297 + }
2309 3298 }
2310 3299 } else {
2311 3300 $posts = self::get_registered_post_types( $search_query );
2312 3301 }
@@ -2320,6 +3309,1085 @@
2320 3309 * @return array
2321 3310 */
2322 3311 public static function get_elementor_breakpoints() {
2323 3312 return ( new \Elementor\Core\Breakpoints\Manager() )->get_breakpoints_config();
3313 + }
3314 +
3315 + /**
3316 + * Render view.
3317 + *
3318 + * @param string $viewName View name.
3319 + * @param array $args View args.
3320 + * @param boolean $return View return.
3321 + * @return string|void
3322 + */
3323 + public static function renderView( $viewName, $args = [], $return = false ) {
3324 + $viewName = str_replace( '.', '/', $viewName );
3325 +
3326 + if ( ! empty( $args ) && is_array( $args ) ) {
3327 + extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
3328 + }
3329 +
3330 + $view_file = rtsb()->plugin_path() . '/resources/' . $viewName . '.php';
3331 +
3332 + if ( ! file_exists( $view_file ) ) {
3333 + _doing_it_wrong( __FUNCTION__, sprintf( '<code>%s</code> does not exist.', esc_html( $view_file ) ), '1.7.0' );
3334 +
3335 + return;
3336 + }
3337 +
3338 + if ( $return ) {
3339 + ob_start();
3340 + include $view_file;
3341 +
3342 + return ob_get_clean();
3343 + } else {
3344 + include $view_file;
3345 + }
3346 + }
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 + }
3362 + $cached_data = get_transient( $cache_key );
3363 + if ( $cached_data ) {
3364 + self::$cache[ $cache_key ] = $cached_data;
3365 + return $cached_data;
3366 + }
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.
3385 + set_transient( $cache_key, $best_selling, DAY_IN_SECONDS );
3386 + self::$cache[ $cache_key ] = $best_selling;
3387 + return $best_selling;
3388 + }
3389 +
3390 + /**
3391 + * @param null|Object $product Product Object.
3392 + * @param int $days_threshold Date String.
3393 + *
3394 + * @return bool
3395 + */
3396 + public static function is_new_product( $product = null, $days_threshold = 30 ) {
3397 + if ( is_null( $product ) ) {
3398 + global $product;
3399 + }
3400 + if ( ! $product instanceof WC_Product ) {
3401 + return false;
3402 + }
3403 +
3404 + $product_id = $product->get_id();
3405 + $days_threshold = ! empty( $days_threshold ) ? $days_threshold : 30;
3406 + $cache_key = 'is_new_product_' . $product_id . '_' . $days_threshold;
3407 +
3408 + if ( isset( self::$cache[ $cache_key ] ) ) {
3409 + return self::$cache[ $cache_key ];
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' ) );
3416 +
3417 + // Calculate the difference in days.
3418 + $days_difference = abs( time() - $publish_timestamp ) / ( 60 * 60 * 24 );
3419 +
3420 + // Check if the product is considered new.
3421 + $is_new = $days_difference <= $days_threshold;
3422 +
3423 + // Cache the result for a day.
3424 + self::$cache[ $cache_key ] = $is_new;
3425 +
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 );
2324 4392 }
2325 4393 }