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 +465 -87 1.5.24 → 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&theme=' . self::getActiveThemeName();
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 = [
@@ -1522,12 +1716,23 @@
1522 1716 ],
1523 1717 [
1524 1718 'value' => 'hidden',
1525 1719 'label' => __('Hidden', 'fluent-booking')
1720 + ],
1721 + [
1722 + 'value' => 'terms-and-conditions',
1723 + 'label' => __('Terms & Conditions', 'fluent-booking')
1526 1724 ]
1527 1725 ]);
1528 1726 }
1529 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 +
1530 1735 public static function getDefaultEmailNotificationSettings()
1531 1736 {
1532 1737 $assetUrl = App::getInstance()['url.assets'];
1533 1738
@@ -1540,9 +1745,9 @@
1540 1745 'enabled' => true,
1541 1746 'title' => __('Booking Confirmation Email to Attendee', 'fluent-booking'),
1542 1747 'email' => [
1543 1748 'subject' => 'Booking Confirmation between {{host.name}} & {{guest.full_name}}',
1544 - '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)
1545 1750 ],
1546 1751 ],
1547 1752 'booking_conf_host' => [
1548 1753 'enabled' => true,
@@ -1550,9 +1755,9 @@
1550 1755 'title' => __('Booking Confirmation Email to Organizer (You)', 'fluent-booking'),
1551 1756 'email' => [
1552 1757 'additional_recipients' => '',
1553 1758 'subject' => 'New Booking: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1554 - '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>'
1555 1760 ],
1556 1761 ],
1557 1762 'reminder_to_attendee' => [
1558 1763 'enabled' => false,
@@ -1616,9 +1821,9 @@
1616 1821 'enabled' => true,
1617 1822 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'),
1618 1823 'email' => [
1619 1824 'subject' => 'Your booking was rescheduled with {{host.name}}',
1620 - '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)
1621 1826 ],
1622 1827 ],
1623 1828 'booking_request_host' => [
1624 1829 'enabled' => true,
@@ -1626,9 +1831,9 @@
1626 1831 'title' => __('Booking Approval Request to Host (email to Organizer)', 'fluent-booking'),
1627 1832 'email' => [
1628 1833 'additional_recipients' => '',
1629 1834 'subject' => 'Awaiting Approval: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1630 - '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>'
1631 1836 ],
1632 1837 ],
1633 1838 'booking_request_attendee' => [
1634 1839 'enabled' => true,
@@ -1634,9 +1839,9 @@
1634 1839 'enabled' => true,
1635 1840 'title' => __('Booking Submission Confirmation (email to Attendee)', 'fluent-booking'),
1636 1841 'email' => [
1637 1842 'subject' => 'Booking Submitted: Meeting between {{host.name}} & {{guest.full_name}}',
1638 - '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>'
1639 1844 ],
1640 1845 ],
1641 1846 'declined_by_host' => [
1642 1847 'enabled' => true,
@@ -1666,10 +1871,17 @@
1666 1871
1667 1872 return apply_filters('fluent_booking/confirm_and_reject_button_html', $html);
1668 1873 }
1669 1874
1670 - public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false)
1875 + public static function getIframeHtml()
1671 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 + {
1672 1884 if (!$isHtmlSupported) {
1673 1885 $groups = [
1674 1886 'guest' => [
1675 1887 'title' => __('Attendee Data', 'fluent-booking'),
@@ -1674,15 +1886,16 @@
1674 1886 'guest' => [
1675 1887 'title' => __('Attendee Data', 'fluent-booking'),
1676 1888 'key' => 'guest',
1677 1889 'shortcodes' => [
1678 - '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1679 - '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1680 - '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1681 - '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1682 - '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1683 - '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1684 - '{{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')
1685 1898 ]
1686 1899 ],
1687 1900 'booking' => [
1688 1901 'title' => __('Booking Data', 'fluent-booking'),
@@ -1687,27 +1900,41 @@
1687 1900 'booking' => [
1688 1901 'title' => __('Booking Data', 'fluent-booking'),
1689 1902 'key' => 'booking',
1690 1903 'shortcodes' => [
1691 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1692 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1693 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1694 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1695 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1696 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1697 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1698 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1699 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1700 - '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'),
1701 - '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1702 - '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'),
1703 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1704 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1705 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1706 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1707 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1708 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1709 - '{{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'),
1710 1937 ]
1711 1938 ],
1712 1939 'host' => [
1713 1940 'title' => __('Host Data', 'fluent-booking'),
@@ -1743,8 +1970,9 @@
1743 1970 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1744 1971 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1745 1972 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1746 1973 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1974 + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking'),
1747 1975 '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking')
1748 1976 ]
1749 1977 ],
1750 1978 'booking' => [
@@ -1750,27 +1978,41 @@
1750 1978 'booking' => [
1751 1979 'title' => __('Booking Data', 'fluent-booking'),
1752 1980 'key' => 'booking',
1753 1981 'shortcodes' => [
1754 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1755 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1756 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1757 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1758 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1759 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1760 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1761 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1762 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1763 - '{{booking.start_date_time_for_attendee}}' => __('Event Date time (with guest timezone)', 'fluent-booking'),
1764 - '{{booking.start_date_time_for_host}}' => __('Event Date time (with host timezone)', 'fluent-booking'),
1765 - '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'),
1766 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1767 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1768 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1769 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1770 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1771 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1772 - '{{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'),
1773 2015 ]
1774 2016 ],
1775 2017 'host' => [
1776 2018 'title' => __('Host Data', 'fluent-booking'),
@@ -1876,11 +2118,17 @@
1876 2118 return $raw_value;
1877 2119 }
1878 2120
1879 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 + }
1880 2125
1881 2126 $method = 'aes-256-ctr';
1882 2127 $ivlen = openssl_cipher_iv_length($method);
2128 + if ($ivlen === false || strlen($raw_value) <= $ivlen) {
2129 + return false;
2130 + }
1883 2131 $iv = substr($raw_value, 0, $ivlen);
1884 2132
1885 2133 $raw_value = substr($raw_value, $ivlen);
1886 2134
@@ -1892,9 +2140,9 @@
1892 2140
1893 2141 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1894 2142
1895 2143 $value = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1896 - if (!$value || substr($value, -strlen($salt)) !== $salt) {
2144 + if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) {
1897 2145 return false;
1898 2146 }
1899 2147
1900 2148 return substr($value, 0, -strlen($salt));
@@ -1902,9 +2150,9 @@
1902 2150
1903 2151 public static function debugLog($data)
1904 2152 {
1905 2153 if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) {
1906 - 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
1907 2155 }
1908 2156 }
1909 2157
1910 2158 public static function getGlobalSettings($settingsKey = null)
@@ -1931,9 +2179,9 @@
1931 2179 'notification_day' => 'mon',
1932 2180 'start_day' => 'sun',
1933 2181 'auto_cancel_timing' => '10',
1934 2182 'auto_complete_timing' => '60',
1935 - 'default_phone_country' => ''
2183 + 'default_country' => ''
1936 2184 ],
1937 2185 'time_format' => '12',
1938 2186 'theme' => 'system-default'
1939 2187 ];
@@ -1943,14 +2191,10 @@
1943 2191 if (empty($settings)) {
1944 2192 $settings = [];
1945 2193 }
1946 2194
1947 - $paymentSettings = get_option('fluent_booking_global_payment_settings', []);
2195 + $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings();
1948 2196
1949 - if ($paymentSettings) {
1950 - $settings['payments'] = $paymentSettings;
1951 - }
1952 -
1953 2197 $settings = wp_parse_args($settings, $defaults);
1954 2198
1955 2199 $emailSettings = $settings['emailing'];
1956 2200
@@ -2020,8 +2264,28 @@
2020 2264 $format = Arr::get($settings, 'time_format', '24');
2021 2265 return $format;
2022 2266 }
2023 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 +
2024 2288 public static function getVerifiedSenders()
2025 2289 {
2026 2290 $verifiedSenders = [];
2027 2291 if (defined('FLUENTMAIL')) {
@@ -2097,9 +2361,9 @@
2097 2361 }
2098 2362 return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args);
2099 2363 }
2100 2364
2101 - public static function getPrefSettins($cached = true)
2365 + public static function getPrefSettings($cached = true)
2102 2366 {
2103 2367 static $pref = null;
2104 2368
2105 2369 if ($cached && $pref) {
@@ -2111,8 +2375,11 @@
2111 2375 'enabled' => 'no',
2112 2376 'slug' => 'my-bookings',
2113 2377 'render_type' => 'standalone',
2114 2378 'page_id' => ''
2379 + ],
2380 + 'coupon' => [
2381 + 'enabled' => 'no'
2115 2382 ]
2116 2383 ];
2117 2384
2118 2385 $storedSettings = get_option('fluent_booking_modules', []);
@@ -2127,8 +2394,24 @@
2127 2394
2128 2395 return $settings;
2129 2396 }
2130 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 +
2131 2414 public static function getActiveThemeName()
2132 2415 {
2133 2416 $ins = get_option('_fb_ins_by');
2134 2417
@@ -2134,8 +2417,103 @@
2134 2417
2135 2418 if ($ins) {
2136 2419 return sanitize_text_field($ins);
2137 2420 }
2138 -
2421 +
2139 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 + }
2140 2518 }
2141 2519 }