PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
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 1.7.2 All 33 releases
← All changes | app/Services/Helper.php +430 -84 1.5.24trunk View file →
@@ -693,11 +693,44 @@
693 693 {
694 694 return self::getAppBaseUrl('scheduled-events?booking_id=' . $bookingId);
695 695 }
696 696
697 - public static function getUpgradeUrl()
697 + /**
698 + * Build a spec-compliant "Upgrade to Pro" URL.
699 + *
700 + * Follows the shared Fluent* UTM spec:
701 + * utm_source = fluent-booking (fixed vocabulary, never the wp.org slug)
702 + * utm_medium = free_plugin | pro_plugin (acquisition vs cross-sell)
703 + * utm_campaign= upgrade_pro (override for xsell_<target> / license_* )
704 + * utm_content = the exact placement, e.g. feature_lock_team_calendar, upgrade_page
705 + * utm_term = plugin version that generated the link
706 + * utm_id = promo id, blank normally (omit unless passed)
707 + *
708 + * @param string $content The utm_content placement.
709 + * @param array $overrides Override any utm_* param (e.g. utm_campaign for cross-sell).
710 + * @return string
711 + */
712 + public static function getUpgradeUrl($content = 'upgrade_page', $overrides = [])
698 713 {
699 - return 'https://fluentbooking.com/pricing/?utm_source=plugin&utm_medium=wp_install&utm_campaign=fcal_upgrade&theme=' . self::getActiveThemeName();
714 + $baseUrl = apply_filters(
715 + 'fluent_booking/pro_upgrade_base_url',
716 + 'https://fluentbooking.com/pricing/'
717 + );
718 +
719 + $params = wp_parse_args($overrides, [
720 + 'utm_source' => 'fluent-booking',
721 + 'utm_medium' => defined('FLUENT_BOOKING_PRO_VERSION') ? 'pro_plugin' : 'free_plugin',
722 + 'utm_campaign' => 'upgrade_pro',
723 + 'utm_content' => $content,
724 + 'utm_term' => FLUENT_BOOKING_VERSION,
725 + ]);
726 +
727 + // Drop any blank params (e.g. an unset utm_id) so they never hit the URL.
728 + $params = array_filter($params, function ($value) {
729 + return $value !== '' && $value !== null;
730 + });
731 +
732 + return add_query_arg($params, $baseUrl);
700 733 }
701 734
702 735 public static function getNextBookingGroup()
703 736 {
@@ -714,8 +747,9 @@
714 747 {
715 748 static $index = 0;
716 749
717 750 $index += 1;
751 +
718 752 return $index;
719 753 }
720 754
721 755 public static function getGlobalPaymentSettings()
@@ -727,21 +761,14 @@
727 761 }
728 762
729 763 $settings = get_option('fluent_booking_global_payment_settings', []);
730 764
731 - if (!$settings) {
732 - $settings = [
733 - 'currency' => 'USD',
734 - 'is_active' => 'no'
735 - ];
736 - }
737 -
738 765 return $settings;
739 766 }
740 767
741 768 public static function isPaymentEnabled($calendarEvent = null)
742 769 {
743 - $settings = self::getGlobalPaymentSettings();
770 + $settings = CurrenciesHelper::getGlobalCurrencySettings();
744 771 if (Arr::get($settings, 'is_active') == 'yes') {
745 772 return true;
746 773 }
747 774
@@ -889,15 +916,29 @@
889 916 }
890 917
891 918 $user = get_user_by('ID', $userId);
892 919
893 - $name = trim($user->first_name . ' ' . $user->last_name);
920 + return self::getDisplayNameFromUser($user);
921 + }
894 922
895 - if ($name) {
923 + public static function getDisplayNameFromUser($user)
924 + {
925 + if (!$user) {
926 + return '';
927 + }
928 +
929 + $firstName = is_object($user) ? ($user->first_name ?? '') : '';
930 + $lastName = is_object($user) ? ($user->last_name ?? '') : '';
931 +
932 + $name = trim($firstName . ' ' . $lastName);
933 +
934 + if ($name !== '') {
896 935 return $name;
897 936 }
898 937
899 - return $user->display_name;
938 + $displayName = is_object($user) ? ($user->display_name ?? '') : '';
939 +
940 + return (string) $displayName;
900 941 }
901 942
902 943 public static function getUserEmail($userId = null)
903 944 {
@@ -943,24 +984,174 @@
943 984
944 985 return apply_filters('fluent_booking/slot_slug', $default, $original);
945 986 }
946 987
947 - public static function getIp()
988 +
989 + public static function getIp($defalt = '127.0.0.1')
948 990 {
949 - $server = $_SERVER;
991 + static $ipAddress;
950 992
951 - $clientIp = Arr::get($server, 'HTTP_CLIENT_IP');
952 - $xForwarded = Arr::get($server, 'HTTP_X_FORWARDED_FOR');
993 + if ($ipAddress) {
994 + return $ipAddress;
995 + }
953 996
954 - if (!empty($clientIp)) {
955 - $ip = $clientIp;
956 - } elseif (!empty($xForwarded)) {
957 - $ip = $clientIp;
997 + if (empty($_SERVER['REMOTE_ADDR'])) {
998 + // It's a local cli request
999 + return $defalt;
1000 + }
1001 +
1002 + $ipAddress = '';
1003 +
1004 + $serverData = $_SERVER;
1005 + $HTTP_CF_CONNECTING_IP = Arr::get($serverData, 'HTTP_CF_CONNECTING_IP');
1006 + $RemoteAddr = Arr::get($serverData, 'REMOTE_ADDR');
1007 + $clientIp = Arr::get($serverData, 'HTTP_CLIENT_IP');
1008 + $HTTP_X_FORWARDED_FOR = Arr::get($serverData, 'HTTP_X_FORWARDED_FOR');
1009 + if ($HTTP_CF_CONNECTING_IP) {
1010 + //If it's a valid Cloudflare request
1011 +
1012 + if (self::isCfIp($RemoteAddr)) {
1013 + //Use the CF-Connecting-IP header.
1014 + $ipAddress = $HTTP_CF_CONNECTING_IP;
1015 + } else {
1016 + //If it isn't valid, then use REMOTE_ADDR.
1017 + $ipAddress = $RemoteAddr;
1018 + }
1019 + } else if ($RemoteAddr == '127.0.0.1') {
1020 + // most probably it's local reverse proxy
1021 + if ($clientIp) {
1022 + $ipAddress = $clientIp;
1023 + } else if ($HTTP_X_FORWARDED_FOR) {
1024 + $ipAddress = (string)rest_is_ip_address(trim(current(preg_split('/,/', sanitize_text_field($HTTP_X_FORWARDED_FOR)))));
1025 + }
1026 + }
1027 +
1028 + if (!$ipAddress) {
1029 + $ipAddress = $RemoteAddr;
1030 + }
1031 +
1032 + $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress);
1033 +
1034 + $ipAddress = apply_filters('fluent_booking/user_ip', $ipAddress, []);
1035 +
1036 + $ipAddress = sanitize_text_field(wp_unslash($ipAddress));
1037 +
1038 + return $ipAddress;
1039 + }
1040 +
1041 + /**
1042 + * Valid E.164 number: 7-15 significant digits.
1043 + *
1044 + * @param string $phone
1045 + * @return bool
1046 + */
1047 + public static function isValidPhoneNumber($phone)
1048 + {
1049 + if (!apply_filters('fluent_booking/enforce_phone_validation', true)) {
1050 + return true;
1051 + }
1052 +
1053 + $phone = trim((string)$phone);
1054 +
1055 + if ($phone === '') {
1056 + return false;
1057 + }
1058 +
1059 + if (!preg_match('/^\+?[0-9\s().\-]+$/', $phone)) {
1060 + return false;
1061 + }
1062 +
1063 + $digitCount = strlen(preg_replace('/\D/', '', $phone));
1064 +
1065 + $min = (int)apply_filters('fluent_booking/phone_min_digits', 7);
1066 + $max = (int)apply_filters('fluent_booking/phone_max_digits', 15);
1067 +
1068 + return $digitCount >= $min && $digitCount <= $max;
1069 + }
1070 +
1071 + private static function isCfIp($ip = '')
1072 + {
1073 + if (!$ip) {
1074 + $serverData = $_SERVER;
1075 + $REMOTE_ADDR = Arr::get($serverData, 'REMOTE_ADDR');
1076 + $ip = $REMOTE_ADDR;
1077 + }
1078 + $cloudflareIPRanges = array(
1079 + '173.245.48.0/20',
1080 + '103.21.244.0/22',
1081 + '103.22.200.0/22',
1082 + '103.31.4.0/22',
1083 + '141.101.64.0/18',
1084 + '108.162.192.0/18',
1085 + '190.93.240.0/20',
1086 + '188.114.96.0/20',
1087 + '197.234.240.0/22',
1088 + '198.41.128.0/17',
1089 + '162.158.0.0/15',
1090 + '104.16.0.0/13',
1091 + '104.24.0.0/14',
1092 + '172.64.0.0/13',
1093 + '131.0.72.0/22',
1094 + );
1095 + //Make sure that the request came via Cloudflare.
1096 + foreach ($cloudflareIPRanges as $range) {
1097 + //Use the ip_in_range function from Joomla.
1098 + if (self::ipInRange($ip, $range)) {
1099 + //IP is valid. Belongs to Cloudflare.
1100 + return true;
1101 + }
1102 + }
1103 +
1104 + return false;
1105 + }
1106 +
1107 + private static function ipInRange($ip, $range)
1108 + {
1109 + if (strpos($range, '/') !== false) {
1110 + // $range is in IP/NETMASK format
1111 + list($range, $netmask) = explode('/', $range, 2);
1112 + if (strpos($netmask, '.') !== false) {
1113 + // $netmask is a 255.255.0.0 format
1114 + $netmask = str_replace('*', '0', $netmask);
1115 + $netmask_dec = ip2long($netmask);
1116 + return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
1117 + } else {
1118 + // $netmask is a CIDR size block
1119 + // fix the range argument
1120 + $x = explode('.', $range);
1121 + while (count($x) < 4) $x[] = '0';
1122 + list($a, $b, $c, $d) = $x;
1123 + $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
1124 + $range_dec = ip2long($range);
1125 + $ip_dec = ip2long($ip);
1126 +
1127 + # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
1128 + #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
1129 +
1130 + # Strategy 2 - Use math to create it
1131 + $wildcard_dec = pow(2, (32 - $netmask)) - 1;
1132 + $netmask_dec = ~$wildcard_dec;
1133 +
1134 + return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
1135 + }
958 1136 } else {
959 - $ip = $clientIp;
1137 + // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
1138 + if (strpos($range, '*') !== false) { // a.b.*.* format
1139 + // Just convert to A-B format by setting * to 0 for A and 255 for B
1140 + $lower = str_replace('*', '0', $range);
1141 + $upper = str_replace('*', '255', $range);
1142 + $range = "$lower-$upper";
1143 + }
1144 +
1145 + if (strpos($range, '-') !== false) { // A-B format
1146 + list($lower, $upper) = explode('-', $range, 2);
1147 + $lower_dec = (float)sprintf("%u", ip2long($lower));
1148 + $upper_dec = (float)sprintf("%u", ip2long($upper));
1149 + $ip_dec = (float)sprintf("%u", ip2long($ip));
1150 + return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
1151 + }
1152 + return false;
960 1153 }
961 -
962 - return sanitize_text_field($ip);
963 1154 }
964 1155
965 1156 public static function fcal_sanitize_html($html)
966 1157 {
@@ -981,9 +1172,8 @@
981 1172 $tags['iframe'] = [
982 1173 'width' => [],
983 1174 'height' => [],
984 1175 'src' => [],
985 - 'srcdoc' => [],
986 1176 'title' => [],
987 1177 'frameborder' => [],
988 1178 'allow' => [],
989 1179 'class' => [],
@@ -991,9 +1181,11 @@
991 1181 'allowfullscreen' => [],
992 1182 'style' => [],
993 1183 ];
994 1184 //button
995 - $tags['button']['onclick'] = [];
1185 + $tags['button'] = [
1186 + 'onclick' => []
1187 + ];
996 1188
997 1189 //svg
998 1190 if (empty($tags['svg'])) {
999 1191 $svg_args = [
@@ -1522,12 +1714,23 @@
1522 1714 ],
1523 1715 [
1524 1716 'value' => 'hidden',
1525 1717 'label' => __('Hidden', 'fluent-booking')
1718 + ],
1719 + [
1720 + 'value' => 'terms-and-conditions',
1721 + 'label' => __('Terms & Conditions', 'fluent-booking')
1526 1722 ]
1527 1723 ]);
1528 1724 }
1529 1725
1726 + public static function getDefaultTermsAndConditions()
1727 + {
1728 + $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');
1729 +
1730 + return apply_filters('fluent_booking/default_terms_and_conditions', $termsAndConditions);
1731 + }
1732 +
1530 1733 public static function getDefaultEmailNotificationSettings()
1531 1734 {
1532 1735 $assetUrl = App::getInstance()['url.assets'];
1533 1736
@@ -1616,9 +1819,9 @@
1616 1819 'enabled' => true,
1617 1820 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'),
1618 1821 'email' => [
1619 1822 'subject' => 'Your booking was rescheduled with {{host.name}}',
1620 - 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $scheduleImage . '" alt="" width="60" height="60" /></p><h2 style="text-align: center;">Booking Rescheduled</h2><hr /><p>Your scheduled meeting has been rescheduled. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>New Time: {{booking.full_start_end_host_timezone}} <span style="color: #ff0000;"><strong>(new)</strong></span></p><p>Previous Time: {{booking.previous_meeting_time}}</p><p><strong>Rescheduling Reason</strong></p><p>{{booking.reschedule_reason}}</p><hr /><p style="text-align: center;">' . __('Need to make a change?', 'fluent-booking') . ' <a href="##booking.reschedule_url##">' . __('Reschedule', 'fluent-booking') . '</a> or <a href="##booking.cancelation_url##">' . __('Cancel', 'fluent-booking') . '</a></p><hr/>' . self::getAddToCalendarHtml($assetUrl)
1823 + 'body' => '<p style="text-align: center;"><img class="alignnone wp-image-76" src="' . $scheduleImage . '" alt="" width="60" height="60" /></p><h2 style="text-align: center;">Booking Rescheduled</h2><hr /><p>Your scheduled meeting has been rescheduled. Here are the details:</p><p><strong>Event Name</strong></p><p>{{booking.event_name}} with {{guest.full_name}}</p><p><strong>When</strong></p><p>New Time: {{booking.full_start_end_host_timezone}} <span style="color: #ff0000;"><strong>(new)</strong></span></p><p>Previous Time: {{booking.previous_meeting_time_guest_timezone}}</p><p><strong>Rescheduling Reason</strong></p><p>{{booking.reschedule_reason}}</p><hr /><p style="text-align: center;">' . __('Need to make a change?', 'fluent-booking') . ' <a href="##booking.reschedule_url##">' . __('Reschedule', 'fluent-booking') . '</a> or <a href="##booking.cancelation_url##">' . __('Cancel', 'fluent-booking') . '</a></p><hr/>' . self::getAddToCalendarHtml($assetUrl)
1621 1824 ],
1622 1825 ],
1623 1826 'booking_request_host' => [
1624 1827 'enabled' => true,
@@ -1666,10 +1869,17 @@
1666 1869
1667 1870 return apply_filters('fluent_booking/confirm_and_reject_button_html', $html);
1668 1871 }
1669 1872
1670 - public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false)
1873 + public static function getIframeHtml()
1671 1874 {
1875 + $html = '<iframe id="fluentbooking" loading="lazy" height="700px" width="100%" style="min-width:320px;height:700px;" frameborder="0" src="##landing_page_url##"></iframe>';
1876 +
1877 + return apply_filters('fluent_booking/get_iframe_html', $html);
1878 + }
1879 +
1880 + public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false, $iframeHtml = '')
1881 + {
1672 1882 if (!$isHtmlSupported) {
1673 1883 $groups = [
1674 1884 'guest' => [
1675 1885 'title' => __('Attendee Data', 'fluent-booking'),
@@ -1674,15 +1884,16 @@
1674 1884 'guest' => [
1675 1885 'title' => __('Attendee Data', 'fluent-booking'),
1676 1886 'key' => 'guest',
1677 1887 'shortcodes' => [
1678 - '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1679 - '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1680 - '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1681 - '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1682 - '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1683 - '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1684 - '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking')
1888 + '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'),
1889 + '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'),
1890 + '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'),
1891 + '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1892 + '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1893 + '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1894 + '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1895 + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking')
1685 1896 ]
1686 1897 ],
1687 1898 'booking' => [
1688 1899 'title' => __('Booking Data', 'fluent-booking'),
@@ -1687,27 +1898,41 @@
1687 1898 'booking' => [
1688 1899 'title' => __('Booking Data', 'fluent-booking'),
1689 1900 'key' => 'booking',
1690 1901 'shortcodes' => [
1691 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1692 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1693 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1694 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1695 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1696 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1697 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1698 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1699 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1700 - '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'),
1701 - '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1702 - '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'),
1703 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1704 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1705 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1706 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1707 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1708 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1709 - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking')
1902 + '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1903 + '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1904 + '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1905 + '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1906 + '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1907 + '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1908 + '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1909 + '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1910 + '{{booking.all_bookings_short_times_guest_timezone}}' => __('All Bookings Short Times (with guest timezone)', 'fluent-booking'),
1911 + '{{booking.all_bookings_short_times_host_timezone}}' => __('All Bookings Short Times (with host timezone)', 'fluent-booking'),
1912 + '{{booking.all_bookings_full_times_guest_timezone}}' => __('All Bookings Full Times (with guest timezone)', 'fluent-booking'),
1913 + '{{booking.all_bookings_full_times_host_timezone}}' => __('All Bookings Full Times (with host timezone)', 'fluent-booking'),
1914 + '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1915 + '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'),
1916 + '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1917 + '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1918 + '{{booking.start_date_time_for_host.format.Y-m-d}}' => __('Event Date Time (with host timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1919 + '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'),
1920 + '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1921 + '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1922 + '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1923 + '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1924 + '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1925 + '{{booking.source_url}}' => __('Source URL', 'fluent-booking'),
1926 + '{{booking.utm_source}}' => __('UTM Source', 'fluent-booking'),
1927 + '{{booking.utm_medium}}' => __('UTM Medium', 'fluent-booking'),
1928 + '{{booking.utm_campaign}}' => __('UTM Campaign', 'fluent-booking'),
1929 + '{{booking.utm_term}}' => __('UTM Term', 'fluent-booking'),
1930 + '{{booking.utm_content}}' => __('UTM Content', 'fluent-booking'),
1931 + '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1932 + '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking'),
1933 + '{{booking.previous_meeting_date_time_host_timezone}}' => __('Previous Meeting Date & Time (with host timezone)', 'fluent-booking'),
1934 + '{{booking.previous_meeting_date_time_guest_timezone}}' => __('Previous Meeting Date & Time (with guest timezone)', 'fluent-booking'),
1710 1935 ]
1711 1936 ],
1712 1937 'host' => [
1713 1938 'title' => __('Host Data', 'fluent-booking'),
@@ -1743,9 +1968,9 @@
1743 1968 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1744 1969 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1745 1970 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1746 1971 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1747 - '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking')
1972 + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking')
1748 1973 ]
1749 1974 ],
1750 1975 'booking' => [
1751 1976 'title' => __('Booking Data', 'fluent-booking'),
@@ -1750,27 +1975,41 @@
1750 1975 'booking' => [
1751 1976 'title' => __('Booking Data', 'fluent-booking'),
1752 1977 'key' => 'booking',
1753 1978 'shortcodes' => [
1754 - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1755 - '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1756 - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1757 - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1758 - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1759 - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1760 - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1761 - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1762 - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1763 - '{{booking.start_date_time_for_attendee}}' => __('Event Date time (with guest timezone)', 'fluent-booking'),
1764 - '{{booking.start_date_time_for_host}}' => __('Event Date time (with host timezone)', 'fluent-booking'),
1765 - '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'),
1766 - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1767 - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1768 - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
1769 - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
1770 - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
1771 - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
1772 - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking')
1979 + '{{booking.event_name}}' => __('Event Name', 'fluent-booking'),
1980 + '{{booking.description}}' => __('Event Description', 'fluent-booking'),
1981 + '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'),
1982 + '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'),
1983 + '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'),
1984 + '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'),
1985 + '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'),
1986 + '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'),
1987 + '{{booking.all_bookings_short_times_guest_timezone}}' => __('All Bookings Short Times (with guest timezone)', 'fluent-booking'),
1988 + '{{booking.all_bookings_short_times_host_timezone}}' => __('All Bookings Short Times (with host timezone)', 'fluent-booking'),
1989 + '{{booking.all_bookings_full_times_guest_timezone}}' => __('All Bookings Full Times (with guest timezone)', 'fluent-booking'),
1990 + '{{booking.all_bookings_full_times_host_timezone}}' => __('All Bookings Full Times (with host timezone)', 'fluent-booking'),
1991 + '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'),
1992 + '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with guest timezone)', 'fluent-booking'),
1993 + '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'),
1994 + '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1995 + '{{booking.start_date_time_for_host.format.Y-m-d}}' => __('Event Date Time (with host timezone) (Ex: 2024-05-20)', 'fluent-booking'),
1996 + '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'),
1997 + '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'),
1998 + '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'),
1999 + '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'),
2000 + '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'),
2001 + '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'),
2002 + '{{booking.source_url}}' => __('Source URL', 'fluent-booking'),
2003 + '{{booking.utm_source}}' => __('UTM Source', 'fluent-booking'),
2004 + '{{booking.utm_medium}}' => __('UTM Medium', 'fluent-booking'),
2005 + '{{booking.utm_campaign}}' => __('UTM Campaign', 'fluent-booking'),
2006 + '{{booking.utm_term}}' => __('UTM Term', 'fluent-booking'),
2007 + '{{booking.utm_content}}' => __('UTM Content', 'fluent-booking'),
2008 + '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'),
2009 + '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking'),
2010 + '{{booking.previous_meeting_date_time_host_timezone}}' => __('Previous Meeting Date & Time (with host timezone)', 'fluent-booking'),
2011 + '{{booking.previous_meeting_date_time_guest_timezone}}' => __('Previous Meeting Date & Time (with guest timezone)', 'fluent-booking'),
1773 2012 ]
1774 2013 ],
1775 2014 'host' => [
1776 2015 'title' => __('Host Data', 'fluent-booking'),
@@ -1876,11 +2115,17 @@
1876 2115 return $raw_value;
1877 2116 }
1878 2117
1879 2118 $raw_value = base64_decode($raw_value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
2119 + if ($raw_value === false) {
2120 + return false;
2121 + }
1880 2122
1881 2123 $method = 'aes-256-ctr';
1882 2124 $ivlen = openssl_cipher_iv_length($method);
2125 + if ($ivlen === false || strlen($raw_value) <= $ivlen) {
2126 + return false;
2127 + }
1883 2128 $iv = substr($raw_value, 0, $ivlen);
1884 2129
1885 2130 $raw_value = substr($raw_value, $ivlen);
1886 2131
@@ -1892,9 +2137,9 @@
1892 2137
1893 2138 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1894 2139
1895 2140 $value = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1896 - if (!$value || substr($value, -strlen($salt)) !== $salt) {
2141 + if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) {
1897 2142 return false;
1898 2143 }
1899 2144
1900 2145 return substr($value, 0, -strlen($salt));
@@ -1902,9 +2147,9 @@
1902 2147
1903 2148 public static function debugLog($data)
1904 2149 {
1905 2150 if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) {
1906 - error_log(print_r($data, true));
2151 + error_log(print_r($data, true)); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
1907 2152 }
1908 2153 }
1909 2154
1910 2155 public static function getGlobalSettings($settingsKey = null)
@@ -1931,9 +2176,9 @@
1931 2176 'notification_day' => 'mon',
1932 2177 'start_day' => 'sun',
1933 2178 'auto_cancel_timing' => '10',
1934 2179 'auto_complete_timing' => '60',
1935 - 'default_phone_country' => ''
2180 + 'default_country' => ''
1936 2181 ],
1937 2182 'time_format' => '12',
1938 2183 'theme' => 'system-default'
1939 2184 ];
@@ -1943,14 +2188,10 @@
1943 2188 if (empty($settings)) {
1944 2189 $settings = [];
1945 2190 }
1946 2191
1947 - $paymentSettings = get_option('fluent_booking_global_payment_settings', []);
2192 + $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings();
1948 2193
1949 - if ($paymentSettings) {
1950 - $settings['payments'] = $paymentSettings;
1951 - }
1952 -
1953 2194 $settings = wp_parse_args($settings, $defaults);
1954 2195
1955 2196 $emailSettings = $settings['emailing'];
1956 2197
@@ -2020,8 +2261,28 @@
2020 2261 $format = Arr::get($settings, 'time_format', '24');
2021 2262 return $format;
2022 2263 }
2023 2264
2265 + public static function getDefaultBookingFilters()
2266 + {
2267 + return apply_filters('fluent_booking/default_booking_filters', [
2268 + 'period' => 'upcoming',
2269 + 'author' => 'me', // me, all, calendar_id
2270 + 'event' => 'all',
2271 + 'event_type' => 'all'
2272 + ]);
2273 + }
2274 +
2275 + public static function getDefaultPaginations()
2276 + {
2277 + return apply_filters('fluent_booking/default_paginations', [
2278 + 'bookings' => 10,
2279 + 'calendars' => 10,
2280 + 'coupons' => 10,
2281 + 'availabilities' => 10
2282 + ]);
2283 + }
2284 +
2024 2285 public static function getVerifiedSenders()
2025 2286 {
2026 2287 $verifiedSenders = [];
2027 2288 if (defined('FLUENTMAIL')) {
@@ -2097,9 +2358,9 @@
2097 2358 }
2098 2359 return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args);
2099 2360 }
2100 2361
2101 - public static function getPrefSettins($cached = true)
2362 + public static function getPrefSettings($cached = true)
2102 2363 {
2103 2364 static $pref = null;
2104 2365
2105 2366 if ($cached && $pref) {
@@ -2111,8 +2372,11 @@
2111 2372 'enabled' => 'no',
2112 2373 'slug' => 'my-bookings',
2113 2374 'render_type' => 'standalone',
2114 2375 'page_id' => ''
2376 + ],
2377 + 'coupon' => [
2378 + 'enabled' => 'no'
2115 2379 ]
2116 2380 ];
2117 2381
2118 2382 $storedSettings = get_option('fluent_booking_modules', []);
@@ -2127,8 +2391,24 @@
2127 2391
2128 2392 return $settings;
2129 2393 }
2130 2394
2395 + public static function getPrefSettins($cached = true)
2396 + {
2397 + return self::getPrefSettings($cached);
2398 + }
2399 +
2400 + public static function getFeatures()
2401 + {
2402 + return apply_filters('fluent_booking/get_features', [
2403 + 'has_fluentcrm' => defined('FLUENTCRM'),
2404 + 'has_fluentsmtp' => defined('FLUENTMAIL'),
2405 + 'has_fluentform' => defined('FLUENTFORM'),
2406 + 'has_fluentboards' => defined('FLUENT_BOARDS'),
2407 + 'has_fluentcart' => defined('FLUENTCART_VERSION')
2408 + ]);
2409 + }
2410 +
2131 2411 public static function getActiveThemeName()
2132 2412 {
2133 2413 $ins = get_option('_fb_ins_by');
2134 2414
@@ -2134,8 +2414,74 @@
2134 2414
2135 2415 if ($ins) {
2136 2416 return sanitize_text_field($ins);
2137 2417 }
2138 -
2418 +
2139 2419 return get_option('template');
2420 + }
2421 +
2422 + /**
2423 + * Per-IP fixed-window rate limiter for public AJAX/REST endpoints.
2424 + *
2425 + * @param string $action Action name (e.g. apply_coupon, schedule_meeting).
2426 + * @param int $limit Max requests per window.
2427 + * @param int $window Window in seconds.
2428 + * @return bool True if under the limit (and the count was incremented),
2429 + * false if over.
2430 + */
2431 + public static function checkRateLimit($action, $limit, $window = 60)
2432 + {
2433 + $args = apply_filters('fluent_booking/public_ajax_ratelimit', [
2434 + 'limit' => $limit,
2435 + 'window' => $window,
2436 + ], $action);
2437 +
2438 + $limit = max(1, (int) (isset($args['limit']) ? $args['limit'] : $limit));
2439 + $window = max(1, (int) (isset($args['window']) ? $args['window'] : $window));
2440 +
2441 + $key = 'fcal_ratelimit_' . $action . '_' . md5(self::getIp());
2442 + $count = (int) get_transient($key);
2443 +
2444 + if ($count >= $limit) {
2445 + return false;
2446 + }
2447 +
2448 + set_transient($key, $count + 1, $window);
2449 +
2450 + return true;
2451 + }
2452 +
2453 + /**
2454 + * Run a callback inside a database transaction, re-throwing on failure so
2455 + * the caller decides how to report it.
2456 + *
2457 + * \Throwable, not \Exception: a TypeError is an Error, and an
2458 + * Exception-only catch would leave the transaction open.
2459 + *
2460 + * Database writes only — a hook fired in here would hold the callback's
2461 + * rows locked for the length of a listener's outbound request.
2462 + *
2463 + * @param callable $callback
2464 + *
2465 + * @return mixed
2466 + *
2467 + * @throws \Throwable after the rollback
2468 + */
2469 + public static function dbTransaction($callback)
2470 + {
2471 + $db = App::getInstance('db');
2472 +
2473 + $db->beginTransaction();
2474 +
2475 + try {
2476 + $result = $callback();
2477 +
2478 + $db->commit();
2479 +
2480 + return $result;
2481 + } catch (\Throwable $e) {
2482 + $db->rollBack();
2483 +
2484 + throw $e;
2485 + }
2140 2486 }
2141 2487 }