| @@ -7,8 +7,9 @@ | ||
| 7 | 7 | use FluentBooking\App\Models\Calendar; |
| 8 | 8 | use FluentBooking\App\Models\CalendarSlot; |
| 9 | 9 | use FluentBooking\App\Models\Meta; |
| 10 | 10 | use FluentBooking\App\Models\BookingMeta; |
| 11 | +use FluentBooking\App\Modules\MCP\Support\SlotLock; | |
| 11 | 12 | use FluentBooking\Framework\Support\Arr; |
| 12 | 13 | |
| 13 | 14 | class Helper |
| 14 | 15 | { |
| @@ -693,11 +694,44 @@ | ||
| 693 | 694 | { |
| 694 | 695 | return self::getAppBaseUrl('scheduled-events?booking_id=' . $bookingId); |
| 695 | 696 | } |
| 696 | 697 | |
| 697 | - public static function getUpgradeUrl() | |
| 698 | + /** | |
| 699 | + * Build a spec-compliant "Upgrade to Pro" URL. | |
| 700 | + * | |
| 701 | + * Follows the shared Fluent* UTM spec: | |
| 702 | + * utm_source = fluent-booking (fixed vocabulary, never the wp.org slug) | |
| 703 | + * utm_medium = free_plugin | pro_plugin (acquisition vs cross-sell) | |
| 704 | + * utm_campaign= upgrade_pro (override for xsell_<target> / license_* ) | |
| 705 | + * utm_content = the exact placement, e.g. feature_lock_team_calendar, upgrade_page | |
| 706 | + * utm_term = plugin version that generated the link | |
| 707 | + * utm_id = promo id, blank normally (omit unless passed) | |
| 708 | + * | |
| 709 | + * @param string $content The utm_content placement. | |
| 710 | + * @param array $overrides Override any utm_* param (e.g. utm_campaign for cross-sell). | |
| 711 | + * @return string | |
| 712 | + */ | |
| 713 | + public static function getUpgradeUrl($content = 'upgrade_page', $overrides = []) | |
| 698 | 714 | { |
| 699 | - return 'https://fluentbooking.com/pricing/?utm_source=plugin&utm_medium=wp_install&utm_campaign=fcal_upgrade'; | |
| 715 | + $baseUrl = apply_filters( | |
| 716 | + 'fluent_booking/pro_upgrade_base_url', | |
| 717 | + 'https://fluentbooking.com/pricing/' | |
| 718 | + ); | |
| 719 | + | |
| 720 | + $params = wp_parse_args($overrides, [ | |
| 721 | + 'utm_source' => 'fluent-booking', | |
| 722 | + 'utm_medium' => defined('FLUENT_BOOKING_PRO_VERSION') ? 'pro_plugin' : 'free_plugin', | |
| 723 | + 'utm_campaign' => 'upgrade_pro', | |
| 724 | + 'utm_content' => $content, | |
| 725 | + 'utm_term' => FLUENT_BOOKING_VERSION, | |
| 726 | + ]); | |
| 727 | + | |
| 728 | + // Drop any blank params (e.g. an unset utm_id) so they never hit the URL. | |
| 729 | + $params = array_filter($params, function ($value) { | |
| 730 | + return $value !== '' && $value !== null; | |
| 731 | + }); | |
| 732 | + | |
| 733 | + return add_query_arg($params, $baseUrl); | |
| 700 | 734 | } |
| 701 | 735 | |
| 702 | 736 | public static function getNextBookingGroup() |
| 703 | 737 | { |
| @@ -714,8 +748,9 @@ | ||
| 714 | 748 | { |
| 715 | 749 | static $index = 0; |
| 716 | 750 | |
| 717 | 751 | $index += 1; |
| 752 | + | |
| 718 | 753 | return $index; |
| 719 | 754 | } |
| 720 | 755 | |
| 721 | 756 | public static function getGlobalPaymentSettings() |
| @@ -727,21 +762,14 @@ | ||
| 727 | 762 | } |
| 728 | 763 | |
| 729 | 764 | $settings = get_option('fluent_booking_global_payment_settings', []); |
| 730 | 765 | |
| 731 | - if (!$settings) { | |
| 732 | - $settings = [ | |
| 733 | - 'currency' => 'USD', | |
| 734 | - 'is_active' => 'no' | |
| 735 | - ]; | |
| 736 | - } | |
| 737 | - | |
| 738 | 766 | return $settings; |
| 739 | 767 | } |
| 740 | 768 | |
| 741 | 769 | public static function isPaymentEnabled($calendarEvent = null) |
| 742 | 770 | { |
| 743 | - $settings = self::getGlobalPaymentSettings(); | |
| 771 | + $settings = CurrenciesHelper::getGlobalCurrencySettings(); | |
| 744 | 772 | if (Arr::get($settings, 'is_active') == 'yes') { |
| 745 | 773 | return true; |
| 746 | 774 | } |
| 747 | 775 | |
| @@ -889,15 +917,29 @@ | ||
| 889 | 917 | } |
| 890 | 918 | |
| 891 | 919 | $user = get_user_by('ID', $userId); |
| 892 | 920 | |
| 893 | - $name = trim($user->first_name . ' ' . $user->last_name); | |
| 921 | + return self::getDisplayNameFromUser($user); | |
| 922 | + } | |
| 894 | 923 | |
| 895 | - if ($name) { | |
| 924 | + public static function getDisplayNameFromUser($user) | |
| 925 | + { | |
| 926 | + if (!$user) { | |
| 927 | + return ''; | |
| 928 | + } | |
| 929 | + | |
| 930 | + $firstName = is_object($user) ? ($user->first_name ?? '') : ''; | |
| 931 | + $lastName = is_object($user) ? ($user->last_name ?? '') : ''; | |
| 932 | + | |
| 933 | + $name = trim($firstName . ' ' . $lastName); | |
| 934 | + | |
| 935 | + if ($name !== '') { | |
| 896 | 936 | return $name; |
| 897 | 937 | } |
| 898 | 938 | |
| 899 | - return $user->display_name; | |
| 939 | + $displayName = is_object($user) ? ($user->display_name ?? '') : ''; | |
| 940 | + | |
| 941 | + return (string) $displayName; | |
| 900 | 942 | } |
| 901 | 943 | |
| 902 | 944 | public static function getUserEmail($userId = null) |
| 903 | 945 | { |
| @@ -943,24 +985,175 @@ | ||
| 943 | 985 | |
| 944 | 986 | return apply_filters('fluent_booking/slot_slug', $default, $original); |
| 945 | 987 | } |
| 946 | 988 | |
| 947 | - public static function getIp() | |
| 989 | + | |
| 990 | + public static function getIp($defalt = '127.0.0.1') | |
| 948 | 991 | { |
| 949 | - $server = $_SERVER; | |
| 992 | + static $ipAddress; | |
| 950 | 993 | |
| 951 | - $clientIp = Arr::get($server, 'HTTP_CLIENT_IP'); | |
| 952 | - $xForwarded = Arr::get($server, 'HTTP_X_FORWARDED_FOR'); | |
| 994 | + if ($ipAddress) { | |
| 995 | + return $ipAddress; | |
| 996 | + } | |
| 953 | 997 | |
| 954 | - if (!empty($clientIp)) { | |
| 955 | - $ip = $clientIp; | |
| 956 | - } elseif (!empty($xForwarded)) { | |
| 957 | - $ip = $clientIp; | |
| 998 | + if (empty($_SERVER['REMOTE_ADDR'])) { | |
| 999 | + // It's a local cli request | |
| 1000 | + return $defalt; | |
| 1001 | + } | |
| 1002 | + | |
| 1003 | + $ipAddress = self::resolveClientIp($_SERVER); | |
| 1004 | + | |
| 1005 | + $ipAddress = apply_filters('fluent_booking/user_ip', $ipAddress, []); | |
| 1006 | + | |
| 1007 | + $ipAddress = sanitize_text_field(wp_unslash($ipAddress)); | |
| 1008 | + | |
| 1009 | + return $ipAddress; | |
| 1010 | + } | |
| 1011 | + | |
| 1012 | + /** | |
| 1013 | + * Pick the client address out of a request's server vars. | |
| 1014 | + * | |
| 1015 | + * Kept apart from getIp(), which caches its answer for the request, so the | |
| 1016 | + * header-trust rules can be exercised one request shape at a time. | |
| 1017 | + * | |
| 1018 | + * @param array $serverData $_SERVER or an equivalent | |
| 1019 | + * @return string | |
| 1020 | + */ | |
| 1021 | + public static function resolveClientIp($serverData) | |
| 1022 | + { | |
| 1023 | + $remoteAddr = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', (string)Arr::get($serverData, 'REMOTE_ADDR')); | |
| 1024 | + $cloudflareIp = (string)Arr::get($serverData, 'HTTP_CF_CONNECTING_IP'); | |
| 1025 | + $trustedProxies = (array)apply_filters('fluent_booking/trusted_proxies', ['127.0.0.1']); | |
| 1026 | + | |
| 1027 | + // A proxy appends the peer it saw, so only the right-most hop is reliable; | |
| 1028 | + // anything to its left is whatever the client chose to send | |
| 1029 | + $hops = explode(',', (string)Arr::get($serverData, 'HTTP_X_FORWARDED_FOR')); | |
| 1030 | + $forwardedIp = rest_is_ip_address(trim(end($hops))); | |
| 1031 | + | |
| 1032 | + if ($cloudflareIp && self::isCfIp($remoteAddr)) { | |
| 1033 | + return $cloudflareIp; | |
| 1034 | + } | |
| 1035 | + | |
| 1036 | + if ($forwardedIp && in_array($remoteAddr, $trustedProxies, true)) { | |
| 1037 | + return $forwardedIp; | |
| 1038 | + } | |
| 1039 | + | |
| 1040 | + return $remoteAddr; | |
| 1041 | + } | |
| 1042 | + | |
| 1043 | + /** | |
| 1044 | + * Valid E.164 number: 7-15 significant digits. | |
| 1045 | + * | |
| 1046 | + * @param string $phone | |
| 1047 | + * @return bool | |
| 1048 | + */ | |
| 1049 | + public static function isValidPhoneNumber($phone) | |
| 1050 | + { | |
| 1051 | + if (!apply_filters('fluent_booking/enforce_phone_validation', true)) { | |
| 1052 | + return true; | |
| 1053 | + } | |
| 1054 | + | |
| 1055 | + $phone = trim((string)$phone); | |
| 1056 | + | |
| 1057 | + if ($phone === '') { | |
| 1058 | + return false; | |
| 1059 | + } | |
| 1060 | + | |
| 1061 | + if (!preg_match('/^\+?[0-9\s().\-]+$/', $phone)) { | |
| 1062 | + return false; | |
| 1063 | + } | |
| 1064 | + | |
| 1065 | + $digitCount = strlen(preg_replace('/\D/', '', $phone)); | |
| 1066 | + | |
| 1067 | + $min = (int)apply_filters('fluent_booking/phone_min_digits', 7); | |
| 1068 | + $max = (int)apply_filters('fluent_booking/phone_max_digits', 15); | |
| 1069 | + | |
| 1070 | + return $digitCount >= $min && $digitCount <= $max; | |
| 1071 | + } | |
| 1072 | + | |
| 1073 | + private static function isCfIp($ip = '') | |
| 1074 | + { | |
| 1075 | + if (!$ip) { | |
| 1076 | + $serverData = $_SERVER; | |
| 1077 | + $REMOTE_ADDR = Arr::get($serverData, 'REMOTE_ADDR'); | |
| 1078 | + $ip = $REMOTE_ADDR; | |
| 1079 | + } | |
| 1080 | + $cloudflareIPRanges = array( | |
| 1081 | + '173.245.48.0/20', | |
| 1082 | + '103.21.244.0/22', | |
| 1083 | + '103.22.200.0/22', | |
| 1084 | + '103.31.4.0/22', | |
| 1085 | + '141.101.64.0/18', | |
| 1086 | + '108.162.192.0/18', | |
| 1087 | + '190.93.240.0/20', | |
| 1088 | + '188.114.96.0/20', | |
| 1089 | + '197.234.240.0/22', | |
| 1090 | + '198.41.128.0/17', | |
| 1091 | + '162.158.0.0/15', | |
| 1092 | + '104.16.0.0/13', | |
| 1093 | + '104.24.0.0/14', | |
| 1094 | + '172.64.0.0/13', | |
| 1095 | + '131.0.72.0/22', | |
| 1096 | + ); | |
| 1097 | + //Make sure that the request came via Cloudflare. | |
| 1098 | + foreach ($cloudflareIPRanges as $range) { | |
| 1099 | + //Use the ip_in_range function from Joomla. | |
| 1100 | + if (self::ipInRange($ip, $range)) { | |
| 1101 | + //IP is valid. Belongs to Cloudflare. | |
| 1102 | + return true; | |
| 1103 | + } | |
| 1104 | + } | |
| 1105 | + | |
| 1106 | + return false; | |
| 1107 | + } | |
| 1108 | + | |
| 1109 | + private static function ipInRange($ip, $range) | |
| 1110 | + { | |
| 1111 | + if (strpos($range, '/') !== false) { | |
| 1112 | + // $range is in IP/NETMASK format | |
| 1113 | + list($range, $netmask) = explode('/', $range, 2); | |
| 1114 | + if (strpos($netmask, '.') !== false) { | |
| 1115 | + // $netmask is a 255.255.0.0 format | |
| 1116 | + $netmask = str_replace('*', '0', $netmask); | |
| 1117 | + $netmask_dec = ip2long($netmask); | |
| 1118 | + return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec)); | |
| 1119 | + } else { | |
| 1120 | + // $netmask is a CIDR size block | |
| 1121 | + // fix the range argument | |
| 1122 | + $x = explode('.', $range); | |
| 1123 | + while (count($x) < 4) $x[] = '0'; | |
| 1124 | + list($a, $b, $c, $d) = $x; | |
| 1125 | + $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d); | |
| 1126 | + $range_dec = ip2long($range); | |
| 1127 | + $ip_dec = ip2long($ip); | |
| 1128 | + | |
| 1129 | + # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s | |
| 1130 | + #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0')); | |
| 1131 | + | |
| 1132 | + # Strategy 2 - Use math to create it | |
| 1133 | + $wildcard_dec = pow(2, (32 - $netmask)) - 1; | |
| 1134 | + $netmask_dec = ~$wildcard_dec; | |
| 1135 | + | |
| 1136 | + return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec)); | |
| 1137 | + } | |
| 958 | 1138 | } else { |
| 959 | - $ip = $clientIp; | |
| 1139 | + // range might be 255.255.*.* or 1.2.3.0-1.2.3.255 | |
| 1140 | + if (strpos($range, '*') !== false) { // a.b.*.* format | |
| 1141 | + // Just convert to A-B format by setting * to 0 for A and 255 for B | |
| 1142 | + $lower = str_replace('*', '0', $range); | |
| 1143 | + $upper = str_replace('*', '255', $range); | |
| 1144 | + $range = "$lower-$upper"; | |
| 1145 | + } | |
| 1146 | + | |
| 1147 | + if (strpos($range, '-') !== false) { // A-B format | |
| 1148 | + list($lower, $upper) = explode('-', $range, 2); | |
| 1149 | + $lower_dec = (float)sprintf("%u", ip2long($lower)); | |
| 1150 | + $upper_dec = (float)sprintf("%u", ip2long($upper)); | |
| 1151 | + $ip_dec = (float)sprintf("%u", ip2long($ip)); | |
| 1152 | + return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec)); | |
| 1153 | + } | |
| 1154 | + return false; | |
| 960 | 1155 | } |
| 961 | - | |
| 962 | - return sanitize_text_field($ip); | |
| 963 | 1156 | } |
| 964 | 1157 | |
| 965 | 1158 | public static function fcal_sanitize_html($html) |
| 966 | 1159 | { |
| @@ -981,9 +1174,8 @@ | ||
| 981 | 1174 | $tags['iframe'] = [ |
| 982 | 1175 | 'width' => [], |
| 983 | 1176 | 'height' => [], |
| 984 | 1177 | 'src' => [], |
| 985 | - 'srcdoc' => [], | |
| 986 | 1178 | 'title' => [], |
| 987 | 1179 | 'frameborder' => [], |
| 988 | 1180 | 'allow' => [], |
| 989 | 1181 | 'class' => [], |
| @@ -991,9 +1183,11 @@ | ||
| 991 | 1183 | 'allowfullscreen' => [], |
| 992 | 1184 | 'style' => [], |
| 993 | 1185 | ]; |
| 994 | 1186 | //button |
| 995 | - $tags['button']['onclick'] = []; | |
| 1187 | + $tags['button'] = [ | |
| 1188 | + 'onclick' => [] | |
| 1189 | + ]; | |
| 996 | 1190 | |
| 997 | 1191 | //svg |
| 998 | 1192 | if (empty($tags['svg'])) { |
| 999 | 1193 | $svg_args = [ |
| @@ -1212,8 +1406,33 @@ | ||
| 1212 | 1406 | |
| 1213 | 1407 | return $durationLookup; |
| 1214 | 1408 | } |
| 1215 | 1409 | |
| 1410 | + public static function formatDuration($totalMinutes) | |
| 1411 | + { | |
| 1412 | + $days = floor($totalMinutes / 1440); // 1440 minutes in a day | |
| 1413 | + $hours = floor(($totalMinutes % 1440) / 60); | |
| 1414 | + $minutes = $totalMinutes % 60; | |
| 1415 | + | |
| 1416 | + $formattedDuration = []; | |
| 1417 | + if ($days > 0) { | |
| 1418 | + $unit = $days > 1 ? __('Days', 'fluent-booking') : __('Day', 'fluent-booking'); | |
| 1419 | + $formattedDuration[] = $days . ' ' . $unit; | |
| 1420 | + } | |
| 1421 | + | |
| 1422 | + if ($hours > 0) { | |
| 1423 | + $unit = $hours > 1 ? __('Hours', 'fluent-booking') : __('Hour', 'fluent-booking'); | |
| 1424 | + $formattedDuration[] = $hours . ' ' . $unit; | |
| 1425 | + } | |
| 1426 | + | |
| 1427 | + if ($minutes > 0 || empty($formattedDuration)) { | |
| 1428 | + $unit = $minutes > 1 ? __('Minutes', 'fluent-booking') : __('Minute', 'fluent-booking'); | |
| 1429 | + $formattedDuration[] = $minutes . ' ' . $unit; | |
| 1430 | + } | |
| 1431 | + | |
| 1432 | + return implode(' ', $formattedDuration); | |
| 1433 | + } | |
| 1434 | + | |
| 1216 | 1435 | public static function getBufferTimes() |
| 1217 | 1436 | { |
| 1218 | 1437 | return apply_filters('fluent_booking/buffer_times_schema', [ |
| 1219 | 1438 | [ |
| @@ -1489,12 +1708,31 @@ | ||
| 1489 | 1708 | ], |
| 1490 | 1709 | [ |
| 1491 | 1710 | 'value' => 'date', |
| 1492 | 1711 | 'label' => __('Date', 'fluent-booking') |
| 1712 | + ], | |
| 1713 | + [ | |
| 1714 | + 'value' => 'file', | |
| 1715 | + 'label' => __('File', 'fluent-booking') | |
| 1716 | + ], | |
| 1717 | + [ | |
| 1718 | + 'value' => 'hidden', | |
| 1719 | + 'label' => __('Hidden', 'fluent-booking') | |
| 1720 | + ], | |
| 1721 | + [ | |
| 1722 | + 'value' => 'terms-and-conditions', | |
| 1723 | + 'label' => __('Terms & Conditions', 'fluent-booking') | |
| 1493 | 1724 | ] |
| 1494 | 1725 | ]); |
| 1495 | 1726 | } |
| 1496 | 1727 | |
| 1728 | + public static function getDefaultTermsAndConditions() | |
| 1729 | + { | |
| 1730 | + $termsAndConditions = __('I have read and agree to the <a href="#" target="_blank" rel="noopener">Terms and Conditions</a> and <a href="#" target="_blank" rel="noopener">Privacy Policy</a>.', 'fluent-booking'); | |
| 1731 | + | |
| 1732 | + return apply_filters('fluent_booking/default_terms_and_conditions', $termsAndConditions); | |
| 1733 | + } | |
| 1734 | + | |
| 1497 | 1735 | public static function getDefaultEmailNotificationSettings() |
| 1498 | 1736 | { |
| 1499 | 1737 | $assetUrl = App::getInstance()['url.assets']; |
| 1500 | 1738 | |
| @@ -1507,9 +1745,9 @@ | ||
| 1507 | 1745 | 'enabled' => true, |
| 1508 | 1746 | 'title' => __('Booking Confirmation Email to Attendee', 'fluent-booking'), |
| 1509 | 1747 | 'email' => [ |
| 1510 | 1748 | 'subject' => 'Booking Confirmation between {{host.name}} & {{guest.full_name}}', |
| 1511 | - '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) | |
| 1512 | 1750 | ], |
| 1513 | 1751 | ], |
| 1514 | 1752 | 'booking_conf_host' => [ |
| 1515 | 1753 | 'enabled' => true, |
| @@ -1517,9 +1755,9 @@ | ||
| 1517 | 1755 | 'title' => __('Booking Confirmation Email to Organizer (You)', 'fluent-booking'), |
| 1518 | 1756 | 'email' => [ |
| 1519 | 1757 | 'additional_recipients' => '', |
| 1520 | 1758 | 'subject' => 'New Booking: {{guest.full_name}} @ {{booking.start_date_time_for_host}}', |
| 1521 | - '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>' | |
| 1522 | 1760 | ], |
| 1523 | 1761 | ], |
| 1524 | 1762 | 'reminder_to_attendee' => [ |
| 1525 | 1763 | 'enabled' => false, |
| @@ -1583,9 +1821,9 @@ | ||
| 1583 | 1821 | 'enabled' => true, |
| 1584 | 1822 | 'title' => __('Booking Rescheduled by Organizer (email to Attendee)', 'fluent-booking'), |
| 1585 | 1823 | 'email' => [ |
| 1586 | 1824 | 'subject' => 'Your booking was rescheduled with {{host.name}}', |
| 1587 | - '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) | |
| 1588 | 1826 | ], |
| 1589 | 1827 | ], |
| 1590 | 1828 | 'booking_request_host' => [ |
| 1591 | 1829 | 'enabled' => true, |
| @@ -1593,9 +1831,9 @@ | ||
| 1593 | 1831 | 'title' => __('Booking Approval Request to Host (email to Organizer)', 'fluent-booking'), |
| 1594 | 1832 | 'email' => [ |
| 1595 | 1833 | 'additional_recipients' => '', |
| 1596 | 1834 | 'subject' => 'Awaiting Approval: {{guest.full_name}} @ {{booking.start_date_time_for_host}}', |
| 1597 | - '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>' | |
| 1598 | 1836 | ], |
| 1599 | 1837 | ], |
| 1600 | 1838 | 'booking_request_attendee' => [ |
| 1601 | 1839 | 'enabled' => true, |
| @@ -1601,9 +1839,9 @@ | ||
| 1601 | 1839 | 'enabled' => true, |
| 1602 | 1840 | 'title' => __('Booking Submission Confirmation (email to Attendee)', 'fluent-booking'), |
| 1603 | 1841 | 'email' => [ |
| 1604 | 1842 | 'subject' => 'Booking Submitted: Meeting between {{host.name}} & {{guest.full_name}}', |
| 1605 | - '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>' | |
| 1606 | 1844 | ], |
| 1607 | 1845 | ], |
| 1608 | 1846 | 'declined_by_host' => [ |
| 1609 | 1847 | 'enabled' => true, |
| @@ -1633,10 +1871,17 @@ | ||
| 1633 | 1871 | |
| 1634 | 1872 | return apply_filters('fluent_booking/confirm_and_reject_button_html', $html); |
| 1635 | 1873 | } |
| 1636 | 1874 | |
| 1637 | - public static function getEditorShortCodes($calendarEvent = null, $isHtmlSupported = false) | |
| 1875 | + public static function getIframeHtml() | |
| 1638 | 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 | + { | |
| 1639 | 1884 | if (!$isHtmlSupported) { |
| 1640 | 1885 | $groups = [ |
| 1641 | 1886 | 'guest' => [ |
| 1642 | 1887 | 'title' => __('Attendee Data', 'fluent-booking'), |
| @@ -1641,15 +1886,16 @@ | ||
| 1641 | 1886 | 'guest' => [ |
| 1642 | 1887 | 'title' => __('Attendee Data', 'fluent-booking'), |
| 1643 | 1888 | 'key' => 'guest', |
| 1644 | 1889 | 'shortcodes' => [ |
| 1645 | - '{{guest.first_name}}' => __('Guest First Name', 'fluent-booking'), | |
| 1646 | - '{{guest.last_name}}' => __('Guest Last Name', 'fluent-booking'), | |
| 1647 | - '{{guest.full_name}}' => __('Guest Full Name', 'fluent-booking'), | |
| 1648 | - '{{guest.email}}' => __('Guest Email', 'fluent-booking'), | |
| 1649 | - '{{guest.note}}' => __('Guest Note', 'fluent-booking'), | |
| 1650 | - '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'), | |
| 1651 | - '{{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') | |
| 1652 | 1898 | ] |
| 1653 | 1899 | ], |
| 1654 | 1900 | 'booking' => [ |
| 1655 | 1901 | 'title' => __('Booking Data', 'fluent-booking'), |
| @@ -1654,27 +1900,41 @@ | ||
| 1654 | 1900 | 'booking' => [ |
| 1655 | 1901 | 'title' => __('Booking Data', 'fluent-booking'), |
| 1656 | 1902 | 'key' => 'booking', |
| 1657 | 1903 | 'shortcodes' => [ |
| 1658 | - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'), | |
| 1659 | - '{{booking.description}}' => __('Event Description', 'fluent-booking'), | |
| 1660 | - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'), | |
| 1661 | - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'), | |
| 1662 | - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'), | |
| 1663 | - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'), | |
| 1664 | - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'), | |
| 1665 | - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'), | |
| 1666 | - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'), | |
| 1667 | - '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'), | |
| 1668 | - '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'), | |
| 1669 | - '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'), | |
| 1670 | - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'), | |
| 1671 | - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'), | |
| 1672 | - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'), | |
| 1673 | - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'), | |
| 1674 | - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'), | |
| 1675 | - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'), | |
| 1676 | - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking') | |
| 1904 | + '{{booking.event_name}}' => __('Event Name', 'fluent-booking'), | |
| 1905 | + '{{booking.description}}' => __('Event Description', 'fluent-booking'), | |
| 1906 | + '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'), | |
| 1907 | + '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'), | |
| 1908 | + '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'), | |
| 1909 | + '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'), | |
| 1910 | + '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'), | |
| 1911 | + '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'), | |
| 1912 | + '{{booking.all_bookings_short_times_guest_timezone}}' => __('All Bookings Short Times (with guest timezone)', 'fluent-booking'), | |
| 1913 | + '{{booking.all_bookings_short_times_host_timezone}}' => __('All Bookings Short Times (with host timezone)', 'fluent-booking'), | |
| 1914 | + '{{booking.all_bookings_full_times_guest_timezone}}' => __('All Bookings Full Times (with guest timezone)', 'fluent-booking'), | |
| 1915 | + '{{booking.all_bookings_full_times_host_timezone}}' => __('All Bookings Full Times (with host timezone)', 'fluent-booking'), | |
| 1916 | + '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'), | |
| 1917 | + '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with attendee timezone)', 'fluent-booking'), | |
| 1918 | + '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'), | |
| 1919 | + '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'), | |
| 1920 | + '{{booking.start_date_time_for_host.format.Y-m-d}}' => __('Event Date Time (with host timezone) (Ex: 2024-05-20)', 'fluent-booking'), | |
| 1921 | + '{{booking.location_details_text}}' => __('Event Location Details', 'fluent-booking'), | |
| 1922 | + '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'), | |
| 1923 | + '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'), | |
| 1924 | + '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'), | |
| 1925 | + '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'), | |
| 1926 | + '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'), | |
| 1927 | + '{{booking.source_url}}' => __('Source URL', 'fluent-booking'), | |
| 1928 | + '{{booking.utm_source}}' => __('UTM Source', 'fluent-booking'), | |
| 1929 | + '{{booking.utm_medium}}' => __('UTM Medium', 'fluent-booking'), | |
| 1930 | + '{{booking.utm_campaign}}' => __('UTM Campaign', 'fluent-booking'), | |
| 1931 | + '{{booking.utm_term}}' => __('UTM Term', 'fluent-booking'), | |
| 1932 | + '{{booking.utm_content}}' => __('UTM Content', 'fluent-booking'), | |
| 1933 | + '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'), | |
| 1934 | + '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking'), | |
| 1935 | + '{{booking.previous_meeting_date_time_host_timezone}}' => __('Previous Meeting Date & Time (with host timezone)', 'fluent-booking'), | |
| 1936 | + '{{booking.previous_meeting_date_time_guest_timezone}}' => __('Previous Meeting Date & Time (with guest timezone)', 'fluent-booking'), | |
| 1677 | 1937 | ] |
| 1678 | 1938 | ], |
| 1679 | 1939 | 'host' => [ |
| 1680 | 1940 | 'title' => __('Host Data', 'fluent-booking'), |
| @@ -1710,8 +1970,9 @@ | ||
| 1710 | 1970 | '{{guest.email}}' => __('Guest Email', 'fluent-booking'), |
| 1711 | 1971 | '{{booking.phone}}' => __('Guest Main Phone Number (if provided)', 'fluent-booking'), |
| 1712 | 1972 | '{{guest.note}}' => __('Guest Note', 'fluent-booking'), |
| 1713 | 1973 | '{{guest.timezone}}' => __('Guest Timezone', 'fluent-booking'), |
| 1974 | + '{{guest.total_guest}}' => __('Total Guest Count', 'fluent-booking'), | |
| 1714 | 1975 | '{{guest.form_data_html}}' => __('Guest Form Submitted Data (HTML)', 'fluent-booking') |
| 1715 | 1976 | ] |
| 1716 | 1977 | ], |
| 1717 | 1978 | 'booking' => [ |
| @@ -1717,27 +1978,41 @@ | ||
| 1717 | 1978 | 'booking' => [ |
| 1718 | 1979 | 'title' => __('Booking Data', 'fluent-booking'), |
| 1719 | 1980 | 'key' => 'booking', |
| 1720 | 1981 | 'shortcodes' => [ |
| 1721 | - '{{booking.event_name}}' => __('Event Name', 'fluent-booking'), | |
| 1722 | - '{{booking.description}}' => __('Event Description', 'fluent-booking'), | |
| 1723 | - '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'), | |
| 1724 | - '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'), | |
| 1725 | - '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'), | |
| 1726 | - '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'), | |
| 1727 | - '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'), | |
| 1728 | - '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'), | |
| 1729 | - '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'), | |
| 1730 | - '{{booking.start_date_time_for_attendee}}' => __('Event Date time (with guest timezone)', 'fluent-booking'), | |
| 1731 | - '{{booking.start_date_time_for_host}}' => __('Event Date time (with host timezone)', 'fluent-booking'), | |
| 1732 | - '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'), | |
| 1733 | - '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'), | |
| 1734 | - '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'), | |
| 1735 | - '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'), | |
| 1736 | - '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'), | |
| 1737 | - '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'), | |
| 1738 | - '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'), | |
| 1739 | - '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking') | |
| 1982 | + '{{booking.event_name}}' => __('Event Name', 'fluent-booking'), | |
| 1983 | + '{{booking.description}}' => __('Event Description', 'fluent-booking'), | |
| 1984 | + '{{booking.booking_title}}' => __('Booking Title', 'fluent-booking'), | |
| 1985 | + '{{booking.additional_guests}}' => __('Additional Guests', 'fluent-booking'), | |
| 1986 | + '{{booking.full_start_end_guest_timezone}}' => __('Full Start Date Time (with guest timezone)', 'fluent-booking'), | |
| 1987 | + '{{booking.full_start_end_host_timezone}}' => __('Full Start Date Time (with host timezone)', 'fluent-booking'), | |
| 1988 | + '{{booking.full_start_and_end_guest_timezone}}' => __('Full Start & End Date Time (with guest timezone)', 'fluent-booking'), | |
| 1989 | + '{{booking.full_start_and_end_host_timezone}}' => __('Full Start & End Date Time (with host timezone)', 'fluent-booking'), | |
| 1990 | + '{{booking.all_bookings_short_times_guest_timezone}}' => __('All Bookings Short Times (with guest timezone)', 'fluent-booking'), | |
| 1991 | + '{{booking.all_bookings_short_times_host_timezone}}' => __('All Bookings Short Times (with host timezone)', 'fluent-booking'), | |
| 1992 | + '{{booking.all_bookings_full_times_guest_timezone}}' => __('All Bookings Full Times (with guest timezone)', 'fluent-booking'), | |
| 1993 | + '{{booking.all_bookings_full_times_host_timezone}}' => __('All Bookings Full Times (with host timezone)', 'fluent-booking'), | |
| 1994 | + '{{booking.start_date_time}}' => __('Event Date Time (UTC)', 'fluent-booking'), | |
| 1995 | + '{{booking.start_date_time_for_attendee}}' => __('Event Date Time (with guest timezone)', 'fluent-booking'), | |
| 1996 | + '{{booking.start_date_time_for_host}}' => __('Event Date Time (with host timezone)', 'fluent-booking'), | |
| 1997 | + '{{booking.start_date_time_for_attendee.format.Y-m-d}}' => __('Event Date Time (with attendee timezone) (Ex: 2024-05-20)', 'fluent-booking'), | |
| 1998 | + '{{booking.start_date_time_for_host.format.Y-m-d}}' => __('Event Date Time (with host timezone) (Ex: 2024-05-20)', 'fluent-booking'), | |
| 1999 | + '{{booking.location_details_html}}' => __('Event Location Details (HTML)', 'fluent-booking'), | |
| 2000 | + '{{booking.cancel_reason}}' => __('Event Cancel Reason', 'fluent-booking'), | |
| 2001 | + '{{booking.start_time_human_format}}' => __('Event Start Time (ex: 2 hours from now)', 'fluent-booking'), | |
| 2002 | + '##booking.cancelation_url##' => __('Booking Cancellation URL', 'fluent-booking'), | |
| 2003 | + '##booking.reschedule_url##' => __('Booking Reschedule URL', 'fluent-booking'), | |
| 2004 | + '##booking.admin_booking_url##' => __('Booking Details Admin URL', 'fluent-booking'), | |
| 2005 | + '{{booking.source_url}}' => __('Source URL', 'fluent-booking'), | |
| 2006 | + '{{booking.utm_source}}' => __('UTM Source', 'fluent-booking'), | |
| 2007 | + '{{booking.utm_medium}}' => __('UTM Medium', 'fluent-booking'), | |
| 2008 | + '{{booking.utm_campaign}}' => __('UTM Campaign', 'fluent-booking'), | |
| 2009 | + '{{booking.utm_term}}' => __('UTM Term', 'fluent-booking'), | |
| 2010 | + '{{booking.utm_content}}' => __('UTM Content', 'fluent-booking'), | |
| 2011 | + '{{booking.booking_hash}}' => __('Unique Booking Hash', 'fluent-booking'), | |
| 2012 | + '{{booking.reschedule_reason}}' => __('Event Reschedule Reason', 'fluent-booking'), | |
| 2013 | + '{{booking.previous_meeting_date_time_host_timezone}}' => __('Previous Meeting Date & Time (with host timezone)', 'fluent-booking'), | |
| 2014 | + '{{booking.previous_meeting_date_time_guest_timezone}}' => __('Previous Meeting Date & Time (with guest timezone)', 'fluent-booking'), | |
| 1740 | 2015 | ] |
| 1741 | 2016 | ], |
| 1742 | 2017 | 'host' => [ |
| 1743 | 2018 | 'title' => __('Host Data', 'fluent-booking'), |
| @@ -1843,11 +2118,17 @@ | ||
| 1843 | 2118 | return $raw_value; |
| 1844 | 2119 | } |
| 1845 | 2120 | |
| 1846 | 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 | + } | |
| 1847 | 2125 | |
| 1848 | 2126 | $method = 'aes-256-ctr'; |
| 1849 | 2127 | $ivlen = openssl_cipher_iv_length($method); |
| 2128 | + if ($ivlen === false || strlen($raw_value) <= $ivlen) { | |
| 2129 | + return false; | |
| 2130 | + } | |
| 1850 | 2131 | $iv = substr($raw_value, 0, $ivlen); |
| 1851 | 2132 | |
| 1852 | 2133 | $raw_value = substr($raw_value, $ivlen); |
| 1853 | 2134 | |
| @@ -1859,9 +2140,9 @@ | ||
| 1859 | 2140 | |
| 1860 | 2141 | $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure'; |
| 1861 | 2142 | |
| 1862 | 2143 | $value = openssl_decrypt($raw_value, $method, $key, 0, $iv); |
| 1863 | - if (!$value || substr($value, -strlen($salt)) !== $salt) { | |
| 2144 | + if (!is_string($value) || substr($value, -strlen($salt)) !== $salt) { | |
| 1864 | 2145 | return false; |
| 1865 | 2146 | } |
| 1866 | 2147 | |
| 1867 | 2148 | return substr($value, 0, -strlen($salt)); |
| @@ -1869,9 +2150,9 @@ | ||
| 1869 | 2150 | |
| 1870 | 2151 | public static function debugLog($data) |
| 1871 | 2152 | { |
| 1872 | 2153 | if (defined('FLUENT_BOOKING_DEBUG') && FLUENT_BOOKING_DEBUG) { |
| 1873 | - 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 | |
| 1874 | 2155 | } |
| 1875 | 2156 | } |
| 1876 | 2157 | |
| 1877 | 2158 | public static function getGlobalSettings($settingsKey = null) |
| @@ -1898,9 +2179,9 @@ | ||
| 1898 | 2179 | 'notification_day' => 'mon', |
| 1899 | 2180 | 'start_day' => 'sun', |
| 1900 | 2181 | 'auto_cancel_timing' => '10', |
| 1901 | 2182 | 'auto_complete_timing' => '60', |
| 1902 | - 'default_phone_country' => '' | |
| 2183 | + 'default_country' => '' | |
| 1903 | 2184 | ], |
| 1904 | 2185 | 'time_format' => '12', |
| 1905 | 2186 | 'theme' => 'system-default' |
| 1906 | 2187 | ]; |
| @@ -1910,14 +2191,10 @@ | ||
| 1910 | 2191 | if (empty($settings)) { |
| 1911 | 2192 | $settings = []; |
| 1912 | 2193 | } |
| 1913 | 2194 | |
| 1914 | - $paymentSettings = get_option('fluent_booking_global_payment_settings', []); | |
| 2195 | + $settings['payments'] = CurrenciesHelper::getGlobalCurrencySettings(); | |
| 1915 | 2196 | |
| 1916 | - if ($paymentSettings) { | |
| 1917 | - $settings['payments'] = $paymentSettings; | |
| 1918 | - } | |
| 1919 | - | |
| 1920 | 2197 | $settings = wp_parse_args($settings, $defaults); |
| 1921 | 2198 | |
| 1922 | 2199 | $emailSettings = $settings['emailing']; |
| 1923 | 2200 | |
| @@ -1987,8 +2264,28 @@ | ||
| 1987 | 2264 | $format = Arr::get($settings, 'time_format', '24'); |
| 1988 | 2265 | return $format; |
| 1989 | 2266 | } |
| 1990 | 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 | + | |
| 1991 | 2288 | public static function getVerifiedSenders() |
| 1992 | 2289 | { |
| 1993 | 2290 | $verifiedSenders = []; |
| 1994 | 2291 | if (defined('FLUENTMAIL')) { |
| @@ -2064,9 +2361,9 @@ | ||
| 2064 | 2361 | } |
| 2065 | 2362 | return apply_filters('fluent_booking/author_photo', get_avatar_url($id_or_email), $args); |
| 2066 | 2363 | } |
| 2067 | 2364 | |
| 2068 | - public static function getPrefSettins($cached = true) | |
| 2365 | + public static function getPrefSettings($cached = true) | |
| 2069 | 2366 | { |
| 2070 | 2367 | static $pref = null; |
| 2071 | 2368 | |
| 2072 | 2369 | if ($cached && $pref) { |
| @@ -2078,8 +2375,11 @@ | ||
| 2078 | 2375 | 'enabled' => 'no', |
| 2079 | 2376 | 'slug' => 'my-bookings', |
| 2080 | 2377 | 'render_type' => 'standalone', |
| 2081 | 2378 | 'page_id' => '' |
| 2379 | + ], | |
| 2380 | + 'coupon' => [ | |
| 2381 | + 'enabled' => 'no' | |
| 2082 | 2382 | ] |
| 2083 | 2383 | ]; |
| 2084 | 2384 | |
| 2085 | 2385 | $storedSettings = get_option('fluent_booking_modules', []); |
| @@ -2092,6 +2392,128 @@ | ||
| 2092 | 2392 | |
| 2093 | 2393 | $pref = $settings; |
| 2094 | 2394 | |
| 2095 | 2395 | return $settings; |
| 2396 | + } | |
| 2397 | + | |
| 2398 | + public static function getPrefSettins($cached = true) | |
| 2399 | + { | |
| 2400 | + return self::getPrefSettings($cached); | |
| 2401 | + } | |
| 2402 | + | |
| 2403 | + public static function getFeatures() | |
| 2404 | + { | |
| 2405 | + return apply_filters('fluent_booking/get_features', [ | |
| 2406 | + 'has_fluentcrm' => defined('FLUENTCRM'), | |
| 2407 | + 'has_fluentsmtp' => defined('FLUENTMAIL'), | |
| 2408 | + 'has_fluentform' => defined('FLUENTFORM'), | |
| 2409 | + 'has_fluentboards' => defined('FLUENT_BOARDS'), | |
| 2410 | + 'has_fluentcart' => defined('FLUENTCART_VERSION') | |
| 2411 | + ]); | |
| 2412 | + } | |
| 2413 | + | |
| 2414 | + public static function getActiveThemeName() | |
| 2415 | + { | |
| 2416 | + $ins = get_option('_fb_ins_by'); | |
| 2417 | + | |
| 2418 | + if ($ins) { | |
| 2419 | + return sanitize_text_field($ins); | |
| 2420 | + } | |
| 2421 | + | |
| 2422 | + return get_option('template'); | |
| 2423 | + } | |
| 2424 | + | |
| 2425 | + /** | |
| 2426 | + * Hold a round robin slot for the rest of the request, so concurrent public | |
| 2427 | + * bookings cannot pick the same least-loaded host. Locks every host, since | |
| 2428 | + * the host is only chosen inside isSpotAvailable() and another event can | |
| 2429 | + * share it. Same keys MCP locks with. | |
| 2430 | + * Released at shutdown because wp_send_json() exits past any finally. | |
| 2431 | + * | |
| 2432 | + * @param CalendarSlot $event | |
| 2433 | + * @param string $startTimeUtc | |
| 2434 | + * @param string $endTimeUtc | |
| 2435 | + * | |
| 2436 | + * @return bool false when another request holds the slot | |
| 2437 | + */ | |
| 2438 | + public static function lockRoundRobinSlot($event, $startTimeUtc, $endTimeUtc) | |
| 2439 | + { | |
| 2440 | + if (!$event->isRoundRobin()) { | |
| 2441 | + return true; | |
| 2442 | + } | |
| 2443 | + | |
| 2444 | + $locks = SlotLock::acquireInterval($event->id, $startTimeUtc, $endTimeUtc, $event->getHostIds()); | |
| 2445 | + | |
| 2446 | + if ($locks) { | |
| 2447 | + register_shutdown_function([SlotLock::class, 'releaseAll'], $locks); | |
| 2448 | + } | |
| 2449 | + | |
| 2450 | + return (bool) $locks; | |
| 2451 | + } | |
| 2452 | + | |
| 2453 | + /** | |
| 2454 | + * Per-IP fixed-window rate limiter for public AJAX/REST endpoints. | |
| 2455 | + * | |
| 2456 | + * @param string $action Action name (e.g. apply_coupon, schedule_meeting). | |
| 2457 | + * @param int $limit Max requests per window. | |
| 2458 | + * @param int $window Window in seconds. | |
| 2459 | + * @param bool $perIp False for one shared bucket that a spoofed IP cannot reset. | |
| 2460 | + * @return bool True if under the limit (and the count was incremented), | |
| 2461 | + * false if over. | |
| 2462 | + */ | |
| 2463 | + public static function checkRateLimit($action, $limit, $window = 60, $perIp = true) | |
| 2464 | + { | |
| 2465 | + $args = apply_filters('fluent_booking/public_ajax_ratelimit', [ | |
| 2466 | + 'limit' => $limit, | |
| 2467 | + 'window' => $window, | |
| 2468 | + ], $action); | |
| 2469 | + | |
| 2470 | + $limit = max(1, (int) (isset($args['limit']) ? $args['limit'] : $limit)); | |
| 2471 | + $window = max(1, (int) (isset($args['window']) ? $args['window'] : $window)); | |
| 2472 | + | |
| 2473 | + $key = 'fcal_ratelimit_' . $action . ($perIp ? '_' . md5(self::getIp()) : ''); | |
| 2474 | + $count = (int) get_transient($key); | |
| 2475 | + | |
| 2476 | + if ($count >= $limit) { | |
| 2477 | + return false; | |
| 2478 | + } | |
| 2479 | + | |
| 2480 | + set_transient($key, $count + 1, $window); | |
| 2481 | + | |
| 2482 | + return true; | |
| 2483 | + } | |
| 2484 | + | |
| 2485 | + /** | |
| 2486 | + * Run a callback inside a database transaction, re-throwing on failure so | |
| 2487 | + * the caller decides how to report it. | |
| 2488 | + * | |
| 2489 | + * \Throwable, not \Exception: a TypeError is an Error, and an | |
| 2490 | + * Exception-only catch would leave the transaction open. | |
| 2491 | + * | |
| 2492 | + * Database writes only — a hook fired in here would hold the callback's | |
| 2493 | + * rows locked for the length of a listener's outbound request. | |
| 2494 | + * | |
| 2495 | + * @param callable $callback | |
| 2496 | + * | |
| 2497 | + * @return mixed | |
| 2498 | + * | |
| 2499 | + * @throws \Throwable after the rollback | |
| 2500 | + */ | |
| 2501 | + public static function dbTransaction($callback) | |
| 2502 | + { | |
| 2503 | + $db = App::getInstance('db'); | |
| 2504 | + | |
| 2505 | + $db->beginTransaction(); | |
| 2506 | + | |
| 2507 | + try { | |
| 2508 | + $result = $callback(); | |
| 2509 | + | |
| 2510 | + $db->commit(); | |
| 2511 | + | |
| 2512 | + return $result; | |
| 2513 | + } catch (\Throwable $e) { | |
| 2514 | + $db->rollBack(); | |
| 2515 | + | |
| 2516 | + throw $e; | |
| 2517 | + } | |
| 2096 | 2518 | } |
| 2097 | 2519 | } |