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\UserJob
;
17 * @copyright CiviCRM LLC https://civicrm.org/licensing
19 abstract class CRM_Import_Parser
{
23 const MAX_WARNINGS
= 25, DEFAULT_TIMEOUT
= 30;
28 const VALID
= 1, WARNING
= 2, ERROR
= 4, CONFLICT
= 8, STOP
= 16, DUPLICATE
= 32, MULTIPLE_DUPE
= 64, NO_MATCH
= 128, UNPARSED_ADDRESS_WARNING
= 256;
33 const MODE_MAPFIELD
= 1, MODE_PREVIEW
= 2, MODE_SUMMARY
= 4, MODE_IMPORT
= 8;
36 * Codes for duplicate record handling
38 const DUPLICATE_SKIP
= 1, DUPLICATE_REPLACE
= 2, DUPLICATE_UPDATE
= 4, DUPLICATE_FILL
= 8, DUPLICATE_NOCHECK
= 16;
43 const CONTACT_INDIVIDUAL
= 1, CONTACT_HOUSEHOLD
= 2, CONTACT_ORGANIZATION
= 4;
49 * This is the primary key of the civicrm_user_job table which is used to
59 public function getUserJobID(): ?
int {
60 return $this->userJobID
;
66 * @param int $userJobID
70 public function setUserJobID(int $userJobID): self
{
71 $this->userJobID
= $userJobID;
78 * API call to retrieve the userJob row.
82 * @throws \API_Exception
84 protected function getUserJob(): array {
86 ->addWhere('id', '=', $this->getUserJobID())
92 * Get the relevant datasource object.
94 * @return \CRM_Import_DataSource|null
96 * @throws \API_Exception
98 protected function getDataSourceObject(): ?CRM_Import_DataSource
{
99 $className = $this->getSubmittedValue('dataSource');
101 /* @var CRM_Import_DataSource $dataSource */
102 return new $className($this->getUserJobID());
108 * Get the submitted value, as stored on the user job.
110 * @param string $fieldName
114 * @throws \API_Exception
116 protected function getSubmittedValue(string $fieldName) {
117 return $this->getUserJob()['metadata']['submitted_values'][$fieldName];
121 * Has the import completed.
125 * @throws \API_Exception
126 * @throws \CRM_Core_Exception
128 public function isComplete() :bool {
129 return $this->getDataSourceObject()->isCompleted();
133 * Get configured contact type.
135 * @throws \API_Exception
137 protected function getContactType() {
138 if (!$this->_contactType
) {
139 $contactTypeMapping = [
140 CRM_Import_Parser
::CONTACT_INDIVIDUAL
=> 'Individual',
141 CRM_Import_Parser
::CONTACT_HOUSEHOLD
=> 'Household',
142 CRM_Import_Parser
::CONTACT_ORGANIZATION
=> 'Organization',
144 $this->_contactType
= $contactTypeMapping[$this->getSubmittedValue('contactType')];
146 return $this->_contactType
;
150 * Get configured contact type.
152 * @return string|null
154 * @throws \API_Exception
156 public function getContactSubType() {
157 if (!$this->_contactSubType
) {
158 $this->_contactSubType
= $this->getSubmittedValue('contactSubType');
160 return $this->_contactSubType
;
164 * Total number of non empty lines
167 protected $_totalCount;
170 * Running total number of valid lines
173 protected $_validCount;
176 * Running total number of invalid rows
179 protected $_invalidRowCount;
182 * Maximum number of non-empty/comment lines to process
186 protected $_maxLinesToProcess;
189 * Array of error lines, bounded by MAX_ERROR
195 * Total number of duplicate (from database) lines
198 protected $_duplicateCount;
201 * Array of duplicate lines
204 protected $_duplicates;
207 * Maximum number of warnings to store
210 protected $_maxWarningCount = self
::MAX_WARNINGS
;
213 * Array of warning lines, bounded by MAX_WARNING
216 protected $_warnings;
219 * Array of all the fields that could potentially be part
220 * of this import process
226 * Metadata for all available fields, keyed by unique name.
228 * This is intended to supercede $_fields which uses a special sauce format which
229 * importableFieldsMetadata uses the standard getfields type format.
233 protected $importableFieldsMetadata = [];
236 * Get metadata for all importable fields in std getfields style format.
240 public function getImportableFieldsMetadata(): array {
241 return $this->importableFieldsMetadata
;
245 * Set metadata for all importable fields in std getfields style format.
247 * @param array $importableFieldsMetadata
249 public function setImportableFieldsMetadata(array $importableFieldsMetadata): void
{
250 $this->importableFieldsMetadata
= $importableFieldsMetadata;
254 * Array of the fields that are actually part of the import process
255 * the position in the array also dictates their position in the import
259 protected $_activeFields;
262 * Cache the count of active fields
266 protected $_activeFieldCount;
269 * Cache of preview rows
276 * Filename of error data
280 protected $_errorFileName;
283 * Filename of duplicate data
287 protected $_duplicateFileName;
294 public $_contactType;
297 * @param string $contactType
299 * @return CRM_Import_Parser
301 public function setContactType(string $contactType): CRM_Import_Parser
{
302 $this->_contactType
= $contactType;
311 public $_contactSubType;
314 * @param int|null $contactSubType
318 public function setContactSubType(?
int $contactSubType): self
{
319 $this->_contactSubType
= $contactSubType;
326 public function __construct() {
327 $this->_maxLinesToProcess
= 0;
331 * Set and validate field values.
333 * @param array $elements
336 public function setActiveFieldValues($elements): void
{
337 $maxCount = count($elements) < $this->_activeFieldCount ?
count($elements) : $this->_activeFieldCount
;
338 for ($i = 0; $i < $maxCount; $i++
) {
339 $this->_activeFields
[$i]->setValue($elements[$i]);
342 // reset all the values that we did not have an equivalent import element
343 for (; $i < $this->_activeFieldCount
; $i++
) {
344 $this->_activeFields
[$i]->resetValue();
349 * Format the field values for input to the api.
352 * (reference) associative array of name/value pairs
354 public function &getActiveFieldParams() {
356 for ($i = 0; $i < $this->_activeFieldCount
; $i++
) {
357 if (isset($this->_activeFields
[$i]->_value
)
358 && !isset($params[$this->_activeFields
[$i]->_name
])
359 && !isset($this->_activeFields
[$i]->_related
)
362 $params[$this->_activeFields
[$i]->_name
] = $this->_activeFields
[$i]->_value
;
369 * Add progress bar to the import process. Calculates time remaining, status etc.
372 * status id of the import process saved in $config->uploadDir.
373 * @param bool $startImport
374 * True when progress bar is to be initiated.
375 * @param $startTimestamp
376 * Initial timestamp when the import was started.
377 * @param $prevTimestamp
378 * Previous timestamp when this function was last called.
379 * @param $totalRowCount
380 * Total number of rows in the import file.
382 * @return NULL|$currTimestamp
384 public function progressImport($statusID, $startImport = TRUE, $startTimestamp = NULL, $prevTimestamp = NULL, $totalRowCount = NULL) {
385 $statusFile = CRM_Core_Config
::singleton()->uploadDir
. "status_{$statusID}.txt";
388 $status = "<div class='description'> " . ts('No processing status reported yet.') . "</div>";
389 //do not force the browser to display the save dialog, CRM-7640
390 $contents = json_encode([0, $status]);
391 file_put_contents($statusFile, $contents);
394 $rowCount = $this->_rowCount ??
$this->_lineCount
;
395 $currTimestamp = time();
396 $time = ($currTimestamp - $prevTimestamp);
397 $recordsLeft = $totalRowCount - $rowCount;
398 if ($recordsLeft < 0) {
401 $estimatedTime = ($recordsLeft / 50) * $time;
402 $estMinutes = floor($estimatedTime / 60);
404 if ($estMinutes > 1) {
405 $timeFormatted = $estMinutes . ' ' . ts('minutes') . ' ';
406 $estimatedTime = $estimatedTime - ($estMinutes * 60);
408 $timeFormatted .= round($estimatedTime) . ' ' . ts('seconds');
409 $processedPercent = (int ) (($rowCount * 100) / $totalRowCount);
410 $statusMsg = ts('%1 of %2 records - %3 remaining',
411 [1 => $rowCount, 2 => $totalRowCount, 3 => $timeFormatted]
413 $status = "<div class=\"description\"> <strong>{$statusMsg}</strong></div>";
414 $contents = json_encode([$processedPercent, $status]);
416 file_put_contents($statusFile, $contents);
417 return $currTimestamp;
424 public function getSelectValues(): array {
426 foreach ($this->_fields
as $name => $field) {
427 $values[$name] = $field->_title
;
435 public function getSelectTypes() {
437 // This is only called from the MapField form in isolation now,
438 // so we need to set the metadata.
440 foreach ($this->_fields
as $name => $field) {
441 if (isset($field->_hasLocationType
)) {
442 $values[$name] = $field->_hasLocationType
;
451 public function getHeaderPatterns() {
453 foreach ($this->_fields
as $name => $field) {
454 if (isset($field->_headerPattern
)) {
455 $values[$name] = $field->_headerPattern
;
464 public function getDataPatterns() {
466 foreach ($this->_fields
as $name => $field) {
467 $values[$name] = $field->_dataPattern
;
473 * Remove single-quote enclosures from a value array (row).
475 * @param array $values
476 * @param string $enclosure
480 public static function encloseScrub(&$values, $enclosure = "'") {
481 if (empty($values)) {
485 foreach ($values as $k => $v) {
486 $values[$k] = preg_replace("/^$enclosure(.*)$enclosure$/", '$1', $v);
497 public function setMaxLinesToProcess($max) {
498 $this->_maxLinesToProcess
= $max;
502 * Validate that we have the required fields to create the contact or find it to update.
504 * Note that the users duplicate selection affects this as follows
505 * - if they did not select an update variant then the id field is not
506 * permitted in the mapping - so we can assume the presence of id means
508 * - the external_identifier field is valid in place of the other fields
509 * when they have chosen update or fill - in this case we are only looking
510 * to update an existing contact.
512 * @param string $contactType
513 * @param array $params
514 * @param bool $isPermitExistingMatchFields
517 * @throws \CRM_Core_Exception
519 protected function validateRequiredContactFields(string $contactType, array $params, bool $isPermitExistingMatchFields = TRUE): void
{
520 if (!empty($params['id'])) {
525 'first_name_last_name' => ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')],
526 'email' => ts('Email Address'),
528 'Organization' => ['organization_name' => ts('Organization Name')],
529 'Household' => ['household_name' => ts('Household Name')],
531 if ($isPermitExistingMatchFields) {
532 $requiredFields['external_identifier'] = ts('External Identifier');
533 // Historically just an email has been accepted as it is 'usually good enough'
534 // for a dedupe rule look up - but really this is a stand in for
535 // whatever is needed to find an existing matching contact using the
536 // specified dedupe rule (or the default Unsupervised if not specified).
537 $requiredFields['email'] = ts('Email Address');
539 $this->validateRequiredFields($requiredFields, $params);
543 * Determines the file extension based on error code.
545 * @var int $type error code constant
548 public static function errorFileName($type) {
554 $config = CRM_Core_Config
::singleton();
555 $fileName = $config->uploadDir
. "sqlImport";
558 $fileName .= '.errors';
561 case self
::DUPLICATE
:
562 $fileName .= '.duplicates';
566 $fileName .= '.mismatch';
569 case self
::UNPARSED_ADDRESS_WARNING
:
570 $fileName .= '.unparsedAddress';
578 * Determines the file name based on error code.
580 * @var $type error code constant
583 public static function saveFileName($type) {
590 $fileName = 'Import_Errors.csv';
593 case self
::DUPLICATE
:
594 $fileName = 'Import_Duplicates.csv';
598 $fileName = 'Import_Mismatch.csv';
601 case self
::UNPARSED_ADDRESS_WARNING
:
602 $fileName = 'Import_Unparsed_Address.csv';
610 * Check if contact is a duplicate .
612 * @param array $formatValues
616 protected function checkContactDuplicate(&$formatValues) {
617 //retrieve contact id using contact dedupe rule
618 $formatValues['contact_type'] = $formatValues['contact_type'] ??
$this->_contactType
;
619 $formatValues['version'] = 3;
620 require_once 'CRM/Utils/DeprecatedUtils.php';
621 $params = $formatValues;
622 static $cIndieFields = NULL;
623 static $defaultLocationId = NULL;
625 $contactType = $params['contact_type'];
626 if ($cIndieFields == NULL) {
627 $cTempIndieFields = CRM_Contact_BAO_Contact
::importableFields($contactType);
628 $cIndieFields = $cTempIndieFields;
630 $defaultLocation = CRM_Core_BAO_LocationType
::getDefault();
632 // set the value to default location id else set to 1
633 if (!$defaultLocationId = (int) $defaultLocation->id
) {
634 $defaultLocationId = 1;
638 $locationFields = CRM_Contact_BAO_Query
::$_locationSpecificFields;
640 $contactFormatted = [];
641 foreach ($params as $key => $field) {
642 if ($field == NULL ||
$field === '') {
645 // CRM-17040, Considering only primary contact when importing contributions. So contribution inserts into primary contact
646 // instead of soft credit contact.
647 if (is_array($field) && $key != "soft_credit") {
648 foreach ($field as $value) {
650 if (is_array($value)) {
651 foreach ($value as $name => $testForEmpty) {
652 if ($name !== 'phone_type' &&
653 ($testForEmpty === '' ||
$testForEmpty == NULL)
664 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
670 $value = [$key => $field];
672 // check if location related field, then we need to add primary location type
673 if (in_array($key, $locationFields)) {
674 $value['location_type_id'] = $defaultLocationId;
676 elseif (array_key_exists($key, $cIndieFields)) {
677 $value['contact_type'] = $contactType;
680 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
683 $contactFormatted['contact_type'] = $contactType;
685 return _civicrm_api3_deprecated_duplicate_formatted_contact($contactFormatted);
689 * This function adds the contact variable in $values to the
690 * parameter list $params. For most cases, $values should have length 1. If
691 * the variable being added is a child of Location, a location_type_id must
692 * also be included. If it is a child of phone, a phone_type must be included.
694 * @param array $values
695 * The variable(s) to be added.
696 * @param array $params
697 * The structured parameter list.
699 * @return bool|CRM_Utils_Error
701 private function _civicrm_api3_deprecated_add_formatted_param(&$values, &$params) {
702 // @todo - like most functions in import ... most of this is cruft....
703 // Crawl through the possible classes:
716 // Cache the various object fields
717 static $fields = NULL;
719 if ($fields == NULL) {
723 // first add core contact values since for other Civi modules they are not added
724 require_once 'CRM/Contact/BAO/Contact.php';
725 $contactFields = CRM_Contact_DAO_Contact
::fields();
726 _civicrm_api3_store_values($contactFields, $values, $params);
728 if (isset($values['contact_type'])) {
729 // we're an individual/household/org property
731 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact
::fields();
733 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
737 if (isset($values['individual_prefix'])) {
738 if (!empty($params['prefix_id'])) {
739 $prefixes = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'prefix_id');
740 $params['prefix'] = $prefixes[$params['prefix_id']];
743 $params['prefix'] = $values['individual_prefix'];
748 if (isset($values['individual_suffix'])) {
749 if (!empty($params['suffix_id'])) {
750 $suffixes = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'suffix_id');
751 $params['suffix'] = $suffixes[$params['suffix_id']];
754 $params['suffix'] = $values['individual_suffix'];
760 if (isset($values['email_greeting'])) {
761 if (!empty($params['email_greeting_id'])) {
762 $emailGreetingFilter = [
763 'contact_type' => $params['contact_type'] ??
NULL,
764 'greeting_type' => 'email_greeting',
766 $emailGreetings = CRM_Core_PseudoConstant
::greeting($emailGreetingFilter);
767 $params['email_greeting'] = $emailGreetings[$params['email_greeting_id']];
770 $params['email_greeting'] = $values['email_greeting'];
776 if (isset($values['postal_greeting'])) {
777 if (!empty($params['postal_greeting_id'])) {
778 $postalGreetingFilter = [
779 'contact_type' => $params['contact_type'] ??
NULL,
780 'greeting_type' => 'postal_greeting',
782 $postalGreetings = CRM_Core_PseudoConstant
::greeting($postalGreetingFilter);
783 $params['postal_greeting'] = $postalGreetings[$params['postal_greeting_id']];
786 $params['postal_greeting'] = $values['postal_greeting'];
791 if (isset($values['addressee'])) {
792 $params['addressee'] = $values['addressee'];
796 if (isset($values['gender'])) {
797 if (!empty($params['gender_id'])) {
798 $genders = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'gender_id');
799 $params['gender'] = $genders[$params['gender_id']];
802 $params['gender'] = $values['gender'];
807 if (!empty($values['preferred_communication_method'])) {
809 $pcm = array_change_key_case(array_flip(CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'preferred_communication_method')), CASE_LOWER
);
811 $preffComm = explode(',', $values['preferred_communication_method']);
812 foreach ($preffComm as $v) {
813 $v = strtolower(trim($v));
814 if (array_key_exists($v, $pcm)) {
819 $params['preferred_communication_method'] = $comm;
823 // format the website params.
824 if (!empty($values['url'])) {
825 static $websiteFields;
826 if (!is_array($websiteFields)) {
827 require_once 'CRM/Core/DAO/Website.php';
828 $websiteFields = CRM_Core_DAO_Website
::fields();
830 if (!array_key_exists('website', $params) ||
831 !is_array($params['website'])
833 $params['website'] = [];
836 $websiteCount = count($params['website']);
837 _civicrm_api3_store_values($websiteFields, $values,
838 $params['website'][++
$websiteCount]
844 // get the formatted location blocks into params - w/ 3.0 format, CRM-4605
845 if (!empty($values['location_type_id'])) {
846 static $fields = NULL;
847 if ($fields == NULL) {
851 foreach (['Phone', 'Email', 'IM', 'OpenID', 'Phone_Ext'] as $block) {
852 $name = strtolower($block);
853 if (!array_key_exists($name, $values)) {
857 if ($name === 'phone_ext') {
861 // block present in value array.
862 if (!array_key_exists($name, $params) ||
!is_array($params[$name])) {
866 if (!array_key_exists($block, $fields)) {
867 $className = "CRM_Core_DAO_$block";
868 $fields[$block] =& $className::fields();
871 $blockCnt = count($params[$name]);
873 // copy value to dao field name.
875 $values['name'] = $values[$name];
878 _civicrm_api3_store_values($fields[$block], $values,
879 $params[$name][++
$blockCnt]
882 if (empty($params['id']) && ($blockCnt == 1)) {
883 $params[$name][$blockCnt]['is_primary'] = TRUE;
886 // we only process single block at a time.
890 // handle address fields.
891 if (!array_key_exists('address', $params) ||
!is_array($params['address'])) {
892 $params['address'] = [];
896 foreach ($params['address'] as $cnt => $addressBlock) {
897 if (CRM_Utils_Array
::value('location_type_id', $values) ==
898 CRM_Utils_Array
::value('location_type_id', $addressBlock)
906 if (!array_key_exists('Address', $fields)) {
907 $fields['Address'] = CRM_Core_DAO_Address
::fields();
910 // Note: we doing multiple value formatting here for address custom fields, plus putting into right format.
911 // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving
912 // the address in CRM_Core_BAO_Address::create method
913 if (!empty($values['location_type_id'])) {
914 static $customFields = [];
915 if (empty($customFields)) {
916 $customFields = CRM_Core_BAO_CustomField
::getFields('Address');
918 // make a copy of values, as we going to make changes
919 $newValues = $values;
920 foreach ($values as $key => $val) {
921 $customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key);
922 if ($customFieldID && array_key_exists($customFieldID, $customFields)) {
923 // mark an entry in fields array since we want the value of custom field to be copied
924 $fields['Address'][$key] = NULL;
926 $htmlType = $customFields[$customFieldID]['html_type'] ??
NULL;
927 if (CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID]) && $val) {
928 $mulValues = explode(',', $val);
929 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
930 $newValues[$key] = [];
931 foreach ($mulValues as $v1) {
932 foreach ($customOption as $v2) {
933 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
934 (strtolower($v2['value']) == strtolower(trim($v1)))
936 if ($htmlType == 'CheckBox') {
937 $newValues[$key][$v2['value']] = 1;
940 $newValues[$key][] = $v2['value'];
948 // consider new values
949 $values = $newValues;
952 _civicrm_api3_store_values($fields['Address'], $values, $params['address'][$addressCnt]);
958 'supplemental_address_1',
959 'supplemental_address_2',
960 'supplemental_address_3',
961 'StateProvince.name',
964 foreach ($addressFields as $field) {
965 if (array_key_exists($field, $values)) {
966 if (!array_key_exists('address', $params)) {
967 $params['address'] = [];
969 $params['address'][$addressCnt][$field] = $values[$field];
973 if ($addressCnt == 1) {
975 $params['address'][$addressCnt]['is_primary'] = TRUE;
980 if (isset($values['note'])) {
982 if (!isset($params['note'])) {
983 $params['note'] = [];
985 $noteBlock = count($params['note']) +
1;
987 $params['note'][$noteBlock] = [];
988 if (!isset($fields['Note'])) {
989 $fields['Note'] = CRM_Core_DAO_Note
::fields();
992 // get the current logged in civicrm user
993 $session = CRM_Core_Session
::singleton();
994 $userID = $session->get('userID');
997 $values['contact_id'] = $userID;
1000 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
1005 // Check for custom field values
1007 if (empty($fields['custom'])) {
1008 $fields['custom'] = &CRM_Core_BAO_CustomField
::getFields(CRM_Utils_Array
::value('contact_type', $values),
1009 FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE
1013 foreach ($values as $key => $value) {
1014 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
1015 // check if it's a valid custom field id
1017 if (!array_key_exists($customFieldID, $fields['custom'])) {
1018 return civicrm_api3_create_error('Invalid custom field ID');
1021 $params[$key] = $value;
1028 * Parse a field which could be represented by a label or name value rather than the DB value.
1030 * We will try to match name first or (per https://lab.civicrm.org/dev/core/issues/1285 if we have an id.
1032 * but if not available then see if we have a label that can be converted to a name.
1034 * @param string|int|null $submittedValue
1035 * @param array $fieldSpec
1036 * Metadata for the field
1040 protected function parsePseudoConstantField($submittedValue, $fieldSpec) {
1041 // dev/core#1289 Somehow we have wound up here but the BAO has not been specified in the fieldspec so we need to check this but future us problem, for now lets just return the submittedValue
1042 if (!isset($fieldSpec['bao'])) {
1043 return $submittedValue;
1045 /* @var \CRM_Core_DAO $bao */
1046 $bao = $fieldSpec['bao'];
1047 // For historical reasons use validate as context - ie disabled name matches ARE permitted.
1048 $nameOptions = $bao::buildOptions($fieldSpec['name'], 'validate');
1049 if (isset($nameOptions[$submittedValue])) {
1050 return $submittedValue;
1052 if (in_array($submittedValue, $nameOptions)) {
1053 return array_search($submittedValue, $nameOptions, TRUE);
1056 $labelOptions = array_flip($bao::buildOptions($fieldSpec['name'], 'match'));
1057 if (isset($labelOptions[$submittedValue])) {
1058 return array_search($labelOptions[$submittedValue], $nameOptions, TRUE);
1064 * This is code extracted from 4 places where this exact snippet was being duplicated.
1066 * FIXME: Extracting this was a first step, but there's also
1067 * 1. Inconsistency in the way other select options are handled.
1068 * Contribution adds handling for Select/Radio/Autocomplete
1069 * Participant/Activity only handles Select/Radio and misses Autocomplete
1070 * Membership is missing all of it
1071 * 2. Inconsistency with the way this works vs. how it's implemented in Contact import.
1073 * @param $customFieldID
1078 public static function unserializeCustomValue($customFieldID, $value, $fieldType) {
1079 $mulValues = explode(',', $value);
1080 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1082 foreach ($mulValues as $v1) {
1083 foreach ($customOption as $customValueID => $customLabel) {
1084 $customValue = $customLabel['value'];
1085 if ((strtolower(trim($customLabel['label'])) == strtolower(trim($v1))) ||
1086 (strtolower(trim($customValue)) == strtolower(trim($v1)))
1088 $values[] = $customValue;
1096 * Get the ids of any contacts that match according to the rule.
1098 * @param array $formatted
1102 protected function getIdsOfMatchingContacts(array $formatted):array {
1103 // the call to the deprecated function seems to add no value other that to do an additional
1104 // check for the contact_id & type.
1105 $error = _civicrm_api3_deprecated_duplicate_formatted_contact($formatted);
1106 if (!CRM_Core_Error
::isAPIError($error, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
1109 if (is_array($error['error_message']['params'][0])) {
1110 return $error['error_message']['params'][0];
1113 return explode(',', $error['error_message']['params'][0]);
1118 * Validate that the field requirements are met in the params.
1120 * @param array $requiredFields
1121 * @param array $params
1122 * An array of required fields (fieldName => label)
1123 * - note this follows the and / or array nesting we see in permission checks
1126 * 'email' => ts('Email'),
1127 * ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')]
1129 * Means 'email' OR 'first_name AND 'last_name'.
1131 * @throws \CRM_Core_Exception
1132 * Exception thrown if field requirements are not met.
1134 protected function validateRequiredFields(array $requiredFields, array $params): void
{
1135 $missingFields = [];
1136 foreach ($requiredFields as $key => $required) {
1137 if (!is_array($required)) {
1138 $importParameter = $params[$key] ??
[];
1139 if (!is_array($importParameter)) {
1140 if (!empty($importParameter)) {
1145 foreach ($importParameter as $locationValues) {
1146 if (!empty($locationValues[$key])) {
1152 $missingFields[$key] = $required;
1155 foreach ($required as $field => $label) {
1156 if (empty($params[$field])) {
1157 $missing[$field] = $label;
1160 if (empty($missing)) {
1163 $missingFields[$key] = implode(' ' . ts('and') . ' ', $missing);
1166 throw new CRM_Core_Exception(ts('Missing required fields:') . ' ' . implode(' ' . ts('OR') . ' ', $missingFields));