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