PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.20
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.20
1.6.5 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 All 48 releases
fluent-cart / app / Helpers / AddressHelper.php

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

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