PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
fluent-booking / app / Services / Helper.php

Helper.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Services/Helper.php

2,520 lines 91.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Services;
4
5 use FluentBooking\App\App;
6 use FluentBooking\App\Models\Booking;
7 use FluentBooking\App\Models\Calendar;
8 use FluentBooking\App\Models\CalendarSlot;
9 use FluentBooking\App\Models\Meta;
10 use FluentBooking\App\Models\BookingMeta;
11 use FluentBooking\App\Modules\MCP\Support\SlotLock;
12 use FluentBooking\Framework\Support\Arr;
13
14 class Helper
15 {
16 public static $reserved = [
17 '0',
18 'about',
19 'access',
20 'account',
21 'accounts',
22 'activate',
23 'activities',
24 'activity',
25 'ad',
26 'add',
27 'address',
28 'adm',
29 'admin',
30 'administration',
31 'administrator',
32 'ads',
33 'adult',
34 'advertising',
35 'affiliate',
36 'affiliates',
37 'ajax',
38 'all',
39 'alpha',
40 'analysis',
41 'analytics',
42 'android',
43 'anon',
44 'anonymous',
45 'api',
46 'app',
47 'apps',
48 'archive',
49 'archives',
50 'article',
51 'asct',
52 'asset',
53 'atom',
54 'auth',
55 'authentication',
56 'avatar',
57 'backup',
58 'balancer-manager',
59 'banner',
60 'banners',
61 'beta',
62 'billing',
63 'bin',
64 'blog',
65 'blogs',
66 'board',
67 'book',
68 'bookmark',
69 'bot',
70 'bots',
71 'bug',
72 'business',
73 'cache',
74 'cadastro',
75 'calendar',
76 'call',
77 'campaign',
78 'cancel',
79 'captcha',
80 'career',
81 'careers',
82 'cart',
83 'categories',
84 'category',
85 'cgi',
86 'cgi-bin',
87 'changelog',
88 'chat',
89 'check',
90 'checking',
91 'checkout',
92 'client',
93 'cliente',
94 'clients',
95 'code',
96 'codereview',
97 'comercial',
98 'comment',
99 'comments',
100 'communities',
101 'community',
102 'company',
103 'compare',
104 'compras',
105 'config',
106 'configuration',
107 'connect',
108 'contact',
109 'contact-us',
110 'contact_us',
111 'contactus',
112 'contest',
113 'contribute',
114 'corp',
115 'create',
116 'css',
117 'dashboard',
118 'data',
119 'db',
120 'default',
121 'delete',
122 'demo',
123 'design',
124 'designer',
125 'destroy',
126 'dev',
127 'devel',
128 'developer',
129 'developers',
130 'diagram',
131 'diary',
132 'dict',
133 'dictionary',
134 'die',
135 'dir',
136 'direct_messages',
137 'directory',
138 'dist',
139 'doc',
140 'docs',
141 'documentation',
142 'domain',
143 'download',
144 'downloads',
145 'ecommerce',
146 'edit',
147 'editor',
148 'edu',
149 'education',
150 'email',
151 'employment',
152 'empty',
153 'end',
154 'enterprise',
155 'entries',
156 'entry',
157 'error',
158 'errors',
159 'eval',
160 'event',
161 'exit',
162 'explore',
163 'facebook',
164 'faq',
165 'favorite',
166 'favorites',
167 'feature',
168 'features',
169 'feed',
170 'feedback',
171 'feeds',
172 'file',
173 'files',
174 'first',
175 'flash',
176 'fleet',
177 'fleets',
178 'flog',
179 'follow',
180 'followers',
181 'following',
182 'forgot',
183 'form',
184 'forum',
185 'forums',
186 'founder',
187 'free',
188 'friend',
189 'friends',
190 'ftp',
191 'gadget',
192 'gadgets',
193 'game',
194 'games',
195 'get',
196 'ghost',
197 'gift',
198 'gifts',
199 'gist',
200 'github',
201 'graph',
202 'group',
203 'groups',
204 'guest',
205 'guests',
206 'help',
207 'home',
208 'homepage',
209 'host',
210 'hosting',
211 'hostmaster',
212 'hostname',
213 'howto',
214 'hpg',
215 'html',
216 'http',
217 'httpd',
218 'https',
219 'i',
220 'iamges',
221 'icon',
222 'icons',
223 'id',
224 'idea',
225 'ideas',
226 'image',
227 'images',
228 'imap',
229 'img',
230 'index',
231 'indice',
232 'info',
233 'information',
234 'inquiry',
235 'instagram',
236 'intranet',
237 'invitations',
238 'invite',
239 'ipad',
240 'iphone',
241 'irc',
242 'is',
243 'issue',
244 'issues',
245 'it',
246 'item',
247 'items',
248 'java',
249 'javascript',
250 'job',
251 'jobs',
252 'join',
253 'js',
254 'json',
255 'jump',
256 'knowledgebase',
257 'language',
258 'languages',
259 'last',
260 'ldap-status',
261 'legal',
262 'license',
263 'link',
264 'links',
265 'linux',
266 'list',
267 'lists',
268 'log',
269 'log-in',
270 'log-out',
271 'log_in',
272 'log_out',
273 'login',
274 'logout',
275 'logs',
276 'm',
277 'mac',
278 'mail',
279 'mail1',
280 'mail2',
281 'mail3',
282 'mail4',
283 'mail5',
284 'mailer',
285 'mailing',
286 'maintenance',
287 'manager',
288 'manual',
289 'map',
290 'maps',
291 'marketing',
292 'master',
293 'me',
294 'media',
295 'member',
296 'members',
297 'message',
298 'messages',
299 'messenger',
300 'microblog',
301 'microblogs',
302 'mine',
303 'mis',
304 'mob',
305 'mobile',
306 'movie',
307 'movies',
308 'mp3',
309 'msg',
310 'msn',
311 'music',
312 'musicas',
313 'mx',
314 'my',
315 'mysql',
316 'name',
317 'named',
318 'nan',
319 'navi',
320 'navigation',
321 'net',
322 'network',
323 'new',
324 'news',
325 'newsletter',
326 'nick',
327 'nickname',
328 'notes',
329 'noticias',
330 'notification',
331 'notifications',
332 'notify',
333 'ns',
334 'ns1',
335 'ns10',
336 'ns2',
337 'ns3',
338 'ns4',
339 'ns5',
340 'ns6',
341 'ns7',
342 'ns8',
343 'ns9',
344 'null',
345 'oauth',
346 'oauth_clients',
347 'offer',
348 'offers',
349 'official',
350 'old',
351 'online',
352 'openid',
353 'operator',
354 'order',
355 'orders',
356 'organization',
357 'organizations',
358 'overview',
359 'owner',
360 'owners',
361 'page',
362 'pager',
363 'pages',
364 'panel',
365 'password',
366 'payment',
367 'perl',
368 'phone',
369 'photo',
370 'photoalbum',
371 'photos',
372 'php',
373 'phpmyadmin',
374 'phppgadmin',
375 'phpredisadmin',
376 'pic',
377 'pics',
378 'ping',
379 'plan',
380 'plans',
381 'plugin',
382 'plugins',
383 'policy',
384 'pop',
385 'pop3',
386 'popular',
387 'portal',
388 'post',
389 'postfix',
390 'postmaster',
391 'posts',
392 'pr',
393 'premium',
394 'press',
395 'price',
396 'pricing',
397 'privacy',
398 'privacy-policy',
399 'privacy_policy',
400 'privacypolicy',
401 'private',
402 'product',
403 'products',
404 'profile',
405 'project',
406 'projects',
407 'promo',
408 'pub',
409 'public',
410 'purpose',
411 'put',
412 'python',
413 'query',
414 'random',
415 'ranking',
416 'read',
417 'readme',
418 'recent',
419 'recruit',
420 'recruitment',
421 'register',
422 'registration',
423 'release',
424 'remove',
425 'replies',
426 'report',
427 'reports',
428 'repositories',
429 'repository',
430 'req',
431 'request',
432 'requests',
433 'reset',
434 'roc',
435 'rss',
436 'ruby',
437 'rule',
438 'sag',
439 'sale',
440 'sales',
441 'sample',
442 'samples',
443 'save',
444 'school',
445 'script',
446 'scripts',
447 'search',
448 'secure',
449 'security',
450 'self',
451 'send',
452 'server',
453 'server-info',
454 'server-status',
455 'service',
456 'services',
457 'session',
458 'sessions',
459 'setting',
460 'settings',
461 'setup',
462 'share',
463 'shop',
464 'show',
465 'sign-in',
466 'sign-up',
467 'sign_in',
468 'sign_up',
469 'signin',
470 'signout',
471 'signup',
472 'site',
473 'sitemap',
474 'sites',
475 'smartphone',
476 'smtp',
477 'soporte',
478 'source',
479 'spec',
480 'special',
481 'sql',
482 'src',
483 'ssh',
484 'ssl',
485 'ssladmin',
486 'ssladministrator',
487 'sslwebmaster',
488 'staff',
489 'stage',
490 'staging',
491 'start',
492 'stat',
493 'state',
494 'static',
495 'stats',
496 'status',
497 'store',
498 'stores',
499 'stories',
500 'style',
501 'styleguide',
502 'stylesheet',
503 'stylesheets',
504 'subdomain',
505 'subscribe',
506 'subscriptions',
507 'suporte',
508 'support',
509 'svn',
510 'swf',
511 'sys',
512 'sysadmin',
513 'sysadministrator',
514 'system',
515 'tablet',
516 'tablets',
517 'tag',
518 'talk',
519 'task',
520 'tasks',
521 'team',
522 'teams',
523 'tech',
524 'telnet',
525 'term',
526 'terms',
527 'terms-of-service',
528 'terms_of_service',
529 'termsofservice',
530 'test',
531 'test1',
532 'test2',
533 'test3',
534 'teste',
535 'testing',
536 'tests',
537 'theme',
538 'themes',
539 'thread',
540 'threads',
541 'tmp',
542 'todo',
543 'tool',
544 'tools',
545 'top',
546 'topic',
547 'topics',
548 'tos',
549 'tour',
550 'translations',
551 'trends',
552 'tutorial',
553 'tux',
554 'tv',
555 'twitter',
556 'undef',
557 'unfollow',
558 'unsubscribe',
559 'update',
560 'upload',
561 'uploads',
562 'url',
563 'usage',
564 'user',
565 'username',
566 'users',
567 'usuario',
568 'vendas',
569 'ver',
570 'version',
571 'video',
572 'videos',
573 'visitor',
574 'watch',
575 'weather',
576 'web',
577 'webhook',
578 'webhooks',
579 'webmail',
580 'webmaster',
581 'website',
582 'websites',
583 'welcome',
584 'widget',
585 'widgets',
586 'wiki',
587 'win',
588 'windows',
589 'word',
590 'work',
591 'works',
592 'workshop',
593 'ww',
594 'wws',
595 'www',
596 'www1',
597 'www2',
598 'www3',
599 'www4',
600 'www5',
601 'www6',
602 'www7',
603 'wwws',
604 'wwww',
605 'xfn',
606 'xml',
607 'xmpp',
608 'xpg',
609 'xxx',
610 'yaml',
611 'year',
612 'yml',
613 'you',
614 'yourdomain',
615 'yourname',
616 'yoursite',
617 'yourusername'
618 ];
619
620 public static function isCalendarSlugAvailable($slug, $checkDb = true, $exceptId = false)
621 {
622 if (in_array($slug, self::$reserved)) {
623 return false;
624 }
625
626 if (strlen($slug) < 4) {
627 return false;
628 }
629
630 if ($checkDb) {
631
632 if ($exceptId) {
633 $exist = Calendar::where('slug', $slug)->where('id', '!=', $exceptId)->first();
634 } else {
635 $exist = Calendar::where('slug', $slug)->first();
636 }
637
638 if ($exist) {
639 return false;
640 }
641 }
642
643
644 if (is_numeric($slug)) {
645 return false;
646 }
647
648 // check if $slug has any special characters or any space. We will only allow alpha-numeric values
649 return preg_match('/^[a-zA-Z0-9_-]+$/', $slug);
650 }
651
652 public static function isEventSlugAvailable($slug, $checkDb = true, $calendarId = null, $exceptId = null)
653 {
654 if (in_array($slug, self::$reserved)) {
655 return false;
656 }
657
658 if (strlen($slug) < 4) {
659 return false;
660 }
661
662 if ($checkDb) {
663 $eventQuery = CalendarSlot::where('slug', $slug);
664
665 if ($exceptId) {
666 $eventQuery->where('id', '!=', $exceptId);
667 }
668
669 if ($calendarId) {
670 $eventQuery->where('calendar_id', $calendarId);
671 }
672
673 $exist = $eventQuery->first();
674
675 if ($exist) {
676 return false;
677 }
678 }
679
680 if (is_numeric($slug)) {
681 return false;
682 }
683
684 // check if $slug has any special characters or any space. We will only allow alpha-numeric values
685 return preg_match('/^[a-zA-Z0-9_-]+$/', $slug);
686 }
687
688 public static function getAppBaseUrl($extension = '')
689 {
690 return apply_filters('fluent_booking/admin_base_url', admin_url('admin.php?page=fluent-booking#/' . $extension), $extension);
691 }
692
693 public static function getAdminBookingUrl($bookingId)
694 {
695 return self::getAppBaseUrl('scheduled-events?booking_id=' . $bookingId);
696 }
697
698 /**
699 * Build a spec-compliant "Upgrade to Pro" URL.
700 *
701 * Follows the shared Fluent* UTM spec:
702 * utm_source = fluent-booking (fixed vocabulary, never the wp.org slug)
703 * utm_medium = free_plugin | pro_plugin (acquisition vs cross-sell)
704 * utm_campaign= upgrade_pro (override for xsell_<target> / license_* )
705 * utm_content = the exact placement, e.g. feature_lock_team_calendar, upgrade_page
706 * utm_term = plugin version that generated the link
707 * utm_id = promo id, blank normally (omit unless passed)
708 *
709 * @param string $content The utm_content placement.
710 * @param array $overrides Override any utm_* param (e.g. utm_campaign for cross-sell).
711 * @return string
712 */
713 public static function getUpgradeUrl($content = 'upgrade_page', $overrides = [])
714 {
715 $baseUrl = apply_filters(
716 'fluent_booking/pro_upgrade_base_url',
717 'https://fluentbooking.com/pricing/'
718 );
719
720 $params = wp_parse_args($overrides, [
721 'utm_source' => 'fluent-booking',
722 'utm_medium' => defined('FLUENT_BOOKING_PRO_VERSION') ? 'pro_plugin' : 'free_plugin',
723 'utm_campaign' => 'upgrade_pro',
724 'utm_content' => $content,
725 'utm_term' => FLUENT_BOOKING_VERSION,
726 ]);
727
728 // Drop any blank params (e.g. an unset utm_id) so they never hit the URL.
729 $params = array_filter($params, function ($value) {
730 return $value !== '' && $value !== null;
731 });
732
733 return add_query_arg($params, $baseUrl);
734 }
735
736 public static function getNextBookingGroup()
737 {
738 $lastBooking = Booking::orderBy('group_id', 'desc')->first(['group_id']);
739
740 if ($lastBooking) {
741 return $lastBooking->group_id + 1;
742 }
743
744 return 1;
745 }
746
747 public static function getNextIndex()
748 {
749 static $index = 0;
750
751 $index += 1;
752
753 return $index;
754 }
755
756 public static function getGlobalPaymentSettings()
757 {
758 static $settings;
759
760 if ($settings) {
761 return $settings;
762 }
763
764 $settings = get_option('fluent_booking_global_payment_settings', []);
765
766 return $settings;
767 }
768
769 public static function isPaymentEnabled($calendarEvent = null)
770 {
771 $settings = CurrenciesHelper::getGlobalCurrencySettings();
772 if (Arr::get($settings, 'is_active') == 'yes') {
773 return true;
774 }
775
776 if ($calendarEvent) {
777 if ($calendarEvent->type != 'paid') {
778 return false;
779 }
780
781 $exist = Meta::where('object_type', 'calendar_slot')
782 ->where('object_id', $calendarEvent->id)
783 ->where('key', 'payment_settings')
784 ->first();
785
786 return $exist && $exist->value && Arr::get($exist->value, 'enabled') == 'yes';
787 }
788
789 return false;
790 }
791
792 public static function isPaymentConfigured($method = 'stripe')
793 {
794 $settings = get_option('fluent_booking_payment_settings_' . $method, []);
795
796 return Arr::get($settings, 'is_active', 'no') == 'yes';
797 }
798
799 /**
800 * Sanitize form inputs recursively.
801 *
802 * @param $input
803 *
804 * @return mixed $input
805 */
806 public static function fluentbookingSanitizer($input, $attribute = null, $fields = [])
807 {
808 if (is_string($input)) {
809 $element = Arr::get($fields, $attribute . '.element');
810
811 if (in_array($element, ['post_content', 'rich_text_input'])) {
812 return wp_kses_post($input);
813 } elseif ('textarea' === $element) {
814 $input = sanitize_textarea_field($input);
815 } elseif ('input_email' === $element) {
816 $input = strtolower(sanitize_text_field($input));
817 } elseif ('input_url' === $element) {
818 $input = sanitize_url($input);
819 } else {
820 $input = sanitize_text_field($input);
821 }
822 } elseif (is_array($input)) {
823 foreach ($input as $key => &$value) {
824 $attribute = $attribute ? $attribute . '[' . $key . ']' : $key;
825
826 $value = self::fluentbookingSanitizer($value, $attribute, $fields);
827
828 $attribute = null;
829 }
830 }
831
832 return $input;
833 }
834
835 public static function getMeta($group, $objectId, $key, $withModel = false)
836 {
837 $meta = Meta::where('object_type', $group)
838 ->where('object_id', $objectId)
839 ->where('key', $key)
840 ->first();
841
842 if ($meta) {
843 if ($withModel) {
844 return $meta;
845 }
846
847 return $meta->value;
848 }
849
850 return null;
851 }
852
853 public static function updateMeta($group, $objectId, $key, $value)
854 {
855 $meta = self::getMeta($group, $objectId, $key, true);
856
857 if ($meta) {
858 $meta->value = $value;
859 $meta->save();
860 return $meta;
861 }
862
863 return Meta::create([
864 'object_type' => $group,
865 'key' => $key,
866 'object_id' => $objectId,
867 'value' => $value
868 ]);
869 }
870
871 public static function deleteMeta($group, $objectId, $key)
872 {
873 return Meta::where('object_type', $group)
874 ->where('object_id', $objectId)
875 ->where('key', $key)
876 ->delete();
877 }
878
879 public static function getBookingMeta($eventId, $metaKey, $withModel = false)
880 {
881 $bookingMeta = BookingMeta::where('booking_id', $eventId)
882 ->where('meta_key', $metaKey)
883 ->first();
884
885 if ($bookingMeta) {
886 return $withModel ? $bookingMeta : $bookingMeta->value;
887 }
888
889 return null;
890 }
891
892 public static function updateBookingMeta($eventId, $metaKey, $value)
893 {
894 $bookingMeta = self::getBookingMeta($eventId, $metaKey, true);
895
896 if ($bookingMeta) {
897 $bookingMeta->value = $value;
898 $bookingMeta->save();
899 return $bookingMeta;
900 }
901
902 return BookingMeta::create([
903 'booking_id' => $eventId,
904 'meta_key' => $metaKey, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
905 'value' => $value
906 ]);
907 }
908
909 public static function getUserDisplayName($userId = null)
910 {
911 if (!$userId) {
912 $userId = get_current_user_id();
913 }
914
915 if (!$userId) {
916 return '';
917 }
918
919 $user = get_user_by('ID', $userId);
920
921 return self::getDisplayNameFromUser($user);
922 }
923
924 public static function getDisplayNameFromUser($user)
925 {
926 if (!$user) {
927 return '';
928 }
929
930 $firstName = is_object($user) ? ($user->first_name ?? '') : '';
931 $lastName = is_object($user) ? ($user->last_name ?? '') : '';
932
933 $name = trim($firstName . ' ' . $lastName);
934
935 if ($name !== '') {
936 return $name;
937 }
938
939 $displayName = is_object($user) ? ($user->display_name ?? '') : '';
940
941 return (string) $displayName;
942 }
943
944 public static function getUserEmail($userId = null)
945 {
946 $userId = $userId ?: get_current_user_id();
947
948 if (!$userId) {
949 return '';
950 }
951
952 $user = get_user_by('ID', $userId);
953
954 return $user->user_email;
955 }
956
957 public static function excerpt($text, $max_length = 160)
958 {
959 // Strip HTML tags and convert entities to their corresponding characters
960 $text = html_entity_decode(wp_strip_all_tags($text));
961
962 // Remove any line breaks, tabs, or extra whitespace
963 $text = preg_replace('/\s+/', ' ', trim($text));
964
965 if (mb_strlen($text) > $max_length) {
966 $text = mb_substr($text, 0, $max_length);
967 $text = preg_replace('/\s+\S+$/', '', $text) . '...';
968 }
969
970 return $text;
971 }
972
973 public static function generateSlotSlug($default, $calendar)
974 {
975 $original = sanitize_title($default, $default, 'display');
976
977 $default = $original;
978
979 $counter = 1;
980
981 while (CalendarSlot::where('calendar_id', $calendar->id)->where('slug', $default)->first()) {
982 $default = $original . '-' . $counter;
983 $counter += 1;
984 }
985
986 return apply_filters('fluent_booking/slot_slug', $default, $original);
987 }
988
989
990 public static function getIp($defalt = '127.0.0.1')
991 {
992 static $ipAddress;
993
994 if ($ipAddress) {
995 return $ipAddress;
996 }
997
998 if (empty($_SERVER['REMOTE_ADDR'])) {
999 // It's a local cli request
1000 return $defalt;
1001 }
1002
1003 $ipAddress = self::resolveClientIp($_SERVER);
1004
1005 $ipAddress = apply_filters('fluent_booking/user_ip', $ipAddress, []);
1006
1007 $ipAddress = sanitize_text_field(wp_unslash($ipAddress));
1008
1009 return $ipAddress;
1010 }
1011
1012 /**
1013 * Pick the client address out of a request's server vars.
1014 *
1015 * Kept apart from getIp(), which caches its answer for the request, so the
1016 * header-trust rules can be exercised one request shape at a time.
1017 *
1018 * @param array $serverData $_SERVER or an equivalent
1019 * @return string
1020 */
1021 public static function resolveClientIp($serverData)
1022 {
1023 $remoteAddr = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', (string)Arr::get($serverData, 'REMOTE_ADDR'));
1024 $cloudflareIp = (string)Arr::get($serverData, 'HTTP_CF_CONNECTING_IP');
1025 $trustedProxies = (array)apply_filters('fluent_booking/trusted_proxies', ['127.0.0.1']);
1026
1027 // A proxy appends the peer it saw, so only the right-most hop is reliable;
1028 // anything to its left is whatever the client chose to send
1029 $hops = explode(',', (string)Arr::get($serverData, 'HTTP_X_FORWARDED_FOR'));
1030 $forwardedIp = rest_is_ip_address(trim(end($hops)));
1031
1032 if ($cloudflareIp && self::isCfIp($remoteAddr)) {
1033 return $cloudflareIp;
1034 }
1035
1036 if ($forwardedIp && in_array($remoteAddr, $trustedProxies, true)) {
1037 return $forwardedIp;
1038 }
1039
1040 return $remoteAddr;
1041 }
1042
1043 /**
1044 * Valid E.164 number: 7-15 significant digits.
1045 *
1046 * @param string $phone
1047 * @return bool
1048 */
1049 public static function isValidPhoneNumber($phone)
1050 {
1051 if (!apply_filters('fluent_booking/enforce_phone_validation', true)) {
1052 return true;
1053 }
1054
1055 $phone = trim((string)$phone);
1056
1057 if ($phone === '') {
1058 return false;
1059 }
1060
1061 if (!preg_match('/^\+?[0-9\s().\-]+$/', $phone)) {
1062 return false;
1063 }
1064
1065 $digitCount = strlen(preg_replace('/\D/', '', $phone));
1066
1067 $min = (int)apply_filters('fluent_booking/phone_min_digits', 7);
1068 $max = (int)apply_filters('fluent_booking/phone_max_digits', 15);
1069
1070 return $digitCount >= $min && $digitCount <= $max;
1071 }
1072
1073 private static function isCfIp($ip = '')
1074 {
1075 if (!$ip) {
1076 $serverData = $_SERVER;
1077 $REMOTE_ADDR = Arr::get($serverData, 'REMOTE_ADDR');
1078 $ip = $REMOTE_ADDR;
1079 }
1080 $cloudflareIPRanges = array(
1081 '173.245.48.0/20',
1082 '103.21.244.0/22',
1083 '103.22.200.0/22',
1084 '103.31.4.0/22',
1085 '141.101.64.0/18',
1086 '108.162.192.0/18',
1087 '190.93.240.0/20',
1088 '188.114.96.0/20',
1089 '197.234.240.0/22',
1090 '198.41.128.0/17',
1091 '162.158.0.0/15',
1092 '104.16.0.0/13',
1093 '104.24.0.0/14',
1094 '172.64.0.0/13',
1095 '131.0.72.0/22',
1096 );
1097 //Make sure that the request came via Cloudflare.
1098 foreach ($cloudflareIPRanges as $range) {
1099 //Use the ip_in_range function from Joomla.
1100 if (self::ipInRange($ip, $range)) {
1101 //IP is valid. Belongs to Cloudflare.
1102 return true;
1103 }
1104 }
1105
1106 return false;
1107 }
1108
1109 private static function ipInRange($ip, $range)
1110 {
1111 if (strpos($range, '/') !== false) {
1112 // $range is in IP/NETMASK format
1113 list($range, $netmask) = explode('/', $range, 2);
1114 if (strpos($netmask, '.') !== false) {
1115 // $netmask is a 255.255.0.0 format
1116 $netmask = str_replace('*', '0', $netmask);
1117 $netmask_dec = ip2long($netmask);
1118 return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
1119 } else {
1120 // $netmask is a CIDR size block
1121 // fix the range argument
1122 $x = explode('.', $range);
1123 while (count($x) < 4) $x[] = '0';
1124 list($a, $b, $c, $d) = $x;
1125 $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
1126 $range_dec = ip2long($range);
1127 $ip_dec = ip2long($ip);
1128
1129 # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
1130 #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
1131
1132 # Strategy 2 - Use math to create it
1133 $wildcard_dec = pow(2, (32 - $netmask)) - 1;
1134 $netmask_dec = ~$wildcard_dec;
1135
1136 return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
1137 }
1138 } else {
1139 // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
1140 if (strpos($range, '*') !== false) { // a.b.*.* format
1141 // Just convert to A-B format by setting * to 0 for A and 255 for B
1142 $lower = str_replace('*', '0', $range);
1143 $upper = str_replace('*', '255', $range);
1144 $range = "$lower-$upper";
1145 }
1146
1147 if (strpos($range, '-') !== false) { // A-B format
1148 list($lower, $upper) = explode('-', $range, 2);
1149 $lower_dec = (float)sprintf("%u", ip2long($lower));
1150 $upper_dec = (float)sprintf("%u", ip2long($upper));
1151 $ip_dec = (float)sprintf("%u", ip2long($ip));
1152 return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
1153 }
1154 return false;
1155 }
1156 }
1157
1158 public static function fcal_sanitize_html($html)
1159 {
1160 if (!$html) {
1161 return $html;
1162 }
1163
1164 // Return $html if it's just a plain text
1165 if (!preg_match('/<[^>]*>/', $html)) {
1166 return $html;
1167 }
1168
1169 $tags = wp_kses_allowed_html('post');
1170 $tags['style'] = [
1171 'types' => [],
1172 ];
1173 // iframe
1174 $tags['iframe'] = [
1175 'width' => [],
1176 'height' => [],
1177 'src' => [],
1178 'title' => [],
1179 'frameborder' => [],
1180 'allow' => [],
1181 'class' => [],
1182 'id' => [],
1183 'allowfullscreen' => [],
1184 'style' => [],
1185 ];
1186 //button
1187 $tags['button'] = [
1188 'onclick' => []
1189 ];
1190
1191 //svg
1192 if (empty($tags['svg'])) {
1193 $svg_args = [
1194 'svg' => [
1195 'class' => true,
1196 'aria-hidden' => true,
1197 'aria-labelledby' => true,
1198 'role' => true,
1199 'xmlns' => true,
1200 'width' => true,
1201 'height' => true,
1202 'viewbox' => true,
1203 ],
1204 'g' => ['fill' => true],
1205 'title' => ['title' => true],
1206 'path' => [
1207 'd' => true,
1208 'fill' => true,
1209 'transform' => true,
1210 ],
1211 ];
1212 $tags = array_merge($tags, $svg_args);
1213 }
1214
1215 $tags = apply_filters('fluent_booking/allowed_html_tags', $tags);
1216
1217 return wp_kses($html, $tags);
1218 }
1219
1220 /**
1221 * Sanitize inputs recursively.
1222 *
1223 * @param array $input
1224 * @param array $sanitizeMap
1225 *
1226 * @return array $input
1227 */
1228 public static function fcal_backend_sanitizer($inputs, $sanitizeMap = [])
1229 {
1230 $originalValues = $inputs;
1231 foreach ($inputs as $key => &$value) {
1232 if (is_array($value)) {
1233 $value = self::fcal_backend_sanitizer($value, $sanitizeMap);
1234 } else {
1235 $method = Arr::get($sanitizeMap, $key);
1236 if (!$method) {
1237 continue;
1238 }
1239 if (is_callable($method)) {
1240 $value = call_user_func($method, $value);
1241 } elseif (method_exists(self::class, $method)) {
1242 $value = call_user_func([self::class, $method], $value);
1243 }
1244 }
1245 }
1246
1247 return apply_filters('fluent_booking/backend_sanitized_values', $inputs, $originalValues);
1248 }
1249
1250 /**
1251 * Recursively implode a multi-dimentional array
1252 *
1253 * @param string $glue
1254 * @param array $array
1255 *
1256 * @return string
1257 */
1258 public static function fcalImplodeRecursive($glue, array $array)
1259 {
1260 $fn = function ($glue, array $array) use (&$fn) {
1261 $result = '';
1262 foreach ($array as $item) {
1263 if (is_array($item)) {
1264 $result .= $fn($glue, $item);
1265 } else {
1266 $result .= $glue . $item;
1267 }
1268 }
1269
1270 return $result;
1271 };
1272
1273 return ltrim($fn($glue, $array), $glue);
1274 }
1275
1276 public static function getEventColors()
1277 {
1278 return apply_filters('fluent_booking/event_colors', [
1279 [
1280 'label' => __('Red-Orange', 'fluent-booking'),
1281 'value' => '#ff4f00'
1282 ],
1283 [
1284 'label' => __('Deep Lilac', 'fluent-booking'),
1285 'value' => '#e55cff'
1286 ],
1287 [
1288 'label' => __('Purple', 'fluent-booking'),
1289 'value' => '#8247f5'
1290 ],
1291 [
1292 'label' => __('Vivid Blue', 'fluent-booking'),
1293 'value' => '#0099ff'
1294 ],
1295 [
1296 'label' => __('Cyan', 'fluent-booking'),
1297 'value' => '#0ae8f0'
1298 ],
1299 [
1300 'label' => __('Emerald Green', 'fluent-booking'),
1301 'value' => '#17e885'
1302 ],
1303 [
1304 'label' => __('Lime Green', 'fluent-booking'),
1305 'value' => '#ccf000'
1306 ],
1307 [
1308 'label' => __('Amber', 'fluent-booking'),
1309 'value' => '#ffa600'
1310 ]
1311 ]);
1312 }
1313
1314 public static function getMeetingDurations()
1315 {
1316 return apply_filters('fluent_booking/meeting_durations_schema', [
1317 [
1318 'value' => '15',
1319 'label' => __('15 Minutes', 'fluent-booking')
1320 ],
1321 [
1322 'value' => '30',
1323 'label' => __('30 Minutes', 'fluent-booking')
1324 ],
1325 [
1326 'value' => '45',
1327 'label' => __('45 Minutes', 'fluent-booking')
1328 ],
1329 [
1330 'value' => '60',
1331 'label' => __('60 Minutes', 'fluent-booking')
1332 ],
1333 [
1334 'value' => 'custom',
1335 'label' => __('Custom', 'fluent-booking')
1336 ]
1337 ]);
1338 }
1339
1340 public static function getMeetingMultiDurations()
1341 {
1342 return apply_filters('fluent_booking/meeting_multi_durations_schema', [
1343 [
1344 'value' => '5',
1345 'label' => __('5 Minutes', 'fluent-booking')
1346 ],
1347 [
1348 'value' => '10',
1349 'label' => __('10 Minutes', 'fluent-booking')
1350 ],
1351 [
1352 'value' => '15',
1353 'label' => __('15 Minutes', 'fluent-booking')
1354 ],
1355 [
1356 'value' => '30',
1357 'label' => __('30 Minutes', 'fluent-booking')
1358 ],
1359 [
1360 'value' => '45',
1361 'label' => __('45 Minutes', 'fluent-booking')
1362 ],
1363 [
1364 'value' => '50',
1365 'label' => __('50 Minutes', 'fluent-booking')
1366 ],
1367 [
1368 'value' => '60',
1369 'label' => __('60 Minutes', 'fluent-booking')
1370 ],
1371 [
1372 'value' => '90',
1373 'label' => __('90 Minutes', 'fluent-booking')
1374 ],
1375 [
1376 'value' => '120',
1377 'label' => __('120 Minutes', 'fluent-booking')
1378 ],
1379 [
1380 'value' => '150',
1381 'label' => __('150 Minutes', 'fluent-booking')
1382 ],
1383 [
1384 'value' => '180',
1385 'label' => __('180 Minutes', 'fluent-booking')
1386 ],
1387 [
1388 'value' => '240',
1389 'label' => __('240 Minutes', 'fluent-booking')
1390 ],
1391 [
1392 'value' => '480',
1393 'label' => __('480 Minutes', 'fluent-booking')
1394 ]
1395 ]);
1396 }
1397
1398 public static function getDurationLookup($multiDuration = false)
1399 {
1400 $durations = $multiDuration ? self::getMeetingMultiDurations() : self::getMeetingDurations();
1401
1402 $durationLookup = [];
1403 foreach ($durations as $duration) {
1404 $durationLookup[$duration['value']] = $duration['label'];
1405 }
1406
1407 return $durationLookup;
1408 }
1409
1410 public static function formatDuration($totalMinutes)
1411 {
1412 $days = floor($totalMinutes / 1440); // 1440 minutes in a day
1413 $hours = floor(($totalMinutes % 1440) / 60);
1414 $minutes = $totalMinutes % 60;
1415
1416 $formattedDuration = [];
1417 if ($days > 0) {
1418 $unit = $days > 1 ? __('Days', 'fluent-booking') : __('Day', 'fluent-booking');
1419 $formattedDuration[] = $days . ' ' . $unit;
1420 }
1421
1422 if ($hours > 0) {
1423 $unit = $hours > 1 ? __('Hours', 'fluent-booking') : __('Hour', 'fluent-booking');
1424 $formattedDuration[] = $hours . ' ' . $unit;
1425 }
1426
1427 if ($minutes > 0 || empty($formattedDuration)) {
1428 $unit = $minutes > 1 ? __('Minutes', 'fluent-booking') : __('Minute', 'fluent-booking');
1429 $formattedDuration[] = $minutes . ' ' . $unit;
1430 }
1431
1432 return implode(' ', $formattedDuration);
1433 }
1434
1435 public static function getBufferTimes()
1436 {
1437 return apply_filters('fluent_booking/buffer_times_schema', [
1438 [
1439 'value' => '0',
1440 'label' => __('No buffer time', 'fluent-booking')
1441 ],
1442 [
1443 'value' => '5',
1444 'label' => __('5 Minutes', 'fluent-booking')
1445 ],
1446 [
1447 'value' => '10',
1448 'label' => __('10 Minutes', 'fluent-booking')
1449 ],
1450 [
1451 'value' => '15',
1452 'label' => __('15 Minutes', 'fluent-booking')
1453 ],
1454 [
1455 'value' => '20',
1456 'label' => __('20 Minutes', 'fluent-booking')
1457 ],
1458 [
1459 'value' => '30',
1460 'label' => __('30 Minutes', 'fluent-booking')
1461 ],
1462 [
1463 'value' => '45',
1464 'label' => __('45 Minutes', 'fluent-booking')
1465 ],
1466 [
1467 'value' => '60',
1468 'label' => __('60 Minutes', 'fluent-booking')
1469 ],
1470 [
1471 'value' => '90',
1472 'label' => __('90 Minutes', 'fluent-booking')
1473 ],
1474 [
1475 'value' => '120',
1476 'label' => __('120 Minutes', 'fluent-booking')
1477 ]
1478 ]);
1479 }
1480
1481 public static function getSlotIntervals()
1482 {
1483 return apply_filters('fluent_booking/slot_intervals_schema', [
1484 [
1485 'value' => '',
1486 'label' => __('Use event length (default)', 'fluent-booking')
1487 ],
1488 [
1489 'value' => '5',
1490 'label' => __('5 Minutes', 'fluent-booking')
1491 ],
1492 [
1493 'value' => '10',
1494 'label' => __('10 Minutes', 'fluent-booking')
1495 ],
1496 [
1497 'value' => '15',
1498 'label' => __('15 Minutes', 'fluent-booking')
1499 ],
1500 [
1501 'value' => '20',
1502 'label' => __('20 Minutes', 'fluent-booking')
1503 ],
1504 [
1505 'value' => '30',
1506 'label' => __('30 Minutes', 'fluent-booking')
1507 ],
1508 [
1509 'value' => '45',
1510 'label' => __('45 Minutes', 'fluent-booking')
1511 ],
1512 [
1513 'value' => '60',
1514 'label' => __('60 Minutes', 'fluent-booking')
1515 ],
1516 [
1517 'value' => '75',
1518 'label' => __('75 Minutes', 'fluent-booking')
1519 ],
1520 [
1521 'value' => '90',
1522 'label' => __('90 Minutes', 'fluent-booking')
1523 ],
1524 [
1525 'value' => '105',
1526 'label' => __('105 Minutes', 'fluent-booking')
1527 ],
1528 [
1529 'value' => '120',
1530 'label' => __('120 Minutes', 'fluent-booking')
1531 ]
1532 ]);
1533 }
1534
1535 public static function getBookingStatusChangingTimes()
1536 {
1537 return apply_filters('fluent_booking/booking_status_changing_times_schema', [
1538 [
1539 'value' => '5',
1540 'label' => __('5 Minutes', 'fluent-booking')
1541 ],
1542 [
1543 'value' => '10',
1544 'label' => __('10 Minutes', 'fluent-booking')
1545 ],
1546 [
1547 'value' => '20',
1548 'label' => __('20 Minutes', 'fluent-booking')
1549 ],
1550 [
1551 'value' => '30',
1552 'label' => __('30 Minutes', 'fluent-booking')
1553 ],
1554 [
1555 'value' => '40',
1556 'label' => __('40 Minutes', 'fluent-booking')
1557 ],
1558 [
1559 'value' => '50',
1560 'label' => __('50 Minutes', 'fluent-booking')
1561 ],
1562 [
1563 'value' => '60',
1564 'label' => __('1 Hour', 'fluent-booking')
1565 ],
1566 [
1567 'value' => '120',
1568 'label' => __('2 Hours', 'fluent-booking')
1569 ],
1570 [
1571 'value' => '180',
1572 'label' => __('3 Hours', 'fluent-booking')
1573 ],
1574 [
1575 'value' => '360',
1576 'label' => __('6 Hours', 'fluent-booking')
1577 ],
1578 [
1579 'value' => '720',
1580 'label' => __('12 Hours', 'fluent-booking')
1581 ],
1582 [
1583 'value' => '1440',
1584 'label' => __('1 Day', 'fluent-booking')
1585 ],
1586 [
1587 'value' => '2880',
1588 'label' => __('2 Days', 'fluent-booking')
1589 ]
1590 ]);
1591 }
1592
1593 public static function getBookingPeriodOptions()
1594 {
1595 return apply_filters('fluent_booking/booking_period_options', [
1596 'upcoming' => __('Upcoming', 'fluent-booking'),
1597 'completed' => __('Completed', 'fluent-booking'),
1598 'pending' => __('Pending', 'fluent-booking'),
1599 'cancelled' => __('Cancelled', 'fluent-booking'),
1600 'all' => __('All', 'fluent-booking'),
1601 ]);
1602 }
1603
1604 public static function getWeekSelectTimes()
1605 {
1606 return apply_filters('fluent_booking/week_select_times_schema', [
1607 'start' => '00:00',
1608 'step' => '00:15',
1609 'end' => '23:45'
1610 ]);
1611 }
1612
1613 public static function getOverrideSelectTimes()
1614 {
1615 return apply_filters('fluent_booking/override_select_times_schema', [
1616 'start' => '00:00',
1617 'step' => '00:15',
1618 'end' => '23:45'
1619 ]);
1620 }
1621
1622 public static function getWeeklyScheduleSchema()
1623 {
1624 return apply_filters('fluent_booking/weekly_schedule_schema', [
1625 'sun' => [
1626 'enabled' => false,
1627 'slots' => []
1628 ],
1629 'mon' => [
1630 'enabled' => true,
1631 'slots' => [
1632 ['start' => '09:00', 'end' => '17:00']
1633 ],
1634 ],
1635 'tue' => [
1636 'enabled' => true,
1637 'slots' => [
1638 ['start' => '09:00', 'end' => '17:00']
1639 ],
1640 ],
1641 'wed' => [
1642 'enabled' => true,
1643 'slots' => [
1644 ['start' => '09:00', 'end' => '17:00']
1645 ],
1646 ],
1647 'thu' => [
1648 'enabled' => true,
1649 'slots' => [
1650 ['start' => '09:00', 'end' => '17:00']
1651 ],
1652 ],
1653 'fri' => [
1654 'enabled' => true,
1655 'slots' => [
1656 ['start' => '09:00', 'end' => '17:00']
1657 ],
1658 ],
1659 'sat' => [
1660 'enabled' => false,
1661 'slots' => []
1662 ],
1663 ]);
1664 }
1665
1666 public static function getCustomFieldTypes()
1667 {
1668 return apply_filters('fluent_booking/custom_fields_types', [
1669 [
1670 'value' => 'email',
1671 'label' => __('Email', 'fluent-booking')
1672 ],
1673 [
1674 'value' => 'text',
1675 'label' => __('Text', 'fluent-booking')
1676 ],
1677 [
1678 'value' => 'textarea',
1679 'label' => __('Textarea', 'fluent-booking')
1680 ],
1681 [
1682 'value' => 'number',
1683 'label' => __('Number', 'fluent-booking')
1684 ],
1685 [
1686 'value' => 'phone',
1687 'label' => __('Phone', 'fluent-booking')
1688 ],
1689 [
1690 'value' => 'radio',
1691 'label' => __('Radio', 'fluent-booking')
1692 ],
1693 [
1694 'value' => 'dropdown',
1695 'label' => __('Select', 'fluent-booking')
1696 ],
1697 [
1698 'value' => 'multi-select',
1699 'label' => __('Multi Select', 'fluent-booking')
1700 ],
1701 [
1702 'value' => 'checkbox',
1703 'label' => __('Checkbox', 'fluent-booking')
1704 ],
1705 [
1706 'value' => 'checkbox-group',
1707 'label' => __('Checkbox Group', 'fluent-booking')
1708 ],
1709 [
1710 'value' => 'date',
1711 'label' => __('Date', 'fluent-booking')
1712 ],
1713 [
1714 'value' => 'file',
1715 'label' => __('File', 'fluent-booking')
1716 ],
1717 [
1718 'value' => 'hidden',
1719 'label' => __('Hidden', 'fluent-booking')
1720 ],
1721 [
1722 'value' => 'terms-and-conditions',
1723 'label' => __('Terms & Conditions', 'fluent-booking')
1724 ]
1725 ]);
1726 }
1727
1728 public static function getDefaultTermsAndConditions()
1729 {
1730 $termsAndConditions = __('I have read and agree to the <a href="#" target="_blank" rel="noopener">Terms and Conditions</a> and <a href="#" target="_blank" rel="noopener">Privacy Policy</a>.', 'fluent-booking');
1731
1732 return apply_filters('fluent_booking/default_terms_and_conditions', $termsAndConditions);
1733 }
1734
1735 public static function getDefaultEmailNotificationSettings()
1736 {
1737 $assetUrl = App::getInstance()['url.assets'];
1738
1739 $checkImage = $assetUrl . 'images/check-mark.png';
1740 $cancelImage = $assetUrl . 'images/cancel-mark.png';
1741 $scheduleImage = $assetUrl . 'images/schedule-mark.png';
1742
1743 return apply_filters('fluent_booking/default_email_notification_settings', [
1744 'booking_conf_attendee' => [
1745 'enabled' => true,
1746 'title' => __('Booking Confirmation Email to Attendee', 'fluent-booking'),
1747 'email' => [
1748 'subject' => 'Booking Confirmation between {{host.name}} & {{guest.full_name}}',
1749 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $checkImage . '" alt="" width="60" height="60" /></p><h2 class="p1" style="text-align: center;">Your event has been scheduled</h2><hr /><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{host.name}}</p><p><strong>When</strong></p><p>{{booking.all_bookings_short_times_guest_timezone}}</p><p><strong>Who</strong></p><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} - you</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Additional notes</strong></p><p>{{guest.note}}</p><hr /><p style="text-align: center;">' . __('Need to make a change?', 'fluent-booking') . ' <a href="##booking.reschedule_url##">' . __('Reschedule', 'fluent-booking') . '</a> or <a href="##booking.cancelation_url##">' . __('Cancel', 'fluent-booking') . '</p><hr/>' . self::getAddToCalendarHtml($assetUrl)
1750 ],
1751 ],
1752 'booking_conf_host' => [
1753 'enabled' => true,
1754 'is_host' => true,
1755 'title' => __('Booking Confirmation Email to Organizer (You)', 'fluent-booking'),
1756 'email' => [
1757 'additional_recipients' => '',
1758 'subject' => 'New Booking: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1759 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $checkImage . '" alt="" width="60" height="60" /></p><h2 class="p1" style="text-align: center;">A new event has been scheduled</h2><hr /><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>{{booking.all_bookings_short_times_host_timezone}}</p><p><strong>Who</strong></p><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} ({{guest.email}}) - Guest</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Note</strong></p><p>{{guest.note}}</p><p><strong>Additional Data</strong></p><p>{{guest.form_data_html}}</p><hr /><p style="text-align: center;"><a href="##booking.admin_booking_url##">View on the Website</a></p>'
1760 ],
1761 ],
1762 'reminder_to_attendee' => [
1763 'enabled' => false,
1764 'title' => __('Configure Meeting Reminder to Attendee', 'fluent-booking'),
1765 'email' => [
1766 'subject' => 'Meeting Reminder with {{host.name}} @ {{booking.start_date_time_for_attendee}}',
1767 'body' => '<h2 style="text-align: center;">Reminder: Your meeting will start in {{booking.start_time_human_format}}</h2><hr /><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{host.name}}</p><h3><strong>When</strong></h3><p>{{booking.full_start_end_guest_timezone}}</p><h3><strong>Who</strong></h3><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} - you</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Additional notes</strong></p><p>{{guest.note}}</p><hr /><p style="text-align: center;">' . __('Need to make a change?', 'fluent-booking') . ' <a href="##booking.reschedule_url##">' . __('Reschedule', 'fluent-booking') . '</a> or <a href="##booking.cancelation_url##">' . __('Cancel', 'fluent-booking') . '</a></p>',
1768 'times' => [
1769 [
1770 'unit' => 'minutes',
1771 'value' => 15,
1772 ]
1773 ]
1774 ],
1775 ],
1776 'reminder_to_host' => [
1777 'enabled' => false,
1778 'is_host' => true,
1779 'title' => __('Configure Meeting Reminder to Organizer (You)', 'fluent-booking'),
1780 'email' => [
1781 'additional_recipients' => '',
1782 'subject' => 'Meeting Reminder with {{host.name}} @ {{booking.start_date_time_for_host}}',
1783 'body' => '<h2 style="text-align: center;">Reminder: Your meeting will start in {{booking.start_time_human_format}}</h2><hr /><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>{{booking.full_start_end_host_timezone}}</p><p><strong>Who</strong></p><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} ({{guest.email}}) - Guest</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Note</strong></p><p>{{guest.note}}</p><p><strong>Additional Data</strong></p><p>{{guest.form_data_html}}</p><hr /><p style="text-align: center;"><a href="##booking.admin_booking_url##">View on the Website</a></p>',
1784 'times' => [
1785 [
1786 'unit' => 'minutes',
1787 'value' => 15,
1788 ]
1789 ]
1790 ],
1791 ],
1792 'cancelled_by_attendee' => [
1793 'enabled' => true,
1794 'is_host' => true,
1795 'title' => __('Booking Cancelled by Attendee (email to Organizer)', 'fluent-booking'),
1796 'email' => [
1797 'additional_recipients' => '',
1798 'subject' => 'A booking was cancelled with {{guest.full_name}}',
1799 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $cancelImage . '" alt="" width="60" height="60" /></p><h2 style="text-align: center;">Booking Cancellation</h2><hr /><p>A scheduled meeting has been canceled. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>{{booking.full_start_end_host_timezone}} <span style="color: #ff0000;"><strong>(cancelled)</strong></span></p><p><strong>Cancellation Reason</strong></p><p>{{booking.cancel_reason}}</p><p><strong>Who</strong></p><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} ({{guest.email}}) - Guest</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Note</strong></p><p>{{guest.note}}</p><p><strong>Additional Data</strong></p><p>{{guest.form_data_html}}</p><hr /><p style="text-align: center;"><a href="##booking.admin_booking_url##">View on the Website</a></p>'
1800 ],
1801 ],
1802 'cancelled_by_host' => [
1803 'enabled' => true,
1804 'title' => __('Booking Cancelled by Organizer (email to Attendee)', 'fluent-booking'),
1805 'email' => [
1806 'subject' => 'Your booking was cancelled with {{host.name}}',
1807 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $cancelImage . '" alt="" width="60" height="60" /></p><h2 style="text-align: center;">Booking Cancellation</h2><hr /><p>Your scheduled meeting has been canceled. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>{{booking.full_start_end_host_timezone}} <span style="color: #ff0000;"><strong>(cancelled)</strong></span></p><p><strong>Cancellation Reason</strong></p><p>{{booking.cancel_reason}}</p>'
1808 ],
1809 ],
1810 'rescheduled_by_attendee' => [
1811 'enabled' => true,
1812 'is_host' => true,
1813 'title' => __('Booking Rescheduled by Attendee (email to Organizer)', 'fluent-booking'),
1814 'email' => [
1815 'additional_recipients' => '',
1816 'subject' => 'A booking was rescheduled with {{guest.full_name}}',
1817 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $scheduleImage . '" alt="" width="60" height="60" /></p><h2 style="text-align: center;">Booking Rescheduled</h2><hr /><p>A scheduled meeting has been rescheduled. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>New Time: {{booking.full_start_end_host_timezone}} <span style="color: #ff0000;"><strong>(new)</strong></span></p><p>Previous Time: {{booking.previous_meeting_time}}</p><p><strong>Rescheduling Reason</strong></p><p>{{booking.reschedule_reason}}</p><p><strong>Who</strong></p><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} ({{guest.email}}) - Guest</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Note</strong></p><p>{{guest.note}}</p><p><strong>Additional Data</strong></p><p>{{guest.form_data_html}}</p><hr /><p style="text-align: center;"><a href="##booking.admin_booking_url##">View on the Website</a></p>'
1818 ],
1819 ],
1820 'rescheduled_by_host' => [
1821 'enabled' => true,
1822 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'),
1823 'email' => [
1824 'subject' => 'Your booking was rescheduled with {{host.name}}',
1825 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $scheduleImage . '" alt="" width="60" height="60" /></p><h2 style="text-align: center;">Booking Rescheduled</h2><hr /><p>Your scheduled meeting has been rescheduled. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>New Time: {{booking.full_start_end_host_timezone}} <span style="color: #ff0000;"><strong>(new)</strong></span></p><p>Previous Time: {{booking.previous_meeting_time_guest_timezone}}</p><p><strong>Rescheduling Reason</strong></p><p>{{booking.reschedule_reason}}</p><hr /><p style="text-align: center;">' . __('Need to make a change?', 'fluent-booking') . ' <a href="##booking.reschedule_url##">' . __('Reschedule', 'fluent-booking') . '</a> or <a href="##booking.cancelation_url##">' . __('Cancel', 'fluent-booking') . '</a></p><hr/>' . self::getAddToCalendarHtml($assetUrl)
1826 ],
1827 ],
1828 'booking_request_host' => [
1829 'enabled' => true,
1830 'is_host' => true,
1831 'title' => __('Booking Approval Request to Host (email to Organizer)', 'fluent-booking'),
1832 'email' => [
1833 'additional_recipients' => '',
1834 'subject' => 'Awaiting Approval: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1835 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $scheduleImage . '" alt="" width="60" height="60" /></p><h2 class="p1" style="text-align: center;">A booking is still waiting for your approval</h2><hr /><p>Someone has requested to schedule an event on your calendar. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>{{booking.all_bookings_short_times_host_timezone}}</p><p><strong>Who</strong></p><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} ({{guest.email}}) - Guest</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Note</strong></p><p>{{guest.note}}</p><p><strong>Additional Data</strong></p><p>{{guest.form_data_html}}</p><hr />' . self::getConfirmAndRejectButton($assetUrl) . '<p style="text-align: center;"><a href="##booking.admin_booking_url##">View on the Website</a></p>'
1836 ],
1837 ],
1838 'booking_request_attendee' => [
1839 'enabled' => true,
1840 'title' => __('Booking Submission Confirmation (email to Attendee)', 'fluent-booking'),
1841 'email' => [
1842 'subject' => 'Booking Submitted: Meeting between {{host.name}} & {{guest.full_name}}',
1843 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $scheduleImage . '" alt="" width="60" height="60" /></p><h2 class="p1" style="text-align: center;">Your booking has been submitted</h2><hr /><p>Please wait for the host to confirm your booking.</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{host.name}}</p><p><strong>When</strong></p><p>{{booking.all_bookings_short_times_guest_timezone}}</p><p><strong>Who</strong></p><ul><li>{{host.name}} - Organizer</li><li>{{guest.full_name}} - you</li></ul><p><strong>Where</strong></p><p>{{booking.location_details_html}}</p><p><strong>Additional notes</strong></p><p>{{guest.note}}</p><hr /><p style="text-align: center;">' . __('Need to make a change?', 'fluent-booking') . ' <a href="##booking.reschedule_url##">' . __('Reschedule', 'fluent-booking') . '</a> or <a href="##booking.cancelation_url##">' . __('Cancel', 'fluent-booking') . '</p>'
1844 ],
1845 ],
1846 'declined_by_host' => [
1847 'enabled' => true,
1848 'title' => __('Booking Declined by Organizer (email to Attendee)', 'fluent-booking'),
1849 'email' => [
1850 'subject' => 'Booking Declined: Your booking was declined with {{host.name}}',
1851 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $cancelImage . '" alt="" width="60" height="60" /></p><h2 style="text-align: center;">Booking Declined</h2><hr /><p>Your booking request has been declined. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>{{booking.full_start_end_host_timezone}} <span style="color: #ff0000;"><strong>(declined)</strong></span></p><p><strong>Reason</strong></p><p>{{booking.reject_reason}}</p>'
1852 ],
1853 ],
1854 ]);
1855 }
1856
1857 public static function getAddToCalendarHtml($assetUrl = '')
1858 {
1859 $assetUrl = $assetUrl ?: App::getInstance()['url.assets'];
1860
1861 $html = '<table style="margin: 0 auto; border: none;"><tbody><tr><td style="border: none; font-size: 1rem;">' . __('Add to calendar', 'fluent-booking') . '</td><td style="border: 1px solid black; padding: 5px 5px 0 5px;"><a href="##add_to_g_calendar_url##"><img width="20" height="20" src="' . $assetUrl . 'images/g-icon.png" alt="Google Calendar" /></a></td><td style="border: 1px solid black; padding: 5px 5px 0 5px;"><a href="##add_to_ol_calendar_url##"><img width="20" height="20" src="' . $assetUrl . 'images/ol-icon.png" alt="Outlook" /></a></td><td style="border: 1px solid black; padding: 5px 5px 0 5px;"><a href="##add_to_ms_calendar_url##"><img width="20" height="20" src="' . $assetUrl . 'images/msoffice.png" alt="Microsoft Office" /></a></td><td style="border: 1px solid black; padding: 5px 5px 0 5px;"><a href="##add_to_ics_calendar_url##"><img width="20" height="20" src="' . $assetUrl . 'images/ics.png" alt="Other Calendar" /></a></td></tr></tbody></table>';
1862
1863 return apply_filters('fluent_booking/add_to_calendar_html', $html);
1864 }
1865
1866 public static function getConfirmAndRejectButton($assetUrl = '')
1867 {
1868 $assetUrl = $assetUrl ?: App::getInstance()['url.assets'];
1869
1870 $html = '<table style="margin: 16px auto; border: none;"><tbody><tr><td style="border: none; border-radius: 3px;" align="center" valign="middle"><p style="display: inline-block; background: #292929; color: #ffffff; font-size: 14px; font-weight: 500; line-height: 16px; margin: 0; text-decoration: none; text-transform: none; padding: 10px 16px 10px 14px; border-radius: 6px; box-sizing: border-box; height: 36px;"><a href="##booking.booking_confirm_url##" style="color: #ffffff; text-decoration: none; display: flex; margin: auto;"><img src="' . $assetUrl . 'images/confirm-mark.png" style="height: 16px; width: 16px; margin-left: 0; margin-right: 0.5rem;" alt="" width="16px" data-bit="iit" />' . __('Confirm', 'fluent-booking') . '</a></p><p style="width: 16px; height: 16px; display: inline-block;"> </p><p style="display: inline-block; background: #ffffff; border: 1px solid #d1d5db; color: #ffffff; font-size: 14px; font-weight: 500; line-height: 16px; margin: 0; text-decoration: none; text-transform: none; padding: 10px 16px 10px 14px; border-radius: 6px; box-sizing: border-box; height: 36px;"><a href="##booking.booking_reject_url##" style="color: #292929; text-decoration: none; display: flex; margin: auto;"><img src="' . $assetUrl . 'images/reject-mark.png" style="height: 16px; width: 16px; margin-left: 0; margin-right: 8px;" alt="" width="16px" data-bit="iit" />' . __('Reject', 'fluent-booking') . '</a></p></td></tr></tbody></table>';
1871
1872 return apply_filters('fluent_booking/confirm_and_reject_button_html', $html);
1873 }
1874
1875 public static function getIframeHtml()
1876 {
1877 $html = '<iframe id="fluentbooking" loading="lazy" height="700px" width="100%" style="min-width:320px;height:700px;" frameborder="0" src="##landing_page_url##"></iframe>';
1878
1879 return apply_filters('fluent_booking/get_iframe_html', $html);
1880 }
1881
1882 public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false, $iframeHtml = '')
1883 {
1884 if (!$isHtmlSupported) {
1885 $groups = [
1886 'guest' => [
1887 'title' => __('Attendee Data', 'fluent-booking'),
1888 'key' => 'guest',
1889 'shortcodes' => [
1890 '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1891 '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1892 '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1893 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1894 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1895 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1896 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1897 '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking')
1898 ]
1899 ],
1900 'booking' => [
1901 'title' => __('Booking Data', 'fluent-booking'),
1902 'key' => 'booking',
1903 'shortcodes' => [
1904 '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1905 '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1906 '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1907 '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1908 '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1909 '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1910 '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1911 '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1912 '{{booking.all_bookings_short_times_guest_timezone}}' => __('All Bookings Short Times (with guest timezone)', 'fluent-booking'),
1913 '{{booking.all_bookings_short_times_host_timezone}}' => __('All Bookings Short Times (with host timezone)', 'fluent-booking'),
1914 '{{booking.all_bookings_full_times_guest_timezone}}' => __('All Bookings Full Times (with guest timezone)', 'fluent-booking'),
1915 '{{booking.all_bookings_full_times_host_timezone}}' => __('All Bookings Full Times (with host timezone)', 'fluent-booking'),
1916 '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1917 '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'),
1918 '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1919 '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1920 '{{booking.start_date_time_for_host.format.Y-m-d}}' => __('Event Date Time (with host timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1921 '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'),
1922 '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1923 '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1924 '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1925 '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1926 '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1927 '{{booking.source_url}}' => __('Source URL', 'fluent-booking'),
1928 '{{booking.utm_source}}' => __('UTM Source', 'fluent-booking'),
1929 '{{booking.utm_medium}}' => __('UTM Medium', 'fluent-booking'),
1930 '{{booking.utm_campaign}}' => __('UTM Campaign', 'fluent-booking'),
1931 '{{booking.utm_term}}' => __('UTM Term', 'fluent-booking'),
1932 '{{booking.utm_content}}' => __('UTM Content', 'fluent-booking'),
1933 '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1934 '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking'),
1935 '{{booking.previous_meeting_date_time_host_timezone}}' => __('Previous Meeting Date & Time (with host timezone)', 'fluent-booking'),
1936 '{{booking.previous_meeting_date_time_guest_timezone}}' => __('Previous Meeting Date & Time (with guest timezone)', 'fluent-booking'),
1937 ]
1938 ],
1939 'host' => [
1940 'title' => __('Host Data', 'fluent-booking'),
1941 'key' => 'host',
1942 'shortcodes' => [
1943 '{{host.name}}' => __('Host Name', 'fluent-booking'),
1944 '{{host.email}}' => __('Host Email', 'fluent-booking'),
1945 '{{host.timezone}}' => __('Host Timezone', 'fluent-booking'),
1946 ]
1947 ],
1948 'other' => [
1949 'title' => __('Other', 'fluent-booking'),
1950 'key' => 'other',
1951 'shortcodes' => [
1952 '{{event.id}}' => __('Event ID', 'fluent-booking'),
1953 '{{calendar.id}}' => __('Calendar ID', 'fluent-booking'),
1954 '{{event.title}}' => __('Event Title', 'fluent-booking'),
1955 '{{calendar.title}}' => __('Calendar Title', 'fluent-booking'),
1956 '{{calendar.description}}' => __('Calendar Description', 'fluent-booking'),
1957 '{{add_booking_to_calendar}}' => __('Add Booking to Calendar', 'fluent-booking'),
1958 ]
1959 ]
1960 ];
1961 } else {
1962 $groups = [
1963 'guest' => [
1964 'title' => __('Attendee Data', 'fluent-booking'),
1965 'key' => 'guest',
1966 'shortcodes' => [
1967 '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1968 '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1969 '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1970 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1971 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1972 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1973 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1974 '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking'),
1975 '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking')
1976 ]
1977 ],
1978 'booking' => [
1979 'title' => __('Booking Data', 'fluent-booking'),
1980 'key' => 'booking',
1981 'shortcodes' => [
1982 '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1983 '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1984 '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1985 '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1986 '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1987 '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1988 '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1989 '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1990 '{{booking.all_bookings_short_times_guest_timezone}}' => __('All Bookings Short Times (with guest timezone)', 'fluent-booking'),
1991 '{{booking.all_bookings_short_times_host_timezone}}' => __('All Bookings Short Times (with host timezone)', 'fluent-booking'),
1992 '{{booking.all_bookings_full_times_guest_timezone}}' => __('All Bookings Full Times (with guest timezone)', 'fluent-booking'),
1993 '{{booking.all_bookings_full_times_host_timezone}}' => __('All Bookings Full Times (with host timezone)', 'fluent-booking'),
1994 '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1995 '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with guest timezone)', 'fluent-booking'),
1996 '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1997 '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1998 '{{booking.start_date_time_for_host.format.Y-m-d}}' => __('Event Date Time (with host timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1999 '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'),
2000 '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
2001 '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
2002 '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
2003 '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
2004 '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
2005 '{{booking.source_url}}' => __('Source URL', 'fluent-booking'),
2006 '{{booking.utm_source}}' => __('UTM Source', 'fluent-booking'),
2007 '{{booking.utm_medium}}' => __('UTM Medium', 'fluent-booking'),
2008 '{{booking.utm_campaign}}' => __('UTM Campaign', 'fluent-booking'),
2009 '{{booking.utm_term}}' => __('UTM Term', 'fluent-booking'),
2010 '{{booking.utm_content}}' => __('UTM Content', 'fluent-booking'),
2011 '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
2012 '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking'),
2013 '{{booking.previous_meeting_date_time_host_timezone}}' => __('Previous Meeting Date & Time (with host timezone)', 'fluent-booking'),
2014 '{{booking.previous_meeting_date_time_guest_timezone}}' => __('Previous Meeting Date & Time (with guest timezone)', 'fluent-booking'),
2015 ]
2016 ],
2017 'host' => [
2018 'title' => __('Host Data', 'fluent-booking'),
2019 'key' => 'host',
2020 'shortcodes' => [
2021 '{{host.name}}' => __('Host Name', 'fluent-booking'),
2022 '{{host.email}}' => __('Host Email', 'fluent-booking'),
2023 '{{host.timezone}}' => __('Host Timezone', 'fluent-booking'),
2024 ]
2025 ],
2026 'other' => [
2027 'title' => __('Other', 'fluent-booking'),
2028 'key' => 'other',
2029 'shortcodes' => [
2030 '{{event.id}}' => __('Event ID', 'fluent-booking'),
2031 '{{event.calendar_id}}' => __('Calendar ID', 'fluent-booking'),
2032 '{{event.title}}' => __('Event Title', 'fluent-booking'),
2033 '{{calendar.title}}' => __('Calendar Title', 'fluent-booking'),
2034 '{{calendar.description}}' => __('Calendar Description', 'fluent-booking'),
2035 '{{add_booking_to_calendar}}' => __('Add Booking to Calendar', 'fluent-booking'),
2036 ]
2037 ]
2038 ];
2039 }
2040
2041 if ($calendarEvent) {
2042 $customFields = BookingFieldService::getCustomFields($calendarEvent, true);
2043 foreach ($customFields as $fieldKey => $field) {
2044 $groups['booking']['shortcodes']['{{booking.custom.' . $fieldKey . '}}'] = $field['label'];
2045 if ($field['type'] == 'date') {
2046 $groups['booking']['shortcodes']['{{booking.custom.' . $fieldKey . '.format.Y-m-d}}'] = $field['label'] . ' (Ex: 2024-05-20)';
2047 }
2048 }
2049
2050 if (Helper::isPaymentEnabled($calendarEvent)) {
2051 $groups['payment'] = [
2052 'title' => __('Payment Data', 'fluent-booking'),
2053 'key' => 'payment',
2054 'shortcodes' => [
2055 '{{payment.payment_total}}' => __('Payment Total', 'fluent-booking'),
2056 '{{payment.payment_status}}' => __('Payment Status', 'fluent-booking'),
2057 '{{payment.payment_method}}' => __('Payment Method', 'fluent-booking'),
2058 '{{payment.currency}}' => __('Currency', 'fluent-booking'),
2059 '{{payment.payment_date}}' => __('Payment Date', 'fluent-booking'),
2060 ]
2061 ];
2062
2063 if ($isHtmlSupported) {
2064 $groups['payment']['shortcodes']['{{payment.receipt_html}}'] = __('Payment Receipt (HTML)', 'fluent-booking');
2065 }
2066 }
2067
2068 if ($calendarEvent->isMultiHostsEvent()) {
2069 $totalhost = count($calendarEvent->getHostIds());
2070 for ($i = 1; $i < $totalhost; $i++) {
2071 $groups['host']['shortcodes']['{{team_member.' . $i . '.name}}'] = __('Team Member ', 'fluent-booking') . $i . __(' Name', 'fluent-booking');
2072 $groups['host']['shortcodes']['{{team_member.' . $i . '.email}}'] = __('Team Member ', 'fluent-booking') . $i . __(' Email', 'fluent-booking');
2073 }
2074 }
2075 }
2076
2077 return apply_filters('fluent_booking/editor_shortcodes_groups', $groups, $calendarEvent, $isHtmlSupported);
2078 }
2079
2080 public static function encryptKey($value)
2081 {
2082 if (!$value) {
2083 return $value;
2084 }
2085
2086 if (!extension_loaded('openssl')) {
2087 return $value;
2088 }
2089
2090 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
2091
2092 if (defined('FLUENT_BOOKING_ENCRYPTION_KEY')) {
2093 $key = FLUENT_BOOKING_ENCRYPTION_KEY;
2094 } else {
2095 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
2096 }
2097
2098 $method = 'aes-256-ctr';
2099 $ivlen = openssl_cipher_iv_length($method);
2100 $iv = openssl_random_pseudo_bytes($ivlen);
2101
2102 $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
2103 if (!$raw_value) {
2104 return false;
2105 }
2106
2107 return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
2108 }
2109
2110 public static function decryptKey($raw_value)
2111 {
2112
2113 if (!$raw_value) {
2114 return $raw_value;
2115 }
2116
2117 if (!extension_loaded('openssl')) {
2118 return $raw_value;
2119 }
2120
2121 $raw_value = base64_decode($raw_value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
2122 if ($raw_value === false) {
2123 return false;
2124 }
2125
2126 $method = 'aes-256-ctr';
2127 $ivlen = openssl_cipher_iv_length($method);
2128 if ($ivlen === false || strlen($raw_value) <= $ivlen) {
2129 return false;
2130 }
2131 $iv = substr($raw_value, 0, $ivlen);
2132
2133 $raw_value = substr($raw_value, $ivlen);
2134
2135 if (defined('FLUENT_BOOKING_ENCRYPTION_KEY')) {
2136 $key = FLUENT_BOOKING_ENCRYPTION_KEY;
2137 } else {
2138 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
2139 }
2140
2141 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
2142
2143 $value = openssl_decrypt($raw_value, $method, $key, 0, $iv);
2144 if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) {
2145 return false;
2146 }
2147
2148 return substr($value, 0, -strlen($salt));
2149 }
2150
2151 public static function debugLog($data)
2152 {
2153 if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) {
2154 error_log(print_r($data, true)); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
2155 }
2156 }
2157
2158 public static function getGlobalSettings($settingsKey = null)
2159 {
2160 $defaults = [
2161 'payments' => [
2162 'currency' => 'USD',
2163 'is_active' => 'no'
2164 ],
2165 'emailing' => [
2166 'from_name' => '',
2167 'from_email' => '',
2168 'reply_to_name' => '',
2169 'reply_to_email' => '',
2170 'use_host_name' => '',
2171 'use_host_email_on_reply' => '',
2172 'attach_ics_on_confirmation' => '',
2173 'email_footer' => ''
2174 ],
2175 'administration' => [
2176 'admin_email' => '{{wp.admin_email}}',
2177 'summary_notification' => 'no',
2178 'notification_frequency' => 'daily',
2179 'notification_day' => 'mon',
2180 'start_day' => 'sun',
2181 'auto_cancel_timing' => '10',
2182 'auto_complete_timing' => '60',
2183 'default_country' => ''
2184 ],
2185 'time_format' => '12',
2186 'theme' => 'system-default'
2187 ];
2188
2189 $settings = get_option('_fluent_booking_settings', []);
2190
2191 if (empty($settings)) {
2192 $settings = [];
2193 }
2194
2195 $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings();
2196
2197 $settings = wp_parse_args($settings, $defaults);
2198
2199 $emailSettings = $settings['emailing'];
2200
2201 if (empty($settings['administration']['auto_cancel_timing'])) {
2202 $settings['administration']['auto_cancel_timing'] = '10';
2203 }
2204
2205 if (empty($settings['administration']['auto_complete_timing'])) {
2206 $settings['administration']['auto_complete_timing'] = '10';
2207 }
2208
2209 if (empty($emailSettings['from_name']) && defined('FLUENTCRM')) {
2210 $crmSettings = fluentcrmGetGlobalSettings('email_settings', []);
2211 $emailSettings['from_name'] = Arr::get($crmSettings, 'from_name');
2212 if (empty($emailSettings['from_email'])) {
2213 $emailSettings['from_email'] = Arr::get($crmSettings, 'from_email');
2214 }
2215 if (empty($emailSettings['reply_to_name'])) {
2216 $emailSettings['reply_to_name'] = Arr::get($crmSettings, 'reply_to_name');
2217 }
2218 if (empty($emailSettings['reply_to_email'])) {
2219 $emailSettings['reply_to_email'] = Arr::get($crmSettings, 'reply_to_email');
2220 }
2221
2222 $settings['emailing'] = $emailSettings;
2223 }
2224
2225 if ($settingsKey) {
2226 return Arr::get($settings, $settingsKey, []);
2227 }
2228
2229 return $settings;
2230
2231 }
2232
2233 public static function getGlobalAdminSetting($key = null, $default = null)
2234 {
2235 static $settings;
2236 if ($settings) {
2237
2238 if ($key) {
2239 return Arr::get($settings, $key, $default);
2240 }
2241
2242 return $settings;
2243 }
2244
2245 $globalSettings = self::getGlobalSettings();
2246 $settings = Arr::get($globalSettings, 'administration', []);
2247
2248 if ($key) {
2249 return Arr::get($settings, $key, $default);
2250 }
2251
2252 return $settings;
2253 }
2254
2255 public static function getDefaultTimeFormat()
2256 {
2257 static $format;
2258
2259 if ($format) {
2260 return $format;
2261 }
2262
2263 $settings = self::getGlobalSettings();
2264 $format = Arr::get($settings, 'time_format', '24');
2265 return $format;
2266 }
2267
2268 public static function getDefaultBookingFilters()
2269 {
2270 return apply_filters('fluent_booking/default_booking_filters', [
2271 'period' => 'upcoming',
2272 'author' => 'me', // me, all, calendar_id
2273 'event' => 'all',
2274 'event_type' => 'all'
2275 ]);
2276 }
2277
2278 public static function getDefaultPaginations()
2279 {
2280 return apply_filters('fluent_booking/default_paginations', [
2281 'bookings' => 10,
2282 'calendars' => 10,
2283 'coupons' => 10,
2284 'availabilities' => 10
2285 ]);
2286 }
2287
2288 public static function getVerifiedSenders()
2289 {
2290 $verifiedSenders = [];
2291 if (defined('FLUENTMAIL')) {
2292 $smtpSettings = get_option('fluentmail-settings', []);
2293 if ($smtpSettings && count($smtpSettings['mappings'])) {
2294 $verifiedSenders = array_keys($smtpSettings['mappings']);
2295 }
2296 }
2297 /**
2298 * Filter the verified email senders
2299 * @param array $verifiedSenders
2300 */
2301 return apply_filters('fluent_booking/verfied_email_senders', $verifiedSenders);
2302 }
2303
2304 public static function getBookingReceiptLandingBaseUrl()
2305 {
2306 return apply_filters('fluent_booking/booking_receipt_landing_base_url', site_url('/'));
2307 }
2308
2309 public static function getGlobalModuleSettings($cached = true)
2310 {
2311 static $settings = null;
2312
2313 if ($cached && $settings !== null) {
2314 return $settings;
2315 }
2316
2317 $settings = get_option('_fluent_booking_enabled_modules', []);
2318
2319 if (empty($settings) || !\is_array($settings)) {
2320 $settings = [];
2321 }
2322
2323 return $settings;
2324 }
2325
2326 public static function updateGlobalModuleSettings($settings = [])
2327 {
2328 if (!is_array($settings)) {
2329 $settings = [];
2330 }
2331
2332 update_option('_fluent_booking_enabled_modules', $settings);
2333
2334 return self::getGlobalModuleSettings(false);
2335 }
2336
2337 public static function isModuleEnabled($module)
2338 {
2339 $settings = self::getGlobalModuleSettings();
2340
2341 return isset($settings[$module]) && $settings[$module] === 'yes';
2342 }
2343
2344 /**
2345 * @return mixed|void
2346 */
2347 public static function fluentbooking_is_rtl()
2348 {
2349 /**
2350 * If FluentBooking is running on RTL Mode
2351 *
2352 * @param bool $is_rtl
2353 */
2354 return apply_filters('fluent_booking/is_rtl', is_rtl());
2355 }
2356
2357 public static function fluentBookingUserAvatar($id_or_email, $args)
2358 {
2359 if (empty($id_or_email)) {
2360 return '';
2361 }
2362 return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args);
2363 }
2364
2365 public static function getPrefSettings($cached = true)
2366 {
2367 static $pref = null;
2368
2369 if ($cached && $pref) {
2370 return $pref;
2371 }
2372
2373 $settings = [
2374 'frontend' => [
2375 'enabled' => 'no',
2376 'slug' => 'my-bookings',
2377 'render_type' => 'standalone',
2378 'page_id' => ''
2379 ],
2380 'coupon' => [
2381 'enabled' => 'no'
2382 ]
2383 ];
2384
2385 $storedSettings = get_option('fluent_booking_modules', []);
2386
2387 if ($storedSettings && is_array($storedSettings)) {
2388 $settings = wp_parse_args($storedSettings, $settings);
2389 } else {
2390 update_option('fluent_booking_modules', $settings, 'yes');
2391 }
2392
2393 $pref = $settings;
2394
2395 return $settings;
2396 }
2397
2398 public static function getPrefSettins($cached = true)
2399 {
2400 return self::getPrefSettings($cached);
2401 }
2402
2403 public static function getFeatures()
2404 {
2405 return apply_filters('fluent_booking/get_features', [
2406 'has_fluentcrm' => defined('FLUENTCRM'),
2407 'has_fluentsmtp' => defined('FLUENTMAIL'),
2408 'has_fluentform' => defined('FLUENTFORM'),
2409 'has_fluentboards' => defined('FLUENT_BOARDS'),
2410 'has_fluentcart' => defined('FLUENTCART_VERSION')
2411 ]);
2412 }
2413
2414 public static function getActiveThemeName()
2415 {
2416 $ins = get_option('_fb_ins_by');
2417
2418 if ($ins) {
2419 return sanitize_text_field($ins);
2420 }
2421
2422 return get_option('template');
2423 }
2424
2425 /**
2426 * Hold a round robin slot for the rest of the request, so concurrent public
2427 * bookings cannot pick the same least-loaded host. Locks every host, since
2428 * the host is only chosen inside isSpotAvailable() and another event can
2429 * share it. Same keys MCP locks with.
2430 * Released at shutdown because wp_send_json() exits past any finally.
2431 *
2432 * @param CalendarSlot $event
2433 * @param string $startTimeUtc
2434 * @param string $endTimeUtc
2435 *
2436 * @return bool false when another request holds the slot
2437 */
2438 public static function lockRoundRobinSlot($event, $startTimeUtc, $endTimeUtc)
2439 {
2440 if (!$event->isRoundRobin()) {
2441 return true;
2442 }
2443
2444 $locks = SlotLock::acquireInterval($event->id, $startTimeUtc, $endTimeUtc, $event->getHostIds());
2445
2446 if ($locks) {
2447 register_shutdown_function([SlotLock::class, 'releaseAll'], $locks);
2448 }
2449
2450 return (bool) $locks;
2451 }
2452
2453 /**
2454 * Per-IP fixed-window rate limiter for public AJAX/REST endpoints.
2455 *
2456 * @param string $action Action name (e.g. apply_coupon, schedule_meeting).
2457 * @param int $limit Max requests per window.
2458 * @param int $window Window in seconds.
2459 * @param bool $perIp False for one shared bucket that a spoofed IP cannot reset.
2460 * @return bool True if under the limit (and the count was incremented),
2461 * false if over.
2462 */
2463 public static function checkRateLimit($action, $limit, $window = 60, $perIp = true)
2464 {
2465 $args = apply_filters('fluent_booking/public_ajax_ratelimit', [
2466 'limit' => $limit,
2467 'window' => $window,
2468 ], $action);
2469
2470 $limit = max(1, (int) (isset($args['limit']) ? $args['limit'] : $limit));
2471 $window = max(1, (int) (isset($args['window']) ? $args['window'] : $window));
2472
2473 $key = 'fcal_ratelimit_' . $action . ($perIp ? '_' . md5(self::getIp()) : '');
2474 $count = (int) get_transient($key);
2475
2476 if ($count >= $limit) {
2477 return false;
2478 }
2479
2480 set_transient($key, $count + 1, $window);
2481
2482 return true;
2483 }
2484
2485 /**
2486 * Run a callback inside a database transaction, re-throwing on failure so
2487 * the caller decides how to report it.
2488 *
2489 * \Throwable, not \Exception: a TypeError is an Error, and an
2490 * Exception-only catch would leave the transaction open.
2491 *
2492 * Database writes only — a hook fired in here would hold the callback's
2493 * rows locked for the length of a listener's outbound request.
2494 *
2495 * @param callable $callback
2496 *
2497 * @return mixed
2498 *
2499 * @throws \Throwable after the rollback
2500 */
2501 public static function dbTransaction($callback)
2502 {
2503 $db = App::getInstance('db');
2504
2505 $db->beginTransaction();
2506
2507 try {
2508 $result = $callback();
2509
2510 $db->commit();
2511
2512 return $result;
2513 } catch (\Throwable $e) {
2514 $db->rollBack();
2515
2516 throw $e;
2517 }
2518 }
2519 }
2520