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 +450 -84 1.5.21trunk 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';
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 = [
@@ -1212,9 +1404,10 @@
1212 1404
1213 1405 return $durationLookup;
1214 1406 }
1215 1407
1216 - public static function formatDuration($totalMinutes) {
1408 + public static function formatDuration($totalMinutes)
1409 + {
1217 1410 $days = floor($totalMinutes / 1440); // 1440 minutes in a day
1218 1411 $hours = floor(($totalMinutes % 1440) / 60);
1219 1412 $minutes = $totalMinutes % 60;
1220 1413
@@ -1513,12 +1706,31 @@
1513 1706 ],
1514 1707 [
1515 1708 'value' => 'date',
1516 1709 'label' => __('Date', 'fluent-booking')
1710 + ],
1711 + [
1712 + 'value' => 'file',
1713 + 'label' => __('File', 'fluent-booking')
1714 + ],
1715 + [
1716 + 'value' => 'hidden',
1717 + 'label' => __('Hidden', 'fluent-booking')
1718 + ],
1719 + [
1720 + 'value' => 'terms-and-conditions',
1721 + 'label' => __('Terms & Conditions', 'fluent-booking')
1517 1722 ]
1518 1723 ]);
1519 1724 }
1520 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 +
1521 1733 public static function getDefaultEmailNotificationSettings()
1522 1734 {
1523 1735 $assetUrl = App::getInstance()['url.assets'];
1524 1736
@@ -1607,9 +1819,9 @@
1607 1819 'enabled' => true,
1608 1820 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'),
1609 1821 'email' => [
1610 1822 '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)
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)
1612 1824 ],
1613 1825 ],
1614 1826 'booking_request_host' => [
1615 1827 'enabled' => true,
@@ -1657,10 +1869,17 @@
1657 1869
1658 1870 return apply_filters('fluent_booking/confirm_and_reject_button_html', $html);
1659 1871 }
1660 1872
1661 - public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false)
1873 + public static function getIframeHtml()
1662 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 + {
1663 1882 if (!$isHtmlSupported) {
1664 1883 $groups = [
1665 1884 'guest' => [
1666 1885 'title' => __('Attendee Data', 'fluent-booking'),
@@ -1665,15 +1884,16 @@
1665 1884 'guest' => [
1666 1885 'title' => __('Attendee Data', 'fluent-booking'),
1667 1886 'key' => 'guest',
1668 1887 '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')
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')
1676 1896 ]
1677 1897 ],
1678 1898 'booking' => [
1679 1899 'title' => __('Booking Data', 'fluent-booking'),
@@ -1678,27 +1898,41 @@
1678 1898 'booking' => [
1679 1899 'title' => __('Booking Data', 'fluent-booking'),
1680 1900 'key' => 'booking',
1681 1901 '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')
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'),
1701 1935 ]
1702 1936 ],
1703 1937 'host' => [
1704 1938 'title' => __('Host Data', 'fluent-booking'),
@@ -1734,9 +1968,9 @@
1734 1968 '{{guest.email}}' => __('Guest Email', 'fluent-booking'),
1735 1969 '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'),
1736 1970 '{{guest.note}}' => __('Guest Note', 'fluent-booking'),
1737 1971 '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'),
1738 - '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking')
1972 + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking')
1739 1973 ]
1740 1974 ],
1741 1975 'booking' => [
1742 1976 'title' => __('Booking Data', 'fluent-booking'),
@@ -1741,27 +1975,41 @@
1741 1975 'booking' => [
1742 1976 'title' => __('Booking Data', 'fluent-booking'),
1743 1977 'key' => 'booking',
1744 1978 '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')
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'),
1764 2012 ]
1765 2013 ],
1766 2014 'host' => [
1767 2015 'title' => __('Host Data', 'fluent-booking'),
@@ -1867,11 +2115,17 @@
1867 2115 return $raw_value;
1868 2116 }
1869 2117
1870 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 + }
1871 2122
1872 2123 $method = 'aes-256-ctr';
1873 2124 $ivlen = openssl_cipher_iv_length($method);
2125 + if ($ivlen === false || strlen($raw_value) <= $ivlen) {
2126 + return false;
2127 + }
1874 2128 $iv = substr($raw_value, 0, $ivlen);
1875 2129
1876 2130 $raw_value = substr($raw_value, $ivlen);
1877 2131
@@ -1883,9 +2137,9 @@
1883 2137
1884 2138 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
1885 2139
1886 2140 $value = openssl_decrypt($raw_value, $method, $key, 0, $iv);
1887 - if (!$value || substr($value, -strlen($salt)) !== $salt) {
2141 + if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) {
1888 2142 return false;
1889 2143 }
1890 2144
1891 2145 return substr($value, 0, -strlen($salt));
@@ -1893,9 +2147,9 @@
1893 2147
1894 2148 public static function debugLog($data)
1895 2149 {
1896 2150 if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) {
1897 - 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
1898 2152 }
1899 2153 }
1900 2154
1901 2155 public static function getGlobalSettings($settingsKey = null)
@@ -1922,9 +2176,9 @@
1922 2176 'notification_day' => 'mon',
1923 2177 'start_day' => 'sun',
1924 2178 'auto_cancel_timing' => '10',
1925 2179 'auto_complete_timing' => '60',
1926 - 'default_phone_country' => ''
2180 + 'default_country' => ''
1927 2181 ],
1928 2182 'time_format' => '12',
1929 2183 'theme' => 'system-default'
1930 2184 ];
@@ -1934,14 +2188,10 @@
1934 2188 if (empty($settings)) {
1935 2189 $settings = [];
1936 2190 }
1937 2191
1938 - $paymentSettings = get_option('fluent_booking_global_payment_settings', []);
2192 + $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings();
1939 2193
1940 - if ($paymentSettings) {
1941 - $settings['payments'] = $paymentSettings;
1942 - }
1943 -
1944 2194 $settings = wp_parse_args($settings, $defaults);
1945 2195
1946 2196 $emailSettings = $settings['emailing'];
1947 2197
@@ -2011,8 +2261,28 @@
2011 2261 $format = Arr::get($settings, 'time_format', '24');
2012 2262 return $format;
2013 2263 }
2014 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 +
2015 2285 public static function getVerifiedSenders()
2016 2286 {
2017 2287 $verifiedSenders = [];
2018 2288 if (defined('FLUENTMAIL')) {
@@ -2088,9 +2358,9 @@
2088 2358 }
2089 2359 return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args);
2090 2360 }
2091 2361
2092 - public static function getPrefSettins($cached = true)
2362 + public static function getPrefSettings($cached = true)
2093 2363 {
2094 2364 static $pref = null;
2095 2365
2096 2366 if ($cached && $pref) {
@@ -2102,8 +2372,11 @@
2102 2372 'enabled' => 'no',
2103 2373 'slug' => 'my-bookings',
2104 2374 'render_type' => 'standalone',
2105 2375 'page_id' => ''
2376 + ],
2377 + 'coupon' => [
2378 + 'enabled' => 'no'
2106 2379 ]
2107 2380 ];
2108 2381
2109 2382 $storedSettings = get_option('fluent_booking_modules', []);
@@ -2116,6 +2389,99 @@
2116 2389
2117 2390 $pref = $settings;
2118 2391
2119 2392 return $settings;
2393 + }
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 +
2411 + public static function getActiveThemeName()
2412 + {
2413 + $ins = get_option('_fb_ins_by');
2414 +
2415 + if ($ins) {
2416 + return sanitize_text_field($ins);
2417 + }
2418 +
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 + }
2120 2486 }
2121 2487 }