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