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