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 +416 -53 1.7.0 → 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 = [
@@ -1551,9 +1745,9 @@
1551 1745 'enabled' => true,
1552 1746 'title' => __('Booking Confirmation Email to Attendee', 'fluent-booking'),
1553 1747 'email' => [
1554 1748 'subject' => 'Booking Confirmation between {{host.name}} & {{guest.full_name}}',
1555 - '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)
1556 1750 ],
1557 1751 ],
1558 1752 'booking_conf_host' => [
1559 1753 'enabled' => true,
@@ -1561,9 +1755,9 @@
1561 1755 'title' => __('Booking Confirmation Email to Organizer (You)', 'fluent-booking'),
1562 1756 'email' => [
1563 1757 'additional_recipients' => '',
1564 1758 'subject' => 'New Booking: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1565 - '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>'
1566 1760 ],
1567 1761 ],
1568 1762 'reminder_to_attendee' => [
1569 1763 'enabled' => false,
@@ -1627,9 +1821,9 @@
1627 1821 'enabled' => true,
1628 1822 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'),
1629 1823 'email' => [
1630 1824 'subject' => 'Your booking was rescheduled with {{host.name}}',
1631 - '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)
1632 1826 ],
1633 1827 ],
1634 1828 'booking_request_host' => [
1635 1829 'enabled' => true,
@@ -1637,9 +1831,9 @@
1637 1831 'title' => __('Booking Approval Request to Host (email to Organizer)', 'fluent-booking'),
1638 1832 'email' => [
1639 1833 'additional_recipients' => '',
1640 1834 'subject' => 'Awaiting Approval: {{guest.full_name}} @ {{booking.start_date_time_for_host}}',
1641 - '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>'
1642 1836 ],
1643 1837 ],
1644 1838 'booking_request_attendee' => [
1645 1839 'enabled' => true,
@@ -1645,9 +1839,9 @@
1645 1839 'enabled' => true,
1646 1840 'title' => __('Booking Submission Confirmation (email to Attendee)', 'fluent-booking'),
1647 1841 'email' => [
1648 1842 'subject' => 'Booking Submitted: Meeting between {{host.name}} & {{guest.full_name}}',
1649 - '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>'
1650 1844 ],
1651 1845 ],
1652 1846 'declined_by_host' => [
1653 1847 'enabled' => true,
@@ -1677,10 +1871,17 @@
1677 1871
1678 1872 return apply_filters('fluent_booking/confirm_and_reject_button_html', $html);
1679 1873 }
1680 1874
1681 - public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false)
1875 + public static function getIframeHtml()
1682 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 + {
1683 1884 if (!$isHtmlSupported) {
1684 1885 $groups = [
1685 1886 'guest' => [
1686 1887 'title' => __('Attendee Data', 'fluent-booking'),
@@ -1685,15 +1886,16 @@
1685 1886 'guest' => [
1686 1887 'title' => __('Attendee Data', 'fluent-booking'),
1687 1888 'key' => 'guest',
1688 1889 'shortcodes' => [
1689 - '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1690 - '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1691 - '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1692 - '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1693 - '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1694 - '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1695 - '{{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')
1696 1898 ]
1697 1899 ],
1698 1900 'booking' => [
1699 1901 'title' => __('Booking Data', 'fluent-booking'),
@@ -1706,8 +1908,12 @@
1706 1908 '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1707 1909 '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1708 1910 '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1709 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'),
1710 1916 '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1711 1917 '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'),
1712 1918 '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1713 1919 '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'),
@@ -1717,10 +1923,18 @@
1717 1923 '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1718 1924 '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1719 1925 '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1720 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'),
1721 1933 '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1722 - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', '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'),
1723 1937 ]
1724 1938 ],
1725 1939 'host' => [
1726 1940 'title' => __('Host Data', 'fluent-booking'),
@@ -1756,8 +1970,9 @@
1756 1970 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1757 1971 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1758 1972 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1759 1973 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1974 + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking'),
1760 1975 '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking')
1761 1976 ]
1762 1977 ],
1763 1978 'booking' => [
@@ -1771,11 +1986,15 @@
1771 1986 '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1772 1987 '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1773 1988 '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1774 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'),
1775 1994 '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1776 - '{{booking.start_date_time_for_attendee}}' => __('Event Date time (with guest timezone)', 'fluent-booking'),
1777 - '{{booking.start_date_time_for_host}}' => __('Event Date time (with host timezone)', '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'),
1778 1997 '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1779 1998 '{{booking.start_date_time_for_host.format.Y-m-d}}' => __('Event Date Time (with host timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1780 1999 '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'),
1781 2000 '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
@@ -1782,10 +2001,18 @@
1782 2001 '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1783 2002 '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1784 2003 '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1785 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'),
1786 2011 '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1787 - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', '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'),
1788 2015 ]
1789 2016 ],
1790 2017 'host' => [
1791 2018 'title' => __('Host Data', 'fluent-booking'),
@@ -1891,11 +2118,17 @@
1891 2118 return $raw_value;
1892 2119 }
1893 2120
1894 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 + }
1895 2125
1896 2126 $method = 'aes-256-ctr';
1897 2127 $ivlen = openssl_cipher_iv_length($method);
2128 + if ($ivlen === false || strlen($raw_value) <= $ivlen) {
2129 + return false;
2130 + }
1898 2131 $iv = substr($raw_value, 0, $ivlen);
1899 2132
1900 2133 $raw_value = substr($raw_value, $ivlen);
1901 2134
@@ -1907,9 +2140,9 @@
1907 2140
1908 2141 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1909 2142
1910 2143 $value = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1911 - if (!$value || substr($value, -strlen($salt)) !== $salt) {
2144 + if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) {
1912 2145 return false;
1913 2146 }
1914 2147
1915 2148 return substr($value, 0, -strlen($salt));
@@ -1917,9 +2150,9 @@
1917 2150
1918 2151 public static function debugLog($data)
1919 2152 {
1920 2153 if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) {
1921 - 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
1922 2155 }
1923 2156 }
1924 2157
1925 2158 public static function getGlobalSettings($settingsKey = null)
@@ -1946,9 +2179,9 @@
1946 2179 'notification_day' => 'mon',
1947 2180 'start_day' => 'sun',
1948 2181 'auto_cancel_timing' => '10',
1949 2182 'auto_complete_timing' => '60',
1950 - 'default_phone_country' => ''
2183 + 'default_country' => ''
1951 2184 ],
1952 2185 'time_format' => '12',
1953 2186 'theme' => 'system-default'
1954 2187 ];
@@ -1958,14 +2191,10 @@
1958 2191 if (empty($settings)) {
1959 2192 $settings = [];
1960 2193 }
1961 2194
1962 - $paymentSettings = get_option('fluent_booking_global_payment_settings', []);
2195 + $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings();
1963 2196
1964 - if ($paymentSettings) {
1965 - $settings['payments'] = $paymentSettings;
1966 - }
1967 -
1968 2197 $settings = wp_parse_args($settings, $defaults);
1969 2198
1970 2199 $emailSettings = $settings['emailing'];
1971 2200
@@ -2035,8 +2264,28 @@
2035 2264 $format = Arr::get($settings, 'time_format', '24');
2036 2265 return $format;
2037 2266 }
2038 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 +
2039 2288 public static function getVerifiedSenders()
2040 2289 {
2041 2290 $verifiedSenders = [];
2042 2291 if (defined('FLUENTMAIL')) {
@@ -2112,9 +2361,9 @@
2112 2361 }
2113 2362 return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args);
2114 2363 }
2115 2364
2116 - public static function getPrefSettins($cached = true)
2365 + public static function getPrefSettings($cached = true)
2117 2366 {
2118 2367 static $pref = null;
2119 2368
2120 2369 if ($cached && $pref) {
@@ -2126,8 +2375,11 @@
2126 2375 'enabled' => 'no',
2127 2376 'slug' => 'my-bookings',
2128 2377 'render_type' => 'standalone',
2129 2378 'page_id' => ''
2379 + ],
2380 + 'coupon' => [
2381 + 'enabled' => 'no'
2130 2382 ]
2131 2383 ];
2132 2384
2133 2385 $storedSettings = get_option('fluent_booking_modules', []);
@@ -2142,8 +2394,24 @@
2142 2394
2143 2395 return $settings;
2144 2396 }
2145 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 +
2146 2414 public static function getActiveThemeName()
2147 2415 {
2148 2416 $ins = get_option('_fb_ins_by');
2149 2417
@@ -2149,8 +2417,103 @@
2149 2417
2150 2418 if ($ins) {
2151 2419 return sanitize_text_field($ins);
2152 2420 }
2153 -
2421 +
2154 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 + }
2155 2518 }
2156 2519 }