Fix State handling to be case insensitive again
[civicrm-core.git] / CRM / Contact / Import / Parser / Contact.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Api4\Contact;
13 use Civi\Api4\RelationshipType;
14 use Civi\Api4\StateProvince;
15
16 require_once 'api/v3/utils.php';
17
18 /**
19 *
20 * @package CRM
21 * @copyright CiviCRM LLC https://civicrm.org/licensing
22 */
23
24 /**
25 * class to parse contact csv files
26 */
27 class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
28
29 use CRM_Contact_Import_MetadataTrait;
30
31 protected $_mapperKeys = [];
32
33 /**
34 * Is update only permitted on an id match.
35 *
36 * Note this historically was true for when id or external identifier was
37 * present. However, CRM-17275 determined that a dedupe-match could over-ride
38 * external identifier.
39 *
40 * @var bool
41 */
42 protected $_updateWithId;
43 protected $_retCode;
44
45 protected $_externalIdentifierIndex;
46 protected $_allExternalIdentifiers = [];
47 protected $_parseStreetAddress;
48
49 /**
50 * Array of successfully imported contact id's
51 *
52 * @var array
53 */
54 protected $_newContacts;
55
56 /**
57 * Line count id.
58 *
59 * @var int
60 */
61 protected $_lineCount;
62
63 /**
64 * Array of successfully imported related contact id's
65 *
66 * @var array
67 */
68 protected $_newRelatedContacts;
69
70 protected $_tableName;
71
72 /**
73 * Total number of lines in file
74 *
75 * @var int
76 */
77 protected $_rowCount;
78
79 protected $_primaryKeyName;
80 protected $_statusFieldName;
81
82 protected $fieldMetadata = [];
83
84 /**
85 * Fields which are being handled by metadata formatting & validation functions.
86 *
87 * This is intended as a temporary parameter as we phase in metadata handling.
88 *
89 * The end result is that all fields will be & this will go but for now it is
90 * opt in.
91 *
92 * @var string[]
93 */
94 protected $metadataHandledFields = [
95 'contact_type',
96 'contact_sub_type',
97 'gender_id',
98 'birth_date',
99 'deceased_date',
100 'is_deceased',
101 'prefix_id',
102 'suffix_id',
103 'communication_style',
104 'preferred_language',
105 'preferred_communication_method',
106 'phone',
107 'im',
108 'openid',
109 'email',
110 'website',
111 'url',
112 'email_greeting',
113 'email_greeting_id',
114 'postal_greeting',
115 'postal_greeting_id',
116 'addressee',
117 'addressee_id',
118 'geo_code_1',
119 'geo_code_2',
120 ];
121
122 /**
123 * Relationship labels.
124 *
125 * Temporary cache of labels to reduce queries in getRelationshipLabels.
126 *
127 * @var array
128 * e.g ['5a_b' => 'Employer', '5b_a' => 'Employee']
129 */
130 protected $relationshipLabels = [];
131
132 /**
133 * On duplicate
134 *
135 * @var int
136 */
137 public $_onDuplicate;
138
139 /**
140 * Dedupe rule group id to use if set
141 *
142 * @var int
143 */
144 public $_dedupeRuleGroupID = NULL;
145
146 /**
147 * Class constructor.
148 *
149 * @param array $mapperKeys
150 */
151 public function __construct($mapperKeys = []) {
152 parent::__construct();
153 $this->_mapperKeys = $mapperKeys;
154 }
155
156 /**
157 * The initializer code, called before processing.
158 */
159 public function init() {
160 $this->setFieldMetadata();
161 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
162 $this->addField($name, $field['title'], CRM_Utils_Array::value('type', $field), CRM_Utils_Array::value('headerPattern', $field), CRM_Utils_Array::value('dataPattern', $field), CRM_Utils_Array::value('hasLocationType', $field));
163 }
164 $this->_newContacts = [];
165
166 $this->setActiveFields($this->_mapperKeys);
167
168 $this->_externalIdentifierIndex = -1;
169
170 $index = 0;
171 foreach ($this->_mapperKeys as $key) {
172 if ($key == 'external_identifier') {
173 $this->_externalIdentifierIndex = $index;
174 }
175 $index++;
176 }
177
178 $this->_updateWithId = FALSE;
179 if (in_array('id', $this->_mapperKeys) || ($this->_externalIdentifierIndex >= 0 && $this->isUpdateExistingContacts())) {
180 $this->_updateWithId = TRUE;
181 }
182
183 $this->_parseStreetAddress = CRM_Utils_Array::value('street_address_parsing', CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'address_options'), FALSE);
184 }
185
186 /**
187 * Is this a case where the user has opted to update existing contacts.
188 *
189 * @return bool
190 *
191 * @throws \API_Exception
192 */
193 private function isUpdateExistingContacts(): bool {
194 return in_array((int) $this->getSubmittedValue('onDuplicate'), [
195 CRM_Import_Parser::DUPLICATE_UPDATE,
196 CRM_Import_Parser::DUPLICATE_FILL,
197 ], TRUE);
198 }
199
200 /**
201 * Did the user specify duplicates checking should be skipped, resulting in possible duplicate contacts.
202 *
203 * Note we still need to check for external_identifier as it will hard-fail
204 * if we duplicate.
205 *
206 * @return bool
207 *
208 * @throws \API_Exception
209 */
210 private function isIgnoreDuplicates(): bool {
211 return ((int) $this->getSubmittedValue('onDuplicate')) === CRM_Import_Parser::DUPLICATE_NOCHECK;
212 }
213
214 /**
215 * Handle the values in preview mode.
216 *
217 * Function will be deprecated in favour of validateValues.
218 *
219 * @param array $values
220 * The array of values belonging to this line.
221 *
222 * @return bool
223 * the result of this processing
224 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
225 */
226 public function preview(&$values) {
227 return $this->summary($values);
228 }
229
230 /**
231 * Handle the values in summary mode.
232 *
233 * Function will be deprecated in favour of validateValues.
234 *
235 * @param array $values
236 * The array of values belonging to this line.
237 *
238 * @return int
239 * the result of this processing
240 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
241 */
242 public function summary(&$values): int {
243 $rowNumber = (int) ($values[array_key_last($values)]);
244 try {
245 $this->validateValues($values);
246 }
247 catch (CRM_Core_Exception $e) {
248 $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
249 array_unshift($values, $e->getMessage());
250 return CRM_Import_Parser::ERROR;
251 }
252 $this->setImportStatus($rowNumber, 'NEW', '');
253
254 return CRM_Import_Parser::VALID;
255 }
256
257 /**
258 * Get Array of all the fields that could potentially be part
259 * import process
260 *
261 * @return array
262 */
263 public function getAllFields() {
264 return $this->_fields;
265 }
266
267 /**
268 * Handle the values in import mode.
269 *
270 * @param int $onDuplicate
271 * The code for what action to take on duplicates.
272 * @param array $values
273 * The array of values belonging to this line.
274 *
275 * @return bool
276 * the result of this processing
277 *
278 * @throws \CiviCRM_API3_Exception
279 * @throws \CRM_Core_Exception
280 * @throws \API_Exception
281 */
282 public function import($onDuplicate, &$values) {
283 $rowNumber = (int) $values[array_key_last($values)];
284 $this->_unparsedStreetAddressContacts = [];
285 if (!$this->getSubmittedValue('doGeocodeAddress')) {
286 // CRM-5854, reset the geocode method to null to prevent geocoding
287 CRM_Utils_GeocodeProvider::disableForSession();
288 }
289
290 // first make sure this is a valid line
291 //$this->_updateWithId = false;
292 $response = $this->summary($values);
293
294 if ($response != CRM_Import_Parser::VALID) {
295 $this->setImportStatus((int) $values[count($values) - 1], 'Invalid', "Invalid (Error Code: $response)");
296 return FALSE;
297 }
298
299 $params = $this->getMappedRow($values);
300 $formatted = [];
301 foreach ($params as $key => $value) {
302 if ($value !== '') {
303 $formatted[$key] = $value;
304 }
305 }
306
307 $contactFields = CRM_Contact_DAO_Contact::import();
308
309 $params['contact_sub_type'] = $this->getContactSubType() ?: ($params['contact_sub_type'] ?? NULL);
310
311 try {
312 [$formatted, $params] = $this->processContact($params, $formatted);
313 }
314 catch (CRM_Core_Exception $e) {
315 $statuses = [CRM_Import_Parser::DUPLICATE => 'DUPLICATE', CRM_Import_Parser::ERROR => 'ERROR', CRM_Import_Parser::NO_MATCH => 'invalid_no_match'];
316 $this->setImportStatus($rowNumber, $statuses[$e->getErrorCode()], $e->getMessage());
317 return FALSE;
318 }
319
320 // Get contact id to format common data in update/fill mode,
321 // prioritising a dedupe rule check over an external_identifier check, but falling back on ext id.
322
323 //format common data, CRM-4062
324 $this->formatCommonData($params, $formatted, $contactFields);
325
326 //fixed CRM-4148
327 //now we create new contact in update/fill mode also.
328 $contactID = NULL;
329 //CRM-4430, don't carry if not submitted.
330 if ($this->_updateWithId && !empty($params['id'])) {
331 $contactID = $params['id'];
332 }
333 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID);
334
335 if (is_object($newContact) && ($newContact instanceof CRM_Contact_BAO_Contact)) {
336 $newContact = clone($newContact);
337 $contactID = $newContact->id;
338 $this->_newContacts[] = $contactID;
339
340 //get return code if we create new contact in update mode, CRM-4148
341 if ($this->_updateWithId) {
342 $this->_retCode = CRM_Import_Parser::VALID;
343 }
344 }
345 elseif (is_array($newContact)) {
346 // if duplicate, no need of further processing
347 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
348 $this->setImportStatus($rowNumber, 'DUPLICATE', 'Skipping duplicate record');
349 return FALSE;
350 }
351
352 // CRM-10433/CRM-20739 - IDs could be string or array; handle accordingly
353 if (!is_array($dupeContactIDs = $newContact['error_message']['params'][0])) {
354 $dupeContactIDs = explode(',', $dupeContactIDs);
355 }
356 $dupeCount = count($dupeContactIDs);
357 $contactID = array_pop($dupeContactIDs);
358 // check to see if we had more than one duplicate contact id.
359 // if we have more than one, the record will be rejected below
360 if ($dupeCount == 1) {
361 // there was only one dupe, we will continue normally...
362 if (!in_array($contactID, $this->_newContacts)) {
363 $this->_newContacts[] = $contactID;
364 }
365 }
366 }
367
368 if ($contactID) {
369 // call import hook
370 $currentImportID = end($values);
371
372 $hookParams = [
373 'contactID' => $contactID,
374 'importID' => $currentImportID,
375 'importTempTable' => $this->_tableName,
376 'fieldHeaders' => $this->_mapperKeys,
377 'fields' => $this->_activeFields,
378 ];
379
380 CRM_Utils_Hook::import('Contact', 'process', $this, $hookParams);
381 }
382
383 $primaryContactId = NULL;
384 if (is_array($newContact)) {
385 if ($dupeCount == 1 && CRM_Utils_Rule::integer($contactID)) {
386 $primaryContactId = $contactID;
387 }
388 }
389 else {
390 $primaryContactId = $newContact->id;
391 }
392
393 if ((is_array($newContact) || is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) {
394
395 //relationship contact insert
396 foreach ($this->getRelatedContactsParams($params) as $key => $field) {
397 $formatting = $field;
398 try {
399 [$formatting, $field] = $this->processContact($field, $formatting);
400 }
401 catch (CRM_Core_Exception $e) {
402 $statuses = [CRM_Import_Parser::DUPLICATE => 'DUPLICATE', CRM_Import_Parser::ERROR => 'ERROR', CRM_Import_Parser::NO_MATCH => 'invalid_no_match'];
403 $this->setImportStatus((int) $values[count($values) - 1], $statuses[$e->getErrorCode()], $e->getMessage());
404 return FALSE;
405 }
406
407 $contactFields = CRM_Contact_DAO_Contact::import();
408
409 //format common data, CRM-4062
410 $this->formatCommonData($field, $formatting, $contactFields);
411
412 //fixed for CRM-4148
413 if (!empty($params[$key]['id'])) {
414 $contact = [
415 'contact_id' => $params[$key]['id'],
416 ];
417 $defaults = [];
418 $relatedNewContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
419 }
420 else {
421 $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE);
422 }
423
424 if (is_object($relatedNewContact) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) {
425 $relatedNewContact = clone($relatedNewContact);
426 }
427
428 $matchedIDs = [];
429 // To update/fill contact, get the matching contact Ids if duplicate contact found
430 // otherwise get contact Id from object of related contact
431 if (is_array($relatedNewContact)) {
432 $matchedIDs = $relatedNewContact['error_message']['params'][0];
433 if (!is_array($matchedIDs)) {
434 $matchedIDs = explode(',', $matchedIDs);
435 }
436 }
437 else {
438 $matchedIDs[] = $relatedNewContact->id;
439 }
440 // update/fill related contact after getting matching Contact Ids, CRM-4424
441 if (in_array($onDuplicate, [
442 CRM_Import_Parser::DUPLICATE_UPDATE,
443 CRM_Import_Parser::DUPLICATE_FILL,
444 ])) {
445 //validation of related contact subtype for update mode
446 //CRM-5125
447 $relatedCsType = NULL;
448 if (!empty($formatting['contact_sub_type'])) {
449 $relatedCsType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $matchedIDs[0], 'contact_sub_type');
450 }
451
452 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($matchedIDs[0], $relatedCsType) && $relatedCsType != CRM_Utils_Array::value('contact_sub_type', $formatting))) {
453 $this->setImportStatus((int) $values[count($values) - 1], 'invalid_no_match', 'Mismatched or Invalid contact subtype found for this related contact.');
454 return FALSE;
455 }
456 else {
457 $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]);
458 }
459 }
460 static $relativeContact = [];
461 if (is_array($relatedNewContact)) {
462 if (count($matchedIDs) >= 1) {
463 $relContactId = $matchedIDs[0];
464 //add relative contact to count during update & fill mode.
465 //logic to make count distinct by contact id.
466 if ($this->_newRelatedContacts || !empty($relativeContact)) {
467 $reContact = array_keys($relativeContact, $relContactId);
468
469 if (empty($reContact)) {
470 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
471 }
472 }
473 else {
474 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
475 }
476 }
477 }
478 else {
479 $relContactId = $relatedNewContact->id;
480 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
481 }
482
483 if (is_array($relatedNewContact) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) {
484 //fix for CRM-1993.Checks for duplicate related contacts
485 if (count($matchedIDs) >= 1) {
486 //if more than one duplicate contact
487 //found, create relationship with first contact
488 // now create the relationship record
489 $relationParams = [
490 'relationship_type_id' => $key,
491 'contact_check' => [
492 $relContactId => 1,
493 ],
494 'is_active' => 1,
495 'skipRecentView' => TRUE,
496 ];
497
498 // we only handle related contact success, we ignore failures for now
499 // at some point wold be nice to have related counts as separate
500 $relationIds = [
501 'contact' => $primaryContactId,
502 ];
503
504 [$valid, $duplicate] = self::legacyCreateMultiple($relationParams, $relationIds);
505
506 if ($valid || $duplicate) {
507 $relationIds['contactTarget'] = $relContactId;
508 $action = ($duplicate) ? CRM_Core_Action::UPDATE : CRM_Core_Action::ADD;
509 CRM_Contact_BAO_Relationship::relatedMemberships($primaryContactId, $relationParams, $relationIds, $action);
510 }
511
512 //handle current employer, CRM-3532
513 if ($valid) {
514 $allRelationships = CRM_Core_PseudoConstant::relationshipType('name');
515 $relationshipTypeId = str_replace([
516 '_a_b',
517 '_b_a',
518 ], [
519 '',
520 '',
521 ], $key);
522 $relationshipType = str_replace($relationshipTypeId . '_', '', $key);
523 $orgId = $individualId = NULL;
524 if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') {
525 $orgId = $relContactId;
526 $individualId = $primaryContactId;
527 }
528 elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') {
529 $orgId = $primaryContactId;
530 $individualId = $relContactId;
531 }
532 if ($orgId && $individualId) {
533 $currentEmpParams[$individualId] = $orgId;
534 CRM_Contact_BAO_Contact_Utils::setCurrentEmployer($currentEmpParams);
535 }
536 }
537 }
538 }
539 }
540 }
541 if ($this->_updateWithId) {
542 //return warning if street address is unparsed, CRM-5886
543 return $this->processMessage($values, $this->_retCode);
544 }
545 //dupe checking
546 if (is_array($newContact)) {
547 return $this->handleDuplicateError($newContact, $values, $onDuplicate, $formatted, $contactFields);
548 }
549
550 if (empty($this->_unparsedStreetAddressContacts)) {
551 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '', $contactID);
552 return CRM_Import_Parser::VALID;
553 }
554
555 // @todo - record unparsed address as 'imported' but the presence of a message is meaningful?
556 return $this->processMessage($values, CRM_Import_Parser::VALID);
557 }
558
559 /**
560 * Only called from import now... plus one place outside of core & tests.
561 *
562 * @todo - deprecate more aggressively - will involve copying to the import
563 * class, adding a deprecation notice here & removing from tests.
564 *
565 * Takes an associative array and creates a relationship object.
566 *
567 * @deprecated For single creates use the api instead (it's tested).
568 * For multiple a new variant of this function needs to be written and migrated to as this is a bit
569 * nasty
570 *
571 * @param array $params
572 * (reference ) an assoc array of name/value pairs.
573 * @param array $ids
574 * The array that holds all the db ids.
575 * per http://wiki.civicrm.org/confluence/display/CRM/Database+layer
576 * "we are moving away from the $ids param "
577 *
578 * @return array
579 * @throws \CRM_Core_Exception
580 */
581 private static function legacyCreateMultiple($params, $ids = []) {
582 // clarify that the only key ever pass in the ids array is 'contact'
583 // There is legacy handling for other keys but a universe search on
584 // calls to this function (not supported to be called from outside core)
585 // only returns 2 calls - one in CRM_Contact_Import_Parser_Contact
586 // and the other in jma grant applications (CRM_Grant_Form_Grant_Confirm)
587 // both only pass in contact as a key here.
588 $contactID = $ids['contact'];
589 unset($ids);
590 // There is only ever one value passed in from the 2 places above that call
591 // this - by clarifying here like this we can cleanup within this
592 // function without having to do more universe searches.
593 $relatedContactID = key($params['contact_check']);
594
595 // check if the relationship is valid between contacts.
596 // step 1: check if the relationship is valid if not valid skip and keep the count
597 // step 2: check the if two contacts already have a relationship if yes skip and keep the count
598 // step 3: if valid relationship then add the relation and keep the count
599
600 // step 1
601 [$contactFields['relationship_type_id'], $firstLetter, $secondLetter] = explode('_', $params['relationship_type_id']);
602 $contactFields['contact_id_' . $firstLetter] = $contactID;
603 $contactFields['contact_id_' . $secondLetter] = $relatedContactID;
604 if (!CRM_Contact_BAO_Relationship::checkRelationshipType($contactFields['contact_id_a'], $contactFields['contact_id_b'],
605 $contactFields['relationship_type_id'])) {
606 return [0, 0];
607 }
608
609 if (
610 CRM_Contact_BAO_Relationship::checkDuplicateRelationship(
611 $contactFields,
612 $contactID,
613 // step 2
614 $relatedContactID
615 )
616 ) {
617 return [0, 1];
618 }
619
620 $singleInstanceParams = array_merge($params, $contactFields);
621 CRM_Contact_BAO_Relationship::add($singleInstanceParams);
622 return [1, 0];
623 }
624
625 /**
626 * Format common params data to proper format to store.
627 *
628 * @param array $params
629 * Contain record values.
630 * @param array $formatted
631 * Array of formatted data.
632 * @param array $contactFields
633 * Contact DAO fields.
634 */
635 private function formatCommonData($params, &$formatted, $contactFields) {
636 $customFields = CRM_Core_BAO_CustomField::getFields($formatted['contact_type'], FALSE, FALSE, $formatted['contact_sub_type'] ?? NULL);
637
638 $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address');
639 $customFields = $customFields + $addressCustomFields;
640
641 //format date first
642 $session = CRM_Core_Session::singleton();
643 $dateType = $session->get("dateTypes");
644 foreach ($params as $key => $val) {
645 $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key);
646 if ($customFieldID &&
647 !array_key_exists($customFieldID, $addressCustomFields)
648 ) {
649 //we should not update Date to null, CRM-4062
650 if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) {
651 //CRM-21267
652 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key);
653 }
654 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
655 if (empty($val) && !is_numeric($val) && $this->_onDuplicate == CRM_Import_Parser::DUPLICATE_FILL) {
656 //retain earlier value when Import mode is `Fill`
657 unset($params[$key]);
658 }
659 else {
660 $params[$key] = CRM_Utils_String::strtoboolstr($val);
661 }
662 }
663 }
664 }
665 $metadataBlocks = ['phone', 'im', 'openid', 'email', 'address'];
666 foreach ($metadataBlocks as $block) {
667 foreach ($formatted[$block] ?? [] as $blockKey => $blockValues) {
668 if ($blockValues['location_type_id'] === 'Primary') {
669 $this->fillPrimary($formatted[$block][$blockKey], $blockValues, $block, $formatted['id'] ?? NULL);
670 }
671 }
672 }
673 //now format custom data.
674 foreach ($params as $key => $field) {
675 if (in_array($key, $metadataBlocks, TRUE)) {
676 // This location block is already fully handled at this point.
677 continue;
678 }
679 if (is_array($field)) {
680 $isAddressCustomField = FALSE;
681
682 foreach ($field as $value) {
683 $break = FALSE;
684 if (is_array($value)) {
685 foreach ($value as $name => $testForEmpty) {
686 if ($addressCustomFieldID = CRM_Core_BAO_CustomField::getKeyID($name)) {
687 $isAddressCustomField = TRUE;
688 break;
689 }
690
691 if (($testForEmpty === '' || $testForEmpty == NULL)) {
692 $break = TRUE;
693 break;
694 }
695 }
696 }
697 else {
698 $break = TRUE;
699 }
700
701 if (!$break) {
702 if (!empty($value['location_type_id'])) {
703 $this->formatLocationBlock($value, $formatted);
704 }
705 }
706 }
707 if (!$isAddressCustomField) {
708 continue;
709 }
710 }
711
712 $formatValues = [
713 $key => $field,
714 ];
715
716 if ($key == 'id' && isset($field)) {
717 $formatted[$key] = $field;
718 }
719 $this->formatContactParameters($formatValues, $formatted);
720
721 //Handling Custom Data
722 // note: Address custom fields will be handled separately inside formatContactParameters
723 if (($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) &&
724 array_key_exists($customFieldID, $customFields) &&
725 !array_key_exists($customFieldID, $addressCustomFields)
726 ) {
727
728 $extends = $customFields[$customFieldID]['extends'] ?? NULL;
729 $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL;
730 $dataType = $customFields[$customFieldID]['data_type'] ?? NULL;
731 $serialized = CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]);
732
733 if (!$serialized && in_array($htmlType, ['Select', 'Radio', 'Autocomplete-Select']) && in_array($dataType, ['String', 'Int'])) {
734 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
735 foreach ($customOption as $customValue) {
736 $val = $customValue['value'] ?? NULL;
737 $label = strtolower($customValue['label'] ?? '');
738 $value = strtolower(trim($formatted[$key]));
739 if (($value == $label) || ($value == strtolower($val))) {
740 $params[$key] = $formatted[$key] = $val;
741 }
742 }
743 }
744 elseif ($serialized && !empty($formatted[$key]) && !empty($params[$key])) {
745 $mulValues = explode(',', $formatted[$key]);
746 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
747 $formatted[$key] = [];
748 $params[$key] = [];
749 foreach ($mulValues as $v1) {
750 foreach ($customOption as $v2) {
751 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
752 (strtolower($v2['value']) == strtolower(trim($v1)))
753 ) {
754 if ($htmlType == 'CheckBox') {
755 $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1;
756 }
757 else {
758 $params[$key][] = $formatted[$key][] = $v2['value'];
759 }
760 }
761 }
762 }
763 }
764 }
765 }
766
767 if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) && array_key_exists($customFieldID, $customFields) &&
768 !array_key_exists($customFieldID, $addressCustomFields)
769 ) {
770 // @todo calling api functions directly is not supported
771 _civicrm_api3_custom_format_params($params, $formatted, $extends);
772 }
773
774 // to check if not update mode and unset the fields with empty value.
775 if (!$this->_updateWithId && array_key_exists('custom', $formatted)) {
776 foreach ($formatted['custom'] as $customKey => $customvalue) {
777 if (empty($formatted['custom'][$customKey][-1]['is_required'])) {
778 $formatted['custom'][$customKey][-1]['is_required'] = $customFields[$customKey]['is_required'];
779 }
780 $emptyValue = $customvalue[-1]['value'] ?? NULL;
781 if (!isset($emptyValue)) {
782 unset($formatted['custom'][$customKey]);
783 }
784 }
785 }
786
787 // parse street address, CRM-5450
788 if ($this->_parseStreetAddress) {
789 if (array_key_exists('address', $formatted) && is_array($formatted['address'])) {
790 foreach ($formatted['address'] as $instance => & $address) {
791 $streetAddress = $address['street_address'] ?? NULL;
792 if (empty($streetAddress)) {
793 continue;
794 }
795 // parse address field.
796 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($streetAddress);
797
798 //street address consider to be parsed properly,
799 //If we get street_name and street_number.
800 if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) {
801 $parsedFields = array_fill_keys(array_keys($parsedFields), '');
802 }
803
804 // merge parse address w/ main address block.
805 $address = array_merge($address, $parsedFields);
806 }
807 }
808 }
809 }
810
811 /**
812 * Get the array of successfully imported contact id's
813 *
814 * @return array
815 */
816 public function getImportedContacts() {
817 return $this->_newContacts;
818 }
819
820 /**
821 * Get the array of successfully imported related contact id's
822 *
823 * @return array
824 */
825 public function &getRelatedImportedContacts() {
826 return $this->_newRelatedContacts;
827 }
828
829 /**
830 * Check if an error in custom data.
831 *
832 * @param array $params
833 * @param string $errorMessage
834 * A string containing all the error-fields.
835 *
836 * @param null $csType
837 */
838 public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL) {
839 $dateType = CRM_Core_Session::singleton()->get("dateTypes");
840 $errors = [];
841
842 if (!empty($params['contact_sub_type'])) {
843 $csType = $params['contact_sub_type'] ?? NULL;
844 }
845
846 if (empty($params['contact_type'])) {
847 $params['contact_type'] = 'Individual';
848 }
849
850 // get array of subtypes - CRM-18708
851 if (in_array($csType, CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) {
852 $csType = self::getSubtypes($params['contact_type']);
853 }
854
855 if (is_array($csType)) {
856 // fetch custom fields for every subtype and add it to $customFields array
857 // CRM-18708
858 $customFields = [];
859 foreach ($csType as $cType) {
860 $customFields += CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $cType);
861 }
862 }
863 else {
864 $customFields = CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $csType);
865 }
866
867 $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address');
868 $parser = new CRM_Contact_Import_Parser_Contact();
869 foreach ($params as $key => $value) {
870 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
871 //For address custom fields, we do get actual custom field value as an inner array of
872 //values so need to modify
873 if (!array_key_exists($customFieldID, $customFields)) {
874 return ts('field ID');
875 }
876 /* check if it's a valid custom field id */
877 $errors[] = $parser->validateCustomField($customFieldID, $value, $customFields[$customFieldID], $dateType);
878 }
879 elseif (is_array($params[$key]) && isset($params[$key]["contact_type"]) && in_array(substr($key, -3), ['a_b', 'b_a'], TRUE)) {
880 //CRM-5125
881 //supporting custom data of related contact subtypes
882 $relation = $key;
883 if (!empty($relation)) {
884 [$id, $first, $second] = CRM_Utils_System::explode('_', $relation, 3);
885 $direction = "contact_sub_type_$second";
886 $relationshipType = new CRM_Contact_BAO_RelationshipType();
887 $relationshipType->id = $id;
888 if ($relationshipType->find(TRUE)) {
889 if (isset($relationshipType->$direction)) {
890 $params[$key]['contact_sub_type'] = $relationshipType->$direction;
891 }
892 }
893 }
894
895 self::isErrorInCustomData($params[$key], $errorMessage, $csType);
896 }
897 }
898 if ($errors) {
899 $errorMessage .= ($errorMessage ? '; ' : '') . implode('; ', array_filter($errors));
900 }
901 }
902
903 /**
904 * Check if an error in Core( non-custom fields ) field
905 *
906 * @param array $params
907 * @param string $errorMessage
908 * A string containing all the error-fields.
909 */
910 public function isErrorInCoreData($params, &$errorMessage) {
911 $errors = [];
912 if (!empty($params['contact_sub_type']) && !CRM_Contact_BAO_ContactType::isExtendsContactType($params['contact_sub_type'], $params['contact_type'])) {
913 $errors[] = ts('Mismatched or Invalid Contact Subtype.');
914 }
915
916 foreach ($params as $key => $value) {
917 if ($value) {
918
919 switch ($key) {
920 case 'do_not_email':
921 case 'do_not_phone':
922 case 'do_not_mail':
923 case 'do_not_sms':
924 case 'do_not_trade':
925 if (CRM_Utils_Rule::boolean($value) == FALSE) {
926 $key = ucwords(str_replace("_", " ", $key));
927 $errors[] = $key;
928 }
929 break;
930
931 default:
932 if (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
933 //check for any relationship data ,FIX ME
934 self::isErrorInCoreData($params[$key], $errorMessage);
935 }
936 }
937 }
938 }
939 if ($errors) {
940 $errorMessage .= ($errorMessage ? '; ' : '') . implode('; ', $errors);
941 }
942 }
943
944 /**
945 * Ckeck a value present or not in a array.
946 *
947 * @param $value
948 * @param $valueArray
949 *
950 * @return bool
951 */
952 public static function in_value($value, $valueArray) {
953 foreach ($valueArray as $key => $v) {
954 //fix for CRM-1514
955 if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) {
956 return TRUE;
957 }
958 }
959 return FALSE;
960 }
961
962 /**
963 * Build error-message containing error-fields
964 *
965 * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function.
966 *
967 * @todo just say no!
968 *
969 * @param string $errorName
970 * A string containing error-field name.
971 * @param string $errorMessage
972 * A string containing all the error-fields, where the new errorName is concatenated.
973 *
974 */
975 public static function addToErrorMsg($errorName, &$errorMessage) {
976 if ($errorMessage) {
977 $errorMessage .= "; $errorName";
978 }
979 else {
980 $errorMessage = $errorName;
981 }
982 }
983
984 /**
985 * Method for creating contact.
986 *
987 * @param array $formatted
988 * @param array $contactFields
989 * @param int $onDuplicate
990 * @param int $contactId
991 * @param bool $requiredCheck
992 * @param int $dedupeRuleGroupID
993 *
994 * @return array|\CRM_Contact_BAO_Contact
995 * If a duplicate is found an array is returned, otherwise CRM_Contact_BAO_Contact
996 */
997 public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) {
998 $dupeCheck = FALSE;
999 $newContact = NULL;
1000
1001 if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser::DUPLICATE_NOCHECK)) {
1002 $dupeCheck = (bool) ($onDuplicate);
1003 }
1004
1005 if ($dupeCheck) {
1006 // @todo this is already done in lookupContactID
1007 // the differences are that a couple of functions are callled in between
1008 // and that call doesn't error out if multiple are found. - once
1009 // those 2 things are fixed this can go entirely.
1010 $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($formatted, $formatted['contact_type'], 'Unsupervised', [], FALSE, $dedupeRuleGroupID);
1011
1012 if ($ids != NULL) {
1013 return [
1014 'is_error' => 1,
1015 'error_message' => [
1016 'code' => CRM_Core_Error::DUPLICATE_CONTACT,
1017 'params' => $ids,
1018 'level' => 'Fatal',
1019 'message' => 'Found matching contacts: ' . implode(',', $ids),
1020 ],
1021 ];
1022 }
1023 }
1024
1025 if ($contactId) {
1026 $this->formatParams($formatted, $onDuplicate, (int) $contactId);
1027 }
1028
1029 // Resetting and rebuilding cache could be expensive.
1030 CRM_Core_Config::setPermitCacheFlushMode(FALSE);
1031
1032 // If a user has logged in, or accessed via a checksum
1033 // Then deliberately 'blanking' a value in the profile should remove it from their record
1034 // @todo this should either be TRUE or FALSE in the context of import - once
1035 // we figure out which we can remove all the rest.
1036 // Also note the meaning of this parameter is less than it used to
1037 // be following block cleanup.
1038 $formatted['updateBlankLocInfo'] = TRUE;
1039 if ((CRM_Core_Session::singleton()->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0) {
1040 $formatted['updateBlankLocInfo'] = FALSE;
1041 }
1042
1043 [$data, $contactDetails] = $this->formatProfileContactParams($formatted, $contactFields, $contactId, $formatted['contact_type']);
1044
1045 // manage is_opt_out
1046 if (array_key_exists('is_opt_out', $contactFields) && array_key_exists('is_opt_out', $formatted)) {
1047 $wasOptOut = $contactDetails['is_opt_out'] ?? FALSE;
1048 $isOptOut = $formatted['is_opt_out'];
1049 $data['is_opt_out'] = $isOptOut;
1050 // on change, create new civicrm_subscription_history entry
1051 if (($wasOptOut != $isOptOut) && !empty($contactDetails['contact_id'])) {
1052 $shParams = [
1053 'contact_id' => $contactDetails['contact_id'],
1054 'status' => $isOptOut ? 'Removed' : 'Added',
1055 'method' => 'Web',
1056 ];
1057 CRM_Contact_BAO_SubscriptionHistory::create($shParams);
1058 }
1059 }
1060
1061 $contact = civicrm_api3('Contact', 'create', $data);
1062 $cid = $contact['id'];
1063
1064 CRM_Core_Config::setPermitCacheFlushMode(TRUE);
1065
1066 $contact = [
1067 'contact_id' => $cid,
1068 ];
1069
1070 $defaults = [];
1071 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
1072
1073 //get the id of the contact whose street address is not parsable, CRM-5886
1074 if ($this->_parseStreetAddress && is_object($newContact) && property_exists($newContact, 'address') && $newContact->address) {
1075 foreach ($newContact->address as $address) {
1076 if (!empty($address['street_address']) && (empty($address['street_number']) || empty($address['street_name']))) {
1077 $this->_unparsedStreetAddressContacts[] = [
1078 'id' => $newContact->id,
1079 'streetAddress' => $address['street_address'],
1080 ];
1081 }
1082 }
1083 }
1084 return $newContact;
1085 }
1086
1087 /**
1088 * Legacy format profile contact parameters.
1089 *
1090 * This is a formerly shared function - most of the stuff in it probably does
1091 * nothing but copied here to star unravelling that...
1092 *
1093 * @param array $params
1094 * @param array $fields
1095 * @param int|null $contactID
1096 * @param string|null $ctype
1097 *
1098 * @return array
1099 */
1100 private function formatProfileContactParams(
1101 &$params,
1102 $fields,
1103 $contactID = NULL,
1104 $ctype = NULL
1105 ) {
1106
1107 $data = $contactDetails = [];
1108
1109 // get the contact details (hier)
1110 if ($contactID) {
1111 $details = CRM_Contact_BAO_Contact::getHierContactDetails($contactID, $fields);
1112
1113 $contactDetails = $details[$contactID];
1114 $data['contact_type'] = $contactDetails['contact_type'] ?? NULL;
1115 $data['contact_sub_type'] = $contactDetails['contact_sub_type'] ?? NULL;
1116 }
1117 else {
1118 //we should get contact type only if contact
1119 if ($ctype) {
1120 $data['contact_type'] = $ctype;
1121 }
1122 else {
1123 $data['contact_type'] = 'Individual';
1124 }
1125 }
1126
1127 //fix contact sub type CRM-5125
1128 if (array_key_exists('contact_sub_type', $params) &&
1129 !empty($params['contact_sub_type'])
1130 ) {
1131 $data['contact_sub_type'] = CRM_Utils_Array::implodePadded($params['contact_sub_type']);
1132 }
1133 elseif (array_key_exists('contact_sub_type_hidden', $params) &&
1134 !empty($params['contact_sub_type_hidden'])
1135 ) {
1136 // if profile was used, and had any subtype, we obtain it from there
1137 //CRM-13596 - add to existing contact types, rather than overwriting
1138 if (empty($data['contact_sub_type'])) {
1139 // If we don't have a contact ID the $data['contact_sub_type'] will not be defined...
1140 $data['contact_sub_type'] = CRM_Utils_Array::implodePadded($params['contact_sub_type_hidden']);
1141 }
1142 else {
1143 $data_contact_sub_type_arr = CRM_Utils_Array::explodePadded($data['contact_sub_type']);
1144 if (!in_array($params['contact_sub_type_hidden'], $data_contact_sub_type_arr)) {
1145 //CRM-20517 - make sure contact_sub_type gets the correct delimiters
1146 $data['contact_sub_type'] = trim($data['contact_sub_type'], CRM_Core_DAO::VALUE_SEPARATOR);
1147 $data['contact_sub_type'] = CRM_Core_DAO::VALUE_SEPARATOR . $data['contact_sub_type'] . CRM_Utils_Array::implodePadded($params['contact_sub_type_hidden']);
1148 }
1149 }
1150 }
1151
1152 if ($ctype == 'Organization') {
1153 $data['organization_name'] = $contactDetails['organization_name'] ?? NULL;
1154 }
1155 elseif ($ctype == 'Household') {
1156 $data['household_name'] = $contactDetails['household_name'] ?? NULL;
1157 }
1158
1159 $locationType = [];
1160 $count = 1;
1161
1162 if ($contactID) {
1163 //add contact id
1164 $data['contact_id'] = $contactID;
1165 $primaryLocationType = CRM_Contact_BAO_Contact::getPrimaryLocationType($contactID);
1166 }
1167 else {
1168 $defaultLocation = CRM_Core_BAO_LocationType::getDefault();
1169 $defaultLocationId = $defaultLocation->id;
1170 }
1171
1172 $billingLocationTypeId = CRM_Core_BAO_LocationType::getBilling();
1173
1174 $multiplFields = ['url'];
1175
1176 $session = CRM_Core_Session::singleton();
1177 foreach ($params as $key => $value) {
1178 [$fieldName, $locTypeId, $typeId] = CRM_Utils_System::explode('-', $key, 3);
1179
1180 if ($locTypeId == 'Primary') {
1181 if ($contactID) {
1182 $locTypeId = CRM_Contact_BAO_Contact::getPrimaryLocationType($contactID, FALSE, 'address');
1183 $primaryLocationType = $locTypeId;
1184 }
1185 else {
1186 $locTypeId = $defaultLocationId;
1187 }
1188 }
1189
1190 if (is_numeric($locTypeId) &&
1191 !in_array($fieldName, $multiplFields) &&
1192 substr($fieldName, 0, 7) != 'custom_'
1193 ) {
1194 $index = $locTypeId;
1195
1196 if (is_numeric($typeId)) {
1197 $index .= '-' . $typeId;
1198 }
1199 if (!in_array($index, $locationType)) {
1200 $locationType[$count] = $index;
1201 $count++;
1202 }
1203
1204 $loc = CRM_Utils_Array::key($index, $locationType);
1205
1206 $blockName = strtolower($this->getFieldEntity($fieldName));
1207
1208 $data[$blockName][$loc]['location_type_id'] = $locTypeId;
1209
1210 //set is_billing true, for location type "Billing"
1211 if ($locTypeId == $billingLocationTypeId) {
1212 $data[$blockName][$loc]['is_billing'] = 1;
1213 }
1214
1215 if ($contactID) {
1216 //get the primary location type
1217 if ($locTypeId == $primaryLocationType) {
1218 $data[$blockName][$loc]['is_primary'] = 1;
1219 }
1220 }
1221 elseif ($locTypeId == $defaultLocationId) {
1222 $data[$blockName][$loc]['is_primary'] = 1;
1223 }
1224
1225 if (0) {
1226 }
1227 else {
1228 if ($fieldName === 'state_province') {
1229 // CRM-3393
1230 if (is_numeric($value) && ((int ) $value) >= 1000) {
1231 $data['address'][$loc]['state_province_id'] = $value;
1232 }
1233 elseif (empty($value)) {
1234 $data['address'][$loc]['state_province_id'] = '';
1235 }
1236 else {
1237 $data['address'][$loc]['state_province'] = $value;
1238 }
1239 }
1240 elseif ($fieldName === 'country_id') {
1241 $data['address'][$loc]['country_id'] = $value;
1242 }
1243 elseif ($fieldName === 'county') {
1244 $data['address'][$loc]['county_id'] = $value;
1245 }
1246 elseif ($fieldName == 'address_name') {
1247 $data['address'][$loc]['name'] = $value;
1248 }
1249 elseif (substr($fieldName, 0, 14) === 'address_custom') {
1250 $data['address'][$loc][substr($fieldName, 8)] = $value;
1251 }
1252 else {
1253 $data[$blockName][$loc][$fieldName] = $value;
1254 }
1255 }
1256 }
1257 else {
1258 if (($customFieldId = CRM_Core_BAO_CustomField::getKeyID($key))) {
1259 // for autocomplete transfer hidden value instead of label
1260 if ($params[$key] && isset($params[$key . '_id'])) {
1261 $value = $params[$key . '_id'];
1262 }
1263
1264 // we need to append time with date
1265 if ($params[$key] && isset($params[$key . '_time'])) {
1266 $value .= ' ' . $params[$key . '_time'];
1267 }
1268
1269 // if auth source is not checksum / login && $value is blank, do not proceed - CRM-10128
1270 if (($session->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0 &&
1271 ($value == '' || !isset($value))
1272 ) {
1273 continue;
1274 }
1275
1276 $valueId = NULL;
1277
1278 //CRM-13596 - check for contact_sub_type_hidden first
1279 if (array_key_exists('contact_sub_type_hidden', $params)) {
1280 $type = $params['contact_sub_type_hidden'];
1281 }
1282 else {
1283 $type = $data['contact_type'];
1284 if (!empty($data['contact_sub_type'])) {
1285 $type = CRM_Utils_Array::explodePadded($data['contact_sub_type']);
1286 }
1287 }
1288
1289 CRM_Core_BAO_CustomField::formatCustomField($customFieldId,
1290 $data['custom'],
1291 $value,
1292 $type,
1293 $valueId,
1294 $contactID,
1295 FALSE,
1296 FALSE
1297 );
1298 }
1299 elseif ($key === 'edit') {
1300 continue;
1301 }
1302 else {
1303 if ($key === 'location') {
1304 foreach ($value as $locationTypeId => $field) {
1305 foreach ($field as $block => $val) {
1306 if ($block === 'address' && array_key_exists('address_name', $val)) {
1307 $value[$locationTypeId][$block]['name'] = $value[$locationTypeId][$block]['address_name'];
1308 }
1309 }
1310 }
1311 }
1312 if (in_array($key, ['nick_name', 'job_title', 'middle_name', 'birth_date', 'gender_id', 'current_employer', 'prefix_id', 'suffix_id'])
1313 && ($value == '' || !isset($value)) &&
1314 ($session->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0 ||
1315 ($key === 'current_employer' && empty($params['current_employer']))) {
1316 // CRM-10128: if auth source is not checksum / login && $value is blank, do not fill $data with empty value
1317 // to avoid update with empty values
1318 continue;
1319 }
1320 else {
1321 $data[$key] = $value;
1322 }
1323 }
1324 }
1325 }
1326
1327 if (!isset($data['contact_type'])) {
1328 $data['contact_type'] = 'Individual';
1329 }
1330
1331 //set the values for checkboxes (do_not_email, do_not_mail, do_not_trade, do_not_phone)
1332 $privacy = CRM_Core_SelectValues::privacy();
1333 foreach ($privacy as $key => $value) {
1334 if (array_key_exists($key, $fields)) {
1335 // do not reset values for existing contacts, if fields are added to a profile
1336 if (array_key_exists($key, $params)) {
1337 $data[$key] = $params[$key];
1338 if (empty($params[$key])) {
1339 $data[$key] = 0;
1340 }
1341 }
1342 elseif (!$contactID) {
1343 $data[$key] = 0;
1344 }
1345 }
1346 }
1347
1348 return [$data, $contactDetails];
1349 }
1350
1351 /**
1352 * Format params for update and fill mode.
1353 *
1354 * @param array $params
1355 * reference to an array containing all the.
1356 * values for import
1357 * @param int $onDuplicate
1358 * @param int $cid
1359 * contact id.
1360 */
1361 public function formatParams(&$params, $onDuplicate, $cid) {
1362 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
1363 return;
1364 }
1365
1366 $contactParams = [
1367 'contact_id' => $cid,
1368 ];
1369
1370 $defaults = [];
1371 $contactObj = CRM_Contact_BAO_Contact::retrieve($contactParams, $defaults);
1372
1373 $modeFill = ($onDuplicate == CRM_Import_Parser::DUPLICATE_FILL);
1374
1375 $groupTree = CRM_Core_BAO_CustomGroup::getTree($params['contact_type'], NULL, $cid, 0, NULL);
1376 CRM_Core_BAO_CustomGroup::setDefaults($groupTree, $defaults, FALSE, FALSE);
1377
1378 $locationFields = [
1379 'address' => 'address',
1380 ];
1381
1382 $contact = get_object_vars($contactObj);
1383
1384 foreach ($params as $key => $value) {
1385 if ($key == 'id' || $key == 'contact_type') {
1386 continue;
1387 }
1388
1389 if (array_key_exists($key, $locationFields)) {
1390 continue;
1391 }
1392
1393 if ($customFieldId = CRM_Core_BAO_CustomField::getKeyID($key)) {
1394 $custom_params = ['id' => $contact['id'], 'return' => $key];
1395 $getValue = civicrm_api3('Contact', 'getvalue', $custom_params);
1396 if (empty($getValue)) {
1397 unset($getValue);
1398 }
1399 }
1400 else {
1401 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $key);
1402 }
1403 if ($key == 'contact_source') {
1404 $params['source'] = $params[$key];
1405 unset($params[$key]);
1406 }
1407
1408 if ($modeFill && isset($getValue)) {
1409 unset($params[$key]);
1410 if ($customFieldId) {
1411 // Extra values must be unset to ensure the values are not
1412 // imported.
1413 unset($params['custom'][$customFieldId]);
1414 }
1415 }
1416 }
1417
1418 foreach ($locationFields as $locKeys) {
1419 if (isset($params[$locKeys]) && is_array($params[$locKeys])) {
1420 foreach ($params[$locKeys] as $key => $value) {
1421 if ($modeFill) {
1422 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $locKeys);
1423
1424 if (isset($getValue)) {
1425 foreach ($getValue as $cnt => $values) {
1426 if ((!empty($getValue[$cnt]['location_type_id']) && !empty($params[$locKeys][$key]['location_type_id'])) && $getValue[$cnt]['location_type_id'] == $params[$locKeys][$key]['location_type_id']) {
1427 unset($params[$locKeys][$key]);
1428 }
1429 }
1430 }
1431 }
1432 }
1433 if (count($params[$locKeys]) == 0) {
1434 unset($params[$locKeys]);
1435 }
1436 }
1437 }
1438 }
1439
1440 /**
1441 * Convert any given date string to default date array.
1442 *
1443 * @param array $params
1444 * Has given date-format.
1445 * @param array $formatted
1446 * Store formatted date in this array.
1447 * @param int $dateType
1448 * Type of date.
1449 * @param string $dateParam
1450 * Index of params.
1451 */
1452 public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) {
1453 //fix for CRM-2687
1454 CRM_Utils_Date::convertToDefaultDate($params, $dateType, $dateParam);
1455 $formatted[$dateParam] = CRM_Utils_Date::processDate($params[$dateParam]);
1456 }
1457
1458 /**
1459 * Generate status and error message for unparsed street address records.
1460 *
1461 * @param array $values
1462 * The array of values belonging to each row.
1463 * @param $returnCode
1464 *
1465 * @return int
1466 */
1467 private function processMessage(&$values, $returnCode) {
1468 if (empty($this->_unparsedStreetAddressContacts)) {
1469 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '');
1470 }
1471 else {
1472 $errorMessage = ts("Record imported successfully but unable to parse the street address: ");
1473 foreach ($this->_unparsedStreetAddressContacts as $contactInfo => $contactValue) {
1474 $contactUrl = CRM_Utils_System::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contactValue['id'], TRUE, NULL, FALSE);
1475 $errorMessage .= "\n Contact ID:" . $contactValue['id'] . " <a href=\"$contactUrl\"> " . $contactValue['streetAddress'] . "</a>";
1476 }
1477 array_unshift($values, $errorMessage);
1478 $returnCode = CRM_Import_Parser::UNPARSED_ADDRESS_WARNING;
1479 $this->setImportStatus((int) ($values[count($values) - 1]), 'ERROR', $errorMessage);
1480 }
1481 return $returnCode;
1482 }
1483
1484 /**
1485 * get subtypes given the contact type
1486 *
1487 * @param string $contactType
1488 * @return array $subTypes
1489 */
1490 public static function getSubtypes($contactType) {
1491 $subTypes = [];
1492 $types = CRM_Contact_BAO_ContactType::subTypeInfo($contactType);
1493
1494 if (count($types) > 0) {
1495 foreach ($types as $type) {
1496 $subTypes[] = $type['name'];
1497 }
1498 }
1499 return $subTypes;
1500 }
1501
1502 /**
1503 * Get the possible contact matches.
1504 *
1505 * 1) the chosen dedupe rule falling back to
1506 * 2) a check for the external ID.
1507 *
1508 * @see https://issues.civicrm.org/jira/browse/CRM-17275
1509 *
1510 * @param array $params
1511 * @param int|null $extIDMatch
1512 * @param int|null $dedupeRuleID
1513 *
1514 * @return int|null
1515 * IDs of a possible.
1516 *
1517 * @throws \CRM_Core_Exception
1518 * @throws \CiviCRM_API3_Exception
1519 */
1520 protected function getPossibleContactMatch(array $params, ?int $extIDMatch, ?int $dedupeRuleID): ?int {
1521 $checkParams = ['check_permissions' => FALSE, 'match' => $params, 'dedupe_rule_id' => $dedupeRuleID];
1522 $possibleMatches = civicrm_api3('Contact', 'duplicatecheck', $checkParams);
1523 if (!$extIDMatch) {
1524 // Historically we have used the last ID - it is not clear if this was
1525 // deliberate.
1526 return array_key_last($possibleMatches['values']);
1527 }
1528 if ($possibleMatches['count']) {
1529 if (array_key_exists($extIDMatch, $possibleMatches['values'])) {
1530 return $extIDMatch;
1531 }
1532 throw new CRM_Core_Exception(ts(
1533 'Matching this contact based on the de-dupe rule would cause an external ID conflict'));
1534 }
1535 return $extIDMatch;
1536 }
1537
1538 /**
1539 * Format the form mapping parameters ready for the parser.
1540 *
1541 * @param int $count
1542 * Number of rows.
1543 *
1544 * @return array $parserParameters
1545 */
1546 public static function getParameterForParser($count) {
1547 $baseArray = [];
1548 for ($i = 0; $i < $count; $i++) {
1549 $baseArray[$i] = NULL;
1550 }
1551 $parserParameters['mapperLocType'] = $baseArray;
1552 $parserParameters['mapperPhoneType'] = $baseArray;
1553 $parserParameters['mapperImProvider'] = $baseArray;
1554 $parserParameters['mapperWebsiteType'] = $baseArray;
1555 $parserParameters['mapperRelated'] = $baseArray;
1556 $parserParameters['relatedContactType'] = $baseArray;
1557 $parserParameters['relatedContactDetails'] = $baseArray;
1558 $parserParameters['relatedContactLocType'] = $baseArray;
1559 $parserParameters['relatedContactPhoneType'] = $baseArray;
1560 $parserParameters['relatedContactImProvider'] = $baseArray;
1561 $parserParameters['relatedContactWebsiteType'] = $baseArray;
1562
1563 return $parserParameters;
1564
1565 }
1566
1567 /**
1568 * Set field metadata.
1569 */
1570 protected function setFieldMetadata() {
1571 $this->setImportableFieldsMetadata($this->getContactImportMetadata());
1572 }
1573
1574 /**
1575 * @param array $newContact
1576 * @param array $values
1577 * @param int $onDuplicate
1578 * @param array $formatted
1579 * @param array $contactFields
1580 *
1581 * @return int
1582 *
1583 * @throws \CRM_Core_Exception
1584 * @throws \CiviCRM_API3_Exception
1585 * @throws \Civi\API\Exception\UnauthorizedException
1586 */
1587 private function handleDuplicateError(array $newContact, array $values, int $onDuplicate, array $formatted, array $contactFields): int {
1588 $urls = [];
1589 // need to fix at some stage and decide if the error will return an
1590 // array or string, crude hack for now
1591 if (is_array($newContact['error_message']['params'][0])) {
1592 $cids = $newContact['error_message']['params'][0];
1593 }
1594 else {
1595 $cids = explode(',', $newContact['error_message']['params'][0]);
1596 }
1597
1598 foreach ($cids as $cid) {
1599 $urls[] = CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE);
1600 }
1601
1602 $url_string = implode("\n", $urls);
1603
1604 // If we duplicate more than one record, skip no matter what
1605 if (count($cids) > 1) {
1606 $errorMessage = ts('Record duplicates multiple contacts');
1607 //combine error msg to avoid mismatch between error file columns.
1608 $errorMessage .= "\n" . $url_string;
1609 array_unshift($values, $errorMessage);
1610 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $errorMessage);
1611 return CRM_Import_Parser::ERROR;
1612 }
1613
1614 // Params only had one id, so shift it out
1615 $contactId = array_shift($cids);
1616 $cid = NULL;
1617
1618 $vals = ['contact_id' => $contactId];
1619 if (in_array((int) $onDuplicate, [CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::DUPLICATE_FILL], TRUE)) {
1620 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactId);
1621 }
1622 // else skip does nothing and just returns an error code.
1623 if ($cid) {
1624 $contact = [
1625 'contact_id' => $cid,
1626 ];
1627 $defaults = [];
1628 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
1629 }
1630
1631 if (is_array($newContact)) {
1632 $contactID = $newContact['error_message']['params'][0];
1633 if (is_array($contactID)) {
1634 $contactID = array_pop($contactID);
1635 }
1636 if (!in_array($contactID, $this->_newContacts)) {
1637 $this->_newContacts[] = $contactID;
1638 }
1639 }
1640 //CRM-262 No Duplicate Checking
1641 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
1642 array_unshift($values, $url_string);
1643 $this->setImportStatus((int) $values[count($values) - 1], 'DUPLICATE', 'Skipping duplicate record');
1644 return CRM_Import_Parser::DUPLICATE;
1645 }
1646
1647 $this->setImportStatus((int) $values[count($values) - 1], 'Imported', '');
1648 //return warning if street address is not parsed, CRM-5886
1649 return $this->processMessage($values, CRM_Import_Parser::VALID);
1650 }
1651
1652 /**
1653 * Run import.
1654 *
1655 * @param array $mapper Mapping as entered on MapField form.
1656 * e.g [['first_name']['email', 1]].
1657 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
1658 * @param int $mode
1659 * @param int $statusID
1660 *
1661 * @return mixed
1662 * @throws \API_Exception|\CRM_Core_Exception
1663 */
1664 public function run(
1665 $mapper = [],
1666 $mode = self::MODE_PREVIEW,
1667 $statusID = NULL
1668 ) {
1669
1670 // TODO: Make the timeout actually work
1671 $this->_onDuplicate = $onDuplicate = $this->getSubmittedValue('onDuplicate');
1672 $this->_dedupeRuleGroupID = $this->getSubmittedValue('dedupe_rule_id');
1673 // Since $this->_contactType is still being called directly do a get call
1674 // here to make sure it is instantiated.
1675 $this->getContactType();
1676 $this->getContactSubType();
1677
1678 $this->init();
1679
1680 $this->_rowCount = 0;
1681 $this->_totalCount = 0;
1682
1683 $this->_primaryKeyName = '_id';
1684 $this->_statusFieldName = '_status';
1685
1686 if ($statusID) {
1687 $this->progressImport($statusID);
1688 $startTimestamp = $currTimestamp = $prevTimestamp = time();
1689 }
1690 $dataSource = $this->getDataSourceObject();
1691 $totalRowCount = $dataSource->getRowCount(['new']);
1692 if ($mode == self::MODE_IMPORT) {
1693 $dataSource->setStatuses(['new']);
1694 }
1695
1696 while ($row = $dataSource->getRow()) {
1697 $values = array_values($row);
1698 $this->_rowCount++;
1699
1700 $this->_totalCount++;
1701
1702 if ($mode == self::MODE_PREVIEW) {
1703 $returnCode = $this->preview($values);
1704 }
1705 elseif ($mode == self::MODE_SUMMARY) {
1706 $returnCode = $this->summary($values);
1707 }
1708 elseif ($mode == self::MODE_IMPORT) {
1709 try {
1710 $returnCode = $this->import($onDuplicate, $values);
1711 }
1712 catch (CiviCRM_API3_Exception $e) {
1713 // When we catch errors here we are not adding to the errors array - mostly
1714 // because that will become obsolete once https://github.com/civicrm/civicrm-core/pull/23292
1715 // is merged and this will replace it as the main way to handle errors (ie. update the table
1716 // and move on).
1717 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $e->getMessage());
1718 }
1719 if ($statusID && (($this->_rowCount % 50) == 0)) {
1720 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
1721 }
1722 }
1723 // @todo this should be done within import - it probably is!
1724 if (isset($returnCode) && $returnCode === self::UNPARSED_ADDRESS_WARNING) {
1725 $this->setImportStatus((int) $values[count($values) - 1], 'warning_unparsed_address', array_shift($values));
1726 }
1727 }
1728 }
1729
1730 /**
1731 * Given a list of the importable field keys that the user has selected.
1732 * set the active fields array to this list
1733 *
1734 * @param array $fieldKeys
1735 * Mapped array of values.
1736 */
1737 public function setActiveFields($fieldKeys) {
1738 foreach ($fieldKeys as $key) {
1739 if (empty($this->_fields[$key])) {
1740 $this->_activeFields[] = new CRM_Contact_Import_Field('', ts('- do not import -'));
1741 }
1742 else {
1743 $this->_activeFields[] = clone($this->_fields[$key]);
1744 }
1745 }
1746 }
1747
1748 /**
1749 * Format the field values for input to the api.
1750 *
1751 * @param array $values
1752 * The row from the datasource.
1753 *
1754 * @return array
1755 * Parameters mapped as described in getMappedRow
1756 *
1757 * @throws \API_Exception
1758 * @todo - clean this up a bit & merge back into `getMappedRow`
1759 *
1760 */
1761 private function getParams(array $values): array {
1762 $params = [];
1763
1764 foreach ($this->getFieldMappings() as $i => $mappedField) {
1765 // The key is in the format 5_a_b where 5 is the relationship_type_id and a_b is the direction.
1766 $relatedContactKey = $mappedField['relationship_type_id'] ? ($mappedField['relationship_type_id'] . '_' . $mappedField['relationship_direction']) : NULL;
1767 $fieldName = $mappedField['name'];
1768 $importedValue = $values[$i];
1769 if ($fieldName === 'do_not_import' || $importedValue === NULL) {
1770 continue;
1771 }
1772
1773 $locationFields = ['location_type_id', 'phone_type_id', 'provider_id', 'website_type_id'];
1774 $locationValues = array_filter(array_intersect_key($mappedField, array_fill_keys($locationFields, 1)));
1775
1776 if ($relatedContactKey) {
1777 if (!isset($params[$relatedContactKey])) {
1778 $params[$relatedContactKey] = [
1779 // These will be over-written by any the importer has chosen but defaults are based on the relationship.
1780 'contact_type' => $this->getRelatedContactType($mappedField['relationship_type_id'], $mappedField['relationship_direction']),
1781 'contact_sub_type' => $this->getRelatedContactSubType($mappedField['relationship_type_id'], $mappedField['relationship_direction']),
1782 ];
1783 }
1784 $this->addFieldToParams($params[$relatedContactKey], $locationValues, $fieldName, $importedValue);
1785 }
1786 else {
1787 $this->addFieldToParams($params, $locationValues, $fieldName, $importedValue);
1788 }
1789 }
1790
1791 $this->fillStateProvince($params);
1792
1793 return $params;
1794 }
1795
1796 /**
1797 * @param string $name
1798 * @param $title
1799 * @param int $type
1800 * @param string $headerPattern
1801 * @param string $dataPattern
1802 * @param bool $hasLocationType
1803 */
1804 public function addField(
1805 $name, $title, $type = CRM_Utils_Type::T_INT,
1806 $headerPattern = '//', $dataPattern = '//',
1807 $hasLocationType = FALSE
1808 ) {
1809 $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
1810 if (empty($name)) {
1811 $this->_fields['doNotImport'] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
1812 }
1813 }
1814
1815 /**
1816 * Store parser values.
1817 *
1818 * @param CRM_Core_Session $store
1819 *
1820 * @param int $mode
1821 */
1822 public function set($store, $mode = self::MODE_SUMMARY) {
1823 }
1824
1825 /**
1826 * Export data to a CSV file.
1827 *
1828 * @param string $fileName
1829 * @param array $header
1830 * @param array $data
1831 */
1832 public static function exportCSV($fileName, $header, $data) {
1833
1834 if (file_exists($fileName) && !is_writable($fileName)) {
1835 CRM_Core_Error::movedSiteError($fileName);
1836 }
1837 //hack to remove '_status', '_statusMsg' and '_id' from error file
1838 $errorValues = [];
1839 $dbRecordStatus = ['IMPORTED', 'ERROR', 'DUPLICATE', 'INVALID', 'NEW'];
1840 foreach ($data as $rowCount => $rowValues) {
1841 $count = 0;
1842 foreach ($rowValues as $key => $val) {
1843 if (in_array($val, $dbRecordStatus) && $count == (count($rowValues) - 3)) {
1844 break;
1845 }
1846 $errorValues[$rowCount][$key] = $val;
1847 $count++;
1848 }
1849 }
1850 $data = $errorValues;
1851
1852 $output = [];
1853 $fd = fopen($fileName, 'w');
1854
1855 foreach ($header as $key => $value) {
1856 $header[$key] = "\"$value\"";
1857 }
1858 $config = CRM_Core_Config::singleton();
1859 $output[] = implode($config->fieldSeparator, $header);
1860
1861 foreach ($data as $datum) {
1862 foreach ($datum as $key => $value) {
1863 $datum[$key] = "\"$value\"";
1864 }
1865 $output[] = implode($config->fieldSeparator, $datum);
1866 }
1867 fwrite($fd, implode("\n", $output));
1868 fclose($fd);
1869 }
1870
1871 /**
1872 * Update the status of the import row to reflect the processing outcome.
1873 *
1874 * @param int $id
1875 * @param string $status
1876 * @param string $message
1877 * @param int|null $entityID
1878 * Optional created entity ID
1879 * @param array $relatedEntityIDs
1880 * Optional array e.g ['related_contact' => 4]
1881 *
1882 * @throws \API_Exception
1883 * @throws \CRM_Core_Exception
1884 */
1885 public function setImportStatus(int $id, string $status, string $message, ?int $entityID = NULL, array $relatedEntityIDs = []): void {
1886 $this->getDataSourceObject()->updateStatus($id, $status, $message, $entityID, $relatedEntityIDs);
1887 }
1888
1889 /**
1890 * Format contact parameters.
1891 *
1892 * @todo this function needs re-writing & re-merging into the main function.
1893 *
1894 * Here be dragons.
1895 *
1896 * @param array $values
1897 * @param array $params
1898 *
1899 * @return bool
1900 */
1901 protected function formatContactParameters(&$values, &$params) {
1902 // Crawl through the possible classes:
1903 // Contact
1904 // Individual
1905 // Household
1906 // Organization
1907 // Location
1908 // Address
1909 // Email
1910 // IM
1911 // Note
1912 // Custom
1913
1914 // first add core contact values since for other Civi modules they are not added
1915 $contactFields = CRM_Contact_DAO_Contact::fields();
1916 _civicrm_api3_store_values($contactFields, $values, $params);
1917
1918 if (isset($values['contact_type'])) {
1919 // we're an individual/household/org property
1920
1921 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact::fields();
1922
1923 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
1924 return TRUE;
1925 }
1926
1927 // Cache the various object fields
1928 // @todo - remove this after confirming this is just a compilation of other-wise-cached fields.
1929 static $fields = [];
1930
1931 if (isset($values['note'])) {
1932 // add a note field
1933 if (!isset($params['note'])) {
1934 $params['note'] = [];
1935 }
1936 $noteBlock = count($params['note']) + 1;
1937
1938 $params['note'][$noteBlock] = [];
1939 if (!isset($fields['Note'])) {
1940 $fields['Note'] = CRM_Core_DAO_Note::fields();
1941 }
1942
1943 // get the current logged in civicrm user
1944 $session = CRM_Core_Session::singleton();
1945 $userID = $session->get('userID');
1946
1947 if ($userID) {
1948 $values['contact_id'] = $userID;
1949 }
1950
1951 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
1952
1953 return TRUE;
1954 }
1955
1956 // Check for custom field values
1957 $customFields = CRM_Core_BAO_CustomField::getFields(CRM_Utils_Array::value('contact_type', $values),
1958 FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE
1959 );
1960
1961 foreach ($values as $key => $value) {
1962 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
1963 // check if it's a valid custom field id
1964
1965 if (!array_key_exists($customFieldID, $customFields)) {
1966 return civicrm_api3_create_error('Invalid custom field ID');
1967 }
1968 else {
1969 $params[$key] = $value;
1970 }
1971 }
1972 }
1973 return TRUE;
1974 }
1975
1976 /**
1977 * Format location block ready for importing.
1978 *
1979 * There is some test coverage for this in
1980 * CRM_Contact_Import_Parser_ContactTest e.g. testImportPrimaryAddress.
1981 *
1982 * @param array $values
1983 * @param array $params
1984 *
1985 * @return bool
1986 * @throws \CiviCRM_API3_Exception
1987 */
1988 protected function formatLocationBlock(&$values, &$params) {
1989 // Note: we doing multiple value formatting here for address custom fields, plus putting into right format.
1990 // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving
1991 // the address in CRM_Core_BAO_Address::create method
1992 if (!empty($values['location_type_id'])) {
1993 static $customFields = [];
1994 if (empty($customFields)) {
1995 $customFields = CRM_Core_BAO_CustomField::getFields('Address');
1996 }
1997 // make a copy of values, as we going to make changes
1998 $newValues = $values;
1999 foreach ($values as $key => $val) {
2000 $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key);
2001 if ($customFieldID && array_key_exists($customFieldID, $customFields)) {
2002
2003 $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL;
2004 if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]) && $val) {
2005 $mulValues = explode(',', $val);
2006 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
2007 $newValues[$key] = [];
2008 foreach ($mulValues as $v1) {
2009 foreach ($customOption as $v2) {
2010 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
2011 (strtolower($v2['value']) == strtolower(trim($v1)))
2012 ) {
2013 if ($htmlType == 'CheckBox') {
2014 $newValues[$key][$v2['value']] = 1;
2015 }
2016 else {
2017 $newValues[$key][] = $v2['value'];
2018 }
2019 }
2020 }
2021 }
2022 }
2023 }
2024 }
2025 // consider new values
2026 $values = $newValues;
2027 }
2028
2029 return TRUE;
2030 }
2031
2032 /**
2033 * Get the field metadata for the relevant entity.
2034 *
2035 * @param string $entity
2036 *
2037 * @return array
2038 */
2039 protected function getMetadataForEntity($entity) {
2040 if (!isset($this->fieldMetadata[$entity])) {
2041 $className = "CRM_Core_DAO_$entity";
2042 $this->fieldMetadata[$entity] = $className::fields();
2043 }
2044 return $this->fieldMetadata[$entity];
2045 }
2046
2047 /**
2048 * Fill in the primary location.
2049 *
2050 * If the contact has a primary address we update it. Otherwise
2051 * we add an address of the default location type.
2052 *
2053 * @param array $params
2054 * Address block parameters
2055 * @param array $values
2056 * Input values
2057 * @param string $entity
2058 * - address, email, phone
2059 * @param int|null $contactID
2060 *
2061 * @throws \CiviCRM_API3_Exception
2062 */
2063 protected function fillPrimary(&$params, $values, $entity, $contactID) {
2064 if ($values['location_type_id'] === 'Primary') {
2065 if ($contactID) {
2066 $primary = civicrm_api3($entity, 'get', [
2067 'return' => 'location_type_id',
2068 'contact_id' => $contactID,
2069 'is_primary' => 1,
2070 'sequential' => 1,
2071 ]);
2072 }
2073 $defaultLocationType = CRM_Core_BAO_LocationType::getDefault();
2074 $params['location_type_id'] = (int) (isset($primary) && $primary['count']) ? $primary['values'][0]['location_type_id'] : $defaultLocationType->id;
2075 $params['is_primary'] = 1;
2076 }
2077 }
2078
2079 /**
2080 * Get the civicrm_mapping_field appropriate layout for the mapper input.
2081 *
2082 * The input looks something like ['street_address', 1]
2083 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
2084 * 1]
2085 *
2086 * @param array $fieldMapping
2087 * Field as submitted on the MapField form - this is a non-associative array,
2088 * the keys of which depend on the data/ field. Generally it will be one of
2089 * [$fieldName],
2090 * [$fieldName, $locationTypeID, $phoneTypeIDOrIMProviderIDIfRelevant],
2091 * [$fieldName, $websiteTypeID],
2092 * If the mapping is for a related contact it will be as above but the first
2093 * key will be the relationship key - eg. 5_a_b.
2094 * @param int $mappingID
2095 * @param int $columnNumber
2096 *
2097 * @return array
2098 * @throws \API_Exception
2099 */
2100 public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array {
2101 $isRelationshipField = preg_match('/\d*_a_b|b_a$/', $fieldMapping[0]);
2102 $fieldName = $isRelationshipField ? $fieldMapping[1] : $fieldMapping[0];
2103 $locationTypeID = NULL;
2104 $possibleLocationField = $isRelationshipField ? 2 : 1;
2105 $entity = strtolower($this->getFieldEntity($fieldName));
2106 if ($entity !== 'website' && is_numeric($fieldMapping[$possibleLocationField] ?? NULL)) {
2107 $locationTypeID = $fieldMapping[$possibleLocationField];
2108 }
2109
2110 return [
2111 'name' => $fieldName,
2112 'mapping_id' => $mappingID,
2113 'relationship_type_id' => $isRelationshipField ? substr($fieldMapping[0], 0, -4) : NULL,
2114 'relationship_direction' => $isRelationshipField ? substr($fieldMapping[0], -3) : NULL,
2115 'column_number' => $columnNumber,
2116 'contact_type' => $this->getContactType(),
2117 'website_type_id' => $entity !== 'website' ? NULL : ($isRelationshipField ? $fieldMapping[2] : $fieldMapping[1]),
2118 'phone_type_id' => $entity !== 'phone' ? NULL : ($isRelationshipField ? $fieldMapping[3] : $fieldMapping[2]),
2119 'im_provider_id' => $entity !== 'im' ? NULL : ($isRelationshipField ? $fieldMapping[3] : $fieldMapping[2]),
2120 'location_type_id' => $locationTypeID,
2121 ];
2122 }
2123
2124 /**
2125 * @param array $mappedField
2126 * Field detail as would be saved in field_mapping table
2127 * or as returned from getMappingFieldFromMapperInput
2128 *
2129 * @return string
2130 * @throws \API_Exception
2131 */
2132 public function getMappedFieldLabel(array $mappedField): string {
2133 $this->setFieldMetadata();
2134 $title = [];
2135 if ($mappedField['relationship_type_id']) {
2136 $title[] = $this->getRelationshipLabel($mappedField['relationship_type_id'], $mappedField['relationship_direction']);
2137 }
2138 $title[] = $this->getFieldMetadata($mappedField['name'])['title'];
2139 if ($mappedField['location_type_id']) {
2140 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Address', 'location_type_id', $mappedField['location_type_id']);
2141 }
2142 if ($mappedField['website_type_id']) {
2143 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Website', 'website_type_id', $mappedField['website_type_id']);
2144 }
2145 if ($mappedField['phone_type_id']) {
2146 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $mappedField['phone_type_id']);
2147 }
2148 if ($mappedField['im_provider_id']) {
2149 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $mappedField['im_provider_id']);
2150 }
2151 return implode(' - ', $title);
2152 }
2153
2154 /**
2155 * Get the relevant label for the relationship.
2156 *
2157 * @param int $id
2158 * @param string $direction
2159 *
2160 * @return string
2161 * @throws \API_Exception
2162 */
2163 protected function getRelationshipLabel(int $id, string $direction): string {
2164 if (empty($this->relationshipLabels[$id . $direction])) {
2165 $this->relationshipLabels[$id . $direction] =
2166 $fieldName = 'label_' . $direction;
2167 $this->relationshipLabels[$id . $direction] = (string) RelationshipType::get(FALSE)
2168 ->addWhere('id', '=', $id)
2169 ->addSelect($fieldName)->execute()->first()[$fieldName];
2170 }
2171 return $this->relationshipLabels[$id . $direction];
2172 }
2173
2174 /**
2175 * Transform the input parameters into the form handled by the input routine.
2176 *
2177 * @param array $values
2178 * Input parameters as they come in from the datasource
2179 * eg. ['Bob', 'Smith', 'bob@example.org', '123-456']
2180 *
2181 * @return array
2182 * Parameters mapped to CiviCRM fields based on the mapping
2183 * and specified contact type. eg.
2184 * [
2185 * 'contact_type' => 'Individual',
2186 * 'first_name' => 'Bob',
2187 * 'last_name' => 'Smith',
2188 * 'phone' => ['phone' => '123', 'location_type_id' => 1, 'phone_type_id' => 1],
2189 * '5_a_b' => ['contact_type' => 'Organization', 'url' => ['url' => 'https://example.org', 'website_type_id' => 1]]
2190 * 'im' => ['im' => 'my-handle', 'location_type_id' => 1, 'provider_id' => 1],
2191 *
2192 * @throws \API_Exception
2193 */
2194 public function getMappedRow(array $values): array {
2195 $params = $this->getParams($values);
2196 $params['contact_type'] = $this->getContactType();
2197 if ($this->getContactSubType()) {
2198 $params['contact_sub_type'] = $this->getContactSubType();
2199 }
2200 return $params;
2201 }
2202
2203 /**
2204 * Validate the import values.
2205 *
2206 * The values array represents a row in the datasource.
2207 *
2208 * @param array $values
2209 *
2210 * @throws \API_Exception
2211 * @throws \CRM_Core_Exception
2212 */
2213 public function validateValues(array $values): void {
2214 $params = $this->getMappedRow($values);
2215 $contacts = array_merge(['0' => $params], $this->getRelatedContactsParams($params));
2216 $errors = [];
2217 foreach ($contacts as $value) {
2218 // If we are referencing a related contact, or are in update mode then we
2219 // don't need all the required fields if we have enough to find an existing contact.
2220 $useExistingMatchFields = !empty($value['relationship_type_id']) || $this->isUpdateExistingContacts();
2221 $prefixString = !empty($value['relationship_label']) ? '(' . $value['relationship_label'] . ') ' : '';
2222 $this->validateRequiredContactFields($value['contact_type'], $value, $useExistingMatchFields, $prefixString);
2223
2224 $errors = array_merge($errors, $this->getInvalidValuesForContact($value, $prefixString));
2225 if (!empty($value['relationship_type_id'])) {
2226 $requiredSubType = $this->getRelatedContactSubType($value['relationship_type_id'], $value['relationship_direction']);
2227 if ($requiredSubType && $value['contact_sub_type'] && $requiredSubType !== $value['contact_sub_type']) {
2228 throw new CRM_Core_Exception($prefixString . ts('Mismatched or Invalid contact subtype found for this related contact.'));
2229 }
2230 }
2231 }
2232
2233 //check for duplicate external Identifier
2234 $externalID = $params['external_identifier'] ?? NULL;
2235 if ($externalID) {
2236 /* If it's a dupe,external Identifier */
2237
2238 if ($externalDupe = CRM_Utils_Array::value($externalID, $this->_allExternalIdentifiers)) {
2239 $errorMessage = ts('External ID conflicts with record %1', [1 => $externalDupe]);
2240 throw new CRM_Core_Exception($errorMessage);
2241 }
2242 //otherwise, count it and move on
2243 $this->_allExternalIdentifiers[$externalID] = $this->_lineCount;
2244 }
2245
2246 //date-format part ends
2247
2248 $errorMessage = implode(', ', $errors);
2249
2250 //checking error in core data
2251 $this->isErrorInCoreData($params, $errorMessage);
2252 if ($errorMessage) {
2253 $tempMsg = "Invalid value for field(s) : $errorMessage";
2254 throw new CRM_Core_Exception($tempMsg);
2255 }
2256 }
2257
2258 /**
2259 * Get the invalid values in the params for the given contact.
2260 *
2261 * @param array|int|string $value
2262 * @param string $prefixString
2263 *
2264 * @return array
2265 * @throws \API_Exception
2266 * @throws \Civi\API\Exception\NotImplementedException
2267 */
2268 protected function getInvalidValuesForContact($value, string $prefixString): array {
2269 $errors = [];
2270 foreach ($value as $contactKey => $contactValue) {
2271 if (!preg_match('/^\d+_[a|b]_[a|b]$/', $contactKey)) {
2272 $result = $this->getInvalidValues($contactValue, $contactKey, $prefixString);
2273 if (!empty($result)) {
2274 $errors = array_merge($errors, $result);
2275 }
2276 }
2277 }
2278 return $errors;
2279 }
2280
2281 /**
2282 * Get the field mappings for the import.
2283 *
2284 * This is the same format as saved in civicrm_mapping_field except
2285 * that location_type_id = 'Primary' rather than empty where relevant.
2286 * Also 'im_provider_id' is mapped to the 'real' field name 'provider_id'
2287 *
2288 * @return array
2289 * @throws \API_Exception
2290 */
2291 protected function getFieldMappings(): array {
2292 $mappedFields = [];
2293 foreach ($this->getSubmittedValue('mapper') as $i => $mapperRow) {
2294 $mappedField = $this->getMappingFieldFromMapperInput($mapperRow, 0, $i);
2295 if (!$mappedField['location_type_id'] && !empty($this->importableFieldsMetadata[$mappedField['name']]['hasLocationType'])) {
2296 $mappedField['location_type_id'] = 'Primary';
2297 }
2298 // Just for clarity since 0 is a pseudo-value
2299 unset($mappedField['mapping_id']);
2300 // Annoyingly the civicrm_mapping_field name for this differs from civicrm_im.
2301 // Test cover in `CRM_Contact_Import_Parser_ContactTest::testMapFields`
2302 $mappedField['provider_id'] = $mappedField['im_provider_id'];
2303 unset($mappedField['im_provider_id']);
2304 $mappedFields[] = $mappedField;
2305 }
2306 return $mappedFields;
2307 }
2308
2309 /**
2310 * Get the related contact type.
2311 *
2312 * @param int|null $relationshipTypeID
2313 * @param int|string $relationshipDirection
2314 *
2315 * @return null|string
2316 *
2317 * @throws \API_Exception
2318 */
2319 protected function getRelatedContactType($relationshipTypeID, $relationshipDirection): ?string {
2320 if (!$relationshipTypeID) {
2321 return NULL;
2322 }
2323 $relationshipField = 'contact_type_' . substr($relationshipDirection, -1);
2324 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2325 }
2326
2327 /**
2328 * Get the related contact sub type.
2329 *
2330 * @param int|null $relationshipTypeID
2331 * @param int|string $relationshipDirection
2332 *
2333 * @return null|string
2334 *
2335 * @throws \API_Exception
2336 */
2337 protected function getRelatedContactSubType(int $relationshipTypeID, $relationshipDirection): ?string {
2338 if (!$relationshipTypeID) {
2339 return NULL;
2340 }
2341 $relationshipField = 'contact_sub_type_' . substr($relationshipDirection, -1);
2342 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2343 }
2344
2345 /**
2346 * Get the related contact type.
2347 *
2348 * @param int|null $relationshipTypeID
2349 * @param int|string $relationshipDirection
2350 *
2351 * @return null|string
2352 *
2353 * @throws \API_Exception
2354 */
2355 protected function getRelatedContactLabel($relationshipTypeID, $relationshipDirection): ?string {
2356 $relationshipField = 'label_' . $relationshipDirection;
2357 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2358 }
2359
2360 /**
2361 * Get the relationship type.
2362 *
2363 * @param int $relationshipTypeID
2364 *
2365 * @return string[]
2366 * @throws \API_Exception
2367 */
2368 protected function getRelationshipType(int $relationshipTypeID): array {
2369 $cacheKey = 'relationship_type' . $relationshipTypeID;
2370 if (!isset(Civi::$statics[__CLASS__][$cacheKey])) {
2371 Civi::$statics[__CLASS__][$cacheKey] = RelationshipType::get(FALSE)
2372 ->addWhere('id', '=', $relationshipTypeID)
2373 ->addSelect('*')->execute()->first();
2374 }
2375 return Civi::$statics[__CLASS__][$cacheKey];
2376 }
2377
2378 /**
2379 * Add the given field to the contact array.
2380 *
2381 * @param array $contactArray
2382 * @param array $locationValues
2383 * @param string $fieldName
2384 * @param mixed $importedValue
2385 *
2386 * @return void
2387 * @throws \API_Exception
2388 */
2389 private function addFieldToParams(array &$contactArray, array $locationValues, string $fieldName, $importedValue): void {
2390 if (!empty($locationValues)) {
2391 $fieldMap = ['country' => 'country_id', 'state_province' => 'state_province_id', 'county' => 'county_id'];
2392 $realFieldName = empty($fieldMap[$fieldName]) ? $fieldName : $fieldMap[$fieldName];
2393 $entity = strtolower($this->getFieldEntity($fieldName));
2394
2395 // The entity key is either location_type_id for address, email - eg. 1, or
2396 // location_type_id + '_' + phone_type_id or im_provider_id
2397 // or the value for website(since websites are not historically one-per-type)
2398 $entityKey = $locationValues['location_type_id'] ?? $importedValue;
2399 if (!empty($locationValues['phone_type_id']) || !empty($locationValues['provider_id'])) {
2400 $entityKey .= '_' . ($locationValues['phone_type_id'] ?? '' . $locationValues['provider_id'] ?? '');
2401 }
2402 $fieldValue = $this->getTransformedFieldValue($realFieldName, $importedValue);
2403
2404 if (!isset($contactArray[$entity][$entityKey])) {
2405 $contactArray[$entity][$entityKey] = $locationValues;
2406 }
2407 // So im has really non-standard handling...
2408 $reallyRealFieldName = $realFieldName === 'im' ? 'name' : $realFieldName;
2409 $contactArray[$entity][$entityKey][$reallyRealFieldName] = $fieldValue;
2410 }
2411 else {
2412 $fieldName = array_search($fieldName, $this->getOddlyMappedMetadataFields(), TRUE) ?: $fieldName;
2413 $contactArray[$fieldName] = $this->getTransformedFieldValue($fieldName, $importedValue);
2414 }
2415 }
2416
2417 /**
2418 * Get any related contacts designated for update.
2419 *
2420 * This extracts the parts that relate to separate related
2421 * contacts from the 'params' array.
2422 *
2423 * It is probably a bit silly not to nest them more clearly in
2424 * `getParams` in the first place & maybe in future we can do that.
2425 *
2426 * @param array $params
2427 *
2428 * @return array
2429 * e.g ['5_a_b' => ['contact_type' => 'Organization', 'organization_name' => 'The Firm']]
2430 * @throws \API_Exception
2431 */
2432 protected function getRelatedContactsParams(array $params): array {
2433 $relatedContacts = [];
2434 foreach ($params as $key => $value) {
2435 // If the key is a relationship key - eg. 5_a_b or 10_b_a
2436 // then the value is an array that describes an existing contact.
2437 // We need to check the fields are present to identify or create this
2438 // contact.
2439 if (preg_match('/^\d+_[a|b]_[a|b]$/', $key)) {
2440 $value['relationship_type_id'] = substr($key, 0, -4);
2441 $value['relationship_direction'] = substr($key, -3);
2442 $value['relationship_label'] = $this->getRelationshipLabel($value['relationship_type_id'], $value['relationship_direction']);
2443 $relatedContacts[$key] = $value;
2444 }
2445 }
2446 return $relatedContacts;
2447 }
2448
2449 /**
2450 * Look up for an existing contact with the given external_identifier.
2451 *
2452 * If the identifier is found on a deleted contact then it is not a match
2453 * but it must be removed from that contact to allow the new contact to
2454 * have that external_identifier.
2455 *
2456 * @param string|null $externalIdentifier
2457 * @param string $contactType
2458 *
2459 * @return int|null
2460 *
2461 * @throws \CRM_Core_Exception
2462 * @throws \CiviCRM_API3_Exception
2463 */
2464 protected function lookupExternalIdentifier(?string $externalIdentifier, string $contactType): ?int {
2465 if (!$externalIdentifier) {
2466 return NULL;
2467 }
2468 // Check for any match on external id, deleted or otherwise.
2469 $foundContact = civicrm_api3('Contact', 'get', [
2470 'external_identifier' => $externalIdentifier,
2471 'showAll' => 'all',
2472 'sequential' => TRUE,
2473 'return' => ['id', 'contact_is_deleted', 'contact_type'],
2474 ]);
2475 if (empty($foundContact['id'])) {
2476 return NULL;
2477 }
2478 if (!empty($foundContact['values'][0]['contact_is_deleted'])) {
2479 // If the contact is deleted, update external identifier to be blank
2480 // to avoid key error from MySQL.
2481 $params = ['id' => $foundContact['id'], 'external_identifier' => ''];
2482 civicrm_api3('Contact', 'create', $params);
2483 return NULL;
2484 }
2485 if ($foundContact['values'][0]['contact_type'] !== $contactType) {
2486 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser::NO_MATCH);
2487 }
2488 return (int) $foundContact['id'];
2489 }
2490
2491 /**
2492 * Lookup the contact's contact ID.
2493 *
2494 * @param array $params
2495 * @param bool $isDuplicateIfExternalIdentifierExists
2496 *
2497 * @return int|null
2498 *
2499 * @throws \API_Exception
2500 * @throws \CRM_Core_Exception
2501 * @throws \CiviCRM_API3_Exception
2502 */
2503 protected function lookupContactID(array $params, bool $isDuplicateIfExternalIdentifierExists): ?int {
2504 $extIDMatch = $this->lookupExternalIdentifier($params['external_identifier'] ?? NULL, $params['contact_type']);
2505 if (!empty($params['external_identifier']) && !$extIDMatch && $isDuplicateIfExternalIdentifierExists) {
2506 throw new CRM_Core_Exception(ts('Existing external ID lookup failed.'), CRM_Import_Parser::ERROR);
2507 }
2508 $contactID = !empty($params['id']) ? (int) $params['id'] : NULL;
2509 //check if external identifier exists in database
2510 if ($extIDMatch && $contactID && $extIDMatch !== $contactID) {
2511 throw new CRM_Core_Exception(ts('Existing external ID does not match the imported contact ID.'), CRM_Import_Parser::ERROR);
2512 }
2513 if ($extIDMatch && $isDuplicateIfExternalIdentifierExists) {
2514 throw new CRM_Core_Exception(ts('External ID already exists in Database.'), CRM_Import_Parser::DUPLICATE);
2515 }
2516 if ($contactID) {
2517 $existingContact = Contact::get(FALSE)
2518 ->addWhere('id', '=', $contactID)
2519 // Don't auto-filter deleted - people use import to undelete.
2520 ->addWhere('is_deleted', 'IN', [0, 1])
2521 ->addSelect('contact_type')->execute()->first();
2522 if (empty($existingContact['id'])) {
2523 throw new CRM_Core_Exception('No contact found for this contact ID:' . $params['id'], CRM_Import_Parser::NO_MATCH);
2524 }
2525 if ($existingContact['contact_type'] !== $params['contact_type']) {
2526 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser::NO_MATCH);
2527 }
2528 return $contactID;
2529 }
2530 // Time to see if we can find an existing contact ID to make this an update
2531 // not a create.
2532 if ($extIDMatch || $this->isUpdateExistingContacts()) {
2533 return $this->getPossibleContactMatch($params, $extIDMatch, $this->getSubmittedValue('dedupe_rule_id') ?: NULL);
2534 }
2535 return NULL;
2536 }
2537
2538 /**
2539 * @param array $params
2540 * @param array $formatted
2541 * @return array[]
2542 * @throws \API_Exception
2543 * @throws \CRM_Core_Exception
2544 * @throws \CiviCRM_API3_Exception
2545 * @throws \Civi\API\Exception\UnauthorizedException
2546 */
2547 protected function processContact(array $params, array $formatted): array {
2548 $params['id'] = $formatted['id'] = $this->lookupContactID($params, ($this->isSkipDuplicates() || $this->isIgnoreDuplicates()));
2549 if ($params['id'] && $params['contact_sub_type']) {
2550 $contactSubType = Contact::get(FALSE)
2551 ->addWhere('id', '=', $params['id'])
2552 ->addSelect('contact_sub_type')
2553 ->execute()
2554 ->first()['contact_sub_type'];
2555 if (!empty($contactSubType) && $contactSubType[0] !== $params['contact_sub_type'] && !CRM_Contact_BAO_ContactType::isAllowEdit($params['id'], $contactSubType[0])) {
2556 throw new CRM_Core_Exception('Mismatched contact SubTypes :', CRM_Import_Parser::NO_MATCH);
2557 }
2558 }
2559 return array($formatted, $params);
2560 }
2561
2562 /**
2563 * Try to get the correct state province using what country information we have.
2564 *
2565 * If the state matches more than one possibility then either the imported
2566 * country of the site country should help us....
2567 *
2568 * @param string $stateProvince
2569 * @param int|null|string $countryID
2570 *
2571 * @return int|string
2572 * @throws \API_Exception
2573 * @throws \Civi\API\Exception\UnauthorizedException
2574 */
2575 private function tryToResolveStateProvince(string $stateProvince, $countryID) {
2576 // Try to disambiguate since we likely have the country now.
2577 $possibleStates = $this->ambiguousOptions['state_province_id'][mb_strtolower($stateProvince)];
2578 if ($countryID) {
2579 return $this->checkStatesForCountry($countryID, $possibleStates) ?: 'invalid_import_value';
2580 }
2581 // Try the default country next.
2582 $defaultCountryMatch = $this->checkStatesForCountry($this->getSiteDefaultCountry(), $possibleStates);
2583 if ($defaultCountryMatch) {
2584 return $defaultCountryMatch;
2585 }
2586
2587 if ($this->getAvailableCountries()) {
2588 $countryMatches = [];
2589 foreach ($this->getAvailableCountries() as $availableCountryID) {
2590 $possible = $this->checkStatesForCountry($availableCountryID, $possibleStates);
2591 if ($possible) {
2592 $countryMatches[] = $possible;
2593 }
2594 }
2595 if (count($countryMatches) === 1) {
2596 return reset($countryMatches);
2597 }
2598
2599 }
2600 return $stateProvince;
2601 }
2602
2603 /**
2604 * @param array $params
2605 *
2606 * @return array
2607 * @throws \API_Exception
2608 */
2609 private function fillStateProvince(array &$params): array {
2610 foreach ($params as $key => $value) {
2611 if ($key === 'address') {
2612 foreach ($value as $index => $address) {
2613 $stateProvinceID = $address['state_province_id'] ?? NULL;
2614 if ($stateProvinceID) {
2615 if (!is_numeric($stateProvinceID)) {
2616 $params['address'][$index]['state_province_id'] = $this->tryToResolveStateProvince($stateProvinceID, $address['country_id'] ?? NULL);
2617 }
2618 elseif (!empty($address['country_id']) && is_numeric($address['country_id'])) {
2619 if (!$this->checkStatesForCountry((int) $address['country_id'], [$stateProvinceID])) {
2620 $params['address'][$index]['state_province_id'] = 'invalid_import_value';
2621 }
2622 }
2623 }
2624 }
2625 }
2626 elseif (is_array($value) && !in_array($key, ['email', 'phone', 'im', 'website', 'openid'], TRUE)) {
2627 $this->fillStateProvince($params[$key]);
2628 }
2629 }
2630 return $params;
2631 }
2632
2633 /**
2634 * Check is any of the given states correlate to the country.
2635 *
2636 * @param int $countryID
2637 * @param array $possibleStates
2638 *
2639 * @return int|null
2640 * @throws \API_Exception
2641 */
2642 private function checkStatesForCountry(int $countryID, array $possibleStates) {
2643 foreach ($possibleStates as $index => $state) {
2644 if (!empty($this->statesByCountry[$state])) {
2645 if ($this->statesByCountry[$state] === $countryID) {
2646 return $state;
2647 }
2648 unset($possibleStates[$index]);
2649 }
2650 }
2651 if (!empty($possibleStates)) {
2652 $states = StateProvince::get(FALSE)
2653 ->addSelect('country_id')
2654 ->addWhere('id', 'IN', $possibleStates)
2655 ->execute()
2656 ->indexBy('country_id');
2657 foreach ($states as $state) {
2658 $this->statesByCountry[$state['id']] = $state['country_id'];
2659 }
2660 foreach ($possibleStates as $state) {
2661 if ($this->statesByCountry[$state] === $countryID) {
2662 return $state;
2663 }
2664 }
2665 }
2666 return FALSE;
2667 }
2668
2669 }