PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.9.6
Subscriptions for WooCommerce with Stripe Recurring Payments v1.9.6
2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / includes / functions.php

functions.php in Subscriptions for WooCommerce with Stripe Recurring Payments 1.9.6, at includes/functions.php

746 lines 22.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
4 use SpringDevs\Subscription\Illuminate\Subscription\Subscription;
5 use SpringDevs\Subscription\Utils\Product;
6
7 /**
8 * Include tailwind CSS file
9 *
10 * * Add this class to the parent element to apply tailwind css styles:
11 * * "`wpsubs-tw-root`"
12 * *
13 * * Use "`yarn build:tailwind`" to build the tailwind CSS file.
14 * * Use "`yarn watch:tailwind`" to continuously build the tailwind CSS file.
15 */
16 function subscrpt_include_tailwind_css() {
17 wp_enqueue_style( 'wpsubs-tailwind', SUBSCRPT_ASSETS . '/css/tailwind/output.css', [], SUBSCRPT_VERSION );
18 }
19
20 /**
21 * Generate URL for Subscription Action.
22 *
23 * @param string $action Action.
24 * @param string $nonce nonce.
25 * @param int $subscription_id Subscription ID.
26 *
27 * @return string
28 */
29 function subscrpt_get_action_url( $action, $nonce, $subscription_id ) {
30 $view_subscription_endpoint = Subscription::get_user_endpoint( 'view_subs' );
31 return add_query_arg(
32 array(
33 'subscrpt_id' => $subscription_id,
34 'action' => $action,
35 'wpnonce' => $nonce,
36 ),
37 wc_get_endpoint_url( $view_subscription_endpoint, $subscription_id, wc_get_page_permalink( 'myaccount' ) )
38 );
39 }
40
41
42 /**
43 * Get typos.
44 *
45 * @param int $number Number.
46 * @param string $typo Typo.
47 *
48 * @return string
49 */
50 function subscrpt_get_typos( $number, $typo ) {
51 if ( $number == 1 && $typo == 'days' ) {
52 return ucfirst( __( 'day', 'subscription' ) );
53 } elseif ( $number == 1 && $typo == 'weeks' ) {
54 return ucfirst( __( 'week', 'subscription' ) );
55 } elseif ( $number == 1 && $typo == 'months' ) {
56 return ucfirst( __( 'month', 'subscription' ) );
57 } elseif ( $number == 1 && $typo == 'years' ) {
58 return ucfirst( __( 'year', 'subscription' ) );
59 } else {
60 return ucfirst( $typo );
61 }
62 }
63
64 /**
65 * Format time with trial.
66 *
67 * @param mixed $time Time.
68 * @param null|string $trial Trial.
69 *
70 * @return string
71 */
72 function subscrpt_next_date( $time, $trial = null ) {
73 if ( null === $trial ) {
74 $start_date = time();
75 } else {
76 $start_date = strtotime( $trial );
77 }
78
79 return gmdate( 'F d, Y', strtotime( $time, $start_date ) );
80 }
81
82 /**
83 * Check if subscription-pro activated.
84 *
85 * @return bool
86 */
87 function subscrpt_pro_activated(): bool {
88 return class_exists( 'Sdevs_Wc_Subscription_Pro' );
89 }
90
91 /**
92 * Get renewal process settings.
93 *
94 * @return bool
95 */
96 function subscrpt_is_auto_renew_enabled() {
97 return 'auto' === get_option( 'subscrpt_renewal_process', 'auto' );
98 }
99
100 /**
101 * Get maximum payments for a subscription, checking variation, product, and subscription meta.
102 *
103 * @param int $subscription_id Subscription ID.
104 * @return string|int Maximum payments or empty string if not set.
105 */
106 function subscrpt_get_max_payments( $subscription_id ) {
107 $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true );
108 if ( ! $product_id ) {
109 return '';
110 }
111
112 $max_payments = null;
113
114 // Check for variation first
115 $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true );
116 if ( $variation_id ) {
117 $max_payments = get_post_meta( $variation_id, '_subscrpt_max_no_payment', true );
118 }
119
120 // Fallback to product if variation doesn't have max payments or no variation
121 if ( ! $max_payments ) {
122 $max_payments = get_post_meta( $product_id, '_subscrpt_max_no_payment', true );
123 }
124
125 // Also check subscription's own meta data as final fallback
126 if ( ! $max_payments ) {
127 $max_payments = get_post_meta( $subscription_id, '_subscrpt_max_no_payment', true );
128 }
129
130 return $max_payments ?: '';
131 }
132
133 /**
134 * Count total payments made.
135 *
136 * @param int $subscription_id Subscription ID.
137 * @return int Number of payments made.
138 */
139 function subscrpt_count_payments_made( $subscription_id ) {
140 global $wpdb;
141
142 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
143
144 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
145 $relations = $wpdb->get_results(
146 $wpdb->prepare(
147 "SELECT sr.*, p.post_status, p.post_date
148 FROM $table_name sr
149 INNER JOIN {$wpdb->posts} p ON sr.order_id = p.ID
150 WHERE sr.subscription_id = %d
151 ORDER BY p.post_date ASC",
152 $subscription_id
153 )
154 );
155 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
156
157 // Define all payment-related order types (allow filtering for extensibility)
158 $payment_types = apply_filters( 'subscrpt_payment_order_types', array( 'new', 'renew', 'early-renew' ) );
159
160 // Count successful payments
161 $successful_count = 0;
162 foreach ( $relations as $relation ) {
163 // Count all payment-related types
164 if ( in_array( $relation->type, $payment_types ) ) {
165 // Get the actual WooCommerce order
166 $order = wc_get_order( $relation->order_id );
167 if ( $order ) {
168 // Check if order was paid/successful
169 if ( $order->is_paid() || in_array( $order->get_status(), array( 'completed', 'processing', 'on-hold' ) ) ) {
170 ++$successful_count;
171 }
172 }
173 }
174 }
175
176 return $successful_count;
177 }
178
179 /**
180 * Check if subscription has reached its maximum payment limit.
181 *
182 * @param int $subscription_id Subscription ID.
183 * @return bool True if limit reached, false otherwise.
184 */
185 function subscrpt_is_max_payments_reached( $subscription_id ) {
186 // Get the product ID from subscription
187 $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true );
188 if ( ! $product_id ) {
189 return false;
190 }
191
192 // Get maximum payments using helper function
193 $max_payments = subscrpt_get_max_payments( $subscription_id );
194
195 // Allow override of total installments
196 $max_payments = apply_filters( 'subscrpt_split_payment_total_override', $max_payments, $subscription_id, $product_id );
197
198 // If no limit set or unlimited, more payments are allowed
199 if ( ! $max_payments || intval( $max_payments ) <= 0 ) {
200 return false;
201 }
202
203 // Count payments made
204 $payments_made = subscrpt_count_payments_made( $subscription_id );
205
206 // Enhanced completion logic considering failed payments
207 $is_reached = subscrpt_check_enhanced_completion( $subscription_id, $payments_made, $max_payments );
208
209 // Fire action when split payment plan is completed (first time only)
210 if ( $is_reached && ! get_post_meta( $subscription_id, '_subscrpt_split_payment_completed_fired', true ) ) {
211 // Add completion milestone note
212 subscrpt_add_payment_completion_note( $subscription_id, $payments_made, $max_payments );
213
214 // Allow customization of subscription status after completion
215 $expire_status = apply_filters( 'subscrpt_split_payment_expire_status', 'expired', $subscription_id, $payments_made, $max_payments );
216
217 // Update subscription status if different from current
218 $current_status = get_post_status( $subscription_id );
219 if ( $current_status !== $expire_status ) {
220 wp_update_post(
221 array(
222 'ID' => $subscription_id,
223 'post_status' => $expire_status,
224 )
225 );
226 }
227
228 do_action( 'subscrpt_split_payment_completed', $subscription_id, $payments_made, $max_payments );
229 update_post_meta( $subscription_id, '_subscrpt_split_payment_completed_fired', true );
230
231 // Handle split payment access timing if Pro version is active
232 if ( function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated() ) {
233 if ( class_exists( '\SpringDevs\SubscriptionPro\Illuminate\SplitPaymentHandler' ) ) {
234 \SpringDevs\SubscriptionPro\Illuminate\SplitPaymentHandler::handle_split_payment_completion( $subscription_id, $payments_made, $max_payments );
235 }
236 }
237 }
238
239 return $is_reached;
240 }
241
242 /**
243 * Get remaining payments for a subscription.
244 *
245 * @param int $subscription_id Subscription ID.
246 * @return int|string Number of remaining payments or 'unlimited'.
247 */
248 function subscrpt_get_remaining_payments( $subscription_id ) {
249 // Get maximum payments using helper function
250 $max_payments = subscrpt_get_max_payments( $subscription_id );
251
252 // If no limit set or unlimited
253 if ( ! $max_payments || intval( $max_payments ) <= 0 ) {
254 return 'unlimited';
255 }
256
257 // Count payments made
258 $payments_made = subscrpt_count_payments_made( $subscription_id );
259
260 // Calculate remaining
261 $remaining = intval( $max_payments ) - intval( $payments_made );
262
263 return max( 0, $remaining );
264 }
265
266 /**
267 * Get payment type for a subscription (handles variations properly).
268 *
269 * @param int $subscription_id Subscription ID.
270 * @return string Payment type ('split_payment' or 'recurring').
271 */
272 function subscrpt_get_payment_type( $subscription_id ) {
273 $product_id = get_post_meta( $subscription_id, '_subscrpt_product_id', true );
274 $variation_id = get_post_meta( $subscription_id, '_subscrpt_variation_id', true );
275
276 $payment_type = 'recurring'; // Default
277
278 // Check variation first if it exists
279 if ( $variation_id ) {
280 $variation_payment_type = get_post_meta( $variation_id, '_subscrpt_payment_type', true );
281 if ( $variation_payment_type ) {
282 $payment_type = $variation_payment_type;
283 }
284 }
285
286 // Fallback to product if no variation payment type
287 if ( $payment_type === 'recurring' && $product_id ) {
288 $product_payment_type = get_post_meta( $product_id, '_subscrpt_payment_type', true );
289 if ( $product_payment_type ) {
290 $payment_type = $product_payment_type;
291 }
292 }
293
294 // Final fallback: check subscription's own meta data
295 if ( $payment_type === 'recurring' ) {
296 $subscription_payment_type = get_post_meta( $subscription_id, '_subscrpt_payment_type', true );
297 if ( $subscription_payment_type ) {
298 $payment_type = $subscription_payment_type;
299 }
300 }
301
302 return $payment_type;
303 }
304
305 /**
306 * Enhanced completion check considering failed payments and access suspension.
307 *
308 * @param int $subscription_id Subscription ID.
309 * @param int $payments_made Number of successful payments made.
310 * @param int $max_payments Maximum payments required.
311 * @return bool True if subscription should be considered complete.
312 */
313 function subscrpt_check_enhanced_completion( $subscription_id, $payments_made, $max_payments ) {
314 // Standard completion check
315 if ( $payments_made >= $max_payments ) {
316 return true;
317 }
318
319 // Check for access suspension due to payment failures
320 if ( function_exists( '\SpringDevs\SubscriptionPro\Illuminate\PaymentFailureHandler::is_access_suspended' ) ) {
321 $is_suspended = \SpringDevs\SubscriptionPro\Illuminate\PaymentFailureHandler::is_access_suspended( $subscription_id );
322 if ( $is_suspended ) {
323 // If access is suspended, check if we should force completion
324 $force_completion_on_suspension = apply_filters( 'subscrpt_force_completion_on_suspension', false, $subscription_id );
325 if ( $force_completion_on_suspension ) {
326 return true;
327 }
328 }
329 }
330
331 // Check for maximum failure threshold
332 $failure_count = get_post_meta( $subscription_id, '_subscrpt_payment_failure_count', true ) ?: 0;
333 $max_failures_before_completion = apply_filters( 'subscrpt_max_failures_before_completion', 0, $subscription_id );
334
335 if ( $max_failures_before_completion > 0 && $failure_count >= $max_failures_before_completion ) {
336 // Force completion after too many failures
337 return true;
338 }
339
340 // Check for time-based completion (e.g., if too much time has passed)
341 $completion_timeout_days = apply_filters( 'subscrpt_completion_timeout_days', 0, $subscription_id );
342 if ( $completion_timeout_days > 0 ) {
343 $start_date = get_post_meta( $subscription_id, '_subscrpt_start_date', true );
344 if ( $start_date ) {
345 $timeout_timestamp = $start_date + ( $completion_timeout_days * DAY_IN_SECONDS );
346 if ( current_time( 'timestamp' ) >= $timeout_timestamp ) {
347 return true;
348 }
349 }
350 }
351
352 return false;
353 }
354
355 /**
356 * Count total payment attempts (including failed ones) for a subscription.
357 *
358 * @param int $subscription_id Subscription ID.
359 * @return array Array with 'successful', 'failed', and 'total' counts.
360 */
361 function subscrpt_count_all_payment_attempts( $subscription_id ) {
362 global $wpdb;
363
364 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
365
366 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
367 $relations = $wpdb->get_results(
368 $wpdb->prepare(
369 "SELECT sr.*, p.post_status, p.post_date
370 FROM $table_name sr
371 INNER JOIN {$wpdb->posts} p ON sr.order_id = p.ID
372 WHERE sr.subscription_id = %d
373 ORDER BY p.post_date ASC",
374 $subscription_id
375 )
376 );
377 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
378
379 // Define all payment-related order types
380 $payment_types = apply_filters( 'subscrpt_payment_order_types', array( 'new', 'renew', 'early-renew' ) );
381
382 $successful_count = 0;
383 $failed_count = 0;
384
385 foreach ( $relations as $relation ) {
386 if ( in_array( $relation->type, $payment_types ) ) {
387 $order = wc_get_order( $relation->order_id );
388 if ( $order ) {
389 if ( $order->is_paid() || in_array( $order->get_status(), array( 'completed', 'processing', 'on-hold' ) ) ) {
390 ++$successful_count;
391 } elseif ( in_array( $order->get_status(), array( 'failed', 'cancelled' ) ) ) {
392 ++$failed_count;
393 }
394 }
395 }
396 }
397
398 return array(
399 'successful' => $successful_count,
400 'failed' => $failed_count,
401 'total' => $successful_count + $failed_count,
402 );
403 }
404
405 if ( ! function_exists( 'wps_subscription_order_relation_type_cast' ) ) {
406 /**
407 * Return Label against key.
408 *
409 * @param string $key Key to return cast Value.
410 *
411 * @return string
412 */
413 function order_relation_type_cast( string $key ) {
414 // add Deprecated notice
415 _deprecated_function( 'order_relation_type_cast', '1.5.3', 'wps_subscription_order_relation_type_cast' );
416 return wps_subscription_order_relation_type_cast( $key );
417 }
418 /**
419 * Order relation type cast.
420 *
421 * @param string $key Key.
422 *
423 * @return string
424 */
425 function wps_subscription_order_relation_type_cast( string $key ) {
426 $relational_type_keys = apply_filters(
427 'subscrpt_order_relational_types',
428 array(
429 'new' => __( 'New Subscription Order', 'subscription' ),
430 'renew' => __( 'Renewal Order', 'subscription' ),
431 )
432 );
433
434 return isset( $relational_type_keys[ $key ] ) ? $relational_type_keys[ $key ] : '-';
435 }
436 }
437
438 if ( ! function_exists( 'wps_subscription_is_wc_order_hpos_enabled' ) ) {
439 /**
440 * Check if HPOS enabled.
441 */
442 function is_wc_order_hpos_enabled() {
443 // add Deprecated notice
444 _deprecated_function( 'is_wc_order_hpos_enabled', '1.5.3', 'wps_subscription_is_wc_order_hpos_enabled' );
445 return wps_subscription_is_wc_order_hpos_enabled();
446 }
447 /**
448 * Check if HPOS enabled.
449 *
450 * @return bool
451 */
452 function wps_subscription_is_wc_order_hpos_enabled() {
453 return function_exists( 'wc_get_container' ) ?
454 wc_get_container()
455 ->get( CustomOrdersTableController::class )
456 ->custom_orders_table_usage_is_enabled()
457 : false;
458 }
459 }
460
461 if ( ! function_exists( 'sdevs_wp_strtotime' ) ) {
462 /**
463 * Get strtotime with WordPress timezone config.
464 *
465 * @param string $str string.
466 * @param int|null $base_timestamp base timestamp.
467 *
468 * @return int
469 */
470 function sdevs_wp_strtotime( $str, $base_timestamp = null ) {
471 return strtotime( wp_date( 'Y-m-d H:i:s', strtotime( $str, $base_timestamp ) ) );
472 }
473 }
474
475 if ( ! function_exists( 'sdevs_order_status_label' ) ) {
476 /**
477 * Get order status label from slug.
478 *
479 * @param string $status Status.
480 *
481 * @return string
482 */
483 function sdevs_order_status_label( $status ) {
484 $order_statuses = wc_get_order_statuses();
485
486 return ( isset( $order_statuses[ "wc-{$status}" ] ) ? $order_statuses[ "wc-{$status}" ] : $status );
487 }
488 }
489
490 if ( ! function_exists( 'wps_subscription_get_timing_types' ) ) {
491 /**
492 * Get labels.
493 *
494 * @param bool $key_value key_value.
495 *
496 * @return array
497 */
498 function get_timing_types( $key_value = false ): array {
499 // add Deprecated notice
500 _deprecated_function( 'get_timing_types', '1.5.3', 'wps_subscription_get_timing_types' );
501 return wps_subscription_get_timing_types( $key_value );
502 }
503 /**
504 * Get timing types.
505 *
506 * @param bool $key_value Key value.
507 *
508 * @return array
509 */
510 function wps_subscription_get_timing_types( $key_value = false ): array {
511 return $key_value ? array(
512 'days' => 'Daily',
513 'weeks' => 'Weekly',
514 'months' => 'Monthly',
515 'years' => 'Yearly',
516 ) : array(
517 array(
518 'label' => __( 'Day', 'subscription' ),
519 'value' => 'days',
520 ),
521 array(
522 'label' => __( 'Week', 'subscription' ),
523 'value' => 'weeks',
524 ),
525 array(
526 'label' => __( 'Month', 'subscription' ),
527 'value' => 'months',
528 ),
529 array(
530 'label' => __( 'Year', 'subscription' ),
531 'value' => 'years',
532 ),
533 );
534 }
535 }
536
537 /**
538 * Get WC product in subscription wrapper.
539 *
540 * @deprecated 1.8.17 Use SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product().
541 */
542 function sdevs_get_subscription_product( $product ) {
543 // Deprecated notice.
544 _deprecated_function( 'sdevs_get_subscription_product', '1.8.17', 'SpringDevs\Subscription\Illuminate\Subscription\Subscription::get_subs_product' );
545
546 return Subscription::get_subs_product( $product );
547 }
548
549 /**
550 * Logger
551 *
552 * @param mixed $message Message.
553 * @param bool $should_print Print the output.
554 */
555 function subscrpt_write_log( $message, bool $should_print = false ): void {
556 $logger = wc_get_logger();
557
558 $message = is_array( $message ) || is_object( $message ) ? wp_json_encode( $message ) : $message;
559 $logger->add( 'wp_subcription', $message );
560
561 echo esc_html( $should_print ? $message : '' );
562 }
563
564 /**
565 * Debug Logger
566 *
567 * @param mixed $log logs.
568 */
569 function subscrpt_write_debug_log( $log ): void {
570 if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) {
571 if ( is_array( $log ) || is_object( $log ) ) {
572 error_log( print_r( $log, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
573 } else {
574 error_log( 'wp_subcription: ' . $log ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
575 }
576 }
577 }
578
579 /**
580 * Add payment completion note for split payment subscriptions.
581 *
582 * @param int $subscription_id Subscription ID.
583 * @param int $payments_made Number of payments made.
584 * @param int $max_payments Maximum number of payments.
585 */
586 function subscrpt_add_payment_completion_note( $subscription_id, $payments_made, $max_payments ) {
587 // Check if this is a split payment subscription
588 if ( ! function_exists( 'subscrpt_get_payment_type' ) ) {
589 return;
590 }
591
592 $payment_type = subscrpt_get_payment_type( $subscription_id );
593 if ( 'split_payment' !== $payment_type ) {
594 return;
595 }
596
597 // Create completion note
598 $completion_note = sprintf(
599 /* translators: %1$d: payments made, %2$d: total payments */
600 __( 'Split payment plan completed successfully! %1$d of %2$d payments received.', 'subscription' ),
601 $payments_made,
602 $max_payments
603 );
604
605 // Add the completion note
606 $comment_id = wp_insert_comment(
607 array(
608 'comment_author' => 'Subscription for WooCommerce',
609 'comment_content' => $completion_note,
610 'comment_post_ID' => $subscription_id,
611 'comment_type' => 'order_note',
612 )
613 );
614 update_comment_meta( $comment_id, '_subscrpt_activity', __( 'Split Payment - Plan Complete', 'subscription' ) );
615 update_comment_meta( $comment_id, '_subscrpt_activity_type', 'split_payment' );
616
617 // Add payment summary note
618 $payment_summary = sprintf(
619 /* translators: %1$d: payments made, %2$d: total payments, %3$s: completion date */
620 __( 'Payment Summary: %1$d of %2$d installments completed on %3$s. All payments received successfully.', 'subscription' ),
621 $payments_made,
622 $max_payments,
623 date_i18n( wc_date_format(), current_time( 'timestamp' ) )
624 );
625
626 $summary_comment_id = wp_insert_comment(
627 array(
628 'comment_author' => 'Subscription for WooCommerce',
629 'comment_content' => $payment_summary,
630 'comment_post_ID' => $subscription_id,
631 'comment_type' => 'order_note',
632 )
633 );
634 update_comment_meta( $summary_comment_id, '_subscrpt_activity', __( 'Payment Summary - Complete', 'subscription' ) );
635 update_comment_meta( $summary_comment_id, '_subscrpt_activity_type', 'split_payment_summary' );
636 }
637
638
639 /**
640 * Render a WooCommerce-style multiselect field.
641 *
642 * @param array $field {
643 * Field arguments.
644 *
645 * @type string $id Required. Meta key / input ID.
646 * @type string $label Field label.
647 * @type array $options Key => Label pairs for options.
648 * @type array|string $selected Optional. Selected value(s). Array, JSON, or CSV.
649 * @type string $desc_tip Optional. Description tooltip text.
650 * @type string $description Optional. Field description text.
651 * @type string $wrapper_class Optional. Extra wrapper classes.
652 * @type string $class Optional. Extra <select> classes.
653 * @type string $name Optional. Input name. Defaults to $id.'[]'.
654 * }
655 */
656 function subscrpt_multiselect_field( $field ) {
657 $defaults = [
658 'id' => '',
659 'label' => '',
660 'options' => [],
661 'selected' => [],
662 'desc_tip' => false,
663 'description' => '',
664 'wrapper_class' => '',
665 'wrapper_style' => '',
666 'class' => 'wc-enhanced-select',
667 'style' => '',
668 'name' => '',
669 ];
670
671 $field = wp_parse_args( $field, $defaults );
672
673 if ( empty( $field['id'] ) ) {
674 return;
675 }
676
677 $id = esc_attr( $field['id'] );
678 $name = $field['name'] ? $field['name'] : $id . '[]';
679 $label = esc_html( $field['label'] );
680 $description = $field['description'];
681 $desc_tip = $field['desc_tip'];
682
683 // Normalize selected values into array.
684 $selected = [];
685 if ( is_array( $field['selected'] ) ) {
686 $selected = $field['selected'];
687 } elseif ( is_string( $field['selected'] ) && $field['selected'] !== '' ) {
688 if ( false !== strpos( $field['selected'], '[' ) ) {
689 $tmp = json_decode( $field['selected'], true );
690 $selected = is_array( $tmp ) ? $tmp : [];
691 } else {
692 $selected = array_filter( array_map( 'trim', explode( ',', $field['selected'] ) ) );
693 }
694 }
695
696 // Build <option> list.
697 $options_html = '';
698 foreach ( $field['options'] as $key => $text ) {
699 $is_selected = in_array( (string) $key, (array) $selected, true ) ? ' selected="selected"' : '';
700 $options_html .= sprintf(
701 '<option value="%s"%s>%s</option>',
702 esc_attr( $key ),
703 $is_selected,
704 esc_html( $text )
705 );
706 }
707
708 $tooltip_html = '';
709 if ( $desc_tip && $description ) {
710 $tooltip_html = wc_help_tip( $description );
711 }
712
713 $description_html = '';
714 if ( $description && ! $desc_tip ) {
715 $description_html = '<span class="description">' . wp_kses_post( $description ) . '</span>';
716 }
717
718 // ? Escaped intentionally.
719 // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
720 ?>
721 <p
722 class="form-field <?php echo esc_attr( $id . '_field ' . ( $field['wrapper_class'] ) ); ?>"
723 style="<?php echo esc_attr( $field['wrapper_style'] ); ?>"
724 >
725 <label for="<?php echo esc_attr( $id ); ?>">
726 <?php echo esc_html( $label ); ?>
727 </label>
728
729 <?php echo $tooltip_html; ?>
730
731 <select
732 multiple="multiple"
733 id="<?php echo esc_attr( $id ); ?>"
734 name="<?php echo esc_attr( $name ); ?>"
735 class="<?php echo esc_attr( $field['class'] ); ?>"
736 style="<?php echo esc_attr( $field['style'] ); ?>"
737 >
738 <?php echo $options_html; ?>
739 </select>
740
741 <?php echo $description_html; ?>
742 </p>
743 <?php
744 // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
745 }
746