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 * Determines the file extension based on error code.
504 * @var int $type error code constant
507 public static function errorFileName($type) {
513 $config = CRM_Core_Config
::singleton();
514 $fileName = $config->uploadDir
. "sqlImport";
517 $fileName .= '.errors';
520 case self
::DUPLICATE
:
521 $fileName .= '.duplicates';
525 $fileName .= '.mismatch';
528 case self
::UNPARSED_ADDRESS_WARNING
:
529 $fileName .= '.unparsedAddress';
537 * Determines the file name based on error code.
539 * @var $type error code constant
542 public static function saveFileName($type) {
549 $fileName = 'Import_Errors.csv';
552 case self
::DUPLICATE
:
553 $fileName = 'Import_Duplicates.csv';
557 $fileName = 'Import_Mismatch.csv';
560 case self
::UNPARSED_ADDRESS_WARNING
:
561 $fileName = 'Import_Unparsed_Address.csv';
569 * Check if contact is a duplicate .
571 * @param array $formatValues
575 protected function checkContactDuplicate(&$formatValues) {
576 //retrieve contact id using contact dedupe rule
577 $formatValues['contact_type'] = $formatValues['contact_type'] ??
$this->_contactType
;
578 $formatValues['version'] = 3;
579 require_once 'CRM/Utils/DeprecatedUtils.php';
580 $params = $formatValues;
581 static $cIndieFields = NULL;
582 static $defaultLocationId = NULL;
584 $contactType = $params['contact_type'];
585 if ($cIndieFields == NULL) {
586 $cTempIndieFields = CRM_Contact_BAO_Contact
::importableFields($contactType);
587 $cIndieFields = $cTempIndieFields;
589 $defaultLocation = CRM_Core_BAO_LocationType
::getDefault();
591 // set the value to default location id else set to 1
592 if (!$defaultLocationId = (int) $defaultLocation->id
) {
593 $defaultLocationId = 1;
597 $locationFields = CRM_Contact_BAO_Query
::$_locationSpecificFields;
599 $contactFormatted = [];
600 foreach ($params as $key => $field) {
601 if ($field == NULL ||
$field === '') {
604 // CRM-17040, Considering only primary contact when importing contributions. So contribution inserts into primary contact
605 // instead of soft credit contact.
606 if (is_array($field) && $key != "soft_credit") {
607 foreach ($field as $value) {
609 if (is_array($value)) {
610 foreach ($value as $name => $testForEmpty) {
611 if ($name !== 'phone_type' &&
612 ($testForEmpty === '' ||
$testForEmpty == NULL)
623 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
629 $value = [$key => $field];
631 // check if location related field, then we need to add primary location type
632 if (in_array($key, $locationFields)) {
633 $value['location_type_id'] = $defaultLocationId;
635 elseif (array_key_exists($key, $cIndieFields)) {
636 $value['contact_type'] = $contactType;
639 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
642 $contactFormatted['contact_type'] = $contactType;
644 return _civicrm_api3_deprecated_duplicate_formatted_contact($contactFormatted);
648 * This function adds the contact variable in $values to the
649 * parameter list $params. For most cases, $values should have length 1. If
650 * the variable being added is a child of Location, a location_type_id must
651 * also be included. If it is a child of phone, a phone_type must be included.
653 * @param array $values
654 * The variable(s) to be added.
655 * @param array $params
656 * The structured parameter list.
658 * @return bool|CRM_Utils_Error
660 private function _civicrm_api3_deprecated_add_formatted_param(&$values, &$params) {
661 // @todo - like most functions in import ... most of this is cruft....
662 // Crawl through the possible classes:
675 // Cache the various object fields
676 static $fields = NULL;
678 if ($fields == NULL) {
682 // first add core contact values since for other Civi modules they are not added
683 require_once 'CRM/Contact/BAO/Contact.php';
684 $contactFields = CRM_Contact_DAO_Contact
::fields();
685 _civicrm_api3_store_values($contactFields, $values, $params);
687 if (isset($values['contact_type'])) {
688 // we're an individual/household/org property
690 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact
::fields();
692 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
696 if (isset($values['individual_prefix'])) {
697 if (!empty($params['prefix_id'])) {
698 $prefixes = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'prefix_id');
699 $params['prefix'] = $prefixes[$params['prefix_id']];
702 $params['prefix'] = $values['individual_prefix'];
707 if (isset($values['individual_suffix'])) {
708 if (!empty($params['suffix_id'])) {
709 $suffixes = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'suffix_id');
710 $params['suffix'] = $suffixes[$params['suffix_id']];
713 $params['suffix'] = $values['individual_suffix'];
719 if (isset($values['email_greeting'])) {
720 if (!empty($params['email_greeting_id'])) {
721 $emailGreetingFilter = [
722 'contact_type' => $params['contact_type'] ??
NULL,
723 'greeting_type' => 'email_greeting',
725 $emailGreetings = CRM_Core_PseudoConstant
::greeting($emailGreetingFilter);
726 $params['email_greeting'] = $emailGreetings[$params['email_greeting_id']];
729 $params['email_greeting'] = $values['email_greeting'];
735 if (isset($values['postal_greeting'])) {
736 if (!empty($params['postal_greeting_id'])) {
737 $postalGreetingFilter = [
738 'contact_type' => $params['contact_type'] ??
NULL,
739 'greeting_type' => 'postal_greeting',
741 $postalGreetings = CRM_Core_PseudoConstant
::greeting($postalGreetingFilter);
742 $params['postal_greeting'] = $postalGreetings[$params['postal_greeting_id']];
745 $params['postal_greeting'] = $values['postal_greeting'];
750 if (isset($values['addressee'])) {
751 $params['addressee'] = $values['addressee'];
755 if (isset($values['gender'])) {
756 if (!empty($params['gender_id'])) {
757 $genders = CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'gender_id');
758 $params['gender'] = $genders[$params['gender_id']];
761 $params['gender'] = $values['gender'];
766 if (!empty($values['preferred_communication_method'])) {
768 $pcm = array_change_key_case(array_flip(CRM_Core_PseudoConstant
::get('CRM_Contact_DAO_Contact', 'preferred_communication_method')), CASE_LOWER
);
770 $preffComm = explode(',', $values['preferred_communication_method']);
771 foreach ($preffComm as $v) {
772 $v = strtolower(trim($v));
773 if (array_key_exists($v, $pcm)) {
778 $params['preferred_communication_method'] = $comm;
782 // format the website params.
783 if (!empty($values['url'])) {
784 static $websiteFields;
785 if (!is_array($websiteFields)) {
786 require_once 'CRM/Core/DAO/Website.php';
787 $websiteFields = CRM_Core_DAO_Website
::fields();
789 if (!array_key_exists('website', $params) ||
790 !is_array($params['website'])
792 $params['website'] = [];
795 $websiteCount = count($params['website']);
796 _civicrm_api3_store_values($websiteFields, $values,
797 $params['website'][++
$websiteCount]
803 // get the formatted location blocks into params - w/ 3.0 format, CRM-4605
804 if (!empty($values['location_type_id'])) {
805 static $fields = NULL;
806 if ($fields == NULL) {
810 foreach (['Phone', 'Email', 'IM', 'OpenID', 'Phone_Ext'] as $block) {
811 $name = strtolower($block);
812 if (!array_key_exists($name, $values)) {
816 if ($name === 'phone_ext') {
820 // block present in value array.
821 if (!array_key_exists($name, $params) ||
!is_array($params[$name])) {
825 if (!array_key_exists($block, $fields)) {
826 $className = "CRM_Core_DAO_$block";
827 $fields[$block] =& $className::fields();
830 $blockCnt = count($params[$name]);
832 // copy value to dao field name.
834 $values['name'] = $values[$name];
837 _civicrm_api3_store_values($fields[$block], $values,
838 $params[$name][++
$blockCnt]
841 if (empty($params['id']) && ($blockCnt == 1)) {
842 $params[$name][$blockCnt]['is_primary'] = TRUE;
845 // we only process single block at a time.
849 // handle address fields.
850 if (!array_key_exists('address', $params) ||
!is_array($params['address'])) {
851 $params['address'] = [];
855 foreach ($params['address'] as $cnt => $addressBlock) {
856 if (CRM_Utils_Array
::value('location_type_id', $values) ==
857 CRM_Utils_Array
::value('location_type_id', $addressBlock)
865 if (!array_key_exists('Address', $fields)) {
866 $fields['Address'] = CRM_Core_DAO_Address
::fields();
869 // Note: we doing multiple value formatting here for address custom fields, plus putting into right format.
870 // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving
871 // the address in CRM_Core_BAO_Address::create method
872 if (!empty($values['location_type_id'])) {
873 static $customFields = [];
874 if (empty($customFields)) {
875 $customFields = CRM_Core_BAO_CustomField
::getFields('Address');
877 // make a copy of values, as we going to make changes
878 $newValues = $values;
879 foreach ($values as $key => $val) {
880 $customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key);
881 if ($customFieldID && array_key_exists($customFieldID, $customFields)) {
882 // mark an entry in fields array since we want the value of custom field to be copied
883 $fields['Address'][$key] = NULL;
885 $htmlType = $customFields[$customFieldID]['html_type'] ??
NULL;
886 if (CRM_Core_BAO_CustomField
::isSerialized($customFields[$customFieldID]) && $val) {
887 $mulValues = explode(',', $val);
888 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
889 $newValues[$key] = [];
890 foreach ($mulValues as $v1) {
891 foreach ($customOption as $v2) {
892 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
893 (strtolower($v2['value']) == strtolower(trim($v1)))
895 if ($htmlType == 'CheckBox') {
896 $newValues[$key][$v2['value']] = 1;
899 $newValues[$key][] = $v2['value'];
907 // consider new values
908 $values = $newValues;
911 _civicrm_api3_store_values($fields['Address'], $values, $params['address'][$addressCnt]);
917 'supplemental_address_1',
918 'supplemental_address_2',
919 'supplemental_address_3',
920 'StateProvince.name',
923 foreach ($addressFields as $field) {
924 if (array_key_exists($field, $values)) {
925 if (!array_key_exists('address', $params)) {
926 $params['address'] = [];
928 $params['address'][$addressCnt][$field] = $values[$field];
932 if ($addressCnt == 1) {
934 $params['address'][$addressCnt]['is_primary'] = TRUE;
939 if (isset($values['note'])) {
941 if (!isset($params['note'])) {
942 $params['note'] = [];
944 $noteBlock = count($params['note']) +
1;
946 $params['note'][$noteBlock] = [];
947 if (!isset($fields['Note'])) {
948 $fields['Note'] = CRM_Core_DAO_Note
::fields();
951 // get the current logged in civicrm user
952 $session = CRM_Core_Session
::singleton();
953 $userID = $session->get('userID');
956 $values['contact_id'] = $userID;
959 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
964 // Check for custom field values
966 if (empty($fields['custom'])) {
967 $fields['custom'] = &CRM_Core_BAO_CustomField
::getFields(CRM_Utils_Array
::value('contact_type', $values),
968 FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE
972 foreach ($values as $key => $value) {
973 if ($customFieldID = CRM_Core_BAO_CustomField
::getKeyID($key)) {
974 // check if it's a valid custom field id
976 if (!array_key_exists($customFieldID, $fields['custom'])) {
977 return civicrm_api3_create_error('Invalid custom field ID');
980 $params[$key] = $value;
987 * Parse a field which could be represented by a label or name value rather than the DB value.
989 * We will try to match name first or (per https://lab.civicrm.org/dev/core/issues/1285 if we have an id.
991 * but if not available then see if we have a label that can be converted to a name.
993 * @param string|int|null $submittedValue
994 * @param array $fieldSpec
995 * Metadata for the field
999 protected function parsePseudoConstantField($submittedValue, $fieldSpec) {
1000 // 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
1001 if (!isset($fieldSpec['bao'])) {
1002 return $submittedValue;
1004 /* @var \CRM_Core_DAO $bao */
1005 $bao = $fieldSpec['bao'];
1006 // For historical reasons use validate as context - ie disabled name matches ARE permitted.
1007 $nameOptions = $bao::buildOptions($fieldSpec['name'], 'validate');
1008 if (isset($nameOptions[$submittedValue])) {
1009 return $submittedValue;
1011 if (in_array($submittedValue, $nameOptions)) {
1012 return array_search($submittedValue, $nameOptions, TRUE);
1015 $labelOptions = array_flip($bao::buildOptions($fieldSpec['name'], 'match'));
1016 if (isset($labelOptions[$submittedValue])) {
1017 return array_search($labelOptions[$submittedValue], $nameOptions, TRUE);
1023 * This is code extracted from 4 places where this exact snippet was being duplicated.
1025 * FIXME: Extracting this was a first step, but there's also
1026 * 1. Inconsistency in the way other select options are handled.
1027 * Contribution adds handling for Select/Radio/Autocomplete
1028 * Participant/Activity only handles Select/Radio and misses Autocomplete
1029 * Membership is missing all of it
1030 * 2. Inconsistency with the way this works vs. how it's implemented in Contact import.
1032 * @param $customFieldID
1037 public static function unserializeCustomValue($customFieldID, $value, $fieldType) {
1038 $mulValues = explode(',', $value);
1039 $customOption = CRM_Core_BAO_CustomOption
::getCustomOption($customFieldID, TRUE);
1041 foreach ($mulValues as $v1) {
1042 foreach ($customOption as $customValueID => $customLabel) {
1043 $customValue = $customLabel['value'];
1044 if ((strtolower(trim($customLabel['label'])) == strtolower(trim($v1))) ||
1045 (strtolower(trim($customValue)) == strtolower(trim($v1)))
1047 $values[] = $customValue;
1055 * Get the ids of any contacts that match according to the rule.
1057 * @param array $formatted
1061 protected function getIdsOfMatchingContacts(array $formatted):array {
1062 // the call to the deprecated function seems to add no value other that to do an additional
1063 // check for the contact_id & type.
1064 $error = _civicrm_api3_deprecated_duplicate_formatted_contact($formatted);
1065 if (!CRM_Core_Error
::isAPIError($error, CRM_Core_ERROR
::DUPLICATE_CONTACT
)) {
1068 if (is_array($error['error_message']['params'][0])) {
1069 return $error['error_message']['params'][0];
1072 return explode(',', $error['error_message']['params'][0]);