3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
12 use Civi\Api4\Contact
;
13 use Civi\Api4\RelationshipType
;
14 use Civi\Api4\StateProvince
;
16 require_once 'api/v3/utils.php';
21 * @copyright CiviCRM LLC https://civicrm.org/licensing
25 * class to parse contact csv files
27 class CRM_Contact_Import_Parser_Contact
extends CRM_Import_Parser
{
29 use CRM_Contact_Import_MetadataTrait
;
31 protected $_mapperKeys = [];
34 * Is update only permitted on an id match.
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.
42 protected $_updateWithId;
45 protected $_externalIdentifierIndex;
46 protected $_allExternalIdentifiers = [];
47 protected $_parseStreetAddress;
50 * Array of successfully imported contact id's
54 protected $_newContacts;
61 protected $_lineCount;
64 * Array of successfully imported related contact id's
68 protected $_newRelatedContacts;
70 protected $_tableName;
73 * Total number of lines in file
79 protected $_primaryKeyName;
80 protected $_statusFieldName;
82 protected $fieldMetadata = [];
85 * Fields which are being handled by metadata formatting & validation functions.
87 * This is intended as a temporary parameter as we phase in metadata handling.
89 * The end result is that all fields will be & this will go but for now it is
94 protected $metadataHandledFields = [
103 'communication_style',
104 'preferred_language',
105 'preferred_communication_method',
115 'postal_greeting_id',
123 * Relationship labels.
125 * Temporary cache of labels to reduce queries in getRelationshipLabels.
128 * e.g ['5a_b' => 'Employer', '5b_a' => 'Employee']
130 protected $relationshipLabels = [];
137 public $_onDuplicate;
140 * Dedupe rule group id to use if set
144 public $_dedupeRuleGroupID = NULL;
149 * @param array $mapperKeys
151 public function __construct($mapperKeys = []) {
152 parent
::__construct();
153 $this->_mapperKeys
= $mapperKeys;
157 * The initializer code, called before processing.
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));
164 $this->_newContacts
= [];
166 $this->setActiveFields($this->_mapperKeys
);
168 $this->_externalIdentifierIndex
= -1;
171 foreach ($this->_mapperKeys
as $key) {
172 if ($key == 'external_identifier') {
173 $this->_externalIdentifierIndex
= $index;
178 $this->_updateWithId
= FALSE;
179 if (in_array('id', $this->_mapperKeys
) ||
($this->_externalIdentifierIndex
>= 0 && $this->isUpdateExistingContacts())) {
180 $this->_updateWithId
= TRUE;
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);
187 * Is this a case where the user has opted to update existing contacts.
191 * @throws \API_Exception
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
,
201 * Did the user specify duplicates checking should be skipped, resulting in possible duplicate contacts.
203 * Note we still need to check for external_identifier as it will hard-fail
208 * @throws \API_Exception
210 private function isIgnoreDuplicates(): bool {
211 return ((int) $this->getSubmittedValue('onDuplicate')) === CRM_Import_Parser
::DUPLICATE_NOCHECK
;
215 * Handle the values in preview mode.
217 * Function will be deprecated in favour of validateValues.
219 * @param array $values
220 * The array of values belonging to this line.
223 * the result of this processing
224 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
226 public function preview(&$values) {
227 return $this->summary($values);
231 * Handle the values in summary mode.
233 * Function will be deprecated in favour of validateValues.
235 * @param array $values
236 * The array of values belonging to this line.
239 * the result of this processing
240 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
242 public function summary(&$values): int {
243 $rowNumber = (int) ($values[array_key_last($values)]);
245 $this->validateValues($values);
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
;
252 $this->setImportStatus($rowNumber, 'NEW', '');
254 return CRM_Import_Parser
::VALID
;
258 * Get Array of all the fields that could potentially be part
263 public function getAllFields() {
264 return $this->_fields
;
268 * Handle the values in import mode.
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.
276 * the result of this processing
278 * @throws \CiviCRM_API3_Exception
279 * @throws \CRM_Core_Exception
280 * @throws \API_Exception
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();
290 // first make sure this is a valid line
291 //$this->_updateWithId = false;
292 $response = $this->summary($values);
294 if ($response != CRM_Import_Parser
::VALID
) {
295 $this->setImportStatus((int) $values[count($values) - 1], 'Invalid', "Invalid (Error Code: $response)");
299 $params = $this->getMappedRow($values);
301 foreach ($params as $key => $value) {
303 $formatted[$key] = $value;
307 $contactFields = CRM_Contact_DAO_Contact
::import();
309 $params['contact_sub_type'] = $this->getContactSubType() ?
: ($params['contact_sub_type'] ??
NULL);
312 [$formatted, $params] = $this->processContact($params, $formatted);
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());
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.
323 //format common data, CRM-4062
324 $this->formatCommonData($params, $formatted, $contactFields);
327 //now we create new contact in update/fill mode also.
329 //CRM-4430, don't carry if not submitted.
330 if ($this->_updateWithId
&& !empty($params['id'])) {
331 $contactID = $params['id'];
333 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID
);
335 if (is_object($newContact) && ($newContact instanceof CRM_Contact_BAO_Contact
)) {
336 $newContact = clone($newContact);
337 $contactID = $newContact->id
;
338 $this->_newContacts
[] = $contactID;
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
;
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');
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);
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;
370 $currentImportID = end($values);
373 'contactID' => $contactID,
374 'importID' => $currentImportID,
375 'importTempTable' => $this->_tableName
,
376 'fieldHeaders' => $this->_mapperKeys
,
377 'fields' => $this->_activeFields
,
380 CRM_Utils_Hook
::import('Contact', 'process', $this, $hookParams);
383 $primaryContactId = NULL;
384 if (is_array($newContact)) {
385 if ($dupeCount == 1 && CRM_Utils_Rule
::integer($contactID)) {
386 $primaryContactId = $contactID;
390 $primaryContactId = $newContact->id
;
393 if ((is_array($newContact) ||
is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) {
395 //relationship contact insert
396 foreach ($this->getRelatedContactsParams($params) as $key => $field) {
397 $formatting = $field;
399 [$formatting, $field] = $this->processContact($field, $formatting);
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());
407 $contactFields = CRM_Contact_DAO_Contact
::import();
409 //format common data, CRM-4062
410 $this->formatCommonData($field, $formatting, $contactFields);
413 if (!empty($params[$key]['id'])) {
415 'contact_id' => $params[$key]['id'],
418 $relatedNewContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
421 $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE);
424 if (is_object($relatedNewContact) ||
($relatedNewContact instanceof CRM_Contact_BAO_Contact
)) {
425 $relatedNewContact = clone($relatedNewContact);
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);
438 $matchedIDs[] = $relatedNewContact->id
;
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
,
445 //validation of related contact subtype for update mode
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');
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.');
457 $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]);
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);
469 if (empty($reContact)) {
470 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
474 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
479 $relContactId = $relatedNewContact->id
;
480 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
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
490 'relationship_type_id' => $key,
495 'skipRecentView' => TRUE,
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
501 'contact' => $primaryContactId,
504 [$valid, $duplicate] = self
::legacyCreateMultiple($relationParams, $relationIds);
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);
512 //handle current employer, CRM-3532
514 $allRelationships = CRM_Core_PseudoConstant
::relationshipType('name');
515 $relationshipTypeId = str_replace([
522 $relationshipType = str_replace($relationshipTypeId . '_', '', $key);
523 $orgId = $individualId = NULL;
524 if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') {
525 $orgId = $relContactId;
526 $individualId = $primaryContactId;
528 elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') {
529 $orgId = $primaryContactId;
530 $individualId = $relContactId;
532 if ($orgId && $individualId) {
533 $currentEmpParams[$individualId] = $orgId;
534 CRM_Contact_BAO_Contact_Utils
::setCurrentEmployer($currentEmpParams);
541 if ($this->_updateWithId
) {
542 //return warning if street address is unparsed, CRM-5886
543 return $this->processMessage($values, $this->_retCode
);
546 if (is_array($newContact)) {
547 return $this->handleDuplicateError($newContact, $values, $onDuplicate, $formatted, $contactFields);
550 if (empty($this->_unparsedStreetAddressContacts
)) {
551 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '', $contactID);
552 return CRM_Import_Parser
::VALID
;
555 // @todo - record unparsed address as 'imported' but the presence of a message is meaningful?
556 return $this->processMessage($values, CRM_Import_Parser
::VALID
);
560 * Only called from import now... plus one place outside of core & tests.
562 * @todo - deprecate more aggressively - will involve copying to the import
563 * class, adding a deprecation notice here & removing from tests.
565 * Takes an associative array and creates a relationship object.
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
571 * @param array $params
572 * (reference ) an assoc array of name/value pairs.
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 "
579 * @throws \CRM_Core_Exception
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'];
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']);
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
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'])) {
610 CRM_Contact_BAO_Relationship
::checkDuplicateRelationship(
620 $singleInstanceParams = array_merge($params, $contactFields);
621 CRM_Contact_BAO_Relationship
::add($singleInstanceParams);
626 * Format common params data to proper format to store.
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.
635 private function formatCommonData($params, &$formatted, $contactFields) {
636 $customFields = CRM_Core_BAO_CustomField
::getFields($formatted['contact_type'], FALSE, FALSE, $formatted['contact_sub_type'] ??
NULL);
638 $addressCustomFields = CRM_Core_BAO_CustomField
::getFields('Address');
639 $customFields = $customFields +
$addressCustomFields;
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)
649 //we should not update Date to null, CRM-4062
650 if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) {
652 CRM_Contact_Import_Parser_Contact
::formatCustomDate($params, $formatted, $dateType, $key);
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]);
660 $params[$key] = CRM_Utils_String
::strtoboolstr($val);
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);
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.
679 if (is_array($field)) {
680 $isAddressCustomField = FALSE;
682 foreach ($field as $value) {
684 if (is_array($value)) {
685 foreach ($value as $name => $testForEmpty) {
686 if ($addressCustomFieldID = CRM_Core_BAO_CustomField
::getKeyID($name)) {
687 $isAddressCustomField = TRUE;
691 if (($testForEmpty === '' ||
$testForEmpty == NULL)) {
702 if (!empty($value['location_type_id'])) {
703 $this->formatLocationBlock($value, $formatted);
707 if (!$isAddressCustomField) {
716 if ($key == 'id' && isset($field)) {
717 $formatted[$key] = $field;
719 $this->formatContactParameters($formatValues, $formatted);
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)
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]);
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;
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] = [];
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)))
754 if ($htmlType == 'CheckBox') {
755 $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1;
758 $params[$key][] = $formatted[$key][] = $v2['value'];
767 if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) && array_key_exists($customFieldID, $customFields) &&
768 !array_key_exists($customFieldID, $addressCustomFields)
770 // @todo calling api functions directly is not supported
771 _civicrm_api3_custom_format_params($params, $formatted, $extends);
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'];
780 $emptyValue = $customvalue[-1]['value'] ??
NULL;
781 if (!isset($emptyValue)) {
782 unset($formatted['custom'][$customKey]);
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)) {
795 // parse address field.
796 $parsedFields = CRM_Core_BAO_Address
::parseStreetAddress($streetAddress);
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), '');
804 // merge parse address w/ main address block.
805 $address = array_merge($address, $parsedFields);
812 * Get the array of successfully imported contact id's
816 public function getImportedContacts() {
817 return $this->_newContacts
;
821 * Get the array of successfully imported related contact id's
825 public function &getRelatedImportedContacts() {
826 return $this->_newRelatedContacts
;
830 * Check if an error in custom data.
832 * @param array $params
833 * @param string $errorMessage
834 * A string containing all the error-fields.
836 * @param null $csType
838 public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL) {
839 $dateType = CRM_Core_Session
::singleton()->get("dateTypes");
842 if (!empty($params['contact_sub_type'])) {
843 $csType = $params['contact_sub_type'] ??
NULL;
846 if (empty($params['contact_type'])) {
847 $params['contact_type'] = 'Individual';
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']);
855 if (is_array($csType)) {
856 // fetch custom fields for every subtype and add it to $customFields array
859 foreach ($csType as $cType) {
860 $customFields +
= CRM_Core_BAO_CustomField
::getFields($params['contact_type'], FALSE, FALSE, $cType);
864 $customFields = CRM_Core_BAO_CustomField
::getFields($params['contact_type'], FALSE, FALSE, $csType);
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');
876 /* check if it's a valid custom field id */
877 $errors[] = $parser->validateCustomField($customFieldID, $value, $customFields[$customFieldID], $dateType);
879 elseif (is_array($params[$key]) && isset($params[$key]["contact_type"]) && in_array(substr($key, -3), ['a_b', 'b_a'], TRUE)) {
881 //supporting custom data of related contact subtypes
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;
895 self
::isErrorInCustomData($params[$key], $errorMessage, $csType);
899 $errorMessage .= ($errorMessage ?
'; ' : '') . implode('; ', array_filter($errors));
904 * Check if an error in Core( non-custom fields ) field
906 * @param array $params
907 * @param string $errorMessage
908 * A string containing all the error-fields.
910 public function isErrorInCoreData($params, &$errorMessage) {
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.');
916 foreach ($params as $key => $value) {
925 if (CRM_Utils_Rule
::boolean($value) == FALSE) {
926 $key = ucwords(str_replace("_", " ", $key));
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);
940 $errorMessage .= ($errorMessage ?
'; ' : '') . implode('; ', $errors);
945 * Ckeck a value present or not in a array.
952 public static function in_value($value, $valueArray) {
953 foreach ($valueArray as $key => $v) {
955 if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) {
963 * Build error-message containing error-fields
965 * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function.
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.
975 public static function addToErrorMsg($errorName, &$errorMessage) {
977 $errorMessage .= "; $errorName";
980 $errorMessage = $errorName;
985 * Method for creating contact.
987 * @param array $formatted
988 * @param array $contactFields
989 * @param int $onDuplicate
990 * @param int $contactId
991 * @param bool $requiredCheck
992 * @param int $dedupeRuleGroupID
994 * @return array|\CRM_Contact_BAO_Contact
995 * If a duplicate is found an array is returned, otherwise CRM_Contact_BAO_Contact
997 public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) {
1001 if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser
::DUPLICATE_NOCHECK
)) {
1002 $dupeCheck = (bool) ($onDuplicate);
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);
1015 'error_message' => [
1016 'code' => CRM_Core_Error
::DUPLICATE_CONTACT
,
1019 'message' => 'Found matching contacts: ' . implode(',', $ids),
1026 $this->formatParams($formatted, $onDuplicate, (int) $contactId);
1029 // Resetting and rebuilding cache could be expensive.
1030 CRM_Core_Config
::setPermitCacheFlushMode(FALSE);
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;
1043 [$data, $contactDetails] = $this->formatProfileContactParams($formatted, $contactFields, $contactId, $formatted['contact_type']);
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'])) {
1053 'contact_id' => $contactDetails['contact_id'],
1054 'status' => $isOptOut ?
'Removed' : 'Added',
1057 CRM_Contact_BAO_SubscriptionHistory
::create($shParams);
1061 $contact = civicrm_api3('Contact', 'create', $data);
1062 $cid = $contact['id'];
1064 CRM_Core_Config
::setPermitCacheFlushMode(TRUE);
1067 'contact_id' => $cid,
1071 $newContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
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'],
1088 * Legacy format profile contact parameters.
1090 * This is a formerly shared function - most of the stuff in it probably does
1091 * nothing but copied here to star unravelling that...
1093 * @param array $params
1094 * @param array $fields
1095 * @param int|null $contactID
1096 * @param string|null $ctype
1100 private function formatProfileContactParams(
1107 $data = $contactDetails = [];
1109 // get the contact details (hier)
1111 $details = CRM_Contact_BAO_Contact
::getHierContactDetails($contactID, $fields);
1113 $contactDetails = $details[$contactID];
1114 $data['contact_type'] = $contactDetails['contact_type'] ??
NULL;
1115 $data['contact_sub_type'] = $contactDetails['contact_sub_type'] ??
NULL;
1118 //we should get contact type only if contact
1120 $data['contact_type'] = $ctype;
1123 $data['contact_type'] = 'Individual';
1127 //fix contact sub type CRM-5125
1128 if (array_key_exists('contact_sub_type', $params) &&
1129 !empty($params['contact_sub_type'])
1131 $data['contact_sub_type'] = CRM_Utils_Array
::implodePadded($params['contact_sub_type']);
1133 elseif (array_key_exists('contact_sub_type_hidden', $params) &&
1134 !empty($params['contact_sub_type_hidden'])
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']);
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']);
1152 if ($ctype == 'Organization') {
1153 $data['organization_name'] = $contactDetails['organization_name'] ??
NULL;
1155 elseif ($ctype == 'Household') {
1156 $data['household_name'] = $contactDetails['household_name'] ??
NULL;
1164 $data['contact_id'] = $contactID;
1165 $primaryLocationType = CRM_Contact_BAO_Contact
::getPrimaryLocationType($contactID);
1168 $defaultLocation = CRM_Core_BAO_LocationType
::getDefault();
1169 $defaultLocationId = $defaultLocation->id
;
1172 $billingLocationTypeId = CRM_Core_BAO_LocationType
::getBilling();
1174 $multiplFields = ['url'];
1176 $session = CRM_Core_Session
::singleton();
1177 foreach ($params as $key => $value) {
1178 [$fieldName, $locTypeId, $typeId] = CRM_Utils_System
::explode('-', $key, 3);
1180 if ($locTypeId == 'Primary') {
1182 $locTypeId = CRM_Contact_BAO_Contact
::getPrimaryLocationType($contactID, FALSE, 'address');
1183 $primaryLocationType = $locTypeId;
1186 $locTypeId = $defaultLocationId;
1190 if (is_numeric($locTypeId) &&
1191 !in_array($fieldName, $multiplFields) &&
1192 substr($fieldName, 0, 7) != 'custom_'
1194 $index = $locTypeId;
1196 if (is_numeric($typeId)) {
1197 $index .= '-' . $typeId;
1199 if (!in_array($index, $locationType)) {
1200 $locationType[$count] = $index;
1204 $loc = CRM_Utils_Array
::key($index, $locationType);
1206 $blockName = strtolower($this->getFieldEntity($fieldName));
1208 $data[$blockName][$loc]['location_type_id'] = $locTypeId;
1210 //set is_billing true, for location type "Billing"
1211 if ($locTypeId == $billingLocationTypeId) {
1212 $data[$blockName][$loc]['is_billing'] = 1;
1216 //get the primary location type
1217 if ($locTypeId == $primaryLocationType) {
1218 $data[$blockName][$loc]['is_primary'] = 1;
1221 elseif ($locTypeId == $defaultLocationId) {
1222 $data[$blockName][$loc]['is_primary'] = 1;
1228 if ($fieldName === 'state_province') {
1230 if (is_numeric($value) && ((int ) $value) >= 1000) {
1231 $data['address'][$loc]['state_province_id'] = $value;
1233 elseif (empty($value)) {
1234 $data['address'][$loc]['state_province_id'] = '';
1237 $data['address'][$loc]['state_province'] = $value;
1240 elseif ($fieldName === 'country_id') {
1241 $data['address'][$loc]['country_id'] = $value;
1243 elseif ($fieldName === 'county') {
1244 $data['address'][$loc]['county_id'] = $value;
1246 elseif ($fieldName == 'address_name') {
1247 $data['address'][$loc]['name'] = $value;
1249 elseif (substr($fieldName, 0, 14) === 'address_custom') {
1250 $data['address'][$loc][substr($fieldName, 8)] = $value;
1253 $data[$blockName][$loc][$fieldName] = $value;
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'];
1264 // we need to append time with date
1265 if ($params[$key] && isset($params[$key . '_time'])) {
1266 $value .= ' ' . $params[$key . '_time'];
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))
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'];
1283 $type = $data['contact_type'];
1284 if (!empty($data['contact_sub_type'])) {
1285 $type = CRM_Utils_Array
::explodePadded($data['contact_sub_type']);
1289 CRM_Core_BAO_CustomField
::formatCustomField($customFieldId,
1299 elseif ($key === 'edit') {
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'];
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
1321 $data[$key] = $value;
1327 if (!isset($data['contact_type'])) {
1328 $data['contact_type'] = 'Individual';
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])) {
1342 elseif (!$contactID) {
1348 return [$data, $contactDetails];
1352 * Format params for update and fill mode.
1354 * @param array $params
1355 * reference to an array containing all the.
1357 * @param int $onDuplicate
1361 public function formatParams(&$params, $onDuplicate, $cid) {
1362 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
) {
1367 'contact_id' => $cid,
1371 $contactObj = CRM_Contact_BAO_Contact
::retrieve($contactParams, $defaults);
1373 $modeFill = ($onDuplicate == CRM_Import_Parser
::DUPLICATE_FILL
);
1375 $groupTree = CRM_Core_BAO_CustomGroup
::getTree($params['contact_type'], NULL, $cid, 0, NULL);
1376 CRM_Core_BAO_CustomGroup
::setDefaults($groupTree, $defaults, FALSE, FALSE);
1379 'address' => 'address',
1382 $contact = get_object_vars($contactObj);
1384 foreach ($params as $key => $value) {
1385 if ($key == 'id' ||
$key == 'contact_type') {
1389 if (array_key_exists($key, $locationFields)) {
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)) {
1401 $getValue = CRM_Utils_Array
::retrieveValueRecursive($contact, $key);
1403 if ($key == 'contact_source') {
1404 $params['source'] = $params[$key];
1405 unset($params[$key]);
1408 if ($modeFill && isset($getValue)) {
1409 unset($params[$key]);
1410 if ($customFieldId) {
1411 // Extra values must be unset to ensure the values are not
1413 unset($params['custom'][$customFieldId]);
1418 foreach ($locationFields as $locKeys) {
1419 if (isset($params[$locKeys]) && is_array($params[$locKeys])) {
1420 foreach ($params[$locKeys] as $key => $value) {
1422 $getValue = CRM_Utils_Array
::retrieveValueRecursive($contact, $locKeys);
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]);
1433 if (count($params[$locKeys]) == 0) {
1434 unset($params[$locKeys]);
1441 * Convert any given date string to default date array.
1443 * @param array $params
1444 * Has given date-format.
1445 * @param array $formatted
1446 * Store formatted date in this array.
1447 * @param int $dateType
1449 * @param string $dateParam
1452 public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) {
1454 CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $dateParam);
1455 $formatted[$dateParam] = CRM_Utils_Date
::processDate($params[$dateParam]);
1459 * Generate status and error message for unparsed street address records.
1461 * @param array $values
1462 * The array of values belonging to each row.
1463 * @param $returnCode
1467 private function processMessage(&$values, $returnCode) {
1468 if (empty($this->_unparsedStreetAddressContacts
)) {
1469 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '');
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>";
1477 array_unshift($values, $errorMessage);
1478 $returnCode = CRM_Import_Parser
::UNPARSED_ADDRESS_WARNING
;
1479 $this->setImportStatus((int) ($values[count($values) - 1]), 'ERROR', $errorMessage);
1485 * get subtypes given the contact type
1487 * @param string $contactType
1488 * @return array $subTypes
1490 public static function getSubtypes($contactType) {
1492 $types = CRM_Contact_BAO_ContactType
::subTypeInfo($contactType);
1494 if (count($types) > 0) {
1495 foreach ($types as $type) {
1496 $subTypes[] = $type['name'];
1503 * Get the possible contact matches.
1505 * 1) the chosen dedupe rule falling back to
1506 * 2) a check for the external ID.
1508 * @see https://issues.civicrm.org/jira/browse/CRM-17275
1510 * @param array $params
1511 * @param int|null $extIDMatch
1512 * @param int|null $dedupeRuleID
1515 * IDs of a possible.
1517 * @throws \CRM_Core_Exception
1518 * @throws \CiviCRM_API3_Exception
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);
1524 // Historically we have used the last ID - it is not clear if this was
1526 return array_key_last($possibleMatches['values']);
1528 if ($possibleMatches['count']) {
1529 if (array_key_exists($extIDMatch, $possibleMatches['values'])) {
1532 throw new CRM_Core_Exception(ts(
1533 'Matching this contact based on the de-dupe rule would cause an external ID conflict'));
1539 * Format the form mapping parameters ready for the parser.
1544 * @return array $parserParameters
1546 public static function getParameterForParser($count) {
1548 for ($i = 0; $i < $count; $i++
) {
1549 $baseArray[$i] = NULL;
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;
1563 return $parserParameters;
1568 * Set field metadata.
1570 protected function setFieldMetadata() {
1571 $this->setImportableFieldsMetadata($this->getContactImportMetadata());
1575 * @param array $newContact
1576 * @param array $values
1577 * @param int $onDuplicate
1578 * @param array $formatted
1579 * @param array $contactFields
1583 * @throws \CRM_Core_Exception
1584 * @throws \CiviCRM_API3_Exception
1585 * @throws \Civi\API\Exception\UnauthorizedException
1587 private function handleDuplicateError(array $newContact, array $values, int $onDuplicate, array $formatted, array $contactFields): int {
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];
1595 $cids = explode(',', $newContact['error_message']['params'][0]);
1598 foreach ($cids as $cid) {
1599 $urls[] = CRM_Utils_System
::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE);
1602 $url_string = implode("\n", $urls);
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
;
1614 // Params only had one id, so shift it out
1615 $contactId = array_shift($cids);
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);
1622 // else skip does nothing and just returns an error code.
1625 'contact_id' => $cid,
1628 $newContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
1631 if (is_array($newContact)) {
1632 $contactID = $newContact['error_message']['params'][0];
1633 if (is_array($contactID)) {
1634 $contactID = array_pop($contactID);
1636 if (!in_array($contactID, $this->_newContacts
)) {
1637 $this->_newContacts
[] = $contactID;
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
;
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
);
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}
1659 * @param int $statusID
1662 * @throws \API_Exception|\CRM_Core_Exception
1664 public function run(
1666 $mode = self
::MODE_PREVIEW
,
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();
1680 $this->_rowCount
= 0;
1681 $this->_totalCount
= 0;
1683 $this->_primaryKeyName
= '_id';
1684 $this->_statusFieldName
= '_status';
1687 $this->progressImport($statusID);
1688 $startTimestamp = $currTimestamp = $prevTimestamp = time();
1690 $dataSource = $this->getDataSourceObject();
1691 $totalRowCount = $dataSource->getRowCount(['new']);
1692 if ($mode == self
::MODE_IMPORT
) {
1693 $dataSource->setStatuses(['new']);
1696 while ($row = $dataSource->getRow()) {
1697 $values = array_values($row);
1700 $this->_totalCount++
;
1702 if ($mode == self
::MODE_PREVIEW
) {
1703 $returnCode = $this->preview($values);
1705 elseif ($mode == self
::MODE_SUMMARY
) {
1706 $returnCode = $this->summary($values);
1708 elseif ($mode == self
::MODE_IMPORT
) {
1710 $returnCode = $this->import($onDuplicate, $values);
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
1717 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $e->getMessage());
1719 if ($statusID && (($this->_rowCount %
50) == 0)) {
1720 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
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));
1731 * Given a list of the importable field keys that the user has selected.
1732 * set the active fields array to this list
1734 * @param array $fieldKeys
1735 * Mapped array of values.
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 -'));
1743 $this->_activeFields
[] = clone($this->_fields
[$key]);
1749 * Format the field values for input to the api.
1751 * @param array $values
1752 * The row from the datasource.
1755 * Parameters mapped as described in getMappedRow
1757 * @throws \API_Exception
1758 * @todo - clean this up a bit & merge back into `getMappedRow`
1761 private function getParams(array $values): array {
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) {
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)));
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']),
1784 $this->addFieldToParams($params[$relatedContactKey], $locationValues, $fieldName, $importedValue);
1787 $this->addFieldToParams($params, $locationValues, $fieldName, $importedValue);
1791 $this->fillStateProvince($params);
1797 * @param string $name
1800 * @param string $headerPattern
1801 * @param string $dataPattern
1802 * @param bool $hasLocationType
1804 public function addField(
1805 $name, $title, $type = CRM_Utils_Type
::T_INT
,
1806 $headerPattern = '//', $dataPattern = '//',
1807 $hasLocationType = FALSE
1809 $this->_fields
[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
1811 $this->_fields
['doNotImport'] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
1816 * Store parser values.
1818 * @param CRM_Core_Session $store
1822 public function set($store, $mode = self
::MODE_SUMMARY
) {
1826 * Export data to a CSV file.
1828 * @param string $fileName
1829 * @param array $header
1830 * @param array $data
1832 public static function exportCSV($fileName, $header, $data) {
1834 if (file_exists($fileName) && !is_writable($fileName)) {
1835 CRM_Core_Error
::movedSiteError($fileName);
1837 //hack to remove '_status', '_statusMsg' and '_id' from error file
1839 $dbRecordStatus = ['IMPORTED', 'ERROR', 'DUPLICATE', 'INVALID', 'NEW'];
1840 foreach ($data as $rowCount => $rowValues) {
1842 foreach ($rowValues as $key => $val) {
1843 if (in_array($val, $dbRecordStatus) && $count == (count($rowValues) - 3)) {
1846 $errorValues[$rowCount][$key] = $val;
1850 $data = $errorValues;
1853 $fd = fopen($fileName, 'w');
1855 foreach ($header as $key => $value) {
1856 $header[$key] = "\"$value\"";
1858 $config = CRM_Core_Config
::singleton();
1859 $output[] = implode($config->fieldSeparator
, $header);
1861 foreach ($data as $datum) {
1862 foreach ($datum as $key => $value) {
1863 $datum[$key] = "\"$value\"";
1865 $output[] = implode($config->fieldSeparator
, $datum);
1867 fwrite($fd, implode("\n", $output));
1872 * Update the status of the import row to reflect the processing outcome.
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]
1882 * @throws \API_Exception
1883 * @throws \CRM_Core_Exception
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);
1890 * Format contact parameters.
1892 * @todo this function needs re-writing & re-merging into the main function.
1896 * @param array $values
1897 * @param array $params
1901 protected function formatContactParameters(&$values, &$params) {
1902 // Crawl through the possible classes:
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);
1918 if (isset($values['contact_type'])) {
1919 // we're an individual/household/org property
1921 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact
::fields();
1923 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
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 = [];
1931 if (isset($values['note'])) {
1933 if (!isset($params['note'])) {
1934 $params['note'] = [];
1936 $noteBlock = count($params['note']) +
1;
1938 $params['note'][$noteBlock] = [];
1939 if (!isset($fields['Note'])) {
1940 $fields['Note'] = CRM_Core_DAO_Note
::fields();
1943 // get the current logged in civicrm user
1944 $session = CRM_Core_Session
::singleton();
1945 $userID = $session->get('userID');
1948 $values['contact_id'] = $userID;
1951 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
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
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
1965 if (!array_key_exists($customFieldID, $customFields)) {
1966 return civicrm_api3_create_error('Invalid custom field ID');
1969 $params[$key] = $value;
1977 * Format location block ready for importing.
1979 * There is some test coverage for this in
1980 * CRM_Contact_Import_Parser_ContactTest e.g. testImportPrimaryAddress.
1982 * @param array $values
1983 * @param array $params
1986 * @throws \CiviCRM_API3_Exception
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');
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)) {
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)))
2013 if ($htmlType == 'CheckBox') {
2014 $newValues[$key][$v2['value']] = 1;
2017 $newValues[$key][] = $v2['value'];
2025 // consider new values
2026 $values = $newValues;
2033 * Get the field metadata for the relevant entity.
2035 * @param string $entity
2039 protected function getMetadataForEntity($entity) {
2040 if (!isset($this->fieldMetadata
[$entity])) {
2041 $className = "CRM_Core_DAO_$entity";
2042 $this->fieldMetadata
[$entity] = $className::fields();
2044 return $this->fieldMetadata
[$entity];
2048 * Fill in the primary location.
2050 * If the contact has a primary address we update it. Otherwise
2051 * we add an address of the default location type.
2053 * @param array $params
2054 * Address block parameters
2055 * @param array $values
2057 * @param string $entity
2058 * - address, email, phone
2059 * @param int|null $contactID
2061 * @throws \CiviCRM_API3_Exception
2063 protected function fillPrimary(&$params, $values, $entity, $contactID) {
2064 if ($values['location_type_id'] === 'Primary') {
2066 $primary = civicrm_api3($entity, 'get', [
2067 'return' => 'location_type_id',
2068 'contact_id' => $contactID,
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;
2080 * Get the civicrm_mapping_field appropriate layout for the mapper input.
2082 * The input looks something like ['street_address', 1]
2083 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
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
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
2098 * @throws \API_Exception
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];
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,
2125 * @param array $mappedField
2126 * Field detail as would be saved in field_mapping table
2127 * or as returned from getMappingFieldFromMapperInput
2130 * @throws \API_Exception
2132 public function getMappedFieldLabel(array $mappedField): string {
2133 $this->setFieldMetadata();
2135 if ($mappedField['relationship_type_id']) {
2136 $title[] = $this->getRelationshipLabel($mappedField['relationship_type_id'], $mappedField['relationship_direction']);
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']);
2142 if ($mappedField['website_type_id']) {
2143 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_Website', 'website_type_id', $mappedField['website_type_id']);
2145 if ($mappedField['phone_type_id']) {
2146 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $mappedField['phone_type_id']);
2148 if ($mappedField['im_provider_id']) {
2149 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_IM', 'provider_id', $mappedField['im_provider_id']);
2151 return implode(' - ', $title);
2155 * Get the relevant label for the relationship.
2158 * @param string $direction
2161 * @throws \API_Exception
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];
2171 return $this->relationshipLabels
[$id . $direction];
2175 * Transform the input parameters into the form handled by the input routine.
2177 * @param array $values
2178 * Input parameters as they come in from the datasource
2179 * eg. ['Bob', 'Smith', 'bob@example.org', '123-456']
2182 * Parameters mapped to CiviCRM fields based on the mapping
2183 * and specified contact type. eg.
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],
2192 * @throws \API_Exception
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();
2204 * Validate the import values.
2206 * The values array represents a row in the datasource.
2208 * @param array $values
2210 * @throws \API_Exception
2211 * @throws \CRM_Core_Exception
2213 public function validateValues(array $values): void
{
2214 $params = $this->getMappedRow($values);
2215 $contacts = array_merge(['0' => $params], $this->getRelatedContactsParams($params));
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);
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.'));
2233 //check for duplicate external Identifier
2234 $externalID = $params['external_identifier'] ??
NULL;
2236 /* If it's a dupe,external Identifier */
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);
2242 //otherwise, count it and move on
2243 $this->_allExternalIdentifiers
[$externalID] = $this->_lineCount
;
2246 //date-format part ends
2248 $errorMessage = implode(', ', $errors);
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);
2259 * Get the invalid values in the params for the given contact.
2261 * @param array|int|string $value
2262 * @param string $prefixString
2265 * @throws \API_Exception
2266 * @throws \Civi\API\Exception\NotImplementedException
2268 protected function getInvalidValuesForContact($value, string $prefixString): array {
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);
2282 * Get the field mappings for the import.
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'
2289 * @throws \API_Exception
2291 protected function getFieldMappings(): array {
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';
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;
2306 return $mappedFields;
2310 * Get the related contact type.
2312 * @param int|null $relationshipTypeID
2313 * @param int|string $relationshipDirection
2315 * @return null|string
2317 * @throws \API_Exception
2319 protected function getRelatedContactType($relationshipTypeID, $relationshipDirection): ?
string {
2320 if (!$relationshipTypeID) {
2323 $relationshipField = 'contact_type_' . substr($relationshipDirection, -1);
2324 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2328 * Get the related contact sub type.
2330 * @param int|null $relationshipTypeID
2331 * @param int|string $relationshipDirection
2333 * @return null|string
2335 * @throws \API_Exception
2337 protected function getRelatedContactSubType(int $relationshipTypeID, $relationshipDirection): ?
string {
2338 if (!$relationshipTypeID) {
2341 $relationshipField = 'contact_sub_type_' . substr($relationshipDirection, -1);
2342 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2346 * Get the related contact type.
2348 * @param int|null $relationshipTypeID
2349 * @param int|string $relationshipDirection
2351 * @return null|string
2353 * @throws \API_Exception
2355 protected function getRelatedContactLabel($relationshipTypeID, $relationshipDirection): ?
string {
2356 $relationshipField = 'label_' . $relationshipDirection;
2357 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2361 * Get the relationship type.
2363 * @param int $relationshipTypeID
2366 * @throws \API_Exception
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();
2375 return Civi
::$statics[__CLASS__
][$cacheKey];
2379 * Add the given field to the contact array.
2381 * @param array $contactArray
2382 * @param array $locationValues
2383 * @param string $fieldName
2384 * @param mixed $importedValue
2387 * @throws \API_Exception
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));
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'] ??
'');
2402 $fieldValue = $this->getTransformedFieldValue($realFieldName, $importedValue);
2404 if (!isset($contactArray[$entity][$entityKey])) {
2405 $contactArray[$entity][$entityKey] = $locationValues;
2407 // So im has really non-standard handling...
2408 $reallyRealFieldName = $realFieldName === 'im' ?
'name' : $realFieldName;
2409 $contactArray[$entity][$entityKey][$reallyRealFieldName] = $fieldValue;
2412 $fieldName = array_search($fieldName, $this->getOddlyMappedMetadataFields(), TRUE) ?
: $fieldName;
2413 $contactArray[$fieldName] = $this->getTransformedFieldValue($fieldName, $importedValue);
2418 * Get any related contacts designated for update.
2420 * This extracts the parts that relate to separate related
2421 * contacts from the 'params' array.
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.
2426 * @param array $params
2429 * e.g ['5_a_b' => ['contact_type' => 'Organization', 'organization_name' => 'The Firm']]
2430 * @throws \API_Exception
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
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;
2446 return $relatedContacts;
2450 * Look up for an existing contact with the given external_identifier.
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.
2456 * @param string|null $externalIdentifier
2457 * @param string $contactType
2461 * @throws \CRM_Core_Exception
2462 * @throws \CiviCRM_API3_Exception
2464 protected function lookupExternalIdentifier(?
string $externalIdentifier, string $contactType): ?
int {
2465 if (!$externalIdentifier) {
2468 // Check for any match on external id, deleted or otherwise.
2469 $foundContact = civicrm_api3('Contact', 'get', [
2470 'external_identifier' => $externalIdentifier,
2472 'sequential' => TRUE,
2473 'return' => ['id', 'contact_is_deleted', 'contact_type'],
2475 if (empty($foundContact['id'])) {
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);
2485 if ($foundContact['values'][0]['contact_type'] !== $contactType) {
2486 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser
::NO_MATCH
);
2488 return (int) $foundContact['id'];
2492 * Lookup the contact's contact ID.
2494 * @param array $params
2495 * @param bool $isDuplicateIfExternalIdentifierExists
2499 * @throws \API_Exception
2500 * @throws \CRM_Core_Exception
2501 * @throws \CiviCRM_API3_Exception
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
);
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
);
2513 if ($extIDMatch && $isDuplicateIfExternalIdentifierExists) {
2514 throw new CRM_Core_Exception(ts('External ID already exists in Database.'), CRM_Import_Parser
::DUPLICATE
);
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
);
2525 if ($existingContact['contact_type'] !== $params['contact_type']) {
2526 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser
::NO_MATCH
);
2530 // Time to see if we can find an existing contact ID to make this an update
2532 if ($extIDMatch ||
$this->isUpdateExistingContacts()) {
2533 return $this->getPossibleContactMatch($params, $extIDMatch, $this->getSubmittedValue('dedupe_rule_id') ?
: NULL);
2539 * @param array $params
2540 * @param array $formatted
2542 * @throws \API_Exception
2543 * @throws \CRM_Core_Exception
2544 * @throws \CiviCRM_API3_Exception
2545 * @throws \Civi\API\Exception\UnauthorizedException
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')
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
);
2559 return array($formatted, $params);
2563 * Try to get the correct state province using what country information we have.
2565 * If the state matches more than one possibility then either the imported
2566 * country of the site country should help us....
2568 * @param string $stateProvince
2569 * @param int|null|string $countryID
2571 * @return int|string
2572 * @throws \API_Exception
2573 * @throws \Civi\API\Exception\UnauthorizedException
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)];
2579 return $this->checkStatesForCountry($countryID, $possibleStates) ?
: 'invalid_import_value';
2581 // Try the default country next.
2582 $defaultCountryMatch = $this->checkStatesForCountry($this->getSiteDefaultCountry(), $possibleStates);
2583 if ($defaultCountryMatch) {
2584 return $defaultCountryMatch;
2587 if ($this->getAvailableCountries()) {
2588 $countryMatches = [];
2589 foreach ($this->getAvailableCountries() as $availableCountryID) {
2590 $possible = $this->checkStatesForCountry($availableCountryID, $possibleStates);
2592 $countryMatches[] = $possible;
2595 if (count($countryMatches) === 1) {
2596 return reset($countryMatches);
2600 return $stateProvince;
2604 * @param array $params
2607 * @throws \API_Exception
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);
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';
2626 elseif (is_array($value) && !in_array($key, ['email', 'phone', 'im', 'website', 'openid'], TRUE)) {
2627 $this->fillStateProvince($params[$key]);
2634 * Check is any of the given states correlate to the country.
2636 * @param int $countryID
2637 * @param array $possibleStates
2640 * @throws \API_Exception
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) {
2648 unset($possibleStates[$index]);
2651 if (!empty($possibleStates)) {
2652 $states = StateProvince
::get(FALSE)
2653 ->addSelect('country_id')
2654 ->addWhere('id', 'IN', $possibleStates)
2656 ->indexBy('country_id');
2657 foreach ($states as $state) {
2658 $this->statesByCountry
[$state['id']] = $state['country_id'];
2660 foreach ($possibleStates as $state) {
2661 if ($this->statesByCountry
[$state] === $countryID) {