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 / Controllers / Hooks / ActionHooks.php

ActionHooks.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Controllers/Hooks/ActionHooks.php

654 lines 24.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main ActionHooks class.
4 *
5 * @package RadiusTheme\SB
6 */
7
8 namespace RadiusTheme\SB\Controllers\Hooks;
9
10 use RadiusTheme\SB\Helpers\Fns;
11 use RadiusTheme\SB\Helpers\Cache;
12 use RadiusTheme\SB\Models\GeneralList;
13 use RadiusTheme\SB\Helpers\BuilderFns;
14 use RadiusTheme\SB\Models\TemplateSettings;
15 use RadiusTheme\SB\Elementor\Render\Render;
16 use RadiusTheme\SB\Elementor\Helper\RenderHelpers;
17 use RadiusTheme\SB\Modules\AbandonedCartRecovery\CartRecoveryCron;
18
19 defined( 'ABSPATH' ) || exit();
20
21 /**
22 * Main ActionHooks class.
23 */
24 class ActionHooks {
25 /**
26 * Init Hooks.
27 *
28 * @return void
29 */
30 public static function init_hooks() {
31 add_action( 'rtsb/before/save/options', [ __CLASS__, 'clear_cron_schedule' ], 10, 3 );
32 // Always register custom cron schedules so already-scheduled rtsb_* events
33 // can reschedule even when the Abandoned Cart Recovery module is inactive,
34 // preventing the "invalid_schedule" (Event schedule does not exist) error.
35 add_filter( 'cron_schedules', [ CartRecoveryCron::instance(), 'abandoned_cart_cron' ] ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
36 add_action( 'wp_head', [ __CLASS__, 'og_metatags_for_sharing' ], 1 );
37 add_action( 'wp_head', [ __CLASS__, 'reveal_builder_content_css' ], 2 );
38 add_filter( 'style_loader_tag', [ __CLASS__, 'defer_non_critical_styles' ], 10, 4 );
39 add_action( 'woocommerce_share', [ __CLASS__, 'shopbuilder_share' ], 20 );
40 add_action( 'rtsb/wcqv/product/summary', [ __CLASS__, 'shopbuilder_share' ], 40 );
41 add_action( 'rtsb/shop/product/image', [ __CLASS__, 'render_image' ], 10, 2 );
42
43 add_action(
44 'admin_init',
45 function () {
46 remove_post_type_support( BuilderFns::$post_type_tb, 'revisions' );
47 }
48 );
49 add_action( 'deleted_post', [ __CLASS__, 'deleted_post' ], 10, 2 );
50 add_action( 'rtsb/elements/render/before_query', [ __CLASS__, 'modify_wc_query_args' ] );
51 add_action( 'rtsb/elements/render/after_query', [ __CLASS__, 'reset_wc_query_args' ] );
52 // Cart Table.
53 remove_action( 'woocommerce_cart_is_empty', 'wc_empty_cart_message', 10 );
54 add_action( 'woocommerce_cart_is_empty', [ __CLASS__, 'cart_is_empty' ], 10 );
55 /**
56 * Notices.
57 */
58 remove_action( 'woocommerce_cart_is_empty', 'woocommerce_output_all_notices', 5 );
59 remove_action( 'woocommerce_shortcode_before_product_cat_loop', 'woocommerce_output_all_notices', 10 );
60 remove_action( 'woocommerce_before_shop_loop', 'woocommerce_output_all_notices', 10 );
61 remove_action( 'woocommerce_before_single_product', 'woocommerce_output_all_notices', 10 );
62 remove_action( 'woocommerce_before_cart', 'woocommerce_output_all_notices', 10 );
63 remove_action( 'woocommerce_before_checkout_form_cart_notices', 'woocommerce_output_all_notices', 10 );
64 remove_action( 'woocommerce_before_checkout_form', 'woocommerce_output_all_notices', 10 );
65 remove_action( 'woocommerce_account_content', 'woocommerce_output_all_notices', 5 );
66 remove_action( 'woocommerce_before_customer_login_form', 'woocommerce_output_all_notices', 10 );
67 remove_action( 'woocommerce_before_lost_password_form', 'woocommerce_output_all_notices', 10 );
68 remove_action( 'before_woocommerce_pay', 'woocommerce_output_all_notices', 10 );
69 remove_action( 'woocommerce_before_reset_password_form', 'woocommerce_output_all_notices', 10 );
70
71 /**
72 * Notices.
73 */
74 add_action( 'woocommerce_shortcode_before_product_cat_loop', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
75 add_action( 'woocommerce_before_shop_loop', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
76 add_action( 'woocommerce_before_single_product', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
77 add_action( 'woocommerce_before_cart', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
78 add_action( 'woocommerce_before_checkout_form_cart_notices', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
79 add_action( 'woocommerce_before_checkout_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
80 add_action( 'woocommerce_account_content', [ Fns::class, 'woocommerce_output_all_notices' ], 5 );
81 add_action( 'woocommerce_before_customer_login_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
82 add_action( 'woocommerce_before_lost_password_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
83 add_action( 'before_woocommerce_pay', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
84 add_action( 'woocommerce_before_reset_password_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
85
86 // Checkout Page.
87 remove_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_login_form', 10 );
88 remove_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form', 10 );
89 add_action( 'woocommerce_before_checkout_form', [ __CLASS__, 'woocommerce_checkout_login_form' ], 10 );
90 add_action( 'woocommerce_before_checkout_form', [ __CLASS__, 'woocommerce_checkout_coupon_form' ], 10 );
91 add_action( 'woocommerce_after_checkout_form', [ __CLASS__, 'postcode_validation_placeholder' ] );
92 // My Account Order Page.
93 add_action( 'woocommerce_before_account_orders', [ __CLASS__, 'before_account_orders' ], 10 );
94 add_action( 'woocommerce_after_account_orders', [ __CLASS__, 'after_account_orders' ], 10 );
95
96 // Product Filter Widget Hooks.
97 add_action( 'rtsb/before/archive/default/filter/items', [ __CLASS__,'default_filter_header' ], 10 );
98 add_action( 'rtsb/before/archive/default/filter/items', [ __CLASS__,'default_filter_search' ], 15 );
99
100 // WC Query modifier. Always registered so the free `rtsb-product-filters`
101 // widget's URL params (product_cat, product_tag, product_brand, rating_filter,
102 // sale_filter) are applied to the main WooCommerce product query regardless
103 // of whether the Pro add-on is active.
104 add_action( 'woocommerce_product_query', [ __CLASS__, 'query_modifier' ], 15 );
105
106 add_action( 'switch_theme', [ __CLASS__, 'clear_cache' ] );
107 }
108 /**
109 * Clear scheduled cron events when a module is deactivated.
110 *
111 * Checks the given block ID against predefined schedules and clears
112 * related cron hooks if the corresponding module is turned off.
113 *
114 * @param string $section_id Section identifier (not used in this function).
115 * @param string $block_id Module/block identifier to check scheduled hooks.
116 * @param array $rawOptions Module options array containing 'active' status.
117 *
118 * @return void
119 */
120 public static function clear_cron_schedule( $section_id, $block_id, $rawOptions ) {
121 $schedule = [
122 'abandoned_cart_recovery' => [
123 'rtsb_abandoned_cart_recovery_action',
124 'rtsb_send_abandoned_cart_emails',
125 ],
126 'currency_switcher' => [
127 'update_exchange_rates_hourly',
128 ],
129 ];
130 if ( ! isset( $schedule[ $block_id ] ) ) {
131 return;
132 }
133 if ( ! isset( $rawOptions['active'] ) ) {
134 return;
135 }
136 if ( 'on' === $rawOptions['active'] ) {
137 return;
138 }
139 // CLear All Custom Schedule If module disabled.
140 foreach ( $schedule[ $block_id ] as $hook ) {
141 wp_clear_scheduled_hook( $hook );
142 }
143 }
144
145 /**
146 * @param boolen $has_orders Order exist or not.
147 *
148 * @return void
149 */
150 public static function after_account_orders( $has_orders ) {
151 if ( ! $has_orders ) {
152 echo '</div>';
153 }
154 }
155
156 /**
157 * @param boolen $has_orders Order exist or not.
158 *
159 * @return void
160 */
161 public static function before_account_orders( $has_orders ) {
162 if ( ! $has_orders ) {
163 echo '<div class="rtsb-notice">';
164 }
165 }
166
167
168 /**
169 * Render postcode validation placeholder for fragment replacement.
170 *
171 * WooCommerce fragment replacement only targets existing DOM elements.
172 * This placeholder ensures the server-side validation flag can be
173 * injected via the update_order_review AJAX response.
174 *
175 * @return void
176 */
177 public static function postcode_validation_placeholder() {
178 echo '<div class="rtsb-postcode-invalid" style="display:none;"></div>';
179 }
180
181 /**
182 * @return void
183 */
184 public static function woocommerce_checkout_coupon_form() {
185 echo '<div class="rtsb-notice">';
186 woocommerce_checkout_coupon_form();
187 echo '</div>';
188 }
189
190 /**
191 * @return void
192 */
193 public static function woocommerce_checkout_login_form() {
194 echo '<div class="rtsb-notice">';
195 woocommerce_checkout_login_form();
196 echo '</div>';
197 }
198
199 /**
200 * @return void
201 */
202 public static function cart_is_empty() {
203 echo '<div class="rtsb-notice">';
204 wc_empty_cart_message();
205 echo '</div>';
206 }
207
208 /**
209 * @param int $post_id Post Ids.
210 * @param object $post Post Object.
211 *
212 * @return void
213 */
214 public static function deleted_post( $post_id, $post ) {
215 if ( BuilderFns::$post_type_tb !== $post->post_type ) {
216 return;
217 }
218
219 $options = BuilderFns::option_name_for_specific_product_set_default( $post_id );
220 TemplateSettings::instance()->delete_option( $options );
221
222 $template_options = BuilderFns::option_name_by_template_id( $post_id );
223 TemplateSettings::instance()->delete_option( $template_options );
224
225 $archive_options = BuilderFns::archive_option_name_by_template_id( $post_id );
226 TemplateSettings::instance()->delete_option( $archive_options );
227
228 $category_options = BuilderFns::option_name_for_specific_category_set_default( $post_id );
229 TemplateSettings::instance()->delete_option( $category_options );
230
231 $selected_category_options = BuilderFns::archive_option_name_by_template_id( $post_id );
232 TemplateSettings::instance()->delete_option( $selected_category_options );
233
234 // Delete Data From Options.
235 $options_product_selected_cat = BuilderFns::option_name_product_page_selected_cat( $post_id );
236 $cat_list = TemplateSettings::instance()->get_option( $options_product_selected_cat );
237 if ( ! empty( $cat_list ) ) {
238 foreach ( $cat_list as $old_slug ) {
239 $OldcatList = get_term_by( 'slug', $old_slug, 'product_cat' );
240 if ( ! empty( $OldcatList ) && ! is_wp_error( $OldcatList ) ) {
241 delete_term_meta( $OldcatList->term_id, BuilderFns::$categories_product_page_template, $post_id );
242 }
243 }
244 }
245 TemplateSettings::instance()->delete_option( $options_product_selected_cat );
246
247 $set_default_option = BuilderFns::option_name_product_page_specific_cat_set_default( $post_id );
248 TemplateSettings::instance()->delete_option( $set_default_option );
249
250 // Tags Remove.
251 $options_product_selected_tag = BuilderFns::option_name_product_page_selected_tag( $post_id );
252 $tag_list = TemplateSettings::instance()->get_option( $options_product_selected_tag );
253 if ( ! empty( $tag_list ) ) {
254 foreach ( $tag_list as $old_slug ) {
255 $OldTagList = get_term_by( 'slug', $old_slug, 'product_tag' );
256 if ( ! empty( $OldTagList ) && ! is_wp_error( $OldTagList ) ) {
257 delete_term_meta( $OldTagList->term_id, BuilderFns::$tags_product_page_template, $post_id );
258 }
259 }
260 }
261 TemplateSettings::instance()->delete_option( $options_product_selected_tag );
262
263 // Tag Set Default Option.
264 $tag_set_default_option = BuilderFns::option_name_product_page_specific_tag_set_default( $post_id );
265 TemplateSettings::instance()->delete_option( $tag_set_default_option );
266
267 global $wpdb;
268 // Prepare and execute the SQL query.
269 $sql = $wpdb->prepare( "DELETE FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %d", BuilderFns::$view_template_for_products_meta, $post_id );
270 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
271
272 // Prepare and execute the SQL query.
273 $sql = $wpdb->prepare( "DELETE FROM $wpdb->termmeta WHERE meta_key = %s AND meta_value = %d", BuilderFns::$view_template_for_archive_meta, $post_id );
274 $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
275 }
276 /**
277 * Meta tags for social sharing.
278 *
279 * @return void
280 */
281 public static function shopbuilder_share() {
282 $generalList = GeneralList::instance()->get_data();
283
284 if (
285 empty( $generalList['social_share']['share_platforms_to_product_page'] ) ||
286 'on' !== $generalList['social_share']['share_platforms_to_product_page'] ||
287 empty( $generalList['social_share']['share_platforms'] )
288 ) {
289 return;
290 }
291
292 $share_platforms = [];
293
294 foreach ( $generalList['social_share']['share_platforms'] as $value ) {
295 $share_platforms[ $value ] = [
296 'share_items' => $value,
297 'share_text' => ucwords( $value ),
298 ];
299 }
300
301 $settings['share_platforms'] = $share_platforms;
302 $settings['layout'] = ! empty( $generalList['social_share']['share_icon_layout'] ) ? $generalList['social_share']['share_icon_layout'] : 1;
303 $settings['show_share_icon'] = 1;
304
305 /**
306 * Implement Later ! empty( $generalList['social_share']['share_icon_show_to_product_page'] ) && 'on' === $generalList['social_share']['share_icon_show_to_product_page'];
307 */
308 Fns::print_html( Render::instance()->social_share_view( 'elementor/general/social-share', $settings ), true );
309 }
310
311 /**
312 * Meta tags for social sharing.
313 *
314 * @return void
315 */
316 public static function og_metatags_for_sharing() {
317 global $post;
318
319 if ( ! isset( $post ) ) {
320 return;
321 }
322
323 if ( ! is_singular( 'product' ) ) {
324 return;
325 }
326
327 Fns::print_html( '<meta property="og:url" content="' . get_the_permalink() . '" />', true );
328 Fns::print_html( '<meta property="og:type" content="article" />', true );
329 Fns::print_html( '<meta property="og:title" content="' . $post->post_title . '" />', true );
330 Fns::print_html( '<meta property="og:description" content="' . wp_trim_words( $post->post_content, 150 ) . '" />' );
331
332 $attachment = get_the_post_thumbnail_url();
333
334 if ( ! empty( $attachment ) ) {
335 Fns::print_html( '<meta property="og:image" content="' . $attachment . '" />', true );
336 }
337
338 Fns::print_html( '<meta property="og:site_name" content="' . get_bloginfo( 'name' ) . '" />', true );
339 Fns::print_html( '<meta name="twitter:card" content="summary" />', true );
340 }
341
342 /**
343 * Reveal the builder content via a CSS-only failsafe.
344 *
345 * The builder wrapper is hidden by `.content-invisible`
346 * (visibility:hidden;opacity:0) to avoid a flash of unstyled content, and is
347 * normally un-hidden by JavaScript on DOMContentLoaded. On mobile that event
348 * waits for the ShopBuilder JS bundle to download+parse, so the already-loaded
349 * LCP product image cannot paint until the script runs — inflating LCP
350 * "Element Render Delay".
351 *
352 * This inlines a tiny keyframe in the <head> that reveals the content from
353 * CSS alone, capping the hidden window (default 200ms) regardless of when the
354 * JS runs. First paint is therefore driven by HTML + CSS, not JavaScript. The
355 * existing JS reveal still fires and simply removes the class earlier on fast
356 * connections. Only emitted while the gate is active.
357 *
358 * @return void
359 */
360 public static function reveal_builder_content_css() {
361 if ( is_admin() || ! Fns::content_invisible() ) {
362 return;
363 }
364
365 /**
366 * APPLY_FILTERS: rtsb/builder/content_reveal_delay
367 *
368 * Maximum time (ms) the builder content may stay hidden before the CSS
369 * failsafe reveals it, independent of JavaScript. Lower = earlier paint,
370 * higher = longer anti-FOUC window.
371 *
372 * @param int $delay Delay in milliseconds. Default 200.
373 */
374 $delay = (int) apply_filters( 'rtsb/builder/content_reveal_delay', 200 );
375
376 $css = '.rtsb-builder-content.content-invisible{animation:rtsb-reveal-builder-content 1ms linear ' . absint( $delay ) . 'ms forwards}'
377 . '@keyframes rtsb-reveal-builder-content{to{visibility:visible;opacity:1}}';
378
379 Fns::print_html( '<style id="rtsb-content-reveal">' . $css . '</style>', true );
380 }
381
382 /**
383 * Make non-critical ShopBuilder stylesheets non-render-blocking.
384 *
385 * Certain stylesheets (e.g. the icon font) are not required to paint the
386 * first viewport, yet they sit in the <head> as render-blocking <link>s and
387 * delay FCP. This loads them with `media="print"` and swaps to `all` on load,
388 * with a <noscript> fallback so nothing breaks without JavaScript.
389 *
390 * @param string $tag The <link> tag for the enqueued style.
391 * @param string $handle The style handle.
392 * @param string $href The stylesheet source URL.
393 * @param string $media The media attribute.
394 *
395 * @return string
396 */
397 public static function defer_non_critical_styles( $tag, $handle, $href, $media ) {
398 if ( is_admin() ) {
399 return $tag;
400 }
401
402 /**
403 * APPLY_FILTERS: rtsb/frontend/non_critical_styles
404 *
405 * Handles of ShopBuilder stylesheets to load non-render-blocking. Only
406 * include styles that are NOT needed to paint the first viewport.
407 *
408 * @param array $handles Style handles. Default: icon font.
409 */
410 $handles = (array) apply_filters( 'rtsb/frontend/non_critical_styles', [ 'rtsb-fonts' ] );
411
412 if ( ! in_array( $handle, $handles, true ) || false !== strpos( $tag, "media='print'" ) ) {
413 return $tag;
414 }
415
416 $deferred = preg_replace(
417 "/media=(['\"])" . preg_quote( $media, '/' ) . "\\1/",
418 'media="print" onload="this.media=\'all\';this.onload=null;"',
419 $tag
420 );
421
422 // Fallback to the original blocking tag when JavaScript is unavailable.
423 return $deferred . '<noscript>' . $tag . '</noscript>';
424 }
425
426 /**
427 * Render image.
428 *
429 * @param array $args Image args.
430 *
431 * @return void
432 */
433 public static function render_image( $args ) {
434 if ( $args['image_link'] ) {
435 $aria_label = esc_attr(
436 /* translators: Product Name */
437 sprintf( __( 'Image link for Product: %s', 'shopbuilder' ), $args['title'] )
438 );
439 ?>
440 <figure>
441 <a class="woocommerce-LoopProduct-link rtsb-img-link" href="<?php echo esc_url( $args['p_link'] ); ?>" aria-label="<?php echo esc_attr( $aria_label ); ?>">
442 <?php
443 Fns::get_product_image( $args['img_html'], $args['hover_img_html'] );
444 ?>
445 </a>
446 </figure>
447 <?php
448 } else {
449 echo '<figure class="rtsb-img-link">';
450 Fns::get_product_image( $args['img_html'], $args['hover_img_html'] );
451 echo '</figure>';
452 }
453 }
454
455 /**
456 * Modify WooCommerce query arguments for product retrieval.
457 *
458 * @param array $settings The settings array.
459 *
460 * @return void
461 */
462 public static function modify_wc_query_args( $settings ) {
463 if ( ( isset( $settings['tax_relation'] ) && 'OR' === $settings['tax_relation'] ) ||
464 ( isset( $settings['relation'] ) && 'OR' === $settings['relation'] ) ) {
465 add_filter( 'woocommerce_product_data_store_cpt_get_products_query', [ FilterHooks::class, 'extra_query_args_or' ], 10, 2 );
466 }
467 if ( ( isset( $settings['tax_relation'] ) && 'AND' === $settings['tax_relation'] ) ||
468 ( isset( $settings['relation'] ) && 'AND' === $settings['relation'] ) ) {
469 add_filter( 'woocommerce_product_data_store_cpt_get_products_query', [ FilterHooks::class, 'extra_query_args_and' ], 10, 2 );
470 }
471
472 if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
473 add_filter( 'woocommerce_product_data_store_cpt_get_products_query', [ FilterHooks::class, 'extra_query_args_outofstock' ] );
474 }
475 }
476
477 /**
478 * Reset WooCommerce query arguments for product retrieval.
479 *
480 * @param array $settings The settings array.
481 *
482 * @return void
483 */
484 public static function reset_wc_query_args( $settings ) {
485 if ( ( isset( $settings['tax_relation'] ) && 'OR' === $settings['tax_relation'] ) ||
486 ( isset( $settings['relation'] ) && 'OR' === $settings['relation'] ) ) {
487 remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', [ FilterHooks::class, 'extra_query_args_or' ] );
488 }
489 if ( ( isset( $settings['tax_relation'] ) && 'AND' === $settings['tax_relation'] ) ||
490 ( isset( $settings['relation'] ) && 'AND' === $settings['relation'] ) ) {
491 remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', [ FilterHooks::class, 'extra_query_args_and' ] );
492 }
493
494 if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
495 remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', [ FilterHooks::class, 'extra_query_args_outofstock' ] );
496 }
497 }
498 /**
499 * Display a filter header.
500 *
501 * @param array $settings The settings.
502 *
503 * @return void
504 */
505 public static function default_filter_header( $settings ) {
506 if ( empty( $settings['show_filter_header'] ) && empty( $settings['filter_header_text'] ) ) {
507 return;
508 }
509
510 $wrapper_class = 'rtsb-default-filter-header ';
511
512 ?>
513
514 <div class="<?php echo esc_attr( $wrapper_class ); ?>">
515 <h3><?php echo esc_html( $settings['filter_header_text'] ); ?></h3>
516 </div>
517
518 <?php
519 }
520 /**
521 * Display a filter search form.
522 *
523 * @param array $settings The settings.
524 *
525 * @return void
526 */
527 public static function default_filter_search( $settings ) {
528 if ( empty( $settings['search_form'] ) ) {
529 return;
530 }
531
532 $title = [
533 'title' => ! empty( $settings['search_title'] ) ? $settings['search_title'] : '',
534 'show_icon' => ! empty( $settings['show_search_title_icon'] ),
535 'icon' => ! empty( $settings['filter_search_title_icon'] ) ? $settings['filter_search_title_icon'] : [],
536 ];
537 $placeholder = ! empty( $settings['search_placeholder'] ) ? $settings['search_placeholder'] : '';
538 $btn_icon = ! empty( $settings['search_title_icon'] ) ? $settings['search_title_icon'] : [];
539 $wrapper_class = 'rtsb-default-filter-search rtsb-default-product-search';
540 $unique_id = substr( uniqid(), -6 );
541 $search_term = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
542 ?>
543
544 <div class="<?php echo esc_attr( $wrapper_class ); ?>">
545 <?php
546 Fns::print_html( RenderHelpers::get_default_filter_title( $title ) );
547 ?>
548 <div class="default-search-content">
549 <div class="woocommerce-product-search<?php echo ! empty( $search_term ) ? ' active' : ''; ?>">
550 <div class="search-input">
551 <label class="screen-reader-text" for="rtsb-product-default-search-form-<?php echo esc_attr( $unique_id ); ?>"><?php echo esc_html( $placeholder ); ?></label>
552 <input type="search" id="rtsb-product-default-search-form-<?php echo esc_attr( $unique_id ); ?>" class="rtsb-default-search-field" placeholder="<?php echo esc_html( $placeholder ); ?>" value="<?php echo esc_attr( $search_term ); ?>" name="s" autocomplete="off">
553 </div>
554 <button class="rtsb-search-submit">
555 <?php
556
557 Fns::print_html( Fns::icons_manager( $btn_icon, 'search-icon' ) );
558
559 ?>
560 <span></span>
561 </button>
562 </div>
563 </div>
564 </div>
565
566 <?php
567 }
568 /**
569 * Query modifier.
570 *
571 * @param object $query Query.
572 * @return object
573 */
574 public static function query_modifier( $query ) {
575 // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
576
577 // Sale filter.
578 $onsale_filter = isset( $_GET['sale_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['sale_filter'] ) ) : '';
579 $on_sale_products = wc_get_product_ids_on_sale();
580
581 if ( 'onsale' === $onsale_filter ) {
582 $query->set( 'post__in', ! empty( $on_sale_products ) ? $on_sale_products : [ 0 ] );
583 } elseif ( 'regular' === $onsale_filter ) {
584 $query->set( 'post__not_in', $on_sale_products );
585 }
586
587 // Taxonomy filters (product_cat, product_tag, product_brand) from URL.
588 $tax_query = (array) $query->get( 'tax_query' );
589
590 $taxonomy_map = [
591 'product_cat' => 'product_cat',
592 'product_tag' => 'product_tag',
593 'product_brand' => 'product_brand',
594 ];
595
596 foreach ( $taxonomy_map as $param => $taxonomy ) {
597 if ( empty( $_GET[ $param ] ) || ! taxonomy_exists( $taxonomy ) ) {
598 continue;
599 }
600
601 $raw_values = wc_clean( wp_unslash( $_GET[ $param ] ) );
602 $slugs = array_filter( array_map( 'urldecode', explode( ',', $raw_values ) ) );
603
604 if ( empty( $slugs ) ) {
605 continue;
606 }
607
608 $tax_query[] = [
609 'taxonomy' => $taxonomy,
610 'field' => 'slug',
611 'terms' => $slugs,
612 'operator' => 'IN',
613 ];
614 }
615
616 // Rating filter (rating_filter=4,5).
617 if ( ! empty( $_GET['rating_filter'] ) ) {
618 $raw_ratings = wc_clean( wp_unslash( $_GET['rating_filter'] ) );
619 $ratings = array_filter( array_map( 'intval', explode( ',', $raw_ratings ) ) );
620
621 if ( ! empty( $ratings ) ) {
622 $rating_terms = [];
623 foreach ( $ratings as $rating ) {
624 $rating_terms[] = 'rated-' . $rating;
625 }
626
627 $tax_query[] = [
628 'taxonomy' => 'product_visibility',
629 'field' => 'name',
630 'terms' => $rating_terms,
631 'operator' => 'IN',
632 ];
633 }
634 }
635
636 if ( ! empty( $tax_query ) ) {
637 $query->set( 'tax_query', $tax_query );
638 }
639
640 // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
641
642 return $query;
643 }
644
645 /**
646 * Delete cache when switching to another theme.
647 *
648 * @return void
649 */
650 public static function clear_cache() {
651 Cache::clear_all_cache();
652 }
653 }
654