2364e686314da312647275ee90b257e842b9547c
[civicrm-core.git] / CRM / Contact / Import / Parser / Contact.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Api4\Contact;
13 use Civi\Api4\RelationshipType;
14
15 require_once 'CRM/Utils/DeprecatedUtils.php';
16 require_once 'api/v3/utils.php';
17
18 /**
19 *
20 * @package CRM
21 * @copyright CiviCRM LLC https://civicrm.org/licensing
22 */
23
24 /**
25 * class to parse contact csv files
26 */
27 class CRM_Contact_Import_Parser_Contact extends CRM_Import_Parser {
28
29 use CRM_Contact_Import_MetadataTrait;
30
31 protected $_mapperKeys = [];
32
33 /**
34 * Is update only permitted on an id match.
35 *
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.
39 *
40 * @var bool
41 */
42 protected $_updateWithId;
43 protected $_retCode;
44
45 protected $_externalIdentifierIndex;
46 protected $_allExternalIdentifiers = [];
47 protected $_parseStreetAddress;
48
49 /**
50 * Array of successfully imported contact id's
51 *
52 * @var array
53 */
54 protected $_newContacts;
55
56 /**
57 * Line count id.
58 *
59 * @var int
60 */
61 protected $_lineCount;
62
63 /**
64 * Array of successfully imported related contact id's
65 *
66 * @var array
67 */
68 protected $_newRelatedContacts;
69
70 protected $_tableName;
71
72 /**
73 * Total number of lines in file
74 *
75 * @var int
76 */
77 protected $_rowCount;
78
79 protected $_primaryKeyName;
80 protected $_statusFieldName;
81
82 protected $fieldMetadata = [];
83
84 /**
85 * Fields which are being handled by metadata formatting & validation functions.
86 *
87 * This is intended as a temporary parameter as we phase in metadata handling.
88 *
89 * The end result is that all fields will be & this will go but for now it is
90 * opt in.
91 *
92 * @var string[]
93 */
94 protected $metadataHandledFields = [
95 'contact_type',
96 'contact_sub_type',
97 'gender_id',
98 'birth_date',
99 'deceased_date',
100 'is_deceased',
101 ];
102
103 /**
104 * Relationship labels.
105 *
106 * Temporary cache of labels to reduce queries in getRelationshipLabels.
107 *
108 * @var array
109 * e.g ['5a_b' => 'Employer', '5b_a' => 'Employee']
110 */
111 protected $relationshipLabels = [];
112
113 /**
114 * On duplicate
115 *
116 * @var int
117 */
118 public $_onDuplicate;
119
120 /**
121 * Dedupe rule group id to use if set
122 *
123 * @var int
124 */
125 public $_dedupeRuleGroupID = NULL;
126
127 /**
128 * Class constructor.
129 *
130 * @param array $mapperKeys
131 */
132 public function __construct($mapperKeys = []) {
133 parent::__construct();
134 $this->_mapperKeys = $mapperKeys;
135 }
136
137 /**
138 * @param $customFieldID
139 * @param array $customFields
140 * @param array $params
141 * @param $value
142 * @param $dateType
143 *
144 * @return ?string
145 */
146 private function validateCustomField($customFieldID, array $customFields, array $params, $value, $dateType): ?string {
147 if (!array_key_exists($customFieldID, $customFields)) {
148 return ts('field ID');
149 }
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'];
154 }
155
156 /* validate the data against the CF type */
157
158 if ($value) {
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')) {
165 return NULL;
166 }
167 return $fieldMetaData['label'];
168 }
169 elseif ($dataType == 'Boolean') {
170 if (CRM_Utils_String::strtoboolstr($value) === FALSE) {
171 return $customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'];
172 }
173 }
174 // need not check for label filed import
175 $selectHtmlTypes = [
176 'CheckBox',
177 'Select',
178 'Radio',
179 ];
180 if ((!$isSerialized && !in_array($htmlType, $selectHtmlTypes)) || $dataType == 'Boolean' || $dataType == 'ContactReference') {
181 $valid = CRM_Core_BAO_CustomValue::typecheck($dataType, $value);
182 if (!$valid) {
183 return $customFields[$customFieldID]['label'];
184 }
185 }
186
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) {
195 continue;
196 }
197
198 $flag = FALSE;
199 foreach ($customOption as $v2) {
200 if ((strtolower(trim($v2['label'])) == strtolower(trim($v1))) || (strtolower(trim($v2['value'])) == strtolower(trim($v1)))) {
201 $flag = TRUE;
202 }
203 }
204
205 if (!$flag) {
206 return $customFields[$customFieldID]['label'];
207 }
208 }
209 }
210 elseif ($htmlType == 'Select' || ($htmlType == 'Radio' && $dataType != 'Boolean')) {
211 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
212 $flag = FALSE;
213 foreach ($customOption as $v2) {
214 if ((strtolower(trim($v2['label'])) == strtolower(trim($value))) || (strtolower(trim($v2['value'])) == strtolower(trim($value)))) {
215 $flag = TRUE;
216 }
217 }
218 if (!$flag) {
219 return $customFields[$customFieldID]['label'];
220 }
221 }
222 elseif ($isSerialized && $dataType === 'StateProvince') {
223 $mulValues = explode(',', $value);
224 foreach ($mulValues as $stateValue) {
225 if ($stateValue) {
226 if (self::in_value(trim($stateValue), CRM_Core_PseudoConstant::stateProvinceAbbreviation()) || self::in_value(trim($stateValue), CRM_Core_PseudoConstant::stateProvince())) {
227 continue;
228 }
229 else {
230 return $customFields[$customFieldID]['label'];
231 }
232 }
233 }
234 }
235 elseif ($isSerialized && $dataType == 'Country') {
236 $mulValues = explode(',', $value);
237 foreach ($mulValues as $countryValue) {
238 if ($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();
242
243 $error = TRUE;
244 foreach ([$countryNames, $countryIsoCodes, $limitCodes] as $values) {
245 if (in_array(trim($countryValue), $values)) {
246 $error = FALSE;
247 break;
248 }
249 }
250
251 if ($error) {
252 return $customFields[$customFieldID]['label'];
253 }
254 }
255 }
256 }
257 }
258 return NULL;
259 }
260
261 /**
262 * The initializer code, called before processing.
263 */
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));
268 }
269 $this->_newContacts = [];
270
271 $this->setActiveFields($this->_mapperKeys);
272
273 $this->_externalIdentifierIndex = -1;
274
275 $index = 0;
276 foreach ($this->_mapperKeys as $key) {
277 if ($key == 'external_identifier') {
278 $this->_externalIdentifierIndex = $index;
279 }
280 $index++;
281 }
282
283 $this->_updateWithId = FALSE;
284 if (in_array('id', $this->_mapperKeys) || ($this->_externalIdentifierIndex >= 0 && $this->isUpdateExistingContacts())) {
285 $this->_updateWithId = TRUE;
286 }
287
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);
289 }
290
291 /**
292 * Is this a case where the user has opted to update existing contacts.
293 *
294 * @return bool
295 *
296 * @throws \API_Exception
297 */
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,
302 ], TRUE);
303 }
304
305 /**
306 * Gets the fields available for importing in a key-name, title format.
307 *
308 * @return array
309 * eg. ['first_name' => 'First Name'.....]
310 *
311 * @throws \API_Exception
312 *
313 * @todo - we are constructing the metadata before we
314 * have set the contact type so we re-do it here.
315 *
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.
319 */
320 public function getAvailableFields(): array {
321 $this->setFieldMetadata();
322 $return = [];
323 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
324 if ($name === 'id' && $this->isSkipDuplicates()) {
325 // Duplicates are being skipped so id matching is not availble.
326 continue;
327 }
328 $return[$name] = $field['title'];
329 }
330 return $return;
331 }
332
333 /**
334 * Did the user specify duplicates should be skipped and not imported.
335 *
336 * @return bool
337 *
338 * @throws \API_Exception
339 */
340 private function isSkipDuplicates(): bool {
341 return ((int) $this->getSubmittedValue('onDuplicate')) === CRM_Import_Parser::DUPLICATE_SKIP;
342 }
343
344 /**
345 * Did the user specify duplicates checking should be skipped, resulting in possible duplicate contacts.
346 *
347 * Note we still need to check for external_identifier as it will hard-fail
348 * if we duplicate.
349 *
350 * @return bool
351 *
352 * @throws \API_Exception
353 */
354 private function isIgnoreDuplicates(): bool {
355 return ((int) $this->getSubmittedValue('onDuplicate')) === CRM_Import_Parser::DUPLICATE_NOCHECK;
356 }
357
358 /**
359 * Handle the values in preview mode.
360 *
361 * Function will be deprecated in favour of validateValues.
362 *
363 * @param array $values
364 * The array of values belonging to this line.
365 *
366 * @return bool
367 * the result of this processing
368 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
369 */
370 public function preview(&$values) {
371 return $this->summary($values);
372 }
373
374 /**
375 * Handle the values in summary mode.
376 *
377 * Function will be deprecated in favour of validateValues.
378 *
379 * @param array $values
380 * The array of values belonging to this line.
381 *
382 * @return int
383 * the result of this processing
384 * CRM_Import_Parser::ERROR or CRM_Import_Parser::VALID
385 */
386 public function summary(&$values): int {
387 $rowNumber = (int) ($values[count($values) - 1]);
388 try {
389 $this->validateValues($values);
390 }
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;
395 }
396 $this->setImportStatus($rowNumber, 'NEW', '');
397
398 return CRM_Import_Parser::VALID;
399 }
400
401 /**
402 * Get Array of all the fields that could potentially be part
403 * import process
404 *
405 * @return array
406 */
407 public function getAllFields() {
408 return $this->_fields;
409 }
410
411 /**
412 * Handle the values in import mode.
413 *
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.
418 *
419 * @return bool
420 * the result of this processing
421 *
422 * @throws \CiviCRM_API3_Exception
423 * @throws \CRM_Core_Exception
424 * @throws \API_Exception
425 */
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();
431 }
432
433 // first make sure this is a valid line
434 //$this->_updateWithId = false;
435 $response = $this->summary($values);
436
437 if ($response != CRM_Import_Parser::VALID) {
438 $this->setImportStatus((int) $values[count($values) - 1], 'Invalid', "Invalid (Error Code: $response)");
439 return $response;
440 }
441
442 $params = $this->getMappedRow($values);
443 $formatted = array_filter(array_intersect_key($params, array_fill_keys($this->metadataHandledFields, 1)));
444
445 $contactFields = CRM_Contact_DAO_Contact::import();
446
447 $params['contact_sub_type'] = $this->getContactSubType() ?: ($params['contact_sub_type'] ?? NULL);
448
449 try {
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')
455 ->execute()
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);
459 }
460 }
461 }
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());
465 return FALSE;
466 }
467
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.
470
471 //format common data, CRM-4062
472 $this->formatCommonData($params, $formatted, $contactFields);
473
474 $relationship = FALSE;
475 $createNewContact = TRUE;
476 // Support Match and Update Via Contact ID
477 if ($this->_updateWithId && isset($params['id'])) {
478 $createNewContact = FALSE;
479 }
480
481 //fixed CRM-4148
482 //now we create new contact in update/fill mode also.
483 $contactID = NULL;
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.
497 continue;
498 }
499 if (!empty($formatted[$fieldName])
500 && empty($fieldSpec['options'][$formatted[$fieldName]])) {
501 $formatted[$fieldName] = array_search($formatted[$fieldName], $fieldSpec['options'], TRUE) ?? $formatted[$fieldName];
502 }
503 }
504 //CRM-4430, don't carry if not submitted.
505 if ($this->_updateWithId && !empty($params['id'])) {
506 $contactID = $params['id'];
507 }
508 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID);
509 }
510
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;
516
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;
520 }
521 }
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;
529 }
530
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);
535 }
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;
544 }
545 }
546 }
547
548 if ($contactID) {
549 // call import hook
550 $currentImportID = end($values);
551
552 $hookParams = [
553 'contactID' => $contactID,
554 'importID' => $currentImportID,
555 'importTempTable' => $this->_tableName,
556 'fieldHeaders' => $this->_mapperKeys,
557 'fields' => $this->_activeFields,
558 ];
559
560 CRM_Utils_Hook::import('Contact', 'process', $this, $hookParams);
561 }
562
563 if ($relationship) {
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;
568 }
569 }
570 else {
571 $primaryContactId = $newContact->id;
572 }
573
574 if ((CRM_Core_Error::isAPIError($newContact, CRM_Core_ERROR::DUPLICATE_CONTACT) || is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) {
575
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')) {
580 continue;
581 }
582
583 $relationType = new CRM_Contact_DAO_RelationshipType();
584 $relationType->id = $id;
585 $relationType->find(TRUE);
586 $direction = "contact_sub_type_$second";
587
588 $formatting = [
589 'contact_type' => $params[$key]['contact_type'],
590 ];
591
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;
599 }
600 else {
601 $formatting['contact_sub_type'] = $relationType->$direction;
602 }
603 }
604
605 $contactFields = NULL;
606 $contactFields = CRM_Contact_DAO_Contact::import();
607
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');
611 }
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;
622 }
623
624 //validation of related contact subtype for update mode
625 //CRM-5125
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');
629 }
630
631 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($params[$key]['id'], $relatedCsType) &&
632 $relatedCsType != CRM_Utils_Array::value('contact_sub_type', $formatting))
633 ) {
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;
637 }
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'];
641 }
642
643 //format common data, CRM-4062
644 $this->formatCommonData($field, $formatting, $contactFields);
645
646 //fixed for CRM-4148
647 if (!empty($params[$key]['id'])) {
648 $contact = [
649 'contact_id' => $params[$key]['id'],
650 ];
651 $defaults = [];
652 $relatedNewContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
653 }
654 else {
655 $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE);
656 }
657
658 if (is_object($relatedNewContact) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) {
659 $relatedNewContact = clone($relatedNewContact);
660 }
661
662 $matchedIDs = [];
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);
670 }
671 }
672 else {
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;
677 }
678 }
679 else {
680 $matchedIDs[] = $relatedNewContact->id;
681 }
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,
686 ])) {
687 //validation of related contact subtype for update mode
688 //CRM-5125
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');
692 }
693
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;
698 }
699 else {
700 $updatedContact = $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]);
701 }
702 }
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);
711
712 if (empty($reContact)) {
713 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
714 }
715 }
716 else {
717 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
718 }
719 }
720 }
721 else {
722 $relContactId = $relatedNewContact->id;
723 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
724 }
725
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
732 $relationParams = [
733 'relationship_type_id' => $key,
734 'contact_check' => [
735 $relContactId => 1,
736 ],
737 'is_active' => 1,
738 'skipRecentView' => TRUE,
739 ];
740
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
743 $relationIds = [
744 'contact' => $primaryContactId,
745 ];
746
747 [$valid, $duplicate] = self::legacyCreateMultiple($relationParams, $relationIds);
748
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);
753 }
754
755 //handle current employer, CRM-3532
756 if ($valid) {
757 $allRelationships = CRM_Core_PseudoConstant::relationshipType('name');
758 $relationshipTypeId = str_replace([
759 '_a_b',
760 '_b_a',
761 ], [
762 '',
763 '',
764 ], $key);
765 $relationshipType = str_replace($relationshipTypeId . '_', '', $key);
766 $orgId = $individualId = NULL;
767 if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') {
768 $orgId = $relContactId;
769 $individualId = $primaryContactId;
770 }
771 elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') {
772 $orgId = $primaryContactId;
773 $individualId = $relContactId;
774 }
775 if ($orgId && $individualId) {
776 $currentEmpParams[$individualId] = $orgId;
777 CRM_Contact_BAO_Contact_Utils::setCurrentEmployer($currentEmpParams);
778 }
779 }
780 }
781 }
782 }
783 }
784 }
785 if ($this->_updateWithId) {
786 //return warning if street address is unparsed, CRM-5886
787 return $this->processMessage($values, $this->_retCode);
788 }
789 //dupe checking
790 if (is_array($newContact) && civicrm_error($newContact)) {
791 $code = NULL;
792
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);
795 }
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;
801
802 }
803
804 if (empty($this->_unparsedStreetAddressContacts)) {
805 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '', $contactID);
806 return CRM_Import_Parser::VALID;
807 }
808
809 // @todo - record unparsed address as 'imported' but the presence of a message is meaningful?
810 return $this->processMessage($values, CRM_Import_Parser::VALID);
811 }
812
813 /**
814 * Only called from import now... plus one place outside of core & tests.
815 *
816 * @todo - deprecate more aggressively - will involve copying to the import
817 * class, adding a deprecation notice here & removing from tests.
818 *
819 * Takes an associative array and creates a relationship object.
820 *
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
823 * nasty
824 *
825 * @param array $params
826 * (reference ) an assoc array of name/value pairs.
827 * @param array $ids
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 "
831 *
832 * @return array
833 * @throws \CRM_Core_Exception
834 */
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'];
843 unset($ids);
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']);
848
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
853
854 // step 1
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'])) {
860 return [0, 0];
861 }
862
863 if (
864 CRM_Contact_BAO_Relationship::checkDuplicateRelationship(
865 $contactFields,
866 $contactID,
867 // step 2
868 $relatedContactID
869 )
870 ) {
871 return [0, 1];
872 }
873
874 $singleInstanceParams = array_merge($params, $contactFields);
875 CRM_Contact_BAO_Relationship::add($singleInstanceParams);
876 return [1, 0];
877 }
878
879 /**
880 * Format common params data to proper format to store.
881 *
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.
888 */
889 private function formatCommonData($params, &$formatted, $contactFields) {
890 $customFields = CRM_Core_BAO_CustomField::getFields($formatted['contact_type'], FALSE, FALSE, $formatted['contact_sub_type'] ?? NULL);
891
892 $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address');
893 $customFields = $customFields + $addressCustomFields;
894
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
896 $elements = [
897 'email_greeting_custom' => 'email_greeting',
898 'postal_greeting_custom' => 'postal_greeting',
899 'addressee_custom' => 'addressee',
900 ];
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;
905 }
906 }
907
908 //format date first
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)
915 ) {
916 //we should not update Date to null, CRM-4062
917 if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) {
918 //CRM-21267
919 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key);
920 }
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]);
925 }
926 else {
927 $params[$key] = CRM_Utils_String::strtoboolstr($val);
928 }
929 }
930 }
931 }
932
933 //now format custom data.
934 foreach ($params as $key => $field) {
935 if (is_array($field)) {
936 $isAddressCustomField = FALSE;
937 foreach ($field as $value) {
938 $break = FALSE;
939 if (is_array($value)) {
940 foreach ($value as $name => $testForEmpty) {
941 if ($addressCustomFieldID = CRM_Core_BAO_CustomField::getKeyID($name)) {
942 $isAddressCustomField = TRUE;
943 break;
944 }
945 // check if $value does not contain IM provider or phoneType
946 if (($name !== 'phone_type_id' || $name !== 'provider_id') && ($testForEmpty === '' || $testForEmpty == NULL)) {
947 $break = TRUE;
948 break;
949 }
950 }
951 }
952 else {
953 $break = TRUE;
954 }
955
956 if (!$break) {
957 if (!empty($value['location_type_id'])) {
958 $this->formatLocationBlock($value, $formatted);
959 }
960 else {
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);
964 }
965 }
966 }
967 if (!$isAddressCustomField) {
968 continue;
969 }
970 }
971
972 $formatValues = [
973 $key => $field,
974 ];
975
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'];
981 }
982
983 if ($key == 'id' && isset($field)) {
984 $formatted[$key] = $field;
985 }
986 $this->formatContactParameters($formatValues, $formatted);
987
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)
993 ) {
994
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]);
999
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;
1008 }
1009 }
1010 }
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] = [];
1015 $params[$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)))
1020 ) {
1021 if ($htmlType == 'CheckBox') {
1022 $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1;
1023 }
1024 else {
1025 $params[$key][] = $formatted[$key][] = $v2['value'];
1026 }
1027 }
1028 }
1029 }
1030 }
1031 }
1032 }
1033
1034 if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) && array_key_exists($customFieldID, $customFields) &&
1035 !array_key_exists($customFieldID, $addressCustomFields)
1036 ) {
1037 // @todo calling api functions directly is not supported
1038 _civicrm_api3_custom_format_params($params, $formatted, $extends);
1039 }
1040
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'];
1046 }
1047 $emptyValue = $customvalue[-1]['value'] ?? NULL;
1048 if (!isset($emptyValue)) {
1049 unset($formatted['custom'][$customKey]);
1050 }
1051 }
1052 }
1053
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)) {
1060 continue;
1061 }
1062 // parse address field.
1063 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($streetAddress);
1064
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), '');
1069 }
1070
1071 // merge parse address w/ main address block.
1072 $address = array_merge($address, $parsedFields);
1073 }
1074 }
1075 }
1076 }
1077
1078 /**
1079 * Get the array of successfully imported contact id's
1080 *
1081 * @return array
1082 */
1083 public function getImportedContacts() {
1084 return $this->_newContacts;
1085 }
1086
1087 /**
1088 * Get the array of successfully imported related contact id's
1089 *
1090 * @return array
1091 */
1092 public function &getRelatedImportedContacts() {
1093 return $this->_newRelatedContacts;
1094 }
1095
1096 /**
1097 * Check if an error in custom data.
1098 *
1099 * @param array $params
1100 * @param string $errorMessage
1101 * A string containing all the error-fields.
1102 *
1103 * @param null $csType
1104 */
1105 public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL) {
1106 $dateType = CRM_Core_Session::singleton()->get("dateTypes");
1107 $errors = [];
1108
1109 if (!empty($params['contact_sub_type'])) {
1110 $csType = $params['contact_sub_type'] ?? NULL;
1111 }
1112
1113 if (empty($params['contact_type'])) {
1114 $params['contact_type'] = 'Individual';
1115 }
1116
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']);
1120 }
1121
1122 if (is_array($csType)) {
1123 // fetch custom fields for every subtype and add it to $customFields array
1124 // CRM-18708
1125 $customFields = [];
1126 foreach ($csType as $cType) {
1127 $customFields += CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $cType);
1128 }
1129 }
1130 else {
1131 $customFields = CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $csType);
1132 }
1133
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];
1145 }
1146 else {
1147 $input = $params;
1148 }
1149 $errors[] = $parser->validateCustomField($customFieldID, $addressCustomFields, $input, $value, $dateType);
1150 }
1151 else {
1152 /* check if it's a valid custom field id */
1153 $errors[] = $parser->validateCustomField($customFieldID, $customFields, $params, $value, $dateType);
1154 }
1155 }
1156 elseif (is_array($params[$key]) && isset($params[$key]["contact_type"]) && in_array(substr($key, -3), ['a_b', 'b_a'], TRUE)) {
1157 //CRM-5125
1158 //supporting custom data of related contact subtypes
1159 $relation = $key;
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;
1168 }
1169 }
1170 }
1171
1172 self::isErrorInCustomData($params[$key], $errorMessage, $csType);
1173 }
1174 }
1175 if ($errors) {
1176 $errorMessage .= ($errorMessage ? '; ' : '') . implode('; ', array_filter($errors));
1177 }
1178 }
1179
1180 /**
1181 * Check if an error in Core( non-custom fields ) field
1182 *
1183 * @param array $params
1184 * @param string $errorMessage
1185 * A string containing all the error-fields.
1186 */
1187 public function isErrorInCoreData($params, &$errorMessage) {
1188 $errors = [];
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.');
1191 }
1192
1193 foreach ($params as $key => $value) {
1194 if ($value === 'invalid_import_value') {
1195 $errors[] = $this->getFieldMetadata($key)['title'];
1196 }
1197 if ($value) {
1198
1199 switch ($key) {
1200 case 'preferred_communication_method':
1201 $preffComm = [];
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');
1206 }
1207 }
1208 break;
1209
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');
1213 }
1214 break;
1215
1216 case 'individual_prefix':
1217 case 'prefix_id':
1218 if (!self::in_value($value, CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'prefix_id'))) {
1219 $errors[] = ts('Individual Prefix');
1220 }
1221 break;
1222
1223 case 'individual_suffix':
1224 case 'suffix_id':
1225 if (!self::in_value($value, CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'suffix_id'))) {
1226 $errors[] = ts('Individual Suffix');
1227 }
1228 break;
1229
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())
1236 ) {
1237 continue;
1238 }
1239 else {
1240 $errors[] = ts('State/Province');
1241 }
1242 }
1243 }
1244 }
1245 break;
1246
1247 case 'country':
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;
1258 }
1259
1260 if (self::in_value($stateValue['country'], $limitCodes) || self::in_value($stateValue['country'], CRM_Core_PseudoConstant::country())) {
1261 continue;
1262 }
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." ');
1265 }
1266 else {
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."');
1268 }
1269 }
1270 }
1271 }
1272 break;
1273
1274 case 'county':
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.');
1281 }
1282 }
1283 }
1284 }
1285 break;
1286
1287 case 'geo_code_1':
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'])) {
1292 continue;
1293 }
1294 $errors[] = ts('Geo code 1');
1295 }
1296 }
1297 }
1298 break;
1299
1300 case 'geo_code_2':
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'])) {
1305 continue;
1306 }
1307 $errors[] = ts('Geo code 2');
1308 }
1309 }
1310 }
1311 break;
1312
1313 //check for any error in email/postal greeting, addressee,
1314 //custom email/postal greeting, custom addressee, CRM-4575
1315
1316 case 'email_greeting':
1317 $emailGreetingFilter = [
1318 'contact_type' => $this->_contactType,
1319 'greeting_type' => 'email_greeting',
1320 ];
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');
1323 }
1324 break;
1325
1326 case 'postal_greeting':
1327 $postalGreetingFilter = [
1328 'contact_type' => $this->_contactType,
1329 'greeting_type' => 'postal_greeting',
1330 ];
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');
1333 }
1334 break;
1335
1336 case 'addressee':
1337 $addresseeFilter = [
1338 'contact_type' => $this->_contactType,
1339 'greeting_type' => 'addressee',
1340 ];
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');
1343 }
1344 break;
1345
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');
1351 }
1352 }
1353 break;
1354
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');
1360 }
1361 }
1362 break;
1363
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');
1369 }
1370 }
1371 break;
1372
1373 case 'url':
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');
1378 break;
1379 }
1380 }
1381 }
1382 break;
1383
1384 case 'do_not_email':
1385 case 'do_not_phone':
1386 case 'do_not_mail':
1387 case 'do_not_sms':
1388 case 'do_not_trade':
1389 if (CRM_Utils_Rule::boolean($value) == FALSE) {
1390 $key = ucwords(str_replace("_", " ", $key));
1391 $errors[] = $key;
1392 }
1393 break;
1394
1395 case 'email':
1396 if (is_array($value)) {
1397 foreach ($value as $values) {
1398 if (!empty($values['email']) && !CRM_Utils_Rule::email($values['email'])) {
1399 $errors[] = $key;
1400 break;
1401 }
1402 }
1403 }
1404 break;
1405
1406 default:
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);
1410 }
1411 }
1412 }
1413 }
1414 if ($errors) {
1415 $errorMessage .= ($errorMessage ? '; ' : '') . implode('; ', $errors);
1416 }
1417 }
1418
1419 /**
1420 * Ckeck a value present or not in a array.
1421 *
1422 * @param $value
1423 * @param $valueArray
1424 *
1425 * @return bool
1426 */
1427 public static function in_value($value, $valueArray) {
1428 foreach ($valueArray as $key => $v) {
1429 //fix for CRM-1514
1430 if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) {
1431 return TRUE;
1432 }
1433 }
1434 return FALSE;
1435 }
1436
1437 /**
1438 * Build error-message containing error-fields
1439 *
1440 * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function.
1441 *
1442 * @todo just say no!
1443 *
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.
1448 *
1449 */
1450 public static function addToErrorMsg($errorName, &$errorMessage) {
1451 if ($errorMessage) {
1452 $errorMessage .= "; $errorName";
1453 }
1454 else {
1455 $errorMessage = $errorName;
1456 }
1457 }
1458
1459 /**
1460 * Method for creating contact.
1461 *
1462 * @param array $formatted
1463 * @param array $contactFields
1464 * @param int $onDuplicate
1465 * @param int $contactId
1466 * @param bool $requiredCheck
1467 * @param int $dedupeRuleGroupID
1468 *
1469 * @return array|bool|\CRM_Contact_BAO_Contact|\CRM_Core_Error|null
1470 */
1471 public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) {
1472 $dupeCheck = FALSE;
1473 $newContact = NULL;
1474
1475 if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser::DUPLICATE_NOCHECK)) {
1476 $dupeCheck = (bool) ($onDuplicate);
1477 }
1478
1479 //get the prefix id etc if exists
1480 CRM_Contact_BAO_Contact::resolveDefaults($formatted, TRUE);
1481
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
1485 try {
1486 $error = $this->deprecated_contact_check_params($formatted, $dupeCheck, $dedupeRuleGroupID);
1487 if ($error) {
1488 return $error;
1489 }
1490 $this->deprecated_validate_formatted_contact($formatted);
1491 }
1492 catch (CRM_Core_Exception $e) {
1493 return ['error_message' => $e->getMessage(), 'is_error' => 1, 'code' => $e->getCode()];
1494 }
1495
1496 if ($contactId) {
1497 $this->formatParams($formatted, $onDuplicate, (int) $contactId);
1498 }
1499
1500 // Resetting and rebuilding cache could be expensive.
1501 CRM_Core_Config::setPermitCacheFlushMode(FALSE);
1502
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;
1512 }
1513
1514 [$data, $contactDetails] = CRM_Contact_BAO_Contact::formatProfileContactParams($formatted, $contactFields, $contactId, NULL, $formatted['contact_type']);
1515
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'])) {
1523 $shParams = [
1524 'contact_id' => $contactDetails['contact_id'],
1525 'status' => $isOptOut ? 'Removed' : 'Added',
1526 'method' => 'Web',
1527 ];
1528 CRM_Contact_BAO_SubscriptionHistory::create($shParams);
1529 }
1530 }
1531
1532 $contact = civicrm_api3('Contact', 'create', $data);
1533 $cid = $contact['id'];
1534
1535 CRM_Core_Config::setPermitCacheFlushMode(TRUE);
1536
1537 $contact = [
1538 'contact_id' => $cid,
1539 ];
1540
1541 $defaults = [];
1542 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
1543
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'],
1551 ];
1552 }
1553 }
1554 }
1555 return $newContact;
1556 }
1557
1558 /**
1559 * Format params for update and fill mode.
1560 *
1561 * @param array $params
1562 * reference to an array containing all the.
1563 * values for import
1564 * @param int $onDuplicate
1565 * @param int $cid
1566 * contact id.
1567 */
1568 public function formatParams(&$params, $onDuplicate, $cid) {
1569 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
1570 return;
1571 }
1572
1573 $contactParams = [
1574 'contact_id' => $cid,
1575 ];
1576
1577 $defaults = [];
1578 $contactObj = CRM_Contact_BAO_Contact::retrieve($contactParams, $defaults);
1579
1580 $modeFill = ($onDuplicate == CRM_Import_Parser::DUPLICATE_FILL);
1581
1582 $groupTree = CRM_Core_BAO_CustomGroup::getTree($params['contact_type'], NULL, $cid, 0, NULL);
1583 CRM_Core_BAO_CustomGroup::setDefaults($groupTree, $defaults, FALSE, FALSE);
1584
1585 $locationFields = [
1586 'email' => 'email',
1587 'phone' => 'phone',
1588 'im' => 'name',
1589 'website' => 'website',
1590 'address' => 'address',
1591 ];
1592
1593 $contact = get_object_vars($contactObj);
1594
1595 foreach ($params as $key => $value) {
1596 if ($key == 'id' || $key == 'contact_type') {
1597 continue;
1598 }
1599
1600 if (array_key_exists($key, $locationFields)) {
1601 continue;
1602 }
1603 if (in_array($key, [
1604 'email_greeting',
1605 'postal_greeting',
1606 'addressee',
1607 ])) {
1608 // CRM-4575, need to null custom
1609 if ($params["{$key}_id"] != 4) {
1610 $params["{$key}_custom"] = 'null';
1611 }
1612 unset($params[$key]);
1613 }
1614 else {
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)) {
1619 unset($getValue);
1620 }
1621 }
1622 else {
1623 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $key);
1624 }
1625 if ($key == 'contact_source') {
1626 $params['source'] = $params[$key];
1627 unset($params[$key]);
1628 }
1629
1630 if ($modeFill && isset($getValue)) {
1631 unset($params[$key]);
1632 if ($customFieldId) {
1633 // Extra values must be unset to ensure the values are not
1634 // imported.
1635 unset($params['custom'][$customFieldId]);
1636 }
1637 }
1638 }
1639 }
1640
1641 foreach ($locationFields as $locKeys) {
1642 if (isset($params[$locKeys]) && is_array($params[$locKeys])) {
1643 foreach ($params[$locKeys] as $key => $value) {
1644 if ($modeFill) {
1645 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $locKeys);
1646
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]);
1652 }
1653 }
1654 else {
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]);
1657 }
1658 }
1659 }
1660 }
1661 }
1662 }
1663 if (count($params[$locKeys]) == 0) {
1664 unset($params[$locKeys]);
1665 }
1666 }
1667 }
1668 }
1669
1670 /**
1671 * Convert any given date string to default date array.
1672 *
1673 * @param array $params
1674 * Has given date-format.
1675 * @param array $formatted
1676 * Store formatted date in this array.
1677 * @param int $dateType
1678 * Type of date.
1679 * @param string $dateParam
1680 * Index of params.
1681 */
1682 public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) {
1683 //fix for CRM-2687
1684 CRM_Utils_Date::convertToDefaultDate($params, $dateType, $dateParam);
1685 $formatted[$dateParam] = CRM_Utils_Date::processDate($params[$dateParam]);
1686 }
1687
1688 /**
1689 * Generate status and error message for unparsed street address records.
1690 *
1691 * @param array $values
1692 * The array of values belonging to each row.
1693 * @param $returnCode
1694 *
1695 * @return int
1696 */
1697 private function processMessage(&$values, $returnCode) {
1698 if (empty($this->_unparsedStreetAddressContacts)) {
1699 $this->setImportStatus((int) ($values[count($values) - 1]), 'IMPORTED', '');
1700 }
1701 else {
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>";
1706 }
1707 array_unshift($values, $errorMessage);
1708 $returnCode = CRM_Import_Parser::UNPARSED_ADDRESS_WARNING;
1709 $this->setImportStatus((int) ($values[count($values) - 1]), 'ERROR', $errorMessage);
1710 }
1711 return $returnCode;
1712 }
1713
1714 /**
1715 * get subtypes given the contact type
1716 *
1717 * @param string $contactType
1718 * @return array $subTypes
1719 */
1720 public static function getSubtypes($contactType) {
1721 $subTypes = [];
1722 $types = CRM_Contact_BAO_ContactType::subTypeInfo($contactType);
1723
1724 if (count($types) > 0) {
1725 foreach ($types as $type) {
1726 $subTypes[] = $type['name'];
1727 }
1728 }
1729 return $subTypes;
1730 }
1731
1732 /**
1733 * Get the possible contact matches.
1734 *
1735 * 1) the chosen dedupe rule falling back to
1736 * 2) a check for the external ID.
1737 *
1738 * @see https://issues.civicrm.org/jira/browse/CRM-17275
1739 *
1740 * @param array $params
1741 * @param int|null $extIDMatch
1742 * @param int|null $dedupeRuleID
1743 *
1744 * @return int|null
1745 * IDs of a possible.
1746 *
1747 * @throws \CRM_Core_Exception
1748 * @throws \CiviCRM_API3_Exception
1749 */
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);
1753 if (!$extIDMatch) {
1754 // Historically we have used the last ID - it is not clear if this was
1755 // deliberate.
1756 return array_key_last($possibleMatches['values']);
1757 }
1758 if ($possibleMatches['count']) {
1759 if (array_key_exists($extIDMatch, $possibleMatches['values'])) {
1760 return $extIDMatch;
1761 }
1762 throw new CRM_Core_Exception(ts(
1763 'Matching this contact based on the de-dupe rule would cause an external ID conflict'));
1764 }
1765 return $extIDMatch;
1766 }
1767
1768 /**
1769 * Format the form mapping parameters ready for the parser.
1770 *
1771 * @param int $count
1772 * Number of rows.
1773 *
1774 * @return array $parserParameters
1775 */
1776 public static function getParameterForParser($count) {
1777 $baseArray = [];
1778 for ($i = 0; $i < $count; $i++) {
1779 $baseArray[$i] = NULL;
1780 }
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;
1792
1793 return $parserParameters;
1794
1795 }
1796
1797 /**
1798 * Set field metadata.
1799 */
1800 protected function setFieldMetadata() {
1801 $this->setImportableFieldsMetadata($this->getContactImportMetadata());
1802 }
1803
1804 /**
1805 * @param array $newContact
1806 * @param array $values
1807 * @param int $onDuplicate
1808 * @param array $formatted
1809 * @param array $contactFields
1810 *
1811 * @return int
1812 *
1813 * @throws \CRM_Core_Exception
1814 * @throws \CiviCRM_API3_Exception
1815 * @throws \Civi\API\Exception\UnauthorizedException
1816 */
1817 private function handleDuplicateError(array $newContact, array $values, int $onDuplicate, array $formatted, array $contactFields): int {
1818 $urls = [];
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];
1823 }
1824 else {
1825 $cids = explode(',', $newContact['error_message']['params'][0]);
1826 }
1827
1828 foreach ($cids as $cid) {
1829 $urls[] = CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE);
1830 }
1831
1832 $url_string = implode("\n", $urls);
1833
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;
1842 }
1843
1844 // Params only had one id, so shift it out
1845 $contactId = array_shift($cids);
1846 $cid = NULL;
1847
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);
1851 }
1852 // else skip does nothing and just returns an error code.
1853 if ($cid) {
1854 $contact = [
1855 'contact_id' => $cid,
1856 ];
1857 $defaults = [];
1858 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
1859 }
1860
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;
1868 }
1869
1870 $contactID = $newContact['error_message']['params'][0];
1871 if (is_array($contactID)) {
1872 $contactID = array_pop($contactID);
1873 }
1874 if (!in_array($contactID, $this->_newContacts)) {
1875 $this->_newContacts[] = $contactID;
1876 }
1877 }
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;
1883 }
1884
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);
1888 }
1889
1890 /**
1891 * Validate a formatted contact parameter list.
1892 *
1893 * @param array $params
1894 * Structured parameter list (as in crm_format_params).
1895 *
1896 * @throw CRM_Core_Error
1897 */
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)
1906 );
1907 if (!$valid && $value['is_required']) {
1908 throw new CRM_Core_Exception('Invalid value for custom field \'' .
1909 $custom['name'] . '\''
1910 );
1911 }
1912 if (CRM_Utils_Array::value('type', $custom) == 'Date') {
1913 $params['custom'][$key][$fieldId]['value'] = str_replace('-', '', $params['custom'][$key][$fieldId]['value']);
1914 }
1915 }
1916 }
1917 }
1918 }
1919 }
1920
1921 /**
1922 * @param array $params
1923 * @param bool $dupeCheck
1924 * @param null|int $dedupeRuleGroupID
1925 *
1926 * @return ?array
1927 * @throws \CRM_Core_Exception
1928 */
1929 public function deprecated_contact_check_params(
1930 $params,
1931 $dupeCheck = TRUE,
1932 $dedupeRuleGroupID = NULL) {
1933
1934 if ($dupeCheck) {
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);
1939
1940 if ($ids != NULL) {
1941 return [
1942 'is_error' => 1,
1943 'error_message' => [
1944 'code' => CRM_Core_Error::DUPLICATE_CONTACT,
1945 'params' => $ids,
1946 'level' => 'Fatal',
1947 'message' => 'Found matching contacts: ' . implode(',', $ids),
1948 ],
1949 ];
1950 }
1951 }
1952 }
1953
1954 /**
1955 * Run import.
1956 *
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}
1960 * @param int $mode
1961 * @param int $statusID
1962 *
1963 * @return mixed
1964 * @throws \API_Exception|\CRM_Core_Exception
1965 */
1966 public function run(
1967 $mapper = [],
1968 $mode = self::MODE_PREVIEW,
1969 $statusID = NULL
1970 ) {
1971
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();
1979
1980 $this->init();
1981
1982 $this->_rowCount = 0;
1983 $this->_totalCount = 0;
1984
1985 $this->_primaryKeyName = '_id';
1986 $this->_statusFieldName = '_status';
1987
1988 if ($statusID) {
1989 $this->progressImport($statusID);
1990 $startTimestamp = $currTimestamp = $prevTimestamp = time();
1991 }
1992 $dataSource = $this->getDataSourceObject();
1993 $totalRowCount = $dataSource->getRowCount(['new']);
1994 if ($mode == self::MODE_IMPORT) {
1995 $dataSource->setStatuses(['new']);
1996 }
1997
1998 while ($row = $dataSource->getRow()) {
1999 $values = array_values($row);
2000 $this->_rowCount++;
2001
2002 $this->_totalCount++;
2003
2004 if ($mode == self::MODE_PREVIEW) {
2005 $returnCode = $this->preview($values);
2006 }
2007 elseif ($mode == self::MODE_SUMMARY) {
2008 $returnCode = $this->summary($values);
2009 }
2010 elseif ($mode == self::MODE_IMPORT) {
2011 try {
2012 $returnCode = $this->import($onDuplicate, $values);
2013 }
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
2018 // and move on).
2019 $this->setImportStatus((int) $values[count($values) - 1], 'ERROR', $e->getMessage());
2020 }
2021 if ($statusID && (($this->_rowCount % 50) == 0)) {
2022 $prevTimestamp = $this->progressImport($statusID, FALSE, $startTimestamp, $prevTimestamp, $totalRowCount);
2023 }
2024 }
2025 else {
2026 $returnCode = self::ERROR;
2027 }
2028
2029 if ($returnCode & self::NO_MATCH) {
2030 $this->setImportStatus((int) $values[count($values) - 1], 'invalid_no_match', array_shift($values));
2031 }
2032
2033 if ($returnCode & self::UNPARSED_ADDRESS_WARNING) {
2034 $this->setImportStatus((int) $values[count($values) - 1], 'warning_unparsed_address', array_shift($values));
2035 }
2036 }
2037 }
2038
2039 /**
2040 * Given a list of the importable field keys that the user has selected.
2041 * set the active fields array to this list
2042 *
2043 * @param array $fieldKeys
2044 * Mapped array of values.
2045 */
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 -'));
2050 }
2051 else {
2052 $this->_activeFields[] = clone($this->_fields[$key]);
2053 }
2054 }
2055 }
2056
2057 /**
2058 * Format the field values for input to the api.
2059 *
2060 * @param array $values
2061 * The row from the datasource.
2062 *
2063 * @return array
2064 * Parameters mapped as described in getMappedRow
2065 *
2066 * @throws \API_Exception
2067 * @todo - clean this up a bit & merge back into `getMappedRow`
2068 *
2069 */
2070 private function getParams(array $values): array {
2071 $params = [];
2072
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) {
2079 continue;
2080 }
2081
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)));
2084
2085 if ($relatedContactKey) {
2086 if (!isset($params[$relatedContactKey])) {
2087 $params[$relatedContactKey] = ['contact_type' => $this->getRelatedContactType($mappedField['relationship_type_id'], $mappedField['relationship_direction'])];
2088 }
2089 $this->addFieldToParams($params[$relatedContactKey], $locationValues, $fieldName, $importedValue);
2090 }
2091 else {
2092 $this->addFieldToParams($params, $locationValues, $fieldName, $importedValue);
2093 }
2094 }
2095
2096 return $params;
2097 }
2098
2099 /**
2100 * @param string $name
2101 * @param $title
2102 * @param int $type
2103 * @param string $headerPattern
2104 * @param string $dataPattern
2105 * @param bool $hasLocationType
2106 */
2107 public function addField(
2108 $name, $title, $type = CRM_Utils_Type::T_INT,
2109 $headerPattern = '//', $dataPattern = '//',
2110 $hasLocationType = FALSE
2111 ) {
2112 $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
2113 if (empty($name)) {
2114 $this->_fields['doNotImport'] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
2115 }
2116 }
2117
2118 /**
2119 * Store parser values.
2120 *
2121 * @param CRM_Core_Session $store
2122 *
2123 * @param int $mode
2124 */
2125 public function set($store, $mode = self::MODE_SUMMARY) {
2126 }
2127
2128 /**
2129 * Export data to a CSV file.
2130 *
2131 * @param string $fileName
2132 * @param array $header
2133 * @param array $data
2134 */
2135 public static function exportCSV($fileName, $header, $data) {
2136
2137 if (file_exists($fileName) && !is_writable($fileName)) {
2138 CRM_Core_Error::movedSiteError($fileName);
2139 }
2140 //hack to remove '_status', '_statusMsg' and '_id' from error file
2141 $errorValues = [];
2142 $dbRecordStatus = ['IMPORTED', 'ERROR', 'DUPLICATE', 'INVALID', 'NEW'];
2143 foreach ($data as $rowCount => $rowValues) {
2144 $count = 0;
2145 foreach ($rowValues as $key => $val) {
2146 if (in_array($val, $dbRecordStatus) && $count == (count($rowValues) - 3)) {
2147 break;
2148 }
2149 $errorValues[$rowCount][$key] = $val;
2150 $count++;
2151 }
2152 }
2153 $data = $errorValues;
2154
2155 $output = [];
2156 $fd = fopen($fileName, 'w');
2157
2158 foreach ($header as $key => $value) {
2159 $header[$key] = "\"$value\"";
2160 }
2161 $config = CRM_Core_Config::singleton();
2162 $output[] = implode($config->fieldSeparator, $header);
2163
2164 foreach ($data as $datum) {
2165 foreach ($datum as $key => $value) {
2166 $datum[$key] = "\"$value\"";
2167 }
2168 $output[] = implode($config->fieldSeparator, $datum);
2169 }
2170 fwrite($fd, implode("\n", $output));
2171 fclose($fd);
2172 }
2173
2174 /**
2175 * Update the status of the import row to reflect the processing outcome.
2176 *
2177 * @param int $id
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]
2184 *
2185 * @throws \API_Exception
2186 * @throws \CRM_Core_Exception
2187 */
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);
2190 }
2191
2192 /**
2193 * Format contact parameters.
2194 *
2195 * @todo this function needs re-writing & re-merging into the main function.
2196 *
2197 * Here be dragons.
2198 *
2199 * @param array $values
2200 * @param array $params
2201 *
2202 * @return bool
2203 */
2204 protected function formatContactParameters(&$values, &$params) {
2205 // Crawl through the possible classes:
2206 // Contact
2207 // Individual
2208 // Household
2209 // Organization
2210 // Location
2211 // Address
2212 // Email
2213 // Phone
2214 // IM
2215 // Note
2216 // Custom
2217
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);
2221
2222 if (isset($values['contact_type'])) {
2223 // we're an individual/household/org property
2224
2225 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact::fields();
2226
2227 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
2228 return TRUE;
2229 }
2230
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 = [];
2234
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']];
2239 }
2240 else {
2241 $params['prefix'] = $values['individual_prefix'];
2242 }
2243 return TRUE;
2244 }
2245
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']];
2250 }
2251 else {
2252 $params['suffix'] = $values['individual_suffix'];
2253 }
2254 return TRUE;
2255 }
2256
2257 // CRM-4575
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',
2263 ];
2264 $emailGreetings = CRM_Core_PseudoConstant::greeting($emailGreetingFilter);
2265 $params['email_greeting'] = $emailGreetings[$params['email_greeting_id']];
2266 }
2267 else {
2268 $params['email_greeting'] = $values['email_greeting'];
2269 }
2270
2271 return TRUE;
2272 }
2273
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',
2279 ];
2280 $postalGreetings = CRM_Core_PseudoConstant::greeting($postalGreetingFilter);
2281 $params['postal_greeting'] = $postalGreetings[$params['postal_greeting_id']];
2282 }
2283 else {
2284 $params['postal_greeting'] = $values['postal_greeting'];
2285 }
2286 return TRUE;
2287 }
2288
2289 if (isset($values['addressee'])) {
2290 $params['addressee'] = $values['addressee'];
2291 return TRUE;
2292 }
2293
2294 if (!empty($values['preferred_communication_method'])) {
2295 $comm = [];
2296 $pcm = array_change_key_case(array_flip(CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'preferred_communication_method')), CASE_LOWER);
2297
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;
2303 }
2304 }
2305
2306 $params['preferred_communication_method'] = $comm;
2307 return TRUE;
2308 }
2309
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();
2315 }
2316 if (!array_key_exists('website', $params) ||
2317 !is_array($params['website'])
2318 ) {
2319 $params['website'] = [];
2320 }
2321
2322 $websiteCount = count($params['website']);
2323 _civicrm_api3_store_values($websiteFields, $values,
2324 $params['website'][++$websiteCount]
2325 );
2326
2327 return TRUE;
2328 }
2329
2330 if (isset($values['note'])) {
2331 // add a note field
2332 if (!isset($params['note'])) {
2333 $params['note'] = [];
2334 }
2335 $noteBlock = count($params['note']) + 1;
2336
2337 $params['note'][$noteBlock] = [];
2338 if (!isset($fields['Note'])) {
2339 $fields['Note'] = CRM_Core_DAO_Note::fields();
2340 }
2341
2342 // get the current logged in civicrm user
2343 $session = CRM_Core_Session::singleton();
2344 $userID = $session->get('userID');
2345
2346 if ($userID) {
2347 $values['contact_id'] = $userID;
2348 }
2349
2350 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
2351
2352 return TRUE;
2353 }
2354
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
2358 );
2359
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
2363
2364 if (!array_key_exists($customFieldID, $customFields)) {
2365 return civicrm_api3_create_error('Invalid custom field ID');
2366 }
2367 else {
2368 $params[$key] = $value;
2369 }
2370 }
2371 }
2372 return TRUE;
2373 }
2374
2375 /**
2376 * Format location block ready for importing.
2377 *
2378 * There is some test coverage for this in CRM_Contact_Import_Parser_ContactTest
2379 * e.g. testImportPrimaryAddress.
2380 *
2381 * @param array $values
2382 * @param array $params
2383 *
2384 * @return bool
2385 */
2386 protected function formatLocationBlock(&$values, &$params) {
2387 $blockTypes = [
2388 'phone' => 'Phone',
2389 'email' => 'Email',
2390 'im' => 'IM',
2391 'openid' => 'OpenID',
2392 'phone_ext' => 'Phone',
2393 ];
2394 foreach ($blockTypes as $blockFieldName => $block) {
2395 if (!array_key_exists($blockFieldName, $values)) {
2396 continue;
2397 }
2398 $blockIndex = $values['location_type_id'] . (!empty($values['phone_type_id']) ? '_' . $values['phone_type_id'] : '');
2399
2400 // block present in value array.
2401 if (!array_key_exists($blockFieldName, $params) || !is_array($params[$blockFieldName])) {
2402 $params[$blockFieldName] = [];
2403 }
2404
2405 $fields[$block] = $this->getMetadataForEntity($block);
2406
2407 // copy value to dao field name.
2408 if ($blockFieldName == 'im') {
2409 $values['name'] = $values[$blockFieldName];
2410 }
2411
2412 _civicrm_api3_store_values($fields[$block], $values,
2413 $params[$blockFieldName][$blockIndex]
2414 );
2415
2416 $this->fillPrimary($params[$blockFieldName][$blockIndex], $values, $block, CRM_Utils_Array::value('id', $params));
2417
2418 if (empty($params['id']) && (count($params[$blockFieldName]) == 1)) {
2419 $params[$blockFieldName][$blockIndex]['is_primary'] = TRUE;
2420 }
2421
2422 // we only process single block at a time.
2423 return TRUE;
2424 }
2425
2426 // handle address fields.
2427 if (!array_key_exists('address', $params) || !is_array($params['address'])) {
2428 $params['address'] = [];
2429 }
2430
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');
2438 }
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)) {
2444
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)))
2454 ) {
2455 if ($htmlType == 'CheckBox') {
2456 $newValues[$key][$v2['value']] = 1;
2457 }
2458 else {
2459 $newValues[$key][] = $v2['value'];
2460 }
2461 }
2462 }
2463 }
2464 }
2465 }
2466 }
2467 // consider new values
2468 $values = $newValues;
2469 }
2470
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']]);
2474
2475 $addressFields = [
2476 'county',
2477 'country',
2478 'state_province',
2479 'supplemental_address_1',
2480 'supplemental_address_2',
2481 'supplemental_address_3',
2482 'StateProvince.name',
2483 ];
2484 foreach (array_keys($customFields) as $customFieldID) {
2485 $addressFields[] = 'custom_' . $customFieldID;
2486 }
2487
2488 foreach ($addressFields as $field) {
2489 if (array_key_exists($field, $values)) {
2490 if (!array_key_exists('address', $params)) {
2491 $params['address'] = [];
2492 }
2493 $params['address'][$values['location_type_id']][$field] = $values[$field];
2494 }
2495 }
2496
2497 $this->fillPrimary($params['address'][$values['location_type_id']], $values, 'address', CRM_Utils_Array::value('id', $params));
2498 return TRUE;
2499 }
2500
2501 /**
2502 * Get the field metadata for the relevant entity.
2503 *
2504 * @param string $entity
2505 *
2506 * @return array
2507 */
2508 protected function getMetadataForEntity($entity) {
2509 if (!isset($this->fieldMetadata[$entity])) {
2510 $className = "CRM_Core_DAO_$entity";
2511 $this->fieldMetadata[$entity] = $className::fields();
2512 }
2513 return $this->fieldMetadata[$entity];
2514 }
2515
2516 /**
2517 * Fill in the primary location.
2518 *
2519 * If the contact has a primary address we update it. Otherwise
2520 * we add an address of the default location type.
2521 *
2522 * @param array $params
2523 * Address block parameters
2524 * @param array $values
2525 * Input values
2526 * @param string $entity
2527 * - address, email, phone
2528 * @param int|null $contactID
2529 *
2530 * @throws \CiviCRM_API3_Exception
2531 */
2532 protected function fillPrimary(&$params, $values, $entity, $contactID) {
2533 if ($values['location_type_id'] === 'Primary') {
2534 if ($contactID) {
2535 $primary = civicrm_api3($entity, 'get', [
2536 'return' => 'location_type_id',
2537 'contact_id' => $contactID,
2538 'is_primary' => 1,
2539 'sequential' => 1,
2540 ]);
2541 }
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;
2545 }
2546 }
2547
2548 /**
2549 * Get the civicrm_mapping_field appropriate layout for the mapper input.
2550 *
2551 * The input looks something like ['street_address', 1]
2552 * and would be mapped to ['name' => 'street_address', 'location_type_id' =>
2553 * 1]
2554 *
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
2558 * [$fieldName],
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
2565 *
2566 * @return array
2567 * @throws \API_Exception
2568 */
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];
2576 }
2577 return [
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,
2588 ];
2589 }
2590
2591 /**
2592 * @param array $mappedField
2593 * Field detail as would be saved in field_mapping table
2594 * or as returned from getMappingFieldFromMapperInput
2595 *
2596 * @return string
2597 * @throws \API_Exception
2598 */
2599 public function getMappedFieldLabel(array $mappedField): string {
2600 $this->setFieldMetadata();
2601 $title = [];
2602 if ($mappedField['relationship_type_id']) {
2603 $title[] = $this->getRelationshipLabel($mappedField['relationship_type_id'], $mappedField['relationship_direction']);
2604 }
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']);
2608 }
2609 if ($mappedField['website_type_id']) {
2610 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Website', 'website_type_id', $mappedField['website_type_id']);
2611 }
2612 if ($mappedField['phone_type_id']) {
2613 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $mappedField['phone_type_id']);
2614 }
2615 if ($mappedField['im_provider_id']) {
2616 $title[] = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $mappedField['provider_id']);
2617 }
2618 return implode(' - ', $title);
2619 }
2620
2621 /**
2622 * Get the relevant label for the relationship.
2623 *
2624 * @param int $id
2625 * @param string $direction
2626 *
2627 * @return string
2628 * @throws \API_Exception
2629 */
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];
2637 }
2638 return $this->relationshipLabels[$id . $direction];
2639 }
2640
2641 /**
2642 * Transform the input parameters into the form handled by the input routine.
2643 *
2644 * @param array $values
2645 * Input parameters as they come in from the datasource
2646 * eg. ['Bob', 'Smith', 'bob@example.org', '123-456']
2647 *
2648 * @return array
2649 * Parameters mapped to CiviCRM fields based on the mapping
2650 * and specified contact type. eg.
2651 * [
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],
2658 *
2659 * @throws \API_Exception
2660 */
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();
2666 }
2667 return $params;
2668 }
2669
2670 /**
2671 * Validate the import values.
2672 *
2673 * The values array represents a row in the datasource.
2674 *
2675 * @param array $values
2676 *
2677 * @throws \API_Exception
2678 * @throws \CRM_Core_Exception
2679 */
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'] . ')' : '');
2688 }
2689
2690 //check for duplicate external Identifier
2691 $externalID = $params['external_identifier'] ?? NULL;
2692 if ($externalID) {
2693 /* If it's a dupe,external Identifier */
2694
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);
2698 }
2699 //otherwise, count it and move on
2700 $this->_allExternalIdentifiers[$externalID] = $this->_lineCount;
2701 }
2702
2703 //date-format part ends
2704
2705 $errorMessage = NULL;
2706 //checking error in custom data
2707 $this->isErrorInCustomData($params, $errorMessage, $params['contact_sub_type'] ?? NULL);
2708
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);
2714 }
2715 }
2716
2717 /**
2718 * Get the field mappings for the import.
2719 *
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'
2723 *
2724 * @return array
2725 * @throws \API_Exception
2726 */
2727 protected function getFieldMappings(): array {
2728 $mappedFields = [];
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';
2733 }
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;
2741 }
2742 return $mappedFields;
2743 }
2744
2745 /**
2746 * Get the related contact type.
2747 *
2748 * @param int|null $relationshipTypeID
2749 * @param int|string $relationshipDirection
2750 *
2751 * @return null|string
2752 *
2753 * @throws \API_Exception
2754 */
2755 protected function getRelatedContactType($relationshipTypeID, $relationshipDirection): ?string {
2756 if (!$relationshipTypeID) {
2757 return NULL;
2758 }
2759 $relationshipField = 'contact_type_' . substr($relationshipDirection, -1);
2760 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2761 }
2762
2763 /**
2764 * Get the related contact type.
2765 *
2766 * @param int|null $relationshipTypeID
2767 * @param int|string $relationshipDirection
2768 *
2769 * @return null|string
2770 *
2771 * @throws \API_Exception
2772 */
2773 protected function getRelatedContactLabel($relationshipTypeID, $relationshipDirection): ?string {
2774 $relationshipField = 'label_' . $relationshipDirection;
2775 return $this->getRelationshipType($relationshipTypeID)[$relationshipField];
2776 }
2777
2778 /**
2779 * Get the relationship type.
2780 *
2781 * @param int $relationshipTypeID
2782 *
2783 * @return string[]
2784 * @throws \API_Exception
2785 */
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();
2792 }
2793 return Civi::$statics[__CLASS__][$cacheKey];
2794 }
2795
2796 /**
2797 * Add the given field to the contact array.
2798 *
2799 * @param array $contactArray
2800 * @param array $locationValues
2801 * @param string $fieldName
2802 * @param mixed $importedValue
2803 *
2804 * @return void
2805 * @throws \API_Exception
2806 */
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;
2812 }
2813 else {
2814 $contactArray[$fieldName] = $this->getTransformedFieldValue($fieldName, $importedValue);
2815 }
2816 }
2817
2818 /**
2819 * Get any related contacts designated for update.
2820 *
2821 * This extracts the parts that relate to separate related
2822 * contacts from the 'params' array.
2823 *
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.
2826 *
2827 * @param array $params
2828 *
2829 * @return array
2830 * e.g ['5_a_b' => ['contact_type' => 'Organization', 'organization_name' => 'The Firm']]
2831 * @throws \API_Exception
2832 */
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
2839 // contact.
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;
2845 }
2846 }
2847 return $relatedContacts;
2848 }
2849
2850 /**
2851 * Look up for an existing contact with the given external_identifier.
2852 *
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.
2856 *
2857 * @param string|null $externalIdentifier
2858 * @param string $contactType
2859 *
2860 * @return int|null
2861 *
2862 * @throws \CRM_Core_Exception
2863 * @throws \CiviCRM_API3_Exception
2864 */
2865 protected function lookupExternalIdentifier(?string $externalIdentifier, string $contactType): ?int {
2866 if (!$externalIdentifier) {
2867 return NULL;
2868 }
2869 // Check for any match on external id, deleted or otherwise.
2870 $foundContact = civicrm_api3('Contact', 'get', [
2871 'external_identifier' => $externalIdentifier,
2872 'showAll' => 'all',
2873 'sequential' => TRUE,
2874 'return' => ['id', 'contact_is_deleted', 'contact_type'],
2875 ]);
2876 if (empty($foundContact['id'])) {
2877 return NULL;
2878 }
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);
2884 return NULL;
2885 }
2886 if ($foundContact['values'][0]['contact_type'] !== $contactType) {
2887 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser::NO_MATCH);
2888 }
2889 return (int) $foundContact['id'];
2890 }
2891
2892 /**
2893 * Lookup the contact's contact ID.
2894 *
2895 * @param array $params
2896 * @param bool $isDuplicateIfExternalIdentifierExists
2897 *
2898 * @return int|null
2899 *
2900 * @throws \API_Exception
2901 * @throws \CRM_Core_Exception
2902 * @throws \CiviCRM_API3_Exception
2903 */
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);
2910 }
2911 if ($extIDMatch && $isDuplicateIfExternalIdentifierExists) {
2912 throw new CRM_Core_Exception(ts('External ID already exists in Database.'), CRM_Import_Parser::DUPLICATE);
2913 }
2914 if ($contactID) {
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);
2922 }
2923 if ($existingContact['contact_type'] !== $params['contact_type']) {
2924 throw new CRM_Core_Exception('Mismatched contact Types', CRM_Import_Parser::NO_MATCH);
2925 }
2926 return $contactID;
2927 }
2928 // Time to see if we can find an existing contact ID to make this an update
2929 // not a create.
2930 if ($extIDMatch || $this->isUpdateExistingContacts()) {
2931 return $this->getPossibleContactMatch($params, $extIDMatch, $this->getSubmittedValue('dedupe_rule_id'));
2932 }
2933 return NULL;
2934 }
2935
2936 }