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