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