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