toArray(); if ($type) { $address['type'] = $type; } } } if (empty($address)) { return; } $keysToInclude = ['order_id', 'full_name', 'type', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country']; $addressFieldsData = Arr::only($address, $keysToInclude); $addressFieldsData['name'] = Arr::get($address, 'full_name', ''); // get other data from address without an empty value $addressOtherData = array_filter(Arr::except($address, $keysToInclude)); Arr::set($addressFieldsData, 'meta.other_data', $addressOtherData); $validationData = Arr::except($addressFieldsData, ['type', 'name']); $hasValue = array_filter($validationData); // removes empty/null/false values if (!$hasValue) { return; } $addressFieldsData['order_id'] = $orderId; if (!empty($addressFieldsData)) { if (Arr::get($addressFieldsData, 'order_id') !== null) { $alreadyOrderHasAddress = OrderAddressResource::find( null, [ 'order_id' => $orderId, 'type' => $type ] ); if ($alreadyOrderHasAddress) { OrderAddressResource::update($addressFieldsData, $alreadyOrderHasAddress->id); } else { OrderAddressResource::create($addressFieldsData); } } } } /** * Copy an existing OrderAddress's meta (company/VAT/legal-reg id/label) onto the * order-address row of the same type already created for $orderId. insertOrderAddresses() * only accepts flat column data, so a parent-order snapshot copy (renewals) must * carry meta over separately. */ public static function copyOrderAddressMeta($orderId, $type, $sourceAddress): void { if (!$sourceAddress || empty($sourceAddress->meta)) { return; } OrderAddress::query() ->where('order_id', $orderId) ->where('type', $type) ->update(['meta' => json_encode($sourceAddress->meta)]); } public static function getIpAddress($anonymize = false) { static $ipAddress; if ($ipAddress) { return $ipAddress; } if (empty($_SERVER['REMOTE_ADDR'])) { // It's a local cli request return '127.0.0.1'; } $ipAddress = ''; $serverData = App::request()->server(); $HTTP_CF_CONNECTING_IP = Arr::get($serverData, 'HTTP_CF_CONNECTING_IP'); $RemoteAddr = Arr::get($serverData, 'REMOTE_ADDR'); $clientIp = Arr::get($serverData, 'HTTP_CLIENT_IP'); $HTTP_X_FORWARDED_FOR = Arr::get($serverData, 'HTTP_X_FORWARDED_FOR'); if ($HTTP_CF_CONNECTING_IP) { //If it's a valid Cloudflare request if (self::isCfIp($RemoteAddr)) { //Use the CF-Connecting-IP header. $ipAddress = $HTTP_CF_CONNECTING_IP; } else { //If it isn't valid, then use REMOTE_ADDR. $ipAddress = $RemoteAddr; } } else if ($RemoteAddr == '127.0.0.1') { // most probably it's local reverse proxy if ($clientIp) { $ipAddress = $clientIp; } else if ($HTTP_X_FORWARDED_FOR) { $ipAddress = (string) rest_is_ip_address(trim(current(preg_split('/,/', sanitize_text_field($HTTP_X_FORWARDED_FOR))))); } } if (!$ipAddress) { $ipAddress = $RemoteAddr; } $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress); $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress, []); if ($anonymize) { return wp_privacy_anonymize_ip($ipAddress); } $ipAddress = sanitize_text_field(wp_unslash($ipAddress)); return $ipAddress; } private static function isCfIp($ip = '') { $serverData = App::request()->server(); $REMOTE_ADDR = Arr::get($serverData, 'REMOTE_ADDR'); if (!$ip) { $ip = $REMOTE_ADDR; } $cloudflareIPRanges = array( '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22', ); $validCFRequest = false; //Make sure that the request came via Cloudflare. foreach ($cloudflareIPRanges as $range) { //Use the ip_in_range function from Joomla. if (self::ipInRange($ip, $range)) { //IP is valid. Belongs to Cloudflare. return true; } } return false; } private static function ipInRange($ip, $range) { if (strpos($range, '/') !== false) { // $range is in IP/NETMASK format list($range, $netmask) = explode('/', $range, 2); if (strpos($netmask, '.') !== false) { // $netmask is a 255.255.0.0 format $netmask = str_replace('*', '0', $netmask); $netmask_dec = ip2long($netmask); return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec)); } else { // $netmask is a CIDR size block // fix the range argument $x = explode('.', $range); while (count($x) < 4) $x[] = '0'; list($a, $b, $c, $d) = $x; $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d); $range_dec = ip2long($range); $ip_dec = ip2long($ip); # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0')); # Strategy 2 - Use math to create it $wildcard_dec = pow(2, (32 - $netmask)) - 1; $netmask_dec = ~$wildcard_dec; return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec)); } } else { // range might be 255.255.*.* or 1.2.3.0-1.2.3.255 if (strpos($range, '*') !== false) { // a.b.*.* format // Just convert to A-B format by setting * to 0 for A and 255 for B $lower = str_replace('*', '0', $range); $upper = str_replace('*', '255', $range); $range = "$lower-$upper"; } if (strpos($range, '-') !== false) { // A-B format list($lower, $upper) = explode('-', $range, 2); $lower_dec = (float) sprintf("%u", ip2long($lower)); $upper_dec = (float) sprintf("%u", ip2long($upper)); $ip_dec = (float) sprintf("%u", ip2long($ip)); return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec)); } return false; } } public static function getStateNameByCode($stateCode, $countryCode = null) { return App::localization()->getStateNameByCode($stateCode, $countryCode); } public static function getCountryNameByCode($countryCode) { return App::localization()->getCountryNameByCode($countryCode); } public static function getUserAgent($sanitize = true) { static $userAgent; if ($userAgent !== null) { return $userAgent; } $serverData = App::request()->server(); $userAgentServer = Arr::get($serverData, 'HTTP_USER_AGENT'); // Return empty string for CLI requests or missing user agent if (empty($userAgentServer)) { $userAgent = ''; return $userAgent; } $userAgent = $userAgentServer; // Apply WordPress filter to allow customization $userAgent = apply_filters('fluent_auth/user_agent', $userAgent, []); // Sanitize the user agent if requested if ($sanitize) { $userAgent = sanitize_text_field($userAgent); } // Return empty string if user agent is invalid after sanitization if (empty($userAgent) || strlen($userAgent) > 1000) { $userAgent = ''; } return $userAgent; } /** * Guess first name and last name from the full name. * * @param string $fullName * @return array */ public static function guessFirstNameAndLastName($fullName): array { $fullName = trim($fullName); $parts = explode(' ', $fullName); if (count($parts) == 1) { return [ 'first_name' => trim($fullName), 'last_name' => '' ]; } $lastName = array_pop($parts); $firstName = implode(' ', $parts); return [ 'first_name' => trim($firstName), 'last_name' => trim($lastName) ]; } public static function getAvailableShippingMethodLists($data): array { $state = Arr::get($data, 'state', ''); $countryCode = Arr::get($data, 'country_code', ''); if (!$countryCode) { $timezone = Arr::get($data, 'timezone', ''); if ($timezone) { $countryCode = LocalizationManager::guessCountryFromTimezone($timezone); } } if (!$countryCode) { return [ 'status' => false, 'error_type' => 'no_country', 'message' => __('Please provide your address to view shipping options', 'fluent-cart') ]; } $availableShippingMethods = ShippingMethod::getApplicableForCountry($countryCode, $state); if (!$availableShippingMethods || $availableShippingMethods->isEmpty()) { $settingView = '
' . esc_html__('No shipping methods available for this address.', 'fluent-cart'); if (current_user_can('manage_options')) { $settingsPageUrl = admin_url('admin.php?page=fluent-cart#/settings/shipping'); $settingsLink = '' . esc_html__('Activate from settings.', 'fluent-cart') . ''; $settingView .= ' ' . $settingsLink; } $settingView .= '
'; return [ 'status' => false, 'country_code' => $countryCode, 'view' => $settingView ]; } return [ 'available_shipping_methods' => $availableShippingMethods, 'country_code' => $countryCode ]; } public static function getShippingMethods($country, $state = null, $timezone = null) { if (!$country && $timezone) { $country = LocalizationManager::guessCountryFromTimezone($timezone); } if (!$country) { return new \WP_Error('no_country', __('Please provide your shipping address to get shipping options', 'fluent-cart')); } $shippingMethods = ShippingMethod::getApplicableForCountry($country, $state); // let's filter the shipping methods $formattedMethods = []; $requireState = false; foreach ($shippingMethods as $shippingMethod) { if (!$shippingMethod->states) { $formattedMethods[] = $shippingMethod; continue; } // now we have states for this shipping method if (!$state) { $requireState = true; continue; } if (in_array($state, $shippingMethod->states)) { $formattedMethods[] = $shippingMethod; } } if (!$formattedMethods && $requireState) { return new \WP_Error('require_state', __('Enter your shipping address to view available shipping methods. Billing and shipping address is the same by default unless you ship to a different address.', 'fluent-cart')); } if (!$formattedMethods) { return new \WP_Error('no_shipping_methods', __('No shipping options available for the provided address', 'fluent-cart')); } return $formattedMethods; } /** * Get shipping methods for a specific shipping class (or General if null). * * @param string $country Country code * @param string|null $state State code * @param int|null $shippingClassId Shipping class ID (null for General zones) * @return array|\WP_Error */ public static function getShippingMethodsForClass($country, $state = null, $shippingClassId = null) { if (!$country) { return new \WP_Error('no_country', __('Please provide your shipping address to get shipping options', 'fluent-cart')); } $shippingMethods = ShippingMethod::applicableToCountry($country, $state, $shippingClassId) ->orderBy('amount', 'DESC') ->get(); $formattedMethods = []; foreach ($shippingMethods as $shippingMethod) { if (!$shippingMethod->states) { $formattedMethods[] = $shippingMethod; continue; } if ($state && in_array($state, $shippingMethod->states)) { $formattedMethods[] = $shippingMethod; } } return $formattedMethods; } public static function getCustomerValidatedAddresses($config, $customer): array { $type = Arr::get($config, 'type', 'billing'); // billing or shipping $addressValidations = array_filter(CheckoutFieldsSchema::getCheckoutFieldsRequirements($type, 'physical')); $storeCountry = (new StoreSettings())->get('store_country'); $requirementsFields = CheckoutFieldsSchema::getCheckoutFieldsRequirements( $type, Arr::get($config, 'product_type'), Arr::get($config, 'with_shipping') ); if ($type === 'billing') { unset($requirementsFields['full_name']); unset($requirementsFields['first_name']); unset($requirementsFields['last_name']); unset($requirementsFields['company_name']); } $addresses = CustomerAddressResource::get([ 'type' => $type, 'customer_id' => $customer->id, 'status' => 'active' ]); $allowedAddresses = []; foreach ($addresses as $address) { $isValid = true; // Resolve name fields for validation from available sources $addressName = trim(Arr::get($address, 'name') ?? ''); $metaFirstName = trim(Arr::get($address, 'meta.other_data.first_name') ?? ''); $metaLastName = trim(Arr::get($address, 'meta.other_data.last_name') ?? ''); if (empty(Arr::get($address, 'first_name'))) { if ($metaFirstName) { $address['first_name'] = $metaFirstName; $address['last_name'] = $metaLastName; } else if ($addressName) { $nameParts = static::guessFirstNameAndLastName($addressName); $address['first_name'] = Arr::get($nameParts, 'first_name', ''); $address['last_name'] = Arr::get($nameParts, 'last_name', ''); } } if (empty(Arr::get($address, 'full_name'))) { if ($addressName) { $address['full_name'] = $addressName; } else if ($metaFirstName) { $address['full_name'] = trim($metaFirstName . ' ' . $metaLastName); } } // If country is not required in checkout fields, only allow addresses matching store country if (!isset($addressValidations['country']) && $storeCountry) { $addressCountry = Arr::get($address, 'country'); if ($addressCountry !== $storeCountry) { continue; // Skip this address } } foreach ($requirementsFields as $key => $requirement) { if ($key === 'state') { $country = Arr::get($address, 'country'); if ($country) { $states = LocalizationManager::getInstance()->statesOptions($country); if (!empty($states) && !in_array(Arr::get($address, 'state'), array_column($states, 'value'))) { $isValid = false; break; } } //continue; } else if ($requirement === 'required' && empty(Arr::get($address, $key))) { $isValid = false; break; } } if ($isValid) { $id = Arr::get($address, 'id'); if ($id) { $allowedAddresses[$id] = $address; } } } return $allowedAddresses; } public static function getPrimaryAddress(array $addresses, array $config, $customer, $type = 'billing') { $primaryAddress = Arr::first($addresses); $primaryAddressId = ''; $orderId = Arr::get($config, 'order_id', null); if ($orderId) { $order = Order::find($orderId); if ($type === 'billing') { if ($order && $order->billing_address) { $primaryAddress = $order->billing_address->toArray(); } else if ($order && $order->shipping_address) { $primaryAddress = $order->shipping_address->toArray(); } } } else { if ($type === 'billing' && $customer->primary_billing_address) { $primaryAddressId = $customer->primary_billing_address->id; if (!empty(Arr::get($config, 'billing_address_id', ''))) { $primaryAddressId = Arr::get($config, 'billing_address_id'); } } else if ($customer->primary_shipping_address) { $primaryAddressId = $customer->primary_shipping_address->id; if (!empty(Arr::get($config, 'shipping_address_id', ''))) { $primaryAddressId = Arr::get($config, 'shipping_address_id'); } } if (Arr::has($addresses, $primaryAddressId)) { $primaryAddress = $addresses[$primaryAddressId]; } } return $primaryAddress; } public static function maybePushAddressDataForCheckout($data, $type = 'billing') { if (!empty($data[$type . '_address_id'])) { $currentCustomer = CustomerResource::getCurrentCustomer(); if (!$currentCustomer) { return $data; } $addressModel = CustomerAddresses::find($data[$type . '_address_id']); if ($addressModel && $addressModel->customer_id == $currentCustomer->id) { $addressData = $addressModel->getFormattedDataForCheckout($type . '_'); $businessInfoFields = [$type . '_company_name', $type . '_legal_registration_id']; foreach ($addressData as $key => $value) { if ($value !== '') { if (in_array($key, $businessInfoFields) && !empty($data[$key])) { continue; } $data[$key] = $value; } } if ($type === 'billing') { $vatNumber = Arr::get($addressData, 'billing_vat_number', ''); if ((!isset($data['fct_billing_tax_id']) || $data['fct_billing_tax_id'] === '') && $vatNumber !== '') { $data['fct_billing_tax_id'] = $vatNumber; } } } } return $data; } public static function mergeBillingWithShipping($data) { $keys = [ 'full_name', 'address_1', 'address_2', 'city', 'state', 'phone', 'postcode', 'country', 'company_name' ]; foreach ($keys as $key) { $data['shipping_' . $key] = Arr::get($data, 'billing_' . $key, ''); } return $data; } public static function getDefaultBillingCountryForCheckout() { // get from cloudflare header $countryCode = ''; if (isset($_SERVER["HTTP_CF_IPCOUNTRY"])) { $countryCode = sanitize_text_field(wp_unslash($_SERVER["HTTP_CF_IPCOUNTRY"])); } return apply_filters('fluent_cart/default_billing_country_for_checkout', $countryCode); } }