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
;
15 require_once 'CRM/Utils/DeprecatedUtils.php';
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 = [
104 * Relationship labels.
106 * Temporary cache of labels to reduce queries in getRelationshipLabels.
109 * e.g ['5a_b' => 'Employer', '5b_a' => 'Employee']
111 protected $relationshipLabels = [];
118 public $_onDuplicate;
121 * Dedupe rule group id to use if set
125 public $_dedupeRuleGroupID = NULL;
130 * @param array $mapperKeys
132 public function __construct($mapperKeys = []) {
133 parent
::__construct();
134 $this->_mapperKeys
= $mapperKeys;
138 * @param $customFieldID
139 * @param array $customFields
140 * @param array $params
146 private function validateCustomField($customFieldID, array $customFields, array $params, $value, $dateType): ?
string {
147 if (!array_key_exists($customFieldID, $customFields)) {
148 return ts('field ID');
150 $fieldMetaData = $customFields[$customFieldID];
151 // validate null values for required custom fields of type boolean
152 if (!empty($customFields[$customFieldID]['is_required']) && (empty($params['custom_' . $customFieldID]) && !is_numeric($params['custom_' . $customFieldID])) && $customFields[$customFieldID]['data_type'] == 'Boolean') {
153 return $customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'];
156 /* validate the data against the CF type */
159 $dataType = $customFields[$customFieldID]['data_type'];
160 $htmlType = $customFields[$customFieldID]['html_type'];
161 $isSerialized = CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID]);
162 if ($dataType === 'Date') {
163 $params = ['date_field' => $value];
164 if (CRM_Utils_Date
::convertToDefaultDate($params, $dateType, 'date_field')) {
167 return $fieldMetaData['label'];
169 elseif ($dataType == 'Boolean') {
170 if (CRM_Utils_String
::strtoboolstr($value) === FALSE) {
171 return $customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'];
174 // need not check for label filed import
180 if ((!$isSerialized && !in_array($htmlType, $selectHtmlTypes)) ||
$dataType == 'Boolean' ||
$dataType == 'ContactReference') {
181 $valid = CRM_Core_BAO_CustomValue
::typecheck($dataType, $value);
183 return $customFields[$customFieldID]['label'];
187 // check for values for custom fields for checkboxes and multiselect
188 if ($isSerialized && $dataType != 'ContactReference') {
189 $value = trim($value);
190 $value = str_replace('|', ',', $value);
191 $mulValues = explode(',', $value);
192 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
193 foreach ($mulValues as $v1) {
194 if (strlen($v1) == 0) {
199 foreach ($customOption as $v2) {
200 if ((strtolower(trim($v2['label'])) == strtolower(trim($v1))) ||
(strtolower(trim($v2['value'])) == strtolower(trim($v1)))) {
206 return $customFields[$customFieldID]['label'];
210 elseif ($htmlType == 'Select' ||
($htmlType == 'Radio' && $dataType != 'Boolean')) {
211 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
213 foreach ($customOption as $v2) {
214 if ((strtolower(trim($v2['label'])) == strtolower(trim($value))) ||
(strtolower(trim($v2['value'])) == strtolower(trim($value)))) {
219 return $customFields[$customFieldID]['label'];
222 elseif ($isSerialized && $dataType === 'StateProvince') {
223 $mulValues = explode(',', $value);
224 foreach ($mulValues as $stateValue) {
226 if (self
::in_value(trim($stateValue), CRM_Core_PseudoConstant
::stateProvinceAbbreviation()) || self
::in_value(trim($stateValue), CRM_Core_PseudoConstant
::stateProvince())) {
230 return $customFields[$customFieldID]['label'];
235 elseif ($isSerialized && $dataType == 'Country') {
236 $mulValues = explode(',', $value);
237 foreach ($mulValues as $countryValue) {
239 CRM_Core_PseudoConstant
::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
240 CRM_Core_PseudoConstant
::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
241 $limitCodes = CRM_Core_BAO_Country
::countryLimit();
244 foreach ([$countryNames, $countryIsoCodes, $limitCodes] as $values) {
245 if (in_array(trim($countryValue), $values)) {
252 return $customFields[$customFieldID]['label'];
262 * The initializer code, called before processing.
264 public function init() {
265 $this->setFieldMetadata();
266 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
267 $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));
269 $this->_newContacts
= [];
271 $this->setActiveFields($this->_mapperKeys
);
273 $this->_externalIdentifierIndex
= -1;
276 foreach ($this->_mapperKeys
as $key) {
277 if ($key == 'external_identifier') {
278 $this->_externalIdentifierIndex
= $index;
283 $this->_updateWithId
= FALSE;
284 if (in_array('id', $this->_mapperKeys
) ||
($this->_externalIdentifierIndex
>= 0 && $this->isUpdateExistingContacts())) {
285 $this->_updateWithId
= TRUE;
288 $this->_parseStreetAddress
= CRM_Utils_Array
::value('street_address_parsing', CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
, 'address_options'), FALSE);
292 * Is this a case where the user has opted to update existing contacts.
296 * @throws \API_Exception
298 private function isUpdateExistingContacts(): bool {
299 return in_array((int) $this->getSubmittedValue('onDuplicate'), [
300 CRM_Import_Parser
::DUPLICATE_UPDATE
,
301 CRM_Import_Parser
::DUPLICATE_FILL
,
306 * Gets the fields available for importing in a key-name, title format.
309 * eg. ['first_name' => 'First Name'.....]
311 * @throws \API_Exception
313 * @todo - we are constructing the metadata before we
314 * have set the contact type so we re-do it here.
316 * Once we have cleaned up the way the mapper is handled
317 * we can ditch all the existing _construct parameters in favour
318 * of just the userJobID - there are current open PRs towards this end.
320 public function getAvailableFields(): array {
321 $this->setFieldMetadata();
323 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
324 if ($name === 'id' && $this->isSkipDuplicates()) {
325 // Duplicates are being skipped so id matching is not availble.
328 $return[$name] = $field['title'];
334 * Did the user specify duplicates should be skipped and not imported.
338 * @throws \API_Exception
340 private function isSkipDuplicates(): bool {
341 return ((int) $this->getSubmittedValue('onDuplicate')) === CRM_Import_Parser
::DUPLICATE_SKIP
;
345 * Did the user specify duplicates checking should be skipped, resulting in possible duplicate contacts.
347 * Note we still need to check for external_identifier as it will hard-fail
352 * @throws \API_Exception
354 private function isIgnoreDuplicates(): bool {
355 return ((int) $this->getSubmittedValue('onDuplicate')) === CRM_Import_Parser
::DUPLICATE_NOCHECK
;
359 * Handle the values in preview mode.
361 * Function will be deprecated in favour of validateValues.
363 * @param array $values
364 * The array of values belonging to this line.
367 * the result of this processing
368 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
370 public function preview(&$values) {
371 return $this->summary($values);
375 * Handle the values in summary mode.
377 * Function will be deprecated in favour of validateValues.
379 * @param array $values
380 * The array of values belonging to this line.
383 * the result of this processing
384 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
386 public function summary(&$values): int {
387 $rowNumber = (int) ($values[count($values) - 1]);
389 $this->validateValues($values);
391 catch (CRM_Core_Exception
$e) {
392 $this->setImportStatus($rowNumber, 'ERROR', $e->getMessage());
393 array_unshift($values, $e->getMessage());
394 return CRM_Import_Parser
::ERROR
;
396 $this->setImportStatus($rowNumber, 'NEW', '');
398 return CRM_Import_Parser
::VALID
;
402 * Get Array of all the fields that could potentially be part
407 public function getAllFields() {
408 return $this->_fields
;
412 * Handle the values in import mode.
414 * @param int $onDuplicate
415 * The code for what action to take on duplicates.
416 * @param array $values
417 * The array of values belonging to this line.
420 * the result of this processing
422 * @throws \CiviCRM_API3_Exception
423 * @throws \CRM_Core_Exception
424 * @throws \API_Exception
426 public function import($onDuplicate, &$values) {
427 $this->_unparsedStreetAddressContacts
= [];
428 if (!$this->getSubmittedValue('doGeocodeAddress')) {
429 // CRM-5854, reset the geocode method to null to prevent geocoding
430 CRM_Utils_GeocodeProvider
::disableForSession();
433 // first make sure this is a valid line
434 //$this->_updateWithId = false;
435 $response = $this->summary($values);
437 if ($response != CRM_Import_Parser
::VALID
) {
438 $this->setImportStatus((int) $values[count($values) - 1], 'Invalid', "Invalid (Error Code: $response)");
442 $params = $this->getMappedRow($values);
443 $formatted = array_filter(array_intersect_key($params, array_fill_keys($this->metadataHandledFields
, 1)));
445 $contactFields = CRM_Contact_DAO_Contact
::import();
447 $params['contact_sub_type'] = $this->getContactSubType() ?
: ($params['contact_sub_type'] ??
NULL);
450 $params['id'] = $formatted['id'] = $this->lookupContactID($params, ($this->isSkipDuplicates() ||
$this->isIgnoreDuplicates()));
451 if ($params['id'] && $params['contact_sub_type']) {
452 $contactSubType = Contact
::get(FALSE)
453 ->addWhere('id', '=', $params['id'])
454 ->addSelect('contact_sub_type')
456 ->first()['contact_sub_type'];
457 if (!empty($contactSubType) && $contactSubType[0] !== $params['contact_sub_type'] && !CRM_Contact_BAO_ContactType
::isAllowEdit($params['id'], $contactSubType[0])) {
458 throw new CRM_Core_Exception('Mismatched contact SubTypes :', CRM_Import_Parser
::NO_MATCH
);
462 catch (CRM_Core_Exception
$e) {
463 $statuses = [CRM_Import_Parser
::DUPLICATE
=> 'DUPLICATE', CRM_Import_Parser
::ERROR
=> 'ERROR', CRM_Import_Parser
::NO_MATCH
=> 'invalid_no_match'];
464 $this->setImportStatus((int) $values[count($values) - 1], $statuses[$e->getErrorCode()], $e->getMessage());
468 // Get contact id to format common data in update/fill mode,
469 // prioritising a dedupe rule check over an external_identifier check, but falling back on ext id.
471 //format common data, CRM-4062
472 $this->formatCommonData($params, $formatted, $contactFields);
474 $relationship = FALSE;
475 $createNewContact = TRUE;
476 // Support Match and Update Via Contact ID
477 if ($this->_updateWithId
&& isset($params['id'])) {
478 $createNewContact = FALSE;
482 //now we create new contact in update/fill mode also.
484 if ($createNewContact ||
($this->_updateWithId
)) {
485 // @todo - there are multiple places where formatting is done that need consolidation.
486 // This handles where the label has been passed in and it has gotten this far.
487 // probably a bunch of hard-coded stuff could be removed to rely on this.
488 $fields = Contact
::getFields(FALSE)
489 ->addWhere('options', '=', TRUE)
490 ->setLoadOptions(TRUE)
491 ->execute()->indexBy('name');
492 foreach ($fields as $fieldName => $fieldSpec) {
493 if (isset($formatted[$fieldName]) && is_array($formatted[$fieldName])) {
494 // If we have an array at this stage, it's probably a multi-select
495 // field that has already been parsed properly into the value that
496 // should be inserted into the database.
499 if (!empty($formatted[$fieldName])
500 && empty($fieldSpec['options'][$formatted[$fieldName]])) {
501 $formatted[$fieldName] = array_search($formatted[$fieldName], $fieldSpec['options'], TRUE) ??
$formatted[$fieldName];
504 //CRM-4430, don't carry if not submitted.
505 if ($this->_updateWithId
&& !empty($params['id'])) {
506 $contactID = $params['id'];
508 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID
);
511 if (isset($newContact) && is_object($newContact) && ($newContact instanceof CRM_Contact_BAO_Contact
)) {
512 $relationship = TRUE;
513 $newContact = clone($newContact);
514 $contactID = $newContact->id
;
515 $this->_newContacts
[] = $contactID;
517 //get return code if we create new contact in update mode, CRM-4148
518 if ($this->_updateWithId
) {
519 $this->_retCode
= CRM_Import_Parser
::VALID
;
522 elseif (isset($newContact) && CRM_Core_Error
::isAPIError($newContact, CRM_Core_Error
::DUPLICATE_CONTACT
)) {
523 // if duplicate, no need of further processing
524 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
) {
525 $errorMessage = "Skipping duplicate record";
526 array_unshift($values, $errorMessage);
527 $this->setImportStatus((int) $values[count($values) - 1], 'DUPLICATE', $errorMessage);
528 return CRM_Import_Parser
::DUPLICATE
;
531 $relationship = TRUE;
532 // CRM-10433/CRM-20739 - IDs could be string or array; handle accordingly
533 if (!is_array($dupeContactIDs = $newContact['error_message']['params'][0])) {
534 $dupeContactIDs = explode(',', $dupeContactIDs);
536 $dupeCount = count($dupeContactIDs);
537 $contactID = array_pop($dupeContactIDs);
538 // check to see if we had more than one duplicate contact id.
539 // if we have more than one, the record will be rejected below
540 if ($dupeCount == 1) {
541 // there was only one dupe, we will continue normally...
542 if (!in_array($contactID, $this->_newContacts
)) {
543 $this->_newContacts
[] = $contactID;
550 $currentImportID = end($values);
553 'contactID' => $contactID,
554 'importID' => $currentImportID,
555 'importTempTable' => $this->_tableName
,
556 'fieldHeaders' => $this->_mapperKeys
,
557 'fields' => $this->_activeFields
,
560 CRM_Utils_Hook
::import('Contact', 'process', $this, $hookParams);
564 $primaryContactId = NULL;
565 if (CRM_Core_Error
::isAPIError($newContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
566 if ($dupeCount == 1 && CRM_Utils_Rule
::integer($contactID)) {
567 $primaryContactId = $contactID;
571 $primaryContactId = $newContact->id
;
574 if ((CRM_Core_Error
::isAPIError($newContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
) ||
is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) {
576 //relationship contact insert
577 foreach ($params as $key => $field) {
578 [$id, $first, $second] = CRM_Utils_System
::explode('_', $key, 3);
579 if (!($first == 'a' && $second == 'b') && !($first == 'b' && $second == 'a')) {
583 $relationType = new CRM_Contact_DAO_RelationshipType();
584 $relationType->id
= $id;
585 $relationType->find(TRUE);
586 $direction = "contact_sub_type_$second";
589 'contact_type' => $params[$key]['contact_type'],
592 //set subtype for related contact CRM-5125
593 if (isset($relationType->$direction)) {
594 //validation of related contact subtype for update mode
595 if ($relCsType = CRM_Utils_Array
::value('contact_sub_type', $params[$key]) && $relCsType != $relationType->$direction) {
596 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
597 array_unshift($values, $errorMessage);
598 return CRM_Import_Parser
::NO_MATCH
;
601 $formatting['contact_sub_type'] = $relationType->$direction;
605 $contactFields = NULL;
606 $contactFields = CRM_Contact_DAO_Contact
::import();
608 //Relation on the basis of External Identifier.
609 if (empty($params[$key]['id']) && !empty($params[$key]['external_identifier'])) {
610 $params[$key]['id'] = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['external_identifier'], 'id', 'external_identifier');
612 // check for valid related contact id in update/fill mode, CRM-4424
613 if (in_array($onDuplicate, [
614 CRM_Import_Parser
::DUPLICATE_UPDATE
,
615 CRM_Import_Parser
::DUPLICATE_FILL
,
616 ]) && !empty($params[$key]['id'])) {
617 $relatedContactType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_type');
618 if (!$relatedContactType) {
619 $errorMessage = ts("No contact found for this related contact ID: %1", [1 => $params[$key]['id']]);
620 array_unshift($values, $errorMessage);
621 return CRM_Import_Parser
::NO_MATCH
;
624 //validation of related contact subtype for update mode
626 $relatedCsType = NULL;
627 if (!empty($formatting['contact_sub_type'])) {
628 $relatedCsType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_sub_type');
631 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType
::isAllowEdit($params[$key]['id'], $relatedCsType) &&
632 $relatedCsType != CRM_Utils_Array
::value('contact_sub_type', $formatting))
634 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.") . ' ' . ts("ID: %1", [1 => $params[$key]['id']]);
635 array_unshift($values, $errorMessage);
636 return CRM_Import_Parser
::NO_MATCH
;
638 // get related contact id to format data in update/fill mode,
639 //if external identifier is present, CRM-4423
640 $formatting['id'] = $params[$key]['id'];
643 //format common data, CRM-4062
644 $this->formatCommonData($field, $formatting, $contactFields);
647 if (!empty($params[$key]['id'])) {
649 'contact_id' => $params[$key]['id'],
652 $relatedNewContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
655 $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE);
658 if (is_object($relatedNewContact) ||
($relatedNewContact instanceof CRM_Contact_BAO_Contact
)) {
659 $relatedNewContact = clone($relatedNewContact);
663 // To update/fill contact, get the matching contact Ids if duplicate contact found
664 // otherwise get contact Id from object of related contact
665 if (is_array($relatedNewContact) && civicrm_error($relatedNewContact)) {
666 if (CRM_Core_Error
::isAPIError($relatedNewContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
667 $matchedIDs = $relatedNewContact['error_message']['params'][0];
668 if (!is_array($matchedIDs)) {
669 $matchedIDs = explode(',', $matchedIDs);
673 $errorMessage = $relatedNewContact['error_message'];
674 array_unshift($values, $errorMessage);
675 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $errorMessage);
676 return CRM_Import_Parser
::ERROR
;
680 $matchedIDs[] = $relatedNewContact->id
;
682 // update/fill related contact after getting matching Contact Ids, CRM-4424
683 if (in_array($onDuplicate, [
684 CRM_Import_Parser
::DUPLICATE_UPDATE
,
685 CRM_Import_Parser
::DUPLICATE_FILL
,
687 //validation of related contact subtype for update mode
689 $relatedCsType = NULL;
690 if (!empty($formatting['contact_sub_type'])) {
691 $relatedCsType = CRM_Core_DAO
::getFieldValue('CRM_Contact_DAO_Contact', $matchedIDs[0], 'contact_sub_type');
694 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType
::isAllowEdit($matchedIDs[0], $relatedCsType) && $relatedCsType != CRM_Utils_Array
::value('contact_sub_type', $formatting))) {
695 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
696 array_unshift($values, $errorMessage);
697 return CRM_Import_Parser
::NO_MATCH
;
700 $updatedContact = $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]);
703 static $relativeContact = [];
704 if (CRM_Core_Error
::isAPIError($relatedNewContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
705 if (count($matchedIDs) >= 1) {
706 $relContactId = $matchedIDs[0];
707 //add relative contact to count during update & fill mode.
708 //logic to make count distinct by contact id.
709 if ($this->_newRelatedContacts ||
!empty($relativeContact)) {
710 $reContact = array_keys($relativeContact, $relContactId);
712 if (empty($reContact)) {
713 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
717 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
722 $relContactId = $relatedNewContact->id
;
723 $this->_newRelatedContacts
[] = $relativeContact[] = $relContactId;
726 if (CRM_Core_Error
::isAPIError($relatedNewContact, CRM_Core_ERROR
::DUPLICATE_CONTACT
) ||
($relatedNewContact instanceof CRM_Contact_BAO_Contact
)) {
727 //fix for CRM-1993.Checks for duplicate related contacts
728 if (count($matchedIDs) >= 1) {
729 //if more than one duplicate contact
730 //found, create relationship with first contact
731 // now create the relationship record
733 'relationship_type_id' => $key,
738 'skipRecentView' => TRUE,
741 // we only handle related contact success, we ignore failures for now
742 // at some point wold be nice to have related counts as separate
744 'contact' => $primaryContactId,
747 [$valid, $duplicate] = self
::legacyCreateMultiple($relationParams, $relationIds);
749 if ($valid ||
$duplicate) {
750 $relationIds['contactTarget'] = $relContactId;
751 $action = ($duplicate) ? CRM_Core_Action
::UPDATE
: CRM_Core_Action
::ADD
;
752 CRM_Contact_BAO_Relationship
::relatedMemberships($primaryContactId, $relationParams, $relationIds, $action);
755 //handle current employer, CRM-3532
757 $allRelationships = CRM_Core_PseudoConstant
::relationshipType('name');
758 $relationshipTypeId = str_replace([
765 $relationshipType = str_replace($relationshipTypeId . '_', '', $key);
766 $orgId = $individualId = NULL;
767 if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') {
768 $orgId = $relContactId;
769 $individualId = $primaryContactId;
771 elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') {
772 $orgId = $primaryContactId;
773 $individualId = $relContactId;
775 if ($orgId && $individualId) {
776 $currentEmpParams[$individualId] = $orgId;
777 CRM_Contact_BAO_Contact_Utils
::setCurrentEmployer($currentEmpParams);
785 if ($this->_updateWithId
) {
786 //return warning if street address is unparsed, CRM-5886
787 return $this->processMessage($values, $this->_retCode
);
790 if (is_array($newContact) && civicrm_error($newContact)) {
793 if (($code = CRM_Utils_Array
::value('code', $newContact['error_message'])) && ($code == CRM_Core_Error
::DUPLICATE_CONTACT
)) {
794 return $this->handleDuplicateError($newContact, $values, $onDuplicate, $formatted, $contactFields);
796 // Not a dupe, so we had an error
797 $errorMessage = $newContact['error_message'];
798 array_unshift($values, $errorMessage);
799 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $errorMessage);
800 return CRM_Import_Parser
::ERROR
;
804 if (empty($this->_unparsedStreetAddressContacts
)) {
805 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '', $contactID);
806 return CRM_Import_Parser
::VALID
;
809 // @todo - record unparsed address as 'imported' but the presence of a message is meaningful?
810 return $this->processMessage($values, CRM_Import_Parser
::VALID
);
814 * Only called from import now... plus one place outside of core & tests.
816 * @todo - deprecate more aggressively - will involve copying to the import
817 * class, adding a deprecation notice here & removing from tests.
819 * Takes an associative array and creates a relationship object.
821 * @deprecated For single creates use the api instead (it's tested).
822 * For multiple a new variant of this function needs to be written and migrated to as this is a bit
825 * @param array $params
826 * (reference ) an assoc array of name/value pairs.
828 * The array that holds all the db ids.
829 * per http://wiki.civicrm.org/confluence/display/CRM/Database+layer
830 * "we are moving away from the $ids param "
833 * @throws \CRM_Core_Exception
835 private static function legacyCreateMultiple($params, $ids = []) {
836 // clarify that the only key ever pass in the ids array is 'contact'
837 // There is legacy handling for other keys but a universe search on
838 // calls to this function (not supported to be called from outside core)
839 // only returns 2 calls - one in CRM_Contact_Import_Parser_Contact
840 // and the other in jma grant applications (CRM_Grant_Form_Grant_Confirm)
841 // both only pass in contact as a key here.
842 $contactID = $ids['contact'];
844 // There is only ever one value passed in from the 2 places above that call
845 // this - by clarifying here like this we can cleanup within this
846 // function without having to do more universe searches.
847 $relatedContactID = key($params['contact_check']);
849 // check if the relationship is valid between contacts.
850 // step 1: check if the relationship is valid if not valid skip and keep the count
851 // step 2: check the if two contacts already have a relationship if yes skip and keep the count
852 // step 3: if valid relationship then add the relation and keep the count
855 [$contactFields['relationship_type_id'], $firstLetter, $secondLetter] = explode('_', $params['relationship_type_id']);
856 $contactFields['contact_id_' . $firstLetter] = $contactID;
857 $contactFields['contact_id_' . $secondLetter] = $relatedContactID;
858 if (!CRM_Contact_BAO_Relationship
::checkRelationshipType($contactFields['contact_id_a'], $contactFields['contact_id_b'],
859 $contactFields['relationship_type_id'])) {
864 CRM_Contact_BAO_Relationship
::checkDuplicateRelationship(
874 $singleInstanceParams = array_merge($params, $contactFields);
875 CRM_Contact_BAO_Relationship
::add($singleInstanceParams);
880 * Format common params data to proper format to store.
882 * @param array $params
883 * Contain record values.
884 * @param array $formatted
885 * Array of formatted data.
886 * @param array $contactFields
887 * Contact DAO fields.
889 private function formatCommonData($params, &$formatted, $contactFields) {
890 $customFields = CRM_Core_BAO_CustomField
::getFields($formatted['contact_type'], FALSE, FALSE, $formatted['contact_sub_type'] ??
NULL);
892 $addressCustomFields = CRM_Core_BAO_CustomField
::getFields('Address');
893 $customFields = $customFields +
$addressCustomFields;
895 //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
897 'email_greeting_custom' => 'email_greeting',
898 'postal_greeting_custom' => 'postal_greeting',
899 'addressee_custom' => 'addressee',
901 foreach ($elements as $k => $v) {
902 if (array_key_exists($k, $params) && !(array_key_exists($v, $params))) {
903 $label = key(CRM_Core_OptionGroup
::values($v, TRUE, NULL, NULL, 'AND v.name = "Customized"'));
904 $params[$v] = $label;
909 $session = CRM_Core_Session
::singleton();
910 $dateType = $session->get("dateTypes");
911 foreach ($params as $key => $val) {
912 $customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key);
913 if ($customFieldID &&
914 !array_key_exists($customFieldID, $addressCustomFields)
916 //we should not update Date to null, CRM-4062
917 if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) {
919 CRM_Contact_Import_Parser_Contact
::formatCustomDate($params, $formatted, $dateType, $key);
921 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
922 if (empty($val) && !is_numeric($val) && $this->_onDuplicate
== CRM_Import_Parser
::DUPLICATE_FILL
) {
923 //retain earlier value when Import mode is `Fill`
924 unset($params[$key]);
927 $params[$key] = CRM_Utils_String
::strtoboolstr($val);
933 //now format custom data.
934 foreach ($params as $key => $field) {
935 if (is_array($field)) {
936 $isAddressCustomField = FALSE;
937 foreach ($field as $value) {
939 if (is_array($value)) {
940 foreach ($value as $name => $testForEmpty) {
941 if ($addressCustomFieldID = CRM_Core_BAO_CustomField
::getKeyID($name)) {
942 $isAddressCustomField = TRUE;
945 // check if $value does not contain IM provider or phoneType
946 if (($name !== 'phone_type_id' ||
$name !== 'provider_id') && ($testForEmpty === '' ||
$testForEmpty == NULL)) {
957 if (!empty($value['location_type_id'])) {
958 $this->formatLocationBlock($value, $formatted);
961 // @todo - this is still reachable - e.g. import with related contact info like firstname,lastname,spouse-first-name,spouse-last-name,spouse-home-phone
962 CRM_Core_Error
::deprecatedFunctionWarning('this is not expected to be reachable now');
963 $this->formatContactParameters($value, $formatted);
967 if (!$isAddressCustomField) {
976 if (($key !== 'preferred_communication_method') && (array_key_exists($key, $contactFields))) {
977 // due to merging of individual table and
978 // contact table, we need to avoid
979 // preferred_communication_method forcefully
980 $formatValues['contact_type'] = $formatted['contact_type'];
983 if ($key == 'id' && isset($field)) {
984 $formatted[$key] = $field;
986 $this->formatContactParameters($formatValues, $formatted);
988 //Handling Custom Data
989 // note: Address custom fields will be handled separately inside formatContactParameters
990 if (($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) &&
991 array_key_exists($customFieldID, $customFields) &&
992 !array_key_exists($customFieldID, $addressCustomFields)
995 $extends = $customFields[$customFieldID]['extends'] ??
NULL;
996 $htmlType = $customFields[$customFieldID]['html_type'] ??
NULL;
997 $dataType = $customFields[$customFieldID]['data_type'] ??
NULL;
998 $serialized = CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID]);
1000 if (!$serialized && in_array($htmlType, ['Select', 'Radio', 'Autocomplete-Select']) && in_array($dataType, ['String', 'Int'])) {
1001 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1002 foreach ($customOption as $customValue) {
1003 $val = $customValue['value'] ??
NULL;
1004 $label = strtolower($customValue['label'] ??
'');
1005 $value = strtolower(trim($formatted[$key]));
1006 if (($value == $label) ||
($value == strtolower($val))) {
1007 $params[$key] = $formatted[$key] = $val;
1011 elseif ($serialized && !empty($formatted[$key]) && !empty($params[$key])) {
1012 $mulValues = explode(',', $formatted[$key]);
1013 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1014 $formatted[$key] = [];
1016 foreach ($mulValues as $v1) {
1017 foreach ($customOption as $v2) {
1018 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
1019 (strtolower($v2['value']) == strtolower(trim($v1)))
1021 if ($htmlType == 'CheckBox') {
1022 $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1;
1025 $params[$key][] = $formatted[$key][] = $v2['value'];
1034 if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) && array_key_exists($customFieldID, $customFields) &&
1035 !array_key_exists($customFieldID, $addressCustomFields)
1037 // @todo calling api functions directly is not supported
1038 _civicrm_api3_custom_format_params($params, $formatted, $extends);
1041 // to check if not update mode and unset the fields with empty value.
1042 if (!$this->_updateWithId
&& array_key_exists('custom', $formatted)) {
1043 foreach ($formatted['custom'] as $customKey => $customvalue) {
1044 if (empty($formatted['custom'][$customKey][-1]['is_required'])) {
1045 $formatted['custom'][$customKey][-1]['is_required'] = $customFields[$customKey]['is_required'];
1047 $emptyValue = $customvalue[-1]['value'] ??
NULL;
1048 if (!isset($emptyValue)) {
1049 unset($formatted['custom'][$customKey]);
1054 // parse street address, CRM-5450
1055 if ($this->_parseStreetAddress
) {
1056 if (array_key_exists('address', $formatted) && is_array($formatted['address'])) {
1057 foreach ($formatted['address'] as $instance => & $address) {
1058 $streetAddress = $address['street_address'] ??
NULL;
1059 if (empty($streetAddress)) {
1062 // parse address field.
1063 $parsedFields = CRM_Core_BAO_Address
::parseStreetAddress($streetAddress);
1065 //street address consider to be parsed properly,
1066 //If we get street_name and street_number.
1067 if (empty($parsedFields['street_name']) ||
empty($parsedFields['street_number'])) {
1068 $parsedFields = array_fill_keys(array_keys($parsedFields), '');
1071 // merge parse address w/ main address block.
1072 $address = array_merge($address, $parsedFields);
1079 * Get the array of successfully imported contact id's
1083 public function getImportedContacts() {
1084 return $this->_newContacts
;
1088 * Get the array of successfully imported related contact id's
1092 public function &getRelatedImportedContacts() {
1093 return $this->_newRelatedContacts
;
1097 * Check if an error in custom data.
1099 * @param array $params
1100 * @param string $errorMessage
1101 * A string containing all the error-fields.
1103 * @param null $csType
1105 public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL) {
1106 $dateType = CRM_Core_Session
::singleton()->get("dateTypes");
1109 if (!empty($params['contact_sub_type'])) {
1110 $csType = $params['contact_sub_type'] ??
NULL;
1113 if (empty($params['contact_type'])) {
1114 $params['contact_type'] = 'Individual';
1117 // get array of subtypes - CRM-18708
1118 if (in_array($csType, CRM_Contact_BAO_ContactType
::basicTypes(TRUE), TRUE)) {
1119 $csType = self
::getSubtypes($params['contact_type']);
1122 if (is_array($csType)) {
1123 // fetch custom fields for every subtype and add it to $customFields array
1126 foreach ($csType as $cType) {
1127 $customFields +
= CRM_Core_BAO_CustomField
::getFields($params['contact_type'], FALSE, FALSE, $cType);
1131 $customFields = CRM_Core_BAO_CustomField
::getFields($params['contact_type'], FALSE, FALSE, $csType);
1134 $addressCustomFields = CRM_Core_BAO_CustomField
::getFields('Address');
1135 $parser = new CRM_Contact_Import_Parser_Contact();
1136 foreach ($params as $key => $value) {
1137 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1138 //For address custom fields, we do get actual custom field value as an inner array of
1139 //values so need to modify
1140 if (array_key_exists($customFieldID, $addressCustomFields)) {
1141 $value = $value[0][$key];
1142 $dataType = $addressCustomFields[$customFieldID]['data_type'];
1143 if ($dataType === 'Date') {
1144 $input = ['custom_' . $customFieldID => $value];
1149 $errors[] = $parser->validateCustomField($customFieldID, $addressCustomFields, $input, $value, $dateType);
1152 /* check if it's a valid custom field id */
1153 $errors[] = $parser->validateCustomField($customFieldID, $customFields, $params, $value, $dateType);
1156 elseif (is_array($params[$key]) && isset($params[$key]["contact_type"]) && in_array(substr($key, -3), ['a_b', 'b_a'], TRUE)) {
1158 //supporting custom data of related contact subtypes
1160 if (!empty($relation)) {
1161 [$id, $first, $second] = CRM_Utils_System
::explode('_', $relation, 3);
1162 $direction = "contact_sub_type_$second";
1163 $relationshipType = new CRM_Contact_BAO_RelationshipType();
1164 $relationshipType->id
= $id;
1165 if ($relationshipType->find(TRUE)) {
1166 if (isset($relationshipType->$direction)) {
1167 $params[$key]['contact_sub_type'] = $relationshipType->$direction;
1172 self
::isErrorInCustomData($params[$key], $errorMessage, $csType);
1176 $errorMessage .= ($errorMessage ?
'; ' : '') . implode('; ', array_filter($errors));
1181 * Check if an error in Core( non-custom fields ) field
1183 * @param array $params
1184 * @param string $errorMessage
1185 * A string containing all the error-fields.
1187 public function isErrorInCoreData($params, &$errorMessage) {
1189 if (!empty($params['contact_sub_type']) && !CRM_Contact_BAO_ContactType
::isExtendsContactType($params['contact_sub_type'], $params['contact_type'])) {
1190 $errors[] = ts('Mismatched or Invalid Contact Subtype.');
1193 foreach ($params as $key => $value) {
1194 if ($value === 'invalid_import_value') {
1195 $errors[] = $this->getFieldMetadata($key)['title'];
1200 case 'preferred_communication_method':
1202 $preffComm = explode(',', $value);
1203 foreach ($preffComm as $v) {
1204 if (!self
::in_value(trim($v), CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'preferred_communication_method'))) {
1205 $errors[] = ts('Preferred Communication Method');
1210 case 'preferred_mail_format':
1211 if (!array_key_exists(strtolower($value), array_change_key_case(CRM_Core_SelectValues
::pmf(), CASE_LOWER
))) {
1212 $errors[] = ts('Preferred Mail Format');
1216 case 'individual_prefix':
1218 if (!self
::in_value($value, CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'prefix_id'))) {
1219 $errors[] = ts('Individual Prefix');
1223 case 'individual_suffix':
1225 if (!self
::in_value($value, CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'suffix_id'))) {
1226 $errors[] = ts('Individual Suffix');
1230 case 'state_province':
1231 if (!empty($value)) {
1232 foreach ($value as $stateValue) {
1233 if ($stateValue['state_province']) {
1234 if (self
::in_value($stateValue['state_province'], CRM_Core_PseudoConstant
::stateProvinceAbbreviation()) ||
1235 self
::in_value($stateValue['state_province'], CRM_Core_PseudoConstant
::stateProvince())
1240 $errors[] = ts('State/Province');
1248 if (!empty($value)) {
1249 foreach ($value as $stateValue) {
1250 if ($stateValue['country']) {
1251 CRM_Core_PseudoConstant
::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
1252 CRM_Core_PseudoConstant
::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
1253 $limitCodes = CRM_Core_BAO_Country
::countryLimit();
1254 //If no country is selected in
1255 //localization then take all countries
1256 if (empty($limitCodes)) {
1257 $limitCodes = $countryIsoCodes;
1260 if (self
::in_value($stateValue['country'], $limitCodes) || self
::in_value($stateValue['country'], CRM_Core_PseudoConstant
::country())) {
1263 if (self
::in_value($stateValue['country'], $countryIsoCodes) || self
::in_value($stateValue['country'], $countryNames)) {
1264 $errors[] = 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." ');
1267 $errors[] = 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."');
1275 if (!empty($value)) {
1276 foreach ($value as $county) {
1277 if ($county['county']) {
1278 $countyNames = CRM_Core_PseudoConstant
::county();
1279 if (!empty($county['county']) && !in_array($county['county'], $countyNames)) {
1280 $errors[] = 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.');
1288 if (!empty($value)) {
1289 foreach ($value as $codeValue) {
1290 if (!empty($codeValue['geo_code_1'])) {
1291 if (CRM_Utils_Rule
::numeric($codeValue['geo_code_1'])) {
1294 $errors[] = ts('Geo code 1');
1301 if (!empty($value)) {
1302 foreach ($value as $codeValue) {
1303 if (!empty($codeValue['geo_code_2'])) {
1304 if (CRM_Utils_Rule
::numeric($codeValue['geo_code_2'])) {
1307 $errors[] = ts('Geo code 2');
1313 //check for any error in email/postal greeting, addressee,
1314 //custom email/postal greeting, custom addressee, CRM-4575
1316 case 'email_greeting':
1317 $emailGreetingFilter = [
1318 'contact_type' => $this->_contactType
,
1319 'greeting_type' => 'email_greeting',
1321 if (!self
::in_value($value, CRM_Core_PseudoConstant
::greeting($emailGreetingFilter))) {
1322 $errors[] = ts('Email Greeting must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Email Greetings for valid values');
1326 case 'postal_greeting':
1327 $postalGreetingFilter = [
1328 'contact_type' => $this->_contactType
,
1329 'greeting_type' => 'postal_greeting',
1331 if (!self
::in_value($value, CRM_Core_PseudoConstant
::greeting($postalGreetingFilter))) {
1332 $errors[] = ts('Postal Greeting must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Postal Greetings for valid values');
1337 $addresseeFilter = [
1338 'contact_type' => $this->_contactType
,
1339 'greeting_type' => 'addressee',
1341 if (!self
::in_value($value, CRM_Core_PseudoConstant
::greeting($addresseeFilter))) {
1342 $errors[] = ts('Addressee must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Addressee for valid values');
1346 case 'email_greeting_custom':
1347 if (array_key_exists('email_greeting', $params)) {
1348 $emailGreetingLabel = key(CRM_Core_OptionGroup
::values('email_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1349 if (CRM_Utils_Array
::value('email_greeting', $params) != $emailGreetingLabel) {
1350 $errors[] = ts('Email Greeting - Custom');
1355 case 'postal_greeting_custom':
1356 if (array_key_exists('postal_greeting', $params)) {
1357 $postalGreetingLabel = key(CRM_Core_OptionGroup
::values('postal_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1358 if (CRM_Utils_Array
::value('postal_greeting', $params) != $postalGreetingLabel) {
1359 $errors[] = ts('Postal Greeting - Custom');
1364 case 'addressee_custom':
1365 if (array_key_exists('addressee', $params)) {
1366 $addresseeLabel = key(CRM_Core_OptionGroup
::values('addressee', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1367 if (CRM_Utils_Array
::value('addressee', $params) != $addresseeLabel) {
1368 $errors[] = ts('Addressee - Custom');
1374 if (is_array($value)) {
1375 foreach ($value as $values) {
1376 if (!empty($values['url']) && !CRM_Utils_Rule
::url($values['url'])) {
1377 $errors[] = ts('Website');
1384 case 'do_not_email':
1385 case 'do_not_phone':
1388 case 'do_not_trade':
1389 if (CRM_Utils_Rule
::boolean($value) == FALSE) {
1390 $key = ucwords(str_replace("_", " ", $key));
1396 if (is_array($value)) {
1397 foreach ($value as $values) {
1398 if (!empty($values['email']) && !CRM_Utils_Rule
::email($values['email'])) {
1407 if (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
1408 //check for any relationship data ,FIX ME
1409 self
::isErrorInCoreData($params[$key], $errorMessage);
1415 $errorMessage .= ($errorMessage ?
'; ' : '') . implode('; ', $errors);
1420 * Ckeck a value present or not in a array.
1423 * @param $valueArray
1427 public static function in_value($value, $valueArray) {
1428 foreach ($valueArray as $key => $v) {
1430 if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) {
1438 * Build error-message containing error-fields
1440 * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function.
1442 * @todo just say no!
1444 * @param string $errorName
1445 * A string containing error-field name.
1446 * @param string $errorMessage
1447 * A string containing all the error-fields, where the new errorName is concatenated.
1450 public static function addToErrorMsg($errorName, &$errorMessage) {
1451 if ($errorMessage) {
1452 $errorMessage .= "; $errorName";
1455 $errorMessage = $errorName;
1460 * Method for creating contact.
1462 * @param array $formatted
1463 * @param array $contactFields
1464 * @param int $onDuplicate
1465 * @param int $contactId
1466 * @param bool $requiredCheck
1467 * @param int $dedupeRuleGroupID
1469 * @return array|bool|\CRM_Contact_BAO_Contact|\CRM_Core_Error|null
1471 public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) {
1475 if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser
::DUPLICATE_NOCHECK
)) {
1476 $dupeCheck = (bool) ($onDuplicate);
1479 //get the prefix id etc if exists
1480 CRM_Contact_BAO_Contact
::resolveDefaults($formatted, TRUE);
1482 //@todo direct call to API function not supported.
1483 // setting required check to false, CRM-2839
1484 // plus we do our own required check in import
1486 $error = $this->deprecated_contact_check_params($formatted, $dupeCheck, $dedupeRuleGroupID);
1490 $this->deprecated_validate_formatted_contact($formatted);
1492 catch (CRM_Core_Exception
$e) {
1493 return ['error_message' => $e->getMessage(), 'is_error' => 1, 'code' => $e->getCode()];
1497 $this->formatParams($formatted, $onDuplicate, (int) $contactId);
1500 // Resetting and rebuilding cache could be expensive.
1501 CRM_Core_Config
::setPermitCacheFlushMode(FALSE);
1503 // If a user has logged in, or accessed via a checksum
1504 // Then deliberately 'blanking' a value in the profile should remove it from their record
1505 // @todo this should either be TRUE or FALSE in the context of import - once
1506 // we figure out which we can remove all the rest.
1507 // Also note the meaning of this parameter is less than it used to
1508 // be following block cleanup.
1509 $formatted['updateBlankLocInfo'] = TRUE;
1510 if ((CRM_Core_Session
::singleton()->get('authSrc') & (CRM_Core_Permission
::AUTH_SRC_CHECKSUM + CRM_Core_Permission
::AUTH_SRC_LOGIN
)) == 0) {
1511 $formatted['updateBlankLocInfo'] = FALSE;
1514 [$data, $contactDetails] = CRM_Contact_BAO_Contact
::formatProfileContactParams($formatted, $contactFields, $contactId, NULL, $formatted['contact_type']);
1516 // manage is_opt_out
1517 if (array_key_exists('is_opt_out', $contactFields) && array_key_exists('is_opt_out', $formatted)) {
1518 $wasOptOut = $contactDetails['is_opt_out'] ??
FALSE;
1519 $isOptOut = $formatted['is_opt_out'];
1520 $data['is_opt_out'] = $isOptOut;
1521 // on change, create new civicrm_subscription_history entry
1522 if (($wasOptOut != $isOptOut) && !empty($contactDetails['contact_id'])) {
1524 'contact_id' => $contactDetails['contact_id'],
1525 'status' => $isOptOut ?
'Removed' : 'Added',
1528 CRM_Contact_BAO_SubscriptionHistory
::create($shParams);
1532 $contact = civicrm_api3('Contact', 'create', $data);
1533 $cid = $contact['id'];
1535 CRM_Core_Config
::setPermitCacheFlushMode(TRUE);
1538 'contact_id' => $cid,
1542 $newContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
1544 //get the id of the contact whose street address is not parsable, CRM-5886
1545 if ($this->_parseStreetAddress
&& is_object($newContact) && property_exists($newContact, 'address') && $newContact->address
) {
1546 foreach ($newContact->address
as $address) {
1547 if (!empty($address['street_address']) && (empty($address['street_number']) ||
empty($address['street_name']))) {
1548 $this->_unparsedStreetAddressContacts
[] = [
1549 'id' => $newContact->id
,
1550 'streetAddress' => $address['street_address'],
1559 * Format params for update and fill mode.
1561 * @param array $params
1562 * reference to an array containing all the.
1564 * @param int $onDuplicate
1568 public function formatParams(&$params, $onDuplicate, $cid) {
1569 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
) {
1574 'contact_id' => $cid,
1578 $contactObj = CRM_Contact_BAO_Contact
::retrieve($contactParams, $defaults);
1580 $modeFill = ($onDuplicate == CRM_Import_Parser
::DUPLICATE_FILL
);
1582 $groupTree = CRM_Core_BAO_CustomGroup
::getTree($params['contact_type'], NULL, $cid, 0, NULL);
1583 CRM_Core_BAO_CustomGroup
::setDefaults($groupTree, $defaults, FALSE, FALSE);
1589 'website' => 'website',
1590 'address' => 'address',
1593 $contact = get_object_vars($contactObj);
1595 foreach ($params as $key => $value) {
1596 if ($key == 'id' ||
$key == 'contact_type') {
1600 if (array_key_exists($key, $locationFields)) {
1603 if (in_array($key, [
1608 // CRM-4575, need to null custom
1609 if ($params["{$key}_id"] != 4) {
1610 $params["{$key}_custom"] = 'null';
1612 unset($params[$key]);
1615 if ($customFieldId = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1616 $custom_params = ['id' => $contact['id'], 'return' => $key];
1617 $getValue = civicrm_api3('Contact', 'getvalue', $custom_params);
1618 if (empty($getValue)) {
1623 $getValue = CRM_Utils_Array
::retrieveValueRecursive($contact, $key);
1625 if ($key == 'contact_source') {
1626 $params['source'] = $params[$key];
1627 unset($params[$key]);
1630 if ($modeFill && isset($getValue)) {
1631 unset($params[$key]);
1632 if ($customFieldId) {
1633 // Extra values must be unset to ensure the values are not
1635 unset($params['custom'][$customFieldId]);
1641 foreach ($locationFields as $locKeys) {
1642 if (isset($params[$locKeys]) && is_array($params[$locKeys])) {
1643 foreach ($params[$locKeys] as $key => $value) {
1645 $getValue = CRM_Utils_Array
::retrieveValueRecursive($contact, $locKeys);
1647 if (isset($getValue)) {
1648 foreach ($getValue as $cnt => $values) {
1649 if ($locKeys == 'website') {
1650 if (($getValue[$cnt]['website_type_id'] == $params[$locKeys][$key]['website_type_id'])) {
1651 unset($params[$locKeys][$key]);
1655 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']) {
1656 unset($params[$locKeys][$key]);
1663 if (count($params[$locKeys]) == 0) {
1664 unset($params[$locKeys]);
1671 * Convert any given date string to default date array.
1673 * @param array $params
1674 * Has given date-format.
1675 * @param array $formatted
1676 * Store formatted date in this array.
1677 * @param int $dateType
1679 * @param string $dateParam
1682 public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) {
1684 CRM_Utils_Date
::convertToDefaultDate($params, $dateType, $dateParam);
1685 $formatted[$dateParam] = CRM_Utils_Date
::processDate($params[$dateParam]);
1689 * Generate status and error message for unparsed street address records.
1691 * @param array $values
1692 * The array of values belonging to each row.
1693 * @param $returnCode
1697 private function processMessage(&$values, $returnCode) {
1698 if (empty($this->_unparsedStreetAddressContacts
)) {
1699 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '');
1702 $errorMessage = ts("Record imported successfully but unable to parse the street address: ");
1703 foreach ($this->_unparsedStreetAddressContacts
as $contactInfo => $contactValue) {
1704 $contactUrl = CRM_Utils_System
::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contactValue['id'], TRUE, NULL, FALSE);
1705 $errorMessage .= "\n Contact ID:" . $contactValue['id'] . " <a href=\"$contactUrl\"> " . $contactValue['streetAddress'] . "</a>";
1707 array_unshift($values, $errorMessage);
1708 $returnCode = CRM_Import_Parser
::UNPARSED_ADDRESS_WARNING
;
1709 $this->setImportStatus((int) ($values[count($values) - 1]), 'ERROR', $errorMessage);
1715 * get subtypes given the contact type
1717 * @param string $contactType
1718 * @return array $subTypes
1720 public static function getSubtypes($contactType) {
1722 $types = CRM_Contact_BAO_ContactType
::subTypeInfo($contactType);
1724 if (count($types) > 0) {
1725 foreach ($types as $type) {
1726 $subTypes[] = $type['name'];
1733 * Get the possible contact matches.
1735 * 1) the chosen dedupe rule falling back to
1736 * 2) a check for the external ID.
1738 * @see https://issues.civicrm.org/jira/browse/CRM-17275
1740 * @param array $params
1741 * @param int|null $extIDMatch
1742 * @param int|null $dedupeRuleID
1745 * IDs of a possible.
1747 * @throws \CRM_Core_Exception
1748 * @throws \CiviCRM_API3_Exception
1750 protected function getPossibleContactMatch(array $params, ?
int $extIDMatch, ?
int $dedupeRuleID): ?
int {
1751 $checkParams = ['check_permissions' => FALSE, 'match' => $params, 'dedupe_rule_id' => $dedupeRuleID];
1752 $possibleMatches = civicrm_api3('Contact', 'duplicatecheck', $checkParams);
1754 // Historically we have used the last ID - it is not clear if this was
1756 return array_key_last($possibleMatches['values']);
1758 if ($possibleMatches['count']) {
1759 if (array_key_exists($extIDMatch, $possibleMatches['values'])) {
1762 throw new CRM_Core_Exception(ts(
1763 'Matching this contact based on the de-dupe rule would cause an external ID conflict'));
1769 * Format the form mapping parameters ready for the parser.
1774 * @return array $parserParameters
1776 public static function getParameterForParser($count) {
1778 for ($i = 0; $i < $count; $i++
) {
1779 $baseArray[$i] = NULL;
1781 $parserParameters['mapperLocType'] = $baseArray;
1782 $parserParameters['mapperPhoneType'] = $baseArray;
1783 $parserParameters['mapperImProvider'] = $baseArray;
1784 $parserParameters['mapperWebsiteType'] = $baseArray;
1785 $parserParameters['mapperRelated'] = $baseArray;
1786 $parserParameters['relatedContactType'] = $baseArray;
1787 $parserParameters['relatedContactDetails'] = $baseArray;
1788 $parserParameters['relatedContactLocType'] = $baseArray;
1789 $parserParameters['relatedContactPhoneType'] = $baseArray;
1790 $parserParameters['relatedContactImProvider'] = $baseArray;
1791 $parserParameters['relatedContactWebsiteType'] = $baseArray;
1793 return $parserParameters;
1798 * Set field metadata.
1800 protected function setFieldMetadata() {
1801 $this->setImportableFieldsMetadata($this->getContactImportMetadata());
1805 * @param array $newContact
1806 * @param array $values
1807 * @param int $onDuplicate
1808 * @param array $formatted
1809 * @param array $contactFields
1813 * @throws \CRM_Core_Exception
1814 * @throws \CiviCRM_API3_Exception
1815 * @throws \Civi\API\Exception\UnauthorizedException
1817 private function handleDuplicateError(array $newContact, array $values, int $onDuplicate, array $formatted, array $contactFields): int {
1819 // need to fix at some stage and decide if the error will return an
1820 // array or string, crude hack for now
1821 if (is_array($newContact['error_message']['params'][0])) {
1822 $cids = $newContact['error_message']['params'][0];
1825 $cids = explode(',', $newContact['error_message']['params'][0]);
1828 foreach ($cids as $cid) {
1829 $urls[] = CRM_Utils_System
::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE);
1832 $url_string = implode("\n", $urls);
1834 // If we duplicate more than one record, skip no matter what
1835 if (count($cids) > 1) {
1836 $errorMessage = ts('Record duplicates multiple contacts');
1837 //combine error msg to avoid mismatch between error file columns.
1838 $errorMessage .= "\n" . $url_string;
1839 array_unshift($values, $errorMessage);
1840 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $errorMessage);
1841 return CRM_Import_Parser
::ERROR
;
1844 // Params only had one id, so shift it out
1845 $contactId = array_shift($cids);
1848 $vals = ['contact_id' => $contactId];
1849 if (in_array((int) $onDuplicate, [CRM_Import_Parser
::DUPLICATE_UPDATE
, CRM_Import_Parser
::DUPLICATE_FILL
], TRUE)) {
1850 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactId);
1852 // else skip does nothing and just returns an error code.
1855 'contact_id' => $cid,
1858 $newContact = CRM_Contact_BAO_Contact
::retrieve($contact, $defaults);
1861 if (civicrm_error($newContact)) {
1862 if (empty($newContact['error_message']['params'])) {
1863 // different kind of error other than DUPLICATE
1864 $errorMessage = $newContact['error_message'];
1865 array_unshift($values, $errorMessage);
1866 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $errorMessage);
1867 return CRM_Import_Parser
::ERROR
;
1870 $contactID = $newContact['error_message']['params'][0];
1871 if (is_array($contactID)) {
1872 $contactID = array_pop($contactID);
1874 if (!in_array($contactID, $this->_newContacts
)) {
1875 $this->_newContacts
[] = $contactID;
1878 //CRM-262 No Duplicate Checking
1879 if ($onDuplicate == CRM_Import_Parser
::DUPLICATE_SKIP
) {
1880 array_unshift($values, $url_string);
1881 $this->setImportStatus((int) $values[count($values) - 1], 'DUPLICATE', 'Skipping duplicate record');
1882 return CRM_Import_Parser
::DUPLICATE
;
1885 $this->setImportStatus((int) $values[count($values) - 1], 'Imported', '');
1886 //return warning if street address is not parsed, CRM-5886
1887 return $this->processMessage($values, CRM_Import_Parser
::VALID
);
1891 * Validate a formatted contact parameter list.
1893 * @param array $params
1894 * Structured parameter list (as in crm_format_params).
1896 * @throw CRM_Core_Error
1898 public function deprecated_validate_formatted_contact(&$params): void
{
1899 // Validate custom data fields
1900 if (array_key_exists('custom', $params) && is_array($params['custom'])) {
1901 foreach ($params['custom'] as $key => $custom) {
1902 if (is_array($custom)) {
1903 foreach ($custom as $fieldId => $value) {
1904 $valid = CRM_Core_BAO_CustomValue
::typecheck(CRM_Utils_Array
::value('type', $value),
1905 CRM_Utils_Array
::value('value', $value)
1907 if (!$valid && $value['is_required']) {
1908 throw new CRM_Core_Exception('Invalid value for custom field \'' .
1909 $custom['name'] . '\''
1912 if (CRM_Utils_Array
::value('type', $custom) == 'Date') {
1913 $params['custom'][$key][$fieldId]['value'] = str_replace('-', '', $params['custom'][$key][$fieldId]['value']);
1922 * @param array $params
1923 * @param bool $dupeCheck
1924 * @param null|int $dedupeRuleGroupID
1927 * @throws \CRM_Core_Exception
1929 public function deprecated_contact_check_params(
1932 $dedupeRuleGroupID = NULL) {
1935 // @todo switch to using api version
1936 // $dupes = civicrm_api3('Contact', 'duplicatecheck', (array('match' => $params, 'dedupe_rule_id' => $dedupeRuleGroupID)));
1937 // $ids = $dupes['count'] ? implode(',', array_keys($dupes['values'])) : NULL;
1938 $ids = CRM_Contact_BAO_Contact
::getDuplicateContacts($params, $params['contact_type'], 'Unsupervised', [], CRM_Utils_Array
::value('check_permissions', $params), $dedupeRuleGroupID);
1943 'error_message' => [
1944 'code' => CRM_Core_Error
::DUPLICATE_CONTACT
,
1947 'message' => 'Found matching contacts: ' . implode(',', $ids),
1957 * @param array $mapper Mapping as entered on MapField form.
1958 * e.g [['first_name']['email', 1]].
1959 * {@see \CRM_Contact_Import_Parser_Contact::getMappingFieldFromMapperInput}
1961 * @param int $statusID
1964 * @throws \API_Exception|\CRM_Core_Exception
1966 public function run(
1968 $mode = self
::MODE_PREVIEW
,
1972 // TODO: Make the timeout actually work
1973 $this->_onDuplicate
= $onDuplicate = $this->getSubmittedValue('onDuplicate');
1974 $this->_dedupeRuleGroupID
= $this->getSubmittedValue('dedupe_rule_id');
1975 // Since $this->_contactType is still being called directly do a get call
1976 // here to make sure it is instantiated.
1977 $this->getContactType();
1978 $this->getContactSubType();
1982 $this->_rowCount
= 0;
1983 $this->_totalCount
= 0;
1985 $this->_primaryKeyName
= '_id';
1986 $this->_statusFieldName
= '_status';
1989 $this->progressImport($statusID);
1990 $startTimestamp = $currTimestamp = $prevTimestamp = time();
1992 $dataSource = $this->getDataSourceObject();
1993 $totalRowCount = $dataSource->getRowCount(['new']);
1994 if ($mode == self
::MODE_IMPORT
) {
1995 $dataSource->setStatuses(['new']);
1998 while ($row = $dataSource->getRow()) {
1999 $values = array_values($row);
2002 $this->_totalCount++
;
2004 if ($mode == self
::MODE_PREVIEW
) {
2005 $returnCode = $this->preview($values);
2007 elseif ($mode == self
::MODE_SUMMARY
) {
2008 $returnCode = $this->summary($values);
2010 elseif ($mode == self
::MODE_IMPORT
) {
2012 $returnCode = $this->import($onDuplicate, $values);
2014 catch (CiviCRM_API3_Exception
$e) {
2015 // When we catch errors here we are not adding to the errors array - mostly
2016 // because that will become obsolete once https://github.com/civicrm/civicrm-core/pull/23292
2017 // is merged and this will replace it as the main way to handle errors (ie. update the table
2019 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $e->getMessage());
2021 if ($statusID && (($this->_rowCount %
50) == 0)) {
2022 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
2026 $returnCode = self
::ERROR
;
2029 if ($returnCode & self
::NO_MATCH
) {
2030 $this->setImportStatus((int) $values[count($values) - 1], 'invalid_no_match', array_shift($values));
2033 if ($returnCode & self
::UNPARSED_ADDRESS_WARNING
) {
2034 $this->setImportStatus((int) $values[count($values) - 1], 'warning_unparsed_address', array_shift($values));
2040 * Given a list of the importable field keys that the user has selected.
2041 * set the active fields array to this list
2043 * @param array $fieldKeys
2044 * Mapped array of values.
2046 public function setActiveFields($fieldKeys) {
2047 foreach ($fieldKeys as $key) {
2048 if (empty($this->_fields
[$key])) {
2049 $this->_activeFields
[] = new CRM_Contact_Import_Field('', ts('- do not import -'));
2052 $this->_activeFields
[] = clone($this->_fields
[$key]);
2058 * Format the field values for input to the api.
2060 * @param array $values
2061 * The row from the datasource.
2064 * Parameters mapped as described in getMappedRow
2066 * @throws \API_Exception
2067 * @todo - clean this up a bit & merge back into `getMappedRow`
2070 private function getParams(array $values): array {
2073 foreach ($this->getFieldMappings() as $i => $mappedField) {
2074 // The key is in the format 5_a_b where 5 is the relationship_type_id and a_b is the direction.
2075 $relatedContactKey = $mappedField['relationship_type_id'] ?
($mappedField['relationship_type_id'] . '_' . $mappedField['relationship_direction']) : NULL;
2076 $fieldName = $mappedField['name'];
2077 $importedValue = $values[$i];
2078 if ($fieldName === 'do_not_import' ||
$importedValue === NULL) {
2082 $locationFields = ['location_type_id', 'phone_type_id', 'provider_id', 'website_type_id'];
2083 $locationValues = array_filter(array_intersect_key($mappedField, array_fill_keys($locationFields, 1)));
2085 if ($relatedContactKey) {
2086 if (!isset($params[$relatedContactKey])) {
2087 $params[$relatedContactKey] = ['contact_type' => $this->getRelatedContactType($mappedField['relationship_type_id'], $mappedField['relationship_direction'])];
2089 $this->addFieldToParams($params[$relatedContactKey], $locationValues, $fieldName, $importedValue);
2092 $this->addFieldToParams($params, $locationValues, $fieldName, $importedValue);
2100 * @param string $name
2103 * @param string $headerPattern
2104 * @param string $dataPattern
2105 * @param bool $hasLocationType
2107 public function addField(
2108 $name, $title, $type = CRM_Utils_Type
::T_INT
,
2109 $headerPattern = '//', $dataPattern = '//',
2110 $hasLocationType = FALSE
2112 $this->_fields
[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
2114 $this->_fields
['doNotImport'] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
2119 * Store parser values.
2121 * @param CRM_Core_Session $store
2125 public function set($store, $mode = self
::MODE_SUMMARY
) {
2129 * Export data to a CSV file.
2131 * @param string $fileName
2132 * @param array $header
2133 * @param array $data
2135 public static function exportCSV($fileName, $header, $data) {
2137 if (file_exists($fileName) && !is_writable($fileName)) {
2138 CRM_Core_Error
::movedSiteError($fileName);
2140 //hack to remove '_status', '_statusMsg' and '_id' from error file
2142 $dbRecordStatus = ['IMPORTED', 'ERROR', 'DUPLICATE', 'INVALID', 'NEW'];
2143 foreach ($data as $rowCount => $rowValues) {
2145 foreach ($rowValues as $key => $val) {
2146 if (in_array($val, $dbRecordStatus) && $count == (count($rowValues) - 3)) {
2149 $errorValues[$rowCount][$key] = $val;
2153 $data = $errorValues;
2156 $fd = fopen($fileName, 'w');
2158 foreach ($header as $key => $value) {
2159 $header[$key] = "\"$value\"";
2161 $config = CRM_Core_Config
::singleton();
2162 $output[] = implode($config->fieldSeparator
, $header);
2164 foreach ($data as $datum) {
2165 foreach ($datum as $key => $value) {
2166 $datum[$key] = "\"$value\"";
2168 $output[] = implode($config->fieldSeparator
, $datum);
2170 fwrite($fd, implode("\n", $output));
2175 * Update the status of the import row to reflect the processing outcome.
2178 * @param string $status
2179 * @param string $message
2180 * @param int|null $entityID
2181 * Optional created entity ID
2182 * @param array $relatedEntityIDs
2183 * Optional array e.g ['related_contact' => 4]
2185 * @throws \API_Exception
2186 * @throws \CRM_Core_Exception
2188 public function setImportStatus(int $id, string $status, string $message, ?
int $entityID = NULL, array $relatedEntityIDs = []): void
{
2189 $this->getDataSourceObject()->updateStatus($id, $status, $message, $entityID, $relatedEntityIDs);
2193 * Format contact parameters.
2195 * @todo this function needs re-writing & re-merging into the main function.
2199 * @param array $values
2200 * @param array $params
2204 protected function formatContactParameters(&$values, &$params) {
2205 // Crawl through the possible classes:
2218 // first add core contact values since for other Civi modules they are not added
2219 $contactFields = CRM_Contact_DAO_Contact
::fields();
2220 _civicrm_api3_store_values($contactFields, $values, $params);
2222 if (isset($values['contact_type'])) {
2223 // we're an individual/household/org property
2225 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact
::fields();
2227 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
2231 // Cache the various object fields
2232 // @todo - remove this after confirming this is just a compilation of other-wise-cached fields.
2233 static $fields = [];
2235 if (isset($values['individual_prefix'])) {
2236 if (!empty($params['prefix_id'])) {
2237 $prefixes = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'prefix_id');
2238 $params['prefix'] = $prefixes[$params['prefix_id']];
2241 $params['prefix'] = $values['individual_prefix'];
2246 if (isset($values['individual_suffix'])) {
2247 if (!empty($params['suffix_id'])) {
2248 $suffixes = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'suffix_id');
2249 $params['suffix'] = $suffixes[$params['suffix_id']];
2252 $params['suffix'] = $values['individual_suffix'];
2258 if (isset($values['email_greeting'])) {
2259 if (!empty($params['email_greeting_id'])) {
2260 $emailGreetingFilter = [
2261 'contact_type' => $params['contact_type'] ??
NULL,
2262 'greeting_type' => 'email_greeting',
2264 $emailGreetings = CRM_Core_PseudoConstant
::greeting($emailGreetingFilter);
2265 $params['email_greeting'] = $emailGreetings[$params['email_greeting_id']];
2268 $params['email_greeting'] = $values['email_greeting'];
2274 if (isset($values['postal_greeting'])) {
2275 if (!empty($params['postal_greeting_id'])) {
2276 $postalGreetingFilter = [
2277 'contact_type' => $params['contact_type'] ??
NULL,
2278 'greeting_type' => 'postal_greeting',
2280 $postalGreetings = CRM_Core_PseudoConstant
::greeting($postalGreetingFilter);
2281 $params['postal_greeting'] = $postalGreetings[$params['postal_greeting_id']];
2284 $params['postal_greeting'] = $values['postal_greeting'];
2289 if (isset($values['addressee'])) {
2290 $params['addressee'] = $values['addressee'];
2294 if (!empty($values['preferred_communication_method'])) {
2296 $pcm = array_change_key_case(array_flip(CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'preferred_communication_method')), CASE_LOWER
);
2298 $preffComm = explode(',', $values['preferred_communication_method']);
2299 foreach ($preffComm as $v) {
2300 $v = strtolower(trim($v));
2301 if (array_key_exists($v, $pcm)) {
2302 $comm[$pcm[$v]] = 1;
2306 $params['preferred_communication_method'] = $comm;
2310 // format the website params.
2311 if (!empty($values['url'])) {
2312 static $websiteFields;
2313 if (!is_array($websiteFields)) {
2314 $websiteFields = CRM_Core_DAO_Website
::fields();
2316 if (!array_key_exists('website', $params) ||
2317 !is_array($params['website'])
2319 $params['website'] = [];
2322 $websiteCount = count($params['website']);
2323 _civicrm_api3_store_values($websiteFields, $values,
2324 $params['website'][++
$websiteCount]
2330 if (isset($values['note'])) {
2332 if (!isset($params['note'])) {
2333 $params['note'] = [];
2335 $noteBlock = count($params['note']) +
1;
2337 $params['note'][$noteBlock] = [];
2338 if (!isset($fields['Note'])) {
2339 $fields['Note'] = CRM_Core_DAO_Note
::fields();
2342 // get the current logged in civicrm user
2343 $session = CRM_Core_Session
::singleton();
2344 $userID = $session->get('userID');
2347 $values['contact_id'] = $userID;
2350 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
2355 // Check for custom field values
2356 $customFields = CRM_Core_BAO_CustomField
::getFields(CRM_Utils_Array
::value('contact_type', $values),
2357 FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE
2360 foreach ($values as $key => $value) {
2361 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
2362 // check if it's a valid custom field id
2364 if (!array_key_exists($customFieldID, $customFields)) {
2365 return civicrm_api3_create_error('Invalid custom field ID');
2368 $params[$key] = $value;
2376 * Format location block ready for importing.
2378 * There is some test coverage for this in CRM_Contact_Import_Parser_ContactTest
2379 * e.g. testImportPrimaryAddress.
2381 * @param array $values
2382 * @param array $params
2386 protected function formatLocationBlock(&$values, &$params) {
2391 'openid' => 'OpenID',
2392 'phone_ext' => 'Phone',
2394 foreach ($blockTypes as $blockFieldName => $block) {
2395 if (!array_key_exists($blockFieldName, $values)) {
2398 $blockIndex = $values['location_type_id'] . (!empty($values['phone_type_id']) ?
'_' . $values['phone_type_id'] : '');
2400 // block present in value array.
2401 if (!array_key_exists($blockFieldName, $params) ||
!is_array($params[$blockFieldName])) {
2402 $params[$blockFieldName] = [];
2405 $fields[$block] = $this->getMetadataForEntity($block);
2407 // copy value to dao field name.
2408 if ($blockFieldName == 'im') {
2409 $values['name'] = $values[$blockFieldName];
2412 _civicrm_api3_store_values($fields[$block], $values,
2413 $params[$blockFieldName][$blockIndex]
2416 $this->fillPrimary($params[$blockFieldName][$blockIndex], $values, $block, CRM_Utils_Array
::value('id', $params));
2418 if (empty($params['id']) && (count($params[$blockFieldName]) == 1)) {
2419 $params[$blockFieldName][$blockIndex]['is_primary'] = TRUE;
2422 // we only process single block at a time.
2426 // handle address fields.
2427 if (!array_key_exists('address', $params) ||
!is_array($params['address'])) {
2428 $params['address'] = [];
2431 // Note: we doing multiple value formatting here for address custom fields, plus putting into right format.
2432 // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving
2433 // the address in CRM_Core_BAO_Address::create method
2434 if (!empty($values['location_type_id'])) {
2435 static $customFields = [];
2436 if (empty($customFields)) {
2437 $customFields = CRM_Core_BAO_CustomField
::getFields('Address');
2439 // make a copy of values, as we going to make changes
2440 $newValues = $values;
2441 foreach ($values as $key => $val) {
2442 $customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key);
2443 if ($customFieldID && array_key_exists($customFieldID, $customFields)) {
2445 $htmlType = $customFields[$customFieldID]['html_type'] ??
NULL;
2446 if (CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID]) && $val) {
2447 $mulValues = explode(',', $val);
2448 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
2449 $newValues[$key] = [];
2450 foreach ($mulValues as $v1) {
2451 foreach ($customOption as $v2) {
2452 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
2453 (strtolower($v2['value']) == strtolower(trim($v1)))
2455 if ($htmlType == 'CheckBox') {
2456 $newValues[$key][$v2['value']] = 1;
2459 $newValues[$key][] = $v2['value'];
2467 // consider new values
2468 $values = $newValues;
2471 $fields['Address'] = $this->getMetadataForEntity('Address');
2472 // @todo this is kinda replicated below....
2473 _civicrm_api3_store_values($fields['Address'], $values, $params['address'][$values['location_type_id']]);
2479 'supplemental_address_1',
2480 'supplemental_address_2',
2481 'supplemental_address_3',
2482 'StateProvince.name',
2484 foreach (array_keys($customFields) as $customFieldID) {
2485 $addressFields[] = 'custom_' . $customFieldID;
2488 foreach ($addressFields as $field) {
2489 if (array_key_exists($field, $values)) {
2490 if (!array_key_exists('address', $params)) {
2491 $params['address'] = [];
2493 $params['address'][$values['location_type_id']][$field] = $values[$field];
2497 $this->fillPrimary($params['address'][$values['location_type_id']], $values, 'address', CRM_Utils_Array
::value('id', $params));
2502 * Get the field metadata for the relevant entity.
2504 * @param string $entity
2508 protected function getMetadataForEntity($entity) {
2509 if (!isset($this->fieldMetadata
[$entity])) {
2510 $className = "CRM_Core_DAO_$entity";
2511 $this->fieldMetadata
[$entity] = $className::fields();
2513 return $this->fieldMetadata
[$entity];
2517 * Fill in the primary location.
2519 * If the contact has a primary address we update it. Otherwise
2520 * we add an address of the default location type.
2522 * @param array $params
2523 * Address block parameters
2524 * @param array $values
2526 * @param string $entity
2527 * - address, email, phone
2528 * @param int|null $contactID
2530 * @throws \CiviCRM_API3_Exception
2532 protected function fillPrimary(&$params, $values, $entity, $contactID) {
2533 if ($values['location_type_id'] === 'Primary') {
2535 $primary = civicrm_api3($entity, 'get', [
2536 'return' => 'location_type_id',
2537 'contact_id' => $contactID,
2542 $defaultLocationType = CRM_Core_BAO_LocationType
::getDefault();
2543 $params['location_type_id'] = (int) (isset($primary) && $primary['count']) ?
$primary['values'][0]['location_type_id'] : $defaultLocationType->id
;
2544 $params['is_primary'] = 1;
2549 * Get the civicrm_mapping_field appropriate layout for the mapper input.
2551 * The input looks something like ['street_address', 1]
2552 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
2555 * @param array $fieldMapping
2556 * Field as submitted on the MapField form - this is a non-associative array,
2557 * the keys of which depend on the data/ field. Generally it will be one of
2559 * [$fieldName, $locationTypeID, $phoneTypeIDOrIMProviderIDIfRelevant],
2560 * [$fieldName, $websiteTypeID],
2561 * If the mapping is for a related contact it will be as above but the first
2562 * key will be the relationship key - eg. 5_a_b.
2563 * @param int $mappingID
2564 * @param int $columnNumber
2567 * @throws \API_Exception
2569 public function getMappingFieldFromMapperInput(array $fieldMapping, int $mappingID, int $columnNumber): array {
2570 $isRelationshipField = preg_match('/\d*_a_b|b_a$/', $fieldMapping[0]);
2571 $fieldName = $isRelationshipField ?
$fieldMapping[1] : $fieldMapping[0];
2572 $locationTypeID = NULL;
2573 $possibleLocationField = $isRelationshipField ?
2 : 1;
2574 if ($fieldName !== 'url' && is_numeric($fieldMapping[$possibleLocationField] ??
NULL)) {
2575 $locationTypeID = $fieldMapping[$possibleLocationField];
2578 'name' => $fieldName,
2579 'mapping_id' => $mappingID,
2580 'relationship_type_id' => $isRelationshipField ?
substr($fieldMapping[0], 0, -4) : NULL,
2581 'relationship_direction' => $isRelationshipField ?
substr($fieldMapping[0], -3) : NULL,
2582 'column_number' => $columnNumber,
2583 'contact_type' => $this->getContactType(),
2584 'website_type_id' => $fieldName !== 'url' ?
NULL : ($isRelationshipField ?
$fieldMapping[2] : $fieldMapping[1]),
2585 'phone_type_id' => $fieldName !== 'phone' ?
NULL : ($isRelationshipField ?
$fieldMapping[3] : $fieldMapping[2]),
2586 'im_provider_id' => $fieldName !== 'im' ?
NULL : ($isRelationshipField ?
$fieldMapping[3] : $fieldMapping[2]),
2587 'location_type_id' => $locationTypeID,
2592 * @param array $mappedField
2593 * Field detail as would be saved in field_mapping table
2594 * or as returned from getMappingFieldFromMapperInput
2597 * @throws \API_Exception
2599 public function getMappedFieldLabel(array $mappedField): string {
2600 $this->setFieldMetadata();
2602 if ($mappedField['relationship_type_id']) {
2603 $title[] = $this->getRelationshipLabel($mappedField['relationship_type_id'], $mappedField['relationship_direction']);
2605 $title[] = $this->getFieldMetadata($mappedField['name'])['title'];
2606 if ($mappedField['location_type_id']) {
2607 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_Address', 'location_type_id', $mappedField['location_type_id']);
2609 if ($mappedField['website_type_id']) {
2610 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_Website', 'website_type_id', $mappedField['website_type_id']);
2612 if ($mappedField['phone_type_id']) {
2613 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $mappedField['phone_type_id']);
2615 if ($mappedField['im_provider_id']) {
2616 $title[] = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_IM', 'provider_id', $mappedField['provider_id']);
2618 return implode(' - ', $title);
2622 * Get the relevant label for the relationship.
2625 * @param string $direction
2628 * @throws \API_Exception
2630 protected function getRelationshipLabel(int $id, string $direction): string {
2631 if (empty($this->relationshipLabels
[$id . $direction])) {
2632 $this->relationshipLabels
[$id . $direction] =
2633 $fieldName = 'label_' . $direction;
2634 $this->relationshipLabels
[$id . $direction] = (string) RelationshipType
::get(FALSE)
2635 ->addWhere('id', '=', $id)
2636 ->addSelect($fieldName)->execute()->first()[$fieldName];
2638 return $this->relationshipLabels
[$id . $direction];
2642 * Transform the input parameters into the form handled by the input routine.
2644 * @param array $values
2645 * Input parameters as they come in from the datasource
2646 * eg. ['Bob', 'Smith', 'bob@example.org', '123-456']
2649 * Parameters mapped to CiviCRM fields based on the mapping
2650 * and specified contact type. eg.
2652 * 'contact_type' => 'Individual',
2653 * 'first_name' => 'Bob',
2654 * 'last_name' => 'Smith',
2655 * 'phone' => ['phone' => '123', 'location_type_id' => 1, 'phone_type_id' => 1],
2656 * '5_a_b' => ['contact_type' => 'Organization', 'url' => ['url' => 'https://example.org', 'website_type_id' => 1]]
2657 * 'im' => ['im' => 'my-handle', 'location_type_id' => 1, 'provider_id' => 1],
2659 * @throws \API_Exception
2661 public function getMappedRow(array $values): array {
2662 $params = $this->getParams($values);
2663 $params['contact_type'] = $this->getContactType();
2664 if ($this->getContactSubType()) {
2665 $params['contact_sub_type'] = $this->getContactSubType();
2671 * Validate the import values.
2673 * The values array represents a row in the datasource.
2675 * @param array $values
2677 * @throws \API_Exception
2678 * @throws \CRM_Core_Exception
2680 public function validateValues(array $values): void
{
2681 $params = $this->getMappedRow($values);
2682 $contacts = array_merge(['0' => $params], $this->getRelatedContactsParams($params));
2683 foreach ($contacts as $value) {
2684 // If we are referencing a related contact, or are in update mode then we
2685 // don't need all the required fields if we have enough to find an existing contact.
2686 $useExistingMatchFields = !empty($value['relationship_type_id']) ||
$this->isUpdateExistingContacts();
2687 $this->validateRequiredContactFields($value['contact_type'], $value, $useExistingMatchFields, !empty($value['relationship_label']) ?
'(' . $value['relationship_label'] . ')' : '');
2690 //check for duplicate external Identifier
2691 $externalID = $params['external_identifier'] ??
NULL;
2693 /* If it's a dupe,external Identifier */
2695 if ($externalDupe = CRM_Utils_Array
::value($externalID, $this->_allExternalIdentifiers
)) {
2696 $errorMessage = ts('External ID conflicts with record %1', [1 => $externalDupe]);
2697 throw new CRM_Core_Exception($errorMessage);
2699 //otherwise, count it and move on
2700 $this->_allExternalIdentifiers
[$externalID] = $this->_lineCount
;
2703 //date-format part ends
2705 $errorMessage = NULL;
2706 //checking error in custom data
2707 $this->isErrorInCustomData($params, $errorMessage, $params['contact_sub_type'] ??
NULL);
2709 //checking error in core data
2710 $this->isErrorInCoreData($params, $errorMessage);
2711 if ($errorMessage) {
2712 $tempMsg = "Invalid value for field(s) : $errorMessage";
2713 throw new CRM_Core_Exception($tempMsg);
2718 * Get the field mappings for the import.
2720 * This is the same format as saved in civicrm_mapping_field except
2721 * that location_type_id = 'Primary' rather than empty where relevant.
2722 * Also 'im_provider_id' is mapped to the 'real' field name 'provider_id'
2725 * @throws \API_Exception
2727 protected function getFieldMappings(): array {
2729 foreach ($this->getSubmittedValue('mapper') as $i => $mapperRow) {
2730 $mappedField = $this->getMappingFieldFromMapperInput($mapperRow, 0, $i);
2731 if (!$mappedField['location_type_id'] && !empty($this->importableFieldsMetadata
[$mappedField['name']]['hasLocationType'])) {
2732 $mappedField['location_type_id'] = 'Primary';
2734 // Just for clarity since 0 is a pseudo-value
2735 unset($mappedField['mapping_id']);
2736 // Annoyingly the civicrm_mapping_field name for this differs from civicrm_im.
2737 // Test cover in `CRM_Contact_Import_Parser_ContactTest::testMapFields`
2738 $mappedField['provider_id'] = $mappedField['im_provider_id'];
2739 unset($mappedField['im_provider_id']);
2740 $mappedFields[] = $mappedField;
2742 return $mappedFields;
2746 * Get the related contact type.
2748 * @param int|null $relationshipTypeID
2749 * @param int|string $relationshipDirection
2751 * @return null|string
2753 * @throws \API_Exception
2755 protected function getRelatedContactType($relationshipTypeID, $relationshipDirection): ?
string {
2756 if (!$relationshipTypeID) {
2759 $relationshipField = 'contact_type_' . substr($relationshipDirection, -1);
2760 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2764 * Get the related contact type.
2766 * @param int|null $relationshipTypeID
2767 * @param int|string $relationshipDirection
2769 * @return null|string
2771 * @throws \API_Exception
2773 protected function getRelatedContactLabel($relationshipTypeID, $relationshipDirection): ?
string {
2774 $relationshipField = 'label_' . $relationshipDirection;
2775 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2779 * Get the relationship type.
2781 * @param int $relationshipTypeID
2784 * @throws \API_Exception
2786 protected function getRelationshipType(int $relationshipTypeID): array {
2787 $cacheKey = 'relationship_type' . $relationshipTypeID;
2788 if (!isset(Civi
::$statics[__CLASS__
][$cacheKey])) {
2789 Civi
::$statics[__CLASS__
][$cacheKey] = RelationshipType
::get(FALSE)
2790 ->addWhere('id', '=', $relationshipTypeID)
2791 ->addSelect('*')->execute()->first();
2793 return Civi
::$statics[__CLASS__
][$cacheKey];
2797 * Add the given field to the contact array.
2799 * @param array $contactArray
2800 * @param array $locationValues
2801 * @param string $fieldName
2802 * @param mixed $importedValue
2805 * @throws \API_Exception
2807 private function addFieldToParams(array &$contactArray, array $locationValues, string $fieldName, $importedValue): void
{
2808 if (!empty($locationValues)) {
2809 $locationValues[$fieldName] = $importedValue;
2810 $contactArray[$fieldName] = (array) ($contactArray[$fieldName] ??
[]);
2811 $contactArray[$fieldName][] = $locationValues;
2814 $contactArray[$fieldName] = $this->getTransformedFieldValue($fieldName, $importedValue);
2819 * Get any related contacts designated for update.
2821 * This extracts the parts that relate to separate related
2822 * contacts from the 'params' array.
2824 * It is probably a bit silly not to nest them more clearly in
2825 * `getParams` in the first place & maybe in future we can do that.
2827 * @param array $params
2830 * e.g ['5_a_b' => ['contact_type' => 'Organization', 'organization_name' => 'The Firm']]
2831 * @throws \API_Exception
2833 protected function getRelatedContactsParams(array $params): array {
2834 $relatedContacts = [];
2835 foreach ($params as $key => $value) {
2836 // If the key is a relationship key - eg. 5_a_b or 10_b_a
2837 // then the value is an array that describes an existing contact.
2838 // We need to check the fields are present to identify or create this
2840 if (preg_match('/^\d+_[a|b]_[a|b]$/', $key)) {
2841 $value['relationship_type_id'] = substr($key, 0, -4);
2842 $value['relationship_direction'] = substr($key, -3);
2843 $value['relationship_label'] = $this->getRelationshipLabel($value['relationship_type_id'], $value['relationship_direction']);
2844 $relatedContacts[$key] = $value;
2847 return $relatedContacts;
2851 * Look up for an existing contact with the given external_identifier.
2853 * If the identifier is found on a deleted contact then it is not a match
2854 * but it must be removed from that contact to allow the new contact to
2855 * have that external_identifier.
2857 * @param string|null $externalIdentifier
2858 * @param string $contactType
2862 * @throws \CRM_Core_Exception
2863 * @throws \CiviCRM_API3_Exception
2865 protected function lookupExternalIdentifier(?
string $externalIdentifier, string $contactType): ?
int {
2866 if (!$externalIdentifier) {
2869 // Check for any match on external id, deleted or otherwise.
2870 $foundContact = civicrm_api3('Contact', 'get', [
2871 'external_identifier' => $externalIdentifier,
2873 'sequential' => TRUE,
2874 'return' => ['id', 'contact_is_deleted', 'contact_type'],
2876 if (empty($foundContact['id'])) {
2879 if (!empty($foundContact['values'][0]['contact_is_deleted'])) {
2880 // If the contact is deleted, update external identifier to be blank
2881 // to avoid key error from MySQL.
2882 $params = ['id' => $foundContact['id'], 'external_identifier' => ''];
2883 civicrm_api3('Contact', 'create', $params);
2886 if ($foundContact['values'][0]['contact_type'] !== $contactType) {
2887 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser
::NO_MATCH
);
2889 return (int) $foundContact['id'];
2893 * Lookup the contact's contact ID.
2895 * @param array $params
2896 * @param bool $isDuplicateIfExternalIdentifierExists
2900 * @throws \API_Exception
2901 * @throws \CRM_Core_Exception
2902 * @throws \CiviCRM_API3_Exception
2904 protected function lookupContactID(array $params, bool $isDuplicateIfExternalIdentifierExists): ?
int {
2905 $extIDMatch = $this->lookupExternalIdentifier($params['external_identifier'] ??
NULL, $params['contact_type']);
2906 $contactID = !empty($params['id']) ?
(int) $params['id'] : NULL;
2907 //check if external identifier exists in database
2908 if ($extIDMatch && $contactID && $extIDMatch !== $contactID) {
2909 throw new CRM_Core_Exception(ts('Existing external ID does not match the imported contact ID.'), CRM_Import_Parser
::ERROR
);
2911 if ($extIDMatch && $isDuplicateIfExternalIdentifierExists) {
2912 throw new CRM_Core_Exception(ts('External ID already exists in Database.'), CRM_Import_Parser
::DUPLICATE
);
2915 $existingContact = Contact
::get(FALSE)
2916 ->addWhere('id', '=', $contactID)
2917 // Don't auto-filter deleted - people use import to undelete.
2918 ->addWhere('is_deleted', 'IN', [0, 1])
2919 ->addSelect('contact_type')->execute()->first();
2920 if (empty($existingContact['id'])) {
2921 throw new CRM_Core_Exception('No contact found for this contact ID:' . $params['id'], CRM_Import_Parser
::NO_MATCH
);
2923 if ($existingContact['contact_type'] !== $params['contact_type']) {
2924 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser
::NO_MATCH
);
2928 // Time to see if we can find an existing contact ID to make this an update
2930 if ($extIDMatch ||
$this->isUpdateExistingContacts()) {
2931 return $this->getPossibleContactMatch($params, $extIDMatch, $this->getSubmittedValue('dedupe_rule_id'));