3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2018
35 * This is class to handle address related functions.
37 class CRM_Core_BAO_Address
extends CRM_Core_DAO_Address
{
40 * Takes an associative array and creates a address.
42 * @param array $params
43 * (reference ) an assoc array of name/value pairs.
44 * @param bool $fixAddress
45 * True if you need to fix (format) address values.
46 * before inserting in db
51 * array of created address
53 public static function create(&$params, $fixAddress = TRUE, $entity = NULL) {
54 if (!isset($params['address']) ||
!is_array($params['address'])) {
57 CRM_Core_BAO_Block
::sortPrimaryFirst($params['address']);
61 $updateBlankLocInfo = CRM_Utils_Array
::value('updateBlankLocInfo', $params, FALSE);
63 $contactId = $params['contact_id'];
64 //get all the addresses for this contact
65 $addresses = self
::allAddress($contactId);
68 // get all address from location block
69 $entityElements = array(
70 'entity_table' => $params['entity_table'],
71 'entity_id' => $params['entity_id'],
73 $addresses = self
::allEntityAddress($entityElements);
76 $isPrimary = $isBilling = TRUE;
78 foreach ($params['address'] as $key => $value) {
79 if (!is_array($value)) {
83 $addressExists = self
::dataExists($value);
84 if (empty($value['id'])) {
85 if (!empty($addresses) && array_key_exists(CRM_Utils_Array
::value('location_type_id', $value), $addresses)) {
86 $value['id'] = $addresses[CRM_Utils_Array
::value('location_type_id', $value)];
90 // Note there could be cases when address info already exist ($value[id] is set) for a contact/entity
91 // BUT info is not present at this time, and therefore we should be really careful when deleting the block.
92 // $updateBlankLocInfo will help take appropriate decision. CRM-5969
93 if (isset($value['id']) && !$addressExists && $updateBlankLocInfo) {
94 //delete the existing record
95 CRM_Core_BAO_Block
::blockDelete('Address', array('id' => $value['id']));
98 elseif (!$addressExists) {
102 if ($isPrimary && !empty($value['is_primary'])) {
106 $value['is_primary'] = 0;
109 if ($isBilling && !empty($value['is_billing'])) {
113 $value['is_billing'] = 0;
116 if (empty($value['manual_geo_code'])) {
117 $value['manual_geo_code'] = 0;
119 $value['contact_id'] = $contactId;
120 $blocks[] = self
::add($value, $fixAddress);
127 * Takes an associative array and adds address.
129 * @param array $params
130 * (reference ) an assoc array of name/value pairs.
131 * @param bool $fixAddress
132 * True if you need to fix (format) address values.
133 * before inserting in db
135 * @return CRM_Core_BAO_Address|null
137 public static function add(&$params, $fixAddress = FALSE) {
139 $address = new CRM_Core_DAO_Address();
140 $checkPermissions = isset($params['check_permissions']) ?
$params['check_permissions'] : TRUE;
142 // fixAddress mode to be done
144 CRM_Core_BAO_Address
::fixAddress($params);
147 $hook = empty($params['id']) ?
'create' : 'edit';
148 CRM_Utils_Hook
::pre($hook, 'Address', CRM_Utils_Array
::value('id', $params), $params);
150 // if id is set & is_primary isn't we can assume no change
151 if (is_numeric(CRM_Utils_Array
::value('is_primary', $params)) ||
empty($params['id'])) {
152 CRM_Core_BAO_Block
::handlePrimary($params, get_class());
155 // (prevent chaining 1 and 3) CRM-21214
156 if (isset($params['master_id']) && !CRM_Utils_System
::isNull($params['master_id'])) {
157 self
::fixSharedAddress($params);
160 $address->copyValues($params);
165 $customFields = CRM_Core_BAO_CustomField
::getFields('Address', FALSE, TRUE, NULL, NULL, FALSE, FALSE, $checkPermissions);
167 if (!empty($customFields)) {
168 $addressCustom = CRM_Core_BAO_CustomField
::postProcess($params,
175 if (!empty($addressCustom)) {
176 CRM_Core_BAO_CustomValueTable
::store($addressCustom, 'civicrm_address', $address->id
);
179 // call the function to sync shared address and create relationships
180 // if address is already shared, share master_id with all children and update relationships accordingly
181 // (prevent chaining 2) CRM-21214
182 self
::processSharedAddress($address->id
, $params);
184 // lets call the post hook only after we've done all the follow on processing
185 CRM_Utils_Hook
::post($hook, 'Address', $address->id
, $address);
192 * Format the address params to have reasonable values.
194 * @param array $params
195 * (reference ) an assoc array of name/value pairs.
197 public static function fixAddress(&$params) {
198 if (!empty($params['billing_street_address'])) {
199 //Check address is coming from online contribution / registration page
202 'street_address' => 'billing_street_address',
203 'city' => 'billing_city',
204 'postal_code' => 'billing_postal_code',
205 'state_province' => 'billing_state_province',
206 'state_province_id' => 'billing_state_province_id',
207 'country' => 'billing_country',
208 'country_id' => 'billing_country_id',
211 foreach ($billing as $key => $val) {
212 if ($value = CRM_Utils_Array
::value($val, $params)) {
213 if (!empty($params[$key])) {
214 unset($params[$val]);
217 //add new key and removed old
218 $params[$key] = $value;
219 unset($params[$val]);
225 /* Split the zip and +4, if it's in US format */
226 if (!empty($params['postal_code']) &&
227 preg_match('/^(\d{4,5})[+-](\d{4})$/',
228 $params['postal_code'],
232 $params['postal_code'] = $match[1];
233 $params['postal_code_suffix'] = $match[2];
236 // add country id if not set
237 if ((!isset($params['country_id']) ||
!is_numeric($params['country_id'])) &&
238 isset($params['country'])
240 $country = new CRM_Core_DAO_Country();
241 $country->name
= $params['country'];
242 if (!$country->find(TRUE)) {
243 $country->name
= NULL;
244 $country->iso_code
= $params['country'];
245 $country->find(TRUE);
247 $params['country_id'] = $country->id
;
250 // add state_id if state is set
251 if ((!isset($params['state_province_id']) ||
!is_numeric($params['state_province_id']))
252 && isset($params['state_province'])
254 if (!empty($params['state_province'])) {
255 $state_province = new CRM_Core_DAO_StateProvince();
256 $state_province->name
= $params['state_province'];
258 // add country id if present
259 if (!empty($params['country_id'])) {
260 $state_province->country_id
= $params['country_id'];
263 if (!$state_province->find(TRUE)) {
264 unset($state_province->name
);
265 $state_province->abbreviation
= $params['state_province'];
266 $state_province->find(TRUE);
268 $params['state_province_id'] = $state_province->id
;
269 if (empty($params['country_id'])) {
270 // set this here since we have it
271 $params['country_id'] = $state_province->country_id
;
275 $params['state_province_id'] = 'null';
279 // add county id if county is set
281 if ((!isset($params['county_id']) ||
!is_numeric($params['county_id']))
282 && isset($params['county']) && !empty($params['county'])
284 $county = new CRM_Core_DAO_County();
285 $county->name
= $params['county'];
287 if (isset($params['state_province_id'])) {
288 $county->state_province_id
= $params['state_province_id'];
291 if ($county->find(TRUE)) {
292 $params['county_id'] = $county->id
;
296 // currently copy values populates empty fields with the string "null"
297 // and hence need to check for the string null
298 if (isset($params['state_province_id']) &&
299 is_numeric($params['state_province_id']) &&
300 (!isset($params['country_id']) ||
empty($params['country_id']))
302 // since state id present and country id not present, hence lets populate it
303 // jira issue http://issues.civicrm.org/jira/browse/CRM-56
304 $params['country_id'] = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_StateProvince',
305 $params['state_province_id'],
310 //special check to ignore non numeric values if they are not
311 //detected by formRule(sometimes happens due to internet latency), also allow user to unselect state/country
312 if (isset($params['state_province_id'])) {
313 if (empty($params['state_province_id'])) {
314 $params['state_province_id'] = 'null';
316 elseif (!is_numeric($params['state_province_id']) ||
317 ((int ) $params['state_province_id'] < 1000)
319 // CRM-3393 ( the hacky 1000 check)
320 $params['state_province_id'] = 'null';
324 if (isset($params['country_id'])) {
325 if (empty($params['country_id'])) {
326 $params['country_id'] = 'null';
328 elseif (!is_numeric($params['country_id']) ||
329 ((int ) $params['country_id'] < 1000)
331 // CRM-3393 ( the hacky 1000 check)
332 $params['country_id'] = 'null';
336 // add state and country names from the ids
337 if (isset($params['state_province_id']) && is_numeric($params['state_province_id'])) {
338 $params['state_province'] = CRM_Core_PseudoConstant
::stateProvinceAbbreviation($params['state_province_id']);
341 if (isset($params['country_id']) && is_numeric($params['country_id'])) {
342 $params['country'] = CRM_Core_PseudoConstant
::country($params['country_id']);
345 $asp = Civi
::settings()->get('address_standardization_provider');
346 // clean up the address via USPS web services if enabled
347 if ($asp === 'USPS' &&
348 $params['country_id'] == 1228
350 CRM_Utils_Address_USPS
::checkAddress($params);
352 // do street parsing again if enabled, since street address might have changed
353 $parseStreetAddress = CRM_Utils_Array
::value(
354 'street_address_parsing',
355 CRM_Core_BAO_Setting
::valueOptions(
356 CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
362 if ($parseStreetAddress && !empty($params['street_address'])) {
367 'street_number_suffix',
369 unset($params[$fld]);
371 // main parse string.
372 $parseString = CRM_Utils_Array
::value('street_address', $params);
373 $parsedFields = CRM_Core_BAO_Address
::parseStreetAddress($parseString);
375 // merge parse address in to main address block.
376 $params = array_merge($params, $parsedFields);
379 // skip_geocode is an optional parameter through the api.
380 // manual_geo_code is on the contact edit form. They do the same thing....
381 if (empty($params['skip_geocode']) && empty($params['manual_geo_code'])) {
382 self
::addGeocoderData($params);
388 * Check if there is data to create the object.
390 * @param array $params
391 * (reference ) an assoc array of name/value pairs.
395 public static function dataExists(&$params) {
396 //check if location type is set if not return false
397 if (!isset($params['location_type_id'])) {
401 $config = CRM_Core_Config
::singleton();
402 foreach ($params as $name => $value) {
403 if (in_array($name, array(
414 elseif (!CRM_Utils_System
::isNull($value)) {
415 // name could be country or country id
416 if (substr($name, 0, 7) == 'country') {
417 // make sure its different from the default country
419 $defaultCountry = $config->defaultContactCountry();
421 $defaultCountryName = $config->defaultContactCountryName();
423 if ($defaultCountry) {
424 if ($value == $defaultCountry ||
425 $value == $defaultCountryName ||
426 $value == $config->defaultContactCountry
435 // return if null default
449 * Given the list of params in the params array, fetch the object
450 * and store the values in the values array
452 * @param array $entityBlock
453 * Associated array of fields.
454 * @param bool $microformat
455 * If microformat output is required.
456 * @param int|string $fieldName conditional field name
459 * array with address fields
461 public static function &getValues($entityBlock, $microformat = FALSE, $fieldName = 'contact_id') {
462 if (empty($entityBlock)) {
465 $addresses = array();
466 $address = new CRM_Core_BAO_Address();
468 if (empty($entityBlock['entity_table'])) {
469 $address->$fieldName = CRM_Utils_Array
::value($fieldName, $entityBlock);
472 $addressIds = array();
473 $addressIds = self
::allEntityAddress($entityBlock);
475 if (!empty($addressIds[1])) {
476 $address->id
= $addressIds[1];
482 if (isset($entityBlock['is_billing']) && $entityBlock['is_billing'] == 1) {
483 $address->orderBy('is_billing desc, id');
486 //get primary address as a first block.
487 $address->orderBy('is_primary desc, id');
492 $locationTypes = CRM_Core_PseudoConstant
::get('CRM_Core_DAO_Address', 'location_type_id');
494 while ($address->fetch()) {
495 // deprecate reference.
503 if (isset($address->$fld)) {
504 unset($address->$fld);
508 $stree = $address->street_address
;
510 CRM_Core_DAO
::storeValues($address, $values);
512 // add state and country information: CRM-369
513 if (!empty($address->location_type_id
)) {
514 $values['location_type'] = CRM_Utils_Array
::value($address->location_type_id
, $locationTypes);
516 if (!empty($address->state_province_id
)) {
517 $address->state
= CRM_Core_PseudoConstant
::stateProvinceAbbreviation($address->state_province_id
, FALSE);
518 $address->state_name
= CRM_Core_PseudoConstant
::stateProvince($address->state_province_id
, FALSE);
521 if (!empty($address->country_id
)) {
522 $address->country
= CRM_Core_PseudoConstant
::country($address->country_id
);
525 $regionId = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_Country', $address->country_id
, 'region_id');
527 $address->world_region
= CRM_Core_PseudoConstant
::worldregion($regionId);
530 $address->addDisplay($microformat);
532 $values['display'] = $address->display
;
533 $values['display_text'] = $address->display_text
;
535 if (isset($address->master_id
) && !CRM_Utils_System
::isNull($address->master_id
)) {
536 $values['use_shared_address'] = 1;
539 $addresses[$count] = $values;
541 //There should never be more than one primary blocks, hence set is_primary = 0 other than first
542 // Calling functions expect the key is_primary to be set, so do not unset it here!
544 $addresses[$count]['is_primary'] = 0;
554 * Add the formatted address to $this-> display.
556 * @param bool $microformat
557 * Unexplained parameter that I've always wondered about.
559 public function addDisplay($microformat = FALSE) {
561 // added this for CRM 1200
562 'address_id' => $this->id
,
564 'address_name' => str_replace('\ 1', ' ', $this->name
),
565 'street_address' => $this->street_address
,
566 'supplemental_address_1' => $this->supplemental_address_1
,
567 'supplemental_address_2' => $this->supplemental_address_2
,
568 'supplemental_address_3' => $this->supplemental_address_3
,
569 'city' => $this->city
,
570 'state_province_name' => isset($this->state_name
) ?
$this->state_name
: "",
571 'state_province' => isset($this->state
) ?
$this->state
: "",
572 'postal_code' => isset($this->postal_code
) ?
$this->postal_code
: "",
573 'postal_code_suffix' => isset($this->postal_code_suffix
) ?
$this->postal_code_suffix
: "",
574 'country' => isset($this->country
) ?
$this->country
: "",
575 'world_region' => isset($this->world_region
) ?
$this->world_region
: "",
578 if (isset($this->county_id
) && $this->county_id
) {
579 $fields['county'] = CRM_Core_PseudoConstant
::county($this->county_id
);
582 $fields['county'] = NULL;
585 $this->display
= CRM_Utils_Address
::format($fields, NULL, $microformat);
586 $this->display_text
= CRM_Utils_Address
::format($fields);
590 * Get all the addresses for a specified contact_id, with the primary address being first
595 * @param bool $updateBlankLocInfo
598 * the array of adrress data
600 public static function allAddress($id, $updateBlankLocInfo = FALSE) {
606 SELECT civicrm_address.id as address_id, civicrm_address.location_type_id as location_type_id
607 FROM civicrm_contact, civicrm_address
608 WHERE civicrm_address.contact_id = civicrm_contact.id AND civicrm_contact.id = %1
609 ORDER BY civicrm_address.is_primary DESC, address_id ASC";
610 $params = array(1 => array($id, 'Integer'));
612 $addresses = array();
613 $dao = CRM_Core_DAO
::executeQuery($query, $params);
615 while ($dao->fetch()) {
616 if ($updateBlankLocInfo) {
617 $addresses[$count++
] = $dao->address_id
;
620 $addresses[$dao->location_type_id
] = $dao->address_id
;
627 * Get all the addresses for a specified location_block id, with the primary address being first
629 * @param array $entityElements
630 * The array containing entity_id and.
634 * the array of adrress data
636 public static function allEntityAddress(&$entityElements) {
637 $addresses = array();
638 if (empty($entityElements)) {
642 $entityId = $entityElements['entity_id'];
643 $entityTable = $entityElements['entity_table'];
646 SELECT civicrm_address.id as address_id
647 FROM civicrm_loc_block loc, civicrm_location_type ltype, civicrm_address, {$entityTable} ev
649 AND loc.id = ev.loc_block_id
650 AND civicrm_address.id IN (loc.address_id, loc.address_2_id)
651 AND ltype.id = civicrm_address.location_type_id
652 ORDER BY civicrm_address.is_primary DESC, civicrm_address.location_type_id DESC, address_id ASC ";
654 $params = array(1 => array($entityId, 'Integer'));
655 $dao = CRM_Core_DAO
::executeQuery($sql, $params);
657 while ($dao->fetch()) {
658 $addresses[$locationCount] = $dao->address_id
;
665 * Get address sequence.
668 * Array of address sequence.
670 public static function addressSequence() {
671 $config = CRM_Core_Config
::singleton();
672 $addressSequence = $config->addressSequence();
674 $countryState = $cityPostal = FALSE;
675 foreach ($addressSequence as $key => $field) {
677 in_array($field, array('country', 'state_province')) &&
680 $countryState = TRUE;
681 $addressSequence[$key] = 'country_state_province';
684 in_array($field, array('city', 'postal_code')) &&
688 $addressSequence[$key] = 'city_postal_code';
691 in_array($field, array('country', 'state_province', 'city', 'postal_code'))
693 unset($addressSequence[$key]);
697 return $addressSequence;
701 * Parse given street address string in to street_name,
702 * street_unit, street_number and street_number_suffix
703 * eg "54A Excelsior Ave. Apt 1C", or "917 1/2 Elm Street"
705 * NB: civic street formats for en_CA and fr_CA used by default if those locales are active
706 * otherwise en_US format is default action
708 * @param string $streetAddress
709 * Street address including number and apt.
710 * @param string $locale
711 * Locale used to parse address.
714 * parsed fields values.
716 public static function parseStreetAddress($streetAddress, $locale = NULL) {
717 // use 'en_US' for address parsing if the requested locale is not supported.
718 if (!self
::isSupportedParsingLocale($locale)) {
722 $emptyParseFields = $parseFields = array(
725 'street_number' => '',
726 'street_number_suffix' => '',
729 if (empty($streetAddress)) {
733 $streetAddress = trim($streetAddress);
736 if (in_array($locale, array(
739 )) && preg_match('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', $streetAddress, $matches)
741 $parseFields['street_unit'] = $matches[1];
742 // unset from rest of street address
743 $streetAddress = preg_replace('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', '', $streetAddress);
746 // get street number and suffix.
748 //alter street number/suffix handling so that we accept -digit
749 if (preg_match('/^[A-Za-z0-9]+([\S]+)/', $streetAddress, $matches)) {
750 // check that $matches[0] is numeric, else assume no street number
751 if (preg_match('/^(\d+)/', $matches[0])) {
752 $streetNumAndSuffix = $matches[0];
754 // get street number.
756 if (preg_match('/^(\d+)/', $streetNumAndSuffix, $matches)) {
757 $parseFields['street_number'] = $matches[0];
758 $suffix = preg_replace('/^(\d+)/', '', $streetNumAndSuffix);
759 $parseFields['street_number_suffix'] = trim($suffix);
762 // unset from main street address.
763 $streetAddress = preg_replace('/^[A-Za-z0-9]+([\S]+)/', '', $streetAddress);
764 $streetAddress = trim($streetAddress);
767 elseif (preg_match('/^(\d+)/', $streetAddress, $matches)) {
768 $parseFields['street_number'] = $matches[0];
769 // unset from main street address.
770 $streetAddress = preg_replace('/^(\d+)/', '', $streetAddress);
771 $streetAddress = trim($streetAddress);
774 // suffix might be like 1/2
776 if (preg_match('/^\d\/\d/', $streetAddress, $matches)) {
777 $parseFields['street_number_suffix'] .= $matches[0];
779 // unset from main street address.
780 $streetAddress = preg_replace('/^\d+\/\d+/', '', $streetAddress);
781 $streetAddress = trim($streetAddress);
784 // now get the street unit.
785 // supportable street unit formats.
786 $streetUnitFormats = array(
829 // overwriting $streetUnitFormats for 'en_CA' and 'fr_CA' locale
830 if (in_array($locale, array(
834 $streetUnitFormats = array('APT', 'APP', 'SUITE', 'BUREAU', 'UNIT');
836 //@todo per CRM-14459 this regex picks up words with the string in them - e.g APT picks up
837 //Captain - presuming fixing regex (& adding test) to ensure a-z does not preced string will fix
838 $streetUnitPreg = '/(' . implode('|\s', $streetUnitFormats) . ')(.+)?/i';
840 if (preg_match($streetUnitPreg, $streetAddress, $matches)) {
841 $parseFields['street_unit'] = trim($matches[0]);
842 $streetAddress = str_replace($matches[0], '', $streetAddress);
843 $streetAddress = trim($streetAddress);
846 // consider remaining string as street name.
847 $parseFields['street_name'] = $streetAddress;
849 //run parsed fields through stripSpaces to clean
850 foreach ($parseFields as $parseField => $value) {
851 $parseFields[$parseField] = CRM_Utils_String
::stripSpaces($value);
853 //CRM-14459 if the field is too long we should assume it didn't get it right & skip rather than allow
855 $fields = CRM_Core_BAO_Address
::fields();
856 foreach ($fields as $fieldname => $field) {
857 if (!empty($field['maxlength']) && strlen(CRM_Utils_Array
::value($fieldname, $parseFields)) > $field['maxlength']) {
858 return $emptyParseFields;
866 * Determines if the specified locale is
867 * supported by address parsing.
868 * If no locale is specified then it
869 * will check the default configured locale.
871 * locales supported include:
872 * en_US - http://pe.usps.com/cpim/ftp/pubs/pub28/pub28.pdf
873 * en_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp
874 * fr_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-f.asp
875 * NB: common use of comma after street number also supported
877 * @param string $locale
878 * The locale to be checked
882 public static function isSupportedParsingLocale($locale = NULL) {
884 $config = CRM_Core_Config
::singleton();
885 $locale = $config->lcMessages
;
888 $parsingSupportedLocales = array('en_US', 'en_CA', 'fr_CA');
890 if (in_array($locale, $parsingSupportedLocales)) {
898 * Validate the address fields based on the address options enabled.
899 * in the Address Settings
901 * @param array $fields
902 * An array of importable/exportable contact fields.
905 * an array of contact fields and only the enabled address options
907 public static function validateAddressOptions($fields) {
908 static $addressOptions = NULL;
909 if (!$addressOptions) {
910 $addressOptions = CRM_Core_BAO_Setting
::valueOptions(
911 CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
916 if (is_array($fields) && !empty($fields)) {
917 foreach ($addressOptions as $key => $value) {
918 if (!$value && isset($fields[$key])) {
919 unset($fields[$key]);
927 * Check if current address is used by any other contacts.
929 * @param int $addressId
933 * count of contacts that use this shared address
935 public static function checkContactSharedAddress($addressId) {
936 $query = 'SELECT count(id) FROM civicrm_address WHERE master_id = %1';
937 return CRM_Core_DAO
::singleValueQuery($query, array(1 => array($addressId, 'Integer')));
941 * Check if current address fields are shared with any other address.
943 * @param array $fields
944 * Address fields in profile.
945 * @param int $contactId
949 public static function checkContactSharedAddressFields(&$fields, $contactId) {
950 if (!$contactId ||
!is_array($fields) ||
empty($fields)) {
954 $sharedLocations = array();
960 WHERE contact_id = %1
961 AND master_id IS NOT NULL";
963 $dao = CRM_Core_DAO
::executeQuery($query, array(1 => array($contactId, 'Positive')));
964 while ($dao->fetch()) {
965 $sharedLocations[$dao->location_type_id
] = $dao->location_type_id
;
966 if ($dao->is_primary
) {
967 $sharedLocations['Primary'] = 'Primary';
971 //no need to process further.
972 if (empty($sharedLocations)) {
976 $addressFields = array(
986 'postal_code_suffix',
987 'supplemental_address_1',
988 'supplemental_address_2',
989 'supplemental_address_3',
992 foreach ($fields as $name => & $values) {
993 if (!is_array($values) ||
empty($values)) {
997 $nameVal = explode('-', $values['name']);
998 $fldName = CRM_Utils_Array
::value(0, $nameVal);
999 $locType = CRM_Utils_Array
::value(1, $nameVal);
1000 if (!empty($values['location_type_id'])) {
1001 $locType = $values['location_type_id'];
1004 if (in_array($fldName, $addressFields) &&
1005 in_array($locType, $sharedLocations)
1007 $values['is_shared'] = TRUE;
1013 * Fix the shared address if address is already shared
1014 * or if address will be shared with itself.
1016 * @param array $params
1017 * Associated array of address params.
1019 public static function fixSharedAddress(&$params) {
1020 // if address master address is shared, use its master (prevent chaining 1) CRM-21214
1021 $masterMasterId = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_Address', $params['master_id'], 'master_id');
1022 if ($masterMasterId > 0) {
1023 $params['master_id'] = $masterMasterId;
1026 // prevent an endless chain between two shared addresses (prevent chaining 3) CRM-21214
1027 if (CRM_Utils_Array
::value('id', $params) == $params['master_id']) {
1028 $params['master_id'] = NULL;
1029 CRM_Core_Session
::setStatus(ts("You can't connect an address to itself"), '', 'warning');
1034 * Update the shared addresses if master address is modified.
1036 * @param int $addressId
1038 * @param array $params
1039 * Associated array of address params.
1041 public static function processSharedAddress($addressId, $params) {
1042 $query = 'SELECT id, contact_id FROM civicrm_address WHERE master_id = %1';
1043 $dao = CRM_Core_DAO
::executeQuery($query, array(1 => array($addressId, 'Integer')));
1046 $skipFields = array('is_primary', 'location_type_id', 'is_billing', 'contact_id');
1047 if (isset($params['master_id']) && !CRM_Utils_System
::isNull($params['master_id'])) {
1048 // call the function to create a relationship for the new shared address
1049 self
::processSharedAddressRelationship($params['master_id'], $params['contact_id']);
1052 // else no new shares will be created, only update shared addresses
1053 $skipFields[] = 'master_id';
1055 foreach ($skipFields as $value) {
1056 unset($params[$value]);
1059 $addressDAO = new CRM_Core_DAO_Address();
1060 while ($dao->fetch()) {
1061 // call the function to update the relationship
1062 if (isset($params['master_id']) && !CRM_Utils_System
::isNull($params['master_id'])) {
1063 self
::processSharedAddressRelationship($params['master_id'], $dao->contact_id
);
1065 $addressDAO->copyValues($params);
1066 $addressDAO->id
= $dao->id
;
1067 $addressDAO->save();
1068 $addressDAO->free();
1073 * Merge contacts with the Same address to get one shared label.
1074 * @param array $rows
1075 * Array[contact_id][contactDetails].
1077 public static function mergeSameAddress(&$rows) {
1078 $uniqueAddress = array();
1079 foreach (array_keys($rows) as $rowID) {
1080 // load complete address as array key
1081 $address = trim($rows[$rowID]['street_address'])
1082 . trim($rows[$rowID]['city'])
1083 . trim($rows[$rowID]['state_province'])
1084 . trim($rows[$rowID]['postal_code'])
1085 . trim($rows[$rowID]['country']);
1086 if (isset($rows[$rowID]['last_name'])) {
1087 $name = $rows[$rowID]['last_name'];
1090 $name = $rows[$rowID]['display_name'];
1095 'first_name' => $rows[$rowID]['first_name'],
1096 'individual_prefix' => $rows[$rowID]['individual_prefix'],
1098 $format = Civi
::settings()->get('display_name_format');
1099 $firstNameWithPrefix = CRM_Utils_Address
::format($formatted, $format, FALSE, FALSE);
1100 $firstNameWithPrefix = trim($firstNameWithPrefix);
1102 // fill uniqueAddress array with last/first name tree
1103 if (isset($uniqueAddress[$address])) {
1104 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1105 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1106 // drop unnecessary rows
1107 unset($rows[$rowID]);
1108 // this is the first listing at this address
1111 $uniqueAddress[$address]['ID'] = $rowID;
1112 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1113 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1116 foreach ($uniqueAddress as $address => $data) {
1117 // copy data back to $rows
1119 // one last name list per row
1120 foreach ($data['names'] as $last_name => $first_names) {
1125 if (count($first_names) == 1) {
1126 $family = $first_names[current(array_keys($first_names))]['addressee_display'];
1129 // collapse the tree to summarize
1130 $family = trim(implode(" & ", array_keys($first_names)) . " " . $last_name);
1133 $processedNames .= "\n" . $family;
1136 // build display_name string
1137 $processedNames = $family;
1141 $rows[$data['ID']]['addressee'] = $rows[$data['ID']]['addressee_display'] = $rows[$data['ID']]['display_name'] = $processedNames;
1146 * Create relationship between contacts who share an address.
1148 * Note that currently we create relationship between
1149 * Individual + Household and Individual + Organization
1151 * @param int $masterAddressId
1152 * Master address id.
1153 * @param int $currentContactId
1154 * Current contact id.
1156 public static function processSharedAddressRelationship($masterAddressId, $currentContactId) {
1157 // get the contact type of contact being edited / created
1158 $currentContactType = CRM_Contact_BAO_Contact
::getContactType($currentContactId);
1160 // if current contact is not of type individual return
1161 if ($currentContactType != 'Individual') {
1165 // get the contact id and contact type of shared contact
1166 // check the contact type of shared contact, return if it is of type Individual
1167 $query = 'SELECT cc.id, cc.contact_type
1168 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1171 $dao = CRM_Core_DAO
::executeQuery($query, array(1 => array($masterAddressId, 'Integer')));
1174 // master address contact needs to be Household or Organization, otherwise return
1175 if ($dao->contact_type
== 'Individual') {
1178 $sharedContactType = $dao->contact_type
;
1179 $sharedContactId = $dao->id
;
1181 // create relationship between ontacts who share an address
1182 if ($sharedContactType == 'Organization') {
1183 return CRM_Contact_BAO_Contact_Utils
::createCurrentEmployerRelationship($currentContactId, $sharedContactId);
1186 // get the relationship type id of "Household Member of"
1187 $relTypeId = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_RelationshipType', 'Household Member of', 'id', 'name_a_b');
1190 CRM_Core_Error
::fatal(ts("You seem to have deleted the relationship type 'Household Member of'"));
1194 'is_active' => TRUE,
1195 'relationship_type_id' => $relTypeId,
1196 'contact_id_a' => $currentContactId,
1197 'contact_id_b' => $sharedContactId,
1200 // If already there is a relationship record of $relParam criteria, avoid creating relationship again or else
1201 // it will casue CRM-16588 as the Duplicate Relationship Exception will revert other contact field values on update
1202 if (CRM_Contact_BAO_Relationship
::checkDuplicateRelationship($relParam, $currentContactId, $sharedContactId)) {
1207 // create relationship
1208 civicrm_api3('relationship', 'create', $relParam);
1210 catch (CiviCRM_API3_Exception
$e) {
1211 // We catch and ignore here because this has historically been a best-effort relationship create call.
1212 // presumably it could refuse due to duplication or similar and we would ignore that.
1217 * Check and set the status for shared address delete.
1219 * @param int $addressId
1221 * @param int $contactId
1223 * @param bool $returnStatus
1228 public static function setSharedAddressDeleteStatus($addressId = NULL, $contactId = NULL, $returnStatus = FALSE) {
1229 // check if address that is being deleted has any shared
1231 $entityId = $addressId;
1232 $query = 'SELECT cc.id, cc.display_name
1233 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1234 WHERE ca.master_id = %1';
1237 $entityId = $contactId;
1238 $query = 'SELECT cc.id, cc.display_name
1239 FROM civicrm_address ca1
1240 INNER JOIN civicrm_address ca2 ON ca1.id = ca2.master_id
1241 INNER JOIN civicrm_contact cc ON ca2.contact_id = cc.id
1242 WHERE ca1.contact_id = %1';
1245 $dao = CRM_Core_DAO
::executeQuery($query, array(1 => array($entityId, 'Integer')));
1247 $deleteStatus = array();
1248 $sharedContactList = array();
1249 $statusMessage = NULL;
1251 while ($dao->fetch()) {
1252 if (empty($deleteStatus)) {
1253 $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.');
1256 $contactViewUrl = CRM_Utils_System
::url('civicrm/contact/view', "reset=1&cid={$dao->id}");
1257 $sharedContactList[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1258 $deleteStatus[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1263 if (!empty($deleteStatus)) {
1264 $statusMessage = implode('<br/>', $deleteStatus) . '<br/>';
1267 if (!$returnStatus) {
1268 CRM_Core_Session
::setStatus($statusMessage, '', 'info');
1272 'contactList' => $sharedContactList,
1273 'count' => $addressCount,
1279 * Call common delete function.
1285 public static function del($id) {
1286 return CRM_Contact_BAO_Contact
::deleteObjectWithPrimary('Address', $id);
1290 * Get options for a given address field.
1291 * @see CRM_Core_DAO::buildOptions
1293 * TODO: Should we always assume chainselect? What fn should be responsible for controlling that flow?
1294 * TODO: In context of chainselect, what to return if e.g. a country has no states?
1296 * @param string $fieldName
1297 * @param string $context
1298 * @see CRM_Core_DAO::buildOptionsContext
1299 * @param array $props
1300 * whatever is known about this dao object.
1302 * @return array|bool
1304 public static function buildOptions($fieldName, $context = NULL, $props = array()) {
1306 // Special logic for fields whose options depend on context or properties
1307 switch ($fieldName) {
1308 // Filter state_province list based on chosen country or site defaults
1309 case 'state_province_id':
1310 case 'state_province_name':
1311 case 'state_province':
1312 // change $fieldName to DB specific names.
1313 $fieldName = 'state_province_id';
1314 if (empty($props['country_id'])) {
1315 $config = CRM_Core_Config
::singleton();
1316 if (!empty($config->provinceLimit
)) {
1317 $props['country_id'] = $config->provinceLimit
;
1320 $props['country_id'] = $config->defaultContactCountry
;
1323 if (!empty($props['country_id']) && $context !== 'validate') {
1324 $params['condition'] = 'country_id IN (' . implode(',', (array) $props['country_id']) . ')';
1328 // Filter country list based on site defaults
1331 // change $fieldName to DB specific names.
1332 $fieldName = 'country_id';
1333 if ($context != 'get' && $context != 'validate') {
1334 $config = CRM_Core_Config
::singleton();
1335 if (!empty($config->countryLimit
) && is_array($config->countryLimit
)) {
1336 $params['condition'] = 'id IN (' . implode(',', $config->countryLimit
) . ')';
1341 // Filter county list based on chosen state
1343 if (!empty($props['state_province_id'])) {
1344 $params['condition'] = 'state_province_id IN (' . implode(',', (array) $props['state_province_id']) . ')';
1348 // Not a real field in this entity
1349 case 'world_region':
1351 case 'worldregion_id':
1352 return CRM_Core_BAO_Country
::buildOptions('region_id', $context, $props);
1354 return CRM_Core_PseudoConstant
::get(__CLASS__
, $fieldName, $params, $context);
1358 * Add data from the configured geocoding provider.
1360 * Generally this means latitude & longitude data.
1362 * @param array $params
1364 * TRUE if params could be passed to a provider, else FALSE.
1366 public static function addGeocoderData(&$params) {
1368 $provider = CRM_Utils_GeocodeProvider
::getConfiguredProvider();
1370 catch (CRM_Core_Exception
$e) {
1373 $provider::format($params);