| @@ -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 | } |