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
;
14 require_once 'CRM/Utils/DeprecatedUtils.php';
15 require_once 'api/v3/utils.php';
20 * @copyright CiviCRM LLC https://civicrm.org/licensing
24 * class to parse contact csv files
26 class CRM_Contact_Import_Parser_Contact
extends CRM_Contact_Import_Parser
{
28 use CRM_Contact_Import_MetadataTrait
;
30 protected $_mapperKeys = [];
31 protected $_mapperLocType = [];
32 protected $_mapperPhoneType;
33 protected $_mapperImProvider;
34 protected $_mapperWebsiteType;
35 protected $_mapperRelated;
36 protected $_mapperRelatedContactType;
37 protected $_mapperRelatedContactDetails;
38 protected $_mapperRelatedContactEmailType;
39 protected $_mapperRelatedContactImProvider;
40 protected $_mapperRelatedContactWebsiteType;
41 protected $_relationships;
43 protected $_emailIndex;
44 protected $_firstNameIndex;
45 protected $_lastNameIndex;
47 protected $_householdNameIndex;
48 protected $_organizationNameIndex;
50 protected $_allEmails;
52 protected $_phoneIndex;
55 * Is update only permitted on an id match.
57 * Note this historically was true for when id or external identifier was
58 * present. However, CRM-17275 determined that a dedupe-match could over-ride
59 * external identifier.
63 protected $_updateWithId;
66 protected $_externalIdentifierIndex;
67 protected $_allExternalIdentifiers;
68 protected $_parseStreetAddress;
71 * Array of successfully imported contact id's
75 protected $_newContacts;
82 protected $_lineCount;
85 * Array of successfully imported related contact id's
89 protected $_newRelatedContacts;
92 * Array of all the contacts whose street addresses are not parsed.
93 * of this import process
96 protected $_unparsedStreetAddressContacts;
101 * @param array $mapperKeys
102 * @param array $mapperLocType
103 * @param array $mapperPhoneType
104 * @param array $mapperImProvider
105 * @param array $mapperRelated
106 * @param array $mapperRelatedContactType
107 * @param array $mapperRelatedContactDetails
108 * @param array $mapperRelatedContactLocType
109 * @param array $mapperRelatedContactPhoneType
110 * @param array $mapperRelatedContactImProvider
111 * @param array $mapperWebsiteType
112 * @param array $mapperRelatedContactWebsiteType
114 public function __construct(
115 $mapperKeys, $mapperLocType = [], $mapperPhoneType = [], $mapperImProvider = [], $mapperRelated = [], $mapperRelatedContactType = [], $mapperRelatedContactDetails = [], $mapperRelatedContactLocType = [], $mapperRelatedContactPhoneType = [], $mapperRelatedContactImProvider = [],
116 $mapperWebsiteType = [], $mapperRelatedContactWebsiteType = []
118 parent
::__construct();
119 $this->_mapperKeys
= $mapperKeys;
120 $this->_mapperLocType
= &$mapperLocType;
121 $this->_mapperPhoneType
= &$mapperPhoneType;
122 $this->_mapperWebsiteType
= $mapperWebsiteType;
123 // get IM service provider type id for contact
124 $this->_mapperImProvider
= &$mapperImProvider;
125 $this->_mapperRelated
= &$mapperRelated;
126 $this->_mapperRelatedContactType
= &$mapperRelatedContactType;
127 $this->_mapperRelatedContactDetails
= &$mapperRelatedContactDetails;
128 $this->_mapperRelatedContactLocType
= &$mapperRelatedContactLocType;
129 $this->_mapperRelatedContactPhoneType
= &$mapperRelatedContactPhoneType;
130 $this->_mapperRelatedContactWebsiteType
= $mapperRelatedContactWebsiteType;
131 // get IM service provider type id for related contact
132 $this->_mapperRelatedContactImProvider
= &$mapperRelatedContactImProvider;
136 * The initializer code, called before processing.
138 public function init() {
139 $this->setFieldMetadata();
140 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
141 $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));
144 $this->_newContacts
= [];
146 $this->setActiveFields($this->_mapperKeys
);
147 $this->setActiveFieldLocationTypes($this->_mapperLocType
);
148 $this->setActiveFieldPhoneTypes($this->_mapperPhoneType
);
149 $this->setActiveFieldWebsiteTypes($this->_mapperWebsiteType
);
150 //set active fields of IM provider of contact
151 $this->setActiveFieldImProviders($this->_mapperImProvider
);
154 $this->setActiveFieldRelated($this->_mapperRelated
);
155 $this->setActiveFieldRelatedContactType($this->_mapperRelatedContactType
);
156 $this->setActiveFieldRelatedContactDetails($this->_mapperRelatedContactDetails
);
157 $this->setActiveFieldRelatedContactLocType($this->_mapperRelatedContactLocType
);
158 $this->setActiveFieldRelatedContactPhoneType($this->_mapperRelatedContactPhoneType
);
159 $this->setActiveFieldRelatedContactWebsiteType($this->_mapperRelatedContactWebsiteType
);
160 //set active fields of IM provider of related contact
161 $this->setActiveFieldRelatedContactImProvider($this->_mapperRelatedContactImProvider
);
163 $this->_phoneIndex
= -1;
164 $this->_emailIndex
= -1;
165 $this->_firstNameIndex
= -1;
166 $this->_lastNameIndex
= -1;
167 $this->_householdNameIndex
= -1;
168 $this->_organizationNameIndex
= -1;
169 $this->_externalIdentifierIndex
= -1;
172 foreach ($this->_mapperKeys
as $key) {
173 if (substr($key, 0, 5) == 'email' && substr($key, 0, 14) != 'email_greeting') {
174 $this->_emailIndex
= $index;
175 $this->_allEmails
= [];
177 if (substr($key, 0, 5) == 'phone') {
178 $this->_phoneIndex
= $index;
180 if ($key == 'first_name') {
181 $this->_firstNameIndex
= $index;
183 if ($key == 'last_name') {
184 $this->_lastNameIndex
= $index;
186 if ($key == 'household_name') {
187 $this->_householdNameIndex
= $index;
189 if ($key == 'organization_name') {
190 $this->_organizationNameIndex
= $index;
193 if ($key == 'external_identifier') {
194 $this->_externalIdentifierIndex
= $index;
195 $this->_allExternalIdentifiers
= [];
200 $this->_updateWithId
= FALSE;
201 if (in_array('id', $this->_mapperKeys
) ||
($this->_externalIdentifierIndex
>= 0 && in_array($this->_onDuplicate
, [
202 CRM_Import_Parser
::DUPLICATE_UPDATE
,
203 CRM_Import_Parser
::DUPLICATE_FILL
,
205 $this->_updateWithId
= TRUE;
208 $this->_parseStreetAddress
= CRM_Utils_Array
::value('street_address_parsing', CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
, 'address_options'), FALSE);
212 * Handle the values in mapField mode.
214 * @param array $values
215 * The array of values belonging to this line.
219 public function mapField(&$values) {
220 return CRM_Import_Parser
::VALID
;
224 * Handle the values in preview mode.
226 * @param array $values
227 * The array of values belonging to this line.
230 * the result of this processing
232 public function preview(&$values) {
233 return $this->summary($values);
237 * Handle the values in summary mode.
239 * @param array $values
240 * The array of values belonging to this line.
243 * the result of this processing
245 public function summary(&$values): int {
246 $erroneousField = NULL;
247 $this->setActiveFieldValues($values, $erroneousField);
248 $rowNumber = (int) ($values[count($values) - 1]);
249 $errorMessage = NULL;
250 $errorRequired = FALSE;
251 switch ($this->_contactType
) {
254 if ($this->_firstNameIndex
< 0 ||
empty($values[$this->_firstNameIndex
])) {
255 $errorRequired = TRUE;
256 $missingNames[] = ts('First Name');
258 if ($this->_lastNameIndex
< 0 ||
empty($values[$this->_lastNameIndex
])) {
259 $errorRequired = TRUE;
260 $missingNames[] = ts('Last Name');
262 if ($errorRequired) {
263 $and = ' ' . ts('and') . ' ';
264 $errorMessage = ts('Missing required fields:') . ' ' . implode($and, $missingNames);
269 if ($this->_householdNameIndex
< 0 ||
empty($values[$this->_householdNameIndex
])) {
270 $errorRequired = TRUE;
271 $errorMessage = ts('Missing required fields:') . ' ' . ts('Household Name');
276 if ($this->_organizationNameIndex
< 0 ||
empty($values[$this->_organizationNameIndex
])) {
277 $errorRequired = TRUE;
278 $errorMessage = ts('Missing required fields:') . ' ' . ts('Organization Name');
283 if ($this->_emailIndex
>= 0) {
284 /* If we don't have the required fields, bail */
286 if ($this->_contactType
=== 'Individual' && !$this->_updateWithId
) {
287 if ($errorRequired && empty($values[$this->_emailIndex
])) {
289 $errorMessage .= ' ' . ts('OR') . ' ' . ts('Email Address');
292 $errorMessage = ts('Missing required field:') . ' ' . ts('Email Address');
294 array_unshift($values, $errorMessage);
295 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
297 return CRM_Import_Parser
::ERROR
;
301 $email = $values[$this->_emailIndex
] ??
NULL;
303 /* If the email address isn't valid, bail */
305 if (!CRM_Utils_Rule
::email($email)) {
306 $errorMessage = ts('Invalid Email address');
307 array_unshift($values, $errorMessage);
308 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
310 return CRM_Import_Parser
::ERROR
;
313 /* otherwise, count it and move on */
314 $this->_allEmails
[$email] = $this->_lineCount
;
317 elseif ($errorRequired && !$this->_updateWithId
) {
319 $errorMessage .= ' ' . ts('OR') . ' ' . ts('Email Address');
322 $errorMessage = ts('Missing required field:') . ' ' . ts('Email Address');
324 array_unshift($values, $errorMessage);
325 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
327 return CRM_Import_Parser
::ERROR
;
330 //check for duplicate external Identifier
331 $externalID = $values[$this->_externalIdentifierIndex
] ??
NULL;
333 /* If it's a dupe,external Identifier */
335 if ($externalDupe = CRM_Utils_Array
::value($externalID, $this->_allExternalIdentifiers
)) {
336 $errorMessage = ts('External ID conflicts with record %1', [1 => $externalDupe]);
337 array_unshift($values, $errorMessage);
338 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
339 return CRM_Import_Parser
::ERROR
;
341 //otherwise, count it and move on
342 $this->_allExternalIdentifiers
[$externalID] = $this->_lineCount
;
345 //Checking error in custom data
346 $params = &$this->getActiveFieldParams();
347 $params['contact_type'] = $this->_contactType
;
348 //date-format part ends
350 $errorMessage = NULL;
353 //add custom fields for contact sub type
355 if (!empty($this->_contactSubType
)) {
356 $csType = $this->_contactSubType
;
359 //checking error in custom data
360 $this->isErrorInCustomData($params, $errorMessage, $csType, $this->_relationships
);
362 //checking error in core data
363 $this->isErrorInCoreData($params, $errorMessage);
365 $tempMsg = "Invalid value for field(s) : $errorMessage";
366 $this->setImportStatus($rowNumber, 'ERROR', $tempMsg);
367 array_unshift($values, $tempMsg);
368 $errorMessage = NULL;
369 return CRM_Import_Parser
::ERROR
;
371 $this->setImportStatus($rowNumber, 'NEW', '');
373 return CRM_Import_Parser
::VALID
;
377 * Get Array of all the fields that could potentially be part
382 public function getAllFields() {
383 return $this->_fields
;
387 * Handle the values in import mode.
389 * @param int $onDuplicate
390 * The code for what action to take on duplicates.
391 * @param array $values
392 * The array of values belonging to this line.
394 * @param bool $doGeocodeAddress
397 * the result of this processing
399 * @throws \CiviCRM_API3_Exception
400 * @throws \CRM_Core_Exception
401 * @throws \API_Exception
403 public function import($onDuplicate, &$values, $doGeocodeAddress = FALSE) {
404 $this->_unparsedStreetAddressContacts
= [];
405 if (!$doGeocodeAddress) {
406 // CRM-5854, reset the geocode method to null to prevent geocoding
407 CRM_Utils_GeocodeProvider
::disableForSession();
410 // first make sure this is a valid line
411 //$this->_updateWithId = false;
412 $response = $this->summary($values);
413 $statusFieldName = $this->_statusFieldName
;
415 if ($response != CRM_Import_Parser
::VALID
) {
416 $importRecordParams = [
417 $statusFieldName => 'INVALID',
418 "${statusFieldName}Msg" => "Invalid (Error Code: $response)",
420 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
424 $params = &$this->getActiveFieldParams();
426 'contact_type' => $this->_contactType
,
429 $contactFields = CRM_Contact_DAO_Contact
::import();
431 //check if external identifier exists in database
432 if (!empty($params['external_identifier']) && (!empty($params['id']) ||
in_array($onDuplicate, [
433 CRM_Import_Parser
::DUPLICATE_SKIP
,
434 CRM_Import_Parser
::DUPLICATE_NOCHECK
,
437 $extIDResult = civicrm_api3('Contact', 'get', [
438 'external_identifier' => $params['external_identifier'],
440 'return' => ['id', 'contact_is_deleted'],
442 if (isset($extIDResult['id'])) {
443 // record with matching external identifier does exist.
444 $internalCid = $extIDResult['id'];
445 if ($internalCid != CRM_Utils_Array
::value('id', $params)) {
446 if ($extIDResult['values'][$internalCid]['contact_is_deleted'] == 1) {
447 // And it is deleted. What to do? If we skip it, they user
448 // will be under the impression that the record exists in
449 // the database, yet they won't be able to find it. If we
450 // don't skip it, the database will try to insert a new record
451 // with an external_identifier that is non-unique. So...
452 // we will update this contact to remove the external_identifier
453 // and let a new record be created.
454 $update_params = ['id' => $internalCid, 'external_identifier' => ''];
455 civicrm_api3('Contact', 'create', $update_params);
458 $errorMessage = ts('External ID already exists in Database.');
459 array_unshift($values, $errorMessage);
460 $importRecordParams = [
461 $statusFieldName => 'ERROR',
462 "${statusFieldName}Msg" => $errorMessage,
464 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
465 return CRM_Import_Parser
::DUPLICATE
;
471 if (!empty($this->_contactSubType
)) {
472 $params['contact_sub_type'] = $this->_contactSubType
;
475 if ($subType = CRM_Utils_Array
::value('contact_sub_type', $params)) {
476 if (CRM_Contact_BAO_ContactType
::isExtendsContactType($subType, $this->_contactType
, FALSE, 'label')) {
477 $subTypes = CRM_Contact_BAO_ContactType
::subTypePairs($this->_contactType
, FALSE, NULL);
478 $params['contact_sub_type'] = array_search($subType, $subTypes);
480 elseif (!CRM_Contact_BAO_ContactType
::isExtendsContactType($subType, $this->_contactType
)) {
481 $message = "Mismatched or Invalid Contact Subtype.";
482 array_unshift($values, $message);
483 return CRM_Import_Parser
::NO_MATCH
;
487 // Get contact id to format common data in update/fill mode,
488 // prioritising a dedupe rule check over an external_identifier check, but falling back on ext id.
489 if ($this->_updateWithId
&& empty($params['id'])) {
491 $possibleMatches = $this->getPossibleContactMatches($params);
493 catch (CRM_Core_Exception
$e) {
494 $errorMessage = $e->getMessage();
495 array_unshift($values, $errorMessage);
497 $importRecordParams = [
498 $statusFieldName => 'ERROR',
499 "${statusFieldName}Msg" => $errorMessage,
501 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
502 return CRM_Import_Parser
::ERROR
;
504 foreach ($possibleMatches as $possibleID) {
505 $params['id'] = $formatted['id'] = $possibleID;
508 //format common data, CRM-4062
509 $this->formatCommonData($params, $formatted, $contactFields);
511 $relationship = FALSE;
512 $createNewContact = TRUE;
513 // Support Match and Update Via Contact ID
514 if ($this->_updateWithId
&& isset($params['id'])) {
515 $createNewContact = FALSE;
516 // @todo - it feels like all the rows from here to the end of the IF
517 // could be removed in favour of a simple check for whether the contact_type & id match
518 $matchedIDs = $this->getIdsOfMatchingContacts($formatted);
519 if (!empty($matchedIDs)) {
520 if (count($matchedIDs) >= 1) {
522 foreach ($matchedIDs as $contactId) {
523 if ($params['id'] == $contactId) {
524 $contactType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_type');
525 if ($formatted['contact_type'] == $contactType) {
526 //validation of subtype for update mode
528 $contactSubType = NULL;
529 if (!empty($params['contact_sub_type'])) {
530 $contactSubType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_sub_type');
533 if (!empty($contactSubType) && (!CRM_Contact_BAO_ContactType
::isAllowEdit($params['id'], $contactSubType) && $contactSubType != CRM_Utils_Array
::value('contact_sub_type', $formatted))) {
535 $message = "Mismatched contact SubTypes :";
536 array_unshift($values, $message);
538 $this->_retCode
= CRM_Import_Parser
::NO_MATCH
;
542 $this->_retCode
= CRM_Import_Parser
::VALID
;
546 $message = "Mismatched contact Types :";
547 array_unshift($values, $message);
549 $this->_retCode
= CRM_Import_Parser
::NO_MATCH
;
554 $message = "Mismatched contact IDs OR Mismatched contact Types :";
555 array_unshift($values, $message);
556 $this->_retCode
= CRM_Import_Parser
::NO_MATCH
;
562 if (!empty($params['id'])) {
563 $contactType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_type');
565 if ($formatted['contact_type'] == $contactType) {
566 //validation of subtype for update mode
568 $contactSubType = NULL;
569 if (!empty($params['contact_sub_type'])) {
570 $contactSubType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_sub_type');
573 if (!empty($contactSubType) && (!CRM_Contact_BAO_ContactType
::isAllowEdit($params['id'], $contactSubType) && $contactSubType != CRM_Utils_Array
::value('contact_sub_type', $formatted))) {
575 $message = "Mismatched contact SubTypes :";
576 array_unshift($values, $message);
577 $this->_retCode
= CRM_Import_Parser
::NO_MATCH
;
580 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $params['id'], FALSE, $this->_dedupeRuleGroupID
);
581 $this->_retCode
= CRM_Import_Parser
::VALID
;
585 $message = "Mismatched contact Types :";
586 array_unshift($values, $message);
587 $this->_retCode
= CRM_Import_Parser
::NO_MATCH
;
591 // we should avoid multiple errors for single record
592 // since we have already retCode and we trying to force again.
593 if ($this->_retCode
!= CRM_Import_Parser
::NO_MATCH
) {
594 $message = "No contact found for this contact ID:" . $params['id'];
595 array_unshift($values, $message);
596 $this->_retCode
= CRM_Import_Parser
::NO_MATCH
;
602 //now we want to create new contact on update/fill also.
603 $createNewContact = TRUE;
607 if (isset($newContact) && is_a($newContact, 'CRM_Contact_BAO_Contact')) {
608 $relationship = TRUE;
613 //now we create new contact in update/fill mode also.
615 if ($createNewContact ||
($this->_retCode
!= CRM_Import_Parser
::NO_MATCH
&& $this->_updateWithId
)) {
616 // @todo - there are multiple places where formatting is done that need consolidation.
617 // This handles where the label has been passed in and it has gotten this far.
618 // probably a bunch of hard-coded stuff could be removed to rely on this.
619 $fields = Contact
::getFields(FALSE)
620 ->addWhere('options', '=', TRUE)
621 ->setLoadOptions(TRUE)
622 ->execute()->indexBy('name');
623 foreach ($fields as $fieldName => $fieldSpec) {
624 if (isset($formatted[$fieldName]) && is_array($formatted[$fieldName])) {
625 // If we have an array at this stage, it's probably a multi-select
626 // field that has already been parsed properly into the value that
627 // should be inserted into the database.
630 if (!empty($formatted[$fieldName])
631 && empty($fieldSpec['options'][$formatted[$fieldName]])) {
632 $formatted[$fieldName] = array_search($formatted[$fieldName], $fieldSpec['options'], TRUE) ??
$formatted[$fieldName];
635 //CRM-4430, don't carry if not submitted.
636 if ($this->_updateWithId
&& !empty($params['id'])) {
637 $contactID = $params['id'];
639 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID
);
642 if (isset($newContact) && is_object($newContact) && ($newContact instanceof CRM_Contact_BAO_Contact
)) {
643 $relationship = TRUE;
644 $newContact = clone($newContact);
645 $contactID = $newContact->id
;
646 $this->_newContacts
[] = $contactID;
648 //get return code if we create new contact in update mode, CRM-4148
649 if ($this->_updateWithId
) {
650 $this->_retCode
= CRM_Import_Parser
::VALID
;
653 elseif (isset($newContact) && CRM_Core_Error
::isAPIError($newContact, CRM_Core_Error
::DUPLICATE_CONTACT
)) {
654 // if duplicate, no need of further processing
655 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
) {
656 $errorMessage = "Skipping duplicate record";
657 array_unshift($values, $errorMessage);
658 $importRecordParams = [
659 $statusFieldName => 'DUPLICATE',
660 "${statusFieldName}Msg" => $errorMessage,
662 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
663 return CRM_Import_Parser
::DUPLICATE
;
666 $relationship = TRUE;
667 // CRM-10433/CRM-20739 - IDs could be string or array; handle accordingly
668 if (!is_array($dupeContactIDs = $newContact['error_message']['params'][0])) {
669 $dupeContactIDs = explode(',', $dupeContactIDs);
671 $dupeCount = count($dupeContactIDs);
672 $contactID = array_pop($dupeContactIDs);
673 // check to see if we had more than one duplicate contact id.
674 // if we have more than one, the record will be rejected below
675 if ($dupeCount == 1) {
676 // there was only one dupe, we will continue normally...
677 if (!in_array($contactID, $this->_newContacts
)) {
678 $this->_newContacts
[] = $contactID;
685 $currentImportID = end($values);
688 'contactID' => $contactID,
689 'importID' => $currentImportID,
690 'importTempTable' => $this->_tableName
,
691 'fieldHeaders' => $this->_mapperKeys
,
692 'fields' => $this->_activeFields
,
695 CRM_Utils_Hook
::import('Contact', 'process', $this, $hookParams);
699 $primaryContactId = NULL;
700 if (CRM_Core_Error
::isAPIError($newContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
701 if ($dupeCount == 1 && CRM_Utils_Rule
::integer($contactID)) {
702 $primaryContactId = $contactID;
706 $primaryContactId = $newContact->id
;
709 if ((CRM_Core_Error
::isAPIError($newContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
) ||
is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) {
711 //relationship contact insert
712 foreach ($params as $key => $field) {
713 [$id, $first, $second] = CRM_Utils_System
::explode('_', $key, 3);
714 if (!($first == 'a' && $second == 'b') && !($first == 'b' && $second == 'a')) {
718 $relationType = new CRM_Contact_DAO_RelationshipType();
719 $relationType->id
= $id;
720 $relationType->find(TRUE);
721 $direction = "contact_sub_type_$second";
724 'contact_type' => $params[$key]['contact_type'],
727 //set subtype for related contact CRM-5125
728 if (isset($relationType->$direction)) {
729 //validation of related contact subtype for update mode
730 if ($relCsType = CRM_Utils_Array
::value('contact_sub_type', $params[$key]) && $relCsType != $relationType->$direction) {
731 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
732 array_unshift($values, $errorMessage);
733 return CRM_Import_Parser
::NO_MATCH
;
736 $formatting['contact_sub_type'] = $relationType->$direction;
740 $contactFields = NULL;
741 $contactFields = CRM_Contact_DAO_Contact
::import();
743 //Relation on the basis of External Identifier.
744 if (empty($params[$key]['id']) && !empty($params[$key]['external_identifier'])) {
745 $params[$key]['id'] = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['external_identifier'], 'id', 'external_identifier');
747 // check for valid related contact id in update/fill mode, CRM-4424
748 if (in_array($onDuplicate, [
749 CRM_Import_Parser
::DUPLICATE_UPDATE
,
750 CRM_Import_Parser
::DUPLICATE_FILL
,
751 ]) && !empty($params[$key]['id'])) {
752 $relatedContactType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_type');
753 if (!$relatedContactType) {
754 $errorMessage = ts("No contact found for this related contact ID: %1", [1 => $params[$key]['id']]);
755 array_unshift($values, $errorMessage);
756 return CRM_Import_Parser
::NO_MATCH
;
759 //validation of related contact subtype for update mode
761 $relatedCsType = NULL;
762 if (!empty($formatting['contact_sub_type'])) {
763 $relatedCsType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_sub_type');
766 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType
::isAllowEdit($params[$key]['id'], $relatedCsType) &&
767 $relatedCsType != CRM_Utils_Array
::value('contact_sub_type', $formatting))
769 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.") . ' ' . ts("ID: %1", [1 => $params[$key]['id']]);
770 array_unshift($values, $errorMessage);
771 return CRM_Import_Parser
::NO_MATCH
;
773 // get related contact id to format data in update/fill mode,
774 //if external identifier is present, CRM-4423
775 $formatting['id'] = $params[$key]['id'];
778 //format common data, CRM-4062
779 $this->formatCommonData($field, $formatting, $contactFields);
781 //do we have enough fields to create related contact.
782 $allowToCreate = $this->checkRelatedContactFields($key, $formatting);
784 if (!$allowToCreate) {
785 $errorMessage = ts('Related contact required fields are missing.');
786 array_unshift($values, $errorMessage);
787 return CRM_Import_Parser
::NO_MATCH
;
791 if (!empty($params[$key]['id'])) {
793 'contact_id' => $params[$key]['id'],
796 $relatedNewContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
799 $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE);
802 if (is_object($relatedNewContact) ||
($relatedNewContact instanceof CRM_Contact_BAO_Contact
)) {
803 $relatedNewContact = clone($relatedNewContact);
807 // To update/fill contact, get the matching contact Ids if duplicate contact found
808 // otherwise get contact Id from object of related contact
809 if (is_array($relatedNewContact) && civicrm_error($relatedNewContact)) {
810 if (CRM_Core_Error
::isAPIError($relatedNewContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
811 $matchedIDs = $relatedNewContact['error_message']['params'][0];
812 if (!is_array($matchedIDs)) {
813 $matchedIDs = explode(',', $matchedIDs);
817 $errorMessage = $relatedNewContact['error_message'];
818 array_unshift($values, $errorMessage);
819 $importRecordParams = [
820 $statusFieldName => 'ERROR',
821 "${statusFieldName}Msg" => $errorMessage,
823 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
824 return CRM_Import_Parser
::ERROR
;
828 $matchedIDs[] = $relatedNewContact->id
;
830 // update/fill related contact after getting matching Contact Ids, CRM-4424
831 if (in_array($onDuplicate, [
832 CRM_Import_Parser
::DUPLICATE_UPDATE
,
833 CRM_Import_Parser
::DUPLICATE_FILL
,
835 //validation of related contact subtype for update mode
837 $relatedCsType = NULL;
838 if (!empty($formatting['contact_sub_type'])) {
839 $relatedCsType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $matchedIDs[0], 'contact_sub_type');
842 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType
::isAllowEdit($matchedIDs[0], $relatedCsType) && $relatedCsType != CRM_Utils_Array
::value('contact_sub_type', $formatting))) {
843 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
844 array_unshift($values, $errorMessage);
845 return CRM_Import_Parser
::NO_MATCH
;
848 $updatedContact = $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]);
851 static $relativeContact = [];
852 if (CRM_Core_Error
::isAPIError($relatedNewContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
853 if (count($matchedIDs) >= 1) {
854 $relContactId = $matchedIDs[0];
855 //add relative contact to count during update & fill mode.
856 //logic to make count distinct by contact id.
857 if ($this->_newRelatedContacts ||
!empty($relativeContact)) {
858 $reContact = array_keys($relativeContact, $relContactId);
860 if (empty($reContact)) {
861 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
865 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
870 $relContactId = $relatedNewContact->id
;
871 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
874 if (CRM_Core_Error
::isAPIError($relatedNewContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
) ||
($relatedNewContact instanceof CRM_Contact_BAO_Contact
)) {
875 //fix for CRM-1993.Checks for duplicate related contacts
876 if (count($matchedIDs) >= 1) {
877 //if more than one duplicate contact
878 //found, create relationship with first contact
879 // now create the relationship record
880 $relationParams = [];
882 'relationship_type_id' => $key,
887 'skipRecentView' => TRUE,
890 // we only handle related contact success, we ignore failures for now
891 // at some point wold be nice to have related counts as separate
893 'contact' => $primaryContactId,
896 [$valid, $duplicate] = CRM_Contact_BAO_Relationship
::legacyCreateMultiple($relationParams, $relationIds);
898 if ($valid ||
$duplicate) {
899 $relationIds['contactTarget'] = $relContactId;
900 $action = ($duplicate) ? CRM_Core_Action
::UPDATE
: CRM_Core_Action
::ADD
;
901 CRM_Contact_BAO_Relationship
::relatedMemberships($primaryContactId, $relationParams, $relationIds, $action);
904 //handle current employer, CRM-3532
906 $allRelationships = CRM_Core_PseudoConstant
::relationshipType('name');
907 $relationshipTypeId = str_replace([
914 $relationshipType = str_replace($relationshipTypeId . '_', '', $key);
915 $orgId = $individualId = NULL;
916 if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') {
917 $orgId = $relContactId;
918 $individualId = $primaryContactId;
920 elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') {
921 $orgId = $primaryContactId;
922 $individualId = $relContactId;
924 if ($orgId && $individualId) {
925 $currentEmpParams[$individualId] = $orgId;
926 CRM_Contact_BAO_Contact_Utils
::setCurrentEmployer($currentEmpParams);
934 if ($this->_updateWithId
) {
935 //return warning if street address is unparsed, CRM-5886
936 return $this->processMessage($values, $statusFieldName, $this->_retCode
);
939 if (is_array($newContact) && civicrm_error($newContact)) {
942 if (($code = CRM_Utils_Array
::value('code', $newContact['error_message'])) && ($code == CRM_Core_Error
::DUPLICATE_CONTACT
)) {
943 return $this->handleDuplicateError($newContact, $statusFieldName, $values, $onDuplicate, $formatted, $contactFields);
945 // Not a dupe, so we had an error
946 $errorMessage = $newContact['error_message'];
947 array_unshift($values, $errorMessage);
948 $importRecordParams = [
949 $statusFieldName => 'ERROR',
950 "${statusFieldName}Msg" => $errorMessage,
952 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
953 return CRM_Import_Parser
::ERROR
;
957 return $this->processMessage($values, $statusFieldName, CRM_Import_Parser
::VALID
);
961 * Format common params data to proper format to store.
963 * @param array $params
964 * Contain record values.
965 * @param array $formatted
966 * Array of formatted data.
967 * @param array $contactFields
968 * Contact DAO fields.
970 private function formatCommonData($params, &$formatted, $contactFields) {
972 CRM_Utils_Array
::value('contact_type', $formatted),
976 //add custom fields for contact sub type
977 if (!empty($this->_contactSubType
)) {
978 $csType = $this->_contactSubType
;
981 if ($relCsType = CRM_Utils_Array
::value('contact_sub_type', $formatted)) {
982 $csType = $relCsType;
985 $customFields = CRM_Core_BAO_CustomField
::getFields($formatted['contact_type'], FALSE, FALSE, $csType);
987 $addressCustomFields = CRM_Core_BAO_CustomField
::getFields('Address');
988 $customFields = $customFields +
$addressCustomFields;
990 //if a Custom Email Greeting, Custom Postal Greeting or Custom Addressee is mapped, and no "Greeting / Addressee Type ID" is provided, then automatically set the type = Customized, CRM-4575
992 'email_greeting_custom' => 'email_greeting',
993 'postal_greeting_custom' => 'postal_greeting',
994 'addressee_custom' => 'addressee',
996 foreach ($elements as $k => $v) {
997 if (array_key_exists($k, $params) && !(array_key_exists($v, $params))) {
998 $label = key(CRM_Core_OptionGroup
::values($v, TRUE, NULL, NULL, 'AND v.name = "Customized"'));
999 $params[$v] = $label;
1004 $session = CRM_Core_Session
::singleton();
1005 $dateType = $session->get("dateTypes");
1006 foreach ($params as $key => $val) {
1007 $customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key);
1008 if ($customFieldID &&
1009 !array_key_exists($customFieldID, $addressCustomFields)
1011 //we should not update Date to null, CRM-4062
1012 if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) {
1014 CRM_Contact_Import_Parser_Contact
::formatCustomDate($params, $formatted, $dateType, $key);
1016 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
1017 if (empty($val) && !is_numeric($val) && $this->_onDuplicate
== CRM_Import_Parser
::DUPLICATE_FILL
) {
1018 //retain earlier value when Import mode is `Fill`
1019 unset($params[$key]);
1022 $params[$key] = CRM_Utils_String
::strtoboolstr($val);
1027 if ($key == 'birth_date' && $val) {
1028 CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $key);
1030 elseif ($key == 'deceased_date' && $val) {
1031 CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $key);
1032 $params['is_deceased'] = 1;
1034 elseif ($key == 'is_deceased' && $val) {
1035 $params[$key] = CRM_Utils_String
::strtoboolstr($val);
1039 //now format custom data.
1040 foreach ($params as $key => $field) {
1041 if (is_array($field)) {
1042 $isAddressCustomField = FALSE;
1043 foreach ($field as $value) {
1045 if (is_array($value)) {
1046 foreach ($value as $name => $testForEmpty) {
1047 if ($addressCustomFieldID = CRM_Core_BAO_CustomField
::getKeyID($name)) {
1048 $isAddressCustomField = TRUE;
1051 // check if $value does not contain IM provider or phoneType
1052 if (($name !== 'phone_type_id' ||
$name !== 'provider_id') && ($testForEmpty === '' ||
$testForEmpty == NULL)) {
1063 if (!empty($value['location_type_id'])) {
1064 $this->formatLocationBlock($value, $formatted);
1067 // @todo - this is still reachable - e.g. import with related contact info like firstname,lastname,spouse-first-name,spouse-last-name,spouse-home-phone
1068 CRM_Core_Error
::deprecatedFunctionWarning('this is not expected to be reachable now');
1069 $this->formatContactParameters($value, $formatted);
1073 if (!$isAddressCustomField) {
1082 if (($key !== 'preferred_communication_method') && (array_key_exists($key, $contactFields))) {
1083 // due to merging of individual table and
1084 // contact table, we need to avoid
1085 // preferred_communication_method forcefully
1086 $formatValues['contact_type'] = $formatted['contact_type'];
1089 if ($key == 'id' && isset($field)) {
1090 $formatted[$key] = $field;
1092 $this->formatContactParameters($formatValues, $formatted);
1094 //Handling Custom Data
1095 // note: Address custom fields will be handled separately inside formatContactParameters
1096 if (($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) &&
1097 array_key_exists($customFieldID, $customFields) &&
1098 !array_key_exists($customFieldID, $addressCustomFields)
1101 $extends = $customFields[$customFieldID]['extends'] ??
NULL;
1102 $htmlType = $customFields[$customFieldID]['html_type'] ??
NULL;
1103 $dataType = $customFields[$customFieldID]['data_type'] ??
NULL;
1104 $serialized = CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID]);
1106 if (!$serialized && in_array($htmlType, ['Select', 'Radio', 'Autocomplete-Select']) && in_array($dataType, ['String', 'Int'])) {
1107 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1108 foreach ($customOption as $customValue) {
1109 $val = $customValue['value'] ??
NULL;
1110 $label = strtolower($customValue['label'] ??
'');
1111 $value = strtolower(trim($formatted[$key]));
1112 if (($value == $label) ||
($value == strtolower($val))) {
1113 $params[$key] = $formatted[$key] = $val;
1117 elseif ($serialized && !empty($formatted[$key]) && !empty($params[$key])) {
1118 $mulValues = explode(',', $formatted[$key]);
1119 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1120 $formatted[$key] = [];
1122 foreach ($mulValues as $v1) {
1123 foreach ($customOption as $v2) {
1124 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
1125 (strtolower($v2['value']) == strtolower(trim($v1)))
1127 if ($htmlType == 'CheckBox') {
1128 $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1;
1131 $params[$key][] = $formatted[$key][] = $v2['value'];
1140 if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) && array_key_exists($customFieldID, $customFields) &&
1141 !array_key_exists($customFieldID, $addressCustomFields)
1143 // @todo calling api functions directly is not supported
1144 _civicrm_api3_custom_format_params($params, $formatted, $extends);
1147 // to check if not update mode and unset the fields with empty value.
1148 if (!$this->_updateWithId
&& array_key_exists('custom', $formatted)) {
1149 foreach ($formatted['custom'] as $customKey => $customvalue) {
1150 if (empty($formatted['custom'][$customKey][-1]['is_required'])) {
1151 $formatted['custom'][$customKey][-1]['is_required'] = $customFields[$customKey]['is_required'];
1153 $emptyValue = $customvalue[-1]['value'] ??
NULL;
1154 if (!isset($emptyValue)) {
1155 unset($formatted['custom'][$customKey]);
1160 // parse street address, CRM-5450
1161 if ($this->_parseStreetAddress
) {
1162 if (array_key_exists('address', $formatted) && is_array($formatted['address'])) {
1163 foreach ($formatted['address'] as $instance => & $address) {
1164 $streetAddress = $address['street_address'] ??
NULL;
1165 if (empty($streetAddress)) {
1168 // parse address field.
1169 $parsedFields = CRM_Core_BAO_Address
::parseStreetAddress($streetAddress);
1171 //street address consider to be parsed properly,
1172 //If we get street_name and street_number.
1173 if (empty($parsedFields['street_name']) ||
empty($parsedFields['street_number'])) {
1174 $parsedFields = array_fill_keys(array_keys($parsedFields), '');
1177 // merge parse address w/ main address block.
1178 $address = array_merge($address, $parsedFields);
1185 * Get the array of successfully imported contact id's
1189 public function getImportedContacts() {
1190 return $this->_newContacts
;
1194 * Get the array of successfully imported related contact id's
1198 public function &getRelatedImportedContacts() {
1199 return $this->_newRelatedContacts
;
1203 * The initializer code, called before the processing.
1205 public function fini() {
1209 * Check if an error in custom data.
1211 * @param array $params
1212 * @param string $errorMessage
1213 * A string containing all the error-fields.
1215 * @param null $csType
1216 * @param null $relationships
1218 public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL, $relationships = NULL) {
1219 $dateType = CRM_Core_Session
::singleton()->get("dateTypes");
1221 if (!empty($params['contact_sub_type'])) {
1222 $csType = $params['contact_sub_type'] ??
NULL;
1225 if (empty($params['contact_type'])) {
1226 $params['contact_type'] = 'Individual';
1229 // get array of subtypes - CRM-18708
1230 if (in_array($csType, CRM_Contact_BAO_ContactType
::basicTypes(TRUE), TRUE)) {
1231 $csType = self
::getSubtypes($params['contact_type']);
1234 if (is_array($csType)) {
1235 // fetch custom fields for every subtype and add it to $customFields array
1238 foreach ($csType as $cType) {
1239 $customFields +
= CRM_Core_BAO_CustomField
::getFields($params['contact_type'], FALSE, FALSE, $cType);
1243 $customFields = CRM_Core_BAO_CustomField
::getFields($params['contact_type'], FALSE, FALSE, $csType);
1246 $addressCustomFields = CRM_Core_BAO_CustomField
::getFields('Address');
1247 $customFields = $customFields +
$addressCustomFields;
1248 foreach ($params as $key => $value) {
1249 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1250 /* check if it's a valid custom field id */
1252 if (!array_key_exists($customFieldID, $customFields)) {
1253 self
::addToErrorMsg(ts('field ID'), $errorMessage);
1255 // validate null values for required custom fields of type boolean
1256 if (!empty($customFields[$customFieldID]['is_required']) && (empty($params['custom_' . $customFieldID]) && !is_numeric($params['custom_' . $customFieldID])) && $customFields[$customFieldID]['data_type'] == 'Boolean') {
1257 self
::addToErrorMsg($customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'], $errorMessage);
1260 //For address custom fields, we do get actual custom field value as an inner array of
1261 //values so need to modify
1262 if (array_key_exists($customFieldID, $addressCustomFields)) {
1263 $value = $value[0][$key];
1265 /* validate the data against the CF type */
1268 $dataType = $customFields[$customFieldID]['data_type'];
1269 $htmlType = $customFields[$customFieldID]['html_type'];
1270 $isSerialized = CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID]);
1271 if ($dataType == 'Date') {
1272 if (array_key_exists($customFieldID, $addressCustomFields) && CRM_Utils_Date
::convertToDefaultDate($params[$key][0], $dateType, $key)) {
1273 $value = $params[$key][0][$key];
1275 elseif (CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $key)) {
1276 $value = $params[$key];
1279 self
::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1282 elseif ($dataType == 'Boolean') {
1283 if (CRM_Utils_String
::strtoboolstr($value) === FALSE) {
1284 self
::addToErrorMsg($customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'], $errorMessage);
1287 // need not check for label filed import
1288 $selectHtmlTypes = [
1293 if ((!$isSerialized && !in_array($htmlType, $selectHtmlTypes)) ||
$dataType == 'Boolean' ||
$dataType == 'ContactReference') {
1294 $valid = CRM_Core_BAO_CustomValue
::typecheck($dataType, $value);
1296 self
::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1300 // check for values for custom fields for checkboxes and multiselect
1301 if ($isSerialized && $dataType != 'ContactReference') {
1302 $value = trim($value);
1303 $value = str_replace('|', ',', $value);
1304 $mulValues = explode(',', $value);
1305 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1306 foreach ($mulValues as $v1) {
1307 if (strlen($v1) == 0) {
1312 foreach ($customOption as $v2) {
1313 if ((strtolower(trim($v2['label'])) == strtolower(trim($v1))) ||
(strtolower(trim($v2['value'])) == strtolower(trim($v1)))) {
1319 self
::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1323 elseif ($htmlType == 'Select' ||
($htmlType == 'Radio' && $dataType != 'Boolean')) {
1324 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1326 foreach ($customOption as $v2) {
1327 if ((strtolower(trim($v2['label'])) == strtolower(trim($value))) ||
(strtolower(trim($v2['value'])) == strtolower(trim($value)))) {
1332 self
::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1335 elseif ($isSerialized && $dataType === 'StateProvince') {
1336 $mulValues = explode(',', $value);
1337 foreach ($mulValues as $stateValue) {
1339 if (self
::in_value(trim($stateValue), CRM_Core_PseudoConstant
::stateProvinceAbbreviation()) || self
::in_value(trim($stateValue), CRM_Core_PseudoConstant
::stateProvince())) {
1343 self
::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1348 elseif ($isSerialized && $dataType == 'Country') {
1349 $mulValues = explode(',', $value);
1350 foreach ($mulValues as $countryValue) {
1351 if ($countryValue) {
1352 CRM_Core_PseudoConstant
::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
1353 CRM_Core_PseudoConstant
::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
1354 $limitCodes = CRM_Core_BAO_Country
::countryLimit();
1362 if (in_array(trim($countryValue), $values)) {
1369 self
::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1376 elseif (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
1378 //supporting custom data of related contact subtypes
1380 if ($relationships) {
1381 if (array_key_exists($key, $relationships)) {
1384 elseif (CRM_Utils_Array
::key($key, $relationships)) {
1385 $relation = CRM_Utils_Array
::key($key, $relationships);
1388 if (!empty($relation)) {
1389 [$id, $first, $second] = CRM_Utils_System
::explode('_', $relation, 3);
1390 $direction = "contact_sub_type_$second";
1391 $relationshipType = new CRM_Contact_BAO_RelationshipType();
1392 $relationshipType->id
= $id;
1393 if ($relationshipType->find(TRUE)) {
1394 if (isset($relationshipType->$direction)) {
1395 $params[$key]['contact_sub_type'] = $relationshipType->$direction;
1400 self
::isErrorInCustomData($params[$key], $errorMessage, $csType, $relationships);
1406 * Check if value present in all genders or.
1407 * as a substring of any gender value, if yes than return corresponding gender.
1408 * eg value might be m/M, ma/MA, mal/MAL, male return 'Male'
1409 * but if value is 'maleabc' than return false
1411 * @param string $gender
1412 * Check this value across gender values.
1414 * retunr gender value / false
1418 public function checkGender($gender) {
1419 $gender = trim($gender, '.');
1424 $allGenders = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'gender_id');
1425 foreach ($allGenders as $key => $value) {
1426 if (strlen($gender) > strlen($value)) {
1429 if ($gender == $value) {
1432 if (substr_compare($value, $gender, 0, strlen($gender), TRUE) === 0) {
1441 * Check if an error in Core( non-custom fields ) field
1443 * @param array $params
1444 * @param string $errorMessage
1445 * A string containing all the error-fields.
1447 public function isErrorInCoreData($params, &$errorMessage) {
1448 foreach ($params as $key => $value) {
1450 $session = CRM_Core_Session
::singleton();
1451 $dateType = $session->get("dateTypes");
1455 if (CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $key)) {
1456 if (!CRM_Utils_Rule
::date($params[$key])) {
1457 self
::addToErrorMsg(ts('Birth Date'), $errorMessage);
1461 self
::addToErrorMsg(ts('Birth-Date'), $errorMessage);
1465 case 'deceased_date':
1466 if (CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $key)) {
1467 if (!CRM_Utils_Rule
::date($params[$key])) {
1468 self
::addToErrorMsg(ts('Deceased Date'), $errorMessage);
1472 self
::addToErrorMsg(ts('Deceased Date'), $errorMessage);
1477 if (CRM_Utils_String
::strtoboolstr($value) === FALSE) {
1478 self
::addToErrorMsg(ts('Deceased'), $errorMessage);
1483 if (!self
::checkGender($value)) {
1484 self
::addToErrorMsg(ts('Gender'), $errorMessage);
1488 case 'preferred_communication_method':
1490 $preffComm = explode(',', $value);
1491 foreach ($preffComm as $v) {
1492 if (!self
::in_value(trim($v), CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'preferred_communication_method'))) {
1493 self
::addToErrorMsg(ts('Preferred Communication Method'), $errorMessage);
1498 case 'preferred_mail_format':
1499 if (!array_key_exists(strtolower($value), array_change_key_case(CRM_Core_SelectValues
::pmf(), CASE_LOWER
))) {
1500 self
::addToErrorMsg(ts('Preferred Mail Format'), $errorMessage);
1504 case 'individual_prefix':
1506 if (!self
::in_value($value, CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'prefix_id'))) {
1507 self
::addToErrorMsg(ts('Individual Prefix'), $errorMessage);
1511 case 'individual_suffix':
1513 if (!self
::in_value($value, CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'suffix_id'))) {
1514 self
::addToErrorMsg(ts('Individual Suffix'), $errorMessage);
1518 case 'state_province':
1519 if (!empty($value)) {
1520 foreach ($value as $stateValue) {
1521 if ($stateValue['state_province']) {
1522 if (self
::in_value($stateValue['state_province'], CRM_Core_PseudoConstant
::stateProvinceAbbreviation()) ||
1523 self
::in_value($stateValue['state_province'], CRM_Core_PseudoConstant
::stateProvince())
1528 self
::addToErrorMsg(ts('State/Province'), $errorMessage);
1536 if (!empty($value)) {
1537 foreach ($value as $stateValue) {
1538 if ($stateValue['country']) {
1539 CRM_Core_PseudoConstant
::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
1540 CRM_Core_PseudoConstant
::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
1541 $limitCodes = CRM_Core_BAO_Country
::countryLimit();
1542 //If no country is selected in
1543 //localization then take all countries
1544 if (empty($limitCodes)) {
1545 $limitCodes = $countryIsoCodes;
1548 if (self
::in_value($stateValue['country'], $limitCodes) || self
::in_value($stateValue['country'], CRM_Core_PseudoConstant
::country())) {
1551 if (self
::in_value($stateValue['country'], $countryIsoCodes) || self
::in_value($stateValue['country'], $countryNames)) {
1552 self
::addToErrorMsg(ts('Country input value is in table but not "available": "This Country is valid but is NOT in the list of Available Countries currently configured for your site. This can be viewed and modifed from Administer > Localization > Languages Currency Locations." '), $errorMessage);
1555 self
::addToErrorMsg(ts('Country input value not in country table: "The Country value appears to be invalid. It does not match any value in CiviCRM table of countries."'), $errorMessage);
1563 if (!empty($value)) {
1564 foreach ($value as $county) {
1565 if ($county['county']) {
1566 $countyNames = CRM_Core_PseudoConstant
::county();
1567 if (!empty($county['county']) && !in_array($county['county'], $countyNames)) {
1568 self
::addToErrorMsg(ts('County input value not in county table: The County value appears to be invalid. It does not match any value in CiviCRM table of counties.'), $errorMessage);
1576 if (!empty($value)) {
1577 foreach ($value as $codeValue) {
1578 if (!empty($codeValue['geo_code_1'])) {
1579 if (CRM_Utils_Rule
::numeric($codeValue['geo_code_1'])) {
1582 self
::addToErrorMsg(ts('Geo code 1'), $errorMessage);
1589 if (!empty($value)) {
1590 foreach ($value as $codeValue) {
1591 if (!empty($codeValue['geo_code_2'])) {
1592 if (CRM_Utils_Rule
::numeric($codeValue['geo_code_2'])) {
1595 self
::addToErrorMsg(ts('Geo code 2'), $errorMessage);
1601 //check for any error in email/postal greeting, addressee,
1602 //custom email/postal greeting, custom addressee, CRM-4575
1604 case 'email_greeting':
1605 $emailGreetingFilter = [
1606 'contact_type' => $this->_contactType
,
1607 'greeting_type' => 'email_greeting',
1609 if (!self
::in_value($value, CRM_Core_PseudoConstant
::greeting($emailGreetingFilter))) {
1610 self
::addToErrorMsg(ts('Email Greeting must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Email Greetings for valid values'), $errorMessage);
1614 case 'postal_greeting':
1615 $postalGreetingFilter = [
1616 'contact_type' => $this->_contactType
,
1617 'greeting_type' => 'postal_greeting',
1619 if (!self
::in_value($value, CRM_Core_PseudoConstant
::greeting($postalGreetingFilter))) {
1620 self
::addToErrorMsg(ts('Postal Greeting must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Postal Greetings for valid values'), $errorMessage);
1625 $addresseeFilter = [
1626 'contact_type' => $this->_contactType
,
1627 'greeting_type' => 'addressee',
1629 if (!self
::in_value($value, CRM_Core_PseudoConstant
::greeting($addresseeFilter))) {
1630 self
::addToErrorMsg(ts('Addressee must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Addressee for valid values'), $errorMessage);
1634 case 'email_greeting_custom':
1635 if (array_key_exists('email_greeting', $params)) {
1636 $emailGreetingLabel = key(CRM_Core_OptionGroup
::values('email_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1637 if (CRM_Utils_Array
::value('email_greeting', $params) != $emailGreetingLabel) {
1638 self
::addToErrorMsg(ts('Email Greeting - Custom'), $errorMessage);
1643 case 'postal_greeting_custom':
1644 if (array_key_exists('postal_greeting', $params)) {
1645 $postalGreetingLabel = key(CRM_Core_OptionGroup
::values('postal_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1646 if (CRM_Utils_Array
::value('postal_greeting', $params) != $postalGreetingLabel) {
1647 self
::addToErrorMsg(ts('Postal Greeting - Custom'), $errorMessage);
1652 case 'addressee_custom':
1653 if (array_key_exists('addressee', $params)) {
1654 $addresseeLabel = key(CRM_Core_OptionGroup
::values('addressee', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1655 if (CRM_Utils_Array
::value('addressee', $params) != $addresseeLabel) {
1656 self
::addToErrorMsg(ts('Addressee - Custom'), $errorMessage);
1662 if (is_array($value)) {
1663 foreach ($value as $values) {
1664 if (!empty($values['url']) && !CRM_Utils_Rule
::url($values['url'])) {
1665 self
::addToErrorMsg(ts('Website'), $errorMessage);
1672 case 'do_not_email':
1673 case 'do_not_phone':
1676 case 'do_not_trade':
1677 if (CRM_Utils_Rule
::boolean($value) == FALSE) {
1678 $key = ucwords(str_replace("_", " ", $key));
1679 self
::addToErrorMsg($key, $errorMessage);
1684 if (is_array($value)) {
1685 foreach ($value as $values) {
1686 if (!empty($values['email']) && !CRM_Utils_Rule
::email($values['email'])) {
1687 self
::addToErrorMsg($key, $errorMessage);
1695 if (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
1696 //check for any relationship data ,FIX ME
1697 self
::isErrorInCoreData($params[$key], $errorMessage);
1705 * Ckeck a value present or not in a array.
1708 * @param $valueArray
1712 public static function in_value($value, $valueArray) {
1713 foreach ($valueArray as $key => $v) {
1715 if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) {
1723 * Build error-message containing error-fields
1725 * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function.
1727 * @todo just say no!
1729 * @param string $errorName
1730 * A string containing error-field name.
1731 * @param string $errorMessage
1732 * A string containing all the error-fields, where the new errorName is concatenated.
1735 public static function addToErrorMsg($errorName, &$errorMessage) {
1736 if ($errorMessage) {
1737 $errorMessage .= "; $errorName";
1740 $errorMessage = $errorName;
1745 * Method for creating contact.
1747 * @param array $formatted
1748 * @param array $contactFields
1749 * @param int $onDuplicate
1750 * @param int $contactId
1751 * @param bool $requiredCheck
1752 * @param int $dedupeRuleGroupID
1754 * @return array|bool|\CRM_Contact_BAO_Contact|\CRM_Core_Error|null
1756 public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) {
1760 if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser
::DUPLICATE_NOCHECK
)) {
1761 $dupeCheck = (bool) ($onDuplicate);
1764 //get the prefix id etc if exists
1765 CRM_Contact_BAO_Contact
::resolveDefaults($formatted, TRUE);
1767 //@todo direct call to API function not supported.
1768 // setting required check to false, CRM-2839
1769 // plus we do our own required check in import
1771 $error = $this->deprecated_contact_check_params($formatted, $dupeCheck, $dedupeRuleGroupID);
1775 $this->deprecated_validate_formatted_contact($formatted);
1777 catch (CRM_Core_Exception
$e) {
1778 return ['error_message' => $e->getMessage(), 'is_error' => 1, 'code' => $e->getCode()];
1782 $this->formatParams($formatted, $onDuplicate, (int) $contactId);
1785 // Resetting and rebuilding cache could be expensive.
1786 CRM_Core_Config
::setPermitCacheFlushMode(FALSE);
1788 // If a user has logged in, or accessed via a checksum
1789 // Then deliberately 'blanking' a value in the profile should remove it from their record
1790 // @todo this should either be TRUE or FALSE in the context of import - once
1791 // we figure out which we can remove all the rest.
1792 // Also note the meaning of this parameter is less than it used to
1793 // be following block cleanup.
1794 $formatted['updateBlankLocInfo'] = TRUE;
1795 if ((CRM_Core_Session
::singleton()->get('authSrc') & (CRM_Core_Permission
::AUTH_SRC_CHECKSUM + CRM_Core_Permission
::AUTH_SRC_LOGIN
)) == 0) {
1796 $formatted['updateBlankLocInfo'] = FALSE;
1799 [$data, $contactDetails] = CRM_Contact_BAO_Contact
::formatProfileContactParams($formatted, $contactFields, $contactId, NULL, $formatted['contact_type']);
1801 // manage is_opt_out
1802 if (array_key_exists('is_opt_out', $contactFields) && array_key_exists('is_opt_out', $formatted)) {
1803 $wasOptOut = $contactDetails['is_opt_out'] ??
FALSE;
1804 $isOptOut = $formatted['is_opt_out'];
1805 $data['is_opt_out'] = $isOptOut;
1806 // on change, create new civicrm_subscription_history entry
1807 if (($wasOptOut != $isOptOut) && !empty($contactDetails['contact_id'])) {
1809 'contact_id' => $contactDetails['contact_id'],
1810 'status' => $isOptOut ?
'Removed' : 'Added',
1813 CRM_Contact_BAO_SubscriptionHistory
::create($shParams);
1817 $contact = civicrm_api3('Contact', 'create', $data);
1818 $cid = $contact['id'];
1820 CRM_Core_Config
::setPermitCacheFlushMode(TRUE);
1823 'contact_id' => $cid,
1827 $newContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
1829 //get the id of the contact whose street address is not parsable, CRM-5886
1830 if ($this->_parseStreetAddress
&& is_object($newContact) && property_exists($newContact, 'address') && $newContact->address
) {
1831 foreach ($newContact->address
as $address) {
1832 if (!empty($address['street_address']) && (empty($address['street_number']) ||
empty($address['street_name']))) {
1833 $this->_unparsedStreetAddressContacts
[] = [
1834 'id' => $newContact->id
,
1835 'streetAddress' => $address['street_address'],
1844 * Format params for update and fill mode.
1846 * @param array $params
1847 * reference to an array containing all the.
1849 * @param int $onDuplicate
1853 public function formatParams(&$params, $onDuplicate, $cid) {
1854 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
) {
1859 'contact_id' => $cid,
1863 $contactObj = CRM_Contact_BAO_Contact
::retrieve($contactParams, $defaults);
1865 $modeFill = ($onDuplicate == CRM_Import_Parser
::DUPLICATE_FILL
);
1867 $groupTree = CRM_Core_BAO_CustomGroup
::getTree($params['contact_type'], NULL, $cid, 0, NULL);
1868 CRM_Core_BAO_CustomGroup
::setDefaults($groupTree, $defaults, FALSE, FALSE);
1874 'website' => 'website',
1875 'address' => 'address',
1878 $contact = get_object_vars($contactObj);
1880 foreach ($params as $key => $value) {
1881 if ($key == 'id' ||
$key == 'contact_type') {
1885 if (array_key_exists($key, $locationFields)) {
1888 if (in_array($key, [
1893 // CRM-4575, need to null custom
1894 if ($params["{$key}_id"] != 4) {
1895 $params["{$key}_custom"] = 'null';
1897 unset($params[$key]);
1900 if ($customFieldId = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1901 $custom_params = ['id' => $contact['id'], 'return' => $key];
1902 $getValue = civicrm_api3('Contact', 'getvalue', $custom_params);
1903 if (empty($getValue)) {
1908 $getValue = CRM_Utils_Array
::retrieveValueRecursive($contact, $key);
1910 if ($key == 'contact_source') {
1911 $params['source'] = $params[$key];
1912 unset($params[$key]);
1915 if ($modeFill && isset($getValue)) {
1916 unset($params[$key]);
1917 if ($customFieldId) {
1918 // Extra values must be unset to ensure the values are not
1920 unset($params['custom'][$customFieldId]);
1926 foreach ($locationFields as $locKeys) {
1927 if (isset($params[$locKeys]) && is_array($params[$locKeys])) {
1928 foreach ($params[$locKeys] as $key => $value) {
1930 $getValue = CRM_Utils_Array
::retrieveValueRecursive($contact, $locKeys);
1932 if (isset($getValue)) {
1933 foreach ($getValue as $cnt => $values) {
1934 if ($locKeys == 'website') {
1935 if (($getValue[$cnt]['website_type_id'] == $params[$locKeys][$key]['website_type_id'])) {
1936 unset($params[$locKeys][$key]);
1940 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']) {
1941 unset($params[$locKeys][$key]);
1948 if (count($params[$locKeys]) == 0) {
1949 unset($params[$locKeys]);
1956 * Convert any given date string to default date array.
1958 * @param array $params
1959 * Has given date-format.
1960 * @param array $formatted
1961 * Store formatted date in this array.
1962 * @param int $dateType
1964 * @param string $dateParam
1967 public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) {
1969 CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $dateParam);
1970 $formatted[$dateParam] = CRM_Utils_Date
::processDate($params[$dateParam]);
1974 * Generate status and error message for unparsed street address records.
1976 * @param array $values
1977 * The array of values belonging to each row.
1978 * @param array $statusFieldName
1979 * Store formatted date in this array.
1980 * @param $returnCode
1984 public function processMessage(&$values, $statusFieldName, $returnCode) {
1985 if (empty($this->_unparsedStreetAddressContacts
)) {
1986 $importRecordParams = [
1987 $statusFieldName => 'IMPORTED',
1991 $errorMessage = ts("Record imported successfully but unable to parse the street address: ");
1992 foreach ($this->_unparsedStreetAddressContacts
as $contactInfo => $contactValue) {
1993 $contactUrl = CRM_Utils_System
::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contactValue['id'], TRUE, NULL, FALSE);
1994 $errorMessage .= "\n Contact ID:" . $contactValue['id'] . " <a href=\"$contactUrl\"> " . $contactValue['streetAddress'] . "</a>";
1996 array_unshift($values, $errorMessage);
1997 $importRecordParams = [
1998 $statusFieldName => 'ERROR',
1999 "${statusFieldName}Msg" => $errorMessage,
2001 $returnCode = CRM_Import_Parser
::UNPARSED_ADDRESS_WARNING
;
2003 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2009 * @param array $params
2013 public function checkRelatedContactFields($relKey, $params) {
2014 //avoid blank contact creation.
2015 $allowToCreate = FALSE;
2017 //build the mapper field array.
2018 static $relatedContactFields = [];
2019 if (!isset($relatedContactFields[$relKey])) {
2020 foreach ($this->_mapperRelated
as $key => $name) {
2025 if (!empty($relatedContactFields[$name]) && !is_array($relatedContactFields[$name])) {
2026 $relatedContactFields[$name] = [];
2028 $fldName = $this->_mapperRelatedContactDetails
[$key] ??
NULL;
2029 if ($fldName == 'url') {
2030 $fldName = 'website';
2033 $relatedContactFields[$name][] = $fldName;
2038 //validate for passed data.
2039 if (is_array($relatedContactFields[$relKey])) {
2040 foreach ($relatedContactFields[$relKey] as $fld) {
2041 if (!empty($params[$fld])) {
2042 $allowToCreate = TRUE;
2048 return $allowToCreate;
2052 * get subtypes given the contact type
2054 * @param string $contactType
2055 * @return array $subTypes
2057 public static function getSubtypes($contactType) {
2059 $types = CRM_Contact_BAO_ContactType
::subTypeInfo($contactType);
2061 if (count($types) > 0) {
2062 foreach ($types as $type) {
2063 $subTypes[] = $type['name'];
2070 * Get the possible contact matches.
2072 * 1) the chosen dedupe rule falling back to
2073 * 2) a check for the external ID.
2075 * @see https://issues.civicrm.org/jira/browse/CRM-17275
2077 * @param array $params
2080 * IDs of possible matches.
2082 * @throws \CRM_Core_Exception
2083 * @throws \CiviCRM_API3_Exception
2085 protected function getPossibleContactMatches($params) {
2088 if (!empty($params['external_identifier'])) {
2089 // Check for any match on external id, deleted or otherwise.
2090 $extIDContact = civicrm_api3('Contact', 'get', [
2091 'external_identifier' => $params['external_identifier'],
2093 'return' => ['id', 'contact_is_deleted'],
2095 if (isset($extIDContact['id'])) {
2096 $extIDMatch = $extIDContact['id'];
2098 if ($extIDContact['values'][$extIDMatch]['contact_is_deleted'] == 1) {
2099 // If the contact is deleted, update external identifier to be blank
2100 // to avoid key error from MySQL.
2101 $params = ['id' => $extIDMatch, 'external_identifier' => ''];
2102 civicrm_api3('Contact', 'create', $params);
2104 // And now it is no longer a match.
2109 $checkParams = ['check_permissions' => FALSE, 'match' => $params];
2110 $checkParams['match']['contact_type'] = $this->_contactType
;
2112 $possibleMatches = civicrm_api3('Contact', 'duplicatecheck', $checkParams);
2114 return array_keys($possibleMatches['values']);
2116 if ($possibleMatches['count']) {
2117 if (array_key_exists($extIDMatch, $possibleMatches['values'])) {
2118 return [$extIDMatch];
2120 throw new CRM_Core_Exception(ts(
2121 'Matching this contact based on the de-dupe rule would cause an external ID conflict'));
2123 return [$extIDMatch];
2127 * Format the form mapping parameters ready for the parser.
2132 * @return array $parserParameters
2134 public static function getParameterForParser($count) {
2136 for ($i = 0; $i < $count; $i++
) {
2137 $baseArray[$i] = NULL;
2139 $parserParameters['mapperLocType'] = $baseArray;
2140 $parserParameters['mapperPhoneType'] = $baseArray;
2141 $parserParameters['mapperImProvider'] = $baseArray;
2142 $parserParameters['mapperWebsiteType'] = $baseArray;
2143 $parserParameters['mapperRelated'] = $baseArray;
2144 $parserParameters['relatedContactType'] = $baseArray;
2145 $parserParameters['relatedContactDetails'] = $baseArray;
2146 $parserParameters['relatedContactLocType'] = $baseArray;
2147 $parserParameters['relatedContactPhoneType'] = $baseArray;
2148 $parserParameters['relatedContactImProvider'] = $baseArray;
2149 $parserParameters['relatedContactWebsiteType'] = $baseArray;
2151 return $parserParameters;
2156 * Set field metadata.
2158 protected function setFieldMetadata() {
2159 $this->setImportableFieldsMetadata($this->getContactImportMetadata());
2160 // Probably no longer needed but here for now.
2161 $this->_relationships
= $this->getRelationships();
2165 * @param array $newContact
2166 * @param $statusFieldName
2167 * @param array $values
2168 * @param int $onDuplicate
2169 * @param array $formatted
2170 * @param array $contactFields
2174 * @throws \CRM_Core_Exception
2175 * @throws \CiviCRM_API3_Exception
2176 * @throws \Civi\API\Exception\UnauthorizedException
2178 protected function handleDuplicateError(array $newContact, $statusFieldName, array $values, int $onDuplicate, array $formatted, array $contactFields): int {
2180 // need to fix at some stage and decide if the error will return an
2181 // array or string, crude hack for now
2182 if (is_array($newContact['error_message']['params'][0])) {
2183 $cids = $newContact['error_message']['params'][0];
2186 $cids = explode(',', $newContact['error_message']['params'][0]);
2189 foreach ($cids as $cid) {
2190 $urls[] = CRM_Utils_System
::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE);
2193 $url_string = implode("\n", $urls);
2195 // If we duplicate more than one record, skip no matter what
2196 if (count($cids) > 1) {
2197 $errorMessage = ts('Record duplicates multiple contacts');
2198 $importRecordParams = [
2199 $statusFieldName => 'ERROR',
2200 "${statusFieldName}Msg" => $errorMessage,
2203 //combine error msg to avoid mismatch between error file columns.
2204 $errorMessage .= "\n" . $url_string;
2205 array_unshift($values, $errorMessage);
2206 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2207 return CRM_Import_Parser
::ERROR
;
2210 // Params only had one id, so shift it out
2211 $contactId = array_shift($cids);
2214 $vals = ['contact_id' => $contactId];
2216 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_REPLACE
) {
2217 civicrm_api('contact', 'delete', $vals);
2218 $cid = CRM_Contact_BAO_Contact
::createProfileContact($formatted, $contactFields, $contactId, NULL, NULL, $formatted['contact_type']);
2220 if (in_array((int) $onDuplicate, [CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::DUPLICATE_FILL
], TRUE)) {
2221 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactId);
2223 // else skip does nothing and just returns an error code.
2226 'contact_id' => $cid,
2229 $newContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
2232 if (civicrm_error($newContact)) {
2233 if (empty($newContact['error_message']['params'])) {
2234 // different kind of error other than DUPLICATE
2235 $errorMessage = $newContact['error_message'];
2236 array_unshift($values, $errorMessage);
2237 $importRecordParams = [
2238 $statusFieldName => 'ERROR',
2239 "${statusFieldName}Msg" => $errorMessage,
2241 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2242 return CRM_Import_Parser
::ERROR
;
2245 $contactID = $newContact['error_message']['params'][0];
2246 if (is_array($contactID)) {
2247 $contactID = array_pop($contactID);
2249 if (!in_array($contactID, $this->_newContacts
)) {
2250 $this->_newContacts
[] = $contactID;
2253 //CRM-262 No Duplicate Checking
2254 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
) {
2255 array_unshift($values, $url_string);
2256 $importRecordParams = [
2257 $statusFieldName => 'DUPLICATE',
2258 "${statusFieldName}Msg" => "Skipping duplicate record",
2260 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2261 return CRM_Import_Parser
::DUPLICATE
;
2264 $importRecordParams = [
2265 $statusFieldName => 'IMPORTED',
2267 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2268 //return warning if street address is not parsed, CRM-5886
2269 return $this->processMessage($values, $statusFieldName, CRM_Import_Parser
::VALID
);
2273 * Validate a formatted contact parameter list.
2275 * @param array $params
2276 * Structured parameter list (as in crm_format_params).
2278 * @throw CRM_Core_Error
2280 public function deprecated_validate_formatted_contact(&$params): void
{
2281 // Look for offending email addresses
2283 if (array_key_exists('email', $params)) {
2284 foreach ($params['email'] as $count => $values) {
2285 if (!is_array($values)) {
2288 if ($email = CRM_Utils_Array
::value('email', $values)) {
2289 // validate each email
2290 if (!CRM_Utils_Rule
::email($email)) {
2291 throw new CRM_Core_Exception('No valid email address');
2294 // check for loc type id.
2295 if (empty($values['location_type_id'])) {
2296 throw new CRM_Core_Exception('Location Type Id missing.');
2302 // Validate custom data fields
2303 if (array_key_exists('custom', $params) && is_array($params['custom'])) {
2304 foreach ($params['custom'] as $key => $custom) {
2305 if (is_array($custom)) {
2306 foreach ($custom as $fieldId => $value) {
2307 $valid = CRM_Core_BAO_CustomValue
::typecheck(CRM_Utils_Array
::value('type', $value),
2308 CRM_Utils_Array
::value('value', $value)
2310 if (!$valid && $value['is_required']) {
2311 throw new CRM_Core_Exception('Invalid value for custom field \'' .
2312 $custom['name'] . '\''
2315 if (CRM_Utils_Array
::value('type', $custom) == 'Date') {
2316 $params['custom'][$key][$fieldId]['value'] = str_replace('-', '', $params['custom'][$key][$fieldId]['value']);
2325 * @param array $params
2326 * @param bool $dupeCheck
2327 * @param null|int $dedupeRuleGroupID
2329 * @throws \CRM_Core_Exception
2331 public function deprecated_contact_check_params(
2334 $dedupeRuleGroupID = NULL) {
2336 $requiredCheck = TRUE;
2338 if (isset($params['id']) && is_numeric($params['id'])) {
2339 $requiredCheck = FALSE;
2341 if ($requiredCheck) {
2344 ['first_name', 'last_name'],
2351 'organization_name',
2355 // contact_type has a limited number of valid values
2356 if (empty($params['contact_type'])) {
2357 throw new CRM_Core_Exception("No Contact Type");
2359 $fields = $required[$params['contact_type']] ??
NULL;
2360 if ($fields == NULL) {
2361 throw new CRM_Core_Exception("Invalid Contact Type: {$params['contact_type']}");
2364 if ($csType = CRM_Utils_Array
::value('contact_sub_type', $params)) {
2365 if (!(CRM_Contact_BAO_ContactType
::isExtendsContactType($csType, $params['contact_type']))) {
2366 throw new CRM_Core_Exception("Invalid or Mismatched Contact Subtype: " . implode(', ', (array) $csType));
2370 if (empty($params['contact_id']) && !empty($params['id'])) {
2373 foreach ($fields as $field) {
2374 if (is_array($field)) {
2376 foreach ($field as $element) {
2377 if (empty($params[$element])) {
2385 if (!empty($params[$field])) {
2395 throw new CRM_Core_Exception("Required fields not found for {$params['contact_type']} : $error");
2401 // @todo switch to using api version
2402 // $dupes = civicrm_api3('Contact', 'duplicatecheck', (array('match' => $params, 'dedupe_rule_id' => $dedupeRuleGroupID)));
2403 // $ids = $dupes['count'] ? implode(',', array_keys($dupes['values'])) : NULL;
2404 $ids = CRM_Contact_BAO_Contact
::getDuplicateContacts($params, $params['contact_type'], 'Unsupervised', [], CRM_Utils_Array
::value('check_permissions', $params), $dedupeRuleGroupID);
2406 $error = CRM_Core_Error
::createError("Found matching contacts: " . implode(',', $ids),
2407 CRM_Core_Error
::DUPLICATE_CONTACT
,
2410 return civicrm_api3_create_error($error->pop());
2414 // check for organisations with same name
2415 if (!empty($params['current_employer'])) {
2416 $organizationParams = ['organization_name' => $params['current_employer']];
2417 $dupeIds = CRM_Contact_BAO_Contact
::getDuplicateContacts($organizationParams, 'Organization', 'Supervised', [], FALSE);
2419 // check for mismatch employer name and id
2420 if (!empty($params['employer_id']) && !in_array($params['employer_id'], $dupeIds)
2422 throw new CRM_Core_Exception('Employer name and Employer id Mismatch');
2425 // show error if multiple organisation with same name exist
2426 if (empty($params['employer_id']) && (count($dupeIds) > 1)
2428 return civicrm_api3_create_error('Found more than one Organisation with same Name.');