PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.2
ShopBuilder – WooCommerce Builder For Elementor v3.4.2
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.2, at app/Helpers/Fns.php

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