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