Merge pull request #18998 from eileenmcnaughton/trans2
[civicrm-core.git] / CRM / Core / BAO / Address.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * This is class to handle address related functions.
20 */
21 class CRM_Core_BAO_Address extends CRM_Core_DAO_Address {
22
23 /**
24 * Takes an associative array and creates a address.
25 *
26 * @param array $params
27 * (reference ) an assoc array of name/value pairs.
28 * @param bool $fixAddress
29 * True if you need to fix (format) address values.
30 * before inserting in db
31 *
32 * @return array|NULL|self
33 * array of created address
34 */
35 public static function create(&$params, $fixAddress = TRUE) {
36 if (!isset($params['address']) || !is_array($params['address'])) {
37 return self::add($params, $fixAddress);
38 }
39 CRM_Core_Error::deprecatedFunctionWarning('Use legacyCreate if not doing a single crud action');
40 return self::legacyCreate($params, $fixAddress);
41 }
42
43 /**
44 * Takes an associative array and adds address.
45 *
46 * @param array $params
47 * (reference ) an assoc array of name/value pairs.
48 * @param bool $fixAddress
49 * True if you need to fix (format) address values.
50 * before inserting in db
51 *
52 * @return CRM_Core_BAO_Address|null
53 */
54 public static function add(&$params, $fixAddress = FALSE) {
55
56 $address = new CRM_Core_DAO_Address();
57 $checkPermissions = $params['check_permissions'] ?? TRUE;
58
59 // fixAddress mode to be done
60 if ($fixAddress) {
61 CRM_Core_BAO_Address::fixAddress($params);
62 }
63
64 $hook = empty($params['id']) ? 'create' : 'edit';
65 CRM_Utils_Hook::pre($hook, 'Address', CRM_Utils_Array::value('id', $params), $params);
66
67 CRM_Core_BAO_Block::handlePrimary($params, get_class());
68
69 // (prevent chaining 1 and 3) CRM-21214
70 if (isset($params['master_id']) && !CRM_Utils_System::isNull($params['master_id'])) {
71 self::fixSharedAddress($params);
72 }
73
74 $address->copyValues($params);
75 $address->save();
76
77 if ($address->id) {
78 // first get custom field from master address if any
79 if (isset($params['master_id']) && !CRM_Utils_System::isNull($params['master_id'])) {
80 $address->copyCustomFields($params['master_id'], $address->id);
81 }
82
83 if (isset($params['custom'])) {
84 $addressCustom = $params['custom'];
85 }
86 else {
87 $customFields = CRM_Core_BAO_CustomField::getFields('Address', FALSE, TRUE, NULL, NULL, FALSE, FALSE, $checkPermissions);
88
89 if (!empty($customFields)) {
90 $addressCustom = CRM_Core_BAO_CustomField::postProcess($params,
91 $address->id,
92 'Address',
93 FALSE,
94 $checkPermissions
95 );
96 }
97 }
98 if (!empty($addressCustom)) {
99 CRM_Core_BAO_CustomValueTable::store($addressCustom, 'civicrm_address', $address->id);
100 }
101
102 // call the function to sync shared address and create relationships
103 // if address is already shared, share master_id with all children and update relationships accordingly
104 // (prevent chaining 2) CRM-21214
105 self::processSharedAddress($address->id, $params);
106
107 // lets call the post hook only after we've done all the follow on processing
108 CRM_Utils_Hook::post($hook, 'Address', $address->id, $address);
109 }
110
111 return $address;
112 }
113
114 /**
115 * Format the address params to have reasonable values.
116 *
117 * @param array $params
118 * (reference ) an assoc array of name/value pairs.
119 */
120 public static function fixAddress(&$params) {
121 if (!empty($params['billing_street_address'])) {
122 //Check address is coming from online contribution / registration page
123 //Fixed :CRM-5076
124 $billing = [
125 'street_address' => 'billing_street_address',
126 'city' => 'billing_city',
127 'postal_code' => 'billing_postal_code',
128 'state_province' => 'billing_state_province',
129 'state_province_id' => 'billing_state_province_id',
130 'country' => 'billing_country',
131 'country_id' => 'billing_country_id',
132 ];
133
134 foreach ($billing as $key => $val) {
135 if ($value = CRM_Utils_Array::value($val, $params)) {
136 if (!empty($params[$key])) {
137 unset($params[$val]);
138 }
139 else {
140 //add new key and removed old
141 $params[$key] = $value;
142 unset($params[$val]);
143 }
144 }
145 }
146 }
147
148 /* Split the zip and +4, if it's in US format */
149 if (!empty($params['postal_code']) &&
150 preg_match('/^(\d{4,5})[+-](\d{4})$/',
151 $params['postal_code'],
152 $match
153 )
154 ) {
155 $params['postal_code'] = $match[1];
156 $params['postal_code_suffix'] = $match[2];
157 }
158
159 // add country id if not set
160 if ((!isset($params['country_id']) || !is_numeric($params['country_id'])) &&
161 isset($params['country'])
162 ) {
163 $country = new CRM_Core_DAO_Country();
164 $country->name = $params['country'];
165 if (!$country->find(TRUE)) {
166 $country->name = NULL;
167 $country->iso_code = $params['country'];
168 $country->find(TRUE);
169 }
170 $params['country_id'] = $country->id;
171 }
172
173 // add state_id if state is set
174 if ((!isset($params['state_province_id']) || !is_numeric($params['state_province_id']))
175 && isset($params['state_province'])
176 ) {
177 if (!empty($params['state_province'])) {
178 $state_province = new CRM_Core_DAO_StateProvince();
179 $state_province->name = $params['state_province'];
180
181 // add country id if present
182 if (!empty($params['country_id'])) {
183 $state_province->country_id = $params['country_id'];
184 }
185
186 if (!$state_province->find(TRUE)) {
187 unset($state_province->name);
188 $state_province->abbreviation = $params['state_province'];
189 $state_province->find(TRUE);
190 }
191 $params['state_province_id'] = $state_province->id;
192 if (empty($params['country_id'])) {
193 // set this here since we have it
194 $params['country_id'] = $state_province->country_id;
195 }
196 }
197 else {
198 $params['state_province_id'] = 'null';
199 }
200 }
201
202 // add county id if county is set
203 // CRM-7837
204 if ((!isset($params['county_id']) || !is_numeric($params['county_id']))
205 && isset($params['county']) && !empty($params['county'])
206 ) {
207 $county = new CRM_Core_DAO_County();
208 $county->name = $params['county'];
209
210 if (isset($params['state_province_id'])) {
211 $county->state_province_id = $params['state_province_id'];
212 }
213
214 if ($county->find(TRUE)) {
215 $params['county_id'] = $county->id;
216 }
217 }
218
219 // currently copy values populates empty fields with the string "null"
220 // and hence need to check for the string null
221 if (isset($params['state_province_id']) &&
222 is_numeric($params['state_province_id']) &&
223 (!isset($params['country_id']) || empty($params['country_id']))
224 ) {
225 // since state id present and country id not present, hence lets populate it
226 // jira issue http://issues.civicrm.org/jira/browse/CRM-56
227 $params['country_id'] = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_StateProvince',
228 $params['state_province_id'],
229 'country_id'
230 );
231 }
232
233 //special check to ignore non numeric values if they are not
234 //detected by formRule(sometimes happens due to internet latency), also allow user to unselect state/country
235 if (isset($params['state_province_id'])) {
236 if (empty($params['state_province_id'])) {
237 $params['state_province_id'] = 'null';
238 }
239 elseif (!is_numeric($params['state_province_id']) ||
240 ((int ) $params['state_province_id'] < 1000)
241 ) {
242 // CRM-3393 ( the hacky 1000 check)
243 $params['state_province_id'] = 'null';
244 }
245 }
246
247 if (isset($params['country_id'])) {
248 if (empty($params['country_id'])) {
249 $params['country_id'] = 'null';
250 }
251 elseif (!is_numeric($params['country_id']) ||
252 ((int ) $params['country_id'] < 1000)
253 ) {
254 // CRM-3393 ( the hacky 1000 check)
255 $params['country_id'] = 'null';
256 }
257 }
258
259 // add state and country names from the ids
260 if (isset($params['state_province_id']) && is_numeric($params['state_province_id'])) {
261 $params['state_province'] = CRM_Core_PseudoConstant::stateProvinceAbbreviation($params['state_province_id']);
262 }
263
264 if (isset($params['country_id']) && is_numeric($params['country_id'])) {
265 $params['country'] = CRM_Core_PseudoConstant::country($params['country_id']);
266 }
267
268 $asp = Civi::settings()->get('address_standardization_provider');
269 // clean up the address via USPS web services if enabled
270 if ($asp === 'USPS' &&
271 $params['country_id'] == 1228
272 ) {
273 CRM_Utils_Address_USPS::checkAddress($params);
274 }
275 // do street parsing again if enabled, since street address might have changed
276 $parseStreetAddress = CRM_Utils_Array::value(
277 'street_address_parsing',
278 CRM_Core_BAO_Setting::valueOptions(
279 CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
280 'address_options'
281 ),
282 FALSE
283 );
284
285 if ($parseStreetAddress && !empty($params['street_address'])) {
286 foreach (['street_number', 'street_name', 'street_unit', 'street_number_suffix'] as $fld) {
287 unset($params[$fld]);
288 }
289 // main parse string.
290 $parseString = $params['street_address'] ?? NULL;
291 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($parseString);
292
293 // merge parse address in to main address block.
294 $params = array_merge($params, $parsedFields);
295 }
296
297 // skip_geocode is an optional parameter through the api.
298 // manual_geo_code is on the contact edit form. They do the same thing....
299 if (empty($params['skip_geocode']) && empty($params['manual_geo_code'])) {
300 self::addGeocoderData($params);
301 }
302
303 }
304
305 /**
306 * Check if there is data to create the object.
307 *
308 * @param array $params
309 * (reference ) an assoc array of name/value pairs.
310 *
311 * @return bool
312 */
313 public static function dataExists(&$params) {
314 //check if location type is set if not return false
315 if (!isset($params['location_type_id'])) {
316 return FALSE;
317 }
318
319 $config = CRM_Core_Config::singleton();
320 foreach ($params as $name => $value) {
321 if (in_array($name, [
322 'is_primary',
323 'location_type_id',
324 'id',
325 'contact_id',
326 'is_billing',
327 'display',
328 'master_id',
329 ])) {
330 continue;
331 }
332 elseif (!CRM_Utils_System::isNull($value)) {
333 // name could be country or country id
334 if (substr($name, 0, 7) == 'country') {
335 // make sure its different from the default country
336 // iso code
337 $defaultCountry = CRM_Core_BAO_Country::defaultContactCountry();
338 // full name
339 $defaultCountryName = CRM_Core_BAO_Country::defaultContactCountryName();
340
341 if ($defaultCountry) {
342 if ($value == $defaultCountry ||
343 $value == $defaultCountryName ||
344 $value == $config->defaultContactCountry
345 ) {
346 // do nothing
347 }
348 else {
349 return TRUE;
350 }
351 }
352 else {
353 // return if null default
354 return TRUE;
355 }
356 }
357 else {
358 return TRUE;
359 }
360 }
361 }
362
363 return FALSE;
364 }
365
366 /**
367 * Given the list of params in the params array, fetch the object
368 * and store the values in the values array
369 *
370 * @param array $entityBlock
371 * Associated array of fields.
372 * @param bool $microformat
373 * If microformat output is required.
374 * @param int|string $fieldName conditional field name
375 *
376 * @return array
377 * array with address fields
378 */
379 public static function &getValues($entityBlock, $microformat = FALSE, $fieldName = 'contact_id') {
380 if (empty($entityBlock)) {
381 return NULL;
382 }
383 $addresses = [];
384 $address = new CRM_Core_BAO_Address();
385
386 if (empty($entityBlock['entity_table'])) {
387 $address->$fieldName = $entityBlock[$fieldName] ?? NULL;
388 }
389 else {
390 $addressIds = [];
391 $addressIds = self::allEntityAddress($entityBlock);
392
393 if (!empty($addressIds[1])) {
394 $address->id = $addressIds[1];
395 }
396 else {
397 return $addresses;
398 }
399 }
400 if (isset($entityBlock['is_billing']) && $entityBlock['is_billing'] == 1) {
401 $address->orderBy('is_billing desc, id');
402 }
403 else {
404 //get primary address as a first block.
405 $address->orderBy('is_primary desc, id');
406 }
407
408 $address->find();
409
410 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
411 $count = 1;
412 while ($address->fetch()) {
413 // deprecate reference.
414 if ($count > 1) {
415 foreach (['state', 'state_name', 'country', 'world_region'] as $fld) {
416 if (isset($address->$fld)) {
417 unset($address->$fld);
418 }
419 }
420 }
421 $stree = $address->street_address;
422 $values = [];
423 CRM_Core_DAO::storeValues($address, $values);
424
425 // add state and country information: CRM-369
426 if (!empty($address->location_type_id)) {
427 $values['location_type'] = $locationTypes[$address->location_type_id] ?? NULL;
428 }
429 if (!empty($address->state_province_id)) {
430 $address->state = CRM_Core_PseudoConstant::stateProvinceAbbreviation($address->state_province_id, FALSE);
431 $address->state_name = CRM_Core_PseudoConstant::stateProvince($address->state_province_id, FALSE);
432 $values['state_province_abbreviation'] = $address->state;
433 $values['state_province'] = $address->state_name;
434 }
435
436 if (!empty($address->country_id)) {
437 $address->country = CRM_Core_PseudoConstant::country($address->country_id);
438 $values['country'] = $address->country;
439
440 //get world region
441 $regionId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', $address->country_id, 'region_id');
442 $values['world_region'] = CRM_Core_PseudoConstant::worldregion($regionId);
443 }
444
445 $address->addDisplay($microformat);
446
447 $values['display'] = $address->display;
448 $values['display_text'] = $address->display_text;
449
450 if (isset($address->master_id) && !CRM_Utils_System::isNull($address->master_id)) {
451 $values['use_shared_address'] = 1;
452 }
453
454 $addresses[$count] = $values;
455
456 //There should never be more than one primary blocks, hence set is_primary = 0 other than first
457 // Calling functions expect the key is_primary to be set, so do not unset it here!
458 if ($count > 1) {
459 $addresses[$count]['is_primary'] = 0;
460 }
461
462 $count++;
463 }
464
465 return $addresses;
466 }
467
468 /**
469 * Add the formatted address to $this-> display.
470 *
471 * @param bool $microformat
472 * Unexplained parameter that I've always wondered about.
473 */
474 public function addDisplay($microformat = FALSE) {
475 $fields = [
476 // added this for CRM 1200
477 'address_id' => $this->id,
478 // CRM-4003
479 'address_name' => str_replace('\ 1', ' ', $this->name),
480 'street_address' => $this->street_address,
481 'supplemental_address_1' => $this->supplemental_address_1,
482 'supplemental_address_2' => $this->supplemental_address_2,
483 'supplemental_address_3' => $this->supplemental_address_3,
484 'city' => $this->city,
485 'state_province_name' => $this->state_name ?? "",
486 'state_province' => $this->state ?? "",
487 'postal_code' => $this->postal_code ?? "",
488 'postal_code_suffix' => $this->postal_code_suffix ?? "",
489 'country' => $this->country ?? "",
490 'world_region' => $this->world_region ?? "",
491 ];
492
493 if (isset($this->county_id) && $this->county_id) {
494 $fields['county'] = CRM_Core_PseudoConstant::county($this->county_id);
495 }
496 else {
497 $fields['county'] = NULL;
498 }
499
500 $this->display = CRM_Utils_Address::format($fields, NULL, $microformat);
501 $this->display_text = CRM_Utils_Address::format($fields);
502 }
503
504 /**
505 * Get all the addresses for a specified contact_id, with the primary address being first
506 *
507 * @param int $id
508 * The contact id.
509 *
510 * @param bool $updateBlankLocInfo
511 *
512 * @return array
513 * the array of adrress data
514 */
515 public static function allAddress($id, $updateBlankLocInfo = FALSE) {
516 if (!$id) {
517 return NULL;
518 }
519
520 $query = "
521 SELECT civicrm_address.id as address_id, civicrm_address.location_type_id as location_type_id
522 FROM civicrm_contact, civicrm_address
523 WHERE civicrm_address.contact_id = civicrm_contact.id AND civicrm_contact.id = %1
524 ORDER BY civicrm_address.is_primary DESC, address_id ASC";
525 $params = [1 => [$id, 'Integer']];
526
527 $addresses = [];
528 $dao = CRM_Core_DAO::executeQuery($query, $params);
529 $count = 1;
530 while ($dao->fetch()) {
531 if ($updateBlankLocInfo) {
532 $addresses[$count++] = $dao->address_id;
533 }
534 else {
535 $addresses[$dao->location_type_id] = $dao->address_id;
536 }
537 }
538 return $addresses;
539 }
540
541 /**
542 * Get all the addresses for a specified location_block id, with the primary address being first
543 *
544 * @param array $entityElements
545 * The array containing entity_id and.
546 * entity_table name
547 *
548 * @return array
549 * the array of adrress data
550 */
551 public static function allEntityAddress(&$entityElements) {
552 $addresses = [];
553 if (empty($entityElements)) {
554 return $addresses;
555 }
556
557 $entityId = $entityElements['entity_id'];
558 $entityTable = $entityElements['entity_table'];
559
560 $sql = "
561 SELECT civicrm_address.id as address_id
562 FROM civicrm_loc_block loc, civicrm_location_type ltype, civicrm_address, {$entityTable} ev
563 WHERE ev.id = %1
564 AND loc.id = ev.loc_block_id
565 AND civicrm_address.id IN (loc.address_id, loc.address_2_id)
566 AND ltype.id = civicrm_address.location_type_id
567 ORDER BY civicrm_address.is_primary DESC, civicrm_address.location_type_id DESC, address_id ASC ";
568
569 $params = [1 => [$entityId, 'Integer']];
570 $dao = CRM_Core_DAO::executeQuery($sql, $params);
571 $locationCount = 1;
572 while ($dao->fetch()) {
573 $addresses[$locationCount] = $dao->address_id;
574 $locationCount++;
575 }
576 return $addresses;
577 }
578
579 /**
580 * Get address sequence.
581 *
582 * @return array
583 * Array of address sequence.
584 */
585 public static function addressSequence() {
586 $addressSequence = CRM_Utils_Address::sequence(\Civi::settings()->get('address_format'));
587
588 $countryState = $cityPostal = FALSE;
589 foreach ($addressSequence as $key => $field) {
590 if (
591 in_array($field, ['country', 'state_province']) &&
592 !$countryState
593 ) {
594 $countryState = TRUE;
595 $addressSequence[$key] = 'country_state_province';
596 }
597 elseif (
598 in_array($field, ['city', 'postal_code']) &&
599 !$cityPostal
600 ) {
601 $cityPostal = TRUE;
602 $addressSequence[$key] = 'city_postal_code';
603 }
604 elseif (
605 in_array($field, ['country', 'state_province', 'city', 'postal_code'])
606 ) {
607 unset($addressSequence[$key]);
608 }
609 }
610
611 return $addressSequence;
612 }
613
614 /**
615 * Parse given street address string in to street_name,
616 * street_unit, street_number and street_number_suffix
617 * eg "54A Excelsior Ave. Apt 1C", or "917 1/2 Elm Street"
618 *
619 * NB: civic street formats for en_CA and fr_CA used by default if those locales are active
620 * otherwise en_US format is default action
621 *
622 * @param string $streetAddress
623 * Street address including number and apt.
624 * @param string $locale
625 * Locale used to parse address.
626 *
627 * @return array
628 * parsed fields values.
629 */
630 public static function parseStreetAddress($streetAddress, $locale = NULL) {
631 // use 'en_US' for address parsing if the requested locale is not supported.
632 if (!self::isSupportedParsingLocale($locale)) {
633 $locale = 'en_US';
634 }
635
636 $emptyParseFields = $parseFields = [
637 'street_name' => '',
638 'street_unit' => '',
639 'street_number' => '',
640 'street_number_suffix' => '',
641 ];
642
643 if (empty($streetAddress)) {
644 return $parseFields;
645 }
646
647 $streetAddress = trim($streetAddress);
648
649 $matches = [];
650 if (in_array($locale, ['en_CA', 'fr_CA'])
651 && preg_match('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', $streetAddress, $matches)
652 ) {
653 $parseFields['street_unit'] = $matches[1];
654 // unset from rest of street address
655 $streetAddress = preg_replace('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', '', $streetAddress);
656 }
657
658 // get street number and suffix.
659 $matches = [];
660 //alter street number/suffix handling so that we accept -digit
661 if (preg_match('/^[A-Za-z0-9]+([\S]+)/', $streetAddress, $matches)) {
662 // check that $matches[0] is numeric, else assume no street number
663 if (preg_match('/^(\d+)/', $matches[0])) {
664 $streetNumAndSuffix = $matches[0];
665
666 // get street number.
667 $matches = [];
668 if (preg_match('/^(\d+)/', $streetNumAndSuffix, $matches)) {
669 $parseFields['street_number'] = $matches[0];
670 $suffix = preg_replace('/^(\d+)/', '', $streetNumAndSuffix);
671 $parseFields['street_number_suffix'] = trim($suffix);
672 }
673
674 // unset from main street address.
675 $streetAddress = preg_replace('/^[A-Za-z0-9]+([\S]+)/', '', $streetAddress);
676 $streetAddress = trim($streetAddress);
677 }
678 }
679 elseif (preg_match('/^(\d+)/', $streetAddress, $matches)) {
680 $parseFields['street_number'] = $matches[0];
681 // unset from main street address.
682 $streetAddress = preg_replace('/^(\d+)/', '', $streetAddress);
683 $streetAddress = trim($streetAddress);
684 }
685
686 // If street number is too large, we cannot store it.
687 if ($parseFields['street_number'] > CRM_Utils_Type::INT_MAX) {
688 return $emptyParseFields;
689 }
690
691 // suffix might be like 1/2
692 $matches = [];
693 if (preg_match('/^\d\/\d/', $streetAddress, $matches)) {
694 $parseFields['street_number_suffix'] .= $matches[0];
695
696 // unset from main street address.
697 $streetAddress = preg_replace('/^\d+\/\d+/', '', $streetAddress);
698 $streetAddress = trim($streetAddress);
699 }
700
701 // now get the street unit.
702 // supportable street unit formats.
703 $streetUnitFormats = [
704 'APT',
705 'APARTMENT',
706 'BSMT',
707 'BASEMENT',
708 'BLDG',
709 'BUILDING',
710 'DEPT',
711 'DEPARTMENT',
712 'FL',
713 'FLOOR',
714 'FRNT',
715 'FRONT',
716 'HNGR',
717 'HANGER',
718 'LBBY',
719 'LOBBY',
720 'LOWR',
721 'LOWER',
722 'OFC',
723 'OFFICE',
724 'PH',
725 'PENTHOUSE',
726 'TRLR',
727 'TRAILER',
728 'UPPR',
729 'RM',
730 'ROOM',
731 'SIDE',
732 'SLIP',
733 'KEY',
734 'LOT',
735 'PIER',
736 'REAR',
737 'SPC',
738 'SPACE',
739 'STOP',
740 'STE',
741 'SUITE',
742 'UNIT',
743 '#',
744 ];
745
746 // overwriting $streetUnitFormats for 'en_CA' and 'fr_CA' locale
747 if (in_array($locale, [
748 'en_CA',
749 'fr_CA',
750 ])) {
751 $streetUnitFormats = ['APT', 'APP', 'SUITE', 'BUREAU', 'UNIT'];
752 }
753 //@todo per CRM-14459 this regex picks up words with the string in them - e.g APT picks up
754 //Captain - presuming fixing regex (& adding test) to ensure a-z does not preced string will fix
755 $streetUnitPreg = '/(' . implode('|\s', $streetUnitFormats) . ')(.+)?/i';
756 $matches = [];
757 if (preg_match($streetUnitPreg, $streetAddress, $matches)) {
758 $parseFields['street_unit'] = trim($matches[0]);
759 $streetAddress = str_replace($matches[0], '', $streetAddress);
760 $streetAddress = trim($streetAddress);
761 }
762
763 // consider remaining string as street name.
764 $parseFields['street_name'] = $streetAddress;
765
766 //run parsed fields through stripSpaces to clean
767 foreach ($parseFields as $parseField => $value) {
768 $parseFields[$parseField] = CRM_Utils_String::stripSpaces($value);
769 }
770 //CRM-14459 if the field is too long we should assume it didn't get it right & skip rather than allow
771 // the DB to fatal
772 $fields = CRM_Core_BAO_Address::fields();
773 foreach ($fields as $fieldname => $field) {
774 if (!empty($field['maxlength']) && strlen(CRM_Utils_Array::value($fieldname, $parseFields)) > $field['maxlength']) {
775 return $emptyParseFields;
776 }
777 }
778
779 return $parseFields;
780 }
781
782 /**
783 * Determines if the specified locale is
784 * supported by address parsing.
785 * If no locale is specified then it
786 * will check the default configured locale.
787 *
788 * locales supported include:
789 * en_US - http://pe.usps.com/cpim/ftp/pubs/pub28/pub28.pdf
790 * en_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp
791 * fr_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-f.asp
792 * NB: common use of comma after street number also supported
793 *
794 * @param string $locale
795 * The locale to be checked
796 *
797 * @return bool
798 */
799 public static function isSupportedParsingLocale($locale = NULL) {
800 if (!$locale) {
801 $config = CRM_Core_Config::singleton();
802 $locale = $config->lcMessages;
803 }
804
805 $parsingSupportedLocales = ['en_US', 'en_CA', 'fr_CA'];
806
807 if (in_array($locale, $parsingSupportedLocales)) {
808 return TRUE;
809 }
810
811 return FALSE;
812 }
813
814 /**
815 * Validate the address fields based on the address options enabled.
816 * in the Address Settings
817 *
818 * @param array $fields
819 * An array of importable/exportable contact fields.
820 *
821 * @return array
822 * an array of contact fields and only the enabled address options
823 */
824 public static function validateAddressOptions($fields) {
825 static $addressOptions = NULL;
826 if (!$addressOptions) {
827 $addressOptions = CRM_Core_BAO_Setting::valueOptions(
828 CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
829 'address_options'
830 );
831 }
832
833 if (is_array($fields) && !empty($fields)) {
834 foreach ($addressOptions as $key => $value) {
835 if (!$value && isset($fields[$key])) {
836 unset($fields[$key]);
837 }
838 }
839 }
840 return $fields;
841 }
842
843 /**
844 * Check if current address is used by any other contacts.
845 *
846 * @param int $addressId
847 * Address id.
848 *
849 * @return int
850 * count of contacts that use this shared address
851 */
852 public static function checkContactSharedAddress($addressId) {
853 $query = 'SELECT count(id) FROM civicrm_address WHERE master_id = %1';
854 return CRM_Core_DAO::singleValueQuery($query, [1 => [$addressId, 'Integer']]);
855 }
856
857 /**
858 * Check if current address fields are shared with any other address.
859 *
860 * @param array $fields
861 * Address fields in profile.
862 * @param int $contactId
863 * Contact id.
864 *
865 */
866 public static function checkContactSharedAddressFields(&$fields, $contactId) {
867 if (!$contactId || !is_array($fields) || empty($fields)) {
868 return;
869 }
870
871 $sharedLocations = [];
872
873 $query = "
874 SELECT is_primary,
875 location_type_id
876 FROM civicrm_address
877 WHERE contact_id = %1
878 AND master_id IS NOT NULL";
879
880 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$contactId, 'Positive']]);
881 while ($dao->fetch()) {
882 $sharedLocations[$dao->location_type_id] = $dao->location_type_id;
883 if ($dao->is_primary) {
884 $sharedLocations['Primary'] = 'Primary';
885 }
886 }
887
888 //no need to process further.
889 if (empty($sharedLocations)) {
890 return;
891 }
892
893 $addressFields = [
894 'city',
895 'county',
896 'country',
897 'geo_code_1',
898 'geo_code_2',
899 'postal_code',
900 'address_name',
901 'state_province',
902 'street_address',
903 'postal_code_suffix',
904 'supplemental_address_1',
905 'supplemental_address_2',
906 'supplemental_address_3',
907 ];
908
909 foreach ($fields as $name => & $values) {
910 if (!is_array($values) || empty($values)) {
911 continue;
912 }
913
914 $nameVal = explode('-', $values['name']);
915 $fldName = $nameVal[0] ?? NULL;
916 $locType = $nameVal[1] ?? NULL;
917 if (!empty($values['location_type_id'])) {
918 $locType = $values['location_type_id'];
919 }
920
921 if (in_array($fldName, $addressFields) &&
922 in_array($locType, $sharedLocations)
923 ) {
924 $values['is_shared'] = TRUE;
925 }
926 }
927 }
928
929 /**
930 * Fix the shared address if address is already shared
931 * or if address will be shared with itself.
932 *
933 * @param array $params
934 * Associated array of address params.
935 */
936 public static function fixSharedAddress(&$params) {
937 // if address master address is shared, use its master (prevent chaining 1) CRM-21214
938 $masterMasterId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Address', $params['master_id'], 'master_id');
939 if ($masterMasterId > 0) {
940 $params['master_id'] = $masterMasterId;
941 }
942
943 // prevent an endless chain between two shared addresses (prevent chaining 3) CRM-21214
944 if (CRM_Utils_Array::value('id', $params) == $params['master_id']) {
945 $params['master_id'] = NULL;
946 CRM_Core_Session::setStatus(ts("You can't connect an address to itself"), '', 'warning');
947 }
948 }
949
950 /**
951 * Update the shared addresses if master address is modified.
952 *
953 * @param int $addressId
954 * Address id.
955 * @param array $params
956 * Associated array of address params.
957 */
958 public static function processSharedAddress($addressId, $params) {
959 $query = 'SELECT id, contact_id FROM civicrm_address WHERE master_id = %1';
960 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$addressId, 'Integer']]);
961
962 // legacy - for api backward compatibility
963 if (!isset($params['add_relationship']) && isset($params['update_current_employer'])) {
964 // warning
965 CRM_Core_Error::deprecatedFunctionWarning('update_current_employer is deprecated, use add_relationship instead');
966 $params['add_relationship'] = $params['update_current_employer'];
967 }
968
969 // Default to TRUE if not set to maintain api backward compatibility.
970 $createRelationship = $params['add_relationship'] ?? TRUE;
971
972 // unset contact id
973 $skipFields = ['is_primary', 'location_type_id', 'is_billing', 'contact_id'];
974 if (isset($params['master_id']) && !CRM_Utils_System::isNull($params['master_id'])) {
975 if ($createRelationship) {
976 // call the function to create a relationship for the new shared address
977 self::processSharedAddressRelationship($params['master_id'], $params['contact_id']);
978 }
979 }
980 else {
981 // else no new shares will be created, only update shared addresses
982 $skipFields[] = 'master_id';
983 }
984 foreach ($skipFields as $value) {
985 unset($params[$value]);
986 }
987
988 $addressDAO = new CRM_Core_DAO_Address();
989 while ($dao->fetch()) {
990 // call the function to update the relationship
991 if ($createRelationship && isset($params['master_id']) && !CRM_Utils_System::isNull($params['master_id'])) {
992 self::processSharedAddressRelationship($params['master_id'], $dao->contact_id);
993 }
994 $addressDAO->copyValues($params);
995 $addressDAO->id = $dao->id;
996 $addressDAO->save();
997 $addressDAO->copyCustomFields($addressId, $addressDAO->id);
998 }
999 }
1000
1001 /**
1002 * Merge contacts with the Same address to get one shared label.
1003 * @param array $rows
1004 * Array[contact_id][contactDetails].
1005 */
1006 public static function mergeSameAddress(&$rows) {
1007 $uniqueAddress = [];
1008 foreach (array_keys($rows) as $rowID) {
1009 // load complete address as array key
1010 $address = trim($rows[$rowID]['street_address'])
1011 . trim($rows[$rowID]['city'])
1012 . trim($rows[$rowID]['state_province'])
1013 . trim($rows[$rowID]['postal_code'])
1014 . trim($rows[$rowID]['country']);
1015 if (isset($rows[$rowID]['last_name'])) {
1016 $name = $rows[$rowID]['last_name'];
1017 }
1018 else {
1019 $name = $rows[$rowID]['display_name'];
1020 }
1021
1022 // CRM-15120
1023 $formatted = [
1024 'first_name' => $rows[$rowID]['first_name'],
1025 'individual_prefix' => $rows[$rowID]['individual_prefix'],
1026 ];
1027 $format = Civi::settings()->get('display_name_format');
1028 $firstNameWithPrefix = CRM_Utils_Address::format($formatted, $format, FALSE, FALSE);
1029 $firstNameWithPrefix = trim($firstNameWithPrefix);
1030
1031 // fill uniqueAddress array with last/first name tree
1032 if (isset($uniqueAddress[$address])) {
1033 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1034 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1035 // drop unnecessary rows
1036 unset($rows[$rowID]);
1037 // this is the first listing at this address
1038 }
1039 else {
1040 $uniqueAddress[$address]['ID'] = $rowID;
1041 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1042 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1043 }
1044 }
1045 foreach ($uniqueAddress as $address => $data) {
1046 // copy data back to $rows
1047 $count = 0;
1048 // one last name list per row
1049 foreach ($data['names'] as $last_name => $first_names) {
1050 // too many to list
1051 if ($count > 2) {
1052 break;
1053 }
1054 if (count($first_names) == 1) {
1055 $family = $first_names[current(array_keys($first_names))]['addressee_display'];
1056 }
1057 else {
1058 // collapse the tree to summarize
1059 $family = trim(implode(" & ", array_keys($first_names)) . " " . $last_name);
1060 }
1061 if ($count) {
1062 $processedNames .= "\n" . $family;
1063 }
1064 else {
1065 // build display_name string
1066 $processedNames = $family;
1067 }
1068 $count++;
1069 }
1070 $rows[$data['ID']]['addressee'] = $rows[$data['ID']]['addressee_display'] = $rows[$data['ID']]['display_name'] = $processedNames;
1071 }
1072 }
1073
1074 /**
1075 * Create relationship between contacts who share an address.
1076 *
1077 * Note that currently we create relationship between
1078 * Individual + Household and Individual + Organization
1079 *
1080 * @param int $masterAddressId
1081 * Master address id.
1082 * @param int $currentContactId
1083 * Current contact id.
1084 */
1085 public static function processSharedAddressRelationship($masterAddressId, $currentContactId) {
1086 // get the contact type of contact being edited / created
1087 $currentContactType = CRM_Contact_BAO_Contact::getContactType($currentContactId);
1088
1089 // if current contact is not of type individual return
1090 if ($currentContactType != 'Individual') {
1091 return;
1092 }
1093
1094 // get the contact id and contact type of shared contact
1095 // check the contact type of shared contact, return if it is of type Individual
1096 $query = 'SELECT cc.id, cc.contact_type
1097 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1098 WHERE ca.id = %1';
1099
1100 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$masterAddressId, 'Integer']]);
1101 $dao->fetch();
1102
1103 // master address contact needs to be Household or Organization, otherwise return
1104 if ($dao->contact_type == 'Individual') {
1105 return;
1106 }
1107 $sharedContactType = $dao->contact_type;
1108 $sharedContactId = $dao->id;
1109
1110 // create relationship between ontacts who share an address
1111 if ($sharedContactType == 'Organization') {
1112 return CRM_Contact_BAO_Contact_Utils::createCurrentEmployerRelationship($currentContactId, $sharedContactId);
1113 }
1114
1115 // get the relationship type id of "Household Member of"
1116 $relTypeId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', 'Household Member of', 'id', 'name_a_b');
1117
1118 if (!$relTypeId) {
1119 throw new CRM_Core_Exception(ts("You seem to have deleted the relationship type 'Household Member of'"));
1120 }
1121
1122 $relParam = [
1123 'is_active' => TRUE,
1124 'relationship_type_id' => $relTypeId,
1125 'contact_id_a' => $currentContactId,
1126 'contact_id_b' => $sharedContactId,
1127 ];
1128
1129 // If already there is a relationship record of $relParam criteria, avoid creating relationship again or else
1130 // it will casue CRM-16588 as the Duplicate Relationship Exception will revert other contact field values on update
1131 if (CRM_Contact_BAO_Relationship::checkDuplicateRelationship($relParam, $currentContactId, $sharedContactId)) {
1132 return;
1133 }
1134
1135 try {
1136 // create relationship
1137 civicrm_api3('relationship', 'create', $relParam);
1138 }
1139 catch (CiviCRM_API3_Exception $e) {
1140 // We catch and ignore here because this has historically been a best-effort relationship create call.
1141 // presumably it could refuse due to duplication or similar and we would ignore that.
1142 }
1143 }
1144
1145 /**
1146 * Check and set the status for shared address delete.
1147 *
1148 * @param int $addressId
1149 * Address id.
1150 * @param int $contactId
1151 * Contact id.
1152 * @param bool $returnStatus
1153 * By default false.
1154 *
1155 * @return string
1156 */
1157 public static function setSharedAddressDeleteStatus($addressId = NULL, $contactId = NULL, $returnStatus = FALSE) {
1158 // check if address that is being deleted has any shared
1159 if ($addressId) {
1160 $entityId = $addressId;
1161 $query = 'SELECT cc.id, cc.display_name
1162 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1163 WHERE ca.master_id = %1';
1164 }
1165 else {
1166 $entityId = $contactId;
1167 $query = 'SELECT cc.id, cc.display_name
1168 FROM civicrm_address ca1
1169 INNER JOIN civicrm_address ca2 ON ca1.id = ca2.master_id
1170 INNER JOIN civicrm_contact cc ON ca2.contact_id = cc.id
1171 WHERE ca1.contact_id = %1';
1172 }
1173
1174 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$entityId, 'Integer']]);
1175
1176 $deleteStatus = [];
1177 $sharedContactList = [];
1178 $statusMessage = NULL;
1179 $addressCount = 0;
1180 while ($dao->fetch()) {
1181 if (empty($deleteStatus)) {
1182 $deleteStatus[] = ts('The following contact(s) have address records which were shared with the address you removed from this contact. These address records are no longer shared - but they have not been removed or altered.');
1183 }
1184
1185 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$dao->id}");
1186 $sharedContactList[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1187 $deleteStatus[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1188
1189 $addressCount++;
1190 }
1191
1192 if (!empty($deleteStatus)) {
1193 $statusMessage = implode('<br/>', $deleteStatus) . '<br/>';
1194 }
1195
1196 if (!$returnStatus) {
1197 CRM_Core_Session::setStatus($statusMessage, '', 'info');
1198 }
1199 else {
1200 return [
1201 'contactList' => $sharedContactList,
1202 'count' => $addressCount,
1203 ];
1204 }
1205 }
1206
1207 /**
1208 * Call common delete function.
1209 *
1210 * @param int $id
1211 *
1212 * @return bool
1213 */
1214 public static function del($id) {
1215 return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('Address', $id);
1216 }
1217
1218 /**
1219 * Get options for a given address field.
1220 * @see CRM_Core_DAO::buildOptions
1221 *
1222 * TODO: Should we always assume chainselect? What fn should be responsible for controlling that flow?
1223 * TODO: In context of chainselect, what to return if e.g. a country has no states?
1224 *
1225 * @param string $fieldName
1226 * @param string $context
1227 * @see CRM_Core_DAO::buildOptionsContext
1228 * @param array $props
1229 * whatever is known about this dao object.
1230 *
1231 * @return array|bool
1232 */
1233 public static function buildOptions($fieldName, $context = NULL, $props = []) {
1234 $params = [];
1235 // Special logic for fields whose options depend on context or properties
1236 switch ($fieldName) {
1237 // Filter state_province list based on chosen country or site defaults
1238 case 'state_province_id':
1239 case 'state_province_name':
1240 case 'state_province':
1241 // change $fieldName to DB specific names.
1242 $fieldName = 'state_province_id';
1243 if (empty($props['country_id']) && $context !== 'validate') {
1244 $config = CRM_Core_Config::singleton();
1245 if (!empty($config->provinceLimit)) {
1246 $props['country_id'] = $config->provinceLimit;
1247 }
1248 else {
1249 $props['country_id'] = $config->defaultContactCountry;
1250 }
1251 }
1252 if (!empty($props['country_id'])) {
1253 if (!CRM_Utils_Rule::commaSeparatedIntegers(implode(',', (array) $props['country_id']))) {
1254 throw new CRM_Core_Exception(ts('Province limit or default country setting is incorrect'));
1255 }
1256 $params['condition'] = 'country_id IN (' . implode(',', (array) $props['country_id']) . ')';
1257 }
1258 break;
1259
1260 // Filter country list based on site defaults
1261 case 'country_id':
1262 case 'country':
1263 // change $fieldName to DB specific names.
1264 $fieldName = 'country_id';
1265 if ($context != 'get' && $context != 'validate') {
1266 $config = CRM_Core_Config::singleton();
1267 if (!empty($config->countryLimit) && is_array($config->countryLimit)) {
1268 if (!CRM_Utils_Rule::commaSeparatedIntegers(implode(',', $config->countryLimit))) {
1269 throw new CRM_Core_Exception(ts('Available Country setting is incorrect'));
1270 }
1271 $params['condition'] = 'id IN (' . implode(',', $config->countryLimit) . ')';
1272 }
1273 }
1274 break;
1275
1276 // Filter county list based on chosen state
1277 case 'county_id':
1278 if (!empty($props['state_province_id'])) {
1279 if (!CRM_Utils_Rule::commaSeparatedIntegers(implode(',', (array) $props['state_province_id']))) {
1280 throw new CRM_Core_Exception(ts('Can only accept Integers for state_province_id filtering'));
1281 }
1282 $params['condition'] = 'state_province_id IN (' . implode(',', (array) $props['state_province_id']) . ')';
1283 }
1284 break;
1285
1286 // Not a real field in this entity
1287 case 'world_region':
1288 case 'worldregion':
1289 case 'worldregion_id':
1290 return CRM_Core_BAO_Country::buildOptions('region_id', $context, $props);
1291 }
1292 return CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context);
1293 }
1294
1295 /**
1296 * Add data from the configured geocoding provider.
1297 *
1298 * Generally this means latitude & longitude data.
1299 *
1300 * @param array $params
1301 * @return bool
1302 * TRUE if params could be passed to a provider, else FALSE.
1303 */
1304 public static function addGeocoderData(&$params) {
1305 try {
1306 $provider = CRM_Utils_GeocodeProvider::getConfiguredProvider();
1307 }
1308 catch (CRM_Core_Exception $e) {
1309 return FALSE;
1310 }
1311 $provider::format($params);
1312 return TRUE;
1313 }
1314
1315 /**
1316 * Create multiple addresses using legacy methodology.
1317 *
1318 * @param array $params
1319 * @param bool $fixAddress
1320 *
1321 * @return array|null
1322 */
1323 public static function legacyCreate(array $params, bool $fixAddress) {
1324 if (!isset($params['address']) || !is_array($params['address'])) {
1325 return NULL;
1326 }
1327 CRM_Core_BAO_Block::sortPrimaryFirst($params['address']);
1328 $contactId = NULL;
1329
1330 $updateBlankLocInfo = CRM_Utils_Array::value('updateBlankLocInfo', $params, FALSE);
1331 $contactId = $params['contact_id'];
1332 //get all the addresses for this contact
1333 $addresses = self::allAddress($contactId);
1334
1335 $isPrimary = $isBilling = TRUE;
1336 $blocks = [];
1337 foreach ($params['address'] as $key => $value) {
1338 if (!is_array($value)) {
1339 continue;
1340 }
1341
1342 $addressExists = self::dataExists($value);
1343 if (empty($value['id'])) {
1344 if (!empty($addresses) && !empty($value['location_type_id']) && array_key_exists($value['location_type_id'], $addresses)) {
1345 $value['id'] = $addresses[$value['location_type_id']];
1346 }
1347 }
1348
1349 // Note there could be cases when address info already exist ($value[id] is set) for a contact/entity
1350 // BUT info is not present at this time, and therefore we should be really careful when deleting the block.
1351 // $updateBlankLocInfo will help take appropriate decision. CRM-5969
1352 if (isset($value['id']) && !$addressExists && $updateBlankLocInfo) {
1353 //delete the existing record
1354 CRM_Core_BAO_Block::blockDelete('Address', ['id' => $value['id']]);
1355 continue;
1356 }
1357 elseif (!$addressExists) {
1358 continue;
1359 }
1360
1361 if ($isPrimary && !empty($value['is_primary'])) {
1362 $isPrimary = FALSE;
1363 }
1364 else {
1365 $value['is_primary'] = 0;
1366 }
1367
1368 if ($isBilling && !empty($value['is_billing'])) {
1369 $isBilling = FALSE;
1370 }
1371 else {
1372 $value['is_billing'] = 0;
1373 }
1374
1375 if (empty($value['manual_geo_code'])) {
1376 $value['manual_geo_code'] = 0;
1377 }
1378 $value['contact_id'] = $contactId;
1379 $blocks[] = self::add($value, $fixAddress);
1380 }
1381 return $blocks;
1382 }
1383
1384 }