PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.3.0
ShopBuilder – WooCommerce Builder For Elementor v3.3.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
shopbuilder / app / Helpers / Fns.php

Fns.php in ShopBuilder – WooCommerce Builder For Elementor 3.3.0, at app/Helpers/Fns.php

4,389 lines 126.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Fns Helpers class
4 *
5 * @package RadiusTheme\SB
6 */
7
8 namespace RadiusTheme\SB\Helpers;
9
10 // Do not allow directly accessing this file.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit( 'This script cannot be accessed directly.' );
13 }
14
15
16 use CodesVault\Howdyqb\DB;
17 use DateTime;
18 use WP_Styles;
19 use WC_Product;
20 use Elementor\Plugin;
21 use WC_Product_Query;
22 use Elementor\Icons_Manager;
23 use RadiusTheme\SB\Models\ReSizer;
24 use RadiusTheme\SB\Models\Settings;
25 use RadiusTheme\SB\Models\DataModel;
26 use RadiusTheme\SB\Models\ModuleList;
27 use RadiusTheme\SB\Models\GeneralList;
28 use RadiusTheme\SB\Models\ElementList;
29 use RadiusTheme\SB\Controllers\AssetRegistry;
30
31 /**
32 * Fns class
33 */
34 class Fns {
35
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 // �
78 Step 1: Theme bypass (no license check).
79 if ( in_array( $current_theme, $allowed_themes, true ) ) {
80 $cached_result = true;
81 return true;
82 }
83 // �
84 Step 3: Normal license validation
85 $license_status = rtsbpro()->get_license( 'license_status' );
86 return ( 'valid' === $license_status );
87 }
88 /**
89 * Verify nonce.
90 *
91 * @return bool
92 */
93 public static function verify_nonce() {
94 $nonce = isset( $_REQUEST[ rtsb()->nonceId ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ rtsb()->nonceId ] ) ) : null;
95 $nonceText = rtsb()->nonceText;
96 if ( wp_verify_nonce( $nonce, $nonceText ) ) {
97 return true;
98 }
99
100 return false;
101 }
102
103 /**
104 * Get nonce.
105 *
106 * @return string|null
107 */
108 public static function enable_loader() {
109 return 'on' !== self::get_option( 'general', 'optimization', 'remove_pre_loader', '' );
110 }
111 /**
112 * Get nonce.
113 *
114 * @return string|null
115 */
116 public static function content_invisible() {
117 $wp_rocket_compatibility = 'on' !== self::get_option( 'general', 'optimization', 'wp_rocket_compatibility', '' );
118 if ( $wp_rocket_compatibility ) {
119 return true;
120 }
121 if ( defined( 'WP_ROCKET_VERSION' ) ) {
122 return false;
123 }
124 return true;
125 }
126 /**
127 * Get nonce.
128 *
129 * @return string|null
130 */
131 public static function get_nonce() {
132 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
133 return isset( $_REQUEST[ rtsb()->nonceId ] ) ? sanitize_text_field( $_REQUEST[ rtsb()->nonceId ] ) : null;
134 }
135
136 /**
137 * Set Session
138 *
139 * @param string $name Name.
140 * @param mixed $value Value.
141 */
142 public static function setSession( $name, $value ) {
143 if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
144 session_start();
145 $_SESSION[ $name ] = $value;
146 } else {
147 $_SESSION[ $name ] = $value;
148 }
149 }
150
151 /**
152 * Get Cookie or Session
153 *
154 * @param string $name Name.
155 *
156 * @return bool
157 */
158 public static function getSession( $name ) {
159 if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
160 session_start();
161 }
162 return $_SESSION[ $name ] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
163 }
164 /**
165 * Remove Session Variable
166 *
167 * @param string $name The name of the session variable to remove.
168 */
169 public static function removeSession( $name ) {
170 if ( ! headers_sent() && session_status() == PHP_SESSION_NONE ) {
171 session_start();
172 unset( $_SESSION[ $name ] );
173 } else {
174 unset( $_SESSION[ $name ] );
175 }
176 }
177 /**
178 * Gets the current timestamp in WordPress timezone.
179 *
180 * @return int Current timestamp.
181 */
182 public static function currentTimestampUTC() {
183 $wp_timezone = wp_timezone();
184 $now = new DateTime( 'now', $wp_timezone ); // Current time in WP timezone.
185 return $now->getTimestamp();
186 }
187 /**
188 * Action.
189 *
190 * @param string $action action.
191 * @return void
192 */
193 public static function add_to_scheduled_hook_list( $action ) {
194 if ( empty( $action ) ) {
195 return;
196 }
197 $schedule = get_option( 'rtsb_cron_schedule_free', [] );
198 $schedule[] = $action;
199 update_option( 'rtsb_cron_schedule_free', array_unique( $schedule ) );
200 }
201 /**
202 * @return DB
203 */
204 public static function DB() {
205 return new DB( 'wpdb' );
206 }
207 /**
208 * Check if a plugin is installed.
209 *
210 * @param string $plugin_slug Plugin slug.
211 *
212 * @return bool
213 */
214 public static function check_plugin_installed( $plugin_slug ): bool {
215 $installed_plugins = get_plugins();
216
217 return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
218 }
219
220 /**
221 * Check if a plugin is active.
222 *
223 * @param string $plugin_slug Plugin slug.
224 *
225 * @return bool
226 */
227 public static function check_plugin_active( $plugin_slug ): bool {
228 if ( is_plugin_active( $plugin_slug ) ) {
229 return true;
230 }
231
232 return false;
233 }
234 /**
235 * Get all user roles.
236 *
237 * @return array
238 */
239 public static function get_current_user_roles() {
240 $current_user = wp_get_current_user();
241 return $current_user->roles;
242 }
243 /**
244 * Get all user roles.
245 *
246 * @return array|false|mixed
247 */
248 public static function get_all_user_roles() {
249 $cache_key = 'rtsb_get_user_roles';
250 $roles = wp_cache_get( $cache_key, 'shopbuilder' );
251
252 if ( empty( $roles ) ) {
253 if ( ! function_exists( 'get_editable_roles' ) ) {
254 require_once ABSPATH . 'wp-admin/includes/user.php';
255 }
256
257 $user_roles = \get_editable_roles();
258 $roles = [];
259
260 foreach ( $user_roles as $key => $role ) {
261 if ( empty( $role['capabilities'] ) ) {
262 continue;
263 }
264 $roles[ $key ] = $role['name'];
265 }
266 }
267 wp_cache_set( $cache_key, $roles, 'shopbuilder' );
268 Cache::set_data_cache_key( $cache_key );
269 return $roles;
270 }
271
272 /**
273 * Stripslashes value.
274 *
275 * @param mixed $data Data.
276 *
277 * @return mixed|string
278 */
279 public static function stripslashes_value( $data ) {
280 if ( is_array( $data ) ) {
281 foreach ( $data as $key => $value ) {
282 $data[ $key ] = self::stripslashes_value( $value );
283 }
284 } elseif ( is_string( $data ) ) {
285 $trimmed = trim( $data );
286 // Try to decode JSON.
287 $json = json_decode( $trimmed, true );
288 if ( json_last_error() === JSON_ERROR_NONE && is_array( $json ) ) {
289 // Recursively stripslashes on decoded array.
290 $json = self::stripslashes_value( $json );
291 return wp_json_encode( $json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
292 }
293 // Not valid JSON, just stripslashes if needed.
294 if ( strpos( $data, '\\' ) !== false ) {
295 return stripslashes( $data );
296 }
297 }
298
299 return $data;
300 }
301
302 /**
303 * Multiselect settings field value
304 *
305 * @param string $string String.
306 *
307 * @return array
308 */
309 public static function multiselect_settings_field_value( $string ) {
310
311 $values = [];
312 if ( empty( $string ) ) {
313 return $values;
314 }
315
316 $stripslashes_data = self::stripslashes_value( $string );
317 if ( is_array( $stripslashes_data ) && count( $stripslashes_data ) ) {
318 foreach ( $stripslashes_data as $item ) {
319 $item = is_array( $item ) ? $item : json_decode( $item, true );
320 $values[] = $item['value'];
321 }
322 }
323
324 return $values;
325 }
326
327 /**
328 * Check if is block theme
329 *
330 * @return bool
331 */
332 public static function check_is_block_theme(): bool {
333 global $wp_version;
334
335 return version_compare( $wp_version, '5.9', '>=' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
336 }
337
338 /**
339 * Locate template
340 *
341 * @param string $template_name Template name.
342 * @param string $template_path Template path. (default: '').
343 * @param string $plugin_path Plugin path. (default: ''). fallback file from plugin.
344 *
345 * @return mixed|void
346 */
347 public static function locate_template( $template_name, $template_path = '', $plugin_path = '' ) {
348 $template_name = self::sanitize_file_name( $template_name ) . '.php';
349
350 if ( ! $template_path ) {
351 $template_path = rtsb()->get_template_path();
352 }
353 if ( ! $plugin_path ) {
354 $plugin_path = RTSB_ABSPATH . 'templates/';
355 }
356
357 $template_rtsb_path = trailingslashit( $template_path ) . $template_name;
358 $template_path = '/' . $template_name;
359 $plugin_path = $plugin_path . $template_name;
360 $pro_plugin_path = rtsb()->has_pro() ? RTSBPRO_PATH . 'templates/' . $template_name : '';
361
362 $located = locate_template(
363 apply_filters(
364 'rtsb/core/locate_template_files',
365 [
366 $template_rtsb_path, // Search in <theme>/shopbuilder/.
367 $template_path, // Search in <theme>/.
368 ]
369 )
370 );
371
372 if ( ! $located ) {
373 if ( $pro_plugin_path && rtsb()->has_pro() && file_exists( $pro_plugin_path ) ) {
374 return apply_filters( 'rtsb/core/locate_template', $pro_plugin_path, $template_name );
375 } elseif ( file_exists( $plugin_path ) ) {
376 return apply_filters( 'rtsb/core/locate_template', $plugin_path, $template_name );
377 }
378 }
379
380 /**
381 * APPLY_FILTERS: rtsb/core/locate_template
382 *
383 * Filter the location of the templates.
384 *
385 * @param string $located Template found
386 * @param string $path Template path
387 *
388 * @return string
389 */
390 return apply_filters( 'rtsb/core/locate_template', $located, $template_name );
391 }
392
393 /**
394 * Remove any character that is not alphanumeric, /, _, or -.
395 *
396 * @param string $name Name to sanitize.
397 *
398 * @return array|string|string[]|null
399 */
400 private static function sanitize_file_name( $name ) {
401 return preg_replace( '/[^a-zA-Z0-9\/_\-]/', '', $name );
402 }
403
404 /**
405 * Template Content
406 *
407 * @param string $template_name Template name.
408 * @param array $args Arguments. (default: array).
409 * @param bool $return Whether to return or print the template.
410 * @param string $template_path Template path. (default: '').
411 * @param string $plugin_path Fallback path from where file will load if fail to load from template. (default: '').
412 *
413 * @return false|string|void
414 */
415 public static function load_template( $template_name, $args = null, $return = false, $template_path = '', $plugin_path = '' ) {
416 $cache_key = sanitize_key( implode( '-', [ 'template', $template_name, $template_path ] ) );
417 $located = (string) wp_cache_get( $cache_key, 'shopbuilder' );
418 if ( ! $located ) {
419 $located = self::locate_template( $template_name, $template_path, $plugin_path );
420 // Don't cache the absolute path so that it can be shared between web servers with different paths.
421 $cache_path = wc_tokenize_path( $located, wc_get_path_define_tokens() );
422 Cache::set_template_cache( $cache_key, $cache_path );
423 } else {
424 // Make sure that the absolute path to the template is resolved.
425 $located = wc_untokenize_path( $located, wc_get_path_define_tokens() );
426 }
427 if ( ! file_exists( $located ) ) {
428 // translators: %s template.
429 self::doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'shopbuilder' ), '<code>' . $located . '</code>' ), '1.0' );
430
431 return;
432 }
433
434 if ( ! empty( $args ) && is_array( $args ) ) {
435 $atts = $args;
436 extract( $args ); // @codingStandardsIgnoreLine
437 }
438
439 // Allow 3rd party plugin filter template file from their plugin.
440 $located = apply_filters( 'rtsb/core/get_template', $located, $template_name, $args );
441
442 if ( $return ) {
443 ob_start();
444 }
445
446 do_action( 'rtsb/core/before_template_part', $template_name, $located, $args );
447 include $located;
448
449 do_action( 'rtsb/core/after_template_part', $template_name, $located, $args );
450
451 if ( $return ) {
452 return ob_get_clean();
453 }
454 }
455
456 /**
457 * Verify nonce.
458 *
459 * @return string
460 */
461 public static function is_woocommerce() {
462 return is_woocommerce() || is_cart() || is_checkout() || is_account_page();
463 }
464
465 /**
466 * Page builder
467 *
468 * @param int $post_id post id.
469 *
470 * @return string
471 */
472 public static function page_edit_with( $post_id ) {
473 if ( ! $post_id ) {
474 return '';
475 }
476
477 $edit_with = get_post_meta( $post_id, '_elementor_edit_mode', true );
478
479 if ( 'builder' === $edit_with ) {
480 $edit_by = 'elementor';
481 } else {
482 $edit_by = 'gutenberg';
483 }
484
485 return $edit_by;
486 }
487
488 /**
489 * Returns default expiration for wishlist cookie
490 *
491 * @return int Number of seconds the cookie should last.
492 */
493 public static function get_cookie_expiration() {
494 return intval( apply_filters( 'rtsb/cookie_expiration', 60 * 60 * 24 * 30 ) );
495 }
496
497 /**
498 * Create a cookie.
499 *
500 * @param string $name Cookie name.
501 * @param mixed $value Cookie value.
502 * @param int $time Cookie expiration time.
503 * @param bool $secure Whether cookie should be available to secured connection only.
504 * @param bool $httponly Whether cookie should be available to HTTP request only (no js handling).
505 *
506 * @return bool
507 * @since 1.0.0
508 */
509 public static function setcookie( $name, $value = [], $time = null, $secure = false, $httponly = false ) {
510
511 if ( ! apply_filters( 'rtsb/set_cookie', true ) || empty( $name ) ) {
512 return false;
513 }
514
515 $time = ! empty( $time ) ? $time : time() + self::get_cookie_expiration();
516
517 $value = wp_json_encode( stripslashes_deep( $value ) );
518 $expiration = apply_filters( 'rtsb/cookie_expiration_time', $time ); // Default 30 days.
519
520 $_COOKIE[ $name ] = $value;
521 wc_setcookie( $name, $value, $expiration, $secure, $httponly );
522
523 return true;
524 }
525
526 /**
527 * Retrieve the value of a cookie.
528 *
529 * @param string $name Cookie name.
530 *
531 * @return mixed
532 * @since 1.0.0
533 */
534 public static function getcookie( $name ) {
535 if ( isset( $_COOKIE[ $name ] ) ) {
536 return json_decode( sanitize_text_field( wp_unslash( $_COOKIE[ $name ] ) ), true );
537 }
538
539 return [];
540 }
541
542 /**
543 * Woocommerce Last product id return
544 */
545 public static function get_prepared_product_id() {
546 if ( is_singular( 'product' ) ) {
547 return get_the_ID();
548 }
549 // Return the Builder Template id.
550
551 if ( get_post_type( get_the_ID() ) == BuilderFns::$post_type_tb ) {
552 $product_id = get_post_meta( get_the_ID(), BuilderFns::$product_template_meta, true );
553 if ( $product_id && 'product' === get_post_type( $product_id ) && get_post_status( $product_id ) ) {
554 return $product_id;
555 }
556 }
557
558 global $wpdb;
559 $cache_key = 'rtsb_prepared_product_id';
560 $_post_id = wp_cache_get( $cache_key, 'shopbuilder' );
561 if ( false === $_post_id || 'publish' !== get_post_status( $_post_id ) ) {
562 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
563 $_post_id = $wpdb->get_var(
564 $wpdb->prepare( "SELECT MAX(ID) FROM {$wpdb->prefix}posts WHERE post_type = %s AND post_status IN('publish', 'draft')", 'product' )
565 );
566 wp_cache_set( $cache_key, $_post_id, 'shopbuilder', 12 * HOUR_IN_SECONDS );
567 Cache::set_data_cache_key( $cache_key );
568 }
569
570 return $_post_id;
571 }
572
573 /**
574 * Get the product function. Only used in single page widgets.
575 *
576 * @return object
577 */
578 public static function get_product() {
579 global $product;
580
581 if ( is_singular( 'product' ) && $product instanceof WC_Product ) {
582 return $product;
583 }
584 $cache_key = 'prepared_product_for_preview';
585 if ( isset( self::$cache[ $cache_key ] ) ) {
586 return self::$cache[ $cache_key ];
587 }
588 $product = wc_get_product( self::get_prepared_product_id() );
589 self::$cache[ $cache_key ] = $product;
590 do_action( 'rtsb_before_product_template_render' );
591 return $product;
592 }
593
594 /**
595 * Error function
596 *
597 * @param [type] $function function name.
598 * @param [type] $message message.
599 * @param [type] $version version.
600 *
601 * @return void
602 */
603 public static function doing_it_wrong( $function, $message, $version ) {
604 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
605 $message .= ' Backtrace: ' . wp_debug_backtrace_summary();
606 _doing_it_wrong( esc_html( $function ), wp_kses_post( $message ), esc_html( $version ) );
607 }
608
609 /**
610 * @return array
611 */
612 public static function get_pages() {
613 $pages = [];
614 $rawPages = get_pages();
615 if ( ! empty( $rawPages ) ) {
616 foreach ( $rawPages as $page ) {
617 $pages[ $page->ID ] = $page->post_title;
618 }
619 }
620
621 return $pages;
622 }
623
624 /**
625 * Get section items
626 *
627 * @param string $section_id Section ID.
628 *
629 * @return array
630 */
631 public static function get_section_items( $section_id ) {
632 if ( ! $section_id ) {
633 return [];
634 }
635
636 return DataModel::source()->get_option( $section_id, [] );
637 }
638
639 /**
640 * Get options items
641 *
642 * @param string $section_id Section ID.
643 * @param string $item_id Item ID.
644 *
645 * @return array
646 */
647 public static function get_options( $section_id, $item_id ) {
648 if ( ! $section_id || ! $item_id ) {
649 return [];
650 }
651 $sections = self::get_section_items( $section_id );
652
653 return isset( $sections[ $item_id ] ) ? $sections[ $item_id ] : [];
654 }
655
656 /**
657 * Get options value with default value if options doesn't exist.
658 *
659 * @param array $group Group.
660 * @param string $option_key Option key.
661 * @param string $default_value Default value.
662 *
663 * @return mixed|string
664 */
665 public static function get_options_by_default_val( $group, $option_key, $default_value = '' ) {
666
667 if ( ! $option_key || ! isset( $group[ $option_key ] ) ) {
668 return $default_value;
669 }
670
671 return $group[ $option_key ];
672 }
673
674 /**
675 * Get option.
676 *
677 * @param string $section_id Section ID.
678 * @param string $item_id Item ID.
679 * @param string $option_id Option ID.
680 * @param null $default EXCEPT multi_checkbox you can provide default value if given option does not set any value.
681 * @param null $type checkbox, multi_checkbox, number.
682 *
683 * @return bool|int|mixed|null
684 */
685 public static function get_option( $section_id, $item_id, $option_id, $default = null, $type = null ) {
686 $options = self::get_options( $section_id, $item_id );
687
688 if ( 'checkbox' === $type ) {
689 if ( isset( $options[ $option_id ] ) ) {
690 return 'on' === $options[ $option_id ];
691 }
692
693 return $default;
694 } elseif ( 'multi_checkbox' === $type ) {
695 return isset( $options[ $option_id ] ) && is_array( $options[ $option_id ] ) && in_array( $default, $options[ $option_id ] ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
696 } elseif ( 'number' === $type ) {
697 return isset( $options[ $option_id ] ) ? absint( $options[ $option_id ] ) : absint( $default );
698 }
699
700 return ! empty( $options[ $option_id ] ) ? $options[ $option_id ] : $default;
701 }
702
703
704 /**
705 * Create a page and store the ID in an option.
706 *
707 * @param mixed $slug Slug for the new page.
708 * @param array $options ['section_id', 'item_id', 'option_id']Option name to store the page's ID.
709 * @param string $page_title (default: '') Title for the new page.
710 * @param string $page_content (default: '') Content for the new page.
711 * @param int $post_parent (default: 0) Parent for the new page.
712 * @param string $post_status (default: publish) The post status of the new page.
713 *
714 * @return int page ID.
715 */
716 public static function create_page( $slug, $options = '', $page_title = '', $page_content = '', $post_parent = 0, $post_status = 'publish' ) {
717 global $wpdb;
718
719 $option_value = 0;
720 if ( ! empty( $options ) ) {
721 if ( is_array( $options ) ) {
722 $options = wp_parse_args(
723 $options,
724 [
725 'section_id' => '',
726 'item_id' => '',
727 'option_id' => '',
728 ]
729 );
730 $option_value = self::get_option( $options['section_id'], $options['item_id'], $options['option_id'], 0, 'number' );
731 } else {
732 $option_value = absint( get_option( $options ) );
733 }
734 }
735
736 if ( $option_value > 0 ) {
737 $page_object = get_post( $option_value );
738
739 if ( $page_object && 'page' === $page_object->post_type && ! in_array(
740 $page_object->post_status,
741 [
742 'pending',
743 'trash',
744 'future',
745 'auto-draft',
746 ],
747 true
748 ) ) {
749 // Valid page is already in place.
750 return $page_object->ID;
751 }
752 }
753
754 if ( strlen( $page_content ) > 0 ) {
755 // Search for an existing page with the specified page content (typically a shortcode).
756 $shortcode = str_replace( [ '<!-- wp:shortcode -->', '<!-- /wp:shortcode -->' ], '', $page_content );
757 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
758 $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}%" ) );
759 } else {
760 // Search for an existing page with the specified page slug.
761 $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
762 }
763
764 $valid_page_found = apply_filters( 'rtsb/core/create_page_id', $valid_page_found, $slug, $page_content, $options );
765
766 if ( $valid_page_found ) {
767 if ( is_array( $options ) ) {
768 if ( ! empty( $options['section_id'] ) && $options['item_id'] && $options['option_id'] ) {
769 $section_items = DataModel::source()->get_option( $options['section_id'], [] );
770 $section_items[ $options['item_id'] ][ $options['option_id'] ] = $valid_page_found;
771 DataModel::source()->set_option( $options['section_id'], $section_items );
772 }
773 } else {
774 if ( $options ) {
775 update_option( $options, $valid_page_found );
776 }
777 }
778
779 return $valid_page_found;
780 }
781
782 // Search for a matching valid trashed page.
783 if ( strlen( $page_content ) > 0 ) {
784 // Search for an existing page with the specified page content (typically a shortcode).
785 $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
786 } else {
787 // Search for an existing page with the specified page slug.
788 $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
789 }
790
791 if ( $trashed_page_found ) {
792 $page_id = $trashed_page_found;
793 $page_data = [
794 'ID' => $page_id,
795 'post_status' => $post_status,
796 ];
797 wp_update_post( $page_data );
798 } else {
799 $page_data = [
800 'post_status' => $post_status,
801 'post_type' => 'page',
802 'post_author' => 1,
803 'post_name' => $slug,
804 'post_title' => $page_title,
805 'post_content' => $page_content,
806 'post_parent' => $post_parent,
807 'comment_status' => 'closed',
808 ];
809 $page_id = wp_insert_post( $page_data );
810
811 do_action( 'rtsb/core/page_created', $page_id, $page_data );
812 }
813
814 if ( is_array( $options ) ) {
815 if ( ! empty( $options['section_id'] ) && $options['item_id'] && $options['option_id'] ) {
816 $section_items = DataModel::source()->get_option( $options['section_id'], [] );
817 $section_items[ $options['item_id'] ][ $options['option_id'] ] = $page_id;
818 DataModel::source()->set_option( $options['section_id'], $section_items );
819 }
820 } else {
821 if ( $options ) {
822 update_option( $options, $page_id );
823 }
824 }
825
826 return $page_id;
827 }
828
829 /**
830 * Get allowed HTML tags.
831 *
832 * @return array
833 */
834 public static function get_kses_array() {
835 return [
836 'a' => [
837 'class' => [],
838 'href' => [],
839 'rel' => [],
840 'title' => [],
841 ],
842 'abbr' => [
843 'title' => [],
844 ],
845 'b' => [],
846 'blockquote' => [
847 'cite' => [],
848 ],
849 'cite' => [
850 'title' => [],
851 ],
852 'code' => [],
853 'del' => [
854 'datetime' => [],
855 'title' => [],
856 ],
857 'dd' => [],
858 'div' => [
859 'class' => [],
860 'title' => [],
861 'style' => [],
862 ],
863 'dl' => [],
864 'dt' => [],
865 'em' => [],
866 'h1' => [
867 'class' => [],
868 ],
869 'h2' => [
870 'class' => [],
871 ],
872 'h3' => [
873 'class' => [],
874 ],
875 'h4' => [
876 'class' => [],
877 ],
878 'h5' => [
879 'class' => [],
880 ],
881 'h6' => [
882 'class' => [],
883 ],
884 'i' => [
885 'class' => [],
886 ],
887 'img' => [
888 'alt' => [],
889 'class' => [],
890 'height' => [],
891 'src' => [],
892 'width' => [],
893 ],
894 'li' => [
895 'class' => [],
896 ],
897 'ol' => [
898 'class' => [],
899 ],
900 'p' => [
901 'class' => [],
902 ],
903 'q' => [
904 'cite' => [],
905 'title' => [],
906 ],
907 'span' => [
908 'class' => [],
909 'title' => [],
910 'style' => [],
911 ],
912 'iframe' => [
913 'width' => [],
914 'height' => [],
915 'scrolling' => [],
916 'frameborder' => [],
917 'allow' => [],
918 'src' => [],
919 ],
920 'strike' => [],
921 'br' => [],
922 'strong' => [],
923 'data-wow-duration' => [],
924 'data-wow-delay' => [],
925 'data-wallpaper-options' => [],
926 'data-stellar-background-ratio' => [],
927 'ul' => [
928 'class' => [],
929 ],
930 ];
931 }
932
933
934 /**
935 * Escape output of wishlist icon
936 *
937 * @param string $data Data to escape.
938 *
939 * @return void
940 */
941 public static function print_icon( $data ) {
942 /**
943 * APPLY_FILTERS: rtsb/core/allowed_icon_html
944 *
945 * Filter the allowed HTML for the icons.
946 *
947 * @param array $allowed_icon_html Allowed HTML
948 *
949 * @return array
950 */
951 $allowed_icon_html = apply_filters(
952 'rtsb/core/allowed_icon_html',
953 [
954 'i' => [
955 'class' => true,
956 ],
957 'img' => [
958 'src' => true,
959 'alt' => true,
960 'width' => true,
961 'height' => true,
962 ],
963 'svg' => [
964 'class' => true,
965 'aria-hidden' => true,
966 'aria-labelledby' => true,
967 'role' => true,
968 'xmlns' => true,
969 'width' => true,
970 'height' => true,
971 'viewbox' => true,
972 'stroke' => true,
973 'fill' => true,
974 ],
975 'g' => [
976 'fill' => true,
977 ],
978 'title' => [
979 'title' => true,
980 ],
981 'path' => [
982 'd' => true,
983 'fill' => true,
984 'stroke' => true,
985 'stroke-width' => true,
986 'stroke-linecap' => true,
987 'stroke-linejoin' => true,
988 'fill-rule' => true,
989 'clip-rule' => true,
990 ],
991 ]
992 );
993
994 echo wp_kses( $data, $allowed_icon_html );
995 }
996
997 /**
998 * Prints HTMl.
999 *
1000 * @param string $html HTML.
1001 * @param bool $allHtml All HTML.
1002 *
1003 * @return void
1004 */
1005 public static function print_html( $html, $allHtml = false ) {
1006 if ( ! $html ) {
1007 return;
1008 }
1009 if ( $allHtml ) {
1010 echo stripslashes_deep( $html ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1011 } else {
1012 echo wp_kses_post( stripslashes_deep( $html ) );
1013 }
1014 }
1015
1016 /**
1017 * Allowed HTML for wp_kses.
1018 *
1019 * @param string $level Tag level.
1020 *
1021 * @return mixed
1022 */
1023 public static function allowedHtml( $level = 'basic' ) {
1024 $allowed_html = [];
1025
1026 switch ( $level ) {
1027 case 'basic':
1028 $allowed_html = [
1029 'b' => [
1030 'class' => [],
1031 'id' => [],
1032 ],
1033 'i' => [
1034 'class' => [],
1035 'id' => [],
1036 ],
1037 'u' => [
1038 'class' => [],
1039 'id' => [],
1040 ],
1041 'br' => [
1042 'class' => [],
1043 'id' => [],
1044 ],
1045 'em' => [
1046 'class' => [],
1047 'id' => [],
1048 ],
1049 'span' => [
1050 'class' => [],
1051 'id' => [],
1052 ],
1053 'strong' => [
1054 'class' => [],
1055 'id' => [],
1056 ],
1057 'hr' => [
1058 'class' => [],
1059 'id' => [],
1060 ],
1061 'div' => [
1062 'class' => [],
1063 'id' => [],
1064 ],
1065 'a' => [
1066 'href' => [],
1067 'title' => [],
1068 'class' => [],
1069 'id' => [],
1070 'target' => [],
1071 ],
1072 ];
1073 break;
1074
1075 case 'advanced':
1076 $allowed_html = [
1077 'b' => [
1078 'class' => [],
1079 'id' => [],
1080 ],
1081 'i' => [
1082 'class' => [],
1083 'id' => [],
1084 ],
1085 'u' => [
1086 'class' => [],
1087 'id' => [],
1088 ],
1089 'br' => [
1090 'class' => [],
1091 'id' => [],
1092 ],
1093 'em' => [
1094 'class' => [],
1095 'id' => [],
1096 ],
1097 'span' => [
1098 'class' => [],
1099 'id' => [],
1100 ],
1101 'strong' => [
1102 'class' => [],
1103 'id' => [],
1104 ],
1105 'hr' => [
1106 'class' => [],
1107 'id' => [],
1108 ],
1109 'a' => [
1110 'href' => [],
1111 'title' => [],
1112 'class' => [],
1113 'id' => [],
1114 'target' => [],
1115 ],
1116 'input' => [
1117 'type' => [],
1118 'name' => [],
1119 'class' => [],
1120 'value' => [],
1121 ],
1122 ];
1123 break;
1124
1125 case 'image':
1126 $allowed_html = [
1127 'img' => [
1128 'src' => [],
1129 'data-src' => [],
1130 'alt' => [],
1131 'height' => [],
1132 'width' => [],
1133 'class' => [],
1134 'id' => [],
1135 'style' => [],
1136 'srcset' => [],
1137 'loading' => [],
1138 'sizes' => [],
1139 ],
1140 'div' => [
1141 'class' => [],
1142 ],
1143 ];
1144 break;
1145
1146 case 'anchor':
1147 $allowed_html = [
1148 'a' => [
1149 'href' => [],
1150 'title' => [],
1151 'class' => [],
1152 'id' => [],
1153 'style' => [],
1154 ],
1155 ];
1156 break;
1157
1158 default:
1159 // code...
1160 break;
1161 }
1162
1163 return $allowed_html;
1164 }
1165
1166 /**
1167 * Safe get a validated HTML tag.
1168 *
1169 * @param string $tag HTML tag.
1170 *
1171 * @return string
1172 */
1173 public static function get_validated_html_tag( $tag ) {
1174 $allowed_html_wrapper_tags = [
1175 'a',
1176 'article',
1177 'aside',
1178 'button',
1179 'div',
1180 'footer',
1181 'h1',
1182 'h2',
1183 'h3',
1184 'h4',
1185 'h5',
1186 'h6',
1187 'header',
1188 'main',
1189 'nav',
1190 'p',
1191 'section',
1192 'span',
1193 ];
1194
1195 return in_array( strtolower( $tag ), $allowed_html_wrapper_tags, true ) ? $tag : 'div';
1196 }
1197
1198 /**
1199 * Safe print a validated HTML tag.
1200 *
1201 * @param string $tag HTML tag.
1202 *
1203 * @return void
1204 */
1205 public static function print_validated_html_tag( $tag ) {
1206 self::print_html( self::get_validated_html_tag( $tag ) );
1207 }
1208
1209 /**
1210 * Insert Some array element
1211 *
1212 * @param [type] $key The elements will insert nearby this key.
1213 * @param [type] $main_array Original array.
1214 * @param [type] $insert_array some element will insert in original array.
1215 * @param boolean $is_after array insert position base on the key.
1216 *
1217 * @return array
1218 */
1219 public static function insert_controls( $key, $main_array, $insert_array, $is_after = false ) {
1220 $index = array_search( $key, array_keys( $main_array ), true );
1221 if ( 'integer' === gettype( $index ) ) {
1222 if ( $is_after ) {
1223 $index++;
1224 }
1225 $main_array = array_merge(
1226 array_slice( $main_array, 0, $index ),
1227 $insert_array,
1228 array_slice( $main_array, $index )
1229 );
1230 }
1231
1232 return $main_array;
1233 }
1234
1235 /**
1236 * Image Sizes
1237 *
1238 * @return array
1239 */
1240 public static function get_image_sizes() {
1241 global $_wp_additional_image_sizes;
1242
1243 $sizes = [];
1244
1245 foreach ( get_intermediate_image_sizes() as $_size ) {
1246 if ( in_array( $_size, [ 'thumbnail', 'medium', 'large' ], true ) ) {
1247 $sizes[ $_size ]['width'] = get_option( "{$_size}_size_w" );
1248 $sizes[ $_size ]['height'] = get_option( "{$_size}_size_h" );
1249 $sizes[ $_size ]['crop'] = (bool) get_option( "{$_size}_crop" );
1250 } elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) {
1251 $sizes[ $_size ] = [
1252 'width' => $_wp_additional_image_sizes[ $_size ]['width'],
1253 'height' => $_wp_additional_image_sizes[ $_size ]['height'],
1254 'crop' => $_wp_additional_image_sizes[ $_size ]['crop'],
1255 ];
1256 }
1257 }
1258
1259 $imgSize = [];
1260
1261 foreach ( $sizes as $key => $img ) {
1262 $imgSize[ $key ] = esc_html( $key ) . " ({$img['width']}*{$img['height']})";
1263 }
1264
1265 $imgSize['full'] = esc_html__( 'Full size', 'shopbuilder' );
1266 $imgSize['rtsb_custom'] = esc_html__( 'Custom image size', 'shopbuilder' );
1267
1268 return $imgSize;
1269 }
1270
1271 /**
1272 * Free Layouts
1273 *
1274 * @param string $layout Layout.
1275 *
1276 * @return array
1277 */
1278 public static function free_layouts( $layout = '' ) {
1279 if ( ! empty( $layout ) && false !== strpos( $layout, 'rtsb-' ) ) {
1280 return [ $layout => esc_html__( 'Addon Layout', 'shopbuilder' ) ];
1281 }
1282
1283 $layouts = [
1284 'grid-layout1' => esc_html__( 'Grid Layout 1', 'shopbuilder' ),
1285 'grid-layout2' => esc_html__( 'Grid Layout 2', 'shopbuilder' ),
1286 'list-layout1' => esc_html__( 'List Layout 1', 'shopbuilder' ),
1287 'list-layout2' => esc_html__( 'List Layout 2', 'shopbuilder' ),
1288 'slider-layout1' => esc_html__( 'Slider Layout 1', 'shopbuilder' ),
1289 'slider-layout2' => esc_html__( 'Slider Layout 2', 'shopbuilder' ),
1290 'category-single-layout1' => esc_html__( 'Category Single Layout 1', 'shopbuilder' ),
1291 'category-layout1' => esc_html__( 'Category Layout 1', 'shopbuilder' ),
1292 'category-layout2' => esc_html__( 'Category Layout 2', 'shopbuilder' ),
1293 ];
1294
1295 return apply_filters( 'rtsb/elements/elementor/free_layouts', $layouts );
1296 }
1297
1298 /**
1299 * Get all terms by taxonomy
1300 *
1301 * @param string $taxonomy Taxonomy name.
1302 * @param bool $placeholder Placeholder..
1303 *
1304 * @return array
1305 */
1306 public static function get_all_terms( $taxonomy, $placeholder = false ) {
1307 $terms = [];
1308
1309 if ( empty( $taxonomy ) ) {
1310 return $terms;
1311 }
1312 $cache_key = 'rtsb_get_all_terms_by_tax_name_' . $taxonomy;
1313 if ( isset( self::$cache[ $cache_key ] ) ) {
1314 return self::$cache[ $cache_key ];
1315 }
1316
1317 $termList = get_terms(
1318 [
1319 'taxonomy' => $taxonomy,
1320 'hide_empty' => false,
1321 ]
1322 );
1323
1324 if ( is_array( $termList ) && ! empty( $termList ) && empty( $termList['errors'] ) ) {
1325 if ( $placeholder ) {
1326 $terms = [
1327 'all' => __( 'All Products', 'shopbuilder' ),
1328 ];
1329 }
1330
1331 foreach ( $termList as $term ) {
1332 $terms[ $term->term_id ] = esc_html( $term->name );
1333 }
1334 }
1335 self::$cache[ $cache_key ] = $terms;
1336 return $terms;
1337 }
1338 /**
1339 * Get all product attribute taxonomy by id
1340 *
1341 * @param array $term_ids Term id.
1342 *
1343 * @return array
1344 */
1345 public static function tax_filter_attr_taxonomy( $term_ids ) {
1346 $taxonomies = [];
1347 foreach ( $term_ids as $id ) {
1348 $term = get_term( $id );
1349 if ( ! is_wp_error( $term ) && $term ) {
1350 $taxonomies[] = $term->taxonomy;
1351 }
1352 }
1353 return array_unique( $taxonomies );
1354 }
1355
1356 /**
1357 * Get all terms by attributes
1358 *
1359 * @return array
1360 */
1361 public static function get_all_attributes() {
1362 $terms = [];
1363 $termList = [];
1364
1365 $attributes = wc_get_attribute_taxonomies();
1366
1367 if ( $attributes ) {
1368 foreach ( $attributes as $tax ) {
1369 if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
1370 $termList[ $tax->attribute_name ] = get_terms( wc_attribute_taxonomy_name( $tax->attribute_name ) );
1371 }
1372 }
1373 }
1374
1375 if ( ! empty( $termList ) && empty( $termList['errors'] ) && is_array( $termList ) ) {
1376 foreach ( $termList as $name => $atts ) {
1377 foreach ( $atts as $term ) {
1378 $terms[ $term->term_id ] = esc_html( ucwords( str_replace( '-', ' ', $name ) ) . ' - ' . $term->name );
1379 }
1380 }
1381 }
1382
1383 return $terms;
1384 }
1385
1386 /**
1387 * Get all attributes name
1388 *
1389 * @return array
1390 */
1391 public static function get_all_attributes_name() {
1392 $attributes_name = [];
1393
1394 $attributes = wc_get_attribute_taxonomies();
1395
1396 if ( $attributes ) {
1397 foreach ( $attributes as $tax ) {
1398 if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
1399 $attributes_name[ wc_attribute_taxonomy_name( $tax->attribute_name ) ] = ucwords( $tax->attribute_name );
1400 }
1401 }
1402 }
1403
1404 return $attributes_name;
1405 }
1406
1407 /**
1408 * Get User list
1409 *
1410 * @return array
1411 */
1412 public static function get_users() {
1413 $users = [];
1414 $u = get_users();
1415
1416 if ( ! empty( $u ) ) {
1417 foreach ( $u as $user ) {
1418 $users[ $user->ID ] = $user->display_name;
1419 }
1420 }
1421
1422 return $users;
1423 }
1424
1425 /**
1426 * Get Taxonomy List.
1427 *
1428 * @return array
1429 */
1430 public static function get_tax_list() {
1431 return apply_filters(
1432 'rtsb/elements/tax_list',
1433 [
1434 'product_cat' => esc_html__( 'Product Category', 'shopbuilder' ),
1435 ]
1436 );
1437 }
1438
1439 /**
1440 * Get Category Query List.
1441 *
1442 * @return array
1443 */
1444 public static function get_cat_list() {
1445 return [
1446 'all' => esc_html__( 'All Categories', 'shopbuilder' ),
1447 'specific_parent' => esc_html__( 'Sub-Categories by Parent', 'shopbuilder' ),
1448 'cat_ids' => esc_html__( 'Select by ID', 'shopbuilder' ),
1449 'selection' => esc_html__( 'Manual Selection', 'shopbuilder' ),
1450 ];
1451 }
1452
1453 /**
1454 * Get Term List.
1455 *
1456 * @param string $taxonomy Taxonomy.
1457 * @param bool $first_term First term.
1458 * @param bool $only_parents Only parent terms.
1459 * @param bool $return_slug Return with slug.
1460 *
1461 * @return int|array
1462 */
1463 public static function get_terms( $taxonomy, $first_term = false, $only_parents = false, $return_slug = false ) {
1464 if ( ! is_admin() ) {
1465 return [];
1466 }
1467
1468 $term_list = [];
1469 $args = [
1470 'taxonomy' => $taxonomy,
1471 'hide_empty' => false,
1472 ];
1473
1474 if ( $only_parents ) {
1475 $args['parent'] = 0;
1476 }
1477
1478 $terms = get_terms( $args );
1479
1480 if ( empty( $terms ) ) {
1481 return [ esc_html__( 'Nothing found', 'shopbuilder' ) ];
1482 }
1483
1484 foreach ( $terms as $term ) {
1485 if ( $return_slug ) {
1486 $term_list[ $term->slug ] = $term->name;
1487 } else {
1488 $term_list[ $term->term_id ] = $term->name;
1489 }
1490 }
1491
1492 if ( $first_term ) {
1493 return array_keys( $term_list )[0];
1494 } else {
1495 return $term_list;
1496 }
1497 }
1498
1499 /**
1500 * Get product rating.
1501 *
1502 * @param array $args Arguments.
1503 *
1504 * @return string|void
1505 */
1506 public static function get_product_rating_html( $args = [] ) {
1507 global $product;
1508
1509 $html = '';
1510 $rating_count = $product->get_rating_count();
1511 $average_rating = $product->get_average_rating();
1512
1513 if ( ! rtsb()->has_pro() || empty( $args ) ) {
1514 if ( ! $rating_count || empty( wc_get_rating_html( $average_rating, $rating_count ) ) ) {
1515 return $html;
1516 }
1517
1518 $html .= '<div class="product-rating">';
1519 $html .= wc_get_rating_html( $average_rating, $rating_count );
1520 $html .= ! empty( $html ) ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1521 $html .= '</div>';
1522
1523 self::print_html( $html, true );
1524
1525 return;
1526 }
1527
1528 if ( empty( $rating_count ) && ! $args['show_empty_rating'] ) {
1529 return '';
1530 }
1531
1532 $preset = ! empty( $args['preset'] ) ? $args['preset'] : 'preset1';
1533 $average = $args['show_average_rating'] ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1534 $count = '';
1535
1536 if ( $args['show_rating_count'] ) {
1537 $count .= '<div class="rtsb-count">';
1538 $count .= sprintf(
1539 /* translators: %s is the number of reviews */
1540 _n( '%s Review', '%s Reviews', $rating_count, 'shopbuilder' ),
1541 $rating_count
1542 );
1543 $count .= '</div>';
1544 }
1545
1546 if ( 'preset2' === $preset ) {
1547 $average = $args['show_average_rating'] ? '<div class="inner-wrapper"><span class="star-icon"></span><span class="average-rating">' . $average_rating . '</span></div>' : '';
1548 }
1549
1550 $html .= '<div class="product-rating ' . esc_attr( $preset ) . '">';
1551
1552 if ( 'preset1' === $preset ) {
1553 $html .= wc_get_rating_html( $average_rating, $rating_count );
1554 $html .= $average;
1555 $html .= $count;
1556 } elseif ( 'preset2' === $preset ) {
1557 $html .= $average;
1558 $html .= $count;
1559 }
1560
1561 $html .= '</div>';
1562
1563 self::print_html( $html, true );
1564 }
1565
1566 /**
1567 * Get product simple rating.
1568 *
1569 * @return void
1570 */
1571 public static function get_product_simple_rating_html() {
1572 global $product;
1573 $html = null;
1574 $rating_count = $product->get_rating_count();
1575 $average_rating = $product->get_average_rating();
1576 $html .= '<div class="product-rating">';
1577 $html .= wc_get_rating_html( $average_rating, $rating_count );
1578 $html .= ! empty( $html ) ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1579 $html .= '</div>';
1580
1581 self::print_html( $html, true );
1582 }
1583
1584
1585
1586 /**
1587 * Text truncation.
1588 *
1589 * @param string $text_to_truncate Text.
1590 * @param int $limit Limit.
1591 * @param string $after After text.
1592 *
1593 * @return string
1594 */
1595 public static function text_truncation( $text_to_truncate, $limit, $after = '&#8230;' ) {
1596 if ( empty( $limit ) ) {
1597 return $text_to_truncate;
1598 }
1599
1600 $limit++;
1601
1602 $text = '';
1603
1604 if ( mb_strlen( $text_to_truncate ) > $limit ) {
1605 $subex = mb_substr( wp_strip_all_tags( $text_to_truncate ), 0, $limit );
1606 $exwords = explode( ' ', $subex );
1607 $excut = - ( mb_strlen( $exwords[ count( $exwords ) - 1 ] ) );
1608
1609 if ( $excut < 0 ) {
1610 $text .= mb_substr( $subex, 0, $excut ) . $after;
1611 } else {
1612 $text .= $subex . $after;
1613 }
1614 } else {
1615 $text .= $text_to_truncate;
1616 }
1617
1618 return $text;
1619 }
1620
1621 /**
1622 * Promo Badge HTML
1623 *
1624 * @param string $text Text.
1625 * @param string $class Class.
1626 *
1627 * @return void
1628 */
1629 public static function get_badge_html( $text, $class = 'fill' ) {
1630 if ( self::is_module_active( 'product_badges' ) && 'rtsb_yes' === $text ) {
1631 do_action( 'rtsb/modules/product_badges/frontend/display' );
1632
1633 return;
1634 }
1635
1636 if ( empty( $text ) ) {
1637 return;
1638 }
1639 ob_start();
1640 ?>
1641
1642 <ul class="rtsb-promotion-list">
1643 <li class="rtsb-promotion-list-item">
1644 <span class="rtsb-tag-<?php echo ! empty( $class ) ? esc_attr( $class ) : ''; ?>"><?php echo esc_html( $text ); ?></span>
1645 </li>
1646 </ul>
1647
1648 <?php
1649 self::print_html( ob_get_clean() );
1650 }
1651
1652 /**
1653 * Categories HTML
1654 *
1655 * @param int $id Product ID.
1656 * @param string $class Custom class.
1657 *
1658 * @return void|string
1659 */
1660 public static function get_categories_list( $id, $class = 'rtsb-category-outline' ) {
1661 if ( empty( $id ) ) {
1662 return '';
1663 }
1664
1665 ob_start();
1666 ?>
1667
1668 <ul class="rtsb-category-list <?php echo esc_attr( $class ); ?>">
1669 <?php
1670 self::print_html(
1671 wc_get_product_category_list(
1672 $id,
1673 '</li><li class="rtsb-category-list-item">',
1674 '<li class="rtsb-category-list-item">',
1675 '</li>'
1676 )
1677 );
1678 ?>
1679 </ul>
1680
1681 <?php
1682 self::print_html( ob_get_clean() );
1683 }
1684 /**
1685 * Brands HTML
1686 *
1687 * @param int $id Product ID.
1688 * @param string $class Custom class.
1689 *
1690 * @return void|string
1691 */
1692 public static function get_brands_list( $id, $class = 'rtsb-brand-outline' ) {
1693 $terms = get_the_terms( $id, 'product_brand' );
1694 if ( empty( $id ) || empty( $terms ) || is_wp_error( $terms ) ) {
1695 return '';
1696 }
1697 ob_start();
1698 ?>
1699
1700 <ul class="rtsb-brand-list <?php echo esc_attr( $class ); ?>">
1701 <?php
1702 $brand_list = get_the_term_list(
1703 $id,
1704 'product_brand',
1705 '<li class="rtsb-brand-list-item">',
1706 '</li><li class="rtsb-brand-list-item">',
1707 '</li>'
1708 );
1709
1710 self::print_html( $brand_list );
1711 ?>
1712 </ul>
1713
1714 <?php
1715 self::print_html( ob_get_clean() );
1716 }
1717
1718 /**
1719 * Get Featured Image HTML.
1720 *
1721 * @param string $type Image type.
1722 * @param int $post_id Post ID.
1723 * @param string $f_img_size Image size.
1724 * @param null $default_img_id Default image ID.
1725 * @param array $custom_img_size Custom image size.
1726 * @param bool $lazy Lazy load check.
1727 * @param bool $hover Hover image.
1728 * @param bool $gallery Gallery image.
1729 * @param bool $custom_image_id Custom category image ID.
1730 *
1731 * @return string|null
1732 */
1733 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 ) {
1734 $img_html = null;
1735 $attachment_id = null;
1736 $c_size = false;
1737 $hover_class = '';
1738 $post_title = '';
1739
1740 if ( 'rtsb_custom' === $f_img_size ) {
1741 $f_img_size = 'full';
1742 $c_size = true;
1743 }
1744
1745 if ( ! rtsb()->has_pro() ) {
1746 $gallery = false;
1747 }
1748
1749 if ( $hover ) {
1750 $a_id = $post_id;
1751 $hover_class = ' rtsb-img-hover';
1752 } elseif ( $gallery ) {
1753 $a_id = $post_id;
1754 } else {
1755 if ( 'product' === $type ) {
1756 $a_id = get_post_thumbnail_id( $post_id );
1757 $post_title = get_the_title( $post_id );
1758 } elseif ( 'category' === $type ) {
1759 $a_id = $custom_image_id ? $custom_image_id : get_term_meta( $post_id, 'thumbnail_id', true );
1760 $post_title = get_term( $post_id )->name;
1761 } else {
1762 $a_id = $default_img_id;
1763 }
1764 }
1765
1766 $img_alt = trim( wp_strip_all_tags( get_post_meta( $a_id, '_wp_attachment_image_alt', true ) ) );
1767 $alt_tag = ! empty( $img_alt ) ? $img_alt : wp_strip_all_tags( $post_title );
1768 $lazy_class = $lazy ? ' swiper-lazy' : '';
1769 $attr = [
1770 'class' => 'img-responsive rtsb-product-image' . $lazy_class . $hover_class,
1771 'alt' => $alt_tag,
1772 ];
1773
1774 if ( $a_id ) {
1775 $img_html = wp_get_attachment_image( $a_id, $f_img_size, false, $attr );
1776 $attachment_id = $a_id;
1777 }
1778
1779 if ( ! $img_html && $default_img_id ) {
1780 $img_html = wp_get_attachment_image( $default_img_id, $f_img_size, false, $attr );
1781 $attachment_id = $default_img_id;
1782 }
1783
1784 if ( $img_html && $c_size ) {
1785 preg_match( '@src="([^"]+)"@', $img_html, $match );
1786 $img_src = array_pop( $match );
1787 $w = ! empty( $custom_img_size['width'] ) ? absint( $custom_img_size['width'] ) : null;
1788 $h = ! empty( $custom_img_size['height'] ) ? absint( $custom_img_size['height'] ) : null;
1789 $c = ! empty( $custom_img_size['crop'] ) && 'soft' === $custom_img_size['crop'] ? false : true;
1790
1791 if ( $w && $h ) {
1792 $image = self::image_resize( $img_src, $w, $h, $c, false );
1793
1794 if ( ! empty( $image ) ) {
1795 [ $src, $width, $height ] = $image;
1796
1797 $hwstring = image_hwstring( $width, $height );
1798 $attachment = get_post( $attachment_id );
1799 $attr = apply_filters( 'wp_get_attachment_image_attributes', $attr, $attachment, $f_img_size );
1800
1801 if ( $lazy ) {
1802 $attr['data-src'] = $src;
1803 } else {
1804 $attr['src'] = $src;
1805 }
1806
1807 $attr = array_map( 'esc_attr', $attr );
1808 $img_html = rtrim( "<img $hwstring" );
1809
1810 foreach ( $attr as $name => $value ) {
1811 $img_html .= " $name=" . '"' . $value . '"';
1812 }
1813
1814 $img_html .= ' />';
1815 }
1816 }
1817 }
1818
1819 if ( ! $img_html ) {
1820 $hwstring = image_hwstring( 160, 160 );
1821 $attr = isset( $attr['src'] ) ? apply_filters( 'wp_get_attachment_image_attributes', $attr, false, $f_img_size ) : [];
1822 $attr['class'] = 'default-img ' . $hover_class;
1823 $attr['src'] = esc_url( rtsb()->get_assets_uri( 'images/demo.png' ) );
1824 $attr['alt'] = esc_html__( 'Default Image', 'shopbuilder' );
1825 $img_html = rtrim( "<img $hwstring" );
1826
1827 foreach ( $attr as $name => $value ) {
1828 $img_html .= " $name=" . '"' . $value . '"';
1829 }
1830
1831 $img_html .= ' />';
1832 }
1833
1834 if ( $lazy ) {
1835 $img_html = $img_html . '<div class="swiper-lazy-preloader swiper-lazy-preloader"></div>';
1836 }
1837
1838 return $img_html;
1839 }
1840
1841 /**
1842 * Call the Image resize model for resize function
1843 *
1844 * @param string $url URL.
1845 * @param int $width Width.
1846 * @param int $height Height.
1847 * @param string $crop Crop.
1848 * @param bool|true $single Single.
1849 * @param bool|false $upscale Upscale.
1850 *
1851 * @return array|bool|string
1852 */
1853 public static function image_resize( $url, $width = null, $height = null, $crop = null, $single = true, $upscale = false ) {
1854 $rtResize = new ReSizer();
1855
1856 return $rtResize->process( $url, $width, $height, $crop, $single, $upscale );
1857 }
1858
1859 /**
1860 * Get Product Image
1861 *
1862 * @param string $f_image Featured Image.
1863 * @param string $h_image Hover Image.
1864 *
1865 * @return void
1866 */
1867 public static function get_product_image( $f_image, $h_image = null ) {
1868 if ( empty( $f_image ) ) {
1869 return;
1870 }
1871
1872 echo wp_kses( $f_image, self::allowedHtml( 'image' ) );
1873
1874 if ( ! empty( $h_image ) ) {
1875 echo wp_kses( $h_image, self::allowedHtml( 'image' ) );
1876 }
1877 }
1878
1879 /**
1880 * Post Custom Field value.
1881 *
1882 * @param int $post_id Post id.
1883 * @param string $field_key Custom Field Key.
1884 * @param string $field_fallback FallBack text.
1885 *
1886 * @return string|void
1887 */
1888 public static function get_post_custom_field_value( $post_id, $field_key = '', $field_fallback = '' ) {
1889 if ( ! $post_id || ! $field_key ) {
1890 return;
1891 }
1892 $field_value = get_post_meta( $post_id, $field_key, true );
1893 if ( empty( $field_value ) ) {
1894 $field_value = ! empty( $field_fallback ) ? $field_fallback : $field_value;
1895 }
1896 $field_value = apply_filters( 'rtsb/get_post_custom_field_value/' . $field_key, $field_value, $field_key );
1897
1898 return sprintf( '<span class="rtsb-woo-custom-field">%s</span>', $field_value );
1899 }
1900
1901 /**
1902 * Elementor Icon
1903 *
1904 * @param array $control Elementor Control array.
1905 * @param string $class Custom class.
1906 * @param string $builder Builder name.
1907 *
1908 * @return string
1909 */
1910 public static function icons_manager( $control, $class = '', $builder = 'elementor' ): string {
1911 if ( empty( $control['value'] ) ) {
1912 return '';
1913 }
1914 if ( is_array( $control['value'] ) ) {
1915 $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1916 } else {
1917 $cache_key = 'icons_managet_' . str_replace( ' ', '', $control['value'] );
1918 }
1919 if ( isset( self::$cache[ $cache_key ] ) ) {
1920 return self::$cache[ $cache_key ];
1921 }
1922
1923 $attributes = [
1924 'aria-hidden' => 'true',
1925 ];
1926
1927 if ( ! empty( $class ) ) {
1928 $attributes['class'] = esc_attr( $class );
1929 }
1930
1931 ob_start();
1932
1933 if ( defined( 'ELEMENTOR_VERSION' ) && ! empty( $control ) && 'elementor' === $builder ) {
1934 Icons_Manager::render_icon( $control, $attributes );
1935 }
1936
1937 $icons = ob_get_clean();
1938 self::$cache[ $cache_key ] = $icons;
1939 return $icons;
1940 }
1941
1942 /**
1943 * Get product swatches.
1944 *
1945 * @param string $type Swatch type.
1946 *
1947 * @return void
1948 */
1949 public static function get_product_swatches( $type ) {
1950 ?>
1951 <div class="rtsb-swatches <?php echo esc_attr( $type ); ?>-layout">
1952 <?php
1953 if ( class_exists( 'Rtwpvsp' ) ) {
1954 do_action( 'rtwpvs_show_archive_variation' );
1955 } else {
1956 do_action( 'rtsb/vs/showcase/variation' );
1957 }
1958 ?>
1959 </div>
1960 <?php
1961 }
1962
1963 /**
1964 * Get sale badge
1965 *
1966 * @param string $type Badge type.
1967 * @param string $text Badge text.
1968 * @param string $out_of_stock_text Out of stock text.
1969 *
1970 * @return string
1971 */
1972 public static function get_sale_badge( $type, $text, $out_of_stock_text ) {
1973 global $product;
1974
1975 if ( ! $product->is_in_stock() ) {
1976 return $out_of_stock_text;
1977 }
1978
1979 if ( ! $product->is_on_sale() ) {
1980 return '';
1981 }
1982
1983 $badge_text = '';
1984 $percentage = self::calculate_sale_percentage( $product );
1985
1986 if ( $percentage > 0 ) {
1987 $badge_text = '-' . round( $percentage ) . '%';
1988 }
1989
1990 if ( 'text' === $type ) {
1991 $badge_text = $text;
1992 }
1993
1994 return $badge_text;
1995 }
1996
1997 /**
1998 * Get sale badge
1999 *
2000 * @param object $product Product Object.
2001 * @param string $type Badge type.
2002 * @param string $text Badge text.
2003 * @param string $out_of_stock_text Out of stock text.
2004 *
2005 * @return string
2006 */
2007 public static function get_promo_badge( $product, $type, $text, $out_of_stock_text ) {
2008 $disable_badges = self::get_option( 'general', 'guest_user', 'hide_badges', '' );
2009 $badges_visibility = rtsb()->has_pro() && ! is_user_logged_in() && $disable_badges;
2010
2011 if ( $badges_visibility ) {
2012 return '';
2013 }
2014
2015 if ( is_null( $product ) ) {
2016 global $product;
2017 }
2018
2019 if ( ! $product instanceof WC_Product ) {
2020 return '';
2021 }
2022
2023 if ( ! $product->is_in_stock() ) {
2024 return $out_of_stock_text;
2025 }
2026
2027 if ( ! $product->is_on_sale() ) {
2028 return '';
2029 }
2030
2031 $badge_text = '';
2032 $percentage = self::calculate_sale_percentage( $product );
2033
2034 if ( $percentage > 0 ) {
2035 $badge_text = '-' . round( $percentage ) . '%';
2036 }
2037
2038 if ( 'text' === $type ) {
2039 $badge_text = $text;
2040 }
2041
2042 return $badge_text;
2043 }
2044
2045 /**
2046 * Calculate Sale Percentage
2047 *
2048 * @param object $product Product object.
2049 *
2050 * @return float|int|string
2051 */
2052 public static function calculate_sale_percentage( $product = null ) {
2053 if ( is_null( $product ) ) {
2054 global $product;
2055 }
2056 if ( ! $product instanceof WC_Product ) {
2057 return '';
2058 }
2059 if ( ! $product->is_on_sale() ) {
2060 return '';
2061 }
2062 $max_percentage = 0;
2063 if ( $product->is_type( 'simple' ) ) {
2064 $regular_price = (float) $product->get_regular_price();
2065 $sale_price = (float) $product->get_sale_price();
2066 if ( $regular_price > 0 && $sale_price > 0 ) {
2067 $max_percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2068 }
2069 } elseif ( $product->is_type( 'variable' ) ) {
2070 $prices = $product->get_variation_prices( true );
2071 if ( ! empty( $prices['regular_price'] ) && ! empty( $prices['sale_price'] ) ) {
2072 foreach ( $prices['regular_price'] as $key => $regular_price ) {
2073 $sale_price = $prices['sale_price'][ $key ];
2074
2075 if ( $regular_price > 0 && $sale_price > 0 && $regular_price > $sale_price ) {
2076 $percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2077 $max_percentage = max( $max_percentage, $percentage );
2078 }
2079 }
2080 }
2081 }
2082 return $max_percentage > 0 ? round( $max_percentage ) : 0;
2083 }
2084
2085 /**
2086 * Pagination
2087 *
2088 * @param string $pages Pages.
2089 * @param integer $range Range.
2090 * @param boolean $ajax Ajax.
2091 *
2092 * @return string
2093 */
2094 public static function custom_pagination( $pages = '', $range = 4, $ajax = false ) {
2095 $html = null;
2096 $visible_range = apply_filters( 'rtsb/elements/pagination/visible_range', ( $range * 2 ) + 1 );
2097
2098 global $paged;
2099
2100 // Prefer `paged` (archive pagination), fall back to `page` (paginated singular posts).
2101 // This handles the case where the shop/archive is set as the front page — `is_front_page()`
2102 // alone would force `page` and miss the archive `paged` value.
2103 $paged = get_query_var( 'paged' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2104 if ( ! $paged ) {
2105 $paged = get_query_var( 'page' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2106 }
2107 if ( ! $paged ) {
2108 $paged = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2109 }
2110
2111 if ( '' === $pages ) {
2112 global $wp_query;
2113
2114 $pages = $wp_query->max_num_pages;
2115
2116 if ( ! $pages ) {
2117 $pages = 1;
2118 }
2119 }
2120
2121 $ajaxClass = null;
2122 $dataAttr = null;
2123
2124 if ( $ajax ) {
2125 $ajaxClass = ' rtsb-pagination-ajax';
2126 $dataAttr = "data-paged='1'";
2127 $dataAttr .= ' data-visible-range="' . esc_attr( $visible_range ) . '"';
2128 }
2129
2130 if ( 1 !== $pages ) {
2131 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
2132 $html .= '<ul class="pagination-list">';
2133
2134 if ( $paged > 2 && $paged > $visible_range + 1 && $visible_range < $pages ) {
2135 $html .= "<li><a data-paged='1' href='" . get_pagenum_link( 1 ) . "' aria-label='First'>&laquo;</a></li>";
2136 }
2137
2138 if ( $paged > 1 && $visible_range < $pages ) {
2139 $p = $paged - 1;
2140 $html .= "<li><a data-paged='{$p}' href='" . get_pagenum_link( $p ) . "' aria-label='Previous'>&lsaquo;</a></li>";
2141 }
2142
2143 for ( $i = 1; $i <= $pages; $i++ ) {
2144 if ( 1 != $pages && ( ! ( $i >= $paged + $visible_range + 1 || $i <= $paged - $visible_range - 1 ) || $pages <= $visible_range ) ) {
2145 $html .= ( $paged == $i ) ? '<li class="active"><span>' . $i . '</span></li>' : "<li><a data-paged='{$i}' href='" . get_pagenum_link( $i ) . "'>" . $i . '</a></li>';
2146 }
2147 }
2148
2149 if ( $paged < $pages && $visible_range < $pages ) {
2150 $p = $paged + 1;
2151 $html .= "<li><a data-paged='{$p}' href=\"" . get_pagenum_link( $paged + 1 ) . "\" aria-label='Next'>&rsaquo;</a></li>";
2152 }
2153
2154 if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $visible_range < $pages ) {
2155 $html .= "<li><a data-paged='{$pages}' href='" . get_pagenum_link( $pages ) . "' aria-label='Last'>&raquo;</a></li>";
2156 }
2157
2158 $html .= '</ul>';
2159 $html .= '</div>';
2160 }
2161
2162 return $html;
2163 }
2164
2165 /**
2166 * Action Buttons HTMl
2167 *
2168 * @param array $items Items.
2169 * @param array $ajax_cart Ajax cart HTML.
2170 * @param string $preset Button preset.
2171 * @param array $position Position.
2172 * @param string $placement Placement.
2173 *
2174 * @return void|string
2175 */
2176 public static function get_formatted_action_buttons( $items, $ajax_cart = '', $preset = 'preset1', $position = 'above', $placement = 'top' ) {
2177 if ( empty( $items ) ) {
2178 return;
2179 }
2180
2181 $html = '';
2182 $class = '';
2183 $args = [ $items, $ajax_cart, $preset, $position, $placement ];
2184
2185 if ( ( 'top' === $placement && 'after' === $position ) || ( 'bottom' === $placement && 'above' === $position ) ) {
2186 return apply_filters( 'rtsb/elements/elementor/formatted_action_buttons', $html, $args );
2187 }
2188
2189 if ( 'preset2' === $preset ) {
2190 $class = 'rtsb-action-buttons-vertical vertical-delay-effect ' . $preset;
2191 } elseif ( 'preset4' === $preset ) {
2192 $class = 'rtsb-action-buttons-vertical rtsb-action-buttons-vertical-left vertical-delay-effect ' . $preset;
2193 } elseif ( 'preset1' === $preset || 'preset3' === $preset ) {
2194 $class = 'rtsb-action-buttons-cart-box-width-auto horizontal-floating-btn ' . $preset;
2195 }
2196
2197 if ( 'after' === $position && 'preset1' === $preset ) {
2198 $class .= ' after-content';
2199 }
2200
2201 if ( 'preset3' === $preset ) {
2202 $html .= '<div class="rtsb-action-buttons top-part ' . esc_attr( $preset ) . '">';
2203 $html .= '<ul class="rtsb-action-button-list">';
2204
2205 ob_start();
2206 /**
2207 * Additional formatted action buttons hook.
2208 */
2209 do_action( 'rtsb/elements/elementor/additional_action_buttons', $items );
2210 $html .= ob_get_clean();
2211
2212 $html .= self::get_action_button_by_type( $items, 'wishlist' );
2213 $html .= self::get_action_button_by_type( $items, 'compare' );
2214 $html .= self::get_action_button_by_type( $items, 'quick_view' );
2215 $html .= '</ul>';
2216 $html .= '</div>';
2217 $html .= '<div class="rtsb-action-buttons bottom-part ' . esc_attr( $preset ) . '">';
2218 $html .= '<ul class="rtsb-action-button-list">';
2219 $html .= self::get_action_button_by_type( $items, 'add_to_cart', $ajax_cart );
2220 $html .= '</ul>';
2221 $html .= '</div>';
2222 } elseif ( 'preset1' === $preset || 'preset2' === $preset || 'preset4' === $preset ) {
2223 $html .= '<div class="rtsb-action-buttons ' . esc_attr( $class ) . '">';
2224 $html .= '<ul class="rtsb-action-button-list">';
2225 $html .= self::get_action_button_by_type( $items, 'add_to_cart', $ajax_cart );
2226
2227 ob_start();
2228 /**
2229 * Additional formatted action buttons hook.
2230 */
2231 do_action( 'rtsb/elements/elementor/additional_action_buttons', $items );
2232 $html .= ob_get_clean();
2233
2234 $html .= self::get_action_button_by_type( $items, 'wishlist' );
2235 $html .= self::get_action_button_by_type( $items, 'compare' );
2236 $html .= self::get_action_button_by_type( $items, 'quick_view' );
2237 $html .= '</ul>';
2238 $html .= '</div>';
2239 }
2240
2241 return apply_filters( 'rtsb/elements/elementor/formatted_action_buttons', $html, $args );
2242 }
2243
2244 /**
2245 * Get Action Button HTML
2246 *
2247 * @param array $items Items.
2248 * @param string $type Button type.
2249 * @param string $cart_html Ajax cart HTML.
2250 * @param string $wrapper Wrapper tag.
2251 *
2252 * @return void|string
2253 */
2254 public static function get_action_button_by_type( $items, $type, $cart_html = '', $wrapper = 'li' ) {
2255 if ( ! in_array( $type, $items, true ) ) {
2256 return;
2257 }
2258
2259 $html = '';
2260 $class = 'rtsb-action-button-item';
2261
2262 if ( 'add_to_cart' === $type ) {
2263 $class .= ' rtsb-cart' . ( empty( $cart_html ) ? esc_attr( ' no-cart-button' ) : '' );
2264 } else {
2265 $class .= ' rtsb-' . esc_attr( str_replace( '_', '-', $type ) );
2266 }
2267
2268 if ( ( 'add_to_cart' === $type ) && ( ! empty( $cart_html ) ) ) {
2269 $html .= $cart_html;
2270 } else {
2271 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
2272 }
2273
2274 if ( ! empty( $html ) ) {
2275 $html = '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">' . $html . '</' . esc_attr( $wrapper ) . '>';
2276 }
2277
2278 return apply_filters( 'rtsb/elements/elementor/get_action_button_by_type', $html );
2279 }
2280
2281 /**
2282 * Social Share Platforms.
2283 *
2284 * @return mixed|null
2285 */
2286 public static function social_share_platforms_list() {
2287 return apply_filters(
2288 'rtsb/settings/social_share/platforms',
2289 [
2290 [
2291 'value' => 'facebook',
2292 'label' => esc_html__( 'Facebook', 'shopbuilder' ),
2293 ],
2294 [
2295 'value' => 'twitter',
2296 'label' => esc_html__( 'Twitter', 'shopbuilder' ),
2297 ],
2298 [
2299 'value' => 'linkedin',
2300 'label' => esc_html__( 'Linkedin', 'shopbuilder' ),
2301 ],
2302 [
2303 'value' => 'pinterest',
2304 'label' => esc_html__( 'Pinterest', 'shopbuilder' ),
2305 ],
2306 [
2307 'value' => 'skype',
2308 'label' => esc_html__( 'Skype', 'shopbuilder' ),
2309 ],
2310 [
2311 'value' => 'whatsapp',
2312 'label' => esc_html__( 'Whatsapp', 'shopbuilder' ),
2313 ],
2314 [
2315 'value' => 'reddit',
2316 'label' => esc_html__( 'Reddit', 'shopbuilder' ),
2317 ],
2318 [
2319 'value' => 'telegram',
2320 'label' => esc_html__( 'Telegram', 'shopbuilder' ),
2321 ],
2322 ]
2323 );
2324 }
2325
2326 /**
2327 * Get Social Share link HTML.
2328 *
2329 * @param int $id Post ID.
2330 * @param array $types Preset type.
2331 * @param string $preset Style type.
2332 * @param boolean $show_icon Show icon.
2333 * @param boolean $show_text Show text.
2334 *
2335 * @return string
2336 */
2337 public static function get_social_share_html( int $id, array $types, string $preset = 'default', $show_icon = true, $show_text = true ) {
2338 $attr = [ 'postid' => $id ];
2339 $output = '';
2340
2341 if ( empty( $types ) ) {
2342 return $output;
2343 }
2344
2345 foreach ( $types as $type ) {
2346 $link = [];
2347 $link['type'] = $type['share_items'];
2348 $link['class'] = '';
2349 $link['img'] = apply_filters( 'rtsb/elements/share/default_img', '', $id, $link );
2350
2351 if ( 'site' === $id ) {
2352 $link['url'] = home_url();
2353 $link['title'] = wp_strip_all_tags( get_bloginfo( 'name' ) );
2354 } elseif ( 0 === strpos( $id, 'http' ) ) {
2355 $link['url'] = $id;
2356 $link['title'] = '';
2357 } else {
2358 $link['url'] = get_permalink( $id );
2359 $link['title'] = wp_strip_all_tags( get_the_title( $id ) );
2360
2361 if ( has_post_thumbnail( $id ) ) {
2362 $link['img'] = wp_get_attachment_image_url( get_post_thumbnail_id( $id ), 'full' );
2363 }
2364
2365 $link['img'] = apply_filters( 'rtsb/elements/share/single_img', $link['img'], $id, $link );
2366 }
2367
2368 $link['url'] = apply_filters( 'rtsb/elements/share/url', $link['url'], $link );
2369
2370 switch ( $type['share_items'] ) {
2371 case 'facebook':
2372 $link['link'] = esc_url( 'https://www.facebook.com/sharer/sharer.php?u=' . $link['url'] . '&display=popup&ref=plugin&src=share_button' );
2373 $link['icon'] = '<svg xmlns="http://www.w3.org/2000/svg" width="18.8125" height="32" viewBox="0 0 602 1024"><path d="M548 6.857v150.857h-89.714q-49.143 0-66.286 20.571t-17.143 61.714v108h167.429l-22.286 169.143h-145.143v433.714h-174.857v-433.714h-145.714v-169.143h145.714v-124.571q0-106.286 59.429-164.857t158.286-58.571q84 0 130.286 6.857z"></path></svg>';
2374 $link['attr_title'] = esc_html__( 'Share on Facebook', 'shopbuilder' );
2375 $link['social_network'] = 'Facebook';
2376 $link['social_action'] = 'Share';
2377 break;
2378 case 'twitter':
2379 $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'] );
2380 $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>';
2381 $link['attr_title'] = esc_html__( 'Share on Twitter', 'shopbuilder' );
2382 $link['social_network'] = 'Twitter';
2383 $link['social_action'] = 'Tweet';
2384 break;
2385 case 'pinterest':
2386 $link['link'] = esc_url( 'https://pinterest.com/pin/create/button/?url=' . $link['url'] . '&media=' . $link['img'] . '&description=' . $link['title'] );
2387 $link['icon'] = '<svg xmlns="http://www.w3.org/2000/svg" width="22.84375" height="32" viewBox="0 0 731 1024"><path d="M0 341.143q0-61.714 21.429-116.286t59.143-95.143 86.857-70.286 105.714-44.571 115.429-14.857q90.286 0 168 38t126.286 110.571 48.571 164q0 54.857-10.857 107.429t-34.286 101.143-57.143 85.429-82.857 58.857-108 22q-38.857 0-77.143-18.286t-54.857-50.286q-5.714 22.286-16 64.286t-13.429 54.286-11.714 40.571-14.857 40.571-18.286 35.714-26.286 44.286-35.429 49.429l-8 2.857-5.143-5.714q-8.571-89.714-8.571-107.429 0-52.571 12.286-118t38-164.286 29.714-116q-18.286-37.143-18.286-96.571 0-47.429 29.714-89.143t75.429-41.714q34.857 0 54.286 23.143t19.429 58.571q0 37.714-25.143 109.143t-25.143 106.857q0 36 25.714 59.714t62.286 23.714q31.429 0 58.286-14.286t44.857-38.857 32-54.286 21.714-63.143 11.429-63.429 3.714-56.857q0-98.857-62.571-154t-163.143-55.143q-114.286 0-190.857 74t-76.571 187.714q0 25.143 7.143 48.571t15.429 37.143 15.429 26 7.143 17.429q0 16-8.571 41.714t-21.143 25.714q-1.143 0-9.714-1.714-29.143-8.571-51.714-32t-34.857-54-18.571-61.714-6.286-60.857z"></path></svg>';
2388 $link['attr_title'] = esc_html__( 'Share on Pinterest', 'shopbuilder' );
2389 $link['social_network'] = 'Pinterest';
2390 $link['social_action'] = 'Pin';
2391 break;
2392 case 'linkedin':
2393 $link['link'] = esc_url( 'https://www.linkedin.com/shareArticle?url=' . $link['url'] . '&title=' . $link['title'] );
2394 $link['icon'] = '<svg xmlns="http://www.w3.org/2000/svg" width="27.4375" height="32" viewBox="0 0 878 1024"><path d="M199.429 357.143v566.286h-188.571v-566.286h188.571zM211.429 182.286q0.571 41.714-28.857 69.714t-77.429 28h-1.143q-46.857 0-75.429-28t-28.571-69.714q0-42.286 29.429-70t76.857-27.714 76 27.714 29.143 70zM877.714 598.857v324.571h-188v-302.857q0-60-23.143-94t-72.286-34q-36 0-60.286 19.714t-36.286 48.857q-6.286 17.143-6.286 46.286v316h-188q1.143-228 1.143-369.714t-0.571-169.143l-0.571-27.429h188v82.286h-1.143q11.429-18.286 23.429-32t32.286-29.714 49.714-24.857 65.429-8.857q97.714 0 157.143 64.857t59.429 190z"></path></svg>';
2395 $link['attr_title'] = esc_html__( 'Share on LinkedIn', 'shopbuilder' );
2396 $link['social_network'] = 'LinkedIn';
2397 $link['social_action'] = 'Share';
2398 break;
2399 case 'skype':
2400 $link['link'] = esc_url( 'https://web.skype.com/share?url=' . $link['url'] );
2401 $link['icon'] = '<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve" class="eapps-social-share-buttons-item-icon"> <path d="M23.016,13.971c0.111-0.638,0.173-1.293,0.173-1.963 c0-6.213-5.014-11.249-11.199-11.249c-0.704,0-1.393,0.068-2.061,0.193C8.939,0.348,7.779,0,6.536,0C2.926,0,0,2.939,0,6.565 c0,1.264,0.357,2.443,0.973,3.445c-0.116,0.649-0.18,1.316-0.18,1.999c0,6.212,5.014,11.25,11.198,11.25 c0.719,0,1.419-0.071,2.099-0.201C15.075,23.656,16.229,24,17.465,24C21.074,24,24,21.061,24,17.435 C24,16.163,23.639,14.976,23.016,13.971z M12.386,19.88c-3.19,0-6.395-1.453-6.378-3.953c0.005-0.754,0.565-1.446,1.312-1.446 c1.877,0,1.86,2.803,4.85,2.803c2.098,0,2.814-1.15,2.814-1.95c0-2.894-9.068-1.12-9.068-6.563c0-2.945,2.409-4.977,6.196-4.753 c3.61,0.213,5.727,1.808,5.932,3.299c0.102,0.973-0.543,1.731-1.662,1.731c-1.633,0-1.8-2.188-4.613-2.188 c-1.269,0-2.341,0.53-2.341,1.679c0,2.402,9.014,1.008,9.014,6.295C18.441,17.882,16.012,19.88,12.386,19.88z"></path> </svg>';
2402 $link['attr_title'] = esc_html__( 'Share on Skype', 'shopbuilder' );
2403 $link['social_network'] = 'Skype';
2404 $link['social_action'] = 'Skype';
2405 break;
2406 case 'whatsapp':
2407 $link['link'] = esc_url( 'https://wa.me/?text=' . $link['title'] . ' ' . $link['url'] );
2408 $link['icon'] = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-whatsapp" viewBox="0 0 16 16"> <path d="M13.601 2.326A7.854 7.854 0 0 0 7.994 0C3.627 0 .068 3.558.064 7.926c0 1.399.366 2.76 1.057 3.965L0 16l4.204-1.102a7.933 7.933 0 0 0 3.79.965h.004c4.368 0 7.926-3.558 7.93-7.93A7.898 7.898 0 0 0 13.6 2.326zM7.994 14.521a6.573 6.573 0 0 1-3.356-.92l-.24-.144-2.494.654.666-2.433-.156-.251a6.56 6.56 0 0 1-1.007-3.505c0-3.626 2.957-6.584 6.591-6.584a6.56 6.56 0 0 1 4.66 1.931 6.557 6.557 0 0 1 1.928 4.66c-.004 3.639-2.961 6.592-6.592 6.592zm3.615-4.934c-.197-.099-1.17-.578-1.353-.646-.182-.065-.315-.099-.445.099-.133.197-.513.646-.627.775-.114.133-.232.148-.43.05-.197-.1-.836-.308-1.592-.985-.59-.525-.985-1.175-1.103-1.372-.114-.198-.011-.304.088-.403.087-.088.197-.232.296-.346.1-.114.133-.198.198-.33.065-.134.034-.248-.015-.347-.05-.099-.445-1.076-.612-1.47-.16-.389-.323-.335-.445-.34-.114-.007-.247-.007-.38-.007a.729.729 0 0 0-.529.247c-.182.198-.691.677-.691 1.654 0 .977.71 1.916.81 2.049.098.133 1.394 2.132 3.383 2.992.47.205.84.326 1.129.418.475.152.904.129 1.246.08.38-.058 1.171-.48 1.338-.943.164-.464.164-.86.114-.943-.049-.084-.182-.133-.38-.232z"/> </svg>';
2409 $link['attr_title'] = esc_html__( 'Share on Whatsapp', 'shopbuilder' );
2410 $link['social_network'] = 'Whatsapp';
2411 $link['social_action'] = 'Share';
2412 break;
2413 case 'reddit':
2414 $link['link'] = esc_url( 'https://reddit.com/submit?url=' . $link['url'] . '&title=' . $link['title'] );
2415 $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>';
2416 $link['attr_title'] = esc_html__( 'Share on Reddit', 'shopbuilder' );
2417 $link['social_network'] = 'Reddit';
2418 $link['social_action'] = 'Share';
2419 break;
2420 case 'telegram':
2421 $link['link'] = esc_url( 'https://telegram.me/share/url?text=' . $link['title'] . '&url=' . $link['url'] );
2422 $link['icon'] = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-telegram" viewBox="0 0 16 16"> <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8.287 5.906c-.778.324-2.334.994-4.666 2.01-.378.15-.577.298-.595.442-.03.243.275.339.69.47l.175.055c.408.133.958.288 1.243.294.26.006.549-.1.868-.32 2.179-1.471 3.304-2.214 3.374-2.23.05-.012.12-.026.166.016.047.041.042.12.037.141-.03.129-1.227 1.241-1.846 1.817-.193.18-.33.307-.358.336a8.154 8.154 0 0 1-.188.186c-.38.366-.664.64.015 1.088.327.216.589.393.85.571.284.194.568.387.936.629.093.06.183.125.27.187.331.236.63.448.997.414.214-.02.435-.22.547-.82.265-1.417.786-4.486.906-5.751a1.426 1.426 0 0 0-.013-.315.337.337 0 0 0-.114-.217.526.526 0 0 0-.31-.093c-.3.005-.763.166-2.984 1.09z"/> </svg>';
2423 $link['attr_title'] = esc_html__( 'Share on Telegram', 'shopbuilder' );
2424 $link['social_network'] = 'Telegram';
2425 $link['social_action'] = 'Share';
2426 break;
2427 }
2428
2429 $link['label'] = $type['share_text'];
2430 $link['target'] = '_blank';
2431 $link['rel'] = 'nofollow noopener noreferrer';
2432
2433 $data = '';
2434 $link = apply_filters( 'rtsb/elements/share/share_link', $link, $id, $preset );
2435 $icon = apply_filters( 'rtsb/elements/share/share_icon', $link['icon'], $preset );
2436 $target = ! empty( $link['target'] ) ? ' target="' . esc_attr( $link['target'] ) . '" ' : '';
2437 $rel = ! empty( $link['rel'] ) ? ' rel="' . esc_attr( $link['rel'] ) . '" ' : '';
2438 $attr_title = ! empty( $link['attr_title'] ) ? ' title="' . esc_attr( $link['attr_title'] ) . '" ' : '';
2439 $elements = [];
2440
2441 // Add classes.
2442 $css_classes = [
2443 'rtsb-share-btn',
2444 sanitize_html_class( $link['type'] ),
2445 ];
2446 $css_classes = array_merge( $css_classes, explode( ' ', $link['class'] ) );
2447 $css_classes = array_map( 'sanitize_html_class', $css_classes );
2448 $css_classes = implode( ' ', array_filter( $css_classes ) );
2449
2450 unset( $attr['pin-do'], $attr['action'] );
2451
2452 if ( 'pinterest' === $type['share_items'] ) {
2453 $attr['pin-do'] = 'none';
2454 }
2455
2456 if ( 'whatsapp' === $type['share_items'] ) {
2457 $attr['action'] = 'share/whatsapp/share';
2458 }
2459
2460 $attr = apply_filters( 'rtsb/elements/share/link_data', $attr, $link, $id );
2461
2462 if ( ! empty( $attr ) ) {
2463 foreach ( $attr as $key => $val ) {
2464 $data .= ' data-' . sanitize_html_class( $key ) . '="' . esc_attr( $val ) . '"';
2465 }
2466 }
2467
2468 $additional_attr = apply_filters( 'rtsb/elements/share/additional_attr', [], $link, $id, $preset );
2469
2470 if ( ! empty( $additional_attr ) ) {
2471 $attr_output = join( ' ', $additional_attr );
2472
2473 if ( ! empty( $data ) ) {
2474 $attr_output = ' ' . $attr_output;
2475 }
2476
2477 $data .= $attr_output;
2478 }
2479
2480 $elements['wrapper_start'] = sprintf(
2481 '<li class="rtsb-share-item"><a href="%s"%s%s%s class="%s"%s>',
2482 ! empty( $link['link'] ) ? esc_attr( $link['link'] ) : '',
2483 $attr_title,
2484 $target,
2485 $rel,
2486 $css_classes,
2487 $data
2488 );
2489 $elements['wrapper_end'] = '</a></li>';
2490
2491 $elements['icon'] = $show_icon ? '<span class="rtsb-share-icon">' . ( ! empty( $icon ) ? $icon : null ) . '</span>' : null;
2492 $elements['label'] = $show_text && ! empty( $link['label'] ) ? '<span class="rtsb-share-label">' . $link['label'] . '</span>' : null;
2493 $elements['icon_label'] = '<span class="rtsb-share-icon-label">' . $elements['icon'] . $elements['label'] . '</span>';
2494 $elements = apply_filters( 'rtsb/elements/share/output_elements', $elements, $link, $id );
2495
2496 $output .= $elements['wrapper_start'] . $elements['icon_label'] . $elements['wrapper_end'];
2497 }
2498
2499 return apply_filters( 'rtsb/elements/share/list_output', $output );
2500 }
2501
2502 /**
2503 * Social Share Platforms.
2504 *
2505 * @param string $module Module.
2506 *
2507 * @return true|void
2508 */
2509 public static function is_module_active( $module ) {
2510 $modulelist = self::get_modules_list();
2511
2512 if ( ! empty( $modulelist[ $module ]['active'] ) ) {
2513 return true;
2514 }
2515 }
2516 /**
2517 * Checks if catalog pro is active
2518 *
2519 * @return boolean
2520 */
2521 public static function is_catalog_mode() {
2522 return function_exists( 'rtsbpro' ) && self::is_module_active( 'catalog_mode' );
2523 }
2524 /**
2525 * Checks if back in stock notifier is active
2526 *
2527 * @return boolean
2528 */
2529 public static function is_back_in_stock_notifier_enable() {
2530 return function_exists( 'rtsbpro' ) && self::is_module_active( 'back_in_stock_notifier' );
2531 }
2532
2533 /**
2534 * Elementor Widget Active.
2535 *
2536 * @param string $widget Elementor Widget.
2537 *
2538 * @return true|void
2539 */
2540 public static function is_elementor_widget_active( $widget ) {
2541 $element_list = self::get_widgets_list();
2542
2543 if ( ! empty( $element_list[ $widget ]['active'] ) ) {
2544 return true;
2545 }
2546 }
2547
2548 /**
2549 * Sanitize a media (fileupload) control value into an { id, source } array.
2550 *
2551 * The control value is posted as an array (or a JSON string). Using it as a
2552 * `sanitize_fn` avoids it being flattened to an empty string by the default
2553 * sanitize_text_field() path in set_options().
2554 *
2555 * @param mixed $raw_value Raw posted value.
2556 *
2557 * @return array|string
2558 */
2559 public static function sanitize_fileupload_value( $raw_value ) {
2560 $decoded = is_string( $raw_value ) && '' !== $raw_value ? json_decode( wp_unslash( $raw_value ), true ) : $raw_value;
2561 if ( ! is_array( $decoded ) ) {
2562 return '';
2563 }
2564 return [
2565 'id' => isset( $decoded['id'] ) ? absint( $decoded['id'] ) : 0,
2566 'source' => isset( $decoded['source'] ) ? esc_url_raw( $decoded['source'] ) : '',
2567 ];
2568 }
2569
2570 /***
2571 * Save Settings data
2572 *
2573 * @param string $section_id Section ID.
2574 * @param string $block_id Block ID.
2575 * @param array $rawOptions Raw Options.
2576 *
2577 * @return array
2578 */
2579 public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) { // phpcs:ignore Generic.Metrics.NestingLevel.TooHigh
2580 $section_id = ! empty( $section_id ) ? sanitize_text_field( wp_unslash( $section_id ) ) : '';
2581 $block_id = ! empty( $block_id ) ? sanitize_text_field( wp_unslash( $block_id ) ) : '';
2582 $rawOptions = ! empty( $rawOptions ) ? $rawOptions : [];
2583 $results = [
2584 'status' => true,
2585 'message' => '',
2586 ];
2587 if ( ! $section_id || ! $block_id ) {
2588 $results['status'] = false;
2589 $results['message'] = esc_html__( 'Section , block or options may be empty', 'shopbuilder' );
2590
2591 return $results;
2592 }
2593 $sections = Settings::instance()->get_sections();
2594 if ( empty( $sections[ $section_id ] ) || empty( $sections[ $section_id ]['list'][ $block_id ] ) ) {
2595 $results['status'] = false;
2596 $results['message'] = esc_html__( 'No section or block found with given data', 'shopbuilder' );
2597
2598 return $results;
2599 }
2600
2601 $options = DataModel::source()->get_option( $section_id, [], false );
2602 $changed = false;
2603 $fields = [];
2604 if ( isset( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) && ! empty( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) ) {
2605 $fields = $sections[ $section_id ]['list'][ $block_id ]['fields'];
2606 }
2607 do_action( 'rtsb/before/save/options', $section_id, $block_id, $rawOptions );
2608 if ( empty( $fields ) ) {
2609 if ( isset( $rawOptions['active'] ) ) {
2610 $changed = true;
2611 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
2612 }
2613 } else {
2614 foreach ( $rawOptions as $raw_option_key => $raw_value ) {
2615 if ( 'active' === $raw_option_key ) {
2616 $changed = true;
2617 $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : ''; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
2618 } else {
2619 if ( isset( $fields[ $raw_option_key ] ) ) {
2620 $field = $fields[ $raw_option_key ];
2621 if ( 'switch' === $field['type'] ) {
2622 $value = 'on' === $raw_value ? 'on' : '';
2623 } elseif ( 'repeaters' === $field['type'] ) {
2624 $the_value = [];
2625 $rep_title = [];
2626 if ( ! empty( $raw_value ) && is_array( $raw_value ) ) {
2627 foreach ( $raw_value as $key => $value ) {
2628 if ( is_string( $value ) ) {
2629 $the_value_decoded = json_decode( stripslashes_deep( $value ) );
2630 } else {
2631 $the_value_decoded = $value;
2632 }
2633 $cr = $the_value_decoded->title ?? '';
2634 if ( in_array( $cr, $rep_title, true ) ) {
2635 continue;
2636 }
2637 $rep_title[] = $cr;
2638 $the_value[] = $the_value_decoded;
2639 }
2640 } elseif ( ! empty( $raw_value ) && is_string( $raw_value ) ) {
2641 $the_value = $raw_value;
2642 }
2643 $value = wp_json_encode( $the_value, JSON_UNESCAPED_UNICODE );
2644 } elseif ( in_array( $field['type'], [ 'product_addons_special_settings', 'checkout_fields' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2645 $manual_field_value = [];
2646 if ( is_array( $raw_value ) ) {
2647 foreach ( $raw_value as $key => $value ) {
2648 $manual_field_value[] = json_decode( stripslashes( $value ), true );
2649 }
2650 $value = wp_json_encode( $manual_field_value, JSON_UNESCAPED_UNICODE );
2651 } elseif ( is_string( $raw_value ) ) {
2652 $value = $raw_value;
2653 }
2654 } else {
2655 if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2656 if ( isset( $raw_value ) && is_array( $raw_value ) ) {
2657 if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2658 $value = array_map( $field['sanitize_fn'], $raw_value );
2659 } else {
2660 $value = array_map( 'sanitize_text_field', $raw_value );
2661 }
2662 } else {
2663 $value = [];
2664 }
2665 } else {
2666 if ( ! empty( $field['sanitize_fn'] ) ) {
2667 if ( is_callable( $field['sanitize_fn'] ) ) {
2668 $value = $field['sanitize_fn']( $raw_value );
2669 } elseif ( 'pass_all' === $field['sanitize_fn'] ) {
2670 $value = $raw_value;
2671 } else {
2672 $value = $field['sanitize_fn']( $raw_value );
2673 }
2674 } else {
2675 $value = sanitize_text_field( $raw_value );
2676 }
2677 }
2678 }
2679 $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
2680 $changed = true;
2681 }
2682 }
2683 }
2684 }
2685 if ( ! $changed ) {
2686 $results['status'] = false;
2687 $results['message'] = esc_html__( 'No changes found for update', 'shopbuilder' );
2688
2689 return $results;
2690 }
2691
2692 DataModel::source()->set_option( $section_id, $options );
2693
2694 $results['status'] = true;
2695 $results['message'] = esc_html__( 'Successfully Saved', 'shopbuilder' );
2696 $results['sections'] = $sections;
2697
2698 return $results;
2699 }
2700
2701 /**
2702 * Count products by taxonomies.
2703 *
2704 * @param array $terms Terms.
2705 * @param string $taxonomy Taxonomy.
2706 *
2707 * @return int|void
2708 */
2709 public static function count_products_by_taxonomies( $terms, $taxonomy = 'product_cat' ) {
2710 if ( empty( $terms ) || ! is_array( $terms ) ) {
2711 return;
2712 }
2713
2714 $args = [
2715 'limit' => -1,
2716 'return' => 'ids',
2717 ];
2718
2719 if ( 'product_cat' === $taxonomy ) {
2720 $args['product_category_id'] = $terms;
2721 } elseif ( 'product_brand' === $taxonomy ) {
2722 $args['product_brand_id'] = $terms;
2723 } else {
2724 $args['product_tag_id'] = $terms;
2725 }
2726
2727 $query = new WC_Product_Query( $args );
2728
2729 return ! empty( $query->get_products() ) ? count( $query->get_products() ) : 0;
2730 }
2731
2732 /**
2733 * Count products by attribute terms.
2734 *
2735 * @param array $term_ids Term IDs.
2736 * @param string $relation Relation.
2737 *
2738 * @return int
2739 */
2740 public static function count_products_by_attribute_terms( $term_ids, $relation = 'AND' ) {
2741 if ( empty( $term_ids ) || ! is_array( $term_ids ) ) {
2742 return 0;
2743 }
2744
2745 $terms_by_taxonomy = [];
2746
2747 foreach ( $term_ids as $term_id ) {
2748 $term = get_term( $term_id );
2749 if ( ! is_wp_error( $term ) && $term ) {
2750 if ( ! isset( $terms_by_taxonomy[ $term->taxonomy ] ) ) {
2751 $terms_by_taxonomy[ $term->taxonomy ] = [];
2752 }
2753 $terms_by_taxonomy[ $term->taxonomy ][] = $term_id;
2754 }
2755 }
2756
2757 if ( empty( $terms_by_taxonomy ) ) {
2758 return 0;
2759 }
2760
2761 $args = [
2762 'status' => 'publish',
2763 'limit' => -1,
2764 'return' => 'ids',
2765 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2766 'relation' => strtoupper( $relation ),
2767 ],
2768 ];
2769
2770 foreach ( $terms_by_taxonomy as $taxonomy => $terms ) {
2771 $args['tax_query'][] = [
2772 'taxonomy' => $taxonomy,
2773 'field' => 'term_id',
2774 'terms' => $terms,
2775 'operator' => 'IN',
2776 ];
2777 }
2778
2779 $query = new WC_Product_Query( $args );
2780 $products = $query->get_products();
2781
2782 return count( $products );
2783 }
2784
2785 /**
2786 * Check if product filters widget has ajax.
2787 *
2788 * @param string $page Page name.
2789 *
2790 * @return bool
2791 */
2792 public static function product_filters_has_ajax( $page ) {
2793 if ( empty( $page ) ) {
2794 return false;
2795 }
2796
2797 if ( 'page' === get_post_type() ) {
2798 return false;
2799 }
2800
2801 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2802 if ( ! $id ) {
2803 return false;
2804 }
2805 $cache_key = 'product_filters_has_ajax_' . $id;
2806 if ( isset( self::$cache[ $cache_key ] ) ) {
2807 return self::$cache[ $cache_key ];
2808 }
2809 $elmap = ElementorDataMap::instance();
2810 $ajax = [];
2811
2812 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
2813 $ajax[] = isset( $data['settings']['ajax_mode'] ) ? false : true;
2814 }
2815
2816 self::$cache[ $cache_key ] = ! empty( $ajax[0] );
2817 return ! empty( $ajax[0] );
2818 }
2819
2820 /**
2821 * Build the active filter chips HTML from the current request query string.
2822 *
2823 * Mirrors the markup produced by the JS `displayAppliedFilter()` method so
2824 * the chips are visible on initial page load (before AJAX hydration).
2825 *
2826 * @return string HTML for the active filter chips, or empty string if none.
2827 */
2828 public static function get_active_filters_html() {
2829 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only query string parsing for display.
2830 if ( empty( $_GET ) ) {
2831 return '';
2832 }
2833
2834 $skip_keys = [
2835 'displayview',
2836 'orderby',
2837 'paged',
2838 'page',
2839 'product-page',
2840 'rtsb_archive_page',
2841 'rtsb_grid_columns',
2842 'min_price',
2843 'max_price',
2844 '_pjax',
2845 ];
2846
2847 // When the "Product Categories" widget is already showing the active
2848 // category navigation, suppress the duplicate product_cat chip group.
2849 if ( self::is_product_categories_widget_active() ) {
2850 $skip_keys[] = 'product_cat';
2851 }
2852
2853 $groups = [];
2854
2855 foreach ( $_GET as $raw_key => $raw_value ) {
2856 $key = sanitize_key( $raw_key );
2857
2858 if ( '' === $key || in_array( $key, $skip_keys, true ) ) {
2859 continue;
2860 }
2861
2862 if ( 0 === strpos( $key, 'query_type_' ) ) {
2863 continue;
2864 }
2865
2866 if ( is_array( $raw_value ) ) {
2867 continue;
2868 }
2869
2870 $value = sanitize_text_field( wp_unslash( $raw_value ) );
2871
2872 if ( '' === $value ) {
2873 continue;
2874 }
2875
2876 $taxonomy = $key;
2877 if ( 0 === strpos( $taxonomy, 'filter_' ) ) {
2878 $taxonomy = 'pa_' . substr( $taxonomy, 7 );
2879 }
2880
2881 $items = [];
2882 foreach ( explode( ',', $value ) as $slug ) {
2883 $slug = trim( $slug );
2884 if ( '' === $slug ) {
2885 continue;
2886 }
2887 $items[] = [
2888 'slug' => $slug,
2889 'name' => self::get_active_filter_term_label( $taxonomy, $slug, $key ),
2890 ];
2891 }
2892
2893 if ( empty( $items ) ) {
2894 continue;
2895 }
2896
2897 $groups[ $key ] = [
2898 'label' => self::get_active_filter_group_label( $taxonomy, $key ),
2899 'items' => $items,
2900 'class' => $key,
2901 ];
2902 }
2903
2904 // Price range as a single combined chip.
2905 if ( isset( $_GET['min_price'] ) || isset( $_GET['max_price'] ) ) {
2906 $min = isset( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : '';
2907 $max = isset( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : '';
2908
2909 if ( '' !== $min || '' !== $max ) {
2910 $symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '';
2911 $groups['price_filter'] = [
2912 'label' => esc_html__( 'Price', 'shopbuilder' ),
2913 'class' => 'price_filter',
2914 'items' => [
2915 [
2916 'slug' => $min . ',' . $max,
2917 'name' => $symbol . $min . ' - ' . $symbol . $max,
2918 'extra_class' => ' remove-price',
2919 ],
2920 ],
2921 ];
2922 }
2923 }
2924
2925 if ( empty( $groups ) ) {
2926 return '';
2927 }
2928
2929 $html = '<div class="rtsb-active-filters">';
2930
2931 foreach ( $groups as $key => $group ) {
2932 $html .= '<div class="active-filter ' . esc_attr( $group['class'] ) . '">';
2933
2934 if ( ! empty( $group['label'] ) ) {
2935 $html .= '<div class="filter-name">' . esc_html( $group['label'] ) . ': </div>';
2936 }
2937
2938 $html .= '<div class="filter-item-container">';
2939
2940 foreach ( $group['items'] as $item ) {
2941 $extra = isset( $item['extra_class'] ) ? $item['extra_class'] : '';
2942 $html .= '<div class="filter-item">' . esc_html( $item['name'] );
2943 $html .= '<span class="remove-filter' . esc_attr( $extra ) . '"';
2944 $html .= ' title="' . esc_attr( sprintf( /* translators: %s: filter term name */ __( 'Remove %s', 'shopbuilder' ), $item['name'] ) ) . '"';
2945 $html .= ' data-filter-name="' . esc_attr( $key ) . '"';
2946 $html .= ' data-filter-value="' . esc_attr( $item['slug'] ) . '">';
2947 $html .= '<i class="rtsb-icon rtsb-icon-delete"></i></span>';
2948 $html .= '</div>';
2949 }
2950
2951 $html .= '</div></div>';
2952 }
2953
2954 $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>';
2955 $html .= '</div>';
2956
2957 return $html;
2958 // phpcs:enable
2959 }
2960
2961 /**
2962 * Determine whether the "Product Categories" Elementor widget is present
2963 * on the current shop/archive layout. Used to avoid duplicating the
2964 * product_cat chip group when the widget already shows the active term.
2965 *
2966 * @return bool
2967 */
2968 private static function is_product_categories_widget_active() {
2969 $elmap = ElementorDataMap::instance();
2970
2971 foreach ( [ 'shop', 'archive' ] as $page ) {
2972 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2973
2974 if ( ! $id ) {
2975 continue;
2976 }
2977
2978 $widgets = $elmap->get_widget_data( 'rtsb-product-categories-general', [], $id );
2979
2980 if ( ! empty( $widgets ) ) {
2981 return true;
2982 }
2983 }
2984
2985 return false;
2986 }
2987
2988 /**
2989 * Resolve the display label for an active filter group (taxonomy heading).
2990 *
2991 * @param string $taxonomy Resolved taxonomy slug (e.g. product_cat, pa_color).
2992 * @param string $raw_key Original URL key (e.g. product_cat, filter_color).
2993 *
2994 * @return string
2995 */
2996 private static function get_active_filter_group_label( $taxonomy, $raw_key ) {
2997 if ( 's' === $raw_key || 'search' === $raw_key ) {
2998 return esc_html__( 'Search', 'shopbuilder' );
2999 }
3000 if ( 'rating_filter' === $raw_key ) {
3001 return esc_html__( 'Ratings', 'shopbuilder' );
3002 }
3003 if ( 'sale_filter' === $raw_key ) {
3004 return esc_html__( 'Sale Filter', 'shopbuilder' );
3005 }
3006
3007 if ( 0 === strpos( $taxonomy, 'pa_' ) && function_exists( 'wc_attribute_label' ) ) {
3008 return wc_attribute_label( $taxonomy );
3009 }
3010
3011 if ( taxonomy_exists( $taxonomy ) ) {
3012 $tax_obj = get_taxonomy( $taxonomy );
3013 if ( $tax_obj && ! empty( $tax_obj->labels->name ) ) {
3014 return $tax_obj->labels->name;
3015 }
3016 }
3017
3018 return ucwords( str_replace( [ '_', '-' ], ' ', $raw_key ) );
3019 }
3020
3021 /**
3022 * Resolve a term slug to its display name; fall back to a humanised slug.
3023 *
3024 * @param string $taxonomy Resolved taxonomy slug.
3025 * @param string $slug Raw slug from query string.
3026 * @param string $raw_key Original URL key.
3027 *
3028 * @return string
3029 */
3030 private static function get_active_filter_term_label( $taxonomy, $slug, $raw_key ) {
3031 if ( 's' === $raw_key || 'search' === $raw_key || 'rating_filter' === $raw_key ) {
3032 return rawurldecode( $slug );
3033 }
3034
3035 if ( taxonomy_exists( $taxonomy ) ) {
3036 $term = get_term_by( 'slug', $slug, $taxonomy );
3037 if ( $term && ! is_wp_error( $term ) ) {
3038 return $term->name;
3039 }
3040 }
3041
3042 return ucwords( str_replace( [ '-', '_' ], ' ', rawurldecode( $slug ) ) );
3043 }
3044
3045 /**
3046 * Check if product has applied filter.
3047 *
3048 * @param string $page Page name.
3049 *
3050 * @return bool
3051 */
3052 public static function product_has_applied_filters( $page ) {
3053 if ( empty( $page ) ) {
3054 return false;
3055 }
3056
3057 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
3058 $elmap = ElementorDataMap::instance();
3059 $ajax = [];
3060 if ( ! $id ) {
3061 return false;
3062 }
3063 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
3064 $ajax[] = ! isset( $data['settings']['active_filter'] );
3065
3066 }
3067
3068 // Also account for the free, non-AJAX "Product Filters" widget so that
3069 // the active filter chips wrapper renders for it on initial page load.
3070 foreach ( $elmap->get_widget_data( 'rtsb-product-filters', [], $id ) as $data ) {
3071 $ajax[] = true;
3072 unset( $data );
3073 }
3074
3075 return ! empty( $ajax[0] );
3076 }
3077
3078 /**
3079 * Get WooCommerce product categories.
3080 *
3081 * @param string|null $search_query The search string for category names.
3082 *
3083 * @return array An array of product categories with 'value' and 'label' keys.
3084 */
3085 public static function products_category_query( $search_query = null ) {
3086 if ( ! is_admin() ) {
3087 return [];
3088 }
3089
3090 $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
3091
3092 if ( isset( self::$cache[ $cache_key ] ) ) {
3093 return self::$cache[ $cache_key ];
3094 }
3095 $results = wp_cache_get( $cache_key, 'shopbuilder' );
3096 if ( ! $results ) {
3097 global $wpdb;
3098 $sql = "SELECT t.term_id, t.name
3099 FROM {$wpdb->terms} t
3100 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
3101 WHERE tt.taxonomy = 'product_cat'";
3102
3103 if ( $search_query ) {
3104 $sql .= ' AND t.name LIKE %s';
3105 $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3106 } else {
3107 $prepared_sql = $sql; // No need for placeholders.
3108 }
3109
3110 $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
3111 // Cache the results in the object cache for future use.
3112 wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
3113 Cache::set_data_cache_key( $cache_key );
3114 }
3115
3116 $cats = [];
3117 if ( ! is_array( $results ) && ! count( $results ) ) {
3118 return $cats;
3119 }
3120 foreach ( $results as $row ) {
3121 $category_id = $row->term_id;
3122 $category_title = $row->name;
3123
3124 $cats[] = [
3125 'value' => $category_id,
3126 'label' => $category_title,
3127 ];
3128 }
3129
3130 // Cache the results in a static variable for the next call.
3131 self::$cache[ $cache_key ] = $cats;
3132 return $cats;
3133 }
3134 /**
3135 * Get WooCommerce product categories.
3136 *
3137 * @param string|null $search_query The search string for category names.
3138 *
3139 * @return array An array of product categories with 'value' and 'label' keys.
3140 */
3141 public static function products_tags_query( $search_query = null ) {
3142 if ( ! is_admin() ) {
3143 return [];
3144 }
3145
3146 $cache_key = 'rtsb_product_tags_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
3147
3148 if ( isset( self::$cache[ $cache_key ] ) ) {
3149 return self::$cache[ $cache_key ];
3150 }
3151 $results = wp_cache_get( $cache_key, 'shopbuilder' );
3152 if ( ! $results ) {
3153 global $wpdb;
3154 $sql = "SELECT t.term_id, t.name
3155 FROM {$wpdb->terms} t
3156 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
3157 WHERE tt.taxonomy = 'product_tag'";
3158
3159 if ( $search_query ) {
3160 $sql .= ' AND t.name LIKE %s';
3161 $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3162 } else {
3163 $prepared_sql = $sql; // No need for placeholders.
3164 }
3165
3166 $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
3167 // Cache the results in the object cache for future use.
3168 wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
3169 Cache::set_data_cache_key( $cache_key );
3170 }
3171
3172 $cats = [];
3173 if ( ! is_array( $results ) && ! count( $results ) ) {
3174 return $cats;
3175 }
3176 foreach ( $results as $row ) {
3177 $category_id = $row->term_id;
3178 $category_title = $row->name;
3179
3180 $cats[] = [
3181 'value' => $category_id,
3182 'label' => $category_title,
3183 ];
3184 }
3185
3186 // Cache the results in a static variable for the next call.
3187 self::$cache[ $cache_key ] = $cats;
3188 return $cats;
3189 }
3190
3191 /**
3192 * @param string $search_query Search query.
3193 *
3194 * @return array
3195 */
3196 public static function get_registered_post_types( $search_query = null ) {
3197 // Get all registered post types.
3198 $registered_post_types = get_post_types(
3199 [
3200 'public' => true,
3201 '_builtin' => false,
3202 ],
3203 'objects'
3204 );
3205
3206 unset( $registered_post_types['elementor_library'] );
3207 unset( $registered_post_types['e-landing-page'] );
3208 unset( $registered_post_types['rtsb_builder'] );
3209
3210 $post_types = [];
3211
3212 // Check if a search query is provided.
3213 if ( ! empty( $search_query ) ) {
3214 // Filter post types based on the search query.
3215 $registered_post_types = array_filter(
3216 $registered_post_types,
3217 function ( $post_type ) use ( $search_query ) {
3218 return stripos( $post_type->labels->singular_name, $search_query ) !== false;
3219 }
3220 );
3221 // Check if 'post' match the search query and include them if they do.
3222 if ( stripos( 'post', $search_query ) !== false ) {
3223 $post_types[] = [
3224 'value' => 'post',
3225 'label' => 'Post',
3226 ];
3227 }
3228 } else {
3229 $post_types[] = [
3230 'value' => 'post',
3231 'label' => 'Post',
3232 ];
3233 }
3234
3235 // Convert the filtered post types to an array.
3236 foreach ( $registered_post_types as $post_type ) {
3237 $post_types[] = [
3238 'value' => $post_type->name,
3239 'label' => $post_type->labels->singular_name,
3240 ];
3241 }
3242
3243 return $post_types;
3244 }
3245
3246 /**
3247 * Get Post Types.
3248 *
3249 * @param string $search_query Search query.
3250 * @param array $args Arguments.
3251 *
3252 * @return array
3253 */
3254 public static function get_post_types( $search_query = null, $args = [] ) {
3255
3256 $args = wp_parse_args(
3257 $args,
3258 [
3259 'post_type' => 'any',
3260 'posts_per_page' => 20,
3261 'orderby' => 'ID',
3262 'order' => 'DESC',
3263 ]
3264 );
3265
3266 if ( 'archive' !== $args['post_type'] ) {
3267 if ( $search_query ) {
3268 $args['s'] = $search_query;
3269 }
3270 $posts = [];
3271 $query_results = get_posts( $args );
3272 foreach ( $query_results as $post ) {
3273 $posts[] = [
3274 'value' => $post->ID,
3275 'label' => $post->post_title . ' (ID#' . $post->ID . ')',
3276 ];
3277 }
3278
3279 if ( $search_query ) {
3280 if ( strpos( '-error 404', $search_query ) ) {
3281 $posts[] = [
3282 'value' => 'error',
3283 'label' => __( '404 Error', 'shopbuilder' ),
3284 ];
3285 }
3286 } else {
3287 if ( 'page' === $args['post_type'] ) {
3288 $posts[] = [
3289 'value' => 'error',
3290 'label' => __( '404 Error', 'shopbuilder' ),
3291 ];
3292 }
3293 }
3294 } else {
3295 $posts = self::get_registered_post_types( $search_query );
3296 }
3297
3298 return $posts;
3299 }
3300
3301 /**
3302 * Get all Elementor breakpoints.
3303 *
3304 * @return array
3305 */
3306 public static function get_elementor_breakpoints() {
3307 return ( new \Elementor\Core\Breakpoints\Manager() )->get_breakpoints_config();
3308 }
3309
3310 /**
3311 * Render view.
3312 *
3313 * @param string $viewName View name.
3314 * @param array $args View args.
3315 * @param boolean $return View return.
3316 * @return string|void
3317 */
3318 public static function renderView( $viewName, $args = [], $return = false ) {
3319 $viewName = str_replace( '.', '/', $viewName );
3320
3321 if ( ! empty( $args ) && is_array( $args ) ) {
3322 extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
3323 }
3324
3325 $view_file = rtsb()->plugin_path() . '/resources/' . $viewName . '.php';
3326
3327 if ( ! file_exists( $view_file ) ) {
3328 _doing_it_wrong( __FUNCTION__, sprintf( '<code>%s</code> does not exist.', esc_html( $view_file ) ), '1.7.0' );
3329
3330 return;
3331 }
3332
3333 if ( $return ) {
3334 ob_start();
3335 include $view_file;
3336
3337 return ob_get_clean();
3338 } else {
3339 include $view_file;
3340 }
3341 }
3342
3343 /**
3344 * Best selling product query.
3345 *
3346 * @param int $minimum_sale Minimum sale.
3347 * @param int $limit Total limit.
3348 *
3349 * @return array|mixed
3350 */
3351 public static function best_selling_products_ids( $minimum_sale = 1, $limit = 10 ) {
3352 $cache_key = 'rtsb_best_selling_products_' . $minimum_sale . '_' . $limit;
3353 // Check if the data is in the cache.
3354 if ( isset( self::$cache[ $cache_key ] ) ) {
3355 return self::$cache[ $cache_key ];
3356 }
3357 $cached_data = get_transient( $cache_key );
3358 if ( $cached_data ) {
3359 self::$cache[ $cache_key ] = $cached_data;
3360 return $cached_data;
3361 }
3362 global $wpdb;
3363 $sql = $wpdb->prepare(
3364 "
3365 SELECT ID
3366 FROM {$wpdb->posts} AS p
3367 LEFT JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id
3368 WHERE p.post_type = 'product'
3369 AND p.post_status = 'publish'
3370 AND pm.meta_key = 'total_sales'
3371 AND pm.meta_value >= %d
3372 ORDER BY pm.meta_value + 0 DESC
3373 LIMIT %d
3374 ",
3375 $minimum_sale,
3376 $limit
3377 );
3378 $best_selling = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
3379 // Cache the result for future use.
3380 set_transient( $cache_key, $best_selling, DAY_IN_SECONDS );
3381 self::$cache[ $cache_key ] = $best_selling;
3382 return $best_selling;
3383 }
3384
3385 /**
3386 * @param null|Object $product Product Object.
3387 * @param int $days_threshold Date String.
3388 *
3389 * @return bool
3390 */
3391 public static function is_new_product( $product = null, $days_threshold = 30 ) {
3392 if ( is_null( $product ) ) {
3393 global $product;
3394 }
3395 if ( ! $product instanceof WC_Product ) {
3396 return false;
3397 }
3398
3399 $product_id = $product->get_id();
3400 $days_threshold = ! empty( $days_threshold ) ? $days_threshold : 30;
3401 $cache_key = 'is_new_product_' . $product_id . '_' . $days_threshold;
3402
3403 if ( isset( self::$cache[ $cache_key ] ) ) {
3404 return self::$cache[ $cache_key ];
3405 }
3406 $date_created = $product->get_date_created();
3407 if ( ! $date_created ) {
3408 return false;
3409 }
3410 $publish_timestamp = strtotime( $date_created->date_i18n( 'Y-m-d H:i:s' ) );
3411
3412 // Calculate the difference in days.
3413 $days_difference = abs( time() - $publish_timestamp ) / ( 60 * 60 * 24 );
3414
3415 // Check if the product is considered new.
3416 $is_new = $days_difference <= $days_threshold;
3417
3418 // Cache the result for a day.
3419 self::$cache[ $cache_key ] = $is_new;
3420
3421 return $is_new;
3422 }
3423
3424 /**
3425 * Checks if a feature is disabled for guest users based on the provided option.
3426 *
3427 * @param string $option The option to retrieve from the item.
3428 * @param mixed $default The default value if the option is not found.
3429 *
3430 * @return bool
3431 */
3432 public static function is_guest_feature_disabled( $option, $default ) {
3433 $disabled = self::get_option( 'general', 'guest_user', $option, $default );
3434
3435 return ! is_user_logged_in() && $disabled;
3436 }
3437
3438 /**
3439 * Checks if a feature is disabled for guest users based on the provided option.
3440 *
3441 * @return void
3442 */
3443 public static function woocommerce_output_all_notices() {
3444 if ( function_exists( 'wc_print_notices' ) ) :
3445 echo '<div class="rtsb-notice">';
3446 woocommerce_output_all_notices();
3447 echo '</div>';
3448 endif;
3449 }
3450
3451 /**
3452 * @param object $product Product.
3453 * @return bool
3454 */
3455 public static function is_visible_qty_input( $product = null ) {
3456 $is_visible_qty = true;
3457 if ( $product instanceof \WC_Product ) {
3458 if ( $product->is_sold_individually() ) {
3459 $is_visible_qty = false;
3460 } elseif ( $product->managing_stock() ) {
3461 if ( in_array( $product->get_backorders(), [ 'notify' , 'yes' ], true ) ) {
3462 $is_visible_qty = true;
3463 } elseif ( $product->get_stock_quantity() < 2 ) {
3464 $is_visible_qty = false;
3465 }
3466 }
3467 }
3468 return $is_visible_qty;
3469 }
3470
3471 /**
3472 * @param int $object_id Object id.
3473 * @param string $element_type element type.
3474 * @param string|null $current_lang null for current language, default for default languages.
3475 * @return false|mixed|null
3476 */
3477 public static function wpml_object_id( $object_id, $element_type, $current_lang = 'default' ) {
3478 if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) {
3479 return $object_id;
3480 }
3481 if ( ! $object_id || ! $element_type ) {
3482 return $object_id;
3483 }
3484 if ( 'default' === $current_lang ) {
3485 $lang = apply_filters( 'wpml_default_language', null );
3486 } else {
3487 $lang = null;
3488 }
3489 return apply_filters( 'wpml_object_id', $object_id, $element_type, false, $lang );
3490 }
3491
3492 /**
3493 * @return string
3494 */
3495 public static function wpml_current_language() {
3496 $current = apply_filters( 'wpml_current_language', null );
3497 $default = apply_filters( 'wpml_default_language', null );
3498 return $default !== $current ? $current : null;
3499 }
3500
3501 /**
3502 * Custom icons.
3503 *
3504 * @return string[]
3505 */
3506 public static function get_custom_icon_names() {
3507 return [
3508 'heart-empty',
3509 'heart',
3510 'eye',
3511 'exchange',
3512 'plus',
3513 'minus',
3514 'avatar',
3515 'pay',
3516 'share',
3517 'clock',
3518 'check-alt',
3519 'check',
3520 'delete',
3521 'marker',
3522 'list',
3523 'list-2',
3524 'power',
3525 'cart',
3526 'cart-2',
3527 'cart-3',
3528 'downloads',
3529 'zoom',
3530 'user-edit',
3531 'grid',
3532 'filter',
3533 'billing',
3534 'login',
3535 'payment',
3536 'search',
3537 'edit',
3538 'coupon',
3539 'arrows-cw',
3540 'trash-empty',
3541 ];
3542 }
3543
3544 /**
3545 * Retrieve an array of custom icons with their HTML representation.
3546 *
3547 * @return array
3548 */
3549 public static function get_icons() {
3550 $icon = self::get_custom_icon_names();
3551 $icons_array = [];
3552 foreach ( $icon as $value ) {
3553 $icons_array[ $value ] = '<i class="rtsb-icon rtsb-icon-' . $value . '"></i> <span class="icon-name">' . ucfirst( str_replace( '-', ' ', $value ) ) . '</span>';
3554 }
3555 return $icons_array;
3556 }
3557
3558 /**
3559 * Generates HTML markup for displaying an endpoint icon.
3560 *
3561 * @param array $data The endpoint data containing icon information.
3562 * @param boolean $wrapper Whether to wrap the icon in a span element.
3563 *
3564 * @return string
3565 */
3566 public static function get_icon_html( $data, $wrapper = true ) {
3567 if ( empty( $data ) || 'none' === $data['icon_source'] ) {
3568 return '';
3569 }
3570 $endpoint = '';
3571 $html = $wrapper ? '<span class="icon ' . esc_attr( $endpoint ) . '_icon">' : '';
3572
3573 if ( 'select_icon' === $data['icon_source'] ) {
3574 $html .= '<i class="rtsb-icon rtsb-icon-' . esc_attr( $data['custom_icon'] ) . '"></i>';
3575 } else {
3576 $icon_html = '';
3577 $icon_url = $data['image_icon']['source'] ?? '';
3578 $icon_id = $data['image_icon']['id'] ?? 0;
3579 $extension = ! empty( $icon_url ) ? strtolower( substr( strrchr( $data['image_icon']['source'], '.' ), 1 ) ) : '';
3580
3581 if ( 'svg' === $extension ) {
3582 $content = file_get_contents( $icon_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
3583 $is_svg = strpos( $content, '<svg' );
3584
3585 if ( false !== $is_svg ) {
3586 $icon_html = substr( $content, $is_svg );
3587 }
3588 } else {
3589 $attr = [
3590 'class' => 'rtsb-icon-img',
3591 'alt' => esc_html( ucfirst( $endpoint ) ) . esc_html__( ' Icon', 'shopbuilder' ),
3592 ];
3593
3594 $icon_html = wp_get_attachment_image( absint( $icon_id ), 'full', true, $attr );
3595 }
3596
3597 $html .= $icon_html;
3598 }
3599
3600 $html .= $wrapper ? '</span>' : '';
3601
3602 return $html;
3603 }
3604 /**
3605 * Flushes rewrite rules.
3606 *
3607 * @return void
3608 */
3609 public static function maybe_flush_rewrite_rules() {
3610 add_action( 'wp_loaded', [ __CLASS__, 'flush_rewrite_rules' ] );
3611 add_action( 'shutdown', [ __CLASS__, 'flush_rewrite_rules_once_more' ] );
3612 }
3613
3614 /**
3615 * Flushes rewrite rules if needed.
3616 *
3617 * @return void
3618 */
3619 public static function flush_rewrite_rules() {
3620 if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3621 flush_rewrite_rules();
3622 }
3623
3624 if ( get_option( 'rtsb_permalinks_flushed' ) === 'yes' ) {
3625 flush_rewrite_rules();
3626 delete_option( 'rtsb_permalinks_flushed' );
3627 }
3628 }
3629
3630 /**
3631 * Flushes rewrite rules if needed for second time.
3632 *
3633 * @return void
3634 */
3635 public static function flush_rewrite_rules_once_more() {
3636 if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3637 flush_rewrite_rules();
3638 update_option( 'rtsb_permalinks_flushed', 'yes' );
3639 delete_option( 'rtsb_permalinks_need_flush' );
3640 }
3641 }
3642
3643 /**
3644 * Social Share Platforms.
3645 *
3646 * @param string $section_id Section ID.
3647 * @param string $block_id Block ID.
3648 * @param string $option_key Option key.
3649 * @param string $compare_key Compare key.
3650 * @param string $compare_value Compare value.
3651 * @param array $values Values.
3652 *
3653 * @return void
3654 */
3655 public static function set_repeater_options( $section_id, $block_id, $option_key, $compare_key, $compare_value, $values ) {
3656 $modules = DataModel::source()->get_option( $section_id, [], false );
3657 if ( empty( $modules[ $block_id ] ) ) {
3658 return;
3659 }
3660 $options = $modules[ $block_id ];
3661 if ( empty( $options[ $option_key ] ) ) {
3662 return;
3663 }
3664 $repeater_options = json_decode( $options[ $option_key ], true );
3665 // Check if options are not empty and are in array format.
3666 if ( ! is_array( $repeater_options ) ) {
3667 return;
3668 }
3669 // Use array_column to get an array of values from $compare_key.
3670 $column_values = array_column( $repeater_options, $compare_key );
3671 // Use array_search to find the index of the $compare_value.
3672 $index = array_search( $compare_value, $column_values, true );
3673 if ( false === $index ) {
3674 return;
3675 }
3676 $repeater_options[ $index ] = wp_parse_args( $values, $repeater_options[ $index ] );
3677 $options[ $option_key ] = wp_json_encode( $repeater_options );
3678 $modules[ $block_id ] = $options;
3679 DataModel::source()->set_option( $section_id, $modules );
3680 }
3681
3682
3683 /**
3684 * @param array $ids products id.
3685 * @return array
3686 */
3687 public static function get_available_products_by_ids( $ids = [] ) {
3688 $ids = array_filter(
3689 $ids,
3690 function ( $id ) {
3691 return 'product' === get_post_type( $id ) && 'publish' === get_post_status( $id );
3692 }
3693 );
3694 return $ids;
3695 }
3696
3697 /**
3698 * Generate and cache dynamic CSS styles.
3699 *
3700 * @param array $options CSS options to apply (e.g. padding, margin).
3701 * @param string $cache_key Cache key for the CSS.
3702 * @param array $css_properties An array of CSS properties with selectors and properties.
3703 *
3704 * @return void
3705 */
3706 public static function dynamic_styles( $options, $cache_key, $css_properties ) {
3707 $cached_css = wp_cache_get( $cache_key, 'shopbuilder' );
3708 $style_handle = self::optimized_handle( 'rtsb-frontend' );
3709
3710 if ( false !== $cached_css ) {
3711 wp_add_inline_style( $style_handle, $cached_css );
3712 return;
3713 }
3714
3715 $css_rules = [];
3716 $grouped_css_rules = [];
3717
3718 foreach ( $css_properties as $option => $details ) {
3719 if ( ! empty( $options[ $option ] ) ) {
3720 $selector = $details['selector'] ?? '';
3721 $property = $details['property'] ?? '';
3722 $unit = $details['unit'] ?? '';
3723 $value = $options[ $option ];
3724
3725 if ( empty( $grouped_css_rules[ $selector ] ) ) {
3726 $grouped_css_rules[ $selector ] = [];
3727 }
3728
3729 if ( is_array( $property ) ) {
3730 foreach ( $property as $prop ) {
3731 $grouped_css_rules[ $selector ][] = $prop . ': ' . $value . $unit . ' !important';
3732 }
3733 } else {
3734 $grouped_css_rules[ $selector ][] = $property . ': ' . $value . $unit . ' !important';
3735 }
3736 }
3737 }
3738
3739 foreach ( $grouped_css_rules as $selector => $properties ) {
3740 $css_rules[] = $selector . ' {' . implode( '; ', $properties ) . '}';
3741 }
3742
3743 $dynamic_css = implode( ' ', $css_rules );
3744
3745 wp_cache_set( $cache_key, $dynamic_css, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3746 Cache::set_data_cache_key( $cache_key );
3747
3748 if ( ! empty( $dynamic_css ) ) {
3749 wp_add_inline_style( $style_handle, $dynamic_css );
3750 }
3751 }
3752
3753 /**
3754 * Pro version notice.
3755 *
3756 * @param string $ver Pro Version.
3757 * @param string $tab Tab.
3758 * @param string $name Name.
3759 * @param bool $enable_free_Link Enable free link.
3760 *
3761 * @return array[]
3762 */
3763 public static function pro_version_notice( $ver, $tab = 'billing_fields', $name = 'ShopBuilder', $enable_free_Link = true ) {
3764 return [
3765 'version_check' => [
3766 'id' => 'version_check',
3767 'type' => 'title',
3768 'label' => sprintf(
3769 /* translators: 1: required version, 2: link to the Plugins page */
3770 esc_html__(
3771 'To access all features and settings of this module, please ensure that "%1$s" is updated to version %2$s. %3$s',
3772 'shopbuilder'
3773 ),
3774 $name,
3775 sprintf(
3776 '<br /><u><b>%s</b></u>',
3777 esc_html( $ver ?? '1.5.0' ) . ' or higher'
3778 ),
3779 $enable_free_Link
3780 ? sprintf(
3781 '%s <a class="pro-update-required" href="%s" target="_blank" title="%s">%s</a>',
3782 esc_attr__( 'Update to the latest version', 'shopbuilder' ),
3783 esc_url( admin_url( 'plugins.php' ) ),
3784 esc_attr__( 'Go to the Plugin Page', 'shopbuilder' ),
3785 esc_html__( 'from the Plugins page', 'shopbuilder' )
3786 )
3787 : ''
3788 ),
3789 'tab' => $tab,
3790 'customClass' => 'checkout-notice',
3791 ],
3792 ];
3793 }
3794
3795 /**
3796 * Get product gallery ids.
3797 *
3798 * @param object $product Product object.
3799 *
3800 * @return mixed
3801 */
3802 public static function get_cached_gallery_ids( $product ) {
3803 $product_id = $product->get_id();
3804 $cache_key = 'rtsb_product_gallery_ids_' . $product_id;
3805
3806 if ( isset( self::$cache[ $cache_key ] ) ) {
3807 return self::$cache[ $cache_key ];
3808 }
3809
3810 $cached_result = wp_cache_get( $cache_key, 'shopbuilder' );
3811
3812 if ( false !== $cached_result ) {
3813 self::$cache[ $cache_key ] = $cached_result;
3814
3815 return $cached_result;
3816 }
3817
3818 $gallery_ids = $product->get_gallery_image_ids();
3819 self::$cache[ $cache_key ] = $gallery_ids;
3820
3821 wp_cache_set( $cache_key, $gallery_ids, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3822 Cache::set_data_cache_key( $cache_key );
3823
3824 return $gallery_ids;
3825 }
3826
3827 /**
3828 * Check if optimization setting is turned on in the database.
3829 *
3830 * This only checks the user's setting value, without validating
3831 * whether the cache directory is writable.
3832 *
3833 * @return bool
3834 */
3835 public static function is_optimization_setting_on() {
3836 $has_pro = rtsb()->has_pro();
3837 $pro_ver = defined( 'RTSBPRO_VERSION' ) ? RTSBPRO_VERSION : 0;
3838
3839 if ( $has_pro && version_compare( $pro_ver, '2.0.0', '<' ) ) {
3840 return false;
3841 }
3842
3843 $generalList = GeneralList::instance()->get_data();
3844
3845 return 'on' === ( $generalList['optimization']['enable_optimization'] ?? '' );
3846 }
3847
3848 /**
3849 * Check if optimization is enabled and the cache directory is writable.
3850 *
3851 * @return bool
3852 */
3853 public static function is_optimization_enabled() {
3854 if ( ! self::is_optimization_setting_on() ) {
3855 return false;
3856 }
3857
3858 $upload = wp_upload_dir();
3859 $cache_dir = trailingslashit( $upload['basedir'] ) . 'shopbuilder_uploads/cache/';
3860
3861 if ( ! wp_is_writable( $cache_dir ) ) {
3862 return false;
3863 }
3864
3865 return true;
3866 }
3867
3868 /**
3869 * Get the optimized handle.
3870 *
3871 * @param string $handle Base handle used for script/style IDs.
3872 *
3873 * @return string
3874 */
3875 public static function optimized_handle( $handle ) {
3876 $use_optimization = self::is_optimization_enabled();
3877
3878 if ( $use_optimization ) {
3879 $handle = self::is_contextual_loading() ? self::get_optimized_handle_by_context() : 'rtsb-bundled';
3880 }
3881
3882 return $handle;
3883 }
3884
3885 /**
3886 * Get the optimized handle by context.
3887 *
3888 * @return string
3889 */
3890 public static function get_optimized_handle_by_context() {
3891 $context = self::detect_context();
3892
3893 if ( 'account' === $context ) {
3894 return 'rtsb-bundled-global';
3895 }
3896
3897 return "rtsb-bundled-$context";
3898 }
3899
3900 /**
3901 * Check if Elementor page.
3902 *
3903 * @param int $post_id Post ID.
3904 *
3905 * @return bool
3906 */
3907 public static function is_elementor_page( $post_id = null ) {
3908 if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
3909 return false;
3910 }
3911
3912 $post_id = $post_id ?: get_the_ID();
3913
3914 if ( ! $post_id ) {
3915 return true;
3916 }
3917
3918 return Plugin::$instance->documents->get( $post_id )->is_built_with_elementor();
3919 }
3920
3921 /**
3922 * Check if shop or archive.
3923 *
3924 * @return bool
3925 */
3926 public static function is_shop_or_archive() {
3927 return is_shop() || is_product_category() || is_product_tag() || is_post_type_archive( 'product' ) || BuilderFns::is_archive() || BuilderFns::is_shop() || is_tax( 'product_brand' );
3928 }
3929
3930 /**
3931 * Detect context.
3932 *
3933 * @return string
3934 */
3935 public static function detect_context() {
3936 switch ( true ) {
3937 case is_product() || BuilderFns::is_product():
3938 $context = 'product';
3939 break;
3940
3941 case is_cart() || BuilderFns::is_cart():
3942 $context = 'cart';
3943 break;
3944
3945 case is_checkout() || BuilderFns::is_checkout():
3946 $context = 'checkout';
3947 break;
3948
3949 case self::is_shop_or_archive():
3950 $context = 'shop';
3951 break;
3952
3953 default:
3954 $context = 'global';
3955 break;
3956 }
3957
3958 return apply_filters( 'rtsb/optimizer/context_detect', $context );
3959 }
3960
3961 /**
3962 * Enqueue optimized assets.
3963 *
3964 * @return void
3965 */
3966 public static function enqueue_optimized_assets() {
3967 $asset_registry = AssetRegistry::instance();
3968 $bundled_assets = $asset_registry->get_bundled_assets();
3969 $context = self::detect_context();
3970 $contextual_loading = self::is_contextual_loading();
3971 $theme_handle = self::find_theme_stylesheet_handle();
3972
3973 if ( $contextual_loading ) {
3974 // Enqueue JS.
3975 if ( ! empty( $bundled_assets['js'] ) ) {
3976 $js_context = isset( $bundled_assets['js'][ $context ] ) ? $context : 'global';
3977
3978 if ( $js_context ) {
3979 wp_enqueue_script( $bundled_assets['js'][ $js_context ]['handle'] );
3980 }
3981 }
3982
3983 // Enqueue CSS.
3984 if ( ! empty( $bundled_assets['css'] ) ) {
3985 $css_context = isset( $bundled_assets['css'][ $context ] ) ? $context : 'global';
3986
3987 if ( $css_context ) {
3988 $handle = $bundled_assets['css'][ $css_context ]['handle'];
3989
3990 wp_enqueue_style( $handle );
3991
3992 if ( ! empty( $theme_handle ) ) {
3993 self::set_theme_dependency( $theme_handle, $handle );
3994 }
3995 }
3996 }
3997 } else {
3998 // Enqueue JS.
3999 if ( ! empty( $bundled_assets['js'] ) ) {
4000 wp_enqueue_script( $bundled_assets['js']['handle'] );
4001 }
4002
4003 // Enqueue CSS.
4004 if ( ! empty( $bundled_assets['css'] ) ) {
4005 $handle = $bundled_assets['css']['handle'];
4006
4007 wp_enqueue_style( $handle );
4008
4009 if ( ! empty( $theme_handle ) ) {
4010 self::set_theme_dependency( $theme_handle, $handle );
4011 }
4012 }
4013 }
4014 }
4015
4016 /**
4017 * Find theme stylesheet handle.
4018 *
4019 * @return string|false
4020 */
4021 private static function find_theme_stylesheet_handle() {
4022 static $cached_handle = null;
4023
4024 if ( null !== $cached_handle ) {
4025 return $cached_handle;
4026 }
4027
4028 global $wp_styles;
4029
4030 if ( ! $wp_styles instanceof WP_Styles ) {
4031 return false;
4032 }
4033
4034 $stylesheet_uri = get_stylesheet_uri();
4035 $theme_directory_uri = get_stylesheet_directory_uri();
4036
4037 $hash = md5( $stylesheet_uri );
4038 $transient_key = 'rtsb_theme_css_handle_' . $hash;
4039
4040 $transient = get_transient( $transient_key );
4041
4042 if ( false !== $transient ) {
4043 $cached_handle = $transient;
4044
4045 return $transient;
4046 }
4047
4048 foreach ( $wp_styles->registered as $handle => $style ) {
4049 $src = $style->src;
4050
4051 if (
4052 ( $src === $stylesheet_uri || strpos( $src, $theme_directory_uri ) !== false ) &&
4053 strpos( $src, 'style.css' ) !== false
4054 ) {
4055 set_transient( $transient_key, $handle, DAY_IN_SECONDS );
4056 Cache::set_transient_cache_key( $transient_key );
4057 $cached_handle = $handle;
4058
4059 return $handle;
4060 }
4061 }
4062
4063 $filtered_handle = apply_filters( 'rtsb/optimizer/theme_stylesheet_handle', false );
4064
4065 if ( is_string( $filtered_handle ) && ! empty( $filtered_handle ) ) {
4066 set_transient( $transient_key, $filtered_handle, DAY_IN_SECONDS );
4067 Cache::set_transient_cache_key( $transient_key );
4068 $cached_handle = $filtered_handle;
4069
4070 return $filtered_handle;
4071 }
4072
4073 set_transient( $transient_key, false, DAY_IN_SECONDS );
4074 Cache::set_transient_cache_key( $transient_key );
4075 $cached_handle = false;
4076
4077 return false;
4078 }
4079
4080 /**
4081 * Set theme dependency.
4082 *
4083 * @param string $theme_handle Theme handle.
4084 * @param string $our_handle Our handle.
4085 *
4086 * @return void
4087 */
4088 private static function set_theme_dependency( $theme_handle, $our_handle ) {
4089 global $wp_styles;
4090
4091 if ( ! isset( $wp_styles->registered[ $theme_handle ] ) ) {
4092 return;
4093 }
4094
4095 $theme_style = $wp_styles->registered[ $theme_handle ];
4096
4097 // Add our handle as a dependency if it's not already there.
4098 if ( ! in_array( $our_handle, $theme_style->deps, true ) ) {
4099 $theme_style->deps[] = $our_handle;
4100 }
4101 }
4102
4103 /**
4104 * Check if Elementor scripts should be loaded.
4105 *
4106 * @return bool
4107 */
4108 public static function should_load_elementor_scripts() {
4109 $data = GeneralList::instance()->get_data()['optimization'] ?? [];
4110
4111 $enable_optimization = $data['enable_optimization'] ?? 'on';
4112 $load_elementor_scripts = $data['load_elementor_scripts'] ?? 'on';
4113
4114 if ( 'on' !== $enable_optimization ) {
4115 return true;
4116 }
4117
4118 return 'on' === $load_elementor_scripts;
4119 }
4120
4121 /**
4122 * Locate asset.
4123 *
4124 * @param string $relative_path Relative path.
4125 *
4126 * @return string|null
4127 */
4128 public static function locate_asset( $relative_path ) {
4129 $free_context = rtsb();
4130 $pro_context = function_exists( 'rtsbpro' ) && rtsb()->has_pro() ? rtsbpro() : null;
4131
4132 $paths = [];
4133
4134 if ( $pro_context ) {
4135 $paths[] = $pro_context->get_assets_path( $relative_path );
4136 }
4137
4138 $paths[] = $free_context->get_assets_path( $relative_path );
4139
4140 foreach ( $paths as $path ) {
4141 if ( file_exists( $path ) ) {
4142 return $path;
4143 }
4144 }
4145
4146 return null;
4147 }
4148
4149 /**
4150 * Enqueue module assets.
4151 *
4152 * @param string $handle Asset handle.
4153 * @param string $module_name Module name.
4154 * @param array $options Options array: ['type' => 'css|js|both', 'deps' => [], 'context' => null].
4155 *
4156 * @return string
4157 */
4158 public static function enqueue_module_assets( $handle, $module_name, $options = [] ) {
4159 $use_optimization = self::is_optimization_enabled();
4160 $handle = self::optimized_handle( $handle );
4161
4162 if ( $use_optimization ) {
4163 return $handle;
4164 }
4165
4166 $defaults = [
4167 'type' => 'both',
4168 'deps' => [ 'jquery', 'rtsb-public' ],
4169 'context' => null,
4170 'version' => RTSB_VERSION,
4171 ];
4172
4173 $config = array_merge( $defaults, $options );
4174 $rtl_suffix = is_rtl() ? '-rtl' : '';
4175 $rtl_dir = is_rtl() ? trailingslashit( 'rtl' ) : trailingslashit( 'css' );
4176 $context = $config['context'] ?: rtsb();
4177 $load_css = in_array( $config['type'], [ 'css', 'both' ], true );
4178 $load_js = in_array( $config['type'], [ 'js', 'both' ], true );
4179
4180 // Register CSS if enabled.
4181 if ( $load_css ) {
4182 wp_register_style(
4183 $handle,
4184 $context->get_assets_uri( $rtl_dir . 'modules/' . $module_name . $rtl_suffix . '.css' ),
4185 [],
4186 $config['version']
4187 );
4188 }
4189
4190 // Register JS if enabled.
4191 if ( $load_js ) {
4192 wp_register_script(
4193 $handle,
4194 $context->get_assets_uri( 'js/modules/' . $module_name . '.js' ),
4195 $config['deps'],
4196 $config['version'],
4197 true
4198 );
4199 }
4200
4201 if ( $load_css ) {
4202 wp_enqueue_style( $handle );
4203
4204 $theme_handle = self::find_theme_stylesheet_handle();
4205
4206 if ( ! empty( $theme_handle ) ) {
4207 self::set_theme_dependency( $theme_handle, $handle );
4208 }
4209 }
4210
4211 if ( $load_js ) {
4212 wp_enqueue_script( $handle );
4213 }
4214
4215 return $handle;
4216 }
4217
4218 /**
4219 * Check if contextual loading is enabled.
4220 *
4221 * @return bool
4222 */
4223 public static function is_contextual_loading() {
4224 $data = GeneralList::instance()->get_data()['optimization'] ?? [];
4225
4226 return ! empty( $data['context_asset_loading'] ) && 'on' === $data['context_asset_loading'];
4227 }
4228
4229 /**
4230 * Get Modules list with cache support.
4231 *
4232 * @return array
4233 */
4234 public static function get_modules_list() {
4235 static $cached_modules = null;
4236
4237 if ( null !== $cached_modules ) {
4238 return $cached_modules;
4239 }
4240
4241 $cache_key = 'rtsb_module_list';
4242 $cache_group = 'shopbuilder';
4243
4244 $cached = wp_cache_get( $cache_key, $cache_group );
4245
4246 if ( false !== $cached ) {
4247 $cached_modules = $cached;
4248
4249 return $cached;
4250 }
4251
4252 $modules = ModuleList::instance()->get_data();
4253
4254 wp_cache_set( $cache_key, $modules, $cache_group, 12 * HOUR_IN_SECONDS );
4255 Cache::set_data_cache_key( $cache_key );
4256
4257 $cached_modules = $modules;
4258
4259 return $modules;
4260 }
4261
4262 /**
4263 * Get Elementor widgets list with cache support.
4264 *
4265 * @return array
4266 */
4267 public static function get_widgets_list() {
4268 static $cached_widgets = null;
4269
4270 if ( null !== $cached_widgets ) {
4271 return $cached_widgets;
4272 }
4273
4274 $cache_key = 'rtsb_elementor_widget_list';
4275 $cache_group = 'shopbuilder';
4276
4277 $cached = wp_cache_get( $cache_key, $cache_group );
4278
4279 if ( false !== $cached ) {
4280 $cached_widgets = $cached;
4281
4282 return $cached;
4283 }
4284
4285 $widgets = ElementList::instance()->get_list();
4286
4287 wp_cache_set( $cache_key, $widgets, $cache_group, 12 * HOUR_IN_SECONDS );
4288 Cache::set_data_cache_key( $cache_key );
4289
4290 $cached_widgets = $widgets;
4291
4292 return $widgets;
4293 }
4294
4295 /**
4296 * Convert the given price to the active currency.
4297 *
4298 * @param float $price The original price.
4299 * @param Object||null $product Product.
4300 *
4301 * @return float
4302 */
4303 public static function get_currency_base_price( $price, $product = null ) {
4304 return apply_filters( 'rtsb/convert/currency/price', $price, $product );
4305 }
4306 /**
4307 * Generate a signed URL with a payload.
4308 *
4309 * @param array $payload Associative data to encode.
4310 * @param string $base_url Base URL to append ?key=... (e.g. wc_get_checkout_url()).
4311 * @param string $key Key name to use in the URL.
4312 * @return string Signed URL with key parameter.
4313 */
4314 public static function generate_signed_url( array $payload, string $base_url, $key = 'key' ) {
4315 if ( empty( $key ) ) {
4316 $key = 'key';
4317 }
4318 // Convert payload to JSON.
4319 $data = wp_json_encode( $payload );
4320 // Create signature.
4321 $signature = wp_hash( $data );
4322 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
4323 $encode_key = base64_encode( $data . '::' . $signature );
4324 // Return full URL with key param.
4325 return add_query_arg( [ $key => rawurlencode( $encode_key ) ], $base_url );
4326 }
4327
4328 /**
4329 * Decode and verify a signed URL key.
4330 *
4331 * @param string $key Encoded key from URL.
4332 * @return array|false Decoded payload array on success, false on failure.
4333 */
4334 public static function decode_signed_key( string $key ) {
4335 if ( empty( $key ) ) {
4336 return false;
4337 }
4338 $key = rawurldecode( wp_unslash( $key ) );
4339 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
4340 $raw = base64_decode( sanitize_text_field( $key ) );
4341 if ( ! $raw ) {
4342 return false;
4343 }
4344 $parts = explode( '::', $raw, 2 );
4345 if ( count( $parts ) !== 2 ) {
4346 return false;
4347 }
4348 list( $data_json, $signature ) = $parts;
4349 // Validate signature.
4350 if ( ! hash_equals( wp_hash( $data_json ), $signature ) ) {
4351 return false;
4352 }
4353 $payload = json_decode( $data_json, true );
4354 return is_array( $payload ) ? $payload : false;
4355 }
4356
4357 /**
4358 * Get Loco Translate MO file path for a plugin.
4359 *
4360 * @param string $textdomain Plugin textdomain.
4361 * @return void|false
4362 */
4363 public static function load_loco_textdomain( $textdomain ) {
4364 if ( ! function_exists( 'loco_plugin_version' ) ) {
4365 return false;
4366 }
4367 $lang = WP_LANG_DIR;
4368 $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4369 if ( ! file_exists( $path ) && defined( 'LOCO_LANG_DIR' ) ) {
4370 $lang = LOCO_LANG_DIR;
4371 $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4372 }
4373 if ( ! file_exists( $path ) ) {
4374 if ( 'shopbuilder' === $textdomain ) {
4375 $plugin_root = dirname( RTSB_FILE );
4376 } elseif ( 'shopbuilder-pro' === $textdomain ) {
4377 $plugin_root = dirname( RTSBPRO_FILE );
4378 } else {
4379 return false;
4380 }
4381 $path = $plugin_root . '/languages/' . $textdomain . '-' . get_locale() . '.mo';
4382 }
4383 if ( ! file_exists( $path ) ) {
4384 return false;
4385 }
4386 load_textdomain( $textdomain, $path );
4387 }
4388 }
4389