3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 * This is class to handle address related functions.
21 class CRM_Core_BAO_Address
extends CRM_Core_DAO_Address
{
22 use CRM_Contact_AccessTrait
;
25 * Takes an associative array and creates a address.
27 * @param array $params
28 * (reference ) an assoc array of name/value pairs.
29 * @param bool $fixAddress
30 * True if you need to fix (format) address values.
31 * before inserting in db
33 * @return array|NULL|self
34 * array of created address
36 public static function create(&$params, $fixAddress = TRUE) {
37 if (!isset($params['address']) ||
!is_array($params['address'])) {
38 return self
::add($params, $fixAddress);
40 CRM_Core_Error
::deprecatedFunctionWarning('Use legacyCreate if not doing a single crud action');
41 return self
::legacyCreate($params, $fixAddress);
45 * Takes an associative array and adds address.
47 * @param array $params
48 * (reference ) an assoc array of name/value pairs.
49 * @param bool $fixAddress
50 * True if you need to fix (format) address values.
51 * before inserting in db
53 * @return CRM_Core_BAO_Address|null
55 public static function add(&$params, $fixAddress = FALSE) {
57 $address = new CRM_Core_DAO_Address();
58 $checkPermissions = $params['check_permissions'] ??
TRUE;
60 // fixAddress mode to be done
62 CRM_Core_BAO_Address
::fixAddress($params);
65 $hook = empty($params['id']) ?
'create' : 'edit';
66 CRM_Utils_Hook
::pre($hook, 'Address', CRM_Utils_Array
::value('id', $params), $params);
68 CRM_Core_BAO_Block
::handlePrimary($params, get_class());
70 // (prevent chaining 1 and 3) CRM-21214
71 if (isset($params['master_id']) && !CRM_Utils_System
::isNull($params['master_id'])) {
72 self
::fixSharedAddress($params);
75 $address->copyValues($params);
79 // first get custom field from master address if any
80 if (isset($params['master_id']) && !CRM_Utils_System
::isNull($params['master_id'])) {
81 $address->copyCustomFields($params['master_id'], $address->id
);
84 if (isset($params['custom'])) {
85 $addressCustom = $params['custom'];
88 $customFields = CRM_Core_BAO_CustomField
::getFields('Address', FALSE, TRUE, NULL, NULL,
89 FALSE, FALSE, $checkPermissions ? CRM_Core_Permission
::EDIT
: FALSE);
91 if (!empty($customFields)) {
92 $addressCustom = CRM_Core_BAO_CustomField
::postProcess($params,
100 if (!empty($addressCustom)) {
101 CRM_Core_BAO_CustomValueTable
::store($addressCustom, 'civicrm_address', $address->id
, $hook);
104 // call the function to sync shared address and create relationships
105 // if address is already shared, share master_id with all children and update relationships accordingly
106 // (prevent chaining 2) CRM-21214
107 self
::processSharedAddress($address->id
, $params, $hook);
109 // lets call the post hook only after we've done all the follow on processing
110 CRM_Utils_Hook
::post($hook, 'Address', $address->id
, $address);
117 * Format the address params to have reasonable values.
119 * @param array $params
120 * (reference ) an assoc array of name/value pairs.
122 public static function fixAddress(&$params) {
123 if (!empty($params['billing_street_address'])) {
124 //Check address is coming from online contribution / registration page
127 'street_address' => 'billing_street_address',
128 'city' => 'billing_city',
129 'postal_code' => 'billing_postal_code',
130 'state_province' => 'billing_state_province',
131 'state_province_id' => 'billing_state_province_id',
132 'country' => 'billing_country',
133 'country_id' => 'billing_country_id',
136 foreach ($billing as $key => $val) {
137 if ($value = CRM_Utils_Array
::value($val, $params)) {
138 if (!empty($params[$key])) {
139 unset($params[$val]);
142 //add new key and removed old
143 $params[$key] = $value;
144 unset($params[$val]);
150 /* Split the zip and +4, if it's in US format */
151 if (!empty($params['postal_code']) &&
152 preg_match('/^(\d{4,5})[+-](\d{4})$/',
153 $params['postal_code'],
157 $params['postal_code'] = $match[1];
158 $params['postal_code_suffix'] = $match[2];
161 // add country id if not set
162 if ((!isset($params['country_id']) ||
!is_numeric($params['country_id'])) &&
163 isset($params['country'])
165 $country = new CRM_Core_DAO_Country();
166 $country->name
= $params['country'];
167 if (!$country->find(TRUE)) {
168 $country->name
= NULL;
169 $country->iso_code
= $params['country'];
170 $country->find(TRUE);
172 $params['country_id'] = $country->id
;
175 // add state_id if state is set
176 if ((!isset($params['state_province_id']) ||
!is_numeric($params['state_province_id']))
177 && isset($params['state_province'])
179 if (!empty($params['state_province'])) {
180 $state_province = new CRM_Core_DAO_StateProvince();
181 $state_province->name
= $params['state_province'];
183 // add country id if present
184 if (!empty($params['country_id'])) {
185 $state_province->country_id
= $params['country_id'];
188 if (!$state_province->find(TRUE)) {
189 unset($state_province->name
);
190 $state_province->abbreviation
= $params['state_province'];
191 $state_province->find(TRUE);
193 $params['state_province_id'] = $state_province->id
;
194 if (empty($params['country_id'])) {
195 // set this here since we have it
196 $params['country_id'] = $state_province->country_id
;
200 $params['state_province_id'] = 'null';
204 // add county id if county is set
206 if ((!isset($params['county_id']) ||
!is_numeric($params['county_id']))
207 && isset($params['county']) && !empty($params['county'])
209 $county = new CRM_Core_DAO_County();
210 $county->name
= $params['county'];
212 if (isset($params['state_province_id'])) {
213 $county->state_province_id
= $params['state_province_id'];
216 if ($county->find(TRUE)) {
217 $params['county_id'] = $county->id
;
221 // currently copy values populates empty fields with the string "null"
222 // and hence need to check for the string null
223 if (isset($params['state_province_id']) &&
224 is_numeric($params['state_province_id']) &&
225 (!isset($params['country_id']) ||
empty($params['country_id']))
227 // since state id present and country id not present, hence lets populate it
228 // jira issue http://issues.civicrm.org/jira/browse/CRM-56
229 $params['country_id'] = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_StateProvince',
230 $params['state_province_id'],
235 //special check to ignore non numeric values if they are not
236 //detected by formRule(sometimes happens due to internet latency), also allow user to unselect state/country
237 if (isset($params['state_province_id'])) {
238 if (empty($params['state_province_id'])) {
239 $params['state_province_id'] = 'null';
241 elseif (!is_numeric($params['state_province_id']) ||
242 ((int ) $params['state_province_id'] < 1000)
244 // CRM-3393 ( the hacky 1000 check)
245 $params['state_province_id'] = 'null';
249 if (isset($params['country_id'])) {
250 if (empty($params['country_id'])) {
251 $params['country_id'] = 'null';
253 elseif (!is_numeric($params['country_id']) ||
254 ((int ) $params['country_id'] < 1000)
256 // CRM-3393 ( the hacky 1000 check)
257 $params['country_id'] = 'null';
261 // add state and country names from the ids
262 if (isset($params['state_province_id']) && is_numeric($params['state_province_id'])) {
263 $params['state_province'] = CRM_Core_PseudoConstant
::stateProvinceAbbreviation($params['state_province_id']);
266 if (isset($params['country_id']) && is_numeric($params['country_id'])) {
267 $params['country'] = CRM_Core_PseudoConstant
::country($params['country_id']);
270 $asp = Civi
::settings()->get('address_standardization_provider');
271 // clean up the address via USPS web services if enabled
272 if ($asp === 'USPS' &&
273 $params['country_id'] == 1228
275 CRM_Utils_Address_USPS
::checkAddress($params);
277 // do street parsing again if enabled, since street address might have changed
278 $parseStreetAddress = CRM_Utils_Array
::value(
279 'street_address_parsing',
280 CRM_Core_BAO_Setting
::valueOptions(
281 CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
287 if ($parseStreetAddress && !empty($params['street_address'])) {
288 foreach (['street_number', 'street_name', 'street_unit', 'street_number_suffix'] as $fld) {
289 unset($params[$fld]);
291 // main parse string.
292 $parseString = $params['street_address'] ??
NULL;
293 $parsedFields = CRM_Core_BAO_Address
::parseStreetAddress($parseString);
295 // merge parse address in to main address block.
296 $params = array_merge($params, $parsedFields);
299 // skip_geocode is an optional parameter through the api.
300 // manual_geo_code is on the contact edit form. They do the same thing....
301 if (empty($params['skip_geocode']) && empty($params['manual_geo_code'])) {
302 self
::addGeocoderData($params);
308 * Check if there is data to create the object.
310 * @param array $params
311 * (reference ) an assoc array of name/value pairs.
315 public static function dataExists(&$params) {
316 //check if location type is set if not return false
317 if (!isset($params['location_type_id'])) {
321 $config = CRM_Core_Config
::singleton();
322 foreach ($params as $name => $value) {
323 if (in_array($name, [
334 elseif (!CRM_Utils_System
::isNull($value)) {
335 // name could be country or country id
336 if (substr($name, 0, 7) == 'country') {
337 // make sure its different from the default country
339 $defaultCountry = CRM_Core_BAO_Country
::defaultContactCountry();
341 $defaultCountryName = CRM_Core_BAO_Country
::defaultContactCountryName();
343 if ($defaultCountry) {
344 if ($value == $defaultCountry ||
345 $value == $defaultCountryName ||
346 $value == $config->defaultContactCountry
355 // return if null default
369 * Given the list of params in the params array, fetch the object
370 * and store the values in the values array
372 * @param array $entityBlock
373 * Associated array of fields.
374 * @param bool $microformat
375 * If microformat output is required.
376 * @param int|string $fieldName conditional field name
379 * array with address fields
381 public static function &getValues($entityBlock, $microformat = FALSE, $fieldName = 'contact_id') {
382 if (empty($entityBlock)) {
386 $address = new CRM_Core_BAO_Address();
388 if (empty($entityBlock['entity_table'])) {
389 $address->$fieldName = $entityBlock[$fieldName] ??
NULL;
393 $addressIds = self
::allEntityAddress($entityBlock);
395 if (!empty($addressIds[1])) {
396 $address->id
= $addressIds[1];
402 if (isset($entityBlock['is_billing']) && $entityBlock['is_billing'] == 1) {
403 $address->orderBy('is_billing desc, id');
406 //get primary address as a first block.
407 $address->orderBy('is_primary desc, id');
412 $locationTypes = CRM_Core_PseudoConstant
::get('CRM_Core_DAO_Address', 'location_type_id');
414 while ($address->fetch()) {
415 // deprecate reference.
417 foreach (['state', 'state_name', 'country', 'world_region'] as $fld) {
418 if (isset($address->$fld)) {
419 unset($address->$fld);
423 $stree = $address->street_address
;
425 CRM_Core_DAO
::storeValues($address, $values);
427 // add state and country information: CRM-369
428 if (!empty($address->location_type_id
)) {
429 $values['location_type'] = $locationTypes[$address->location_type_id
] ??
NULL;
431 if (!empty($address->state_province_id
)) {
432 $address->state
= CRM_Core_PseudoConstant
::stateProvinceAbbreviation($address->state_province_id
, FALSE);
433 $address->state_name
= CRM_Core_PseudoConstant
::stateProvince($address->state_province_id
, FALSE);
434 $values['state_province_abbreviation'] = $address->state
;
435 $values['state_province'] = $address->state_name
;
438 if (!empty($address->country_id
)) {
439 $address->country
= CRM_Core_PseudoConstant
::country($address->country_id
);
440 $values['country'] = $address->country
;
443 $regionId = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_Country', $address->country_id
, 'region_id');
444 $values['world_region'] = CRM_Core_PseudoConstant
::worldregion($regionId);
447 $address->addDisplay($microformat);
449 $values['display'] = $address->display
;
450 $values['display_text'] = $address->display_text
;
452 if (isset($address->master_id
) && !CRM_Utils_System
::isNull($address->master_id
)) {
453 $values['use_shared_address'] = 1;
456 $addresses[$count] = $values;
458 //There should never be more than one primary blocks, hence set is_primary = 0 other than first
459 // Calling functions expect the key is_primary to be set, so do not unset it here!
461 $addresses[$count]['is_primary'] = 0;
471 * Add the formatted address to $this-> display.
473 * @param bool $microformat
474 * Unexplained parameter that I've always wondered about.
476 public function addDisplay($microformat = FALSE) {
478 // added this for CRM 1200
479 'address_id' => $this->id
,
481 'address_name' => str_replace('\ 1', ' ', $this->name
),
482 'street_address' => $this->street_address
,
483 'supplemental_address_1' => $this->supplemental_address_1
,
484 'supplemental_address_2' => $this->supplemental_address_2
,
485 'supplemental_address_3' => $this->supplemental_address_3
,
486 'city' => $this->city
,
487 'state_province_name' => $this->state_name ??
"",
488 'state_province' => $this->state ??
"",
489 'postal_code' => $this->postal_code ??
"",
490 'postal_code_suffix' => $this->postal_code_suffix ??
"",
491 'country' => $this->country ??
"",
492 'world_region' => $this->world_region ??
"",
495 if (isset($this->county_id
) && $this->county_id
) {
496 $fields['county'] = CRM_Core_PseudoConstant
::county($this->county_id
);
499 $fields['county'] = NULL;
502 $this->display
= CRM_Utils_Address
::format($fields, NULL, $microformat);
503 $this->display_text
= CRM_Utils_Address
::format($fields);
507 * Get all the addresses for a specified contact_id, with the primary address being first
512 * @param bool $updateBlankLocInfo
515 * the array of adrress data
517 public static function allAddress($id, $updateBlankLocInfo = FALSE) {
523 SELECT civicrm_address.id as address_id, civicrm_address.location_type_id as location_type_id
524 FROM civicrm_contact, civicrm_address
525 WHERE civicrm_address.contact_id = civicrm_contact.id AND civicrm_contact.id = %1
526 ORDER BY civicrm_address.is_primary DESC, address_id ASC";
527 $params = [1 => [$id, 'Integer']];
530 $dao = CRM_Core_DAO
::executeQuery($query, $params);
532 while ($dao->fetch()) {
533 if ($updateBlankLocInfo) {
534 $addresses[$count++
] = $dao->address_id
;
537 $addresses[$dao->location_type_id
] = $dao->address_id
;
544 * Get all the addresses for a specified location_block id, with the primary address being first
546 * @param array $entityElements
547 * The array containing entity_id and.
551 * the array of adrress data
553 public static function allEntityAddress(&$entityElements) {
555 if (empty($entityElements)) {
559 $entityId = $entityElements['entity_id'];
560 $entityTable = $entityElements['entity_table'];
563 SELECT civicrm_address.id as address_id
564 FROM civicrm_loc_block loc, civicrm_location_type ltype, civicrm_address, {$entityTable} ev
566 AND loc.id = ev.loc_block_id
567 AND civicrm_address.id IN (loc.address_id, loc.address_2_id)
568 AND ltype.id = civicrm_address.location_type_id
569 ORDER BY civicrm_address.is_primary DESC, civicrm_address.location_type_id DESC, address_id ASC ";
571 $params = [1 => [$entityId, 'Integer']];
572 $dao = CRM_Core_DAO
::executeQuery($sql, $params);
574 while ($dao->fetch()) {
575 $addresses[$locationCount] = $dao->address_id
;
582 * Get address sequence.
585 * Array of address sequence.
587 public static function addressSequence() {
588 $addressSequence = CRM_Utils_Address
::sequence(\Civi
::settings()->get('address_format'));
590 $countryState = $cityPostal = FALSE;
591 foreach ($addressSequence as $key => $field) {
593 in_array($field, ['country', 'state_province']) &&
596 $countryState = TRUE;
597 $addressSequence[$key] = 'country_state_province';
600 in_array($field, ['city', 'postal_code']) &&
604 $addressSequence[$key] = 'city_postal_code';
607 in_array($field, ['country', 'state_province', 'city', 'postal_code'])
609 unset($addressSequence[$key]);
613 return $addressSequence;
617 * Parse given street address string in to street_name,
618 * street_unit, street_number and street_number_suffix
619 * eg "54A Excelsior Ave. Apt 1C", or "917 1/2 Elm Street"
621 * NB: civic street formats for en_CA and fr_CA used by default if those locales are active
622 * otherwise en_US format is default action
624 * @param string $streetAddress
625 * Street address including number and apt.
626 * @param string $locale
627 * Locale used to parse address.
630 * parsed fields values.
632 public static function parseStreetAddress($streetAddress, $locale = NULL) {
633 // use 'en_US' for address parsing if the requested locale is not supported.
634 if (!self
::isSupportedParsingLocale($locale)) {
638 $emptyParseFields = $parseFields = [
641 'street_number' => '',
642 'street_number_suffix' => '',
645 if (empty($streetAddress)) {
649 $streetAddress = trim($streetAddress);
652 if (in_array($locale, ['en_CA', 'fr_CA'])
653 && preg_match('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', $streetAddress, $matches)
655 $parseFields['street_unit'] = $matches[1];
656 // unset from rest of street address
657 $streetAddress = preg_replace('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', '', $streetAddress);
660 // get street number and suffix.
662 //alter street number/suffix handling so that we accept -digit
663 if (preg_match('/^[A-Za-z0-9]+([\S]+)/', $streetAddress, $matches)) {
664 // check that $matches[0] is numeric, else assume no street number
665 if (preg_match('/^(\d+)/', $matches[0])) {
666 $streetNumAndSuffix = $matches[0];
668 // get street number.
670 if (preg_match('/^(\d+)/', $streetNumAndSuffix, $matches)) {
671 $parseFields['street_number'] = $matches[0];
672 $suffix = preg_replace('/^(\d+)/', '', $streetNumAndSuffix);
673 $parseFields['street_number_suffix'] = trim($suffix);
676 // unset from main street address.
677 $streetAddress = preg_replace('/^[A-Za-z0-9]+([\S]+)/', '', $streetAddress);
678 $streetAddress = trim($streetAddress);
681 elseif (preg_match('/^(\d+)/', $streetAddress, $matches)) {
682 $parseFields['street_number'] = $matches[0];
683 // unset from main street address.
684 $streetAddress = preg_replace('/^(\d+)/', '', $streetAddress);
685 $streetAddress = trim($streetAddress);
688 // If street number is too large, we cannot store it.
689 if ($parseFields['street_number'] > CRM_Utils_Type
::INT_MAX
) {
690 return $emptyParseFields;
693 // suffix might be like 1/2
695 if (preg_match('/^\d\/\d/', $streetAddress, $matches)) {
696 $parseFields['street_number_suffix'] .= $matches[0];
698 // unset from main street address.
699 $streetAddress = preg_replace('/^\d+\/\d+/', '', $streetAddress);
700 $streetAddress = trim($streetAddress);
703 // now get the street unit.
704 // supportable street unit formats.
705 $streetUnitFormats = [
748 // overwriting $streetUnitFormats for 'en_CA' and 'fr_CA' locale
749 if (in_array($locale, [
753 $streetUnitFormats = ['APT', 'APP', 'SUITE', 'BUREAU', 'UNIT'];
755 //@todo per CRM-14459 this regex picks up words with the string in them - e.g APT picks up
756 //Captain - presuming fixing regex (& adding test) to ensure a-z does not preced string will fix
757 $streetUnitPreg = '/(' . implode('|\s', $streetUnitFormats) . ')(.+)?/i';
759 if (preg_match($streetUnitPreg, $streetAddress, $matches)) {
760 $parseFields['street_unit'] = trim($matches[0]);
761 $streetAddress = str_replace($matches[0], '', $streetAddress);
762 $streetAddress = trim($streetAddress);
765 // consider remaining string as street name.
766 $parseFields['street_name'] = $streetAddress;
768 //run parsed fields through stripSpaces to clean
769 foreach ($parseFields as $parseField => $value) {
770 $parseFields[$parseField] = CRM_Utils_String
::stripSpaces($value);
772 //CRM-14459 if the field is too long we should assume it didn't get it right & skip rather than allow
774 $fields = CRM_Core_BAO_Address
::fields();
775 foreach ($fields as $fieldname => $field) {
776 if (!empty($field['maxlength']) && strlen(CRM_Utils_Array
::value($fieldname, $parseFields)) > $field['maxlength']) {
777 return $emptyParseFields;
785 * Determines if the specified locale is
786 * supported by address parsing.
787 * If no locale is specified then it
788 * will check the default configured locale.
790 * locales supported include:
791 * en_US - http://pe.usps.com/cpim/ftp/pubs/pub28/pub28.pdf
792 * en_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp
793 * fr_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-f.asp
794 * NB: common use of comma after street number also supported
796 * @param string $locale
797 * The locale to be checked
801 public static function isSupportedParsingLocale($locale = NULL) {
803 $config = CRM_Core_Config
::singleton();
804 $locale = $config->lcMessages
;
807 $parsingSupportedLocales = ['en_US', 'en_CA', 'fr_CA'];
809 if (in_array($locale, $parsingSupportedLocales)) {
817 * Validate the address fields based on the address options enabled.
818 * in the Address Settings
820 * @param array $fields
821 * An array of importable/exportable contact fields.
824 * an array of contact fields and only the enabled address options
826 public static function validateAddressOptions($fields) {
827 static $addressOptions = NULL;
828 if (!$addressOptions) {
829 $addressOptions = CRM_Core_BAO_Setting
::valueOptions(
830 CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
835 if (is_array($fields) && !empty($fields)) {
836 foreach ($addressOptions as $key => $value) {
837 if (!$value && isset($fields[$key])) {
838 unset($fields[$key]);
846 * Check if current address is used by any other contacts.
848 * @param int $addressId
852 * count of contacts that use this shared address
854 public static function checkContactSharedAddress($addressId) {
855 $query = 'SELECT count(id) FROM civicrm_address WHERE master_id = %1';
856 return CRM_Core_DAO
::singleValueQuery($query, [1 => [$addressId, 'Integer']]);
860 * Check if current address fields are shared with any other address.
862 * @param array $fields
863 * Address fields in profile.
864 * @param int $contactId
868 public static function checkContactSharedAddressFields(&$fields, $contactId) {
869 if (!$contactId ||
!is_array($fields) ||
empty($fields)) {
873 $sharedLocations = [];
879 WHERE contact_id = %1
880 AND master_id IS NOT NULL";
882 $dao = CRM_Core_DAO
::executeQuery($query, [1 => [$contactId, 'Positive']]);
883 while ($dao->fetch()) {
884 $sharedLocations[$dao->location_type_id
] = $dao->location_type_id
;
885 if ($dao->is_primary
) {
886 $sharedLocations['Primary'] = 'Primary';
890 //no need to process further.
891 if (empty($sharedLocations)) {
905 'postal_code_suffix',
906 'supplemental_address_1',
907 'supplemental_address_2',
908 'supplemental_address_3',
911 foreach ($fields as $name => & $values) {
912 if (!is_array($values) ||
empty($values)) {
916 $nameVal = explode('-', $values['name']);
917 $fldName = $nameVal[0] ??
NULL;
918 $locType = $nameVal[1] ??
NULL;
919 if (!empty($values['location_type_id'])) {
920 $locType = $values['location_type_id'];
923 if (in_array($fldName, $addressFields) &&
924 in_array($locType, $sharedLocations)
926 $values['is_shared'] = TRUE;
932 * Fix the shared address if address is already shared
933 * or if address will be shared with itself.
935 * @param array $params
936 * Associated array of address params.
938 public static function fixSharedAddress(&$params) {
939 // if address master address is shared, use its master (prevent chaining 1) CRM-21214
940 $masterMasterId = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_Address', $params['master_id'], 'master_id');
941 if ($masterMasterId > 0) {
942 $params['master_id'] = $masterMasterId;
945 // prevent an endless chain between two shared addresses (prevent chaining 3) CRM-21214
946 if (CRM_Utils_Array
::value('id', $params) == $params['master_id']) {
947 $params['master_id'] = NULL;
948 CRM_Core_Session
::setStatus(ts("You can't connect an address to itself"), '', 'warning');
953 * Update the shared addresses if master address is modified.
955 * @param int $addressId
957 * @param array $params
958 * Associated array of address params.
959 * @param string $parentOperation Operation being taken on the parent entity.
961 public static function processSharedAddress($addressId, $params, $parentOperation = NULL) {
962 $query = 'SELECT id, contact_id FROM civicrm_address WHERE master_id = %1';
963 $dao = CRM_Core_DAO
::executeQuery($query, [1 => [$addressId, 'Integer']]);
965 // legacy - for api backward compatibility
966 if (!isset($params['add_relationship']) && isset($params['update_current_employer'])) {
968 CRM_Core_Error
::deprecatedFunctionWarning('update_current_employer is deprecated, use add_relationship instead');
969 $params['add_relationship'] = $params['update_current_employer'];
972 // Default to TRUE if not set to maintain api backward compatibility.
973 $createRelationship = $params['add_relationship'] ??
TRUE;
976 $skipFields = ['is_primary', 'location_type_id', 'is_billing', 'contact_id'];
977 if (isset($params['master_id']) && !CRM_Utils_System
::isNull($params['master_id'])) {
978 if ($createRelationship) {
979 // call the function to create a relationship for the new shared address
980 self
::processSharedAddressRelationship($params['master_id'], $params['contact_id']);
984 // else no new shares will be created, only update shared addresses
985 $skipFields[] = 'master_id';
987 foreach ($skipFields as $value) {
988 unset($params[$value]);
991 $addressDAO = new CRM_Core_DAO_Address();
992 while ($dao->fetch()) {
993 // call the function to update the relationship
994 if ($createRelationship && isset($params['master_id']) && !CRM_Utils_System
::isNull($params['master_id'])) {
995 self
::processSharedAddressRelationship($params['master_id'], $dao->contact_id
);
997 $addressDAO->copyValues($params);
998 $addressDAO->id
= $dao->id
;
1000 $addressDAO->copyCustomFields($addressId, $addressDAO->id
, $parentOperation);
1005 * Merge contacts with the Same address to get one shared label.
1006 * @param array $rows
1007 * Array[contact_id][contactDetails].
1009 public static function mergeSameAddress(&$rows) {
1010 $uniqueAddress = [];
1011 foreach (array_keys($rows) as $rowID) {
1012 // load complete address as array key
1013 $address = trim($rows[$rowID]['street_address'])
1014 . trim($rows[$rowID]['city'])
1015 . trim($rows[$rowID]['state_province'])
1016 . trim($rows[$rowID]['postal_code'])
1017 . trim($rows[$rowID]['country']);
1018 if (isset($rows[$rowID]['last_name'])) {
1019 $name = $rows[$rowID]['last_name'];
1022 $name = $rows[$rowID]['display_name'];
1027 'first_name' => $rows[$rowID]['first_name'],
1028 'individual_prefix' => $rows[$rowID]['individual_prefix'],
1030 $format = Civi
::settings()->get('display_name_format');
1031 $firstNameWithPrefix = CRM_Utils_Address
::format($formatted, $format, FALSE, FALSE);
1032 $firstNameWithPrefix = trim($firstNameWithPrefix);
1034 // fill uniqueAddress array with last/first name tree
1035 if (isset($uniqueAddress[$address])) {
1036 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1037 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1038 // drop unnecessary rows
1039 unset($rows[$rowID]);
1040 // this is the first listing at this address
1043 $uniqueAddress[$address]['ID'] = $rowID;
1044 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1045 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1048 foreach ($uniqueAddress as $address => $data) {
1049 // copy data back to $rows
1051 // one last name list per row
1052 foreach ($data['names'] as $last_name => $first_names) {
1057 if (count($first_names) == 1) {
1058 $family = $first_names[current(array_keys($first_names))]['addressee_display'];
1061 // collapse the tree to summarize
1062 $family = trim(implode(" & ", array_keys($first_names)) . " " . $last_name);
1065 $processedNames .= "\n" . $family;
1068 // build display_name string
1069 $processedNames = $family;
1073 $rows[$data['ID']]['addressee'] = $rows[$data['ID']]['addressee_display'] = $rows[$data['ID']]['display_name'] = $processedNames;
1078 * Create relationship between contacts who share an address.
1080 * Note that currently we create relationship between
1081 * Individual + Household and Individual + Organization
1083 * @param int $masterAddressId
1084 * Master address id.
1085 * @param int $currentContactId
1086 * Current contact id.
1088 public static function processSharedAddressRelationship($masterAddressId, $currentContactId) {
1089 // get the contact type of contact being edited / created
1090 $currentContactType = CRM_Contact_BAO_Contact
::getContactType($currentContactId);
1092 // if current contact is not of type individual return
1093 if ($currentContactType != 'Individual') {
1097 // get the contact id and contact type of shared contact
1098 // check the contact type of shared contact, return if it is of type Individual
1099 $query = 'SELECT cc.id, cc.contact_type
1100 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1103 $dao = CRM_Core_DAO
::executeQuery($query, [1 => [$masterAddressId, 'Integer']]);
1106 // master address contact needs to be Household or Organization, otherwise return
1107 if ($dao->contact_type
== 'Individual') {
1110 $sharedContactType = $dao->contact_type
;
1111 $sharedContactId = $dao->id
;
1113 // create relationship between ontacts who share an address
1114 if ($sharedContactType == 'Organization') {
1115 return CRM_Contact_BAO_Contact_Utils
::createCurrentEmployerRelationship($currentContactId, $sharedContactId);
1118 // get the relationship type id of "Household Member of"
1119 $relTypeId = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_RelationshipType', 'Household Member of', 'id', 'name_a_b');
1122 throw new CRM_Core_Exception(ts("You seem to have deleted the relationship type 'Household Member of'"));
1126 'is_active' => TRUE,
1127 'relationship_type_id' => $relTypeId,
1128 'contact_id_a' => $currentContactId,
1129 'contact_id_b' => $sharedContactId,
1132 // If already there is a relationship record of $relParam criteria, avoid creating relationship again or else
1133 // it will casue CRM-16588 as the Duplicate Relationship Exception will revert other contact field values on update
1134 if (CRM_Contact_BAO_Relationship
::checkDuplicateRelationship($relParam, $currentContactId, $sharedContactId)) {
1139 // create relationship
1140 civicrm_api3('relationship', 'create', $relParam);
1142 catch (CiviCRM_API3_Exception
$e) {
1143 // We catch and ignore here because this has historically been a best-effort relationship create call.
1144 // presumably it could refuse due to duplication or similar and we would ignore that.
1149 * Check and set the status for shared address delete.
1151 * @param int $addressId
1153 * @param int $contactId
1155 * @param bool $returnStatus
1160 public static function setSharedAddressDeleteStatus($addressId = NULL, $contactId = NULL, $returnStatus = FALSE) {
1161 // check if address that is being deleted has any shared
1163 $entityId = $addressId;
1164 $query = 'SELECT cc.id, cc.display_name
1165 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1166 WHERE ca.master_id = %1';
1169 $entityId = $contactId;
1170 $query = 'SELECT cc.id, cc.display_name
1171 FROM civicrm_address ca1
1172 INNER JOIN civicrm_address ca2 ON ca1.id = ca2.master_id
1173 INNER JOIN civicrm_contact cc ON ca2.contact_id = cc.id
1174 WHERE ca1.contact_id = %1';
1177 $dao = CRM_Core_DAO
::executeQuery($query, [1 => [$entityId, 'Integer']]);
1180 $sharedContactList = [];
1181 $statusMessage = NULL;
1183 while ($dao->fetch()) {
1184 if (empty($deleteStatus)) {
1185 $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.');
1188 $contactViewUrl = CRM_Utils_System
::url('civicrm/contact/view', "reset=1&cid={$dao->id}");
1189 $sharedContactList[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1190 $deleteStatus[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1195 if (!empty($deleteStatus)) {
1196 $statusMessage = implode('<br/>', $deleteStatus) . '<br/>';
1199 if (!$returnStatus) {
1200 CRM_Core_Session
::setStatus($statusMessage, '', 'info');
1204 'contactList' => $sharedContactList,
1205 'count' => $addressCount,
1211 * Call common delete function.
1213 * @see \CRM_Contact_BAO_Contact::on_hook_civicrm_post
1219 public static function del($id) {
1220 return (bool) self
::deleteRecord(['id' => $id]);
1224 * Get options for a given address field.
1225 * @see CRM_Core_DAO::buildOptions
1227 * TODO: Should we always assume chainselect? What fn should be responsible for controlling that flow?
1228 * TODO: In context of chainselect, what to return if e.g. a country has no states?
1230 * @param string $fieldName
1231 * @param string $context
1232 * @see CRM_Core_DAO::buildOptionsContext
1233 * @param array $props
1234 * whatever is known about this dao object.
1236 * @return array|bool
1238 public static function buildOptions($fieldName, $context = NULL, $props = []) {
1240 // Special logic for fields whose options depend on context or properties
1241 switch ($fieldName) {
1242 // Filter state_province list based on chosen country or site defaults
1243 case 'state_province_id':
1244 case 'state_province_name':
1245 case 'state_province':
1246 // change $fieldName to DB specific names.
1247 $fieldName = 'state_province_id';
1248 if (empty($props['country_id']) && $context !== 'validate') {
1249 $config = CRM_Core_Config
::singleton();
1250 if (!empty($config->provinceLimit
)) {
1251 $props['country_id'] = $config->provinceLimit
;
1254 $props['country_id'] = $config->defaultContactCountry
;
1257 if (!empty($props['country_id'])) {
1258 if (!CRM_Utils_Rule
::commaSeparatedIntegers(implode(',', (array) $props['country_id']))) {
1259 throw new CRM_Core_Exception(ts('Province limit or default country setting is incorrect'));
1261 $params['condition'] = 'country_id IN (' . implode(',', (array) $props['country_id']) . ')';
1265 // Filter country list based on site defaults
1268 // change $fieldName to DB specific names.
1269 $fieldName = 'country_id';
1270 if ($context != 'get' && $context != 'validate') {
1271 $config = CRM_Core_Config
::singleton();
1272 if (!empty($config->countryLimit
) && is_array($config->countryLimit
)) {
1273 if (!CRM_Utils_Rule
::commaSeparatedIntegers(implode(',', $config->countryLimit
))) {
1274 throw new CRM_Core_Exception(ts('Available Country setting is incorrect'));
1276 $params['condition'] = 'id IN (' . implode(',', $config->countryLimit
) . ')';
1281 // Filter county list based on chosen state
1283 if (!empty($props['state_province_id'])) {
1284 if (!CRM_Utils_Rule
::commaSeparatedIntegers(implode(',', (array) $props['state_province_id']))) {
1285 throw new CRM_Core_Exception(ts('Can only accept Integers for state_province_id filtering'));
1287 $params['condition'] = 'state_province_id IN (' . implode(',', (array) $props['state_province_id']) . ')';
1291 // Not a real field in this entity
1292 case 'world_region':
1294 case 'worldregion_id':
1295 return CRM_Core_BAO_Country
::buildOptions('region_id', $context, $props);
1297 return CRM_Core_PseudoConstant
::get(__CLASS__
, $fieldName, $params, $context);
1301 * Add data from the configured geocoding provider.
1303 * Generally this means latitude & longitude data.
1305 * @param array $params
1307 * TRUE if params could be passed to a provider, else FALSE.
1309 public static function addGeocoderData(&$params) {
1311 $provider = CRM_Utils_GeocodeProvider
::getConfiguredProvider();
1312 $providerExists = TRUE;
1314 catch (CRM_Core_Exception
$e) {
1315 $providerExists = FALSE;
1317 if ($providerExists) {
1318 $provider::format($params);
1320 // core#2379 - Limit geocode length to 14 characters to avoid validation error on save in UI.
1321 foreach (['geo_code_1', 'geo_code_2'] as $geocode) {
1322 if ($params[$geocode] ??
FALSE) {
1323 $params[$geocode] = (float) substr($params[$geocode], 0, 14);
1326 return $providerExists;
1330 * Create multiple addresses using legacy methodology.
1332 * @param array $params
1333 * @param bool $fixAddress
1335 * @return array|null
1337 public static function legacyCreate(array $params, bool $fixAddress) {
1338 if (!isset($params['address']) ||
!is_array($params['address'])) {
1341 CRM_Core_BAO_Block
::sortPrimaryFirst($params['address']);
1344 $updateBlankLocInfo = CRM_Utils_Array
::value('updateBlankLocInfo', $params, FALSE);
1345 $contactId = $params['contact_id'];
1346 //get all the addresses for this contact
1347 $addresses = self
::allAddress($contactId);
1349 $isPrimary = $isBilling = TRUE;
1351 foreach ($params['address'] as $key => $value) {
1352 if (!is_array($value)) {
1356 $addressExists = self
::dataExists($value);
1357 if (empty($value['id'])) {
1358 if (!empty($addresses) && !empty($value['location_type_id']) && array_key_exists($value['location_type_id'], $addresses)) {
1359 $value['id'] = $addresses[$value['location_type_id']];
1363 // Note there could be cases when address info already exist ($value[id] is set) for a contact/entity
1364 // BUT info is not present at this time, and therefore we should be really careful when deleting the block.
1365 // $updateBlankLocInfo will help take appropriate decision. CRM-5969
1366 if (isset($value['id']) && !$addressExists && $updateBlankLocInfo) {
1367 //delete the existing record
1368 CRM_Core_BAO_Block
::blockDelete('Address', ['id' => $value['id']]);
1371 elseif (!$addressExists) {
1375 if ($isPrimary && !empty($value['is_primary'])) {
1379 $value['is_primary'] = 0;
1382 if ($isBilling && !empty($value['is_billing'])) {
1386 $value['is_billing'] = 0;
1389 if (empty($value['manual_geo_code'])) {
1390 $value['manual_geo_code'] = 0;
1392 $value['contact_id'] = $contactId;
1393 $blocks[] = self
::add($value, $fixAddress);