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