CRM-14996 - Fix display bug in adv search
[civicrm-core.git] / CRM / Core / BAO / Address.php
CommitLineData
6a488035 1<?php
6a488035
TO
2/*
3 +--------------------------------------------------------------------+
06b69b18 4 | CiviCRM version 4.5 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
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
06b69b18 31 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
32 * $Id$
33 *
34 */
35
36/**
37 * This is class to handle address related functions
38 */
39class CRM_Core_BAO_Address extends CRM_Core_DAO_Address {
40
41 /**
42 * takes an associative array and creates a address
43 *
e63aff1c
EM
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
6a488035
TO
46 * before inserting in db
47 *
e63aff1c
EM
48 * @param null $entity
49 *
6a488035
TO
50 * @return array $blocks array of created address
51 * @access public
52 * @static
53 */
e63aff1c 54 static function create(&$params, $fixAddress = TRUE, $entity = NULL) {
6a488035
TO
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);
a7488080 85 if (empty($value['id'])) {
6a488035
TO
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
8cc574cf 110 if ($isPrimary && !empty($value['is_primary'])) {
6a488035
TO
111 $isPrimary = FALSE;
112 }
113 else {
114 $value['is_primary'] = 0;
115 }
116
8cc574cf 117 if ($isBilling && !empty($value['is_billing'])) {
6a488035
TO
118 $isBilling = FALSE;
119 }
120 else {
121 $value['is_billing'] = 0;
122 }
123
a7488080 124 if (empty($value['manual_geo_code'])) {
6a488035
TO
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) {
a7488080 208 if (!empty($params['billing_street_address'])) {
6a488035
TO
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)) {
a7488080 223 if (!empty($params[$key])) {
6a488035
TO
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 */
a7488080 236 if (!empty($params['postal_code']) &&
6a488035
TO
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
ef0d4de8
DL
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 );
6a488035
TO
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 *
e63aff1c
EM
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
6a488035
TO
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
a7488080 473 if (empty($entityBlock['entity_table'])) {
6a488035
TO
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 *
2a6da8d7
EM
546 * @param bool $microformat
547 *
548 * @internal param $NULL
6a488035
TO
549 *
550 * @return void
551 *
552 * @access public
6a488035
TO
553 */
554 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 the contact id
587 *
2a6da8d7
EM
588 * @param bool $updateBlankLocInfo
589 *
6a488035
TO
590 * @return array the array of adrress data
591 * @access public
592 * @static
593 */
594 static function allAddress($id, $updateBlankLocInfo = FALSE) {
595 if (!$id) {
596 return NULL;
597 }
598
599 $query = "
600SELECT civicrm_address.id as address_id, civicrm_address.location_type_id as location_type_id
601FROM civicrm_contact, civicrm_address
602WHERE civicrm_address.contact_id = civicrm_contact.id AND civicrm_contact.id = %1
603ORDER 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 the array containing entity_id and
624 * entity_table name
625 *
626 * @return array the array of adrress data
627 * @access public
628 * @static
629 */
630 static function allEntityAddress(&$entityElements) {
631 if (empty($entityElements)) {
632 return $addresses;
633 }
634
635 $entityId = $entityElements['entity_id'];
636 $entityTable = $entityElements['entity_table'];
637
638 $sql = "
639SELECT civicrm_address.id as address_id
640FROM civicrm_loc_block loc, civicrm_location_type ltype, civicrm_address, {$entityTable} ev
641WHERE ev.id = %1
642 AND loc.id = ev.loc_block_id
643 AND civicrm_address.id IN (loc.address_id, loc.address_2_id)
644 AND ltype.id = civicrm_address.location_type_id
645ORDER BY civicrm_address.is_primary DESC, civicrm_address.location_type_id DESC, address_id ASC ";
646
647 $params = array(1 => array($entityId, 'Integer'));
648 $addresses = array();
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
b5c2afd0
EM
658 /**
659 * @param $stateCountryMap
660 * @param null $defaults
661 */
bfda9521 662 static function addStateCountryMap($stateCountryMap, $defaults = NULL) {
6a488035
TO
663 // first fix the statecountry map if needed
664 if (empty($stateCountryMap)) {
665 return;
666 }
667
668 $config = CRM_Core_Config::singleton();
669 if (!isset($config->stateCountryMap)) {
670 $config->stateCountryMap = array();
671 }
672
673 $config->stateCountryMap = array_merge($config->stateCountryMap, $stateCountryMap);
674 }
675
b5c2afd0
EM
676 /**
677 * @param $form
678 * @param $defaults
679 * @param bool $batchFieldNames
680 */
c5e02dc5 681 static function fixAllStateSelects(&$form, $defaults, $batchFieldNames = false) {
6a488035 682 $config = CRM_Core_Config::singleton();
c5d184af
DL
683 $map = null;
684 if (is_array($batchFieldNames)) {
685 $map = $batchFieldNames;
686 }
fe3098fd 687 elseif (!empty($config->stateCountryMap)) {
c5d184af
DL
688 $map = $config->stateCountryMap;
689 }
c5e02dc5
AH
690 if (!empty($map)) {
691 foreach ($map as $index => $match) {
fe3098fd
PJ
692 if (array_key_exists('state_province', $match)
693 || array_key_exists('country', $match)
694 || array_key_exists('county', $match)
6a488035 695 ) {
fe3098fd
PJ
696 $countryElementName = CRM_Utils_Array::value('country', $match);
697 $stateProvinceElementName = CRM_Utils_Array::value('state_province', $match);
698 $countyElementName = CRM_Utils_Array::value('county', $match);
6a488035
TO
699 CRM_Contact_Form_Edit_Address::fixStateSelect(
700 $form,
fe3098fd
PJ
701 $countryElementName,
702 $stateProvinceElementName,
703 $countyElementName,
704 CRM_Utils_Array::value($countryElementName, $defaults),
705 CRM_Utils_Array::value($stateProvinceElementName, $defaults)
6a488035
TO
706 );
707 }
708 else {
709 unset($config->stateCountryMap[$index]);
710 }
711 }
712 }
713 }
714
715 /**
716 * Function to get address sequence
717 *
718 * @return array of address sequence.
719 */
720 static function addressSequence() {
721 $config = CRM_Core_Config::singleton();
722 $addressSequence = $config->addressSequence();
723
724 $countryState = $cityPostal = FALSE;
725 foreach ($addressSequence as $key => $field) {
726 if (
727 in_array($field, array('country', 'state_province')) &&
728 !$countryState
729 ) {
730 $countryState = TRUE;
731 $addressSequence[$key] = 'country_state_province';
732 }
733 elseif (
734 in_array($field, array('city', 'postal_code')) &&
735 !$cityPostal
736 ) {
737 $cityPostal = TRUE;
738 $addressSequence[$key] = 'city_postal_code';
739 }
740 elseif (
741 in_array($field, array('country', 'state_province', 'city', 'postal_code'))
742 ) {
743 unset($addressSequence[$key]);
744 }
745 }
746
747 return $addressSequence;
748 }
749
750 /**
751 * Parse given street address string in to street_name,
752 * street_unit, street_number and street_number_suffix
753 * eg "54A Excelsior Ave. Apt 1C", or "917 1/2 Elm Street"
754 *
755 * NB: civic street formats for en_CA and fr_CA used by default if those locales are active
756 * otherwise en_US format is default action
757 *
758 * @param string Street address including number and apt
759 * @param string Locale - to set locale used to parse address
760 *
761 * @return array $parseFields parsed fields values.
762 * @access public
763 * @static
764 */
765 static function parseStreetAddress($streetAddress, $locale = NULL) {
766 $config = CRM_Core_Config::singleton();
767
768 /* locales supported include:
769 * en_US - http://pe.usps.com/cpim/ftp/pubs/pub28/pub28.pdf
770 * en_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp
771 * fr_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-f.asp
772 * NB: common use of comma after street number also supported
773 * default is en_US
774 */
775
776 $supportedLocalesForParsing = array('en_US', 'en_CA', 'fr_CA');
777 if (!$locale) {
778 $locale = $config->lcMessages;
779 }
780 // as different locale explicitly requested but is not available, display warning message and set $locale = 'en_US'
781 if (!in_array($locale, $supportedLocalesForParsing)) {
782 CRM_Core_Session::setStatus(ts('Unsupported locale specified to parseStreetAddress: %1. Proceeding with en_US locale.', array(1 => $locale)), ts('Unsupported Locale'), 'alert');
783 $locale = 'en_US';
784 }
689cecfd 785 $emptyParseFields = $parseFields = array(
6a488035
TO
786 'street_name' => '',
787 'street_unit' => '',
788 'street_number' => '',
789 'street_number_suffix' => '',
790 );
791
792 if (empty($streetAddress)) {
793 return $parseFields;
794 }
795
796 $streetAddress = trim($streetAddress);
797
798 $matches = array();
799 if (in_array($locale, array(
800 'en_CA', 'fr_CA')) && preg_match('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', $streetAddress, $matches)) {
801 $parseFields['street_unit'] = $matches[1];
802 // unset from rest of street address
803 $streetAddress = preg_replace('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', '', $streetAddress);
804 }
805
806 // get street number and suffix.
807 $matches = array();
808 //alter street number/suffix handling so that we accept -digit
809 if (preg_match('/^[A-Za-z0-9]+([\S]+)/', $streetAddress, $matches)) {
810 // check that $matches[0] is numeric, else assume no street number
811 if (preg_match('/^(\d+)/', $matches[0])) {
812 $streetNumAndSuffix = $matches[0];
813
814 // get street number.
815 $matches = array();
816 if (preg_match('/^(\d+)/', $streetNumAndSuffix, $matches)) {
817 $parseFields['street_number'] = $matches[0];
818 $suffix = preg_replace('/^(\d+)/', '', $streetNumAndSuffix);
819 $parseFields['street_number_suffix'] = trim($suffix);
820 }
821
822 // unset from main street address.
823 $streetAddress = preg_replace('/^[A-Za-z0-9]+([\S]+)/', '', $streetAddress);
824 $streetAddress = trim($streetAddress);
825 }
826 }
827 elseif (preg_match('/^(\d+)/', $streetAddress, $matches)) {
828 $parseFields['street_number'] = $matches[0];
829 // unset from main street address.
830 $streetAddress = preg_replace('/^(\d+)/', '', $streetAddress);
831 $streetAddress = trim($streetAddress);
832 }
833
834 // suffix might be like 1/2
835 $matches = array();
836 if (preg_match('/^\d\/\d/', $streetAddress, $matches)) {
837 $parseFields['street_number_suffix'] .= $matches[0];
838
839 // unset from main street address.
840 $streetAddress = preg_replace('/^\d+\/\d+/', '', $streetAddress);
841 $streetAddress = trim($streetAddress);
842 }
843
844 // now get the street unit.
845 // supportable street unit formats.
846 $streetUnitFormats = array(
847 'APT', 'APARTMENT', 'BSMT', 'BASEMENT', 'BLDG', 'BUILDING',
848 'DEPT', 'DEPARTMENT', 'FL', 'FLOOR', 'FRNT', 'FRONT',
849 'HNGR', 'HANGER', 'LBBY', 'LOBBY', 'LOWR', 'LOWER',
850 'OFC', 'OFFICE', 'PH', 'PENTHOUSE', 'TRLR', 'TRAILER',
851 'UPPR', 'RM', 'ROOM', 'SIDE', 'SLIP', 'KEY',
852 'LOT', 'PIER', 'REAR', 'SPC', 'SPACE',
853 'STOP', 'STE', 'SUITE', 'UNIT', '#',
854 );
855
856 // overwriting $streetUnitFormats for 'en_CA' and 'fr_CA' locale
857 if (in_array($locale, array(
858 'en_CA', 'fr_CA'))) {
859 $streetUnitFormats = array('APT', 'APP', 'SUITE', 'BUREAU', 'UNIT');
860 }
689cecfd
EM
861 //@todo per CRM-14459 this regex picks up words with the string in them - e.g APT picks up
862 //Captain - presuming fixing regex (& adding test) to ensure a-z does not preced string will fix
6a488035
TO
863 $streetUnitPreg = '/(' . implode('|\s', $streetUnitFormats) . ')(.+)?/i';
864 $matches = array();
865 if (preg_match($streetUnitPreg, $streetAddress, $matches)) {
866 $parseFields['street_unit'] = trim($matches[0]);
867 $streetAddress = str_replace($matches[0], '', $streetAddress);
868 $streetAddress = trim($streetAddress);
869 }
870
871 // consider remaining string as street name.
872 $parseFields['street_name'] = $streetAddress;
873
874 //run parsed fields through stripSpaces to clean
875 foreach ($parseFields as $parseField => $value) {
876 $parseFields[$parseField] = CRM_Utils_String::stripSpaces($value);
877 }
689cecfd
EM
878 //CRM-14459 if the field is too long we should assume it didn't get it right & skip rather than allow
879 // the DB to fatal
880 $fields = CRM_Core_BAO_Address::fields();
881 foreach ($fields as $fieldname => $field) {
882 if(!empty($field['maxlength']) && strlen(CRM_Utils_Array::value($fieldname, $parseFields)) > $field['maxlength']) {
883 return $emptyParseFields;
884 }
885 }
6a488035
TO
886
887 return $parseFields;
888 }
889
890 /**
891 * Validate the address fields based on the address options enabled
892 * in the Address Settings
893 *
894 * @param array $fields an array of importable/exportable contact fields
895 *
896 * @return array $fields an array of contact fields and only the enabled address options
897 * @access public
898 * @static
899 */
900 static function validateAddressOptions($fields) {
901 static $addressOptions = NULL;
902 if (!$addressOptions) {
ef0d4de8
DL
903 $addressOptions =
904 CRM_Core_BAO_Setting::valueOptions(
905 CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
906 'address_options'
907 );
6a488035
TO
908 }
909
910 if (is_array($fields) && !empty($fields)) {
911 foreach ($addressOptions as $key => $value) {
912 if (!$value && isset($fields[$key])) {
913 unset($fields[$key]);
914 }
915 }
916 }
917 return $fields;
918 }
919
920 /**
921 * Check if current address is used by any other contacts
922 *
923 * @param int $addressId address id
924 *
925 * @return count of contacts that use this shared address
926 * @access public
927 * @static
928 */
929 static function checkContactSharedAddress($addressId) {
930 $query = 'SELECT count(id) FROM civicrm_address WHERE master_id = %1';
931 return CRM_Core_DAO::singleValueQuery($query, array(1 => array($addressId, 'Integer')));
932 }
933
934 /**
935 * Function to check if current address fields are shared with any other address
936 *
937 * @param array $fields address fields in profile
938 * @param int $contactId contact id
939 *
940 * @access public
941 * @static
942 */
943 static function checkContactSharedAddressFields(&$fields, $contactId) {
944 if (!$contactId || !is_array($fields) || empty($fields)) {
945 return;
946 }
947
948 $sharedLocations = array();
949
950 $query = "
951SELECT is_primary,
952 location_type_id
953 FROM civicrm_address
954 WHERE contact_id = %1
955 AND master_id IS NOT NULL";
956
957 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($contactId, 'Positive')));
958 while ($dao->fetch()) {
959 $sharedLocations[$dao->location_type_id] = $dao->location_type_id;
960 if ($dao->is_primary) {
961 $sharedLocations['Primary'] = 'Primary';
962 }
963 }
964
965 //no need to process further.
966 if (empty($sharedLocations)) {
967 return;
968 }
969
970 $addressFields = array(
971 'city',
972 'county',
973 'country',
974 'geo_code_1',
975 'geo_code_2',
976 'postal_code',
977 'address_name',
978 'state_province',
979 'street_address',
980 'postal_code_suffix',
981 'supplemental_address_1',
982 'supplemental_address_2',
983 );
984
985 foreach ($fields as $name => & $values) {
986 if (!is_array($values) || empty($values)) {
987 continue;
988 }
989
990 $nameVal = explode('-', $values['name']);
991 $fldName = CRM_Utils_Array::value(0, $nameVal);
992 $locType = CRM_Utils_Array::value(1, $nameVal);
a7488080 993 if (!empty($values['location_type_id'])) {
6a488035
TO
994 $locType = $values['location_type_id'];
995 }
996
997 if (in_array($fldName, $addressFields) &&
998 in_array($locType, $sharedLocations)
999 ) {
1000 $values['is_shared'] = TRUE;
1001 }
1002 }
1003 }
1004
1005 /**
1006 * Function to update the shared addresses if master address is modified
1007 *
1008 * @param int $addressId address id
1009 * @param array $params associated array of address params
1010 *
1011 * @return void
1012 * @access public
1013 * @static
1014 */
1015 static function processSharedAddress($addressId, $params) {
1016 $query = 'SELECT id FROM civicrm_address WHERE master_id = %1';
1017 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($addressId, 'Integer')));
1018
1019 // unset contact id
1020 $skipFields = array('is_primary', 'location_type_id', 'is_billing', 'master_id', 'contact_id');
1021 foreach ($skipFields as $value) {
1022 unset($params[$value]);
1023 }
1024
1025 $addressDAO = new CRM_Core_DAO_Address();
1026 while ($dao->fetch()) {
1027 $addressDAO->copyValues($params);
1028 $addressDAO->id = $dao->id;
1029 $addressDAO->save();
1030 $addressDAO->free();
1031 }
1032 }
1033
1034 /**
1035 * Function to create relationship between contacts who share an address
1036 *
1037 * Note that currently we create relationship only for Individual contacts
1038 * Individual + Household and Individual + Orgnization
1039 *
1040 * @param int $masterAddressId master address id
1041 * @param array $params associated array of submitted values
1042 *
1043 * @return void
1044 * @access public
1045 * @static
1046 */
1047 static function processSharedAddressRelationship($masterAddressId, $params) {
1048 if (!$masterAddressId) {
1049 return;
1050 }
1051 // get the contact type of contact being edited / created
1052 $currentContactType = CRM_Contact_BAO_Contact::getContactType($params['contact_id']);
1053 $currentContactId = $params['contact_id'];
1054
1055 // if current contact is not of type individual return
1056 if ($currentContactType != 'Individual') {
1057 return;
1058 }
1059
1060 // get the contact id and contact type of shared contact
1061 // check the contact type of shared contact, return if it is of type Individual
1062
1063 $query = 'SELECT cc.id, cc.contact_type
1064 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1065 WHERE ca.id = %1';
1066
1067 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($masterAddressId, 'Integer')));
1068
1069 $dao->fetch();
1070
1071 // if current contact is not of type individual return, since we don't create relationship between
1072 // 2 individuals
1073 if ($dao->contact_type == 'Individual') {
1074 return;
1075 }
1076 $sharedContactType = $dao->contact_type;
1077 $sharedContactId = $dao->id;
1078
1079 // create relationship between ontacts who share an address
1080 if ($sharedContactType == 'Organization') {
1081 return CRM_Contact_BAO_Contact_Utils::createCurrentEmployerRelationship($currentContactId, $sharedContactId);
1082 }
1083 else {
1084 // get the relationship type id of "Household Member of"
1085 $relationshipType = 'Household Member of';
1086 }
1087
1088 $cid = array('contact' => $currentContactId);
1089
1090 $relTypeId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', $relationshipType, 'id', 'name_a_b');
1091
1092 if (!$relTypeId) {
1093 CRM_Core_Error::fatal(ts("You seem to have deleted the relationship type '%1'", array(1 => $relationshipType)));
1094 }
1095
1096 // create relationship
1097 $relationshipParams = array(
1098 'is_active' => TRUE,
1099 'relationship_type_id' => $relTypeId . '_a_b',
1100 'contact_check' => array($sharedContactId => TRUE),
1101 );
1102
1103 list($valid, $invalid, $duplicate,
1104 $saved, $relationshipIds
1105 ) = CRM_Contact_BAO_Relationship::create($relationshipParams, $cid);
1106 }
1107
1108 /**
1109 * Function to check and set the status for shared address delete
1110 *
1111 * @param int $addressId address id
1112 * @param int $contactId contact id
1113 * @param boolean $returnStatus by default false
1114 *
1115 * @return string $statusMessage
1116 * @access public
1117 * @static
1118 */
1119 static function setSharedAddressDeleteStatus($addressId = NULL, $contactId = NULL, $returnStatus = FALSE) {
1120 // check if address that is being deleted has any shared
1121 if ($addressId) {
1122 $entityId = $addressId;
1123 $query = 'SELECT cc.id, cc.display_name
1124 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1125 WHERE ca.master_id = %1';
1126 }
1127 else {
1128 $entityId = $contactId;
1129 $query = 'SELECT cc.id, cc.display_name
1130 FROM civicrm_address ca1
1131 INNER JOIN civicrm_address ca2 ON ca1.id = ca2.master_id
1132 INNER JOIN civicrm_contact cc ON ca2.contact_id = cc.id
1133 WHERE ca1.contact_id = %1';
1134 }
1135
1136 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($entityId, 'Integer')));
1137
1138 $deleteStatus = array();
1139 $sharedContactList = array();
1140 $statusMessage = NULL;
1141 $addressCount = 0;
1142 while ($dao->fetch()) {
1143 if (empty($deleteStatus)) {
1144 $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.');
1145 }
1146
1147 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$dao->id}");
1148 $sharedContactList[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1149 $deleteStatus[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1150
1151 $addressCount++;
1152 }
1153
1154 if (!empty($deleteStatus)) {
1155 $statusMessage = implode('<br/>', $deleteStatus) . '<br/>';
1156 }
1157
1158 if (!$returnStatus) {
1159 CRM_Core_Session::setStatus($statusMessage, '', 'info');
1160 }
1161 else {
1162 return array(
1163 'contactList' => $sharedContactList,
1164 'count' => $addressCount,
1165 );
1166 }
1167 }
12445e1c
CW
1168
1169 /**
1170 * Call common delete function
1171 */
1172 static function del($id) {
a65e2e55 1173 return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('Address', $id);
12445e1c 1174 }
dc86f881
CW
1175
1176 /**
1177 * Get options for a given address field.
1178 * @see CRM_Core_DAO::buildOptions
1179 *
1180 * TODO: Should we always assume chainselect? What fn should be responsible for controlling that flow?
1181 * TODO: In context of chainselect, what to return if e.g. a country has no states?
1182 *
1183 * @param String $fieldName
77b97be7
EM
1184 * @param String $context : @see CRM_Core_DAO::buildOptionsContext
1185 * @param Array $props : whatever is known about this dao object
1186 *
1187 * @return Array|bool
dc86f881
CW
1188 */
1189 public static function buildOptions($fieldName, $context = NULL, $props = array()) {
1190 $params = array();
1191 // Special logic for fields whose options depend on context or properties
1192 switch ($fieldName) {
1193 // Filter state_province list based on chosen country or site defaults
1194 case 'state_province_id':
1195 if (empty($props['country_id'])) {
1196 $config = CRM_Core_Config::singleton();
1197 if (!empty($config->provinceLimit)) {
1198 $props['country_id'] = $config->provinceLimit;
1199 }
1200 else {
1201 $props['country_id'] = $config->defaultContactCountry;
1202 }
1203 }
1204 if (!empty($props['country_id'])) {
1205 $params['condition'] = 'country_id IN (' . implode(',', (array) $props['country_id']) . ')';
1206 }
1207 break;
1208 // Filter country list based on site defaults
1209 case 'country_id':
786ad6e1
CW
1210 if ($context != 'get' && $context != 'validate') {
1211 $config = CRM_Core_Config::singleton();
1212 if (!empty($config->countryLimit) && is_array($config->countryLimit)) {
1213 $params['condition'] = 'id IN (' . implode(',', $config->countryLimit) . ')';
1214 }
dc86f881
CW
1215 }
1216 break;
1217 // Filter county list based on chosen state
1218 case 'county_id':
1219 if (!empty($props['state_province_id'])) {
1220 $params['condition'] = 'state_province_id IN (' . implode(',', (array) $props['state_province_id']) . ')';
1221 }
1222 break;
c7130c3e
CW
1223 // Not a real field in this entity
1224 case 'world_region':
1225 return CRM_Core_PseudoConstant::worldRegion();
1226 break;
dc86f881 1227 }
786ad6e1 1228 return CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context);
dc86f881 1229 }
a7488080 1230}