Fix for CRM-15850
[civicrm-core.git] / CRM / Core / BAO / Address.php
CommitLineData
6a488035 1<?php
6a488035
TO
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
06b69b18 31 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
32 * $Id$
33 *
34 */
35
36/**
37 * This is class to handle address related functions
38 */
39class CRM_Core_BAO_Address extends CRM_Core_DAO_Address {
40
41 /**
fe482240 42 * Takes an associative array and creates a address.
6a488035 43 *
6a0b768e
TO
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.
6a488035
TO
48 * before inserting in db
49 *
e63aff1c
EM
50 * @param null $entity
51 *
a1a2a83d 52 * @return array|NULL
a6c01b45 53 * array of created address
6a488035 54 */
00be9182 55 public static function create(&$params, $fixAddress = TRUE, $entity = NULL) {
6a488035 56 if (!isset($params['address']) || !is_array($params['address'])) {
a1a2a83d 57 return NULL;
6a488035
TO
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);
a7488080 86 if (empty($value['id'])) {
6a488035
TO
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
8cc574cf 111 if ($isPrimary && !empty($value['is_primary'])) {
6a488035
TO
112 $isPrimary = FALSE;
113 }
114 else {
115 $value['is_primary'] = 0;
116 }
117
8cc574cf 118 if ($isBilling && !empty($value['is_billing'])) {
6a488035
TO
119 $isBilling = FALSE;
120 }
121 else {
122 $value['is_billing'] = 0;
123 }
124
a7488080 125 if (empty($value['manual_geo_code'])) {
6a488035
TO
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 /**
fe482240 136 * Takes an associative array and adds address.
6a488035 137 *
6a0b768e
TO
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.
6a488035
TO
142 * before inserting in db
143 *
906e6120 144 * @return CRM_Core_BAO_Address|null
6a488035 145 */
00be9182 146 public static function add(&$params, $fixAddress) {
6a488035
TO
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 /**
fe482240 200 * Format the address params to have reasonable values.
6a488035 201 *
6a0b768e
TO
202 * @param array $params
203 * (reference ) an assoc array of name/value pairs.
6a488035
TO
204 *
205 * @return void
6a488035 206 */
00be9182 207 public static function fixAddress(&$params) {
a7488080 208 if (!empty($params['billing_street_address'])) {
6a488035
TO
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)) {
a7488080 223 if (!empty($params[$key])) {
6a488035
TO
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 */
a7488080 236 if (!empty($params['postal_code']) &&
6a488035
TO
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
6c552737
TO
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 );
6a488035
TO
375
376 if ($parseStreetAddress && !empty($params['street_address'])) {
377 foreach (array(
353ffa53
TO
378 'street_number',
379 'street_name',
380 'street_unit',
af9b09df 381 'street_number_suffix',
353ffa53 382 ) as $fld) {
6a488035
TO
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 /**
fe482240 402 * Check if there is data to create the object.
6a488035 403 *
6a0b768e
TO
404 * @param array $params
405 * (reference ) an assoc array of name/value pairs.
6a488035 406 *
5c766a0b 407 * @return bool
6a488035 408 */
00be9182 409 public static function dataExists(&$params) {
6a488035
TO
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(
353ffa53
TO
418 'is_primary',
419 'location_type_id',
420 'id',
421 'contact_id',
422 'is_billing',
423 'display',
af9b09df 424 'master_id',
353ffa53 425 ))) {
6a488035
TO
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 *
6a0b768e
TO
466 * @param array $entityBlock
467 * Associated array of fields.
468 * @param bool $microformat
469 * If microformat output is required.
e63aff1c 470 * @param int|string $fieldName conditional field name
6a488035 471 *
a6c01b45
CW
472 * @return array
473 * array with address fields
6a488035 474 */
00be9182 475 public static function &getValues($entityBlock, $microformat = FALSE, $fieldName = 'contact_id') {
6a488035
TO
476 if (empty($entityBlock)) {
477 return NULL;
478 }
479 $addresses = array();
480 $address = new CRM_Core_BAO_Address();
481
a7488080 482 if (empty($entityBlock['entity_table'])) {
6a488035
TO
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
ac44bb1c 501 $locationTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Address', 'location_type_id');
6a488035
TO
502 $count = 1;
503 while ($address->fetch()) {
504 // deprecate reference.
505 if ($count > 1) {
506 foreach (array(
353ffa53
TO
507 'state',
508 'state_name',
509 'country',
af9b09df 510 'world_region',
353ffa53 511 ) as $fld) {
4f99ca55
TO
512 if (isset($address->$fld)) {
513 unset($address->$fld);
2aa397bc 514 }
6a488035
TO
515 }
516 }
517 $stree = $address->street_address;
518 $values = array();
519 CRM_Core_DAO::storeValues($address, $values);
520
521 // add state and country information: CRM-369
ac44bb1c
WA
522 if (!empty($address->location_type_id)) {
523 $values['location_type'] = CRM_Utils_Array::value($address->location_type_id, $locationTypes);
524 }
6a488035
TO
525 if (!empty($address->state_province_id)) {
526 $address->state = CRM_Core_PseudoConstant::stateProvinceAbbreviation($address->state_province_id, FALSE);
527 $address->state_name = CRM_Core_PseudoConstant::stateProvince($address->state_province_id, FALSE);
528 }
529
530 if (!empty($address->country_id)) {
531 $address->country = CRM_Core_PseudoConstant::country($address->country_id);
532
533 //get world region
534 $regionId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Country', $address->country_id, 'region_id');
535
536 $address->world_region = CRM_Core_PseudoConstant::worldregion($regionId);
537 }
538
539 $address->addDisplay($microformat);
540
541 $values['display'] = $address->display;
542 $values['display_text'] = $address->display_text;
543
544 if (is_numeric($address->master_id)) {
545 $values['use_shared_address'] = 1;
546 }
547
548 $addresses[$count] = $values;
549
550 //unset is_primary after first block. Due to some bug in earlier version
551 //there might be more than one primary blocks, hence unset is_primary other than first
552 if ($count > 1) {
553 unset($addresses[$count]['is_primary']);
554 }
555
556 $count++;
557 }
558
559 return $addresses;
560 }
561
562 /**
563 * Add the formatted address to $this-> display
564 *
2a6da8d7
EM
565 * @param bool $microformat
566 *
6a488035 567 * @return void
6a488035 568 */
00be9182 569 public function addDisplay($microformat = FALSE) {
6a488035
TO
570 $fields = array(
571 // added this for CRM 1200
572 'address_id' => $this->id,
573 // CRM-4003
574 'address_name' => str_replace('\ 1', ' ', $this->name),
575 'street_address' => $this->street_address,
576 'supplemental_address_1' => $this->supplemental_address_1,
577 'supplemental_address_2' => $this->supplemental_address_2,
578 'city' => $this->city,
579 'state_province_name' => isset($this->state_name) ? $this->state_name : "",
580 'state_province' => isset($this->state) ? $this->state : "",
581 'postal_code' => isset($this->postal_code) ? $this->postal_code : "",
582 'postal_code_suffix' => isset($this->postal_code_suffix) ? $this->postal_code_suffix : "",
583 'country' => isset($this->country) ? $this->country : "",
584 'world_region' => isset($this->world_region) ? $this->world_region : "",
585 );
586
587 if (isset($this->county_id) && $this->county_id) {
588 $fields['county'] = CRM_Core_PseudoConstant::county($this->county_id);
589 }
590 else {
591 $fields['county'] = NULL;
592 }
593
594 $this->display = CRM_Utils_Address::format($fields, NULL, $microformat);
595 $this->display_text = CRM_Utils_Address::format($fields);
596 }
597
598 /**
599 * Get all the addresses for a specified contact_id, with the primary address being first
600 *
6a0b768e
TO
601 * @param int $id
602 * The contact id.
6a488035 603 *
2a6da8d7
EM
604 * @param bool $updateBlankLocInfo
605 *
a6c01b45
CW
606 * @return array
607 * the array of adrress data
6a488035 608 */
00be9182 609 public static function allAddress($id, $updateBlankLocInfo = FALSE) {
6a488035
TO
610 if (!$id) {
611 return NULL;
612 }
613
614 $query = "
615SELECT civicrm_address.id as address_id, civicrm_address.location_type_id as location_type_id
616FROM civicrm_contact, civicrm_address
617WHERE civicrm_address.contact_id = civicrm_contact.id AND civicrm_contact.id = %1
618ORDER BY civicrm_address.is_primary DESC, address_id ASC";
619 $params = array(1 => array($id, 'Integer'));
620
621 $addresses = array();
622 $dao = CRM_Core_DAO::executeQuery($query, $params);
623 $count = 1;
624 while ($dao->fetch()) {
625 if ($updateBlankLocInfo) {
626 $addresses[$count++] = $dao->address_id;
627 }
628 else {
629 $addresses[$dao->location_type_id] = $dao->address_id;
630 }
631 }
632 return $addresses;
633 }
634
635 /**
636 * Get all the addresses for a specified location_block id, with the primary address being first
637 *
6a0b768e
TO
638 * @param array $entityElements
639 * The array containing entity_id and.
16b10e64 640 * entity_table name
6a488035 641 *
a6c01b45
CW
642 * @return array
643 * the array of adrress data
6a488035 644 */
00be9182 645 public static function allEntityAddress(&$entityElements) {
c927c151 646 $addresses = array();
6a488035
TO
647 if (empty($entityElements)) {
648 return $addresses;
649 }
650
651 $entityId = $entityElements['entity_id'];
652 $entityTable = $entityElements['entity_table'];
653
654 $sql = "
655SELECT civicrm_address.id as address_id
656FROM civicrm_loc_block loc, civicrm_location_type ltype, civicrm_address, {$entityTable} ev
657WHERE ev.id = %1
658 AND loc.id = ev.loc_block_id
659 AND civicrm_address.id IN (loc.address_id, loc.address_2_id)
660 AND ltype.id = civicrm_address.location_type_id
661ORDER BY civicrm_address.is_primary DESC, civicrm_address.location_type_id DESC, address_id ASC ";
662
663 $params = array(1 => array($entityId, 'Integer'));
6a488035
TO
664 $dao = CRM_Core_DAO::executeQuery($sql, $params);
665 $locationCount = 1;
666 while ($dao->fetch()) {
667 $addresses[$locationCount] = $dao->address_id;
668 $locationCount++;
669 }
670 return $addresses;
671 }
672
6a488035 673 /**
fe482240 674 * Get address sequence.
6a488035 675 *
a6c01b45 676 * @return array
16b10e64 677 * Array of address sequence.
6a488035 678 */
00be9182 679 public static function addressSequence() {
6a488035
TO
680 $config = CRM_Core_Config::singleton();
681 $addressSequence = $config->addressSequence();
682
683 $countryState = $cityPostal = FALSE;
684 foreach ($addressSequence as $key => $field) {
685 if (
686 in_array($field, array('country', 'state_province')) &&
687 !$countryState
688 ) {
689 $countryState = TRUE;
690 $addressSequence[$key] = 'country_state_province';
691 }
692 elseif (
693 in_array($field, array('city', 'postal_code')) &&
694 !$cityPostal
695 ) {
696 $cityPostal = TRUE;
697 $addressSequence[$key] = 'city_postal_code';
698 }
699 elseif (
353ffa53 700 in_array($field, array('country', 'state_province', 'city', 'postal_code'))
6a488035
TO
701 ) {
702 unset($addressSequence[$key]);
703 }
704 }
705
706 return $addressSequence;
707 }
708
709 /**
710 * Parse given street address string in to street_name,
711 * street_unit, street_number and street_number_suffix
712 * eg "54A Excelsior Ave. Apt 1C", or "917 1/2 Elm Street"
713 *
714 * NB: civic street formats for en_CA and fr_CA used by default if those locales are active
715 * otherwise en_US format is default action
716 *
6c552737
TO
717 * @param string $streetAddress
718 * Street address including number and apt.
719 * @param string $locale
720 * Locale used to parse address.
6a488035 721 *
a6c01b45
CW
722 * @return array
723 * parsed fields values.
6a488035 724 */
00be9182 725 public static function parseStreetAddress($streetAddress, $locale = NULL) {
6a488035
TO
726 $config = CRM_Core_Config::singleton();
727
728 /* locales supported include:
729 * en_US - http://pe.usps.com/cpim/ftp/pubs/pub28/pub28.pdf
730 * en_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-e.asp
731 * fr_CA - http://www.canadapost.ca/tools/pg/manual/PGaddress-f.asp
732 * NB: common use of comma after street number also supported
733 * default is en_US
734 */
735
736 $supportedLocalesForParsing = array('en_US', 'en_CA', 'fr_CA');
737 if (!$locale) {
738 $locale = $config->lcMessages;
739 }
740 // as different locale explicitly requested but is not available, display warning message and set $locale = 'en_US'
741 if (!in_array($locale, $supportedLocalesForParsing)) {
742 CRM_Core_Session::setStatus(ts('Unsupported locale specified to parseStreetAddress: %1. Proceeding with en_US locale.', array(1 => $locale)), ts('Unsupported Locale'), 'alert');
743 $locale = 'en_US';
744 }
689cecfd 745 $emptyParseFields = $parseFields = array(
6a488035
TO
746 'street_name' => '',
747 'street_unit' => '',
748 'street_number' => '',
749 'street_number_suffix' => '',
750 );
751
752 if (empty($streetAddress)) {
753 return $parseFields;
754 }
755
756 $streetAddress = trim($streetAddress);
757
758 $matches = array();
759 if (in_array($locale, array(
353ffa53 760 'en_CA',
af9b09df 761 'fr_CA',
353ffa53
TO
762 )) && preg_match('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', $streetAddress, $matches)
763 ) {
6a488035
TO
764 $parseFields['street_unit'] = $matches[1];
765 // unset from rest of street address
766 $streetAddress = preg_replace('/^([A-Za-z0-9]+)[ ]*\-[ ]*/', '', $streetAddress);
767 }
768
769 // get street number and suffix.
770 $matches = array();
771 //alter street number/suffix handling so that we accept -digit
772 if (preg_match('/^[A-Za-z0-9]+([\S]+)/', $streetAddress, $matches)) {
773 // check that $matches[0] is numeric, else assume no street number
774 if (preg_match('/^(\d+)/', $matches[0])) {
775 $streetNumAndSuffix = $matches[0];
776
777 // get street number.
778 $matches = array();
779 if (preg_match('/^(\d+)/', $streetNumAndSuffix, $matches)) {
780 $parseFields['street_number'] = $matches[0];
781 $suffix = preg_replace('/^(\d+)/', '', $streetNumAndSuffix);
782 $parseFields['street_number_suffix'] = trim($suffix);
783 }
784
785 // unset from main street address.
786 $streetAddress = preg_replace('/^[A-Za-z0-9]+([\S]+)/', '', $streetAddress);
787 $streetAddress = trim($streetAddress);
788 }
789 }
790 elseif (preg_match('/^(\d+)/', $streetAddress, $matches)) {
791 $parseFields['street_number'] = $matches[0];
792 // unset from main street address.
793 $streetAddress = preg_replace('/^(\d+)/', '', $streetAddress);
794 $streetAddress = trim($streetAddress);
795 }
796
797 // suffix might be like 1/2
798 $matches = array();
799 if (preg_match('/^\d\/\d/', $streetAddress, $matches)) {
800 $parseFields['street_number_suffix'] .= $matches[0];
801
802 // unset from main street address.
803 $streetAddress = preg_replace('/^\d+\/\d+/', '', $streetAddress);
804 $streetAddress = trim($streetAddress);
805 }
806
807 // now get the street unit.
808 // supportable street unit formats.
809 $streetUnitFormats = array(
353ffa53
TO
810 'APT',
811 'APARTMENT',
812 'BSMT',
813 'BASEMENT',
814 'BLDG',
815 'BUILDING',
816 'DEPT',
817 'DEPARTMENT',
818 'FL',
819 'FLOOR',
820 'FRNT',
821 'FRONT',
822 'HNGR',
823 'HANGER',
824 'LBBY',
825 'LOBBY',
826 'LOWR',
827 'LOWER',
828 'OFC',
829 'OFFICE',
830 'PH',
831 'PENTHOUSE',
832 'TRLR',
833 'TRAILER',
834 'UPPR',
835 'RM',
836 'ROOM',
837 'SIDE',
838 'SLIP',
839 'KEY',
840 'LOT',
841 'PIER',
842 'REAR',
843 'SPC',
844 'SPACE',
845 'STOP',
846 'STE',
847 'SUITE',
848 'UNIT',
849 '#',
6a488035
TO
850 );
851
852 // overwriting $streetUnitFormats for 'en_CA' and 'fr_CA' locale
853 if (in_array($locale, array(
353ffa53 854 'en_CA',
af9b09df 855 'fr_CA',
353ffa53 856 ))) {
6a488035
TO
857 $streetUnitFormats = array('APT', 'APP', 'SUITE', 'BUREAU', 'UNIT');
858 }
689cecfd
EM
859 //@todo per CRM-14459 this regex picks up words with the string in them - e.g APT picks up
860 //Captain - presuming fixing regex (& adding test) to ensure a-z does not preced string will fix
6a488035
TO
861 $streetUnitPreg = '/(' . implode('|\s', $streetUnitFormats) . ')(.+)?/i';
862 $matches = array();
863 if (preg_match($streetUnitPreg, $streetAddress, $matches)) {
864 $parseFields['street_unit'] = trim($matches[0]);
865 $streetAddress = str_replace($matches[0], '', $streetAddress);
866 $streetAddress = trim($streetAddress);
867 }
868
869 // consider remaining string as street name.
870 $parseFields['street_name'] = $streetAddress;
871
872 //run parsed fields through stripSpaces to clean
873 foreach ($parseFields as $parseField => $value) {
874 $parseFields[$parseField] = CRM_Utils_String::stripSpaces($value);
875 }
689cecfd
EM
876 //CRM-14459 if the field is too long we should assume it didn't get it right & skip rather than allow
877 // the DB to fatal
878 $fields = CRM_Core_BAO_Address::fields();
879 foreach ($fields as $fieldname => $field) {
22e263ad 880 if (!empty($field['maxlength']) && strlen(CRM_Utils_Array::value($fieldname, $parseFields)) > $field['maxlength']) {
689cecfd
EM
881 return $emptyParseFields;
882 }
883 }
6a488035
TO
884
885 return $parseFields;
886 }
887
888 /**
eceb18cc 889 * Validate the address fields based on the address options enabled.
6a488035
TO
890 * in the Address Settings
891 *
6a0b768e
TO
892 * @param array $fields
893 * An array of importable/exportable contact fields.
6a488035 894 *
a6c01b45
CW
895 * @return array
896 * an array of contact fields and only the enabled address options
6a488035 897 */
00be9182 898 public static function validateAddressOptions($fields) {
6a488035
TO
899 static $addressOptions = NULL;
900 if (!$addressOptions) {
6c552737
TO
901 $addressOptions = CRM_Core_BAO_Setting::valueOptions(
902 CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
903 'address_options'
904 );
6a488035
TO
905 }
906
907 if (is_array($fields) && !empty($fields)) {
908 foreach ($addressOptions as $key => $value) {
909 if (!$value && isset($fields[$key])) {
910 unset($fields[$key]);
911 }
912 }
913 }
914 return $fields;
915 }
916
917 /**
fe482240 918 * Check if current address is used by any other contacts.
6a488035 919 *
6a0b768e
TO
920 * @param int $addressId
921 * Address id.
6a488035 922 *
72b3a70c
CW
923 * @return int
924 * count of contacts that use this shared address
6a488035 925 */
00be9182 926 public static function checkContactSharedAddress($addressId) {
6a488035
TO
927 $query = 'SELECT count(id) FROM civicrm_address WHERE master_id = %1';
928 return CRM_Core_DAO::singleValueQuery($query, array(1 => array($addressId, 'Integer')));
929 }
930
931 /**
fe482240 932 * Check if current address fields are shared with any other address.
6a488035 933 *
6a0b768e
TO
934 * @param array $fields
935 * Address fields in profile.
936 * @param int $contactId
937 * Contact id.
6a488035 938 *
6a488035 939 */
00be9182 940 public static function checkContactSharedAddressFields(&$fields, $contactId) {
6a488035
TO
941 if (!$contactId || !is_array($fields) || empty($fields)) {
942 return;
943 }
944
945 $sharedLocations = array();
946
947 $query = "
948SELECT is_primary,
949 location_type_id
950 FROM civicrm_address
951 WHERE contact_id = %1
952 AND master_id IS NOT NULL";
953
954 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($contactId, 'Positive')));
955 while ($dao->fetch()) {
956 $sharedLocations[$dao->location_type_id] = $dao->location_type_id;
957 if ($dao->is_primary) {
958 $sharedLocations['Primary'] = 'Primary';
959 }
960 }
961
962 //no need to process further.
963 if (empty($sharedLocations)) {
964 return;
965 }
966
967 $addressFields = array(
968 'city',
969 'county',
970 'country',
971 'geo_code_1',
972 'geo_code_2',
973 'postal_code',
974 'address_name',
975 'state_province',
976 'street_address',
977 'postal_code_suffix',
978 'supplemental_address_1',
979 'supplemental_address_2',
980 );
981
982 foreach ($fields as $name => & $values) {
983 if (!is_array($values) || empty($values)) {
984 continue;
985 }
986
987 $nameVal = explode('-', $values['name']);
988 $fldName = CRM_Utils_Array::value(0, $nameVal);
989 $locType = CRM_Utils_Array::value(1, $nameVal);
a7488080 990 if (!empty($values['location_type_id'])) {
6a488035
TO
991 $locType = $values['location_type_id'];
992 }
993
994 if (in_array($fldName, $addressFields) &&
995 in_array($locType, $sharedLocations)
996 ) {
997 $values['is_shared'] = TRUE;
998 }
999 }
1000 }
1001
1002 /**
fe482240 1003 * Update the shared addresses if master address is modified.
6a488035 1004 *
6a0b768e
TO
1005 * @param int $addressId
1006 * Address id.
1007 * @param array $params
1008 * Associated array of address params.
6a488035
TO
1009 *
1010 * @return void
6a488035 1011 */
00be9182 1012 public static function processSharedAddress($addressId, $params) {
6a488035
TO
1013 $query = 'SELECT id FROM civicrm_address WHERE master_id = %1';
1014 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($addressId, 'Integer')));
1015
1016 // unset contact id
1017 $skipFields = array('is_primary', 'location_type_id', 'is_billing', 'master_id', 'contact_id');
1018 foreach ($skipFields as $value) {
1019 unset($params[$value]);
1020 }
1021
1022 $addressDAO = new CRM_Core_DAO_Address();
1023 while ($dao->fetch()) {
1024 $addressDAO->copyValues($params);
1025 $addressDAO->id = $dao->id;
1026 $addressDAO->save();
1027 $addressDAO->free();
1028 }
1029 }
1030
8bdfc216 1031 /**
fe482240 1032 * Merge contacts with the Same address to get one shared label.
6a0b768e
TO
1033 * @param array $rows
1034 * Array[contact_id][contactDetails].
8bdfc216 1035 */
1036 public static function mergeSameAddress(&$rows) {
1037 $uniqueAddress = array();
1038 foreach (array_keys($rows) as $rowID) {
1039 // load complete address as array key
6c552737
TO
1040 $address = trim($rows[$rowID]['street_address'])
1041 . trim($rows[$rowID]['city'])
1042 . trim($rows[$rowID]['state_province'])
1043 . trim($rows[$rowID]['postal_code'])
1044 . trim($rows[$rowID]['country']);
8bdfc216 1045 if (isset($rows[$rowID]['last_name'])) {
1046 $name = $rows[$rowID]['last_name'];
1047 }
1048 else {
1049 $name = $rows[$rowID]['display_name'];
1050 }
1051
1052 // CRM-15120
1053 $formatted = array(
1054 'first_name' => $rows[$rowID]['first_name'],
21dfd5f5 1055 'individual_prefix' => $rows[$rowID]['individual_prefix'],
8bdfc216 1056 );
1057 $format = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'display_name_format');
1058 $firstNameWithPrefix = CRM_Utils_Address::format($formatted, $format, FALSE, FALSE, TRUE);
1059 $firstNameWithPrefix = trim($firstNameWithPrefix);
1060
1061 // fill uniqueAddress array with last/first name tree
1062 if (isset($uniqueAddress[$address])) {
1063 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1064 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1065 // drop unnecessary rows
1066 unset($rows[$rowID]);
1067 // this is the first listing at this address
1068 }
1069 else {
1070 $uniqueAddress[$address]['ID'] = $rowID;
1071 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['first_name'] = $rows[$rowID]['first_name'];
1072 $uniqueAddress[$address]['names'][$name][$firstNameWithPrefix]['addressee_display'] = $rows[$rowID]['addressee_display'];
1073 }
1074 }
1075 foreach ($uniqueAddress as $address => $data) {
1076 // copy data back to $rows
1077 $count = 0;
1078 // one last name list per row
1079 foreach ($data['names'] as $last_name => $first_names) {
1080 // too many to list
1081 if ($count > 2) {
1082 break;
1083 }
9b873358 1084 if (count($first_names) == 1) {
2aa397bc
TO
1085 $family = $first_names[current(array_keys($first_names))]['addressee_display'];
1086 }
1087 else {
1088 // collapse the tree to summarize
1089 $family = trim(implode(" & ", array_keys($first_names)) . " " . $last_name);
1090 }
1091 if ($count) {
1092 $processedNames .= "\n" . $family;
1093 }
1094 else {
1095 // build display_name string
1096 $processedNames = $family;
1097 }
8bdfc216 1098 $count++;
1099 }
1100 $rows[$data['ID']]['addressee'] = $rows[$data['ID']]['addressee_display'] = $rows[$data['ID']]['display_name'] = $processedNames;
1101 }
1102 }
1103
6a488035 1104 /**
fe482240 1105 * Create relationship between contacts who share an address.
6a488035
TO
1106 *
1107 * Note that currently we create relationship only for Individual contacts
1108 * Individual + Household and Individual + Orgnization
1109 *
6a0b768e
TO
1110 * @param int $masterAddressId
1111 * Master address id.
1112 * @param array $params
1113 * Associated array of submitted values.
6a488035
TO
1114 *
1115 * @return void
6a488035 1116 */
00be9182 1117 public static function processSharedAddressRelationship($masterAddressId, $params) {
6a488035
TO
1118 if (!$masterAddressId) {
1119 return;
1120 }
1121 // get the contact type of contact being edited / created
1122 $currentContactType = CRM_Contact_BAO_Contact::getContactType($params['contact_id']);
1123 $currentContactId = $params['contact_id'];
1124
1125 // if current contact is not of type individual return
1126 if ($currentContactType != 'Individual') {
1127 return;
1128 }
1129
1130 // get the contact id and contact type of shared contact
1131 // check the contact type of shared contact, return if it is of type Individual
1132
1133 $query = 'SELECT cc.id, cc.contact_type
1134 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1135 WHERE ca.id = %1';
1136
1137 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($masterAddressId, 'Integer')));
1138
1139 $dao->fetch();
1140
1141 // if current contact is not of type individual return, since we don't create relationship between
1142 // 2 individuals
1143 if ($dao->contact_type == 'Individual') {
1144 return;
1145 }
1146 $sharedContactType = $dao->contact_type;
1147 $sharedContactId = $dao->id;
1148
1149 // create relationship between ontacts who share an address
1150 if ($sharedContactType == 'Organization') {
1151 return CRM_Contact_BAO_Contact_Utils::createCurrentEmployerRelationship($currentContactId, $sharedContactId);
1152 }
1153 else {
1154 // get the relationship type id of "Household Member of"
1155 $relationshipType = 'Household Member of';
1156 }
1157
1158 $cid = array('contact' => $currentContactId);
1159
1160 $relTypeId = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_RelationshipType', $relationshipType, 'id', 'name_a_b');
1161
1162 if (!$relTypeId) {
1163 CRM_Core_Error::fatal(ts("You seem to have deleted the relationship type '%1'", array(1 => $relationshipType)));
1164 }
1165
1166 // create relationship
1167 $relationshipParams = array(
1168 'is_active' => TRUE,
1169 'relationship_type_id' => $relTypeId . '_a_b',
1170 'contact_check' => array($sharedContactId => TRUE),
1171 );
1172
1173 list($valid, $invalid, $duplicate,
1174 $saved, $relationshipIds
353ffa53 1175 ) = CRM_Contact_BAO_Relationship::createMultiple($relationshipParams, $cid);
6a488035
TO
1176 }
1177
1178 /**
fe482240 1179 * Check and set the status for shared address delete.
6a488035 1180 *
6a0b768e
TO
1181 * @param int $addressId
1182 * Address id.
1183 * @param int $contactId
1184 * Contact id.
1185 * @param bool $returnStatus
1186 * By default false.
6a488035 1187 *
a6c01b45 1188 * @return string
6a488035 1189 */
00be9182 1190 public static function setSharedAddressDeleteStatus($addressId = NULL, $contactId = NULL, $returnStatus = FALSE) {
6a488035
TO
1191 // check if address that is being deleted has any shared
1192 if ($addressId) {
1193 $entityId = $addressId;
1194 $query = 'SELECT cc.id, cc.display_name
1195 FROM civicrm_contact cc INNER JOIN civicrm_address ca ON cc.id = ca.contact_id
1196 WHERE ca.master_id = %1';
1197 }
1198 else {
1199 $entityId = $contactId;
1200 $query = 'SELECT cc.id, cc.display_name
1201 FROM civicrm_address ca1
1202 INNER JOIN civicrm_address ca2 ON ca1.id = ca2.master_id
1203 INNER JOIN civicrm_contact cc ON ca2.contact_id = cc.id
1204 WHERE ca1.contact_id = %1';
1205 }
1206
1207 $dao = CRM_Core_DAO::executeQuery($query, array(1 => array($entityId, 'Integer')));
1208
1209 $deleteStatus = array();
1210 $sharedContactList = array();
1211 $statusMessage = NULL;
1212 $addressCount = 0;
1213 while ($dao->fetch()) {
1214 if (empty($deleteStatus)) {
1215 $deleteStatus[] = ts('The following contact(s) have address records which were shared with the address you removed from this contact. These address records are no longer shared - but they have not been removed or altered.');
1216 }
1217
1218 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid={$dao->id}");
1219 $sharedContactList[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1220 $deleteStatus[] = "<a href='{$contactViewUrl}'>{$dao->display_name}</a>";
1221
1222 $addressCount++;
1223 }
1224
1225 if (!empty($deleteStatus)) {
1226 $statusMessage = implode('<br/>', $deleteStatus) . '<br/>';
1227 }
1228
1229 if (!$returnStatus) {
1230 CRM_Core_Session::setStatus($statusMessage, '', 'info');
1231 }
1232 else {
1233 return array(
1234 'contactList' => $sharedContactList,
1235 'count' => $addressCount,
1236 );
1237 }
1238 }
12445e1c
CW
1239
1240 /**
fe482240 1241 * Call common delete function.
12445e1c 1242 */
00be9182 1243 public static function del($id) {
a65e2e55 1244 return CRM_Contact_BAO_Contact::deleteObjectWithPrimary('Address', $id);
12445e1c 1245 }
dc86f881
CW
1246
1247 /**
1248 * Get options for a given address field.
1249 * @see CRM_Core_DAO::buildOptions
1250 *
1251 * TODO: Should we always assume chainselect? What fn should be responsible for controlling that flow?
1252 * TODO: In context of chainselect, what to return if e.g. a country has no states?
1253 *
6a0b768e
TO
1254 * @param string $fieldName
1255 * @param string $context
a1a2a83d 1256 * @see CRM_Core_DAO::buildOptionsContext
6a0b768e 1257 * @param array $props
16b10e64 1258 * whatever is known about this dao object.
77b97be7 1259 *
5c766a0b 1260 * @return array|bool
dc86f881
CW
1261 */
1262 public static function buildOptions($fieldName, $context = NULL, $props = array()) {
1263 $params = array();
1264 // Special logic for fields whose options depend on context or properties
1265 switch ($fieldName) {
1266 // Filter state_province list based on chosen country or site defaults
1267 case 'state_province_id':
1268 if (empty($props['country_id'])) {
1269 $config = CRM_Core_Config::singleton();
1270 if (!empty($config->provinceLimit)) {
1271 $props['country_id'] = $config->provinceLimit;
1272 }
1273 else {
1274 $props['country_id'] = $config->defaultContactCountry;
1275 }
1276 }
578d346d 1277 if (!empty($props['country_id']) && $context !== 'validate') {
dc86f881
CW
1278 $params['condition'] = 'country_id IN (' . implode(',', (array) $props['country_id']) . ')';
1279 }
1280 break;
2aa397bc 1281
dc86f881
CW
1282 // Filter country list based on site defaults
1283 case 'country_id':
786ad6e1
CW
1284 if ($context != 'get' && $context != 'validate') {
1285 $config = CRM_Core_Config::singleton();
1286 if (!empty($config->countryLimit) && is_array($config->countryLimit)) {
1287 $params['condition'] = 'id IN (' . implode(',', $config->countryLimit) . ')';
1288 }
dc86f881
CW
1289 }
1290 break;
2aa397bc 1291
dc86f881
CW
1292 // Filter county list based on chosen state
1293 case 'county_id':
1294 if (!empty($props['state_province_id'])) {
1295 $params['condition'] = 'state_province_id IN (' . implode(',', (array) $props['state_province_id']) . ')';
1296 }
1297 break;
2aa397bc 1298
c7130c3e
CW
1299 // Not a real field in this entity
1300 case 'world_region':
1301 return CRM_Core_PseudoConstant::worldRegion();
dc86f881 1302 }
786ad6e1 1303 return CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context);
dc86f881 1304 }
96025800 1305
a7488080 1306}