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