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