PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.0
ShopBuilder – WooCommerce Builder For Elementor v3.4.0
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
shopbuilder / app / Helpers / Fns.php

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

4,396 lines 126.9 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 RadiusTheme\SB\Dependencies\CodesVault\Howdyqb\DB;
17 use DateTime;
18 use WP_Styles;
19 use WC_Product;
20 use Elementor\Plugin;
21 use WC_Product_Query;
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 // Memoize per request: this runs once per product widget while Elementor
1363 // builds the editor control config, firing a get_terms() query per
1364 // attribute taxonomy each time. Caching avoids the repeated N queries.
1365 static $cached = null;
1366
1367 if ( null !== $cached ) {
1368 return $cached;
1369 }
1370
1371 $terms = [];
1372 $termList = [];
1373
1374 $attributes = wc_get_attribute_taxonomies();
1375
1376 if ( $attributes ) {
1377 foreach ( $attributes as $tax ) {
1378 if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
1379 $termList[ $tax->attribute_name ] = get_terms( wc_attribute_taxonomy_name( $tax->attribute_name ) );
1380 }
1381 }
1382 }
1383
1384 if ( ! empty( $termList ) && empty( $termList['errors'] ) && is_array( $termList ) ) {
1385 foreach ( $termList as $name => $atts ) {
1386 foreach ( $atts as $term ) {
1387 $terms[ $term->term_id ] = esc_html( ucwords( str_replace( '-', ' ', $name ) ) . ' - ' . $term->name );
1388 }
1389 }
1390 }
1391
1392 $cached = $terms;
1393
1394 return $terms;
1395 }
1396
1397 /**
1398 * Get all attributes name
1399 *
1400 * @return array
1401 */
1402 public static function get_all_attributes_name() {
1403 $attributes_name = [];
1404
1405 $attributes = wc_get_attribute_taxonomies();
1406
1407 if ( $attributes ) {
1408 foreach ( $attributes as $tax ) {
1409 if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) {
1410 $attributes_name[ wc_attribute_taxonomy_name( $tax->attribute_name ) ] = ucwords( $tax->attribute_name );
1411 }
1412 }
1413 }
1414
1415 return $attributes_name;
1416 }
1417
1418 /**
1419 * Get User list
1420 *
1421 * @return array
1422 */
1423 public static function get_users() {
1424 $users = [];
1425 $u = get_users();
1426
1427 if ( ! empty( $u ) ) {
1428 foreach ( $u as $user ) {
1429 $users[ $user->ID ] = $user->display_name;
1430 }
1431 }
1432
1433 return $users;
1434 }
1435
1436 /**
1437 * Get Taxonomy List.
1438 *
1439 * @return array
1440 */
1441 public static function get_tax_list() {
1442 return apply_filters(
1443 'rtsb/elements/tax_list',
1444 [
1445 'product_cat' => esc_html__( 'Product Category', 'shopbuilder' ),
1446 ]
1447 );
1448 }
1449
1450 /**
1451 * Get Category Query List.
1452 *
1453 * @return array
1454 */
1455 public static function get_cat_list() {
1456 return [
1457 'all' => esc_html__( 'All Categories', 'shopbuilder' ),
1458 'specific_parent' => esc_html__( 'Sub-Categories by Parent', 'shopbuilder' ),
1459 'cat_ids' => esc_html__( 'Select by ID', 'shopbuilder' ),
1460 'selection' => esc_html__( 'Manual Selection', 'shopbuilder' ),
1461 ];
1462 }
1463
1464 /**
1465 * Get Term List.
1466 *
1467 * @param string $taxonomy Taxonomy.
1468 * @param bool $first_term First term.
1469 * @param bool $only_parents Only parent terms.
1470 * @param bool $return_slug Return with slug.
1471 *
1472 * @return int|array
1473 */
1474 public static function get_terms( $taxonomy, $first_term = false, $only_parents = false, $return_slug = false ) {
1475 if ( ! is_admin() ) {
1476 return [];
1477 }
1478
1479 $term_list = [];
1480 $args = [
1481 'taxonomy' => $taxonomy,
1482 'hide_empty' => false,
1483 ];
1484
1485 if ( $only_parents ) {
1486 $args['parent'] = 0;
1487 }
1488
1489 $terms = get_terms( $args );
1490
1491 if ( empty( $terms ) ) {
1492 return [ esc_html__( 'Nothing found', 'shopbuilder' ) ];
1493 }
1494
1495 foreach ( $terms as $term ) {
1496 if ( $return_slug ) {
1497 $term_list[ $term->slug ] = $term->name;
1498 } else {
1499 $term_list[ $term->term_id ] = $term->name;
1500 }
1501 }
1502
1503 if ( $first_term ) {
1504 return array_keys( $term_list )[0];
1505 } else {
1506 return $term_list;
1507 }
1508 }
1509
1510 /**
1511 * Get product rating.
1512 *
1513 * @param array $args Arguments.
1514 *
1515 * @return string|void
1516 */
1517 public static function get_product_rating_html( $args = [] ) {
1518 global $product;
1519
1520 $html = '';
1521 $rating_count = $product->get_rating_count();
1522 $average_rating = $product->get_average_rating();
1523
1524 if ( ! rtsb()->has_pro() || empty( $args ) ) {
1525 if ( ! $rating_count || empty( wc_get_rating_html( $average_rating, $rating_count ) ) ) {
1526 return $html;
1527 }
1528
1529 $html .= '<div class="product-rating">';
1530 $html .= wc_get_rating_html( $average_rating, $rating_count );
1531 $html .= ! empty( $html ) ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1532 $html .= '</div>';
1533
1534 self::print_html( $html, true );
1535
1536 return;
1537 }
1538
1539 if ( empty( $rating_count ) && ! $args['show_empty_rating'] ) {
1540 return '';
1541 }
1542
1543 $preset = ! empty( $args['preset'] ) ? $args['preset'] : 'preset1';
1544 $average = $args['show_average_rating'] ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1545 $count = '';
1546
1547 if ( $args['show_rating_count'] ) {
1548 $count .= '<div class="rtsb-count">';
1549 $count .= sprintf(
1550 /* translators: %s is the number of reviews */
1551 _n( '%s Review', '%s Reviews', $rating_count, 'shopbuilder' ),
1552 $rating_count
1553 );
1554 $count .= '</div>';
1555 }
1556
1557 if ( 'preset2' === $preset ) {
1558 $average = $args['show_average_rating'] ? '<div class="inner-wrapper"><span class="star-icon"></span><span class="average-rating">' . $average_rating . '</span></div>' : '';
1559 }
1560
1561 $html .= '<div class="product-rating ' . esc_attr( $preset ) . '">';
1562
1563 if ( 'preset1' === $preset ) {
1564 $html .= wc_get_rating_html( $average_rating, $rating_count );
1565 $html .= $average;
1566 $html .= $count;
1567 } elseif ( 'preset2' === $preset ) {
1568 $html .= $average;
1569 $html .= $count;
1570 }
1571
1572 $html .= '</div>';
1573
1574 self::print_html( $html, true );
1575 }
1576
1577 /**
1578 * Get product simple rating.
1579 *
1580 * @return void
1581 */
1582 public static function get_product_simple_rating_html() {
1583 global $product;
1584 $html = null;
1585 $rating_count = $product->get_rating_count();
1586 $average_rating = $product->get_average_rating();
1587 $html .= '<div class="product-rating">';
1588 $html .= wc_get_rating_html( $average_rating, $rating_count );
1589 $html .= ! empty( $html ) ? '<span class="rtsb-count">(' . $average_rating . ')</span>' : '';
1590 $html .= '</div>';
1591
1592 self::print_html( $html, true );
1593 }
1594
1595
1596
1597 /**
1598 * Text truncation.
1599 *
1600 * @param string $text_to_truncate Text.
1601 * @param int $limit Limit.
1602 * @param string $after After text.
1603 *
1604 * @return string
1605 */
1606 public static function text_truncation( $text_to_truncate, $limit, $after = '&#8230;' ) {
1607 if ( empty( $limit ) ) {
1608 return $text_to_truncate;
1609 }
1610
1611 $limit++;
1612
1613 $text = '';
1614
1615 if ( mb_strlen( $text_to_truncate ) > $limit ) {
1616 $subex = mb_substr( wp_strip_all_tags( $text_to_truncate ), 0, $limit );
1617 $exwords = explode( ' ', $subex );
1618 $excut = - ( mb_strlen( $exwords[ count( $exwords ) - 1 ] ) );
1619
1620 if ( $excut < 0 ) {
1621 $text .= mb_substr( $subex, 0, $excut ) . $after;
1622 } else {
1623 $text .= $subex . $after;
1624 }
1625 } else {
1626 $text .= $text_to_truncate;
1627 }
1628
1629 return $text;
1630 }
1631
1632 /**
1633 * Promo Badge HTML
1634 *
1635 * @param string $text Text.
1636 * @param string $class Class.
1637 *
1638 * @return void
1639 */
1640 public static function get_badge_html( $text, $class = 'fill' ) {
1641 if ( self::is_module_active( 'product_badges' ) && 'rtsb_yes' === $text ) {
1642 do_action( 'rtsb/modules/product_badges/frontend/display' );
1643
1644 return;
1645 }
1646
1647 if ( empty( $text ) ) {
1648 return;
1649 }
1650 ob_start();
1651 ?>
1652
1653 <ul class="rtsb-promotion-list">
1654 <li class="rtsb-promotion-list-item">
1655 <span class="rtsb-tag-<?php echo ! empty( $class ) ? esc_attr( $class ) : ''; ?>"><?php echo esc_html( $text ); ?></span>
1656 </li>
1657 </ul>
1658
1659 <?php
1660 self::print_html( ob_get_clean() );
1661 }
1662
1663 /**
1664 * Categories HTML
1665 *
1666 * @param int $id Product ID.
1667 * @param string $class Custom class.
1668 *
1669 * @return void|string
1670 */
1671 public static function get_categories_list( $id, $class = 'rtsb-category-outline' ) {
1672 if ( empty( $id ) ) {
1673 return '';
1674 }
1675
1676 ob_start();
1677 ?>
1678
1679 <ul class="rtsb-category-list <?php echo esc_attr( $class ); ?>">
1680 <?php
1681 self::print_html(
1682 wc_get_product_category_list(
1683 $id,
1684 '</li><li class="rtsb-category-list-item">',
1685 '<li class="rtsb-category-list-item">',
1686 '</li>'
1687 )
1688 );
1689 ?>
1690 </ul>
1691
1692 <?php
1693 self::print_html( ob_get_clean() );
1694 }
1695 /**
1696 * Brands HTML
1697 *
1698 * @param int $id Product ID.
1699 * @param string $class Custom class.
1700 *
1701 * @return void|string
1702 */
1703 public static function get_brands_list( $id, $class = 'rtsb-brand-outline' ) {
1704 $terms = get_the_terms( $id, 'product_brand' );
1705 if ( empty( $id ) || empty( $terms ) || is_wp_error( $terms ) ) {
1706 return '';
1707 }
1708 ob_start();
1709 ?>
1710
1711 <ul class="rtsb-brand-list <?php echo esc_attr( $class ); ?>">
1712 <?php
1713 $brand_list = get_the_term_list(
1714 $id,
1715 'product_brand',
1716 '<li class="rtsb-brand-list-item">',
1717 '</li><li class="rtsb-brand-list-item">',
1718 '</li>'
1719 );
1720
1721 self::print_html( $brand_list );
1722 ?>
1723 </ul>
1724
1725 <?php
1726 self::print_html( ob_get_clean() );
1727 }
1728
1729 /**
1730 * Get Featured Image HTML.
1731 *
1732 * @param string $type Image type.
1733 * @param int $post_id Post ID.
1734 * @param string $f_img_size Image size.
1735 * @param null $default_img_id Default image ID.
1736 * @param array $custom_img_size Custom image size.
1737 * @param bool $lazy Lazy load check.
1738 * @param bool $hover Hover image.
1739 * @param bool $gallery Gallery image.
1740 * @param bool $custom_image_id Custom category image ID.
1741 *
1742 * @return string|null
1743 */
1744 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 ) {
1745 $img_html = null;
1746 $attachment_id = null;
1747 $c_size = false;
1748 $hover_class = '';
1749 $post_title = '';
1750
1751 if ( 'rtsb_custom' === $f_img_size ) {
1752 $f_img_size = 'full';
1753 $c_size = true;
1754 }
1755
1756 if ( ! rtsb()->has_pro() ) {
1757 $gallery = false;
1758 }
1759
1760 if ( $hover ) {
1761 $a_id = $post_id;
1762 $hover_class = ' rtsb-img-hover';
1763 } elseif ( $gallery ) {
1764 $a_id = $post_id;
1765 } else {
1766 if ( 'product' === $type ) {
1767 $a_id = get_post_thumbnail_id( $post_id );
1768 $post_title = get_the_title( $post_id );
1769 } elseif ( 'category' === $type ) {
1770 $a_id = $custom_image_id ? $custom_image_id : get_term_meta( $post_id, 'thumbnail_id', true );
1771 $post_title = get_term( $post_id )->name;
1772 } else {
1773 $a_id = $default_img_id;
1774 }
1775 }
1776
1777 $img_alt = trim( wp_strip_all_tags( get_post_meta( $a_id, '_wp_attachment_image_alt', true ) ) );
1778 $alt_tag = ! empty( $img_alt ) ? $img_alt : wp_strip_all_tags( $post_title );
1779 $lazy_class = $lazy ? ' swiper-lazy' : '';
1780 $attr = [
1781 'class' => 'img-responsive rtsb-product-image' . $lazy_class . $hover_class,
1782 'alt' => $alt_tag,
1783 ];
1784
1785 if ( $a_id ) {
1786 $img_html = wp_get_attachment_image( $a_id, $f_img_size, false, $attr );
1787 $attachment_id = $a_id;
1788 }
1789
1790 if ( ! $img_html && $default_img_id ) {
1791 $img_html = wp_get_attachment_image( $default_img_id, $f_img_size, false, $attr );
1792 $attachment_id = $default_img_id;
1793 }
1794
1795 if ( $img_html && $c_size ) {
1796 preg_match( '@src="([^"]+)"@', $img_html, $match );
1797 $img_src = array_pop( $match );
1798 $w = ! empty( $custom_img_size['width'] ) ? absint( $custom_img_size['width'] ) : null;
1799 $h = ! empty( $custom_img_size['height'] ) ? absint( $custom_img_size['height'] ) : null;
1800 $c = ! empty( $custom_img_size['crop'] ) && 'soft' === $custom_img_size['crop'] ? false : true;
1801
1802 if ( $w && $h ) {
1803 $image = self::image_resize( $img_src, $w, $h, $c, false );
1804
1805 if ( ! empty( $image ) ) {
1806 [ $src, $width, $height ] = $image;
1807
1808 $hwstring = image_hwstring( $width, $height );
1809 $attachment = get_post( $attachment_id );
1810 $attr = apply_filters( 'wp_get_attachment_image_attributes', $attr, $attachment, $f_img_size );
1811
1812 if ( $lazy ) {
1813 $attr['data-src'] = $src;
1814 } else {
1815 $attr['src'] = $src;
1816 }
1817
1818 $attr = array_map( 'esc_attr', $attr );
1819 $img_html = rtrim( "<img $hwstring" );
1820
1821 foreach ( $attr as $name => $value ) {
1822 $img_html .= " $name=" . '"' . $value . '"';
1823 }
1824
1825 $img_html .= ' />';
1826 }
1827 }
1828 }
1829
1830 if ( ! $img_html ) {
1831 $hwstring = image_hwstring( 160, 160 );
1832 $attr = isset( $attr['src'] ) ? apply_filters( 'wp_get_attachment_image_attributes', $attr, false, $f_img_size ) : [];
1833 $attr['class'] = 'default-img ' . $hover_class;
1834 $attr['src'] = esc_url( rtsb()->get_assets_uri( 'images/demo.png' ) );
1835 $attr['alt'] = esc_html__( 'Default Image', 'shopbuilder' );
1836 $img_html = rtrim( "<img $hwstring" );
1837
1838 foreach ( $attr as $name => $value ) {
1839 $img_html .= " $name=" . '"' . $value . '"';
1840 }
1841
1842 $img_html .= ' />';
1843 }
1844
1845 if ( $lazy ) {
1846 $img_html = $img_html . '<div class="swiper-lazy-preloader swiper-lazy-preloader"></div>';
1847 }
1848
1849 return $img_html;
1850 }
1851
1852 /**
1853 * Call the Image resize model for resize function
1854 *
1855 * @param string $url URL.
1856 * @param int $width Width.
1857 * @param int $height Height.
1858 * @param string $crop Crop.
1859 * @param bool|true $single Single.
1860 * @param bool|false $upscale Upscale.
1861 *
1862 * @return array|bool|string
1863 */
1864 public static function image_resize( $url, $width = null, $height = null, $crop = null, $single = true, $upscale = false ) {
1865 $rtResize = new ReSizer();
1866
1867 return $rtResize->process( $url, $width, $height, $crop, $single, $upscale );
1868 }
1869
1870 /**
1871 * Get Product Image
1872 *
1873 * @param string $f_image Featured Image.
1874 * @param string $h_image Hover Image.
1875 *
1876 * @return void
1877 */
1878 public static function get_product_image( $f_image, $h_image = null ) {
1879 if ( empty( $f_image ) ) {
1880 return;
1881 }
1882
1883 echo wp_kses( $f_image, self::allowedHtml( 'image' ) );
1884
1885 if ( ! empty( $h_image ) ) {
1886 echo wp_kses( $h_image, self::allowedHtml( 'image' ) );
1887 }
1888 }
1889
1890 /**
1891 * Post Custom Field value.
1892 *
1893 * @param int $post_id Post id.
1894 * @param string $field_key Custom Field Key.
1895 * @param string $field_fallback FallBack text.
1896 *
1897 * @return string|void
1898 */
1899 public static function get_post_custom_field_value( $post_id, $field_key = '', $field_fallback = '' ) {
1900 if ( ! $post_id || ! $field_key ) {
1901 return;
1902 }
1903 $field_value = get_post_meta( $post_id, $field_key, true );
1904 if ( empty( $field_value ) ) {
1905 $field_value = ! empty( $field_fallback ) ? $field_fallback : $field_value;
1906 }
1907 $field_value = apply_filters( 'rtsb/get_post_custom_field_value/' . $field_key, $field_value, $field_key );
1908
1909 return sprintf( '<span class="rtsb-woo-custom-field">%s</span>', $field_value );
1910 }
1911
1912 /**
1913 * Elementor Icon
1914 *
1915 * @param array $control Elementor Control array.
1916 * @param string $class Custom class.
1917 * @param string $builder Builder name.
1918 *
1919 * @return string
1920 */
1921 public static function icons_manager( $control, $class = '', $builder = 'elementor' ): string {
1922 if ( empty( $control['value'] ) ) {
1923 return '';
1924 }
1925 if ( is_array( $control['value'] ) ) {
1926 $cache_key = 'icons_managet_' . str_replace( ' ', '', md5( serialize( $control['value'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1927 } else {
1928 $cache_key = 'icons_managet_' . str_replace( ' ', '', $control['value'] );
1929 }
1930 if ( isset( self::$cache[ $cache_key ] ) ) {
1931 return self::$cache[ $cache_key ];
1932 }
1933
1934 $attributes = [
1935 'aria-hidden' => 'true',
1936 ];
1937
1938 if ( ! empty( $class ) ) {
1939 $attributes['class'] = esc_attr( $class );
1940 }
1941
1942 ob_start();
1943
1944 if ( defined( 'ELEMENTOR_VERSION' ) && ! empty( $control ) && 'elementor' === $builder ) {
1945 Icons_Manager::render_icon( $control, $attributes );
1946 }
1947
1948 $icons = ob_get_clean();
1949 self::$cache[ $cache_key ] = $icons;
1950 return $icons;
1951 }
1952
1953 /**
1954 * Get product swatches.
1955 *
1956 * @param string $type Swatch type.
1957 *
1958 * @return void
1959 */
1960 public static function get_product_swatches( $type ) {
1961 ?>
1962 <div class="rtsb-swatches <?php echo esc_attr( $type ); ?>-layout">
1963 <?php
1964 if ( class_exists( 'Rtwpvsp' ) ) {
1965 do_action( 'rtwpvs_show_archive_variation' );
1966 } else {
1967 do_action( 'rtsb/vs/showcase/variation' );
1968 }
1969 ?>
1970 </div>
1971 <?php
1972 }
1973
1974 /**
1975 * Get sale badge
1976 *
1977 * @param string $type Badge type.
1978 * @param string $text Badge text.
1979 * @param string $out_of_stock_text Out of stock text.
1980 *
1981 * @return string
1982 */
1983 public static function get_sale_badge( $type, $text, $out_of_stock_text ) {
1984 global $product;
1985
1986 if ( ! $product->is_in_stock() ) {
1987 return $out_of_stock_text;
1988 }
1989
1990 if ( ! $product->is_on_sale() ) {
1991 return '';
1992 }
1993
1994 if ( 'text' === $type ) {
1995 return $text;
1996 }
1997
1998 $percentage = self::calculate_sale_percentage( $product );
1999
2000 // The product is on sale (checked above). When a percentage can be derived show
2001 // it; otherwise (e.g. some variable products where variation prices aren't
2002 // primed) fall back to the sale text so an on-sale product always shows a badge.
2003 return $percentage > 0 ? '-' . round( $percentage ) . '%' : $text;
2004 }
2005
2006 /**
2007 * Get sale badge
2008 *
2009 * @param object $product Product Object.
2010 * @param string $type Badge type.
2011 * @param string $text Badge text.
2012 * @param string $out_of_stock_text Out of stock text.
2013 *
2014 * @return string
2015 */
2016 public static function get_promo_badge( $product, $type, $text, $out_of_stock_text ) {
2017 $disable_badges = self::get_option( 'general', 'guest_user', 'hide_badges', '' );
2018 $badges_visibility = rtsb()->has_pro() && ! is_user_logged_in() && $disable_badges;
2019
2020 if ( $badges_visibility ) {
2021 return '';
2022 }
2023
2024 if ( is_null( $product ) ) {
2025 global $product;
2026 }
2027
2028 if ( ! $product instanceof WC_Product ) {
2029 return '';
2030 }
2031
2032 if ( ! $product->is_in_stock() ) {
2033 return $out_of_stock_text;
2034 }
2035
2036 if ( ! $product->is_on_sale() ) {
2037 return '';
2038 }
2039
2040 if ( 'text' === $type ) {
2041 return $text;
2042 }
2043
2044 $percentage = self::calculate_sale_percentage( $product );
2045
2046 // The product is on sale (checked above). When a percentage can be derived show
2047 // it; otherwise (e.g. some variable products where variation prices aren't
2048 // primed) fall back to the sale text so an on-sale product always shows a badge.
2049 return $percentage > 0 ? '-' . round( $percentage ) . '%' : $text;
2050 }
2051
2052 /**
2053 * Calculate Sale Percentage
2054 *
2055 * @param object $product Product object.
2056 *
2057 * @return float|int|string
2058 */
2059 public static function calculate_sale_percentage( $product = null ) {
2060 if ( is_null( $product ) ) {
2061 global $product;
2062 }
2063 if ( ! $product instanceof WC_Product ) {
2064 return '';
2065 }
2066 if ( ! $product->is_on_sale() ) {
2067 return '';
2068 }
2069 $max_percentage = 0;
2070 if ( $product->is_type( 'simple' ) ) {
2071 $regular_price = (float) $product->get_regular_price();
2072 $sale_price = (float) $product->get_sale_price();
2073 if ( $regular_price > 0 && $sale_price > 0 ) {
2074 $max_percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2075 }
2076 } elseif ( $product->is_type( 'variable' ) ) {
2077 $prices = $product->get_variation_prices( true );
2078 if ( ! empty( $prices['regular_price'] ) && ! empty( $prices['sale_price'] ) ) {
2079 foreach ( $prices['regular_price'] as $key => $regular_price ) {
2080 $sale_price = $prices['sale_price'][ $key ];
2081
2082 if ( $regular_price > 0 && $sale_price > 0 && $regular_price > $sale_price ) {
2083 $percentage = ( ( $regular_price - $sale_price ) / $regular_price ) * 100;
2084 $max_percentage = max( $max_percentage, $percentage );
2085 }
2086 }
2087 }
2088 }
2089 return $max_percentage > 0 ? round( $max_percentage ) : 0;
2090 }
2091
2092 /**
2093 * Pagination
2094 *
2095 * @param string $pages Pages.
2096 * @param integer $range Range.
2097 * @param boolean $ajax Ajax.
2098 *
2099 * @return string
2100 */
2101 public static function custom_pagination( $pages = '', $range = 4, $ajax = false ) {
2102 $html = null;
2103 $visible_range = apply_filters( 'rtsb/elements/pagination/visible_range', ( $range * 2 ) + 1 );
2104
2105 global $paged;
2106
2107 // Prefer `paged` (archive pagination), fall back to `page` (paginated singular posts).
2108 // This handles the case where the shop/archive is set as the front page — `is_front_page()`
2109 // alone would force `page` and miss the archive `paged` value.
2110 $paged = get_query_var( 'paged' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2111 if ( ! $paged ) {
2112 $paged = get_query_var( 'page' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2113 }
2114 if ( ! $paged ) {
2115 $paged = 1; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2116 }
2117
2118 if ( '' === $pages ) {
2119 global $wp_query;
2120
2121 $pages = $wp_query->max_num_pages;
2122
2123 if ( ! $pages ) {
2124 $pages = 1;
2125 }
2126 }
2127
2128 $ajaxClass = null;
2129 $dataAttr = null;
2130
2131 if ( $ajax ) {
2132 $ajaxClass = ' rtsb-pagination-ajax';
2133 $dataAttr = "data-paged='1'";
2134 $dataAttr .= ' data-visible-range="' . esc_attr( $visible_range ) . '"';
2135 }
2136
2137 if ( 1 !== $pages ) {
2138 $html .= '<div class="rtsb-pagination' . $ajaxClass . '" ' . $dataAttr . '>';
2139 $html .= '<ul class="pagination-list">';
2140
2141 if ( $paged > 2 && $paged > $visible_range + 1 && $visible_range < $pages ) {
2142 $html .= "<li><a data-paged='1' href='" . get_pagenum_link( 1 ) . "' aria-label='First'>&laquo;</a></li>";
2143 }
2144
2145 if ( $paged > 1 && $visible_range < $pages ) {
2146 $p = $paged - 1;
2147 $html .= "<li><a data-paged='{$p}' href='" . get_pagenum_link( $p ) . "' aria-label='Previous'>&lsaquo;</a></li>";
2148 }
2149
2150 for ( $i = 1; $i <= $pages; $i++ ) {
2151 if ( 1 != $pages && ( ! ( $i >= $paged + $visible_range + 1 || $i <= $paged - $visible_range - 1 ) || $pages <= $visible_range ) ) {
2152 $html .= ( $paged == $i ) ? '<li class="active"><span>' . $i . '</span></li>' : "<li><a data-paged='{$i}' href='" . get_pagenum_link( $i ) . "'>" . $i . '</a></li>';
2153 }
2154 }
2155
2156 if ( $paged < $pages && $visible_range < $pages ) {
2157 $p = $paged + 1;
2158 $html .= "<li><a data-paged='{$p}' href=\"" . get_pagenum_link( $paged + 1 ) . "\" aria-label='Next'>&rsaquo;</a></li>";
2159 }
2160
2161 if ( $paged < $pages - 1 && $paged + $range - 1 < $pages && $visible_range < $pages ) {
2162 $html .= "<li><a data-paged='{$pages}' href='" . get_pagenum_link( $pages ) . "' aria-label='Last'>&raquo;</a></li>";
2163 }
2164
2165 $html .= '</ul>';
2166 $html .= '</div>';
2167 }
2168
2169 return $html;
2170 }
2171
2172 /**
2173 * Action Buttons HTMl
2174 *
2175 * @param array $items Items.
2176 * @param array $ajax_cart Ajax cart HTML.
2177 * @param string $preset Button preset.
2178 * @param array $position Position.
2179 * @param string $placement Placement.
2180 *
2181 * @return void|string
2182 */
2183 public static function get_formatted_action_buttons( $items, $ajax_cart = '', $preset = 'preset1', $position = 'above', $placement = 'top' ) {
2184 if ( empty( $items ) ) {
2185 return;
2186 }
2187
2188 $html = '';
2189 $class = '';
2190 $args = [ $items, $ajax_cart, $preset, $position, $placement ];
2191
2192 if ( ( 'top' === $placement && 'after' === $position ) || ( 'bottom' === $placement && 'above' === $position ) ) {
2193 return apply_filters( 'rtsb/elements/elementor/formatted_action_buttons', $html, $args );
2194 }
2195
2196 if ( 'preset2' === $preset ) {
2197 $class = 'rtsb-action-buttons-vertical vertical-delay-effect ' . $preset;
2198 } elseif ( 'preset4' === $preset ) {
2199 $class = 'rtsb-action-buttons-vertical rtsb-action-buttons-vertical-left vertical-delay-effect ' . $preset;
2200 } elseif ( 'preset1' === $preset || 'preset3' === $preset ) {
2201 $class = 'rtsb-action-buttons-cart-box-width-auto horizontal-floating-btn ' . $preset;
2202 }
2203
2204 if ( 'after' === $position && 'preset1' === $preset ) {
2205 $class .= ' after-content';
2206 }
2207
2208 if ( 'preset3' === $preset ) {
2209 $html .= '<div class="rtsb-action-buttons top-part ' . esc_attr( $preset ) . '">';
2210 $html .= '<ul class="rtsb-action-button-list">';
2211
2212 ob_start();
2213 /**
2214 * Additional formatted action buttons hook.
2215 */
2216 do_action( 'rtsb/elements/elementor/additional_action_buttons', $items );
2217 $html .= ob_get_clean();
2218
2219 $html .= self::get_action_button_by_type( $items, 'wishlist' );
2220 $html .= self::get_action_button_by_type( $items, 'compare' );
2221 $html .= self::get_action_button_by_type( $items, 'quick_view' );
2222 $html .= '</ul>';
2223 $html .= '</div>';
2224 $html .= '<div class="rtsb-action-buttons bottom-part ' . esc_attr( $preset ) . '">';
2225 $html .= '<ul class="rtsb-action-button-list">';
2226 $html .= self::get_action_button_by_type( $items, 'add_to_cart', $ajax_cart );
2227 $html .= '</ul>';
2228 $html .= '</div>';
2229 } elseif ( 'preset1' === $preset || 'preset2' === $preset || 'preset4' === $preset ) {
2230 $html .= '<div class="rtsb-action-buttons ' . esc_attr( $class ) . '">';
2231 $html .= '<ul class="rtsb-action-button-list">';
2232 $html .= self::get_action_button_by_type( $items, 'add_to_cart', $ajax_cart );
2233
2234 ob_start();
2235 /**
2236 * Additional formatted action buttons hook.
2237 */
2238 do_action( 'rtsb/elements/elementor/additional_action_buttons', $items );
2239 $html .= ob_get_clean();
2240
2241 $html .= self::get_action_button_by_type( $items, 'wishlist' );
2242 $html .= self::get_action_button_by_type( $items, 'compare' );
2243 $html .= self::get_action_button_by_type( $items, 'quick_view' );
2244 $html .= '</ul>';
2245 $html .= '</div>';
2246 }
2247
2248 return apply_filters( 'rtsb/elements/elementor/formatted_action_buttons', $html, $args );
2249 }
2250
2251 /**
2252 * Get Action Button HTML
2253 *
2254 * @param array $items Items.
2255 * @param string $type Button type.
2256 * @param string $cart_html Ajax cart HTML.
2257 * @param string $wrapper Wrapper tag.
2258 *
2259 * @return void|string
2260 */
2261 public static function get_action_button_by_type( $items, $type, $cart_html = '', $wrapper = 'li' ) {
2262 if ( ! in_array( $type, $items, true ) ) {
2263 return;
2264 }
2265
2266 $html = '';
2267 $class = 'rtsb-action-button-item';
2268
2269 if ( 'add_to_cart' === $type ) {
2270 $class .= ' rtsb-cart' . ( empty( $cart_html ) ? esc_attr( ' no-cart-button' ) : '' );
2271 } else {
2272 $class .= ' rtsb-' . esc_attr( str_replace( '_', '-', $type ) );
2273 }
2274
2275 if ( ( 'add_to_cart' === $type ) && ( ! empty( $cart_html ) ) ) {
2276 $html .= $cart_html;
2277 } else {
2278 $html .= shortcode_exists( 'rtsb_' . $type . '_button' ) ? do_shortcode( '[rtsb_' . $type . '_button]' ) : null;
2279 }
2280
2281 if ( ! empty( $html ) ) {
2282 $html = '<' . esc_attr( $wrapper ) . ' class="' . esc_attr( $class ) . '">' . $html . '</' . esc_attr( $wrapper ) . '>';
2283 }
2284
2285 return apply_filters( 'rtsb/elements/elementor/get_action_button_by_type', $html );
2286 }
2287
2288 /**
2289 * Social Share Platforms.
2290 *
2291 * @return mixed|null
2292 */
2293 public static function social_share_platforms_list() {
2294 return apply_filters(
2295 'rtsb/settings/social_share/platforms',
2296 [
2297 [
2298 'value' => 'facebook',
2299 'label' => esc_html__( 'Facebook', 'shopbuilder' ),
2300 ],
2301 [
2302 'value' => 'twitter',
2303 'label' => esc_html__( 'Twitter', 'shopbuilder' ),
2304 ],
2305 [
2306 'value' => 'linkedin',
2307 'label' => esc_html__( 'Linkedin', 'shopbuilder' ),
2308 ],
2309 [
2310 'value' => 'pinterest',
2311 'label' => esc_html__( 'Pinterest', 'shopbuilder' ),
2312 ],
2313 [
2314 'value' => 'skype',
2315 'label' => esc_html__( 'Skype', 'shopbuilder' ),
2316 ],
2317 [
2318 'value' => 'whatsapp',
2319 'label' => esc_html__( 'Whatsapp', 'shopbuilder' ),
2320 ],
2321 [
2322 'value' => 'reddit',
2323 'label' => esc_html__( 'Reddit', 'shopbuilder' ),
2324 ],
2325 [
2326 'value' => 'telegram',
2327 'label' => esc_html__( 'Telegram', 'shopbuilder' ),
2328 ],
2329 ]
2330 );
2331 }
2332
2333 /**
2334 * Get Social Share link HTML.
2335 *
2336 * @param int $id Post ID.
2337 * @param array $types Preset type.
2338 * @param string $preset Style type.
2339 * @param boolean $show_icon Show icon.
2340 * @param boolean $show_text Show text.
2341 *
2342 * @return string
2343 */
2344 public static function get_social_share_html( int $id, array $types, string $preset = 'default', $show_icon = true, $show_text = true ) {
2345 $attr = [ 'postid' => $id ];
2346 $output = '';
2347
2348 if ( empty( $types ) ) {
2349 return $output;
2350 }
2351
2352 foreach ( $types as $type ) {
2353 $link = [];
2354 $link['type'] = $type['share_items'];
2355 $link['class'] = '';
2356 $link['img'] = apply_filters( 'rtsb/elements/share/default_img', '', $id, $link );
2357
2358 if ( 'site' === $id ) {
2359 $link['url'] = home_url();
2360 $link['title'] = wp_strip_all_tags( get_bloginfo( 'name' ) );
2361 } elseif ( 0 === strpos( $id, 'http' ) ) {
2362 $link['url'] = $id;
2363 $link['title'] = '';
2364 } else {
2365 $link['url'] = get_permalink( $id );
2366 $link['title'] = wp_strip_all_tags( get_the_title( $id ) );
2367
2368 if ( has_post_thumbnail( $id ) ) {
2369 $link['img'] = wp_get_attachment_image_url( get_post_thumbnail_id( $id ), 'full' );
2370 }
2371
2372 $link['img'] = apply_filters( 'rtsb/elements/share/single_img', $link['img'], $id, $link );
2373 }
2374
2375 $link['url'] = apply_filters( 'rtsb/elements/share/url', $link['url'], $link );
2376
2377 switch ( $type['share_items'] ) {
2378 case 'facebook':
2379 $link['link'] = esc_url( 'https://www.facebook.com/sharer/sharer.php?u=' . $link['url'] . '&display=popup&ref=plugin&src=share_button' );
2380 $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>';
2381 $link['attr_title'] = esc_html__( 'Share on Facebook', 'shopbuilder' );
2382 $link['social_network'] = 'Facebook';
2383 $link['social_action'] = 'Share';
2384 break;
2385 case 'twitter':
2386 $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'] );
2387 $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>';
2388 $link['attr_title'] = esc_html__( 'Share on Twitter', 'shopbuilder' );
2389 $link['social_network'] = 'Twitter';
2390 $link['social_action'] = 'Tweet';
2391 break;
2392 case 'pinterest':
2393 $link['link'] = esc_url( 'https://pinterest.com/pin/create/button/?url=' . $link['url'] . '&media=' . $link['img'] . '&description=' . $link['title'] );
2394 $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>';
2395 $link['attr_title'] = esc_html__( 'Share on Pinterest', 'shopbuilder' );
2396 $link['social_network'] = 'Pinterest';
2397 $link['social_action'] = 'Pin';
2398 break;
2399 case 'linkedin':
2400 $link['link'] = esc_url( 'https://www.linkedin.com/shareArticle?url=' . $link['url'] . '&title=' . $link['title'] );
2401 $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>';
2402 $link['attr_title'] = esc_html__( 'Share on LinkedIn', 'shopbuilder' );
2403 $link['social_network'] = 'LinkedIn';
2404 $link['social_action'] = 'Share';
2405 break;
2406 case 'skype':
2407 $link['link'] = esc_url( 'https://web.skype.com/share?url=' . $link['url'] );
2408 $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>';
2409 $link['attr_title'] = esc_html__( 'Share on Skype', 'shopbuilder' );
2410 $link['social_network'] = 'Skype';
2411 $link['social_action'] = 'Skype';
2412 break;
2413 case 'whatsapp':
2414 $link['link'] = esc_url( 'https://wa.me/?text=' . $link['title'] . ' ' . $link['url'] );
2415 $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>';
2416 $link['attr_title'] = esc_html__( 'Share on Whatsapp', 'shopbuilder' );
2417 $link['social_network'] = 'Whatsapp';
2418 $link['social_action'] = 'Share';
2419 break;
2420 case 'reddit':
2421 $link['link'] = esc_url( 'https://reddit.com/submit?url=' . $link['url'] . '&title=' . $link['title'] );
2422 $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>';
2423 $link['attr_title'] = esc_html__( 'Share on Reddit', 'shopbuilder' );
2424 $link['social_network'] = 'Reddit';
2425 $link['social_action'] = 'Share';
2426 break;
2427 case 'telegram':
2428 $link['link'] = esc_url( 'https://telegram.me/share/url?text=' . $link['title'] . '&url=' . $link['url'] );
2429 $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>';
2430 $link['attr_title'] = esc_html__( 'Share on Telegram', 'shopbuilder' );
2431 $link['social_network'] = 'Telegram';
2432 $link['social_action'] = 'Share';
2433 break;
2434 }
2435
2436 $link['label'] = $type['share_text'];
2437 $link['target'] = '_blank';
2438 $link['rel'] = 'nofollow noopener noreferrer';
2439
2440 $data = '';
2441 $link = apply_filters( 'rtsb/elements/share/share_link', $link, $id, $preset );
2442 $icon = apply_filters( 'rtsb/elements/share/share_icon', $link['icon'], $preset );
2443 $target = ! empty( $link['target'] ) ? ' target="' . esc_attr( $link['target'] ) . '" ' : '';
2444 $rel = ! empty( $link['rel'] ) ? ' rel="' . esc_attr( $link['rel'] ) . '" ' : '';
2445 $attr_title = ! empty( $link['attr_title'] ) ? ' title="' . esc_attr( $link['attr_title'] ) . '" ' : '';
2446 $elements = [];
2447
2448 // Add classes.
2449 $css_classes = [
2450 'rtsb-share-btn',
2451 sanitize_html_class( $link['type'] ),
2452 ];
2453 $css_classes = array_merge( $css_classes, explode( ' ', $link['class'] ) );
2454 $css_classes = array_map( 'sanitize_html_class', $css_classes );
2455 $css_classes = implode( ' ', array_filter( $css_classes ) );
2456
2457 unset( $attr['pin-do'], $attr['action'] );
2458
2459 if ( 'pinterest' === $type['share_items'] ) {
2460 $attr['pin-do'] = 'none';
2461 }
2462
2463 if ( 'whatsapp' === $type['share_items'] ) {
2464 $attr['action'] = 'share/whatsapp/share';
2465 }
2466
2467 $attr = apply_filters( 'rtsb/elements/share/link_data', $attr, $link, $id );
2468
2469 if ( ! empty( $attr ) ) {
2470 foreach ( $attr as $key => $val ) {
2471 $data .= ' data-' . sanitize_html_class( $key ) . '="' . esc_attr( $val ) . '"';
2472 }
2473 }
2474
2475 $additional_attr = apply_filters( 'rtsb/elements/share/additional_attr', [], $link, $id, $preset );
2476
2477 if ( ! empty( $additional_attr ) ) {
2478 $attr_output = join( ' ', $additional_attr );
2479
2480 if ( ! empty( $data ) ) {
2481 $attr_output = ' ' . $attr_output;
2482 }
2483
2484 $data .= $attr_output;
2485 }
2486
2487 $elements['wrapper_start'] = sprintf(
2488 '<li class="rtsb-share-item"><a href="%s"%s%s%s class="%s"%s>',
2489 ! empty( $link['link'] ) ? esc_attr( $link['link'] ) : '',
2490 $attr_title,
2491 $target,
2492 $rel,
2493 $css_classes,
2494 $data
2495 );
2496 $elements['wrapper_end'] = '</a></li>';
2497
2498 $elements['icon'] = $show_icon ? '<span class="rtsb-share-icon">' . ( ! empty( $icon ) ? $icon : null ) . '</span>' : null;
2499 $elements['label'] = $show_text && ! empty( $link['label'] ) ? '<span class="rtsb-share-label">' . $link['label'] . '</span>' : null;
2500 $elements['icon_label'] = '<span class="rtsb-share-icon-label">' . $elements['icon'] . $elements['label'] . '</span>';
2501 $elements = apply_filters( 'rtsb/elements/share/output_elements', $elements, $link, $id );
2502
2503 $output .= $elements['wrapper_start'] . $elements['icon_label'] . $elements['wrapper_end'];
2504 }
2505
2506 return apply_filters( 'rtsb/elements/share/list_output', $output );
2507 }
2508
2509 /**
2510 * Social Share Platforms.
2511 *
2512 * @param string $module Module.
2513 *
2514 * @return true|void
2515 */
2516 public static function is_module_active( $module ) {
2517 $modulelist = self::get_modules_list();
2518
2519 if ( ! empty( $modulelist[ $module ]['active'] ) ) {
2520 return true;
2521 }
2522 }
2523 /**
2524 * Checks if catalog pro is active
2525 *
2526 * @return boolean
2527 */
2528 public static function is_catalog_mode() {
2529 return function_exists( 'rtsbpro' ) && self::is_module_active( 'catalog_mode' );
2530 }
2531 /**
2532 * Checks if back in stock notifier is active
2533 *
2534 * @return boolean
2535 */
2536 public static function is_back_in_stock_notifier_enable() {
2537 return function_exists( 'rtsbpro' ) && self::is_module_active( 'back_in_stock_notifier' );
2538 }
2539
2540 /**
2541 * Elementor Widget Active.
2542 *
2543 * @param string $widget Elementor Widget.
2544 *
2545 * @return true|void
2546 */
2547 public static function is_elementor_widget_active( $widget ) {
2548 $element_list = self::get_widgets_list();
2549
2550 if ( ! empty( $element_list[ $widget ]['active'] ) ) {
2551 return true;
2552 }
2553 }
2554
2555 /**
2556 * Sanitize a media (fileupload) control value into an { id, source } array.
2557 *
2558 * The control value is posted as an array (or a JSON string). Using it as a
2559 * `sanitize_fn` avoids it being flattened to an empty string by the default
2560 * sanitize_text_field() path in set_options().
2561 *
2562 * @param mixed $raw_value Raw posted value.
2563 *
2564 * @return array|string
2565 */
2566 public static function sanitize_fileupload_value( $raw_value ) {
2567 $decoded = is_string( $raw_value ) && '' !== $raw_value ? json_decode( wp_unslash( $raw_value ), true ) : $raw_value;
2568 if ( ! is_array( $decoded ) ) {
2569 return '';
2570 }
2571 return [
2572 'id' => isset( $decoded['id'] ) ? absint( $decoded['id'] ) : 0,
2573 'source' => isset( $decoded['source'] ) ? esc_url_raw( $decoded['source'] ) : '',
2574 ];
2575 }
2576
2577 /***
2578 * Save Settings data
2579 *
2580 * @param string $section_id Section ID.
2581 * @param string $block_id Block ID.
2582 * @param array $rawOptions Raw Options.
2583 *
2584 * @return array
2585 */
2586 public static function set_options( $section_id = '', $block_id = '', $rawOptions = [] ) { // phpcs:ignore Generic.Metrics.NestingLevel.TooHigh
2587 $section_id = ! empty( $section_id ) ? sanitize_text_field( wp_unslash( $section_id ) ) : '';
2588 $block_id = ! empty( $block_id ) ? sanitize_text_field( wp_unslash( $block_id ) ) : '';
2589 $rawOptions = ! empty( $rawOptions ) ? $rawOptions : [];
2590 $results = [
2591 'status' => true,
2592 'message' => '',
2593 ];
2594 if ( ! $section_id || ! $block_id ) {
2595 $results['status'] = false;
2596 $results['message'] = esc_html__( 'Section , block or options may be empty', 'shopbuilder' );
2597
2598 return $results;
2599 }
2600 $sections = Settings::instance()->get_sections();
2601 if ( empty( $sections[ $section_id ] ) || empty( $sections[ $section_id ]['list'][ $block_id ] ) ) {
2602 $results['status'] = false;
2603 $results['message'] = esc_html__( 'No section or block found with given data', 'shopbuilder' );
2604
2605 return $results;
2606 }
2607
2608 $options = DataModel::source()->get_option( $section_id, [], false );
2609 $changed = false;
2610 $fields = [];
2611 if ( isset( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) && ! empty( $sections[ $section_id ]['list'][ $block_id ]['fields'] ) ) {
2612 $fields = $sections[ $section_id ]['list'][ $block_id ]['fields'];
2613 }
2614 do_action( 'rtsb/before/save/options', $section_id, $block_id, $rawOptions );
2615 if ( empty( $fields ) ) {
2616 if ( isset( $rawOptions['active'] ) ) {
2617 $changed = true;
2618 $options[ $block_id ]['active'] = 'on' === $rawOptions['active'] ? 'on' : '';
2619 }
2620 } else {
2621 foreach ( $rawOptions as $raw_option_key => $raw_value ) {
2622 if ( 'active' === $raw_option_key ) {
2623 $changed = true;
2624 $options[ $block_id ]['active'] = $sections[ $section_id ]['list'][ $block_id ]['active'] = 'on' === $raw_value ? 'on' : ''; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
2625 } else {
2626 if ( isset( $fields[ $raw_option_key ] ) ) {
2627 $field = $fields[ $raw_option_key ];
2628 if ( 'switch' === $field['type'] ) {
2629 $value = 'on' === $raw_value ? 'on' : '';
2630 } elseif ( 'repeaters' === $field['type'] ) {
2631 $the_value = [];
2632 $rep_title = [];
2633 if ( ! empty( $raw_value ) && is_array( $raw_value ) ) {
2634 foreach ( $raw_value as $key => $value ) {
2635 if ( is_string( $value ) ) {
2636 $the_value_decoded = json_decode( stripslashes_deep( $value ) );
2637 } else {
2638 $the_value_decoded = $value;
2639 }
2640 $cr = $the_value_decoded->title ?? '';
2641 if ( in_array( $cr, $rep_title, true ) ) {
2642 continue;
2643 }
2644 $rep_title[] = $cr;
2645 $the_value[] = $the_value_decoded;
2646 }
2647 } elseif ( ! empty( $raw_value ) && is_string( $raw_value ) ) {
2648 $the_value = $raw_value;
2649 }
2650 $value = wp_json_encode( $the_value, JSON_UNESCAPED_UNICODE );
2651 } elseif ( in_array( $field['type'], [ 'product_addons_special_settings', 'checkout_fields' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2652 $manual_field_value = [];
2653 if ( is_array( $raw_value ) ) {
2654 foreach ( $raw_value as $key => $value ) {
2655 $manual_field_value[] = json_decode( stripslashes( $value ), true );
2656 }
2657 $value = wp_json_encode( $manual_field_value, JSON_UNESCAPED_UNICODE );
2658 } elseif ( is_string( $raw_value ) ) {
2659 $value = $raw_value;
2660 }
2661 } else {
2662 if ( ! empty( $field['multiple'] ) || in_array( $field['type'], [ 'checkbox', 'search_and_multi_select' ] ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
2663 if ( isset( $raw_value ) && is_array( $raw_value ) ) {
2664 if ( ! empty( $field['sanitize_fn'] ) && is_callable( $field['sanitize_fn'] ) ) {
2665 $value = array_map( $field['sanitize_fn'], $raw_value );
2666 } else {
2667 $value = array_map( 'sanitize_text_field', $raw_value );
2668 }
2669 } else {
2670 $value = [];
2671 }
2672 } else {
2673 if ( ! empty( $field['sanitize_fn'] ) ) {
2674 if ( is_callable( $field['sanitize_fn'] ) ) {
2675 $value = $field['sanitize_fn']( $raw_value );
2676 } elseif ( 'pass_all' === $field['sanitize_fn'] ) {
2677 $value = $raw_value;
2678 } else {
2679 $value = $field['sanitize_fn']( $raw_value );
2680 }
2681 } else {
2682 $value = sanitize_text_field( $raw_value );
2683 }
2684 }
2685 }
2686 $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
2687 $changed = true;
2688 }
2689 }
2690 }
2691 }
2692 if ( ! $changed ) {
2693 $results['status'] = false;
2694 $results['message'] = esc_html__( 'No changes found for update', 'shopbuilder' );
2695
2696 return $results;
2697 }
2698
2699 DataModel::source()->set_option( $section_id, $options );
2700
2701 $results['status'] = true;
2702 $results['message'] = esc_html__( 'Successfully Saved', 'shopbuilder' );
2703 $results['sections'] = $sections;
2704
2705 return $results;
2706 }
2707
2708 /**
2709 * Count products by taxonomies.
2710 *
2711 * @param array $terms Terms.
2712 * @param string $taxonomy Taxonomy.
2713 *
2714 * @return int|void
2715 */
2716 public static function count_products_by_taxonomies( $terms, $taxonomy = 'product_cat' ) {
2717 if ( empty( $terms ) || ! is_array( $terms ) ) {
2718 return;
2719 }
2720
2721 $args = [
2722 'limit' => -1,
2723 'return' => 'ids',
2724 ];
2725
2726 if ( 'product_cat' === $taxonomy ) {
2727 $args['product_category_id'] = $terms;
2728 } elseif ( 'product_brand' === $taxonomy ) {
2729 $args['product_brand_id'] = $terms;
2730 } else {
2731 $args['product_tag_id'] = $terms;
2732 }
2733
2734 $query = new WC_Product_Query( $args );
2735
2736 return ! empty( $query->get_products() ) ? count( $query->get_products() ) : 0;
2737 }
2738
2739 /**
2740 * Count products by attribute terms.
2741 *
2742 * @param array $term_ids Term IDs.
2743 * @param string $relation Relation.
2744 *
2745 * @return int
2746 */
2747 public static function count_products_by_attribute_terms( $term_ids, $relation = 'AND' ) {
2748 if ( empty( $term_ids ) || ! is_array( $term_ids ) ) {
2749 return 0;
2750 }
2751
2752 $terms_by_taxonomy = [];
2753
2754 foreach ( $term_ids as $term_id ) {
2755 $term = get_term( $term_id );
2756 if ( ! is_wp_error( $term ) && $term ) {
2757 if ( ! isset( $terms_by_taxonomy[ $term->taxonomy ] ) ) {
2758 $terms_by_taxonomy[ $term->taxonomy ] = [];
2759 }
2760 $terms_by_taxonomy[ $term->taxonomy ][] = $term_id;
2761 }
2762 }
2763
2764 if ( empty( $terms_by_taxonomy ) ) {
2765 return 0;
2766 }
2767
2768 $args = [
2769 'status' => 'publish',
2770 'limit' => -1,
2771 'return' => 'ids',
2772 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2773 'relation' => strtoupper( $relation ),
2774 ],
2775 ];
2776
2777 foreach ( $terms_by_taxonomy as $taxonomy => $terms ) {
2778 $args['tax_query'][] = [
2779 'taxonomy' => $taxonomy,
2780 'field' => 'term_id',
2781 'terms' => $terms,
2782 'operator' => 'IN',
2783 ];
2784 }
2785
2786 $query = new WC_Product_Query( $args );
2787 $products = $query->get_products();
2788
2789 return count( $products );
2790 }
2791
2792 /**
2793 * Check if product filters widget has ajax.
2794 *
2795 * @param string $page Page name.
2796 *
2797 * @return bool
2798 */
2799 public static function product_filters_has_ajax( $page ) {
2800 if ( empty( $page ) ) {
2801 return false;
2802 }
2803
2804 if ( 'page' === get_post_type() ) {
2805 return false;
2806 }
2807
2808 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2809 if ( ! $id ) {
2810 return false;
2811 }
2812 $cache_key = 'product_filters_has_ajax_' . $id;
2813 if ( isset( self::$cache[ $cache_key ] ) ) {
2814 return self::$cache[ $cache_key ];
2815 }
2816 $elmap = ElementorDataMap::instance();
2817 $ajax = [];
2818
2819 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
2820 $ajax[] = isset( $data['settings']['ajax_mode'] ) ? false : true;
2821 }
2822
2823 self::$cache[ $cache_key ] = ! empty( $ajax[0] );
2824 return ! empty( $ajax[0] );
2825 }
2826
2827 /**
2828 * Build the active filter chips HTML from the current request query string.
2829 *
2830 * Mirrors the markup produced by the JS `displayAppliedFilter()` method so
2831 * the chips are visible on initial page load (before AJAX hydration).
2832 *
2833 * @return string HTML for the active filter chips, or empty string if none.
2834 */
2835 public static function get_active_filters_html() {
2836 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only query string parsing for display.
2837 if ( empty( $_GET ) ) {
2838 return '';
2839 }
2840
2841 $skip_keys = [
2842 'displayview',
2843 'orderby',
2844 'paged',
2845 'page',
2846 'product-page',
2847 'rtsb_archive_page',
2848 'rtsb_grid_columns',
2849 'min_price',
2850 'max_price',
2851 '_pjax',
2852 ];
2853
2854 // When the "Product Categories" widget is already showing the active
2855 // category navigation, suppress the duplicate product_cat chip group.
2856 if ( self::is_product_categories_widget_active() ) {
2857 $skip_keys[] = 'product_cat';
2858 }
2859
2860 $groups = [];
2861
2862 foreach ( $_GET as $raw_key => $raw_value ) {
2863 $key = sanitize_key( $raw_key );
2864
2865 if ( '' === $key || in_array( $key, $skip_keys, true ) ) {
2866 continue;
2867 }
2868
2869 if ( 0 === strpos( $key, 'query_type_' ) ) {
2870 continue;
2871 }
2872
2873 if ( is_array( $raw_value ) ) {
2874 continue;
2875 }
2876
2877 $value = sanitize_text_field( wp_unslash( $raw_value ) );
2878
2879 if ( '' === $value ) {
2880 continue;
2881 }
2882
2883 $taxonomy = $key;
2884 if ( 0 === strpos( $taxonomy, 'filter_' ) ) {
2885 $taxonomy = 'pa_' . substr( $taxonomy, 7 );
2886 }
2887
2888 $items = [];
2889 foreach ( explode( ',', $value ) as $slug ) {
2890 $slug = trim( $slug );
2891 if ( '' === $slug ) {
2892 continue;
2893 }
2894 $items[] = [
2895 'slug' => $slug,
2896 'name' => self::get_active_filter_term_label( $taxonomy, $slug, $key ),
2897 ];
2898 }
2899
2900 if ( empty( $items ) ) {
2901 continue;
2902 }
2903
2904 $groups[ $key ] = [
2905 'label' => self::get_active_filter_group_label( $taxonomy, $key ),
2906 'items' => $items,
2907 'class' => $key,
2908 ];
2909 }
2910
2911 // Price range as a single combined chip.
2912 if ( isset( $_GET['min_price'] ) || isset( $_GET['max_price'] ) ) {
2913 $min = isset( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : '';
2914 $max = isset( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : '';
2915
2916 if ( '' !== $min || '' !== $max ) {
2917 $symbol = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '';
2918 $groups['price_filter'] = [
2919 'label' => esc_html__( 'Price', 'shopbuilder' ),
2920 'class' => 'price_filter',
2921 'items' => [
2922 [
2923 'slug' => $min . ',' . $max,
2924 'name' => $symbol . $min . ' - ' . $symbol . $max,
2925 'extra_class' => ' remove-price',
2926 ],
2927 ],
2928 ];
2929 }
2930 }
2931
2932 if ( empty( $groups ) ) {
2933 return '';
2934 }
2935
2936 $html = '<div class="rtsb-active-filters">';
2937
2938 foreach ( $groups as $key => $group ) {
2939 $html .= '<div class="active-filter ' . esc_attr( $group['class'] ) . '">';
2940
2941 if ( ! empty( $group['label'] ) ) {
2942 $html .= '<div class="filter-name">' . esc_html( $group['label'] ) . ': </div>';
2943 }
2944
2945 $html .= '<div class="filter-item-container">';
2946
2947 foreach ( $group['items'] as $item ) {
2948 $extra = isset( $item['extra_class'] ) ? $item['extra_class'] : '';
2949 $html .= '<div class="filter-item">' . esc_html( $item['name'] );
2950 $html .= '<span class="remove-filter' . esc_attr( $extra ) . '"';
2951 $html .= ' title="' . esc_attr( sprintf( /* translators: %s: filter term name */ __( 'Remove %s', 'shopbuilder' ), $item['name'] ) ) . '"';
2952 $html .= ' data-filter-name="' . esc_attr( $key ) . '"';
2953 $html .= ' data-filter-value="' . esc_attr( $item['slug'] ) . '">';
2954 $html .= '<i class="rtsb-icon rtsb-icon-delete"></i></span>';
2955 $html .= '</div>';
2956 }
2957
2958 $html .= '</div></div>';
2959 }
2960
2961 $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>';
2962 $html .= '</div>';
2963
2964 return $html;
2965 // phpcs:enable
2966 }
2967
2968 /**
2969 * Determine whether the "Product Categories" Elementor widget is present
2970 * on the current shop/archive layout. Used to avoid duplicating the
2971 * product_cat chip group when the widget already shows the active term.
2972 *
2973 * @return bool
2974 */
2975 private static function is_product_categories_widget_active() {
2976 $elmap = ElementorDataMap::instance();
2977
2978 foreach ( [ 'shop', 'archive' ] as $page ) {
2979 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
2980
2981 if ( ! $id ) {
2982 continue;
2983 }
2984
2985 $widgets = $elmap->get_widget_data( 'rtsb-product-categories-general', [], $id );
2986
2987 if ( ! empty( $widgets ) ) {
2988 return true;
2989 }
2990 }
2991
2992 return false;
2993 }
2994
2995 /**
2996 * Resolve the display label for an active filter group (taxonomy heading).
2997 *
2998 * @param string $taxonomy Resolved taxonomy slug (e.g. product_cat, pa_color).
2999 * @param string $raw_key Original URL key (e.g. product_cat, filter_color).
3000 *
3001 * @return string
3002 */
3003 private static function get_active_filter_group_label( $taxonomy, $raw_key ) {
3004 if ( 's' === $raw_key || 'search' === $raw_key ) {
3005 return esc_html__( 'Search', 'shopbuilder' );
3006 }
3007 if ( 'rating_filter' === $raw_key ) {
3008 return esc_html__( 'Ratings', 'shopbuilder' );
3009 }
3010 if ( 'sale_filter' === $raw_key ) {
3011 return esc_html__( 'Sale Filter', 'shopbuilder' );
3012 }
3013
3014 if ( 0 === strpos( $taxonomy, 'pa_' ) && function_exists( 'wc_attribute_label' ) ) {
3015 return wc_attribute_label( $taxonomy );
3016 }
3017
3018 if ( taxonomy_exists( $taxonomy ) ) {
3019 $tax_obj = get_taxonomy( $taxonomy );
3020 if ( $tax_obj && ! empty( $tax_obj->labels->name ) ) {
3021 return $tax_obj->labels->name;
3022 }
3023 }
3024
3025 return ucwords( str_replace( [ '_', '-' ], ' ', $raw_key ) );
3026 }
3027
3028 /**
3029 * Resolve a term slug to its display name; fall back to a humanised slug.
3030 *
3031 * @param string $taxonomy Resolved taxonomy slug.
3032 * @param string $slug Raw slug from query string.
3033 * @param string $raw_key Original URL key.
3034 *
3035 * @return string
3036 */
3037 private static function get_active_filter_term_label( $taxonomy, $slug, $raw_key ) {
3038 if ( 's' === $raw_key || 'search' === $raw_key || 'rating_filter' === $raw_key ) {
3039 return rawurldecode( $slug );
3040 }
3041
3042 if ( taxonomy_exists( $taxonomy ) ) {
3043 $term = get_term_by( 'slug', $slug, $taxonomy );
3044 if ( $term && ! is_wp_error( $term ) ) {
3045 return $term->name;
3046 }
3047 }
3048
3049 return ucwords( str_replace( [ '-', '_' ], ' ', rawurldecode( $slug ) ) );
3050 }
3051
3052 /**
3053 * Check if product has applied filter.
3054 *
3055 * @param string $page Page name.
3056 *
3057 * @return bool
3058 */
3059 public static function product_has_applied_filters( $page ) {
3060 if ( empty( $page ) ) {
3061 return false;
3062 }
3063
3064 $id = BuilderFns::is_builder_preview() ? get_the_ID() : BuilderFns::builder_page_id_by_type( $page );
3065 $elmap = ElementorDataMap::instance();
3066 $ajax = [];
3067 if ( ! $id ) {
3068 return false;
3069 }
3070 foreach ( $elmap->get_widget_data( 'rtsb-ajax-product-filters', [], $id ) as $data ) {
3071 $ajax[] = ! isset( $data['settings']['active_filter'] );
3072
3073 }
3074
3075 // Also account for the free, non-AJAX "Product Filters" widget so that
3076 // the active filter chips wrapper renders for it on initial page load.
3077 foreach ( $elmap->get_widget_data( 'rtsb-product-filters', [], $id ) as $data ) {
3078 $ajax[] = true;
3079 unset( $data );
3080 }
3081
3082 return ! empty( $ajax[0] );
3083 }
3084
3085 /**
3086 * Get WooCommerce product categories.
3087 *
3088 * @param string|null $search_query The search string for category names.
3089 *
3090 * @return array An array of product categories with 'value' and 'label' keys.
3091 */
3092 public static function products_category_query( $search_query = null ) {
3093 if ( ! is_admin() ) {
3094 return [];
3095 }
3096
3097 $cache_key = 'rtsb_product_categories_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
3098
3099 if ( isset( self::$cache[ $cache_key ] ) ) {
3100 return self::$cache[ $cache_key ];
3101 }
3102 $results = wp_cache_get( $cache_key, 'shopbuilder' );
3103 if ( ! $results ) {
3104 global $wpdb;
3105 $sql = "SELECT t.term_id, t.name
3106 FROM {$wpdb->terms} t
3107 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
3108 WHERE tt.taxonomy = 'product_cat'";
3109
3110 if ( $search_query ) {
3111 $sql .= ' AND t.name LIKE %s';
3112 $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3113 } else {
3114 $prepared_sql = $sql; // No need for placeholders.
3115 }
3116
3117 $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
3118 // Cache the results in the object cache for future use.
3119 wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
3120 Cache::set_data_cache_key( $cache_key );
3121 }
3122
3123 $cats = [];
3124 if ( ! is_array( $results ) && ! count( $results ) ) {
3125 return $cats;
3126 }
3127 foreach ( $results as $row ) {
3128 $category_id = $row->term_id;
3129 $category_title = $row->name;
3130
3131 $cats[] = [
3132 'value' => $category_id,
3133 'label' => $category_title,
3134 ];
3135 }
3136
3137 // Cache the results in a static variable for the next call.
3138 self::$cache[ $cache_key ] = $cats;
3139 return $cats;
3140 }
3141 /**
3142 * Get WooCommerce product categories.
3143 *
3144 * @param string|null $search_query The search string for category names.
3145 *
3146 * @return array An array of product categories with 'value' and 'label' keys.
3147 */
3148 public static function products_tags_query( $search_query = null ) {
3149 if ( ! is_admin() ) {
3150 return [];
3151 }
3152
3153 $cache_key = 'rtsb_product_tags_' . md5( serialize( $search_query ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
3154
3155 if ( isset( self::$cache[ $cache_key ] ) ) {
3156 return self::$cache[ $cache_key ];
3157 }
3158 $results = wp_cache_get( $cache_key, 'shopbuilder' );
3159 if ( ! $results ) {
3160 global $wpdb;
3161 $sql = "SELECT t.term_id, t.name
3162 FROM {$wpdb->terms} t
3163 JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
3164 WHERE tt.taxonomy = 'product_tag'";
3165
3166 if ( $search_query ) {
3167 $sql .= ' AND t.name LIKE %s';
3168 $prepared_sql = $wpdb->prepare( $sql, '%' . $wpdb->esc_like( $search_query ) . '%' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
3169 } else {
3170 $prepared_sql = $sql; // No need for placeholders.
3171 }
3172
3173 $results = $wpdb->get_results( $prepared_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
3174 // Cache the results in the object cache for future use.
3175 wp_cache_set( $cache_key, $results, 'shopbuilder' ); // Adjust the expiration time as needed.
3176 Cache::set_data_cache_key( $cache_key );
3177 }
3178
3179 $cats = [];
3180 if ( ! is_array( $results ) && ! count( $results ) ) {
3181 return $cats;
3182 }
3183 foreach ( $results as $row ) {
3184 $category_id = $row->term_id;
3185 $category_title = $row->name;
3186
3187 $cats[] = [
3188 'value' => $category_id,
3189 'label' => $category_title,
3190 ];
3191 }
3192
3193 // Cache the results in a static variable for the next call.
3194 self::$cache[ $cache_key ] = $cats;
3195 return $cats;
3196 }
3197
3198 /**
3199 * @param string $search_query Search query.
3200 *
3201 * @return array
3202 */
3203 public static function get_registered_post_types( $search_query = null ) {
3204 // Get all registered post types.
3205 $registered_post_types = get_post_types(
3206 [
3207 'public' => true,
3208 '_builtin' => false,
3209 ],
3210 'objects'
3211 );
3212
3213 unset( $registered_post_types['elementor_library'] );
3214 unset( $registered_post_types['e-landing-page'] );
3215 unset( $registered_post_types['rtsb_builder'] );
3216
3217 $post_types = [];
3218
3219 // Check if a search query is provided.
3220 if ( ! empty( $search_query ) ) {
3221 // Filter post types based on the search query.
3222 $registered_post_types = array_filter(
3223 $registered_post_types,
3224 function ( $post_type ) use ( $search_query ) {
3225 return stripos( $post_type->labels->singular_name, $search_query ) !== false;
3226 }
3227 );
3228 // Check if 'post' match the search query and include them if they do.
3229 if ( stripos( 'post', $search_query ) !== false ) {
3230 $post_types[] = [
3231 'value' => 'post',
3232 'label' => 'Post',
3233 ];
3234 }
3235 } else {
3236 $post_types[] = [
3237 'value' => 'post',
3238 'label' => 'Post',
3239 ];
3240 }
3241
3242 // Convert the filtered post types to an array.
3243 foreach ( $registered_post_types as $post_type ) {
3244 $post_types[] = [
3245 'value' => $post_type->name,
3246 'label' => $post_type->labels->singular_name,
3247 ];
3248 }
3249
3250 return $post_types;
3251 }
3252
3253 /**
3254 * Get Post Types.
3255 *
3256 * @param string $search_query Search query.
3257 * @param array $args Arguments.
3258 *
3259 * @return array
3260 */
3261 public static function get_post_types( $search_query = null, $args = [] ) {
3262
3263 $args = wp_parse_args(
3264 $args,
3265 [
3266 'post_type' => 'any',
3267 'posts_per_page' => 20,
3268 'orderby' => 'ID',
3269 'order' => 'DESC',
3270 ]
3271 );
3272
3273 if ( 'archive' !== $args['post_type'] ) {
3274 if ( $search_query ) {
3275 $args['s'] = $search_query;
3276 }
3277 $posts = [];
3278 $query_results = get_posts( $args );
3279 foreach ( $query_results as $post ) {
3280 $posts[] = [
3281 'value' => $post->ID,
3282 'label' => $post->post_title . ' (ID#' . $post->ID . ')',
3283 ];
3284 }
3285
3286 if ( $search_query ) {
3287 if ( strpos( '-error 404', $search_query ) ) {
3288 $posts[] = [
3289 'value' => 'error',
3290 'label' => __( '404 Error', 'shopbuilder' ),
3291 ];
3292 }
3293 } else {
3294 if ( 'page' === $args['post_type'] ) {
3295 $posts[] = [
3296 'value' => 'error',
3297 'label' => __( '404 Error', 'shopbuilder' ),
3298 ];
3299 }
3300 }
3301 } else {
3302 $posts = self::get_registered_post_types( $search_query );
3303 }
3304
3305 return $posts;
3306 }
3307
3308 /**
3309 * Get all Elementor breakpoints.
3310 *
3311 * @return array
3312 */
3313 public static function get_elementor_breakpoints() {
3314 return ( new \Elementor\Core\Breakpoints\Manager() )->get_breakpoints_config();
3315 }
3316
3317 /**
3318 * Render view.
3319 *
3320 * @param string $viewName View name.
3321 * @param array $args View args.
3322 * @param boolean $return View return.
3323 * @return string|void
3324 */
3325 public static function renderView( $viewName, $args = [], $return = false ) {
3326 $viewName = str_replace( '.', '/', $viewName );
3327
3328 if ( ! empty( $args ) && is_array( $args ) ) {
3329 extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
3330 }
3331
3332 $view_file = rtsb()->plugin_path() . '/resources/' . $viewName . '.php';
3333
3334 if ( ! file_exists( $view_file ) ) {
3335 _doing_it_wrong( __FUNCTION__, sprintf( '<code>%s</code> does not exist.', esc_html( $view_file ) ), '1.7.0' );
3336
3337 return;
3338 }
3339
3340 if ( $return ) {
3341 ob_start();
3342 include $view_file;
3343
3344 return ob_get_clean();
3345 } else {
3346 include $view_file;
3347 }
3348 }
3349
3350 /**
3351 * Best selling product query.
3352 *
3353 * @param int $minimum_sale Minimum sale.
3354 * @param int $limit Total limit.
3355 *
3356 * @return array|mixed
3357 */
3358 public static function best_selling_products_ids( $minimum_sale = 1, $limit = 10 ) {
3359 $cache_key = 'rtsb_best_selling_products_' . $minimum_sale . '_' . $limit;
3360 // Check if the data is in the cache.
3361 if ( isset( self::$cache[ $cache_key ] ) ) {
3362 return self::$cache[ $cache_key ];
3363 }
3364 $cached_data = get_transient( $cache_key );
3365 if ( $cached_data ) {
3366 self::$cache[ $cache_key ] = $cached_data;
3367 return $cached_data;
3368 }
3369 global $wpdb;
3370 $sql = $wpdb->prepare(
3371 "
3372 SELECT ID
3373 FROM {$wpdb->posts} AS p
3374 LEFT JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id
3375 WHERE p.post_type = 'product'
3376 AND p.post_status = 'publish'
3377 AND pm.meta_key = 'total_sales'
3378 AND pm.meta_value >= %d
3379 ORDER BY pm.meta_value + 0 DESC
3380 LIMIT %d
3381 ",
3382 $minimum_sale,
3383 $limit
3384 );
3385 $best_selling = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
3386 // Cache the result for future use.
3387 set_transient( $cache_key, $best_selling, DAY_IN_SECONDS );
3388 self::$cache[ $cache_key ] = $best_selling;
3389 return $best_selling;
3390 }
3391
3392 /**
3393 * @param null|Object $product Product Object.
3394 * @param int $days_threshold Date String.
3395 *
3396 * @return bool
3397 */
3398 public static function is_new_product( $product = null, $days_threshold = 30 ) {
3399 if ( is_null( $product ) ) {
3400 global $product;
3401 }
3402 if ( ! $product instanceof WC_Product ) {
3403 return false;
3404 }
3405
3406 $product_id = $product->get_id();
3407 $days_threshold = ! empty( $days_threshold ) ? $days_threshold : 30;
3408 $cache_key = 'is_new_product_' . $product_id . '_' . $days_threshold;
3409
3410 if ( isset( self::$cache[ $cache_key ] ) ) {
3411 return self::$cache[ $cache_key ];
3412 }
3413 $date_created = $product->get_date_created();
3414 if ( ! $date_created ) {
3415 return false;
3416 }
3417 $publish_timestamp = strtotime( $date_created->date_i18n( 'Y-m-d H:i:s' ) );
3418
3419 // Calculate the difference in days.
3420 $days_difference = abs( time() - $publish_timestamp ) / ( 60 * 60 * 24 );
3421
3422 // Check if the product is considered new.
3423 $is_new = $days_difference <= $days_threshold;
3424
3425 // Cache the result for a day.
3426 self::$cache[ $cache_key ] = $is_new;
3427
3428 return $is_new;
3429 }
3430
3431 /**
3432 * Checks if a feature is disabled for guest users based on the provided option.
3433 *
3434 * @param string $option The option to retrieve from the item.
3435 * @param mixed $default The default value if the option is not found.
3436 *
3437 * @return bool
3438 */
3439 public static function is_guest_feature_disabled( $option, $default ) {
3440 $disabled = self::get_option( 'general', 'guest_user', $option, $default );
3441
3442 return ! is_user_logged_in() && $disabled;
3443 }
3444
3445 /**
3446 * Checks if a feature is disabled for guest users based on the provided option.
3447 *
3448 * @return void
3449 */
3450 public static function woocommerce_output_all_notices() {
3451 if ( function_exists( 'wc_print_notices' ) ) :
3452 echo '<div class="rtsb-notice">';
3453 woocommerce_output_all_notices();
3454 echo '</div>';
3455 endif;
3456 }
3457
3458 /**
3459 * @param object $product Product.
3460 * @return bool
3461 */
3462 public static function is_visible_qty_input( $product = null ) {
3463 $is_visible_qty = true;
3464 if ( $product instanceof \WC_Product ) {
3465 if ( $product->is_sold_individually() ) {
3466 $is_visible_qty = false;
3467 } elseif ( $product->managing_stock() ) {
3468 if ( in_array( $product->get_backorders(), [ 'notify' , 'yes' ], true ) ) {
3469 $is_visible_qty = true;
3470 } elseif ( $product->get_stock_quantity() < 2 ) {
3471 $is_visible_qty = false;
3472 }
3473 }
3474 }
3475 return $is_visible_qty;
3476 }
3477
3478 /**
3479 * @param int $object_id Object id.
3480 * @param string $element_type element type.
3481 * @param string|null $current_lang null for current language, default for default languages.
3482 * @return false|mixed|null
3483 */
3484 public static function wpml_object_id( $object_id, $element_type, $current_lang = 'default' ) {
3485 if ( ! defined( 'ICL_SITEPRESS_VERSION' ) ) {
3486 return $object_id;
3487 }
3488 if ( ! $object_id || ! $element_type ) {
3489 return $object_id;
3490 }
3491 if ( 'default' === $current_lang ) {
3492 $lang = apply_filters( 'wpml_default_language', null );
3493 } else {
3494 $lang = null;
3495 }
3496 return apply_filters( 'wpml_object_id', $object_id, $element_type, false, $lang );
3497 }
3498
3499 /**
3500 * @return string
3501 */
3502 public static function wpml_current_language() {
3503 $current = apply_filters( 'wpml_current_language', null );
3504 $default = apply_filters( 'wpml_default_language', null );
3505 return $default !== $current ? $current : null;
3506 }
3507
3508 /**
3509 * Custom icons.
3510 *
3511 * @return string[]
3512 */
3513 public static function get_custom_icon_names() {
3514 return [
3515 'heart-empty',
3516 'heart',
3517 'eye',
3518 'exchange',
3519 'plus',
3520 'minus',
3521 'avatar',
3522 'pay',
3523 'share',
3524 'clock',
3525 'check-alt',
3526 'check',
3527 'delete',
3528 'marker',
3529 'list',
3530 'list-2',
3531 'power',
3532 'cart',
3533 'cart-2',
3534 'cart-3',
3535 'downloads',
3536 'zoom',
3537 'user-edit',
3538 'grid',
3539 'filter',
3540 'billing',
3541 'login',
3542 'payment',
3543 'search',
3544 'edit',
3545 'coupon',
3546 'arrows-cw',
3547 'trash-empty',
3548 ];
3549 }
3550
3551 /**
3552 * Retrieve an array of custom icons with their HTML representation.
3553 *
3554 * @return array
3555 */
3556 public static function get_icons() {
3557 $icon = self::get_custom_icon_names();
3558 $icons_array = [];
3559 foreach ( $icon as $value ) {
3560 $icons_array[ $value ] = '<i class="rtsb-icon rtsb-icon-' . $value . '"></i> <span class="icon-name">' . ucfirst( str_replace( '-', ' ', $value ) ) . '</span>';
3561 }
3562 return $icons_array;
3563 }
3564
3565 /**
3566 * Generates HTML markup for displaying an endpoint icon.
3567 *
3568 * @param array $data The endpoint data containing icon information.
3569 * @param boolean $wrapper Whether to wrap the icon in a span element.
3570 *
3571 * @return string
3572 */
3573 public static function get_icon_html( $data, $wrapper = true ) {
3574 if ( empty( $data ) || 'none' === $data['icon_source'] ) {
3575 return '';
3576 }
3577 $endpoint = '';
3578 $html = $wrapper ? '<span class="icon ' . esc_attr( $endpoint ) . '_icon">' : '';
3579
3580 if ( 'select_icon' === $data['icon_source'] ) {
3581 $html .= '<i class="rtsb-icon rtsb-icon-' . esc_attr( $data['custom_icon'] ) . '"></i>';
3582 } else {
3583 $icon_html = '';
3584 $icon_url = $data['image_icon']['source'] ?? '';
3585 $icon_id = $data['image_icon']['id'] ?? 0;
3586 $extension = ! empty( $icon_url ) ? strtolower( substr( strrchr( $data['image_icon']['source'], '.' ), 1 ) ) : '';
3587
3588 if ( 'svg' === $extension ) {
3589 $content = file_get_contents( $icon_url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
3590 $is_svg = strpos( $content, '<svg' );
3591
3592 if ( false !== $is_svg ) {
3593 $icon_html = substr( $content, $is_svg );
3594 }
3595 } else {
3596 $attr = [
3597 'class' => 'rtsb-icon-img',
3598 'alt' => esc_html( ucfirst( $endpoint ) ) . esc_html__( ' Icon', 'shopbuilder' ),
3599 ];
3600
3601 $icon_html = wp_get_attachment_image( absint( $icon_id ), 'full', true, $attr );
3602 }
3603
3604 $html .= $icon_html;
3605 }
3606
3607 $html .= $wrapper ? '</span>' : '';
3608
3609 return $html;
3610 }
3611 /**
3612 * Flushes rewrite rules.
3613 *
3614 * @return void
3615 */
3616 public static function maybe_flush_rewrite_rules() {
3617 add_action( 'wp_loaded', [ __CLASS__, 'flush_rewrite_rules' ] );
3618 add_action( 'shutdown', [ __CLASS__, 'flush_rewrite_rules_once_more' ] );
3619 }
3620
3621 /**
3622 * Flushes rewrite rules if needed.
3623 *
3624 * @return void
3625 */
3626 public static function flush_rewrite_rules() {
3627 if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3628 flush_rewrite_rules();
3629 }
3630
3631 if ( get_option( 'rtsb_permalinks_flushed' ) === 'yes' ) {
3632 flush_rewrite_rules();
3633 delete_option( 'rtsb_permalinks_flushed' );
3634 }
3635 }
3636
3637 /**
3638 * Flushes rewrite rules if needed for second time.
3639 *
3640 * @return void
3641 */
3642 public static function flush_rewrite_rules_once_more() {
3643 if ( get_option( 'rtsb_permalinks_need_flush' ) === 'yes' ) {
3644 flush_rewrite_rules();
3645 update_option( 'rtsb_permalinks_flushed', 'yes' );
3646 delete_option( 'rtsb_permalinks_need_flush' );
3647 }
3648 }
3649
3650 /**
3651 * Social Share Platforms.
3652 *
3653 * @param string $section_id Section ID.
3654 * @param string $block_id Block ID.
3655 * @param string $option_key Option key.
3656 * @param string $compare_key Compare key.
3657 * @param string $compare_value Compare value.
3658 * @param array $values Values.
3659 *
3660 * @return void
3661 */
3662 public static function set_repeater_options( $section_id, $block_id, $option_key, $compare_key, $compare_value, $values ) {
3663 $modules = DataModel::source()->get_option( $section_id, [], false );
3664 if ( empty( $modules[ $block_id ] ) ) {
3665 return;
3666 }
3667 $options = $modules[ $block_id ];
3668 if ( empty( $options[ $option_key ] ) ) {
3669 return;
3670 }
3671 $repeater_options = json_decode( $options[ $option_key ], true );
3672 // Check if options are not empty and are in array format.
3673 if ( ! is_array( $repeater_options ) ) {
3674 return;
3675 }
3676 // Use array_column to get an array of values from $compare_key.
3677 $column_values = array_column( $repeater_options, $compare_key );
3678 // Use array_search to find the index of the $compare_value.
3679 $index = array_search( $compare_value, $column_values, true );
3680 if ( false === $index ) {
3681 return;
3682 }
3683 $repeater_options[ $index ] = wp_parse_args( $values, $repeater_options[ $index ] );
3684 $options[ $option_key ] = wp_json_encode( $repeater_options );
3685 $modules[ $block_id ] = $options;
3686 DataModel::source()->set_option( $section_id, $modules );
3687 }
3688
3689
3690 /**
3691 * @param array $ids products id.
3692 * @return array
3693 */
3694 public static function get_available_products_by_ids( $ids = [] ) {
3695 $ids = array_filter(
3696 $ids,
3697 function ( $id ) {
3698 return 'product' === get_post_type( $id ) && 'publish' === get_post_status( $id );
3699 }
3700 );
3701 return $ids;
3702 }
3703
3704 /**
3705 * Generate and cache dynamic CSS styles.
3706 *
3707 * @param array $options CSS options to apply (e.g. padding, margin).
3708 * @param string $cache_key Cache key for the CSS.
3709 * @param array $css_properties An array of CSS properties with selectors and properties.
3710 *
3711 * @return void
3712 */
3713 public static function dynamic_styles( $options, $cache_key, $css_properties ) {
3714 $cached_css = wp_cache_get( $cache_key, 'shopbuilder' );
3715 $style_handle = self::optimized_handle( 'rtsb-frontend' );
3716
3717 if ( false !== $cached_css ) {
3718 wp_add_inline_style( $style_handle, $cached_css );
3719 return;
3720 }
3721
3722 $css_rules = [];
3723 $grouped_css_rules = [];
3724
3725 foreach ( $css_properties as $option => $details ) {
3726 if ( ! empty( $options[ $option ] ) ) {
3727 $selector = $details['selector'] ?? '';
3728 $property = $details['property'] ?? '';
3729 $unit = $details['unit'] ?? '';
3730 $value = $options[ $option ];
3731
3732 if ( empty( $grouped_css_rules[ $selector ] ) ) {
3733 $grouped_css_rules[ $selector ] = [];
3734 }
3735
3736 if ( is_array( $property ) ) {
3737 foreach ( $property as $prop ) {
3738 $grouped_css_rules[ $selector ][] = $prop . ': ' . $value . $unit . ' !important';
3739 }
3740 } else {
3741 $grouped_css_rules[ $selector ][] = $property . ': ' . $value . $unit . ' !important';
3742 }
3743 }
3744 }
3745
3746 foreach ( $grouped_css_rules as $selector => $properties ) {
3747 $css_rules[] = $selector . ' {' . implode( '; ', $properties ) . '}';
3748 }
3749
3750 $dynamic_css = implode( ' ', $css_rules );
3751
3752 wp_cache_set( $cache_key, $dynamic_css, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3753 Cache::set_data_cache_key( $cache_key );
3754
3755 if ( ! empty( $dynamic_css ) ) {
3756 wp_add_inline_style( $style_handle, $dynamic_css );
3757 }
3758 }
3759
3760 /**
3761 * Pro version notice.
3762 *
3763 * @param string $ver Pro Version.
3764 * @param string $tab Tab.
3765 * @param string $name Name.
3766 * @param bool $enable_free_Link Enable free link.
3767 *
3768 * @return array[]
3769 */
3770 public static function pro_version_notice( $ver, $tab = 'billing_fields', $name = 'ShopBuilder', $enable_free_Link = true ) {
3771 return [
3772 'version_check' => [
3773 'id' => 'version_check',
3774 'type' => 'title',
3775 'label' => sprintf(
3776 /* translators: 1: required version, 2: link to the Plugins page */
3777 esc_html__(
3778 'To access all features and settings of this module, please ensure that "%1$s" is updated to version %2$s. %3$s',
3779 'shopbuilder'
3780 ),
3781 $name,
3782 sprintf(
3783 '<br /><u><b>%s</b></u>',
3784 esc_html( $ver ?? '1.5.0' ) . ' or higher'
3785 ),
3786 $enable_free_Link
3787 ? sprintf(
3788 '%s <a class="pro-update-required" href="%s" target="_blank" title="%s">%s</a>',
3789 esc_attr__( 'Update to the latest version', 'shopbuilder' ),
3790 esc_url( admin_url( 'plugins.php' ) ),
3791 esc_attr__( 'Go to the Plugin Page', 'shopbuilder' ),
3792 esc_html__( 'from the Plugins page', 'shopbuilder' )
3793 )
3794 : ''
3795 ),
3796 'tab' => $tab,
3797 'customClass' => 'checkout-notice',
3798 ],
3799 ];
3800 }
3801
3802 /**
3803 * Get product gallery ids.
3804 *
3805 * @param object $product Product object.
3806 *
3807 * @return mixed
3808 */
3809 public static function get_cached_gallery_ids( $product ) {
3810 $product_id = $product->get_id();
3811 $cache_key = 'rtsb_product_gallery_ids_' . $product_id;
3812
3813 if ( isset( self::$cache[ $cache_key ] ) ) {
3814 return self::$cache[ $cache_key ];
3815 }
3816
3817 $cached_result = wp_cache_get( $cache_key, 'shopbuilder' );
3818
3819 if ( false !== $cached_result ) {
3820 self::$cache[ $cache_key ] = $cached_result;
3821
3822 return $cached_result;
3823 }
3824
3825 $gallery_ids = $product->get_gallery_image_ids();
3826 self::$cache[ $cache_key ] = $gallery_ids;
3827
3828 wp_cache_set( $cache_key, $gallery_ids, 'shopbuilder', 12 * HOUR_IN_SECONDS );
3829 Cache::set_data_cache_key( $cache_key );
3830
3831 return $gallery_ids;
3832 }
3833
3834 /**
3835 * Check if optimization setting is turned on in the database.
3836 *
3837 * This only checks the user's setting value, without validating
3838 * whether the cache directory is writable.
3839 *
3840 * @return bool
3841 */
3842 public static function is_optimization_setting_on() {
3843 $has_pro = rtsb()->has_pro();
3844 $pro_ver = defined( 'RTSBPRO_VERSION' ) ? RTSBPRO_VERSION : 0;
3845
3846 if ( $has_pro && version_compare( $pro_ver, '2.0.0', '<' ) ) {
3847 return false;
3848 }
3849
3850 $generalList = GeneralList::instance()->get_data();
3851
3852 return 'on' === ( $generalList['optimization']['enable_optimization'] ?? '' );
3853 }
3854
3855 /**
3856 * Check if optimization is enabled and the cache directory is writable.
3857 *
3858 * @return bool
3859 */
3860 public static function is_optimization_enabled() {
3861 if ( ! self::is_optimization_setting_on() ) {
3862 return false;
3863 }
3864
3865 $upload = wp_upload_dir();
3866 $cache_dir = trailingslashit( $upload['basedir'] ) . 'shopbuilder_uploads/cache/';
3867
3868 if ( ! wp_is_writable( $cache_dir ) ) {
3869 return false;
3870 }
3871
3872 return true;
3873 }
3874
3875 /**
3876 * Get the optimized handle.
3877 *
3878 * @param string $handle Base handle used for script/style IDs.
3879 *
3880 * @return string
3881 */
3882 public static function optimized_handle( $handle ) {
3883 $use_optimization = self::is_optimization_enabled();
3884
3885 if ( $use_optimization ) {
3886 $handle = self::is_contextual_loading() ? self::get_optimized_handle_by_context() : 'rtsb-bundled';
3887 }
3888
3889 return $handle;
3890 }
3891
3892 /**
3893 * Get the optimized handle by context.
3894 *
3895 * @return string
3896 */
3897 public static function get_optimized_handle_by_context() {
3898 $context = self::detect_context();
3899
3900 if ( 'account' === $context ) {
3901 return 'rtsb-bundled-global';
3902 }
3903
3904 return "rtsb-bundled-$context";
3905 }
3906
3907 /**
3908 * Check if Elementor page.
3909 *
3910 * @param int $post_id Post ID.
3911 *
3912 * @return bool
3913 */
3914 public static function is_elementor_page( $post_id = null ) {
3915 if ( ! defined( 'ELEMENTOR_VERSION' ) ) {
3916 return false;
3917 }
3918
3919 $post_id = $post_id ?: get_the_ID();
3920
3921 if ( ! $post_id ) {
3922 return true;
3923 }
3924
3925 return Plugin::$instance->documents->get( $post_id )->is_built_with_elementor();
3926 }
3927
3928 /**
3929 * Check if shop or archive.
3930 *
3931 * @return bool
3932 */
3933 public static function is_shop_or_archive() {
3934 return is_shop() || is_product_category() || is_product_tag() || is_post_type_archive( 'product' ) || BuilderFns::is_archive() || BuilderFns::is_shop() || is_tax( 'product_brand' );
3935 }
3936
3937 /**
3938 * Detect context.
3939 *
3940 * @return string
3941 */
3942 public static function detect_context() {
3943 switch ( true ) {
3944 case is_product() || BuilderFns::is_product():
3945 $context = 'product';
3946 break;
3947
3948 case is_cart() || BuilderFns::is_cart():
3949 $context = 'cart';
3950 break;
3951
3952 case is_checkout() || BuilderFns::is_checkout():
3953 $context = 'checkout';
3954 break;
3955
3956 case self::is_shop_or_archive():
3957 $context = 'shop';
3958 break;
3959
3960 default:
3961 $context = 'global';
3962 break;
3963 }
3964
3965 return apply_filters( 'rtsb/optimizer/context_detect', $context );
3966 }
3967
3968 /**
3969 * Enqueue optimized assets.
3970 *
3971 * @return void
3972 */
3973 public static function enqueue_optimized_assets() {
3974 $asset_registry = AssetRegistry::instance();
3975 $bundled_assets = $asset_registry->get_bundled_assets();
3976 $context = self::detect_context();
3977 $contextual_loading = self::is_contextual_loading();
3978 $theme_handle = self::find_theme_stylesheet_handle();
3979
3980 if ( $contextual_loading ) {
3981 // Enqueue JS.
3982 if ( ! empty( $bundled_assets['js'] ) ) {
3983 $js_context = isset( $bundled_assets['js'][ $context ] ) ? $context : 'global';
3984
3985 if ( $js_context ) {
3986 wp_enqueue_script( $bundled_assets['js'][ $js_context ]['handle'] );
3987 }
3988 }
3989
3990 // Enqueue CSS.
3991 if ( ! empty( $bundled_assets['css'] ) ) {
3992 $css_context = isset( $bundled_assets['css'][ $context ] ) ? $context : 'global';
3993
3994 if ( $css_context ) {
3995 $handle = $bundled_assets['css'][ $css_context ]['handle'];
3996
3997 wp_enqueue_style( $handle );
3998
3999 if ( ! empty( $theme_handle ) ) {
4000 self::set_theme_dependency( $theme_handle, $handle );
4001 }
4002 }
4003 }
4004 } else {
4005 // Enqueue JS.
4006 if ( ! empty( $bundled_assets['js'] ) ) {
4007 wp_enqueue_script( $bundled_assets['js']['handle'] );
4008 }
4009
4010 // Enqueue CSS.
4011 if ( ! empty( $bundled_assets['css'] ) ) {
4012 $handle = $bundled_assets['css']['handle'];
4013
4014 wp_enqueue_style( $handle );
4015
4016 if ( ! empty( $theme_handle ) ) {
4017 self::set_theme_dependency( $theme_handle, $handle );
4018 }
4019 }
4020 }
4021 }
4022
4023 /**
4024 * Find theme stylesheet handle.
4025 *
4026 * @return string|false
4027 */
4028 private static function find_theme_stylesheet_handle() {
4029 static $cached_handle = null;
4030
4031 if ( null !== $cached_handle ) {
4032 return $cached_handle;
4033 }
4034
4035 global $wp_styles;
4036
4037 if ( ! $wp_styles instanceof WP_Styles ) {
4038 return false;
4039 }
4040
4041 $stylesheet_uri = get_stylesheet_uri();
4042 $theme_directory_uri = get_stylesheet_directory_uri();
4043
4044 $hash = md5( $stylesheet_uri );
4045 $transient_key = 'rtsb_theme_css_handle_' . $hash;
4046
4047 $transient = get_transient( $transient_key );
4048
4049 if ( false !== $transient ) {
4050 $cached_handle = $transient;
4051
4052 return $transient;
4053 }
4054
4055 foreach ( $wp_styles->registered as $handle => $style ) {
4056 $src = $style->src;
4057
4058 if (
4059 ( $src === $stylesheet_uri || strpos( $src, $theme_directory_uri ) !== false ) &&
4060 strpos( $src, 'style.css' ) !== false
4061 ) {
4062 set_transient( $transient_key, $handle, DAY_IN_SECONDS );
4063 Cache::set_transient_cache_key( $transient_key );
4064 $cached_handle = $handle;
4065
4066 return $handle;
4067 }
4068 }
4069
4070 $filtered_handle = apply_filters( 'rtsb/optimizer/theme_stylesheet_handle', false );
4071
4072 if ( is_string( $filtered_handle ) && ! empty( $filtered_handle ) ) {
4073 set_transient( $transient_key, $filtered_handle, DAY_IN_SECONDS );
4074 Cache::set_transient_cache_key( $transient_key );
4075 $cached_handle = $filtered_handle;
4076
4077 return $filtered_handle;
4078 }
4079
4080 set_transient( $transient_key, false, DAY_IN_SECONDS );
4081 Cache::set_transient_cache_key( $transient_key );
4082 $cached_handle = false;
4083
4084 return false;
4085 }
4086
4087 /**
4088 * Set theme dependency.
4089 *
4090 * @param string $theme_handle Theme handle.
4091 * @param string $our_handle Our handle.
4092 *
4093 * @return void
4094 */
4095 private static function set_theme_dependency( $theme_handle, $our_handle ) {
4096 global $wp_styles;
4097
4098 if ( ! isset( $wp_styles->registered[ $theme_handle ] ) ) {
4099 return;
4100 }
4101
4102 $theme_style = $wp_styles->registered[ $theme_handle ];
4103
4104 // Add our handle as a dependency if it's not already there.
4105 if ( ! in_array( $our_handle, $theme_style->deps, true ) ) {
4106 $theme_style->deps[] = $our_handle;
4107 }
4108 }
4109
4110 /**
4111 * Check if Elementor scripts should be loaded.
4112 *
4113 * @return bool
4114 */
4115 public static function should_load_elementor_scripts() {
4116 $data = GeneralList::instance()->get_data()['optimization'] ?? [];
4117
4118 $enable_optimization = $data['enable_optimization'] ?? 'on';
4119 $load_elementor_scripts = $data['load_elementor_scripts'] ?? 'on';
4120
4121 if ( 'on' !== $enable_optimization ) {
4122 return true;
4123 }
4124
4125 return 'on' === $load_elementor_scripts;
4126 }
4127
4128 /**
4129 * Locate asset.
4130 *
4131 * @param string $relative_path Relative path.
4132 *
4133 * @return string|null
4134 */
4135 public static function locate_asset( $relative_path ) {
4136 $free_context = rtsb();
4137 $pro_context = function_exists( 'rtsbpro' ) && rtsb()->has_pro() ? rtsbpro() : null;
4138
4139 $paths = [];
4140
4141 if ( $pro_context ) {
4142 $paths[] = $pro_context->get_assets_path( $relative_path );
4143 }
4144
4145 $paths[] = $free_context->get_assets_path( $relative_path );
4146
4147 foreach ( $paths as $path ) {
4148 if ( file_exists( $path ) ) {
4149 return $path;
4150 }
4151 }
4152
4153 return null;
4154 }
4155
4156 /**
4157 * Enqueue module assets.
4158 *
4159 * @param string $handle Asset handle.
4160 * @param string $module_name Module name.
4161 * @param array $options Options array: ['type' => 'css|js|both', 'deps' => [], 'context' => null].
4162 *
4163 * @return string
4164 */
4165 public static function enqueue_module_assets( $handle, $module_name, $options = [] ) {
4166 $use_optimization = self::is_optimization_enabled();
4167 $handle = self::optimized_handle( $handle );
4168
4169 if ( $use_optimization ) {
4170 return $handle;
4171 }
4172
4173 $defaults = [
4174 'type' => 'both',
4175 'deps' => [ 'jquery', 'rtsb-public' ],
4176 'context' => null,
4177 'version' => RTSB_VERSION,
4178 ];
4179
4180 $config = array_merge( $defaults, $options );
4181 $rtl_suffix = is_rtl() ? '-rtl' : '';
4182 $rtl_dir = is_rtl() ? trailingslashit( 'rtl' ) : trailingslashit( 'css' );
4183 $context = $config['context'] ?: rtsb();
4184 $load_css = in_array( $config['type'], [ 'css', 'both' ], true );
4185 $load_js = in_array( $config['type'], [ 'js', 'both' ], true );
4186
4187 // Register CSS if enabled.
4188 if ( $load_css ) {
4189 wp_register_style(
4190 $handle,
4191 $context->get_assets_uri( $rtl_dir . 'modules/' . $module_name . $rtl_suffix . '.css' ),
4192 [],
4193 $config['version']
4194 );
4195 }
4196
4197 // Register JS if enabled.
4198 if ( $load_js ) {
4199 wp_register_script(
4200 $handle,
4201 $context->get_assets_uri( 'js/modules/' . $module_name . '.js' ),
4202 $config['deps'],
4203 $config['version'],
4204 true
4205 );
4206 }
4207
4208 if ( $load_css ) {
4209 wp_enqueue_style( $handle );
4210
4211 $theme_handle = self::find_theme_stylesheet_handle();
4212
4213 if ( ! empty( $theme_handle ) ) {
4214 self::set_theme_dependency( $theme_handle, $handle );
4215 }
4216 }
4217
4218 if ( $load_js ) {
4219 wp_enqueue_script( $handle );
4220 }
4221
4222 return $handle;
4223 }
4224
4225 /**
4226 * Check if contextual loading is enabled.
4227 *
4228 * @return bool
4229 */
4230 public static function is_contextual_loading() {
4231 $data = GeneralList::instance()->get_data()['optimization'] ?? [];
4232
4233 return ! empty( $data['context_asset_loading'] ) && 'on' === $data['context_asset_loading'];
4234 }
4235
4236 /**
4237 * Get Modules list with cache support.
4238 *
4239 * @return array
4240 */
4241 public static function get_modules_list() {
4242 static $cached_modules = null;
4243
4244 if ( null !== $cached_modules ) {
4245 return $cached_modules;
4246 }
4247
4248 $cache_key = 'rtsb_module_list';
4249 $cache_group = 'shopbuilder';
4250
4251 $cached = wp_cache_get( $cache_key, $cache_group );
4252
4253 if ( false !== $cached ) {
4254 $cached_modules = $cached;
4255
4256 return $cached;
4257 }
4258
4259 $modules = ModuleList::instance()->get_data();
4260
4261 wp_cache_set( $cache_key, $modules, $cache_group, 12 * HOUR_IN_SECONDS );
4262 Cache::set_data_cache_key( $cache_key );
4263
4264 $cached_modules = $modules;
4265
4266 return $modules;
4267 }
4268
4269 /**
4270 * Get Elementor widgets list with cache support.
4271 *
4272 * @return array
4273 */
4274 public static function get_widgets_list() {
4275 static $cached_widgets = null;
4276
4277 if ( null !== $cached_widgets ) {
4278 return $cached_widgets;
4279 }
4280
4281 $cache_key = 'rtsb_elementor_widget_list';
4282 $cache_group = 'shopbuilder';
4283
4284 $cached = wp_cache_get( $cache_key, $cache_group );
4285
4286 if ( false !== $cached ) {
4287 $cached_widgets = $cached;
4288
4289 return $cached;
4290 }
4291
4292 $widgets = ElementList::instance()->get_list();
4293
4294 wp_cache_set( $cache_key, $widgets, $cache_group, 12 * HOUR_IN_SECONDS );
4295 Cache::set_data_cache_key( $cache_key );
4296
4297 $cached_widgets = $widgets;
4298
4299 return $widgets;
4300 }
4301
4302 /**
4303 * Convert the given price to the active currency.
4304 *
4305 * @param float $price The original price.
4306 * @param Object||null $product Product.
4307 *
4308 * @return float
4309 */
4310 public static function get_currency_base_price( $price, $product = null ) {
4311 return apply_filters( 'rtsb/convert/currency/price', $price, $product );
4312 }
4313 /**
4314 * Generate a signed URL with a payload.
4315 *
4316 * @param array $payload Associative data to encode.
4317 * @param string $base_url Base URL to append ?key=... (e.g. wc_get_checkout_url()).
4318 * @param string $key Key name to use in the URL.
4319 * @return string Signed URL with key parameter.
4320 */
4321 public static function generate_signed_url( array $payload, string $base_url, $key = 'key' ) {
4322 if ( empty( $key ) ) {
4323 $key = 'key';
4324 }
4325 // Convert payload to JSON.
4326 $data = wp_json_encode( $payload );
4327 // Create signature.
4328 $signature = wp_hash( $data );
4329 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
4330 $encode_key = base64_encode( $data . '::' . $signature );
4331 // Return full URL with key param.
4332 return add_query_arg( [ $key => rawurlencode( $encode_key ) ], $base_url );
4333 }
4334
4335 /**
4336 * Decode and verify a signed URL key.
4337 *
4338 * @param string $key Encoded key from URL.
4339 * @return array|false Decoded payload array on success, false on failure.
4340 */
4341 public static function decode_signed_key( string $key ) {
4342 if ( empty( $key ) ) {
4343 return false;
4344 }
4345 $key = rawurldecode( wp_unslash( $key ) );
4346 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
4347 $raw = base64_decode( sanitize_text_field( $key ) );
4348 if ( ! $raw ) {
4349 return false;
4350 }
4351 $parts = explode( '::', $raw, 2 );
4352 if ( count( $parts ) !== 2 ) {
4353 return false;
4354 }
4355 list( $data_json, $signature ) = $parts;
4356 // Validate signature.
4357 if ( ! hash_equals( wp_hash( $data_json ), $signature ) ) {
4358 return false;
4359 }
4360 $payload = json_decode( $data_json, true );
4361 return is_array( $payload ) ? $payload : false;
4362 }
4363
4364 /**
4365 * Get Loco Translate MO file path for a plugin.
4366 *
4367 * @param string $textdomain Plugin textdomain.
4368 * @return void|false
4369 */
4370 public static function load_loco_textdomain( $textdomain ) {
4371 if ( ! function_exists( 'loco_plugin_version' ) ) {
4372 return false;
4373 }
4374 $lang = WP_LANG_DIR;
4375 $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4376 if ( ! file_exists( $path ) && defined( 'LOCO_LANG_DIR' ) ) {
4377 $lang = LOCO_LANG_DIR;
4378 $path = $lang . '/plugins/' . $textdomain . '-' . get_locale() . '.mo';
4379 }
4380 if ( ! file_exists( $path ) ) {
4381 if ( 'shopbuilder' === $textdomain ) {
4382 $plugin_root = dirname( RTSB_FILE );
4383 } elseif ( 'shopbuilder-pro' === $textdomain ) {
4384 $plugin_root = dirname( RTSBPRO_FILE );
4385 } else {
4386 return false;
4387 }
4388 $path = $plugin_root . '/languages/' . $textdomain . '-' . get_locale() . '.mo';
4389 }
4390 if ( ! file_exists( $path ) ) {
4391 return false;
4392 }
4393 load_textdomain( $textdomain, $path );
4394 }
4395 }
4396