PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
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 1.7.2 All 33 releases
fluent-booking / app / Services / Helper.php

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

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