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