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 +485 -87 1.5.20 → 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
@@ -1513,12 +1708,31 @@
1513 1708 ],
1514 1709 [
1515 1710 'value' => 'date',
1516 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')
1517 1724 ]
1518 1725 ]);
1519 1726 }
1520 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 +
1521 1735 public static function getDefaultEmailNotificationSettings()
1522 1736 {
1523 1737 $assetUrl = App::getInstance()['url.assets'];
1524 1738
@@ -1531,9 +1745,9 @@
1531 1745 'enabled' => true,
1532 1746 'title' => __('Booking Confirmation Email to Attendee', 'fluent-booking'),
1533 1747 'email' => [
1534 1748 'subject' => 'Booking Confirmation between {{host.name}} & {{guest.full_name}}',
1535 - '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)
1536 1750 ],
1537 1751 ],
1538 1752 'booking_conf_host' => [
1539 1753 'enabled' => true,
@@ -1541,9 +1755,9 @@
1541 1755 'title' => __('Booking Confirmation Email to Organizer (You)', 'fluent-booking'),
1542 1756 'email' => [
1543 1757 'additional_recipients' => '',
1544 1758 'subject' => 'New Booking: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1545 - '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>'
1546 1760 ],
1547 1761 ],
1548 1762 'reminder_to_attendee' => [
1549 1763 'enabled' => false,
@@ -1607,9 +1821,9 @@
1607 1821 'enabled' => true,
1608 1822 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'),
1609 1823 'email' => [
1610 1824 'subject' => 'Your booking was rescheduled with {{host.name}}',
1611 - '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)
1612 1826 ],
1613 1827 ],
1614 1828 'booking_request_host' => [
1615 1829 'enabled' => true,
@@ -1617,9 +1831,9 @@
1617 1831 'title' => __('Booking Approval Request to Host (email to Organizer)', 'fluent-booking'),
1618 1832 'email' => [
1619 1833 'additional_recipients' => '',
1620 1834 'subject' => 'Awaiting Approval: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1621 - '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>'
1622 1836 ],
1623 1837 ],
1624 1838 'booking_request_attendee' => [
1625 1839 'enabled' => true,
@@ -1625,9 +1839,9 @@
1625 1839 'enabled' => true,
1626 1840 'title' => __('Booking Submission Confirmation (email to Attendee)', 'fluent-booking'),
1627 1841 'email' => [
1628 1842 'subject' => 'Booking Submitted: Meeting between {{host.name}} & {{guest.full_name}}',
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;">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>'
1630 1844 ],
1631 1845 ],
1632 1846 'declined_by_host' => [
1633 1847 'enabled' => true,
@@ -1657,10 +1871,17 @@
1657 1871
1658 1872 return apply_filters('fluent_booking/confirm_and_reject_button_html', $html);
1659 1873 }
1660 1874
1661 - public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false)
1875 + public static function getIframeHtml()
1662 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 + {
1663 1884 if (!$isHtmlSupported) {
1664 1885 $groups = [
1665 1886 'guest' => [
1666 1887 'title' => __('Attendee Data', 'fluent-booking'),
@@ -1665,15 +1886,16 @@
1665 1886 'guest' => [
1666 1887 'title' => __('Attendee Data', 'fluent-booking'),
1667 1888 'key' => 'guest',
1668 1889 'shortcodes' => [
1669 - '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1670 - '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1671 - '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1672 - '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1673 - '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1674 - '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1675 - '{{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')
1676 1898 ]
1677 1899 ],
1678 1900 'booking' => [
1679 1901 'title' => __('Booking Data', 'fluent-booking'),
@@ -1678,27 +1900,41 @@
1678 1900 'booking' => [
1679 1901 'title' => __('Booking Data', 'fluent-booking'),
1680 1902 'key' => 'booking',
1681 1903 'shortcodes' => [
1682 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1683 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1684 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1685 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1686 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1687 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1688 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1689 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1690 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1691 - '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'),
1692 - '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1693 - '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'),
1694 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1695 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1696 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1697 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1698 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1699 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1700 - '{{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'),
1701 1937 ]
1702 1938 ],
1703 1939 'host' => [
1704 1940 'title' => __('Host Data', 'fluent-booking'),
@@ -1734,8 +1970,9 @@
1734 1970 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1735 1971 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1736 1972 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1737 1973 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1974 + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking'),
1738 1975 '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking')
1739 1976 ]
1740 1977 ],
1741 1978 'booking' => [
@@ -1741,27 +1978,41 @@
1741 1978 'booking' => [
1742 1979 'title' => __('Booking Data', 'fluent-booking'),
1743 1980 'key' => 'booking',
1744 1981 'shortcodes' => [
1745 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1746 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1747 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1748 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1749 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1750 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1751 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1752 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1753 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1754 - '{{booking.start_date_time_for_attendee}}' => __('Event Date time (with guest timezone)', 'fluent-booking'),
1755 - '{{booking.start_date_time_for_host}}' => __('Event Date time (with host timezone)', 'fluent-booking'),
1756 - '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'),
1757 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1758 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1759 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1760 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1761 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1762 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1763 - '{{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'),
1764 2015 ]
1765 2016 ],
1766 2017 'host' => [
1767 2018 'title' => __('Host Data', 'fluent-booking'),
@@ -1867,11 +2118,17 @@
1867 2118 return $raw_value;
1868 2119 }
1869 2120
1870 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 + }
1871 2125
1872 2126 $method = 'aes-256-ctr';
1873 2127 $ivlen = openssl_cipher_iv_length($method);
2128 + if ($ivlen === false || strlen($raw_value) <= $ivlen) {
2129 + return false;
2130 + }
1874 2131 $iv = substr($raw_value, 0, $ivlen);
1875 2132
1876 2133 $raw_value = substr($raw_value, $ivlen);
1877 2134
@@ -1883,9 +2140,9 @@
1883 2140
1884 2141 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1885 2142
1886 2143 $value = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1887 - if (!$value || substr($value, -strlen($salt)) !== $salt) {
2144 + if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) {
1888 2145 return false;
1889 2146 }
1890 2147
1891 2148 return substr($value, 0, -strlen($salt));
@@ -1893,9 +2150,9 @@
1893 2150
1894 2151 public static function debugLog($data)
1895 2152 {
1896 2153 if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) {
1897 - 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
1898 2155 }
1899 2156 }
1900 2157
1901 2158 public static function getGlobalSettings($settingsKey = null)
@@ -1922,9 +2179,9 @@
1922 2179 'notification_day' => 'mon',
1923 2180 'start_day' => 'sun',
1924 2181 'auto_cancel_timing' => '10',
1925 2182 'auto_complete_timing' => '60',
1926 - 'default_phone_country' => ''
2183 + 'default_country' => ''
1927 2184 ],
1928 2185 'time_format' => '12',
1929 2186 'theme' => 'system-default'
1930 2187 ];
@@ -1934,14 +2191,10 @@
1934 2191 if (empty($settings)) {
1935 2192 $settings = [];
1936 2193 }
1937 2194
1938 - $paymentSettings = get_option('fluent_booking_global_payment_settings', []);
2195 + $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings();
1939 2196
1940 - if ($paymentSettings) {
1941 - $settings['payments'] = $paymentSettings;
1942 - }
1943 -
1944 2197 $settings = wp_parse_args($settings, $defaults);
1945 2198
1946 2199 $emailSettings = $settings['emailing'];
1947 2200
@@ -2011,8 +2264,28 @@
2011 2264 $format = Arr::get($settings, 'time_format', '24');
2012 2265 return $format;
2013 2266 }
2014 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 +
2015 2288 public static function getVerifiedSenders()
2016 2289 {
2017 2290 $verifiedSenders = [];
2018 2291 if (defined('FLUENTMAIL')) {
@@ -2088,9 +2361,9 @@
2088 2361 }
2089 2362 return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args);
2090 2363 }
2091 2364
2092 - public static function getPrefSettins($cached = true)
2365 + public static function getPrefSettings($cached = true)
2093 2366 {
2094 2367 static $pref = null;
2095 2368
2096 2369 if ($cached && $pref) {
@@ -2102,8 +2375,11 @@
2102 2375 'enabled' => 'no',
2103 2376 'slug' => 'my-bookings',
2104 2377 'render_type' => 'standalone',
2105 2378 'page_id' => ''
2379 + ],
2380 + 'coupon' => [
2381 + 'enabled' => 'no'
2106 2382 ]
2107 2383 ];
2108 2384
2109 2385 $storedSettings = get_option('fluent_booking_modules', []);
@@ -2116,6 +2392,128 @@
2116 2392
2117 2393 $pref = $settings;
2118 2394
2119 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 + }
2120 2518 }
2121 2519 }