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