Merge pull request #21562 from jitendrapurohit/notify-user
[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());
e9ff5391 69
be41af0b 70 // (prevent chaining 1 and 3) CRM-21214
6b60d8a3 71 if (isset($params['master_id']) && !CRM_Utils_System::isNull($params['master_id'])) {
38e60804 72 self::fixSharedAddress($params);
810c57a2
D
73 }
74
6a488035 75 $address->copyValues($params);
6a488035
TO
76 $address->save();
77
78 if ($address->id) {
6cee3c2c
SV
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);
82 }
83
2c59f0ca
CW
84 if (isset($params['custom'])) {
85 $addressCustom = $params['custom'];
86 }
87 else {
317103ab
CW
88 $customFields = CRM_Core_BAO_CustomField::getFields('Address', FALSE, TRUE, NULL, NULL,
89 FALSE, FALSE, $checkPermissions ? CRM_Core_Permission::EDIT : FALSE);
2c59f0ca
CW
90
91 if (!empty($customFields)) {
92 $addressCustom = CRM_Core_BAO_CustomField::postProcess($params,
93 $address->id,
94 'Address',
95 FALSE,
96 $checkPermissions
97 );
98 }
6a488035
TO
99 }
100 if (!empty($addressCustom)) {
c01fcb87 101 CRM_Core_BAO_CustomValueTable::store($addressCustom, 'civicrm_address', $address->id, $hook);
6a488035
TO
102 }
103
810c57a2
D
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
38e60804 106 // (prevent chaining 2) CRM-21214
29b23231 107 self::processSharedAddress($address->id, $params, $hook);
6a488035 108
6a488035
TO
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);
111 }
112
113 return $address;
114 }
115
116 /**
fe482240 117 * Format the address params to have reasonable values.
6a488035 118 *
6a0b768e
TO
119 * @param array $params
120 * (reference ) an assoc array of name/value pairs.
6a488035 121 */
00be9182 122 public static function fixAddress(&$params) {
a7488080 123 if (!empty($params['billing_street_address'])) {
b44e3f84 124 //Check address is coming from online contribution / registration page
6a488035 125 //Fixed :CRM-5076
be2fb01f 126 $billing = [
6a488035
TO
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',
be2fb01f 134 ];
6a488035
TO
135
136 foreach ($billing as $key => $val) {
137 if ($value = CRM_Utils_Array::value($val, $params)) {
a7488080 138 if (!empty($params[$key])) {
6a488035
TO
139 unset($params[$val]);
140 }
141 else {
142 //add new key and removed old
143 $params[$key] = $value;
144 unset($params[$val]);
145 }
146 }
147 }
148 }
149
150 /* Split the zip and +4, if it's in US format */
a7488080 151 if (!empty($params['postal_code']) &&
6a488035
TO
152 preg_match('/^(\d{4,5})[+-](\d{4})$/',
153 $params['postal_code'],
154 $match
155 )
156 ) {
157 $params['postal_code'] = $match[1];
158 $params['postal_code_suffix'] = $match[2];
159 }
160
161 // add country id if not set
162 if ((!isset($params['country_id']) || !is_numeric($params['country_id'])) &&
163 isset($params['country'])
164 ) {
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);
171 }
172 $params['country_id'] = $country->id;
173 }
174
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'])
178 ) {
179 if (!empty($params['state_province'])) {
180 $state_province = new CRM_Core_DAO_StateProvince();
181 $state_province->name = $params['state_province'];
182
183 // add country id if present
184 if (!empty($params['country_id'])) {
185 $state_province->country_id = $params['country_id'];
186 }
187
188 if (!$state_province->find(TRUE)) {
189 unset($state_province->name);
190 $state_province->abbreviation = $params['state_province'];
191 $state_province->find(TRUE);
192 }
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;
197 }
198 }
199 else {
200 $params['state_province_id'] = 'null';
201 }
202 }
203
204 // add county id if county is set
205 // CRM-7837
206 if ((!isset($params['county_id']) || !is_numeric($params['county_id']))
207 && isset($params['county']) && !empty($params['county'])
208 ) {
209 $county = new CRM_Core_DAO_County();
210 $county->name = $params['county'];
211
212 if (isset($params['state_province_id'])) {
213 $county->state_province_id = $params['state_province_id'];
214 }
215
216 if ($county->find(TRUE)) {
217 $params['county_id'] = $county->id;
218 }
219 }
220
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']))
226 ) {
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'],
231 'country_id'
232 );
233 }
234
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';
240 }
241 elseif (!is_numeric($params['state_province_id']) ||
242 ((int ) $params['state_province_id'] < 1000)
243 ) {
244 // CRM-3393 ( the hacky 1000 check)
245 $params['state_province_id'] = 'null';
246 }
247 }
248
249 if (isset($params['country_id'])) {
250 if (empty($params['country_id'])) {
251 $params['country_id'] = 'null';
252 }
253 elseif (!is_numeric($params['country_id']) ||
254 ((int ) $params['country_id'] < 1000)
255 ) {
256 // CRM-3393 ( the hacky 1000 check)
257 $params['country_id'] = 'null';
258 }
259 }
260
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']);
264 }
265
266 if (isset($params['country_id']) && is_numeric($params['country_id'])) {
267 $params['country'] = CRM_Core_PseudoConstant::country($params['country_id']);
268 }
269
aaffa79f 270 $asp = Civi::settings()->get('address_standardization_provider');
6a488035
TO
271 // clean up the address via USPS web services if enabled
272 if ($asp === 'USPS' &&
273 $params['country_id'] == 1228
274 ) {
275 CRM_Utils_Address_USPS::checkAddress($params);
e6f202ae
JM
276 }
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,
282 'address_options'
283 ),
284 FALSE
285 );
286
287 if ($parseStreetAddress && !empty($params['street_address'])) {
cf9ccf98 288 foreach (['street_number', 'street_name', 'street_unit', 'street_number_suffix'] as $fld) {
e6f202ae 289 unset($params[$fld]);
6a488035 290 }
e6f202ae 291 // main parse string.
9c1bc317 292 $parseString = $params['street_address'] ?? NULL;
e6f202ae
JM
293 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($parseString);
294
295 // merge parse address in to main address block.
296 $params = array_merge($params, $parsedFields);
6a488035
TO
297 }
298
4882d275
FG
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);
6a488035 303 }
4882d275 304
6a488035
TO
305 }
306
307 /**
fe482240 308 * Check if there is data to create the object.
6a488035 309 *
6a0b768e
TO
310 * @param array $params
311 * (reference ) an assoc array of name/value pairs.
6a488035 312 *
5c766a0b 313 * @return bool
6a488035 314 */
00be9182 315 public static function dataExists(&$params) {
6a488035
TO
316 //check if location type is set if not return false
317 if (!isset($params['location_type_id'])) {
318 return FALSE;
319 }
320
321 $config = CRM_Core_Config::singleton();
322 foreach ($params as $name => $value) {
be2fb01f 323 if (in_array($name, [
353ffa53
TO
324 'is_primary',
325 'location_type_id',
326 'id',
327 'contact_id',
328 'is_billing',
329 'display',
af9b09df 330 'master_id',
be2fb01f 331 ])) {
6a488035
TO
332 continue;
333 }
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
338 // iso code
d14f0a66 339 $defaultCountry = CRM_Core_BAO_Country::defaultContactCountry();
6a488035 340 // full name
d14f0a66 341 $defaultCountryName = CRM_Core_BAO_Country::defaultContactCountryName();
6a488035
TO
342
343 if ($defaultCountry) {
344 if ($value == $defaultCountry ||
345 $value == $defaultCountryName ||
346 $value == $config->defaultContactCountry
347 ) {
348 // do nothing
349 }
350 else {
351 return TRUE;
352 }
353 }
354 else {
355 // return if null default
356 return TRUE;
357 }
358 }
359 else {
360 return TRUE;
361 }
362 }
363 }
364
365 return FALSE;
366 }
367
368 /**
369 * Given the list of params in the params array, fetch the object
370 * and store the values in the values array
371 *
6a0b768e
TO
372 * @param array $entityBlock
373 * Associated array of fields.
374 * @param bool $microformat
375 * If microformat output is required.
e63aff1c 376 * @param int|string $fieldName conditional field name
6a488035 377 *
a6c01b45
CW
378 * @return array
379 * array with address fields
6a488035 380 */
00be9182 381 public static function &getValues($entityBlock, $microformat = FALSE, $fieldName = 'contact_id') {
6a488035
TO
382 if (empty($entityBlock)) {
383 return NULL;
384 }
be2fb01f 385 $addresses = [];
6a488035
TO
386 $address = new CRM_Core_BAO_Address();
387
a7488080 388 if (empty($entityBlock['entity_table'])) {
9c1bc317 389 $address->$fieldName = $entityBlock[$fieldName] ?? NULL;
6a488035
TO
390 }
391 else {
be2fb01f 392 $addressIds = [];
6a488035
TO
393 $addressIds = self::allEntityAddress($entityBlock);
394
395 if (!empty($addressIds[1])) {
396 $address->id = $addressIds[1];
397 }
398 else {
399 return $addresses;
400 }
401 }
001a6635
JV
402 if (isset($entityBlock['is_billing']) && $entityBlock['is_billing'] == 1) {
403 $address->orderBy('is_billing desc, id');
404 }
405 else {
406 //get primary address as a first block.
407 $address->orderBy('is_primary desc, id');
408 }
6a488035
TO
409
410 $address->find();
411
ac44bb1c 412 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
6a488035
TO
413 $count = 1;
414 while ($address->fetch()) {
415 // deprecate reference.
416 if ($count > 1) {
cf9ccf98 417 foreach (['state', 'state_name', 'country', 'world_region'] as $fld) {
4f99ca55
TO
418 if (isset($address->$fld)) {
419 unset($address->$fld);
2aa397bc 420 }
6a488035
TO
421 }
422 }
423 $stree = $address->street_address;
be2fb01f 424 $values = [];
6a488035
TO
425 CRM_Core_DAO::storeValues($address, $values);
426
427 // add state and country information: CRM-369
ac44bb1c 428 if (!empty($address->location_type_id)) {
9c1bc317 429 $values['location_type'] = $locationTypes[$address->location_type_id] ?? NULL;
ac44bb1c 430 }
6a488035
TO
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);
66bc5236
ML
434 $values['state_province_abbreviation'] = $address->state;
435 $values['state_province'] = $address->state_name;
6a488035
TO
436 }
437
438 if (!empty($address->country_id)) {
439 $address->country = CRM_Core_PseudoConstant::country($address->country_id);
66bc5236 440 $values['country'] = $address->country;
6a488035
TO
441
442 //get world region
443 $regionId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', $address->country_id, 'region_id');
66bc5236 444 $values['world_region'] = CRM_Core_PseudoConstant::worldregion($regionId);
6a488035
TO
445 }
446
447 $address->addDisplay($microformat);
448
449 $values['display'] = $address->display;
450 $values['display_text'] = $address->display_text;
451
6b60d8a3 452 if (isset($address->master_id) && !CRM_Utils_System::isNull($address->master_id)) {
6a488035
TO
453 $values['use_shared_address'] = 1;
454 }
455
456 $addresses[$count] = $values;
457
96ca8376
MW
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!
6a488035 460 if ($count > 1) {
96ca8376 461 $addresses[$count]['is_primary'] = 0;
6a488035
TO
462 }
463
464 $count++;
465 }
466
467 return $addresses;
468 }
469
470 /**
192d36c5 471 * Add the formatted address to $this-> display.
6a488035 472 *
2a6da8d7 473 * @param bool $microformat
192d36c5 474 * Unexplained parameter that I've always wondered about.
6a488035 475 */
00be9182 476 public function addDisplay($microformat = FALSE) {
be2fb01f 477 $fields = [
6a488035
TO
478 // added this for CRM 1200
479 'address_id' => $this->id,
480 // CRM-4003
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,
207f62c6 485 'supplemental_address_3' => $this->supplemental_address_3,
6a488035 486 'city' => $this->city,
2e1f50d6
CW
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 ?? "",
be2fb01f 493 ];
6a488035
TO
494
495 if (isset($this->county_id) && $this->county_id) {
496 $fields['county'] = CRM_Core_PseudoConstant::county($this->county_id);
497 }
498 else {
499 $fields['county'] = NULL;
500 }
501
502 $this->display = CRM_Utils_Address::format($fields, NULL, $microformat);
503 $this->display_text = CRM_Utils_Address::format($fields);
504 }
505
506 /**
507 * Get all the addresses for a specified contact_id, with the primary address being first
508 *
6a0b768e
TO
509 * @param int $id
510 * The contact id.
6a488035 511 *
dfcbddc8
DG
512 * @param bool $updateBlankLocInfo
513 *
a6c01b45 514 * @return array
dfcbddc8 515 * the array of adrress data
6a488035 516 */
dfcbddc8 517 public static function allAddress($id, $updateBlankLocInfo = FALSE) {
6a488035
TO
518 if (!$id) {
519 return NULL;
520 }
521
522 $query = "
523SELECT civicrm_address.id as address_id, civicrm_address.location_type_id as location_type_id
524FROM civicrm_contact, civicrm_address
525WHERE civicrm_address.contact_id = civicrm_contact.id AND civicrm_contact.id = %1
526ORDER BY civicrm_address.is_primary DESC, address_id ASC";
be2fb01f 527 $params = [1 => [$id, 'Integer']];
dfcbddc8 528
be2fb01f 529 $addresses = [];
6a488035 530 $dao = CRM_Core_DAO::executeQuery($query, $params);
dfcbddc8 531 $count = 1;
6a488035 532 while ($dao->fetch()) {
dfcbddc8
DG
533 if ($updateBlankLocInfo) {
534 $addresses[$count++] = $dao->address_id;
535 }
536 else {
537 $addresses[$dao->location_type_id] = $dao->address_id;
538 }
6a488035
TO
539 }
540 return $addresses;
541 }
542
543 /**
544 * Get all the addresses for a specified location_block id, with the primary address being first
545 *
6a0b768e
TO
546 * @param array $entityElements
547 * The array containing entity_id and.
16b10e64 548 * entity_table name
6a488035 549 *
a6c01b45 550 * @return array
dfcbddc8 551 * the array of adrress data
6a488035 552 */
00be9182 553 public static function allEntityAddress(&$entityElements) {
be2fb01f 554 $addresses = [];
6a488035
TO
555 if (empty($entityElements)) {
556 return $addresses;
557 }
558
559 $entityId = $entityElements['entity_id'];
560 $entityTable = $entityElements['entity_table'];
561
562 $sql = "
dfcbddc8 563SELECT civicrm_address.id as address_id
6a488035
TO
564FROM civicrm_loc_block loc, civicrm_location_type ltype, civicrm_address, {$entityTable} ev
565WHERE ev.id = %1
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
569ORDER BY civicrm_address.is_primary DESC, civicrm_address.location_type_id DESC, address_id ASC ";
570
be2fb01f 571 $params = [1 => [$entityId, 'Integer']];
6a488035 572 $dao = CRM_Core_DAO::executeQuery($sql, $params);
dfcbddc8 573 $locationCount = 1;
6a488035 574 while ($dao->fetch()) {
dfcbddc8
DG
575 $addresses[$locationCount] = $dao->address_id;
576 $locationCount++;
6a488035
TO
577 }
578 return $addresses;
579 }
580
6a488035 581 /**
fe482240 582 * Get address sequence.
6a488035 583 *
a6c01b45 584 * @return array
16b10e64 585 * Array of address sequence.
6a488035 586 */
00be9182 587 public static function addressSequence() {
9b118398 588 $addressSequence = CRM_Utils_Address::sequence(\Civi::settings()->get('address_format'));
6a488035
TO
589
590 $countryState = $cityPostal = FALSE;
591 foreach ($addressSequence as $key => $field) {
592 if (
be2fb01f 593 in_array($field, ['country', 'state_province']) &&
6a488035
TO
594 !$countryState
595 ) {
596 $countryState = TRUE;
597 $addressSequence[$key] = 'country_state_province';
598 }
599 elseif (
be2fb01f 600 in_array($field, ['city', 'postal_code']) &&
6a488035
TO
601 !$cityPostal
602 ) {
603 $cityPostal = TRUE;
604 $addressSequence[$key] = 'city_postal_code';
605 }
606 elseif (
be2fb01f 607 in_array($field, ['country', 'state_province', 'city', 'postal_code'])
6a488035
TO
608 ) {
609 unset($addressSequence[$key]);
610 }
611 }
612
613 return $addressSequence;
614 }
615
616 /**
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"
620 *
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
623 *
6c552737
TO
624 * @param string $streetAddress
625 * Street address including number and apt.
626 * @param string $locale
627 * Locale used to parse address.
6a488035 628 *
a6c01b45
CW
629 * @return array
630 * parsed fields values.
6a488035 631 */
00be9182 632 public static function parseStreetAddress($streetAddress, $locale = NULL) {
1d148cb9
O
633 // use 'en_US' for address parsing if the requested locale is not supported.
634 if (!self::isSupportedParsingLocale($locale)) {
6a488035
TO
635 $locale = 'en_US';
636 }
1d148cb9 637
be2fb01f 638 $emptyParseFields = $parseFields = [
6a488035
TO
639 'street_name' => '',
640 'street_unit' => '',
641 'street_number' => '',
642 'street_number_suffix' => '',
be2fb01f 643 ];
6a488035
TO
644
645 if (empty($streetAddress)) {
646 return $parseFields;
647 }
648
649 $streetAddress = trim($streetAddress);
650
be2fb01f 651 $matches = [];
cf9ccf98
TO
652 if (in_array($locale, ['en_CA', 'fr_CA'])
653 && preg_match('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', $streetAddress, $matches)
353ffa53 654 ) {
6a488035
TO
655 $parseFields['street_unit'] = $matches[1];
656 // unset from rest of street address
657 $streetAddress = preg_replace('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', '', $streetAddress);
658 }
659
660 // get street number and suffix.
be2fb01f 661 $matches = [];
6a488035
TO
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];
667
668 // get street number.
be2fb01f 669 $matches = [];
6a488035
TO
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);
674 }
675
676 // unset from main street address.
677 $streetAddress = preg_replace('/^[A-Za-z0-9]+([\S]+)/', '', $streetAddress);
678 $streetAddress = trim($streetAddress);
679 }
680 }
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);
686 }
687
d01ae020 688 // If street number is too large, we cannot store it.
689 if ($parseFields['street_number'] > CRM_Utils_Type::INT_MAX) {
690 return $emptyParseFields;
691 }
692
6a488035 693 // suffix might be like 1/2
be2fb01f 694 $matches = [];
6a488035
TO
695 if (preg_match('/^\d\/\d/', $streetAddress, $matches)) {
696 $parseFields['street_number_suffix'] .= $matches[0];
697
698 // unset from main street address.
699 $streetAddress = preg_replace('/^\d+\/\d+/', '', $streetAddress);
700 $streetAddress = trim($streetAddress);
701 }
702
703 // now get the street unit.
704 // supportable street unit formats.
be2fb01f 705 $streetUnitFormats = [
353ffa53
TO
706 'APT',
707 'APARTMENT',
708 'BSMT',
709 'BASEMENT',
710 'BLDG',
711 'BUILDING',
712 'DEPT',
713 'DEPARTMENT',
714 'FL',
715 'FLOOR',
716 'FRNT',
717 'FRONT',
718 'HNGR',
719 'HANGER',
720 'LBBY',
721 'LOBBY',
722 'LOWR',
723 'LOWER',
724 'OFC',
725 'OFFICE',
726 'PH',
727 'PENTHOUSE',
728 'TRLR',
729 'TRAILER',
730 'UPPR',
731 'RM',
732 'ROOM',
733 'SIDE',
734 'SLIP',
735 'KEY',
736 'LOT',
737 'PIER',
738 'REAR',
739 'SPC',
740 'SPACE',
741 'STOP',
742 'STE',
743 'SUITE',
744 'UNIT',
745 '#',
be2fb01f 746 ];
6a488035
TO
747
748 // overwriting $streetUnitFormats for 'en_CA' and 'fr_CA' locale
be2fb01f 749 if (in_array($locale, [
353ffa53 750 'en_CA',
af9b09df 751 'fr_CA',
be2fb01f
CW
752 ])) {
753 $streetUnitFormats = ['APT', 'APP', 'SUITE', 'BUREAU', 'UNIT'];
6a488035 754 }
689cecfd
EM
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
6a488035 757 $streetUnitPreg = '/(' . implode('|\s', $streetUnitFormats) . ')(.+)?/i';
be2fb01f 758 $matches = [];
6a488035
TO
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);
763 }
764
765 // consider remaining string as street name.
766 $parseFields['street_name'] = $streetAddress;
767
768 //run parsed fields through stripSpaces to clean
769 foreach ($parseFields as $parseField => $value) {
770 $parseFields[$parseField] = CRM_Utils_String::stripSpaces($value);
771 }
689cecfd
EM
772 //CRM-14459 if the field is too long we should assume it didn't get it right & skip rather than allow
773 // the DB to fatal
774 $fields = CRM_Core_BAO_Address::fields();
775 foreach ($fields as $fieldname => $field) {
22e263ad 776 if (!empty($field['maxlength']) && strlen(CRM_Utils_Array::value($fieldname, $parseFields)) > $field['maxlength']) {
689cecfd
EM
777 return $emptyParseFields;
778 }
779 }
6a488035
TO
780
781 return $parseFields;
782 }
783
1d148cb9
O
784 /**
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.
789 *
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
795 *
796 * @param string $locale
797 * The locale to be checked
798 *
9a208ac3 799 * @return bool
1d148cb9
O
800 */
801 public static function isSupportedParsingLocale($locale = NULL) {
802 if (!$locale) {
803 $config = CRM_Core_Config::singleton();
804 $locale = $config->lcMessages;
805 }
806
be2fb01f 807 $parsingSupportedLocales = ['en_US', 'en_CA', 'fr_CA'];
1d148cb9
O
808
809 if (in_array($locale, $parsingSupportedLocales)) {
810 return TRUE;
811 }
812
813 return FALSE;
814 }
815
6a488035 816 /**
eceb18cc 817 * Validate the address fields based on the address options enabled.
6a488035
TO
818 * in the Address Settings
819 *
6a0b768e
TO
820 * @param array $fields
821 * An array of importable/exportable contact fields.
6a488035 822 *
a6c01b45
CW
823 * @return array
824 * an array of contact fields and only the enabled address options
6a488035 825 */
00be9182 826 public static function validateAddressOptions($fields) {
6a488035
TO
827 static $addressOptions = NULL;
828 if (!$addressOptions) {
6c552737
TO
829 $addressOptions = CRM_Core_BAO_Setting::valueOptions(
830 CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
831 'address_options'
832 );
6a488035
TO
833 }
834
835 if (is_array($fields) && !empty($fields)) {
836 foreach ($addressOptions as $key => $value) {
837 if (!$value && isset($fields[$key])) {
838 unset($fields[$key]);
839 }
840 }
841 }
842 return $fields;
843 }
844
845 /**
fe482240 846 * Check if current address is used by any other contacts.
6a488035 847 *
6a0b768e
TO
848 * @param int $addressId
849 * Address id.
6a488035 850 *
72b3a70c
CW
851 * @return int
852 * count of contacts that use this shared address
6a488035 853 */
00be9182 854 public static function checkContactSharedAddress($addressId) {
6a488035 855 $query = 'SELECT count(id) FROM civicrm_address WHERE master_id = %1';
be2fb01f 856 return CRM_Core_DAO::singleValueQuery($query, [1 => [$addressId, 'Integer']]);
6a488035
TO
857 }
858
859 /**
fe482240 860 * Check if current address fields are shared with any other address.
6a488035 861 *
6a0b768e
TO
862 * @param array $fields
863 * Address fields in profile.
864 * @param int $contactId
865 * Contact id.
6a488035 866 *
6a488035 867 */
00be9182 868 public static function checkContactSharedAddressFields(&$fields, $contactId) {
6a488035
TO
869 if (!$contactId || !is_array($fields) || empty($fields)) {
870 return;
871 }
872
be2fb01f 873 $sharedLocations = [];
6a488035
TO
874
875 $query = "
876SELECT is_primary,
877 location_type_id
878 FROM civicrm_address
879 WHERE contact_id = %1
880 AND master_id IS NOT NULL";
881
be2fb01f 882 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$contactId, 'Positive']]);
6a488035
TO
883 while ($dao->fetch()) {
884 $sharedLocations[$dao->location_type_id] = $dao->location_type_id;
885 if ($dao->is_primary) {
886 $sharedLocations['Primary'] = 'Primary';
887 }
888 }
889
890 //no need to process further.
891 if (empty($sharedLocations)) {
892 return;
893 }
894
be2fb01f 895 $addressFields = [
6a488035
TO
896 'city',
897 'county',
898 'country',
899 'geo_code_1',
900 'geo_code_2',
901 'postal_code',
902 'address_name',
903 'state_province',
904 'street_address',
905 'postal_code_suffix',
906 'supplemental_address_1',
907 'supplemental_address_2',
207f62c6 908 'supplemental_address_3',
be2fb01f 909 ];
6a488035
TO
910
911 foreach ($fields as $name => & $values) {
912 if (!is_array($values) || empty($values)) {
913 continue;
914 }
915
916 $nameVal = explode('-', $values['name']);
9c1bc317
CW
917 $fldName = $nameVal[0] ?? NULL;
918 $locType = $nameVal[1] ?? NULL;
a7488080 919 if (!empty($values['location_type_id'])) {
6a488035
TO
920 $locType = $values['location_type_id'];
921 }
922
923 if (in_array($fldName, $addressFields) &&
924 in_array($locType, $sharedLocations)
925 ) {
926 $values['is_shared'] = TRUE;
927 }
928 }
929 }
930
38e60804
D
931 /**
932 * Fix the shared address if address is already shared
933 * or if address will be shared with itself.
934 *
935 * @param array $params
936 * Associated array of address params.
937 */
938 public static function fixSharedAddress(&$params) {
be41af0b
D
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;
38e60804
D
943 }
944
be41af0b
D
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']) {
68499476 947 $params['master_id'] = NULL;
38e60804
D
948 CRM_Core_Session::setStatus(ts("You can't connect an address to itself"), '', 'warning');
949 }
950 }
01853fc8 951
6a488035 952 /**
fe482240 953 * Update the shared addresses if master address is modified.
6a488035 954 *
6a0b768e
TO
955 * @param int $addressId
956 * Address id.
957 * @param array $params
958 * Associated array of address params.
29b23231 959 * @param string $parentOperation Operation being taken on the parent entity.
6a488035 960 */
29b23231 961 public static function processSharedAddress($addressId, $params, $parentOperation = NULL) {
810c57a2 962 $query = 'SELECT id, contact_id FROM civicrm_address WHERE master_id = %1';
be2fb01f 963 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$addressId, 'Integer']]);
6a488035 964
1e2c74e1 965 // legacy - for api backward compatibility
0403f8a3 966 if (!isset($params['add_relationship']) && isset($params['update_current_employer'])) {
1e2c74e1
SV
967 // warning
968 CRM_Core_Error::deprecatedFunctionWarning('update_current_employer is deprecated, use add_relationship instead');
969 $params['add_relationship'] = $params['update_current_employer'];
970 }
971
e0fb9216 972 // Default to TRUE if not set to maintain api backward compatibility.
2e1f50d6 973 $createRelationship = $params['add_relationship'] ?? TRUE;
e0fb9216 974
6a488035 975 // unset contact id
be2fb01f 976 $skipFields = ['is_primary', 'location_type_id', 'is_billing', 'contact_id'];
6b60d8a3 977 if (isset($params['master_id']) && !CRM_Utils_System::isNull($params['master_id'])) {
e0fb9216
CW
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']);
981 }
8156e86b
D
982 }
983 else {
be41af0b 984 // else no new shares will be created, only update shared addresses
810c57a2
D
985 $skipFields[] = 'master_id';
986 }
6a488035
TO
987 foreach ($skipFields as $value) {
988 unset($params[$value]);
989 }
990
991 $addressDAO = new CRM_Core_DAO_Address();
992 while ($dao->fetch()) {
b18c70b3 993 // call the function to update the relationship
e0fb9216 994 if ($createRelationship && isset($params['master_id']) && !CRM_Utils_System::isNull($params['master_id'])) {
810c57a2
D
995 self::processSharedAddressRelationship($params['master_id'], $dao->contact_id);
996 }
6a488035
TO
997 $addressDAO->copyValues($params);
998 $addressDAO->id = $dao->id;
999 $addressDAO->save();
29b23231 1000 $addressDAO->copyCustomFields($addressId, $addressDAO->id, $parentOperation);
6a488035
TO
1001 }
1002 }
1003
8bdfc216 1004 /**
fe482240 1005 * Merge contacts with the Same address to get one shared label.
6a0b768e
TO
1006 * @param array $rows
1007 * Array[contact_id][contactDetails].
8bdfc216 1008 */
1009 public static function mergeSameAddress(&$rows) {
be2fb01f 1010 $uniqueAddress = [];
8bdfc216 1011 foreach (array_keys($rows) as $rowID) {
1012 // load complete address as array key
6c552737
TO
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']);
8bdfc216 1018 if (isset($rows[$rowID]['last_name'])) {
1019 $name = $rows[$rowID]['last_name'];
1020 }
1021 else {
1022 $name = $rows[$rowID]['display_name'];
1023 }
1024
1025 // CRM-15120
be2fb01f 1026 $formatted = [
8bdfc216 1027 'first_name' => $rows[$rowID]['first_name'],
21dfd5f5 1028 'individual_prefix' => $rows[$rowID]['individual_prefix'],
be2fb01f 1029 ];
aaffa79f 1030 $format = Civi::settings()->get('display_name_format');
4c49535e 1031 $firstNameWithPrefix = CRM_Utils_Address::format($formatted, $format, FALSE, FALSE);
8bdfc216 1032 $firstNameWithPrefix = trim($firstNameWithPrefix);
1033
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
1041 }
1042 else {
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'];
1046 }
1047 }
1048 foreach ($uniqueAddress as $address => $data) {
1049 // copy data back to $rows
1050 $count = 0;
1051 // one last name list per row
1052 foreach ($data['names'] as $last_name => $first_names) {
1053 // too many to list
1054 if ($count > 2) {
1055 break;
1056 }
9b873358 1057 if (count($first_names) == 1) {
2aa397bc
TO
1058 $family = $first_names[current(array_keys($first_names))]['addressee_display'];
1059 }
1060 else {
1061 // collapse the tree to summarize
1062 $family = trim(implode(" & ", array_keys($first_names)) . " " . $last_name);
1063 }
1064 if ($count) {
1065 $processedNames .= "\n" . $family;
1066 }
1067 else {
1068 // build display_name string
1069 $processedNames = $family;
1070 }
8bdfc216 1071 $count++;
1072 }
1073 $rows[$data['ID']]['addressee'] = $rows[$data['ID']]['addressee_display'] = $rows[$data['ID']]['display_name'] = $processedNames;
1074 }
1075 }
1076
6a488035 1077 /**
fe482240 1078 * Create relationship between contacts who share an address.
6a488035 1079 *
810c57a2
D
1080 * Note that currently we create relationship between
1081 * Individual + Household and Individual + Organization
6a488035 1082 *
6a0b768e
TO
1083 * @param int $masterAddressId
1084 * Master address id.
810c57a2
D
1085 * @param int $currentContactId
1086 * Current contact id.
6a488035 1087 */
810c57a2 1088 public static function processSharedAddressRelationship($masterAddressId, $currentContactId) {
6a488035 1089 // get the contact type of contact being edited / created
810c57a2 1090 $currentContactType = CRM_Contact_BAO_Contact::getContactType($currentContactId);
6a488035
TO
1091
1092 // if current contact is not of type individual return
1093 if ($currentContactType != 'Individual') {
1094 return;
1095 }
1096
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
6a488035
TO
1099 $query = 'SELECT cc.id, cc.contact_type
1100 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1101 WHERE ca.id = %1';
1102
be2fb01f 1103 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$masterAddressId, 'Integer']]);
6a488035
TO
1104 $dao->fetch();
1105
810c57a2 1106 // master address contact needs to be Household or Organization, otherwise return
6a488035
TO
1107 if ($dao->contact_type == 'Individual') {
1108 return;
1109 }
1110 $sharedContactType = $dao->contact_type;
1111 $sharedContactId = $dao->id;
1112
1113 // create relationship between ontacts who share an address
1114 if ($sharedContactType == 'Organization') {
1115 return CRM_Contact_BAO_Contact_Utils::createCurrentEmployerRelationship($currentContactId, $sharedContactId);
1116 }
6a488035 1117
82ffeed5 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');
6a488035
TO
1120
1121 if (!$relTypeId) {
ac15829d 1122 throw new CRM_Core_Exception(ts("You seem to have deleted the relationship type 'Household Member of'"));
82ffeed5 1123 }
1124
be2fb01f 1125 $relParam = [
82ffeed5 1126 'is_active' => TRUE,
1127 'relationship_type_id' => $relTypeId,
1128 'contact_id_a' => $currentContactId,
1129 'contact_id_b' => $sharedContactId,
be2fb01f 1130 ];
82ffeed5 1131
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
9272d8b5 1134 if (CRM_Contact_BAO_Relationship::checkDuplicateRelationship($relParam, $currentContactId, $sharedContactId)) {
82ffeed5 1135 return;
6a488035
TO
1136 }
1137
a7ef8c9d
EM
1138 try {
1139 // create relationship
82ffeed5 1140 civicrm_api3('relationship', 'create', $relParam);
a7ef8c9d
EM
1141 }
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.
1145 }
6a488035
TO
1146 }
1147
1148 /**
fe482240 1149 * Check and set the status for shared address delete.
6a488035 1150 *
6a0b768e
TO
1151 * @param int $addressId
1152 * Address id.
1153 * @param int $contactId
1154 * Contact id.
1155 * @param bool $returnStatus
1156 * By default false.
6a488035 1157 *
a6c01b45 1158 * @return string
6a488035 1159 */
00be9182 1160 public static function setSharedAddressDeleteStatus($addressId = NULL, $contactId = NULL, $returnStatus = FALSE) {
6a488035
TO
1161 // check if address that is being deleted has any shared
1162 if ($addressId) {
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';
1167 }
1168 else {
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';
1175 }
1176
be2fb01f 1177 $dao = CRM_Core_DAO::executeQuery($query, [1 => [$entityId, 'Integer']]);
6a488035 1178
be2fb01f
CW
1179 $deleteStatus = [];
1180 $sharedContactList = [];
6a488035
TO
1181 $statusMessage = NULL;
1182 $addressCount = 0;
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.');
1186 }
1187
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>";
1191
1192 $addressCount++;
1193 }
1194
1195 if (!empty($deleteStatus)) {
1196 $statusMessage = implode('<br/>', $deleteStatus) . '<br/>';
1197 }
1198
1199 if (!$returnStatus) {
1200 CRM_Core_Session::setStatus($statusMessage, '', 'info');
1201 }
1202 else {
be2fb01f 1203 return [
6a488035
TO
1204 'contactList' => $sharedContactList,
1205 'count' => $addressCount,
be2fb01f 1206 ];
6a488035
TO
1207 }
1208 }
12445e1c
CW
1209
1210 /**
fe482240 1211 * Call common delete function.
ad37ac8e 1212 *
7a592535 1213 * @see \CRM_Contact_BAO_Contact::on_hook_civicrm_post
ad37ac8e 1214 *
7a592535
CW
1215 * @param int $id
1216 * @deprecated
ad37ac8e 1217 * @return bool
12445e1c 1218 */
00be9182 1219 public static function del($id) {
7a592535 1220 return (bool) self::deleteRecord(['id' => $id]);
12445e1c 1221 }
dc86f881
CW
1222
1223 /**
1224 * Get options for a given address field.
1225 * @see CRM_Core_DAO::buildOptions
1226 *
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?
1229 *
6a0b768e
TO
1230 * @param string $fieldName
1231 * @param string $context
a1a2a83d 1232 * @see CRM_Core_DAO::buildOptionsContext
6a0b768e 1233 * @param array $props
16b10e64 1234 * whatever is known about this dao object.
77b97be7 1235 *
5c766a0b 1236 * @return array|bool
dc86f881 1237 */
be2fb01f
CW
1238 public static function buildOptions($fieldName, $context = NULL, $props = []) {
1239 $params = [];
dc86f881
CW
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':
d0bfb983 1244 case 'state_province_name':
1245 case 'state_province':
1246 // change $fieldName to DB specific names.
1247 $fieldName = 'state_province_id';
63b56894 1248 if (empty($props['country_id']) && $context !== 'validate') {
dc86f881
CW
1249 $config = CRM_Core_Config::singleton();
1250 if (!empty($config->provinceLimit)) {
1251 $props['country_id'] = $config->provinceLimit;
1252 }
1253 else {
1254 $props['country_id'] = $config->defaultContactCountry;
1255 }
1256 }
eaf39b47 1257 if (!empty($props['country_id'])) {
62d7cac4
SL
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'));
1260 }
dc86f881
CW
1261 $params['condition'] = 'country_id IN (' . implode(',', (array) $props['country_id']) . ')';
1262 }
1263 break;
2aa397bc 1264
dc86f881
CW
1265 // Filter country list based on site defaults
1266 case 'country_id':
d0bfb983 1267 case 'country':
1268 // change $fieldName to DB specific names.
1269 $fieldName = 'country_id';
786ad6e1
CW
1270 if ($context != 'get' && $context != 'validate') {
1271 $config = CRM_Core_Config::singleton();
1272 if (!empty($config->countryLimit) && is_array($config->countryLimit)) {
62d7cac4
SL
1273 if (!CRM_Utils_Rule::commaSeparatedIntegers(implode(',', $config->countryLimit))) {
1274 throw new CRM_Core_Exception(ts('Available Country setting is incorrect'));
1275 }
786ad6e1
CW
1276 $params['condition'] = 'id IN (' . implode(',', $config->countryLimit) . ')';
1277 }
dc86f881
CW
1278 }
1279 break;
2aa397bc 1280
dc86f881
CW
1281 // Filter county list based on chosen state
1282 case 'county_id':
1283 if (!empty($props['state_province_id'])) {
62d7cac4
SL
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'));
1286 }
dc86f881
CW
1287 $params['condition'] = 'state_province_id IN (' . implode(',', (array) $props['state_province_id']) . ')';
1288 }
1289 break;
2aa397bc 1290
c7130c3e
CW
1291 // Not a real field in this entity
1292 case 'world_region':
3493947a 1293 case 'worldregion':
1294 case 'worldregion_id':
b06ca616 1295 return CRM_Core_BAO_Country::buildOptions('region_id', $context, $props);
dc86f881 1296 }
786ad6e1 1297 return CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context);
dc86f881 1298 }
96025800 1299
6d04b727
FG
1300 /**
1301 * Add data from the configured geocoding provider.
1302 *
1303 * Generally this means latitude & longitude data.
1304 *
1305 * @param array $params
1306 * @return bool
1307 * TRUE if params could be passed to a provider, else FALSE.
1308 */
1309 public static function addGeocoderData(&$params) {
1310 try {
1311 $provider = CRM_Utils_GeocodeProvider::getConfiguredProvider();
4e4c1457 1312 $providerExists = TRUE;
6d04b727
FG
1313 }
1314 catch (CRM_Core_Exception $e) {
4e4c1457 1315 $providerExists = FALSE;
1316 }
1317 if ($providerExists) {
1318 $provider::format($params);
1319 }
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);
1324 }
6d04b727 1325 }
4e4c1457 1326 return $providerExists;
6d04b727
FG
1327 }
1328
ae7397a3 1329 /**
1330 * Create multiple addresses using legacy methodology.
1331 *
1332 * @param array $params
1333 * @param bool $fixAddress
1334 *
1335 * @return array|null
1336 */
1337 public static function legacyCreate(array $params, bool $fixAddress) {
1338 if (!isset($params['address']) || !is_array($params['address'])) {
1339 return NULL;
1340 }
1341 CRM_Core_BAO_Block::sortPrimaryFirst($params['address']);
1342 $contactId = NULL;
1343
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);
1348
1349 $isPrimary = $isBilling = TRUE;
1350 $blocks = [];
1351 foreach ($params['address'] as $key => $value) {
1352 if (!is_array($value)) {
1353 continue;
1354 }
1355
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']];
1360 }
1361 }
1362
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']]);
1369 continue;
1370 }
1371 elseif (!$addressExists) {
1372 continue;
1373 }
1374
1375 if ($isPrimary && !empty($value['is_primary'])) {
1376 $isPrimary = FALSE;
1377 }
1378 else {
1379 $value['is_primary'] = 0;
1380 }
1381
1382 if ($isBilling && !empty($value['is_billing'])) {
1383 $isBilling = FALSE;
1384 }
1385 else {
1386 $value['is_billing'] = 0;
1387 }
1388
1389 if (empty($value['manual_geo_code'])) {
1390 $value['manual_geo_code'] = 0;
1391 }
1392 $value['contact_id'] = $contactId;
1393 $blocks[] = self::add($value, $fixAddress);
1394 }
1395 return $blocks;
1396 }
1397
a7488080 1398}