PluginProbe
Eventin – Event Calendar, Tickets, Registration, Booking & WooCommerce / trunk
Eventin – Event Calendar, Tickets, Registration, Booking & WooCommerce vtrunk
4.1.23 4.1.22 4.1.21 4.1.20 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 All 209 releases
wp-event-solution / utils / functions.php

functions.php in Eventin – Event Calendar, Tickets, Registration, Booking & WooCommerce trunk, at utils/functions.php

1,879 lines 59.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined( 'ABSPATH' ) || exit;
4
5 use Eventin\Settings;
6 use Eventin\Validation\Validator;
7 use Etn\Core\Event\Event_Model;
8 use Etn\Utils\Helper;
9 use SureCart\Support\Currency;
10
11 if ( ! function_exists( 'etn_safe_decode' ) ) {
12 /**
13 * Safely unserialize data, preventing PHP Object Injection.
14 *
15 * Uses allowed_classes => false to block instantiation of
16 * PHP objects during unserialization.
17 *
18 * @since 4.1.4
19 * @param mixed $data The data to unserialize.
20 * @return mixed The unserialized data (no objects), or the original data if not serialized.
21 */
22 function etn_safe_decode( $data ) {
23 if ( ! is_string( $data ) ) {
24 return $data;
25 }
26
27 if ( ! is_serialized( $data ) ) {
28 return $data;
29 }
30
31 return @unserialize( $data, [ 'allowed_classes' => false ] );
32 }
33 }
34
35 if ( ! function_exists( 'etn_sanitize_array_input' ) ) {
36 /**
37 * Sanitize input to prevent PHP Object Injection.
38 *
39 * This function rejects serialized strings that could contain malicious PHP objects.
40 * Arrays and non-serialized strings are passed through for further processing.
41 *
42 * @since 4.1.2
43 * @param mixed $input The input to sanitize.
44 * @return array|string Returns array if input is array, string if input is non-serialized string, empty array otherwise.
45 */
46 function etn_sanitize_array_input( $input ) {
47 // If it's already an array, return it
48 if ( is_array( $input ) ) {
49 return $input;
50 }
51 // If it's a serialized string, reject it to prevent PHP Object Injection
52 if ( is_string( $input ) && is_serialized( $input ) ) {
53 return [];
54 }
55 // Return non-serialized strings as-is (safe for further processing like CSV parsing)
56 if ( is_string( $input ) ) {
57 return $input;
58 }
59 // Return empty array for other non-array input types
60 return [];
61 }
62 }
63
64 if ( ! function_exists( 'etn_sanitize_faq_array' ) ) {
65 /**
66 * Sanitize an array of event FAQ items at input/save time.
67 *
68 * Each FAQ item carries a plain-text title, rich-text content, and an
69 * enabled flag. Titles are reduced to plain text, content is passed through
70 * wp_kses_post() so that scripts and event-handler attributes (e.g.
71 * onerror=) are stripped before storage, and the enabled flag is cast to a
72 * boolean. This neutralizes stored XSS at the source rather than relying on
73 * every downstream render context to escape (CVE-2026-12924).
74 *
75 * @since 4.1.17
76 * @param mixed $faqs The raw FAQ input (expected: array of FAQ items).
77 * @return array The sanitized FAQ array (empty array for non-array input).
78 */
79 function etn_sanitize_faq_array( $faqs ) {
80 if ( ! is_array( $faqs ) ) {
81 return [];
82 }
83
84 return array_map(
85 function ( $faq ) {
86 if ( ! is_array( $faq ) ) {
87 return is_string( $faq ) ? wp_kses_post( $faq ) : '';
88 }
89
90 if ( isset( $faq['etn_faq_title'] ) ) {
91 $faq['etn_faq_title'] = sanitize_text_field( $faq['etn_faq_title'] );
92 }
93
94 if ( isset( $faq['etn_faq_content'] ) ) {
95 $faq['etn_faq_content'] = wp_kses_post( $faq['etn_faq_content'] );
96 }
97
98 if ( array_key_exists( 'etn_faq_enabled', $faq ) ) {
99 $faq['etn_faq_enabled'] = filter_var( $faq['etn_faq_enabled'], FILTER_VALIDATE_BOOLEAN );
100 }
101
102 return $faq;
103 },
104 $faqs
105 );
106 }
107 }
108
109 if ( ! function_exists( 'etn_array_csv_column' ) ) {
110 /**
111 * Convert array to CSV column
112 *
113 * @param array $data
114 *
115 * @return string
116 */
117 function etn_array_csv_column( $data = [] ) {
118 $result_string = '';
119
120 foreach ( $data as $data_key => $value ) {
121 if ( ! is_array( $value ) ) {
122 return etn_is_associative_array( $data ) ? etn_single_array_csv_column( $data ) : implode( ',', $data );
123 }
124
125 if ( etn_is_associative_array( $value ) ) {
126 $valueString = etn_single_array_csv_column( $value );
127 $result_string .= rtrim( $valueString, ', ' ) . '|';
128 } else {
129 $result_string .= implode( ',', $value ) . '|';
130 }
131 }
132
133 // Remove the trailing '|'
134 $result_string = rtrim( $result_string, '|' );
135
136 return $result_string;
137 }
138 }
139
140 if ( ! function_exists( 'etn_is_associative_array' ) ) {
141 /**
142 * Check an associative array or not
143 *
144 * @param array $array
145 *
146 * @return bool
147 */
148 function etn_is_associative_array( $array ) {
149 return is_array( $array ) && count( array_filter( array_keys( $array ), 'is_string' ) ) > 0;
150 }
151 }
152
153 if ( ! function_exists( 'etn_single_array_csv_column' ) ) {
154 /**
155 * Convert single array to csv column
156 *
157 * @param array $data
158 *
159 * @return string
160 */
161 function etn_single_array_csv_column( $data ) {
162 if ( ! is_array( $data ) ) {
163 return false;
164 }
165
166 $result_string = '';
167
168 foreach ( $data as $key => $value ) {
169 if ( is_array( $value ) ) {
170 $result_string .= implode( ',', $value );
171 } else {
172 $result_string .= "$key:$value,";
173 }
174 }
175
176 return rtrim( $result_string, ',' );
177 }
178 }
179
180 if ( ! function_exists( 'etn_csv_column_array' ) ) {
181 /**
182 * Convert CSV column to array
183 *
184 * @param string $csvColumn
185 *
186 * @return array|bool
187 */
188 function etn_csv_column_array( $csv_column, $separator = '|' ) {
189 // Explode the CSV column by '|' to get individual array elements
190 if ( strpos( $csv_column, $separator ) !== false ) {
191 return etn_csv_column_multi_dimension_array( $csv_column );
192 }
193
194 return etn_csv_column_single_array( $csv_column );
195 }
196 }
197
198 if ( ! function_exists( 'etn_csv_column_multi_dimension_array' ) ) {
199 /**
200 * Convert CSV column to multi dimensional array
201 *
202 * @param string $csv_column
203 * @param string $separator
204 *
205 * @return array
206 */
207 function etn_csv_column_multi_dimension_array( $csv_column, $separator = '|' ) {
208 $array_strings = explode( $separator, $csv_column );
209 $result_array = [];
210
211 foreach ( $array_strings as $array_string ) {
212 // Add the temporary array to the result array
213 $result_array[] = etn_csv_column_single_array( $array_string );
214 }
215
216 return $result_array;
217 }
218 }
219
220 if ( ! function_exists( 'etn_csv_column_single_array' ) ) {
221 /**
222 * Convert CSV column to multi dimensional array
223 *
224 * @param string $csv_column
225 * @param string $separator
226 *
227 * @return array
228 */
229 function etn_csv_column_single_array( $csv_column, $separator = ',' ) {
230 $temp_array = [];
231
232 if ( false !== strpos( $csv_column, ':' ) ) {
233 $csv_column = explode( $separator, $csv_column );
234
235 foreach ( $csv_column as $pair ) {
236 // Explode key-value pairs by ':' and populate the temporary array
237 list( $key, $value ) = explode( ':', $pair );
238 $temp_array[$key] = $value;
239 }
240
241 return $temp_array;
242 }
243
244 return explode( $separator, $csv_column );
245 }
246 }
247
248 if ( ! function_exists( 'etn_is_request' ) ) {
249 /**
250 * What type of request is this?
251 *
252 * @param string $type admin, ajax, cron or frontend.
253 *
254 * @return bool
255 */
256 function etn_is_request( $type ) {
257 switch ( $type ) {
258 case 'admin':
259 return is_admin();
260
261 case 'ajax':
262 return defined( 'DOING_AJAX' );
263
264 case 'rest':
265 return defined( 'REST_REQUEST' );
266
267 case 'cron':
268 return defined( 'DOING_CRON' );
269
270 case 'frontend':
271 return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' );
272 }
273 }
274 }
275
276 if ( ! function_exists( 'etn_get_locale_data' ) ) {
277 /**
278 * Get locale data
279 *
280 * @return array
281 */
282 function etn_get_locale_data() {
283 $localize_vars = include Wpeventin::plugin_dir() . 'utils/locale/vars.php';
284 $localize_static = include Wpeventin::plugin_dir() . 'utils/locale/static.php';
285
286 $data = array_merge( $localize_static, $localize_vars );
287
288 return apply_filters( 'etn_locale_data', $data );
289 }
290 }
291
292 if ( ! function_exists( 'etn_permision_error' ) ) {
293 /**
294 * Rest api error message
295 *
296 * @param string $message
297 *
298 * @return \WP_REST_Response
299 */
300 function etn_permision_error( $message = '' ) {
301 if ( ! $message ) {
302 $message = __( 'Sorry, you are not allowed to do that.', 'eventin' );
303 }
304
305 $data = [
306 'code' => 'rest_forbidden',
307 'message' => 'Sorry, you are not allowed to do that.',
308 'data' => [
309 'status' => 403,
310 ],
311 ];
312
313 return new WP_REST_Response( $data, 401 );
314 }
315 }
316
317 if ( ! function_exists( 'etn_parse_block_content' ) ) {
318 /**
319 * Parses dynamic blocks out of `post_content` and re-renders them.
320 *
321 * @param string $content
322 *
323 * @return string
324 */
325 function etn_parse_block_content( $content ) {
326 return do_blocks( $content );
327 }
328 }
329
330 if ( ! function_exists( 'etn_validate' ) ) {
331 /**
332 * Validate user input
333 *
334 * @param array $request
335 * @param array $rules
336 *
337 * @return bool | WP_Error
338 */
339 function etn_validate( $request, $rules ) {
340 $validator = new Validator( $request );
341
342 $validator->set_rules( $rules );
343
344 if ( ! $validator->validate() ) {
345 return $validator->get_error();
346 }
347
348 return true;
349 }
350 }
351
352 if ( ! function_exists( 'etn_get_option' ) ) {
353 /**
354 * Get option for eventin
355 *
356 * @since 1.0.0
357 * @return mixed
358 */
359 function etn_get_option( $key = '', $default = false ) {
360 $value = Settings::get( $key );
361
362 if ( ! $value ) {
363 return $default;
364 }
365
366 return $value;
367 }
368 }
369
370 if ( ! function_exists( 'etn_update_option' ) ) {
371
372 /**
373 * Update option
374 *
375 * @param string $key
376 *
377 * @since 1.0.0
378 *
379 * @return boolean
380 */
381 function etn_update_option( $key = '', $value = false ) {
382 if ( ! $key ) {
383 return false;
384 }
385
386 return Settings::update( [
387 $key => $value,
388 ] );
389 }
390 }
391
392 if ( ! function_exists( 'etn_parse_event_datetime' ) ) {
393 /**
394 * Build a DateTime from a stored event date + time pair.
395 *
396 * Normalizes non-standard time shapes ("8h00", "6.45", "6 45") to "HH:MM"
397 * before construction, and falls back to date-only at midnight rather than
398 * fataling on unparseable input.
399 *
400 * @param string $date Date string (Y-m-d).
401 * @param string $time Time string. May be empty or malformed.
402 * @param \DateTimeZone|string|null $timezone DateTimeZone instance, IANA string, or null for wp_timezone().
403 *
404 * @return \DateTime
405 */
406 function etn_parse_event_datetime( $date, $time = '', $timezone = null ) {
407 if ( $timezone instanceof \DateTimeZone ) {
408 $tz = $timezone;
409 } elseif ( is_string( $timezone ) && $timezone !== '' ) {
410 try {
411 $tz = new \DateTimeZone( $timezone );
412 } catch ( \Exception $e ) {
413 $tz = wp_timezone();
414 }
415 } else {
416 $tz = wp_timezone();
417 }
418
419 $time = is_string( $time ) ? trim( $time ) : '';
420
421 // Normalize "digits + non-digits + digits" (e.g. "8h00", "6.45", "6 45") → "08:00" / "06:45".
422 // Skip values that already contain a colon — "9:00 PM" / "21:00" are parsed natively by
423 // \DateTime, and reformatting them here would drop the AM/PM meridiem (turning every PM
424 // time into the morning). Preserve any meridiem for the compact formats that do get normalized.
425 if ( $time && false === strpos( $time, ':' )
426 && preg_match( '/^(\d{1,2})\D+(\d{1,2})\s*([AaPp][Mm])?/', $time, $m ) ) {
427 $time = sprintf( '%02d:%02d', (int) $m[1], (int) $m[2] );
428 if ( ! empty( $m[3] ) ) {
429 $time .= ' ' . strtoupper( $m[3] );
430 }
431 }
432
433 $date_time_string = trim( $date . ' ' . $time );
434
435 try {
436 return new \DateTime( $date_time_string, $tz );
437 } catch ( \Exception $e ) {
438 try {
439 return new \DateTime( (string) $date, $tz );
440 } catch ( \Exception $e2 ) {
441 return new \DateTime( 'now', $tz );
442 }
443 }
444 }
445 }
446
447 if ( ! function_exists( 'etn_is_ticket_sale_end' ) ) {
448 /**
449 * Check an event has attendees or not
450 *
451 * @param string $end_date_time Event ticket sale end date and time
452 * @param string $timezone Event timezone
453 *
454 * @return bool
455 */
456 function etn_is_ticket_sale_end( $end_date_time, $timezone = '' ) {
457 $timezone = $timezone ?: wp_timezone_string();
458 try {
459 $tz = new DateTimeZone( $timezone );
460 $event_end_dt = new DateTime( $end_date_time, $tz );
461 $current_dt = new DateTime( 'now', $tz );
462 } catch ( \Exception $e ) {
463 // Unparseable inputs → treat as not-yet-ended rather than fataling.
464 return false;
465 }
466
467 return $current_dt > $event_end_dt;
468 }
469 }
470
471 if ( ! function_exists( 'etn_is_ticket_sale_start' ) ) {
472 /**
473 * Check an event has attendees or not
474 *
475 * @param string $start_date_time Event ticket sale start date and time
476 * @param string $timezone Event timezone
477 *
478 * @return bool
479 */
480 function etn_is_ticket_sale_start( $start_date_time, $timezone = '' ) {
481 $timezone = $timezone ?: wp_timezone_string();
482 try {
483 $tz = new DateTimeZone( $timezone );
484 $event_date = new DateTime( $start_date_time, $tz );
485 $current_datte = new DateTime( 'now', $tz );
486 } catch ( \Exception $e ) {
487 // Unparseable inputs → treat as already started rather than fataling.
488 return true;
489 }
490
491 return $current_datte >= $event_date;
492 }
493 }
494
495
496 if ( ! function_exists( 'etn_create_date_timezone' ) ) {
497 /**
498 * Create datetimezone object
499 *
500 * @param string $timezoneString Timezone
501 *
502 * @return string
503 */
504 function etn_create_date_timezone( $timezoneString ) {
505 // List of valid named timezones
506 $validTimezones = DateTimeZone::listIdentifiers();
507
508 // Check if the provided timezone is a valid named timezone
509 if ( in_array( $timezoneString, $validTimezones ) ) {
510 return $timezoneString;
511 }
512
513 // Check if the provided timezone is an offset timezone like UTC+6 or UTC-4.5
514 if ( preg_match('/^UTC([+-]\d{1,2})(?:\.(\d))?$/i', $timezoneString, $matches ) ) {
515 // Convert the matched offset to a format recognized by DateTimeZone
516 $hours = intval( $matches[1] );
517 $minutes = isset( $matches[2] ) ? intval($matches[2]) * 6 : 0; // 0.1 fractional part means 6 minutes
518
519 // Ensure the format is like +06:30 or -04:30
520 $formattedOffset = sprintf( '%+03d:%02d', $hours, $minutes );
521 return $formattedOffset;
522 }
523
524 // If the timezone string doesn't match any known format, return default
525 return 'America/New_York';
526 }
527 }
528
529 if ( ! function_exists( 'etn_convert_to_date' ) ) {
530 /**
531 * Convert to date from date time string
532 *
533 * @param string $datetimeString Datetime string
534 *
535 * @return string Date string
536 */
537 function etn_convert_to_date( $datetimeString ) {
538 try {
539 // Create a DateTime object using the provided datetime string
540 $datetime = new DateTime( $datetimeString );
541
542 // Return the formatted date in 'Y-m-d' format
543 return $datetime->format( 'Y-m-d' );
544 } catch ( Exception $e ) {
545 return 'Error: ' . $e->getMessage();
546 }
547 }
548 }
549
550 if ( ! function_exists( 'etn_get_currency' ) ) {
551 /**
552 * Get currency list
553 *
554 * @return array
555 */
556 function etn_get_currency () {
557 $currencies = require Wpeventin::plugin_dir() . '/utils/currency.php';
558
559 return $currencies;
560 }
561 }
562
563 if ( ! function_exists( 'etn_get_currency_by' ) ) {
564 /**
565 * Get currency by name, symbol
566 *
567 * @return string
568 */
569 function etn_get_currency_by_name( $name ) {
570 $currencies = etn_get_currency();
571
572 foreach( $currencies as $currencie ) {
573 if ( $currencie['name'] === $name ) {
574 return $currencie;
575 }
576 }
577
578 return null;
579 }
580 }
581
582 if ( ! function_exists( 'etn_get_currency_symbol' ) ) {
583 /**
584 * Get currency by name, symbol
585 *
586 * @return string
587 */
588 function etn_get_currency_symbol( $name ) {
589 $currency = etn_get_currency_by_name( $name );
590
591 if ( $currency ) {
592 return $currency['symbol'];
593 }
594 }
595 }
596
597 if ( ! function_exists( 'etn_event_url_editable' ) ) {
598 /**
599 * Check editable url
600 *
601 * @return bool
602 */
603 function etn_event_url_editable () {
604 $permalink_structure = get_option('permalink_structure');
605
606 if ( strpos( $permalink_structure, '%postname%' ) !== false) {
607 return true;
608 }
609
610 return false;
611 }
612 }
613
614 if ( ! function_exists( 'etn_get_timezone' ) ) {
615 /**
616 * Get valid timezonelists
617 *
618 * @return array Timezone lists
619 */
620 function etn_get_timezone() {
621 $validTimezones = DateTimeZone::listIdentifiers();
622
623 return $validTimezones;
624 }
625 }
626
627 if ( ! function_exists( 'etn_get_wp_timezones' ) ) {
628 /**
629 * Get valid wp timezone
630 *
631 * @return string
632 */
633 function etn_get_wp_timezones() {
634 $timezones = [];
635
636 // Generate UTC offsets
637 $offset_range = range( -12, 14 );
638 foreach ( $offset_range as $offset ) {
639 // Whole hour
640 $hours = ( $offset > 0 ) ? '+' . $offset : (string) $offset;
641
642 if ( $offset != 0 ) {
643 $timezones[] = 'UTC' . $hours;
644 }
645 else{
646 $timezones[] = 'UTC' . '+'.$hours;
647 }
648
649 // Half-hour offsets
650 if ( $offset != 0 ) {
651 $timezones[] = 'UTC' . $hours . ':30';
652 }
653
654 if ( $offset == 0 ) {
655 $timezones[] = 'UTC' . '+'.$hours . ':30';
656 $timezones[] = 'UTC' . '-'.$hours . ':30';
657 }
658
659 // Special quarter-hour offsets
660 if ( in_array( $offset, [ 5, 8, 12, 13 ] ) ) {
661 $timezones[] = 'UTC+' . $offset . ':45';
662 }
663 }
664
665 // Now add all region identifiers
666 $timezones = array_merge( $timezones, DateTimeZone::listIdentifiers() );
667
668 return $timezones;
669 }
670 }
671
672 if ( ! function_exists( 'etn_prepare_address' ) ) {
673 /**
674 * Prepare event address from event location
675 *
676 * This function is written for temporary solution. We have a nested location issue @since 4.0.0. To resolve this we impletent a temporary function. We have to remove this function when v4.0 is completely statble from location issue. Before remove this function make sure remove from all of the place where we used this.
677 *
678 * @param array $location
679 *
680 * @return string
681 */
682 function etn_prepare_address( $location ) {
683 static $depth = 0;
684
685 if ( $depth >= 10 ) {
686 return '';
687 }
688
689 $depth++;
690
691 if ( ! is_array( $location ) ) {
692 return $location;
693 }
694
695 $address = ! empty( $location['address'] ) ? $location['address'] : '';
696
697 return etn_prepare_address( $address );
698 }
699 }
700
701 if ( ! function_exists( 'etn_get_email_settings' ) ) {
702
703 /**
704 * Get email settings
705 *
706 * @param string $email Email name for the email setting
707 *
708 * @return array
709 */
710 function etn_get_email_settings( $email = '' ) {
711
712 $email_settings = etn_get_option( 'email' );
713
714 $defaults = etn_get_default_email_settings();
715
716 $email_settings = etn_recursive_wp_parse_args( $email_settings, $defaults );
717
718 if ( ! $email ) {
719 return $email_settings;
720 }
721
722 return $email_settings[$email];
723 }
724 }
725
726 if ( ! function_exists( 'etn_get_default_email_settings' ) ) {
727 /**
728 * Get default email settings for the email template
729 *
730 * @return array
731 */
732 function etn_get_default_email_settings() {
733
734 $email_settings = [
735 'purchase_email' => [
736 'from' => get_option( 'admin_email' ),
737 'subject' => sprintf( __( 'Event Ticket', 'eventin' ) ),
738 'body' => __( 'You have purchased ticket(s). Attendee ticket details are as follows.', 'eventin' ),
739 'send_to_admin' => true,
740 'send_email_to_attendees' => true
741 ],
742 'certificate_email' => [
743 'from' => get_option( 'admin_email' ),
744 'subject' => sprintf( __( 'Event Certificate', 'eventin' ) ),
745 // translators: %1$s is the event title
746 'body' => sprintf( __( '<p>Congratulations for successfully attending/completing the event \'%1$s\'. Your certificate is ready! Click on the link provided below to get the PDF certificate. </p>', 'eventin' ), '<span>{%event_title%}</span>' ), // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings
747 'send_to_admin' => true,
748 ],
749 'rsv_email' => [
750 'from' => get_option( 'admin_email' ),
751 'response_type' => 'going',
752 'subject' => sprintf( __( 'RSVP request', 'eventin' ) ),
753 'body' => sprintf( __( 'We received your RSVP request', 'eventin' ) ),
754 'send_to_admin' => true,
755 ],
756 'reminder_email' => [
757 'from' => get_option( 'admin_email' ),
758 'subject' => sprintf( __( 'Reminder email', 'eventin' ) ),
759 'body' => __( 'Just sending you a quick reminder about our retailer meet-up you\'ve registered to attend in two days time. If you\'ve misplaced the Invitation that contained all the details. don\'t worry. Cve added them rn below for you.', 'eventin' ),
760 'send_to_admin' => true,
761 ]
762 ];
763
764 return apply_filters( 'etn_default_email_settings', $email_settings );
765 }
766 }
767
768 if ( ! function_exists( 'etn_recursive_wp_parse_args' ) ) {
769 /**
770 * Perse args recursively
771 *
772 * @param array $args [$args description]
773 * @param array $defaults [$defaults description]
774 *
775 * @return array [return description]
776 */
777 function etn_recursive_wp_parse_args( $args, $defaults ) {
778 $args = (array) $args; // Ensure args is an array
779
780 // Loop through each default value and apply wp_parse_args recursively if it's an array
781 foreach ( $defaults as $key => $value ) {
782 if ( is_array( $value ) ) {
783 // If the key is an array, call recursively
784 $args[$key] = etn_recursive_wp_parse_args( isset( $args[$key] ) ? $args[$key] : [], $value );
785 } else {
786 // Otherwise, use wp_parse_args for the non-array values
787 if ( ! isset( $args[$key] ) ) {
788 $args[$key] = $value;
789 }
790 }
791 }
792
793 return $args;
794 }
795 }
796
797 if ( !function_exists( 'etn_editor_settings' ) ) {
798 /**
799 * Retrieves the settings for the Gutenberg editor.
800 *
801 * This function retrieves the settings for the Gutenberg editor, including the allowed block types,
802 * typography, color palette, and other experimental features. It also applies filters to allow
803 * customization of the settings.
804 *
805 * @return array The settings for the Gutenberg editor.
806 */
807 function etn_editor_settings() {
808
809
810 $coreSettings = get_block_editor_settings( [], 'post' );
811 $wordpressCoreTypography = $coreSettings['__experimentalFeatures']['typography'];
812 $coreExperimentalSpacing = $coreSettings['__experimentalFeatures']['spacing'];
813
814
815 $themePref = getThemePrefScheme();
816
817 $settings = array(
818 'gradients' => [],
819 'alignWide' => false,
820 'allowedMimeTypes' => get_allowed_mime_types(),
821 '__experimentalBlockPatterns' => [],
822 '__experimentalFeatures' => [
823 'appearanceTools' => true,
824 'border' => [
825 'color' => false,
826 'radius' => true,
827 'style' => false,
828 'width' => false,
829 ],
830 'color' => [
831 'background' => true,
832 'customDuotone' => false,
833 'defaultGradients' => false,
834 'defaultPalette' => false,
835 'duotone' => [],
836 'gradients' => [],
837 'link' => false,
838 'palette' => [
839 'theme' => $themePref['colors'],
840 ],
841 'text' => true,
842 ],
843 'spacing' => $coreExperimentalSpacing,
844 'typography' => $wordpressCoreTypography,
845 'blocks' => [
846 'core/button' => [
847 'border' => [
848 'radius' => true,
849 "style" => true,
850 "width" => true,
851 ],
852 'typography' => [
853 'fontSizes' => [],
854 ],
855 'spacing' => $coreExperimentalSpacing,
856 ],
857
858 ],
859 ],
860 '__experimentalSetIsInserterOpened' => false,
861 'disableCustomColors' => get_theme_support( 'disable-custom-colors' ),
862 'disableCustomFontSizes' => false,
863 'disableCustomGradients' => true,
864 'enableCustomLineHeight' => get_theme_support( 'custom-line-height' ),
865 'enableCustomSpacing' => get_theme_support( 'custom-spacing' ),
866 'enableCustomUnits' => false,
867 'keepCaretInsideBlock' => false,
868 'mediaLibrary' => ['type' => true, 'date' => true, 'allowedTypes' => ['image', 'video', 'audio', 'application']],
869 'mediaUpload' => true,
870 );
871
872 $color_palette = current( (array) get_theme_support( 'editor-color-palette' ) );
873 if ( false !== $color_palette ) {
874 $settings['colors'] = $color_palette;
875 } else {
876 $settings['colors'] = [];
877 }
878
879 return $settings;
880 }
881 }
882
883 if ( !function_exists( 'getThemePrefScheme' ) ) {
884 /**
885 * Retrieves the theme preference scheme.
886 *
887 * This function retrieves the theme preference scheme, which includes the color palette and font sizes.
888 * The color palette is an array of objects, each representing a color with its name, slug, and hex code.
889 * The font sizes is an array of objects, each representing a font size with its name, short name, size, and slug.
890 * The function applies filters to allow customization of the theme preference scheme.
891 *
892 * @return array The theme preference scheme.
893 */
894 function getThemePrefScheme() {
895 static $pref;
896 if ( !$pref ) {
897
898 $color_palette = [
899 [
900 "name" => __( "Black", "eventin" ),
901 "slug" => "black",
902 "color" => "#000000",
903 ],
904 [
905 "name" => __( "Cyan bluish gray", "eventin" ),
906 "slug" => "cyan-bluish-gray",
907 "color" => "#abb8c3",
908 ],
909 [
910 "name" => __( "White", "eventin" ),
911 "slug" => "white",
912 "color" => "#ffffff",
913 ],
914 [
915 "name" => __( "Pale pink", "eventin" ),
916 "slug" => "pale-pink",
917 "color" => "#f78da7",
918 ],
919 [
920 "name" => __( "Luminous vivid orange", "eventin" ),
921 "slug" => "luminous-vivid-orange",
922 "color" => "#ff6900",
923 ],
924 [
925 "name" => __( "Luminous vivid amber", "eventin" ),
926 "slug" => "luminous-vivid-amber",
927 "color" => "#fcb900",
928 ],
929 [
930 "name" => __( "Light green cyan", "eventin" ),
931 "slug" => "light-green-cyan",
932 "color" => "#7bdcb5",
933 ],
934 [
935 "name" => __( "Vivid green cyan", "eventin" ),
936 "slug" => "vivid-green-cyan",
937 "color" => "#00d084",
938 ],
939 [
940 "name" => __( "Pale cyan blue", "eventin" ),
941 "slug" => "pale-cyan-blue",
942 "color" => "#8ed1fc",
943 ],
944 [
945 "name" => __( "Vivid cyan blue", "eventin" ),
946 "slug" => "vivid-cyan-blue",
947 "color" => "#0693e3",
948 ],
949 [
950 "name" => __( "Vivid purple", "eventin" ),
951 "slug" => "vivid-purple",
952 "color" => "#9b51e0",
953 ],
954 ];
955
956 $font_sizes = [
957 [
958 'name' => __( 'Small', 'eventin' ),
959 'shortName' => 'S',
960 'size' => 14,
961 'slug' => 'small',
962 ],
963 [
964 'name' => __( 'Medium', 'eventin' ),
965 'shortName' => 'M',
966 'size' => 18,
967 'slug' => 'medium',
968 ],
969 [
970 'name' => __( 'Large', 'eventin' ),
971 'shortName' => 'L',
972 'size' => 24,
973 'slug' => 'large',
974 ],
975 [
976 'name' => __( 'Larger', 'eventin' ),
977 'shortName' => 'XL',
978 'size' => 32,
979 'slug' => 'larger',
980 ],
981 ];
982
983 $pref = apply_filters( 'eventin/theme_pref', [
984 'colors' => (array) $color_palette,
985 'font_sizes' => (array) $font_sizes,
986 ] );
987 }
988
989 return $pref;
990
991 }
992 }
993
994 if ( ! function_exists( 'etn_currency' ) ) {
995 /**
996 * Get currecny
997 *
998 * @return string
999 */
1000 function etn_currency() {
1001 $payment_method = etn_get_option( 'payment_method' );
1002
1003 $is_enabled_wc = 'woocommerce' === $payment_method;
1004
1005 $is_enabled_sc = etn_get_option('surecart_status');
1006
1007 $is_enabled_fc = etn_get_option('fluentcart_status');
1008
1009 if ( $is_enabled_sc && class_exists(SureCart::class) ) {
1010 return \SureCart::account()->currency;
1011 }
1012
1013 if ( $is_enabled_fc && defined('FLUENTCART_VERSION') ) {
1014 return (new \FluentCart\Api\StoreSettings())->getCurrency();
1015 }
1016
1017 if ( function_exists('WC') && $is_enabled_wc ) {
1018 return get_woocommerce_currency();
1019 }
1020
1021 $currency = etn_get_option( 'etn_settings_country_currency', 'USD' );
1022
1023 return $currency;
1024 }
1025 }
1026
1027 if ( ! function_exists( 'etn_currency_symbol' ) ) {
1028 /**
1029 * Get currency symbol
1030 *
1031 * @return string
1032 */
1033 function etn_currency_symbol() {
1034 $currency = etn_currency();
1035
1036 $is_enabled_sc = etn_get_option('surecart_status');
1037
1038 $is_enabled_fc = etn_get_option('fluentcart_status');
1039
1040 if ( $is_enabled_sc && class_exists(SureCart::class) ) {
1041 return html_entity_decode( Currency::getCurrencySymbol( \SureCart::account()->currency ) );
1042 }
1043
1044 if ( $is_enabled_fc && defined('FLUENTCART_VERSION') ) {
1045 return (new \FluentCart\Api\StoreSettings())->getCurrencySymbol();
1046 }
1047
1048 return etn_get_currency_symbol( $currency );
1049 }
1050 }
1051
1052 if ( ! function_exists( 'etn_is_enable_wc' ) ) {
1053 /**
1054 * Check event in is used woocommerce payment method
1055 *
1056 * @return bool
1057 */
1058 function etn_is_enable_wc() {
1059 $payment_method = etn_get_option( 'payment_method' );
1060
1061 return function_exists( 'WC' ) && 'woocommerce' === $payment_method;
1062 }
1063 }
1064
1065 if ( ! function_exists( 'etn_get_thousand_separator' ) ) {
1066
1067 /**
1068 * Thousand separator
1069 *
1070 * @return string
1071 */
1072 function etn_get_thousand_separator() {
1073 if ( etn_is_enable_wc() ) {
1074 return wc_get_price_thousand_separator();
1075 }
1076
1077 $thousand_separator = etn_get_option( 'thousand_separator', ',' );
1078
1079 return apply_filters( 'etn_thousand_separator', $thousand_separator );
1080 }
1081 }
1082
1083 if ( ! function_exists( 'etn_get_decimal_separator' ) ) {
1084
1085 /**
1086 * Get descimal separator
1087 *
1088 * @return string
1089 */
1090 function etn_get_decimal_separator() {
1091 if ( etn_is_enable_wc() ) {
1092 return wc_get_price_decimal_separator();
1093 }
1094
1095 $decimal_separator = etn_get_option( 'decimal_separator', 'comma_dot' );
1096
1097 return apply_filters( 'etn_decimal_separator', $decimal_separator );
1098 }
1099 }
1100
1101 if ( ! function_exists( 'etn_get_decimals' ) ) {
1102
1103 /**
1104 * Get number of decimals
1105 *
1106 * @return string
1107 */
1108 function etn_get_decimals() {
1109 if ( etn_is_enable_wc() ) {
1110 return wc_get_price_decimals();
1111 }
1112
1113 $decimals = etn_get_option( 'decimals', 0 );
1114
1115 return apply_filters( 'etn_decimals', $decimals );
1116 }
1117 }
1118
1119 if ( ! function_exists( 'etn_get_price_format' ) ) {
1120
1121 /**
1122 * Get price format
1123 *
1124 * @return string
1125 */
1126 function etn_get_price_format() {
1127 if ( etn_is_enable_wc() ) {
1128 return get_woocommerce_price_format();
1129 }
1130
1131 $currency_pos = get_option( 'currency_position' );
1132 $format = '%1$s%2$s';
1133
1134 switch ( $currency_pos ) {
1135 case 'left':
1136 $format = '%1$s%2$s';
1137 break;
1138 case 'right':
1139 $format = '%2$s%1$s';
1140 break;
1141 case 'left_space':
1142 $format = '%1$s&nbsp;%2$s';
1143 break;
1144 case 'right_space':
1145 $format = '%2$s&nbsp;%1$s';
1146 break;
1147 }
1148
1149 return apply_filters( 'etn_price_format', $format, $currency_pos );
1150 }
1151 }
1152
1153 if ( ! function_exists( 'etn_get_currency_position' ) ) {
1154
1155 /**
1156 * Get price format
1157 *
1158 * @return string
1159 */
1160 function etn_get_currency_position() {
1161 if ( etn_is_enable_wc() ) {
1162 $currency_pos = get_option( 'woocommerce_currency_pos' );
1163
1164 return $currency_pos;
1165 }
1166
1167 $currency_pos = etn_get_option( 'currency_position', 'left' );
1168
1169 return apply_filters( 'etn_currency_position', $currency_pos );
1170 }
1171 }
1172
1173 if ( ! function_exists( 'etn_get_wc_order_status_list' ) ) {
1174 /**
1175 * Get all woocommerce order statuses without prefix
1176 *
1177 * @return array Woocommerce order statuses
1178 */
1179 function etn_get_wc_order_status_list() {
1180 $statuses_without_prefix = [];
1181
1182 if ( ! function_exists( 'WC' ) ) {
1183 return $statuses_without_prefix;
1184 }
1185
1186 $statuses = wc_get_order_statuses();
1187
1188
1189 foreach ( $statuses as $key => $label ) {
1190 // Remove the 'wc-' prefix from each status key.
1191 $new_key = str_replace( 'wc-', '', $key );
1192 $statuses_without_prefix[$new_key] = $label;
1193 }
1194
1195 return $statuses_without_prefix;
1196 }
1197 }
1198
1199 if ( ! function_exists( 'etn_get_wc_order_statuses' ) ) {
1200 /**
1201 * Get all woocommerce order statuses stored as eventin settings
1202 *
1203 * @return array Woocommerce order statuses
1204 */
1205 function etn_get_wc_order_statuses() {
1206 $defaults = [ 'completed', 'processing' ];
1207 $settings = etn_get_option( 'wc_order_statuses', $defaults );
1208
1209 return $settings;
1210 }
1211 }
1212
1213 if ( ! function_exists( 'etn_is_enable_wc_synchronize_order' ) ) {
1214 /**
1215 * Check wc order synchronizes orders to the posts table enable or not
1216 *
1217 * @return bool Retur true if synchronizes orders is enable otherwise false
1218 */
1219 function etn_is_enable_wc_synchronize_order() {
1220
1221 if ( false === get_option( 'woocommerce_custom_orders_table_enabled' ) ) {
1222 return true;
1223 }
1224
1225 $enable_custom_order_table = get_option( 'woocommerce_custom_orders_table_enabled', true );
1226
1227 if ( 'no' === $enable_custom_order_table ) {
1228 return true;
1229 }
1230
1231 $data_sync_enabled = get_option( 'woocommerce_custom_orders_table_data_sync_enabled', true );
1232
1233 if ( 'yes' === $data_sync_enabled ) {
1234 return true;
1235 }
1236
1237 return false;
1238 }
1239 }
1240
1241 if ( ! function_exists( 'etn_gutenberg_template_path' ) ) {
1242 /**
1243 * Gutenberg block paths
1244 *
1245 * @return string
1246 */
1247 function etn_block_path() {
1248 return Wpeventin::core_dir() . 'Blocks/';
1249 }
1250 }
1251
1252 if ( ! function_exists( 'etn_upload_image_from_url' ) ) {
1253 /**
1254 * Upload image from url
1255 *
1256 * @param string $image_url Public Image url
1257 *
1258 * @return integer Image attatchment id
1259 */
1260 function etn_upload_image_from_url( $image_url ) {
1261 // Check if the URL is valid
1262 if ( empty( $image_url ) || ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) {
1263 return new WP_Error('invalid_url', __('Invalid image URL.', 'eventin'));
1264 }
1265
1266 // Get the file name from the URL
1267 $file_name = basename( wp_parse_url( $image_url, PHP_URL_PATH ) );
1268
1269 // Download the image
1270 $image_data = wp_remote_get( $image_url, [
1271 'timeout' => 30, // Increase timeout to 30 seconds
1272 'blocking' => true,
1273 ] );
1274 if ( is_wp_error( $image_data ) ) {
1275 return new WP_Error( 'download_failed', __( 'Failed to download image.', 'eventin') );
1276 }
1277
1278 $image_body = wp_remote_retrieve_body( $image_data );
1279 if ( empty( $image_body ) ) {
1280 return new WP_Error( 'empty_image', __( 'Downloaded image is empty.', 'eventin' ) );
1281 }
1282
1283 // Get the upload directory
1284 $upload_dir = wp_upload_dir();
1285 $file_path = $upload_dir['path'] . '/' . $file_name;
1286
1287 // Save the image file
1288 if ( ! file_put_contents( $file_path, $image_body ) ) {
1289 return new WP_Error( 'file_write_error', __('Failed to write image file.', 'eventin' ) );
1290 }
1291
1292 // Prepare file data for WordPress upload
1293 $file_info = [
1294 'name' => $file_name,
1295 'type' => mime_content_type( $file_path ),
1296 'tmp_name' => $file_path,
1297 'error' => 0,
1298 'size' => filesize( $file_path ),
1299 ];
1300
1301 // Include WordPress file handling functions
1302 require_once ABSPATH . 'wp-admin/includes/image.php';
1303 require_once ABSPATH . 'wp-admin/includes/file.php';
1304 require_once ABSPATH . 'wp-admin/includes/media.php';
1305
1306 // Upload image to WordPress media library
1307 $attachment_id = media_handle_sideload($file_info, 0);
1308
1309 // Check for errors
1310 if ( is_wp_error( $attachment_id ) ) {
1311 wp_delete_file($file_path); // Delete the temporary file if upload fails
1312 return $attachment_id;
1313 }
1314
1315 // Delete the temporary file after successful upload
1316 wp_delete_file($file_path);
1317
1318 return $attachment_id; // Return the attachment ID
1319 }
1320 }
1321
1322 if ( ! function_exists( 'etn_date_format' ) ) {
1323 /**
1324 * Get date formate from wordpress
1325 *
1326 * @return string
1327 */
1328 function etn_date_format() {
1329 $date_format = get_option( 'date_format' );
1330
1331 return $date_format;
1332 }
1333 }
1334
1335 if ( ! function_exists( 'etn_time_format' ) ) {
1336 /**
1337 * Get time formate from wordpress
1338 *
1339 * @return string
1340 */
1341 function etn_time_format() {
1342 $time_format = get_option( 'time_format' );
1343
1344 return $time_format;
1345 }
1346 }
1347
1348 if ( ! function_exists( 'etn_validate_event_tickets' ) ) {
1349 /**
1350 * Validate event tickets that verify to prevent overselling
1351 *
1352 * @param integer $event_id Event id that will be purchased
1353 * @param array $order_tickets Order ticket list
1354 * @param bool $is_for_update Whether this is an update operation
1355 * @param bool $is_waiting Whether this is for waiting list
1356 *
1357 * @return bool | WP_Error
1358 */
1359 function etn_validate_event_tickets( $event_id, $order_tickets, $is_for_update = false, $is_waiting = false ) {
1360 $event = new Event_Model( $event_id );
1361 $sold_tickets = (array)Helper::etn_get_sold_tickets_by_event( $event_id );
1362
1363 $enable_global_stock = get_post_meta( $event_id, 'etn_enable_global_stock', true );
1364 $global_stock = intval( get_post_meta( $event_id, 'etn_global_stock', true ) );
1365
1366 if ( $enable_global_stock && $global_stock > 0 ) {
1367 $total_requested = 0;
1368 foreach ( $order_tickets as $ticket ) {
1369 $total_requested += intval( $ticket['ticket_quantity'] );
1370 }
1371
1372 $total_sold = array_sum( $sold_tickets );
1373
1374 $ticket_variations = $event->etn_ticket_variations;
1375 $total_pending = 0;
1376 if ( is_array( $ticket_variations ) ) {
1377 foreach ( $ticket_variations as $ticket_var ) {
1378 $total_pending += intval( $ticket_var['pending'] ?? 0 );
1379 }
1380 }
1381
1382 $remaining = $global_stock - $total_sold - $total_pending;
1383 if ( $is_for_update ) {
1384 $remaining += $total_requested;
1385 }
1386
1387 if ( $is_waiting ) {
1388 $global_waiting_list = intval( get_post_meta( $event_id, 'etn_global_waiting_list', true ) );
1389 $total_waiting = array_sum( etn_get_waiting_list_counts_by_slug( $event_id ) );
1390 $waiting_remaining = $global_waiting_list - $total_waiting;
1391 if ( $total_requested > $waiting_remaining ) {
1392 return new WP_Error(
1393 'waiting_list_limit',
1394 // translators: %d is the number of waiting-list spots still available.
1395 sprintf( __( 'Waiting list is full. Only %d spots available.', 'eventin' ), max( 0, $waiting_remaining ) ),
1396 ['status' => 422]
1397 );
1398 }
1399 } else {
1400 if ( $total_requested > $remaining ) {
1401 $only_left = max( 0, $remaining );
1402 return new WP_Error(
1403 'global_stock_limit',
1404 // translators: %d is the number of tickets still left for the event.
1405 sprintf( __( 'Only %d tickets left for this event.', 'eventin' ), $only_left ),
1406 ['status' => 422]
1407 );
1408 }
1409 }
1410
1411 return true;
1412 }
1413
1414 foreach( $order_tickets as $ticket ) {
1415 $event_ticket = $event->get_ticket( $ticket['ticket_slug'] );
1416
1417 if ( ! $event_ticket ) {
1418 return new WP_Error( 'ticket_not_found', __( 'Ticket not found', 'eventin' ), ['status' => 422] );
1419 }
1420
1421 $available = $event_ticket['etn_avaiilable_tickets'] ?? 0;
1422 $sold = $sold_tickets[$ticket['ticket_slug']] ?? 0;
1423 $pending = $event_ticket['pending'] ?? 0;
1424
1425 if ( ! isset( $available ) || ! is_numeric( $available ) ) {
1426 return true;
1427 }
1428
1429 $ticket_left = intval( $available ) - intval( $sold ) - intval( $pending );
1430 if ( $is_for_update ) {
1431 $ticket_left += intval( $ticket['ticket_quantity'] );
1432 }
1433
1434 if ( $is_waiting ) {
1435 $waiting_counts = etn_get_waiting_list_counts_by_slug( $event_id );
1436 $ticket_waiting_limit = intval( $event_ticket['etn_ticket_waiting_list_limit'] ?? 0 );
1437 $waiting_for_slug = $waiting_counts[ $ticket['ticket_slug'] ] ?? 0;
1438 $waiting_remaining = $ticket_waiting_limit - $waiting_for_slug;
1439 if ( intval( $ticket['ticket_quantity'] ) > $waiting_remaining ) {
1440 return new WP_Error(
1441 'waiting_list_limit',
1442 // translators: %d is the number of waiting-list spots still available for this ticket type.
1443 sprintf( __( 'Waiting list is full for this ticket type. Only %d spots available.', 'eventin' ), max( 0, $waiting_remaining ) ),
1444 ['status' => 422]
1445 );
1446 }
1447 } else {
1448 if ( $available > 0 && $ticket['ticket_quantity'] > $ticket_left ) {
1449 return new WP_Error( 'ticket_limit', __( 'The ticket limit has been exceeded', 'eventin' ), ['status' => 422] );
1450 }
1451 }
1452 }
1453
1454 return true;
1455 }
1456 }
1457
1458 if ( ! function_exists( 'etn_validate_ticket_quantity' ) ) {
1459 /**
1460 * Validate ticket quantity against min/max per ticket variant
1461 *
1462 * @param array $order_tickets Order ticket list
1463 * @param integer $event_id Event id
1464 *
1465 * @return bool | WP_Error
1466 */
1467 function etn_validate_ticket_quantity( $order_tickets, $event_id ) {
1468 $event = new Event_Model( $event_id );
1469
1470 foreach ( $order_tickets as $ticket ) {
1471 $event_ticket = $event->get_ticket( $ticket['ticket_slug'] );
1472
1473 if ( ! $event_ticket ) {
1474 continue;
1475 }
1476
1477 $qty = intval( $ticket['ticket_quantity'] );
1478 $min = intval( $event_ticket['etn_min_ticket'] ?? 0 );
1479 $max = intval( $event_ticket['etn_max_ticket'] ?? 0 );
1480
1481 // When no explicit per-order max is configured, fall back to the
1482 // ticket's total capacity so requests cannot exceed what the event
1483 // can ever fulfill (relevant for waiting-list orders where the
1484 // normal stock check is bypassed).
1485 if ( $max === 0 ) {
1486 $available = intval( $event_ticket['etn_avaiilable_tickets'] ?? 0 );
1487 if ( $available > 0 ) {
1488 $max = $available;
1489 }
1490 }
1491
1492 if ( $min > 0 && $qty < $min ) {
1493 return new WP_Error(
1494 'min_ticket_quantity',
1495 // translators: %d is the minimum ticket quantity required for this ticket type.
1496 sprintf( __( 'Minimum %d tickets required for this ticket type.', 'eventin' ), $min ),
1497 ['status' => 422]
1498 );
1499 }
1500
1501 if ( $max > 0 && $qty > $max ) {
1502 return new WP_Error(
1503 'max_ticket_quantity',
1504 // translators: %d is the maximum ticket quantity allowed for this ticket type.
1505 sprintf( __( 'Maximum %d tickets allowed for this ticket type.', 'eventin' ), $max ),
1506 ['status' => 422]
1507 );
1508 }
1509 }
1510
1511 return true;
1512 }
1513 }
1514
1515 if ( ! function_exists( 'etn_get_waiting_list_counts_by_slug' ) ) {
1516 /**
1517 * Return ticket quantities on the waiting list for an event, keyed by ticket slug.
1518 * Counts all orders that carry the is_from_waiting_list meta flag.
1519 *
1520 * @param integer $event_id
1521 *
1522 * @return array [ ticket_slug => total_quantity, ... ]
1523 */
1524 function etn_get_waiting_list_counts_by_slug( $event_id ) {
1525 $query = new WP_Query( [
1526 'post_type' => 'etn-order',
1527 'post_status' => 'any',
1528 'posts_per_page' => -1,
1529 'fields' => 'ids',
1530 'meta_query' => [
1531 [
1532 'key' => 'event_id',
1533 'value' => $event_id,
1534 ],
1535 [
1536 'key' => 'is_from_waiting_list',
1537 'value' => '1',
1538 ],
1539 ],
1540 ] );
1541
1542 $counts = [];
1543 foreach ( $query->posts as $order_id ) {
1544 $tickets = etn_safe_decode( get_post_meta( $order_id, 'tickets', true ) );
1545 if ( ! is_array( $tickets ) ) {
1546 continue;
1547 }
1548 foreach ( $tickets as $ticket ) {
1549 $slug = $ticket['ticket_slug'] ?? '';
1550 $qty = intval( $ticket['ticket_quantity'] ?? 0 );
1551 if ( $slug ) {
1552 $counts[ $slug ] = ( $counts[ $slug ] ?? 0 ) + $qty;
1553 }
1554 }
1555 }
1556
1557 return $counts;
1558 }
1559 }
1560
1561 if ( ! function_exists( 'etn_is_tickets_sold_out' ) ) {
1562 /**
1563 * Check whether the requested tickets are sold out (stock exhausted).
1564 *
1565 * Returns true when every requested ticket type has no remaining stock,
1566 * or a WP_Error naming the ticket type(s) that still have availability.
1567 * Tickets with no capacity limit (available = 0) are treated as unlimited
1568 * and therefore never considered sold out.
1569 *
1570 * @param integer $event_id Event ID.
1571 * @param array $order_tickets Tickets array from the order request.
1572 *
1573 * @return true|WP_Error
1574 */
1575 function etn_is_tickets_sold_out( $event_id, $order_tickets ) {
1576 $event = new Event_Model( $event_id );
1577 $sold_tickets = (array) Helper::etn_get_sold_tickets_by_event( $event_id );
1578
1579 $enable_global_stock = get_post_meta( $event_id, 'etn_enable_global_stock', true );
1580 $global_stock = intval( get_post_meta( $event_id, 'etn_global_stock', true ) );
1581
1582 if ( $enable_global_stock && $global_stock > 0 ) {
1583 $total_sold = array_sum( $sold_tickets );
1584 $ticket_vars = $event->etn_ticket_variations;
1585 $total_pending = 0;
1586 if ( is_array( $ticket_vars ) ) {
1587 foreach ( $ticket_vars as $ticket_var ) {
1588 $total_pending += intval( $ticket_var['pending'] ?? 0 );
1589 }
1590 }
1591 $remaining = $global_stock - $total_sold - $total_pending;
1592 if ( $remaining > 0 ) {
1593 return new WP_Error(
1594 'tickets_available',
1595 sprintf(
1596 /* translators: %d: number of tickets still available */
1597 __( 'Tickets are still available (%d remaining). You can only join the waiting list when the event is sold out.', 'eventin' ),
1598 $remaining
1599 ),
1600 [ 'status' => 400 ]
1601 );
1602 }
1603 return true;
1604 }
1605
1606 // Per-ticket check.
1607 foreach ( $order_tickets as $ticket ) {
1608 $event_ticket = $event->get_ticket( $ticket['ticket_slug'] );
1609
1610 if ( ! $event_ticket ) {
1611 continue;
1612 }
1613
1614 $available = intval( $event_ticket['etn_avaiilable_tickets'] ?? 0 );
1615
1616 // Skip tickets with no capacity limit (unlimited).
1617 if ( $available <= 0 ) {
1618 continue;
1619 }
1620
1621 $sold = intval( $sold_tickets[ $ticket['ticket_slug'] ] ?? 0 );
1622 $pending = intval( $event_ticket['pending'] ?? 0 );
1623 $left = $available - $sold - $pending;
1624
1625 if ( $left > 0 ) {
1626 return new WP_Error(
1627 'tickets_available',
1628 sprintf(
1629 /* translators: 1: ticket name, 2: number of tickets still available */
1630 __( 'Tickets are still available for "%1$s" (%2$d remaining). You can only join the waiting list when tickets are sold out.', 'eventin' ),
1631 $event_ticket['etn_ticket_name'] ?? $ticket['ticket_slug'],
1632 $left
1633 ),
1634 [ 'status' => 400 ]
1635 );
1636 }
1637 }
1638
1639 return true;
1640 }
1641 }
1642
1643 if ( ! function_exists( 'etn_validate_seat_ids' ) ) {
1644 /**
1645 * Validate seat ids
1646 *
1647 * @param array $seat_ids Seat ids
1648 *
1649 * @return bool | WP_Error
1650 */
1651 function etn_validate_seat_ids( $event_id, $seat_ids ) {
1652 $booked_seats = etn_safe_decode( get_post_meta( $event_id, '_etn_seat_unique_id', true ));
1653 $already_booked_seats = $booked_seats ? explode(',', $booked_seats) : [];
1654 $pending_seats = etn_safe_decode( get_post_meta( $event_id, 'pending_seats', true ));
1655 if ( empty( $pending_seats ) ) {
1656 $pending_seats = [];
1657 }
1658 $is_enable_payment_timer = etn_get_option( 'ticket_purchase_timer_enable', 'off' );
1659
1660 foreach ( $seat_ids as $seat_id ) {
1661 // need to handle the corner in rush condition
1662 // if ( !in_array( $seat_id, $pending_seats ) && $is_enable_payment_timer == 'on') {
1663 // return new WP_Error( 'seat_limit', __( 'The requested seat is already booked, please select another seat', 'eventin' ), ['status' => 422] );
1664 // }
1665 if ( in_array( $seat_id, $already_booked_seats ) ) {
1666 return new WP_Error( 'seat_limit', __( 'The requested seat is already booked', 'eventin' ), ['status' => 422] );
1667 }
1668 }
1669
1670 return true;
1671 }
1672 }
1673
1674 if ( ! function_exists('etn_humanize_number') ) {
1675
1676 function etn_humanize_number( $number ) {
1677 if (!is_numeric($number)) {
1678 return $number;
1679 }
1680
1681 $number = (int)$number;
1682
1683 if ($number >= 1000000000) {
1684 return round($number / 1000000000, 1) . 'b';
1685 }
1686
1687 if ($number >= 1000000) {
1688 return round($number / 1000000, 1) . 'm';
1689 }
1690
1691 if ($number >= 1000) {
1692 return round($number / 1000, 1) . 'k';
1693 }
1694
1695 return $number;
1696 }
1697 }
1698
1699
1700
1701 if ( ! function_exists('is_event_template_builder') ) {
1702
1703 function is_event_template_builder() {
1704 $current_post_id = get_the_ID();
1705
1706 $post_type = get_post_type($current_post_id);
1707
1708 if ( $post_type != 'etn-template' ) {
1709 return false;
1710 }
1711
1712 return $current_post_id;
1713 }
1714 }
1715
1716 if ( ! function_exists('get_first_published_event') ) {
1717
1718 function get_first_published_event() {
1719 $query = new WP_Query( [
1720 'post_type' => 'etn',
1721 'post_status' => 'publish',
1722 'posts_per_page' => 1,
1723 'orderby' => 'date',
1724 'order' => 'ASC'
1725 ] );
1726
1727 $first_post_id = $query->have_posts() ? $query->posts[0]->ID : 0;
1728
1729 return $first_post_id;
1730 }
1731 }
1732
1733 if ( ! function_exists( 'etn_get_selected_template_builder' ) ) {
1734 /**
1735 * Get name of the selected template builder
1736 * If selected template builder is deactive or null (not selected) then return emptry string
1737 */
1738 function etn_get_selected_template_builder() {
1739 $selected_template_builder = etn_get_option( 'selected_template_builder' ) ?? '';
1740
1741 if ( $selected_template_builder == 'elementor' ) {
1742 if ( class_exists( '\Elementor\Plugin' ) ) {
1743 return 'elementor';
1744 }
1745
1746 return '';
1747 }
1748
1749 return $selected_template_builder;
1750 }
1751 }
1752
1753 if ( ! function_exists( 'etn_get_static_event_templates' ) ) {
1754 /**
1755 * Returns list of static event (landing page) templates.
1756 *
1757 * @return array
1758 */
1759 function etn_get_static_event_templates() {
1760 return \Eventin\Template\StaticTemplateConfig::by_type( 'event' );
1761 }
1762 }
1763
1764 if ( ! function_exists( 'etn_get_static_ticket_templates' ) ) {
1765 /**
1766 * Returns list of static ticket templates.
1767 *
1768 * @return array
1769 */
1770 function etn_get_static_ticket_templates() {
1771 return \Eventin\Template\StaticTemplateConfig::by_type( 'ticket' );
1772 }
1773 }
1774
1775 if ( ! function_exists( 'etn_get_static_certificate_templates' ) ) {
1776 /**
1777 * Returns list of static certificate templates.
1778 *
1779 * @return array
1780 */
1781 function etn_get_static_certificate_templates() {
1782 return \Eventin\Template\StaticTemplateConfig::by_type( 'certificate' );
1783 }
1784 }
1785
1786 if ( ! function_exists( 'etn_get_static_speaker_templates' ) ) {
1787 /**
1788 * Returns list of static speaker templates.
1789 *
1790 * @return array
1791 */
1792 function etn_get_static_speaker_templates() {
1793 return \Eventin\Template\StaticTemplateConfig::by_type( 'speaker' );
1794 }
1795 }
1796
1797 if ( ! function_exists( 'etn_get_static_templates_by_type' ) ) {
1798 /**
1799 * Returns static templates for a given type, or every type when empty.
1800 *
1801 * Backed by \Eventin\Template\StaticTemplateConfig — edit that config to
1802 * change the starter template list or its free/pro flags.
1803 *
1804 * @param string $type
1805 *
1806 * @return array
1807 */
1808 function etn_get_static_templates_by_type( $type = '' ) {
1809 return \Eventin\Template\StaticTemplateConfig::by_type( $type );
1810 }
1811 }
1812
1813 if ( ! function_exists( 'etn_get_valid_event_socials' ) ) {
1814 /**
1815 * Filter an event's social links down to the renderable ones.
1816 *
1817 * The event form seeds an empty row so the repeater always has an input to
1818 * type into, so `etn_event_socials` can hold entries with no icon and no
1819 * URL at all. A link needs both to be renderable: without a URL there is
1820 * nowhere to go, and without an icon the <a> paints as an empty chip (the
1821 * bare circle next to "Share:"). Every render path filters through this.
1822 *
1823 * @param mixed $socials Raw etn_event_socials meta.
1824 *
1825 * @return array Entries that have both an icon and a URL. Keys are reindexed.
1826 */
1827 function etn_get_valid_event_socials( $socials ) {
1828 if ( ! is_array( $socials ) ) {
1829 return [];
1830 }
1831
1832 $valid = array_filter(
1833 $socials,
1834 function ( $social ) {
1835 return is_array( $social )
1836 && ! empty( $social['icon'] )
1837 && ! empty( $social['etn_social_url'] );
1838 }
1839 );
1840
1841 return array_values( $valid );
1842 }
1843 }
1844
1845 if ( ! function_exists( 'etn_readable_post_text' ) ) {
1846 /**
1847 * An event's body or excerpt, blanked when the visitor may not read it.
1848 *
1849 * Listings show a snippet of each event: the calendar endpoint, the related
1850 * events strip, the Elementor calendar widget, the .ics download. A
1851 * password-protected event is still `publish`, so it turns up in all of them
1852 * like any other event — and printing its text raw handed out exactly what
1853 * the password exists to hide.
1854 *
1855 * WordPress guards its own equivalents this way: get_the_content() returns
1856 * the password form and get_the_excerpt() returns a placeholder when
1857 * post_password_required() is true. These call sites read the column
1858 * directly, so they need the test spelled out.
1859 *
1860 * @param int|\WP_Post $post Post or post id.
1861 * @param string $field 'post_content' (default) or 'post_excerpt'.
1862 *
1863 * @return string Empty string when the visitor may not see it.
1864 */
1865 function etn_readable_post_text( $post, $field = 'post_content' ) {
1866 $post = get_post( $post );
1867
1868 if ( ! $post ) {
1869 return '';
1870 }
1871
1872 if ( ! \Eventin\AccessControl\Ownership::can_read_protected_content( $post ) ) {
1873 return '';
1874 }
1875
1876 return (string) ( 'post_excerpt' === $field ? $post->post_excerpt : $post->post_content );
1877 }
1878 }
1879