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);
}
}