PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Helpers / AddressHelper.php

AddressHelper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Helpers/AddressHelper.php

650 lines 22.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Helpers;
4
5 use FluentCart\Api\Resource\CustomerResource;
6 use FluentCart\Api\Resource\FrontendResource\CustomerAddressResource;
7 use FluentCart\Api\Resource\FrontendResource\OrderAddressResource;
8 use FluentCart\Api\Resource\FrontendResource\OrderResource;
9 use FluentCart\Api\StoreSettings;
10 use FluentCart\App\App;
11 use FluentCart\App\Models\CustomerAddresses;
12 use FluentCart\App\Models\Order;
13 use FluentCart\App\Models\OrderAddress;
14 use FluentCart\App\Models\ShippingMethod;
15 use FluentCart\App\Services\Localization\LocalizationManager;
16 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
17 use FluentCart\Framework\Support\Arr;
18
19 class AddressHelper
20 {
21 public static function insertOrderAddresses($orderId, $billingAddress, $shippingAddress)
22 {
23
24 if (!empty($billingAddress)) {
25 //Don't remove this line,
26 //When an order is placed there no input for address label, so keep it empty
27 $billingAddress['name'] = '';
28 static::createOrderAddress($orderId, $billingAddress);
29 }
30 if (!empty($shippingAddress)) {
31 //Don't remove this line,
32 //When an order is placed there no input for address label, so keep it empty
33 $shippingAddress['name'] = '';
34 static::createOrderAddress($orderId, $shippingAddress);
35 }
36 }
37
38 public static function createOrderAddress($orderId, array $address)
39 {
40 //to-do: will refactor this later
41 $addressId = Arr::get($address, 'address', null);
42 $type = Arr::get($address, 'type', false);
43
44 if ($addressId !== null) {
45 $address = CustomerAddressResource::find($addressId);
46 $address = Arr::get($address, 'address');
47 if ($address) {
48 $address = $address->toArray();
49 if ($type) {
50 $address['type'] = $type;
51 }
52 }
53
54 }
55
56 if (empty($address)) {
57 return;
58 }
59
60 $keysToInclude = ['order_id', 'full_name', 'type', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country'];
61 $addressFieldsData = Arr::only($address, $keysToInclude);
62 $addressFieldsData['name'] = Arr::get($address, 'full_name', '');
63
64 // get other data from address without an empty value
65 $addressOtherData = array_filter(Arr::except($address, $keysToInclude));
66 Arr::set($addressFieldsData, 'meta.other_data', $addressOtherData);
67
68 $validationData = Arr::except($addressFieldsData, ['type', 'name']);
69
70 $hasValue = array_filter($validationData); // removes empty/null/false values
71
72 if (!$hasValue) {
73 return;
74 }
75
76 $addressFieldsData['order_id'] = $orderId;
77
78 if (!empty($addressFieldsData)) {
79 if (Arr::get($addressFieldsData, 'order_id') !== null) {
80 $alreadyOrderHasAddress = OrderAddressResource::find(
81 null,
82 [
83 'order_id' => $orderId,
84 'type' => $type
85 ]
86 );
87
88 if ($alreadyOrderHasAddress) {
89 OrderAddressResource::update($addressFieldsData, $alreadyOrderHasAddress->id);
90 } else {
91 OrderAddressResource::create($addressFieldsData);
92 }
93 }
94 }
95 }
96
97 /**
98 * Copy an existing OrderAddress's meta (company/VAT/legal-reg id/label) onto the
99 * order-address row of the same type already created for $orderId. insertOrderAddresses()
100 * only accepts flat column data, so a parent-order snapshot copy (renewals) must
101 * carry meta over separately.
102 */
103 public static function copyOrderAddressMeta($orderId, $type, $sourceAddress): void
104 {
105 if (!$sourceAddress || empty($sourceAddress->meta)) {
106 return;
107 }
108
109 OrderAddress::query()
110 ->where('order_id', $orderId)
111 ->where('type', $type)
112 ->update(['meta' => json_encode($sourceAddress->meta)]);
113 }
114
115 public static function getIpAddress($anonymize = false)
116 {
117 static $ipAddress;
118
119 if ($ipAddress) {
120 return $ipAddress;
121 }
122
123 if (empty($_SERVER['REMOTE_ADDR'])) {
124 // It's a local cli request
125 return '127.0.0.1';
126 }
127
128 $ipAddress = '';
129
130 $serverData = App::request()->server();
131 $HTTP_CF_CONNECTING_IP = Arr::get($serverData, 'HTTP_CF_CONNECTING_IP');
132 $RemoteAddr = Arr::get($serverData, 'REMOTE_ADDR');
133 $clientIp = Arr::get($serverData, 'HTTP_CLIENT_IP');
134 $HTTP_X_FORWARDED_FOR = Arr::get($serverData, 'HTTP_X_FORWARDED_FOR');
135 if ($HTTP_CF_CONNECTING_IP) {
136 //If it's a valid Cloudflare request
137
138 if (self::isCfIp($RemoteAddr)) {
139 //Use the CF-Connecting-IP header.
140 $ipAddress = $HTTP_CF_CONNECTING_IP;
141 } else {
142 //If it isn't valid, then use REMOTE_ADDR.
143 $ipAddress = $RemoteAddr;
144 }
145 } else if ($RemoteAddr == '127.0.0.1') {
146 // most probably it's local reverse proxy
147 if ($clientIp) {
148 $ipAddress = $clientIp;
149 } else if ($HTTP_X_FORWARDED_FOR) {
150 $ipAddress = (string) rest_is_ip_address(trim(current(preg_split('/,/', sanitize_text_field($HTTP_X_FORWARDED_FOR)))));
151 }
152 }
153
154 if (!$ipAddress) {
155 $ipAddress = $RemoteAddr;
156 }
157
158 $ipAddress = preg_replace('/^(\d+\.\d+\.\d+\.\d+):\d+$/', '\1', $ipAddress);
159
160 $ipAddress = apply_filters('fluent_auth/user_ip', $ipAddress, []);
161
162 if ($anonymize) {
163 return wp_privacy_anonymize_ip($ipAddress);
164 }
165
166 $ipAddress = sanitize_text_field(wp_unslash($ipAddress));
167
168 return $ipAddress;
169 }
170
171 private static function isCfIp($ip = '')
172 {
173 $serverData = App::request()->server();
174 $REMOTE_ADDR = Arr::get($serverData, 'REMOTE_ADDR');
175 if (!$ip) {
176 $ip = $REMOTE_ADDR;
177 }
178 $cloudflareIPRanges = array(
179 '173.245.48.0/20',
180 '103.21.244.0/22',
181 '103.22.200.0/22',
182 '103.31.4.0/22',
183 '141.101.64.0/18',
184 '108.162.192.0/18',
185 '190.93.240.0/20',
186 '188.114.96.0/20',
187 '197.234.240.0/22',
188 '198.41.128.0/17',
189 '162.158.0.0/15',
190 '104.16.0.0/13',
191 '104.24.0.0/14',
192 '172.64.0.0/13',
193 '131.0.72.0/22',
194 );
195 $validCFRequest = false;
196 //Make sure that the request came via Cloudflare.
197 foreach ($cloudflareIPRanges as $range) {
198 //Use the ip_in_range function from Joomla.
199 if (self::ipInRange($ip, $range)) {
200 //IP is valid. Belongs to Cloudflare.
201 return true;
202 }
203 }
204
205 return false;
206 }
207
208 private static function ipInRange($ip, $range)
209 {
210 if (strpos($range, '/') !== false) {
211 // $range is in IP/NETMASK format
212 list($range, $netmask) = explode('/', $range, 2);
213 if (strpos($netmask, '.') !== false) {
214 // $netmask is a 255.255.0.0 format
215 $netmask = str_replace('*', '0', $netmask);
216 $netmask_dec = ip2long($netmask);
217 return ((ip2long($ip) & $netmask_dec) == (ip2long($range) & $netmask_dec));
218 } else {
219 // $netmask is a CIDR size block
220 // fix the range argument
221 $x = explode('.', $range);
222 while (count($x) < 4)
223 $x[] = '0';
224 list($a, $b, $c, $d) = $x;
225 $range = sprintf("%u.%u.%u.%u", empty($a) ? '0' : $a, empty($b) ? '0' : $b, empty($c) ? '0' : $c, empty($d) ? '0' : $d);
226 $range_dec = ip2long($range);
227 $ip_dec = ip2long($ip);
228
229 # Strategy 1 - Create the netmask with 'netmask' 1s and then fill it to 32 with 0s
230 #$netmask_dec = bindec(str_pad('', $netmask, '1') . str_pad('', 32-$netmask, '0'));
231
232 # Strategy 2 - Use math to create it
233 $wildcard_dec = pow(2, (32 - $netmask)) - 1;
234 $netmask_dec = ~$wildcard_dec;
235
236 return (($ip_dec & $netmask_dec) == ($range_dec & $netmask_dec));
237 }
238 } else {
239 // range might be 255.255.*.* or 1.2.3.0-1.2.3.255
240 if (strpos($range, '*') !== false) { // a.b.*.* format
241 // Just convert to A-B format by setting * to 0 for A and 255 for B
242 $lower = str_replace('*', '0', $range);
243 $upper = str_replace('*', '255', $range);
244 $range = "$lower-$upper";
245 }
246
247 if (strpos($range, '-') !== false) { // A-B format
248 list($lower, $upper) = explode('-', $range, 2);
249 $lower_dec = (float) sprintf("%u", ip2long($lower));
250 $upper_dec = (float) sprintf("%u", ip2long($upper));
251 $ip_dec = (float) sprintf("%u", ip2long($ip));
252 return (($ip_dec >= $lower_dec) && ($ip_dec <= $upper_dec));
253 }
254 return false;
255 }
256 }
257
258 public static function getStateNameByCode($stateCode, $countryCode = null)
259 {
260 return App::localization()->getStateNameByCode($stateCode, $countryCode);
261 }
262
263 public static function getCountryNameByCode($countryCode)
264 {
265 return App::localization()->getCountryNameByCode($countryCode);
266 }
267
268 public static function getUserAgent($sanitize = true)
269 {
270 static $userAgent;
271
272 if ($userAgent !== null) {
273 return $userAgent;
274 }
275 $serverData = App::request()->server();
276 $userAgentServer = Arr::get($serverData, 'HTTP_USER_AGENT');
277
278 // Return empty string for CLI requests or missing user agent
279 if (empty($userAgentServer)) {
280 $userAgent = '';
281 return $userAgent;
282 }
283
284 $userAgent = $userAgentServer;
285
286 // Apply WordPress filter to allow customization
287 $userAgent = apply_filters('fluent_auth/user_agent', $userAgent, []);
288
289 // Sanitize the user agent if requested
290 if ($sanitize) {
291 $userAgent = sanitize_text_field($userAgent);
292 }
293
294 // Return empty string if user agent is invalid after sanitization
295 if (empty($userAgent) || strlen($userAgent) > 1000) {
296 $userAgent = '';
297 }
298
299 return $userAgent;
300 }
301
302 /**
303 * Guess first name and last name from the full name.
304 *
305 * @param string $fullName
306 * @return array
307 */
308 public static function guessFirstNameAndLastName($fullName): array
309 {
310 $fullName = trim($fullName);
311 $parts = explode(' ', $fullName);
312 if (count($parts) == 1) {
313 return [
314 'first_name' => trim($fullName),
315 'last_name' => ''
316 ];
317 }
318 $lastName = array_pop($parts);
319 $firstName = implode(' ', $parts);
320
321 return [
322 'first_name' => trim($firstName),
323 'last_name' => trim($lastName)
324 ];
325 }
326
327 public static function getAvailableShippingMethodLists($data): array
328 {
329 $state = Arr::get($data, 'state', '');
330 $countryCode = Arr::get($data, 'country_code', '');
331
332 if (!$countryCode) {
333 $timezone = Arr::get($data, 'timezone', '');
334 if ($timezone) {
335 $countryCode = LocalizationManager::guessCountryFromTimezone($timezone);
336 }
337 }
338
339 if (!$countryCode) {
340 return [
341 'status' => false,
342 'error_type' => 'no_country',
343 'message' => __('Please provide your address to view shipping options', 'fluent-cart')
344 ];
345 }
346
347 $availableShippingMethods = ShippingMethod::getApplicableForCountry($countryCode, $state);
348
349 if (!$availableShippingMethods || $availableShippingMethods->isEmpty()) {
350 $settingView = '<div class="fct-empty-state">'
351 . esc_html__('No shipping methods available for this address.', 'fluent-cart');
352
353 if (current_user_can('manage_options')) {
354 $settingsPageUrl = admin_url('admin.php?page=fluent-cart#/settings/shipping');
355
356 $settingsLink = '<a href="' . esc_url($settingsPageUrl ?? '') . '" target="_blank">' . esc_html__('Activate from settings.', 'fluent-cart') . '</a>';
357
358 $settingView .= ' ' . $settingsLink;
359 }
360
361 $settingView .= '</div>';
362
363 return [
364 'status' => false,
365 'country_code' => $countryCode,
366 'view' => $settingView
367 ];
368 }
369
370 return [
371 'available_shipping_methods' => $availableShippingMethods,
372 'country_code' => $countryCode
373 ];
374 }
375
376 public static function getShippingMethods($country, $state = null, $timezone = null)
377 {
378 if (!$country && $timezone) {
379 $country = LocalizationManager::guessCountryFromTimezone($timezone);
380 }
381
382 if (!$country) {
383 return new \WP_Error('no_country', __('Please provide your shipping address to get shipping options', 'fluent-cart'));
384 }
385
386 $shippingMethods = ShippingMethod::getApplicableForCountry($country, $state);
387
388 // let's filter the shipping methods
389 $formattedMethods = [];
390
391 $requireState = false;
392
393 foreach ($shippingMethods as $shippingMethod) {
394 if (!$shippingMethod->states) {
395 $formattedMethods[] = $shippingMethod;
396 continue;
397 }
398
399 // now we have states for this shipping method
400 if (!$state) {
401 $requireState = true;
402 continue;
403 }
404
405 if (in_array($state, $shippingMethod->states)) {
406 $formattedMethods[] = $shippingMethod;
407 }
408 }
409
410 if (!$formattedMethods && $requireState) {
411 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'));
412 }
413
414 if (!$formattedMethods) {
415 return new \WP_Error('no_shipping_methods', __('No shipping options available for the provided address', 'fluent-cart'));
416 }
417
418 return $formattedMethods;
419 }
420
421 /**
422 * Get shipping methods for a specific shipping class (or General if null).
423 *
424 * @param string $country Country code
425 * @param string|null $state State code
426 * @param int|null $shippingClassId Shipping class ID (null for General zones)
427 * @return array|\WP_Error
428 */
429 public static function getShippingMethodsForClass($country, $state = null, $shippingClassId = null)
430 {
431 if (!$country) {
432 return new \WP_Error('no_country', __('Please provide your shipping address to get shipping options', 'fluent-cart'));
433 }
434
435 $shippingMethods = ShippingMethod::applicableToCountry($country, $state, $shippingClassId)
436 ->orderBy('amount', 'DESC')
437 ->get();
438
439 $formattedMethods = [];
440 foreach ($shippingMethods as $shippingMethod) {
441 if (!$shippingMethod->states) {
442 $formattedMethods[] = $shippingMethod;
443 continue;
444 }
445 if ($state && in_array($state, $shippingMethod->states)) {
446 $formattedMethods[] = $shippingMethod;
447 }
448 }
449
450 return $formattedMethods;
451 }
452
453 public static function getCustomerValidatedAddresses($config, $customer): array
454 {
455 $type = Arr::get($config, 'type', 'billing'); // billing or shipping
456
457 $addressValidations = array_filter(CheckoutFieldsSchema::getCheckoutFieldsRequirements($type, 'physical'));
458 $storeCountry = (new StoreSettings())->get('store_country');
459
460 $requirementsFields = CheckoutFieldsSchema::getCheckoutFieldsRequirements(
461 $type,
462 Arr::get($config, 'product_type'),
463 Arr::get($config, 'with_shipping')
464 );
465 if ($type === 'billing') {
466 unset($requirementsFields['full_name']);
467 unset($requirementsFields['first_name']);
468 unset($requirementsFields['last_name']);
469 unset($requirementsFields['company_name']);
470 }
471
472 $addresses = CustomerAddressResource::get([
473 'type' => $type,
474 'customer_id' => $customer->id,
475 'status' => 'active'
476 ]);
477
478 $allowedAddresses = [];
479
480 foreach ($addresses as $address) {
481 $isValid = true;
482
483
484 // Resolve name fields for validation from available sources
485 $addressName = trim(Arr::get($address, 'name') ?? '');
486 $metaFirstName = trim(Arr::get($address, 'meta.other_data.first_name') ?? '');
487 $metaLastName = trim(Arr::get($address, 'meta.other_data.last_name') ?? '');
488
489 if (empty(Arr::get($address, 'first_name'))) {
490 if ($metaFirstName) {
491 $address['first_name'] = $metaFirstName;
492 $address['last_name'] = $metaLastName;
493 } else if ($addressName) {
494 $nameParts = static::guessFirstNameAndLastName($addressName);
495 $address['first_name'] = Arr::get($nameParts, 'first_name', '');
496 $address['last_name'] = Arr::get($nameParts, 'last_name', '');
497 }
498 }
499
500 if (empty(Arr::get($address, 'full_name'))) {
501 if ($addressName) {
502 $address['full_name'] = $addressName;
503 } else if ($metaFirstName) {
504 $address['full_name'] = trim($metaFirstName . ' ' . $metaLastName);
505 }
506 }
507
508 // If country is not required in checkout fields, only allow addresses matching store country
509 if (!isset($addressValidations['country']) && $storeCountry) {
510 $addressCountry = Arr::get($address, 'country');
511 if ($addressCountry !== $storeCountry) {
512 continue; // Skip this address
513 }
514 }
515
516 foreach ($requirementsFields as $key => $requirement) {
517
518 if ($key === 'state') {
519 $country = Arr::get($address, 'country');
520 if ($country) {
521 $states = LocalizationManager::getInstance()->statesOptions($country);
522 if (!empty($states) && !in_array(Arr::get($address, 'state'), array_column($states, 'value'))) {
523 $isValid = false;
524 break;
525 }
526 }
527 //continue;
528 } else if ($requirement === 'required' && empty(Arr::get($address, $key))) {
529 $isValid = false;
530 break;
531 }
532 }
533
534 if ($isValid) {
535 $id = Arr::get($address, 'id');
536 if ($id) {
537 $allowedAddresses[$id] = $address;
538 }
539 }
540 }
541
542 return $allowedAddresses;
543 }
544
545 public static function getPrimaryAddress(array $addresses, array $config, $customer, $type = 'billing')
546 {
547 $primaryAddress = Arr::first($addresses);
548 $primaryAddressId = '';
549
550 $orderId = Arr::get($config, 'order_id', null);
551 if ($orderId) {
552 $order = Order::find($orderId);
553
554 if ($type === 'billing') {
555 if ($order && $order->billing_address) {
556 $primaryAddress = $order->billing_address->toArray();
557 } else if ($order && $order->shipping_address) {
558 $primaryAddress = $order->shipping_address->toArray();
559 }
560 }
561 } else {
562 if ($type === 'billing' && $customer->primary_billing_address) {
563 $primaryAddressId = $customer->primary_billing_address->id;
564 if (!empty(Arr::get($config, 'billing_address_id', ''))) {
565 $primaryAddressId = Arr::get($config, 'billing_address_id');
566 }
567 } else if ($customer->primary_shipping_address) {
568 $primaryAddressId = $customer->primary_shipping_address->id;
569 if (!empty(Arr::get($config, 'shipping_address_id', ''))) {
570 $primaryAddressId = Arr::get($config, 'shipping_address_id');
571 }
572 }
573
574 if (Arr::has($addresses, $primaryAddressId)) {
575 $primaryAddress = $addresses[$primaryAddressId];
576 }
577 }
578
579 return $primaryAddress;
580 }
581
582
583 public static function maybePushAddressDataForCheckout($data, $type = 'billing')
584 {
585 if (!empty($data[$type . '_address_id'])) {
586 $currentCustomer = CustomerResource::getCurrentCustomer();
587 if (!$currentCustomer) {
588 return $data;
589 }
590
591 $addressModel = CustomerAddresses::find($data[$type . '_address_id']);
592
593 if ($addressModel && $addressModel->customer_id == $currentCustomer->id) {
594 $addressData = $addressModel->getFormattedDataForCheckout($type . '_');
595
596 $businessInfoFields = [$type . '_company_name', $type . '_legal_registration_id'];
597 foreach ($addressData as $key => $value) {
598 if ($value !== '') {
599 if (in_array($key, $businessInfoFields) && !empty($data[$key])) {
600 continue;
601 }
602 $data[$key] = $value;
603 }
604 }
605
606 if ($type === 'billing') {
607 $vatNumber = Arr::get($addressData, 'billing_vat_number', '');
608 if ((!isset($data['fct_billing_tax_id']) || $data['fct_billing_tax_id'] === '') && $vatNumber !== '') {
609 $data['fct_billing_tax_id'] = $vatNumber;
610 }
611 }
612 }
613 }
614 return $data;
615 }
616
617 public static function mergeBillingWithShipping($data)
618 {
619 $keys = [
620 'full_name',
621 'address_1',
622 'address_2',
623 'city',
624 'state',
625 'phone',
626 'postcode',
627 'country',
628 'company_name'
629 ];
630 foreach ($keys as $key) {
631 $data['shipping_' . $key] = Arr::get($data, 'billing_' . $key, '');
632 }
633
634 return $data;
635 }
636
637
638 public static function getDefaultBillingCountryForCheckout()
639 {
640 // get from cloudflare header
641 $countryCode = '';
642 if (isset($_SERVER["HTTP_CF_IPCOUNTRY"])) {
643 $countryCode = sanitize_text_field(wp_unslash($_SERVER["HTTP_CF_IPCOUNTRY"]));
644 }
645
646 return apply_filters('fluent_cart/default_billing_country_for_checkout', $countryCode);
647 }
648
649 }
650