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