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