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
← All changes | app/Services/Helper.php +508 -86 1.5.10 → 2.5.0 View file →
@@ -7,8 +7,9 @@
7 7 use FluentBooking\App\Models\Calendar;
8 8 use FluentBooking\App\Models\CalendarSlot;
9 9 use FluentBooking\App\Models\Meta;
10 10 use FluentBooking\App\Models\BookingMeta;
11 +use FluentBooking\App\Modules\MCP\Support\SlotLock;
11 12 use FluentBooking\Framework\Support\Arr;
12 13
13 14 class Helper
14 15 {
@@ -693,11 +694,44 @@
693 694 {
694 695 return self::getAppBaseUrl('scheduled-events?booking_id=' . $bookingId);
695 696 }
696 697
697 - public static function getUpgradeUrl()
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 = [])
698 714 {
699 - return 'https://fluentbooking.com/pricing/?utm_source=plugin&utm_medium=wp_install&utm_campaign=fcal_upgrade';
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);
700 734 }
701 735
702 736 public static function getNextBookingGroup()
703 737 {
@@ -714,8 +748,9 @@
714 748 {
715 749 static $index = 0;
716 750
717 751 $index += 1;
752 +
718 753 return $index;
719 754 }
720 755
721 756 public static function getGlobalPaymentSettings()
@@ -727,21 +762,14 @@
727 762 }
728 763
729 764 $settings = get_option('fluent_booking_global_payment_settings', []);
730 765
731 - if (!$settings) {
732 - $settings = [
733 - 'currency' => 'USD',
734 - 'is_active' => 'no'
735 - ];
736 - }
737 -
738 766 return $settings;
739 767 }
740 768
741 769 public static function isPaymentEnabled($calendarEvent = null)
742 770 {
743 - $settings = self::getGlobalPaymentSettings();
771 + $settings = CurrenciesHelper::getGlobalCurrencySettings();
744 772 if (Arr::get($settings, 'is_active') == 'yes') {
745 773 return true;
746 774 }
747 775
@@ -889,15 +917,29 @@
889 917 }
890 918
891 919 $user = get_user_by('ID', $userId);
892 920
893 - $name = trim($user->first_name . ' ' . $user->last_name);
921 + return self::getDisplayNameFromUser($user);
922 + }
894 923
895 - if ($name) {
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 !== '') {
896 936 return $name;
897 937 }
898 938
899 - return $user->display_name;
939 + $displayName = is_object($user) ? ($user->display_name ?? '') : '';
940 +
941 + return (string) $displayName;
900 942 }
901 943
902 944 public static function getUserEmail($userId = null)
903 945 {
@@ -943,24 +985,175 @@
943 985
944 986 return apply_filters('fluent_booking/slot_slug', $default, $original);
945 987 }
946 988
947 - public static function getIp()
989 +
990 + public static function getIp($defalt = '127.0.0.1')
948 991 {
949 - $server = $_SERVER;
992 + static $ipAddress;
950 993
951 - $clientIp = Arr::get($server, 'HTTP_CLIENT_IP');
952 - $xForwarded = Arr::get($server, 'HTTP_X_FORWARDED_FOR');
994 + if ($ipAddress) {
995 + return $ipAddress;
996 + }
953 997
954 - if (!empty($clientIp)) {
955 - $ip = $clientIp;
956 - } elseif (!empty($xForwarded)) {
957 - $ip = $clientIp;
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 + }
958 1138 } else {
959 - $ip = $clientIp;
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;
960 1155 }
961 -
962 - return sanitize_text_field($ip);
963 1156 }
964 1157
965 1158 public static function fcal_sanitize_html($html)
966 1159 {
@@ -981,9 +1174,8 @@
981 1174 $tags['iframe'] = [
982 1175 'width' => [],
983 1176 'height' => [],
984 1177 'src' => [],
985 - 'srcdoc' => [],
986 1178 'title' => [],
987 1179 'frameborder' => [],
988 1180 'allow' => [],
989 1181 'class' => [],
@@ -991,9 +1183,11 @@
991 1183 'allowfullscreen' => [],
992 1184 'style' => [],
993 1185 ];
994 1186 //button
995 - $tags['button']['onclick'] = [];
1187 + $tags['button'] = [
1188 + 'onclick' => []
1189 + ];
996 1190
997 1191 //svg
998 1192 if (empty($tags['svg'])) {
999 1193 $svg_args = [
@@ -1212,8 +1406,33 @@
1212 1406
1213 1407 return $durationLookup;
1214 1408 }
1215 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 +
1216 1435 public static function getBufferTimes()
1217 1436 {
1218 1437 return apply_filters('fluent_booking/buffer_times_schema', [
1219 1438 [
@@ -1489,12 +1708,31 @@
1489 1708 ],
1490 1709 [
1491 1710 'value' => 'date',
1492 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')
1493 1724 ]
1494 1725 ]);
1495 1726 }
1496 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 +
1497 1735 public static function getDefaultEmailNotificationSettings()
1498 1736 {
1499 1737 $assetUrl = App::getInstance()['url.assets'];
1500 1738
@@ -1507,9 +1745,9 @@
1507 1745 'enabled' => true,
1508 1746 'title' => __('Booking Confirmation Email to Attendee', 'fluent-booking'),
1509 1747 'email' => [
1510 1748 'subject' => 'Booking Confirmation between {{host.name}} & {{guest.full_name}}',
1511 - '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)
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)
1512 1750 ],
1513 1751 ],
1514 1752 'booking_conf_host' => [
1515 1753 'enabled' => true,
@@ -1517,9 +1755,9 @@
1517 1755 'title' => __('Booking Confirmation Email to Organizer (You)', 'fluent-booking'),
1518 1756 'email' => [
1519 1757 'additional_recipients' => '',
1520 1758 'subject' => 'New Booking: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1521 - '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>'
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>'
1522 1760 ],
1523 1761 ],
1524 1762 'reminder_to_attendee' => [
1525 1763 'enabled' => false,
@@ -1583,9 +1821,9 @@
1583 1821 'enabled' => true,
1584 1822 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'),
1585 1823 'email' => [
1586 1824 'subject' => 'Your booking was rescheduled with {{host.name}}',
1587 - '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}}</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)
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)
1588 1826 ],
1589 1827 ],
1590 1828 'booking_request_host' => [
1591 1829 'enabled' => true,
@@ -1593,9 +1831,9 @@
1593 1831 'title' => __('Booking Approval Request to Host (email to Organizer)', 'fluent-booking'),
1594 1832 'email' => [
1595 1833 'additional_recipients' => '',
1596 1834 'subject' => 'Awaiting Approval: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1597 - '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>'
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>'
1598 1836 ],
1599 1837 ],
1600 1838 'booking_request_attendee' => [
1601 1839 'enabled' => true,
@@ -1601,9 +1839,9 @@
1601 1839 'enabled' => true,
1602 1840 'title' => __('Booking Submission Confirmation (email to Attendee)', 'fluent-booking'),
1603 1841 'email' => [
1604 1842 'subject' => 'Booking Submitted: Meeting between {{host.name}} & {{guest.full_name}}',
1605 - '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>'
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>'
1606 1844 ],
1607 1845 ],
1608 1846 'declined_by_host' => [
1609 1847 'enabled' => true,
@@ -1633,10 +1871,17 @@
1633 1871
1634 1872 return apply_filters('fluent_booking/confirm_and_reject_button_html', $html);
1635 1873 }
1636 1874
1637 - public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false)
1875 + public static function getIframeHtml()
1638 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 + {
1639 1884 if (!$isHtmlSupported) {
1640 1885 $groups = [
1641 1886 'guest' => [
1642 1887 'title' => __('Attendee Data', 'fluent-booking'),
@@ -1641,15 +1886,16 @@
1641 1886 'guest' => [
1642 1887 'title' => __('Attendee Data', 'fluent-booking'),
1643 1888 'key' => 'guest',
1644 1889 'shortcodes' => [
1645 - '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1646 - '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1647 - '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1648 - '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1649 - '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1650 - '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1651 - '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking')
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')
1652 1898 ]
1653 1899 ],
1654 1900 'booking' => [
1655 1901 'title' => __('Booking Data', 'fluent-booking'),
@@ -1654,27 +1900,41 @@
1654 1900 'booking' => [
1655 1901 'title' => __('Booking Data', 'fluent-booking'),
1656 1902 'key' => 'booking',
1657 1903 'shortcodes' => [
1658 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1659 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1660 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1661 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1662 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1663 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1664 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1665 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1666 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1667 - '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'),
1668 - '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1669 - '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'),
1670 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1671 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1672 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1673 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1674 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1675 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1676 - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking')
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'),
1677 1937 ]
1678 1938 ],
1679 1939 'host' => [
1680 1940 'title' => __('Host Data', 'fluent-booking'),
@@ -1710,8 +1970,9 @@
1710 1970 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1711 1971 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1712 1972 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1713 1973 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1974 + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking'),
1714 1975 '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking')
1715 1976 ]
1716 1977 ],
1717 1978 'booking' => [
@@ -1717,27 +1978,41 @@
1717 1978 'booking' => [
1718 1979 'title' => __('Booking Data', 'fluent-booking'),
1719 1980 'key' => 'booking',
1720 1981 'shortcodes' => [
1721 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1722 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1723 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1724 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1725 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1726 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1727 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1728 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1729 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1730 - '{{booking.start_date_time_for_attendee}}' => __('Event Date time (with guest timezone)', 'fluent-booking'),
1731 - '{{booking.start_date_time_for_host}}' => __('Event Date time (with host timezone)', 'fluent-booking'),
1732 - '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'),
1733 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1734 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1735 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1736 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1737 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1738 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1739 - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking')
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'),
1740 2015 ]
1741 2016 ],
1742 2017 'host' => [
1743 2018 'title' => __('Host Data', 'fluent-booking'),
@@ -1843,11 +2118,17 @@
1843 2118 return $raw_value;
1844 2119 }
1845 2120
1846 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 + }
1847 2125
1848 2126 $method = 'aes-256-ctr';
1849 2127 $ivlen = openssl_cipher_iv_length($method);
2128 + if ($ivlen === false || strlen($raw_value) <= $ivlen) {
2129 + return false;
2130 + }
1850 2131 $iv = substr($raw_value, 0, $ivlen);
1851 2132
1852 2133 $raw_value = substr($raw_value, $ivlen);
1853 2134
@@ -1859,9 +2140,9 @@
1859 2140
1860 2141 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1861 2142
1862 2143 $value = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1863 - if (!$value || substr($value, -strlen($salt)) !== $salt) {
2144 + if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) {
1864 2145 return false;
1865 2146 }
1866 2147
1867 2148 return substr($value, 0, -strlen($salt));
@@ -1869,9 +2150,9 @@
1869 2150
1870 2151 public static function debugLog($data)
1871 2152 {
1872 2153 if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) {
1873 - error_log(print_r($data, true));
2154 + error_log(print_r($data, true)); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1874 2155 }
1875 2156 }
1876 2157
1877 2158 public static function getGlobalSettings($settingsKey = null)
@@ -1898,9 +2179,9 @@
1898 2179 'notification_day' => 'mon',
1899 2180 'start_day' => 'sun',
1900 2181 'auto_cancel_timing' => '10',
1901 2182 'auto_complete_timing' => '60',
1902 - 'default_phone_country' => ''
2183 + 'default_country' => ''
1903 2184 ],
1904 2185 'time_format' => '12',
1905 2186 'theme' => 'system-default'
1906 2187 ];
@@ -1910,14 +2191,10 @@
1910 2191 if (empty($settings)) {
1911 2192 $settings = [];
1912 2193 }
1913 2194
1914 - $paymentSettings = get_option('fluent_booking_global_payment_settings', []);
2195 + $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings();
1915 2196
1916 - if ($paymentSettings) {
1917 - $settings['payments'] = $paymentSettings;
1918 - }
1919 -
1920 2197 $settings = wp_parse_args($settings, $defaults);
1921 2198
1922 2199 $emailSettings = $settings['emailing'];
1923 2200
@@ -1987,8 +2264,28 @@
1987 2264 $format = Arr::get($settings, 'time_format', '24');
1988 2265 return $format;
1989 2266 }
1990 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 +
1991 2288 public static function getVerifiedSenders()
1992 2289 {
1993 2290 $verifiedSenders = [];
1994 2291 if (defined('FLUENTMAIL')) {
@@ -2064,9 +2361,9 @@
2064 2361 }
2065 2362 return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args);
2066 2363 }
2067 2364
2068 - public static function getPrefSettins($cached = true)
2365 + public static function getPrefSettings($cached = true)
2069 2366 {
2070 2367 static $pref = null;
2071 2368
2072 2369 if ($cached && $pref) {
@@ -2078,8 +2375,11 @@
2078 2375 'enabled' => 'no',
2079 2376 'slug' => 'my-bookings',
2080 2377 'render_type' => 'standalone',
2081 2378 'page_id' => ''
2379 + ],
2380 + 'coupon' => [
2381 + 'enabled' => 'no'
2082 2382 ]
2083 2383 ];
2084 2384
2085 2385 $storedSettings = get_option('fluent_booking_modules', []);
@@ -2092,6 +2392,128 @@
2092 2392
2093 2393 $pref = $settings;
2094 2394
2095 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 + }
2096 2518 }
2097 2519 }