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