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