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