Merge pull request #20140 from civicrm/5.37
[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
14 require_once 'CRM/Utils/DeprecatedUtils.php';
15 require_once 'api/v3/utils.php';
16
17 /**
18 *
19 * @package CRM
20 * @copyright CiviCRM LLC https://civicrm.org/licensing
21 */
22
23 /**
24 * class to parse contact csv files
25 */
26 class CRM_Contact_Import_Parser_Contact extends CRM_Contact_Import_Parser {
27
28 use CRM_Contact_Import_MetadataTrait;
29
30 protected $_mapperKeys = [];
31 protected $_mapperLocType = [];
32 protected $_mapperPhoneType;
33 protected $_mapperImProvider;
34 protected $_mapperWebsiteType;
35 protected $_mapperRelated;
36 protected $_mapperRelatedContactType;
37 protected $_mapperRelatedContactDetails;
38 protected $_mapperRelatedContactEmailType;
39 protected $_mapperRelatedContactImProvider;
40 protected $_mapperRelatedContactWebsiteType;
41 protected $_relationships;
42
43 protected $_emailIndex;
44 protected $_firstNameIndex;
45 protected $_lastNameIndex;
46
47 protected $_householdNameIndex;
48 protected $_organizationNameIndex;
49
50 protected $_allEmails;
51
52 protected $_phoneIndex;
53
54 /**
55 * Is update only permitted on an id match.
56 *
57 * Note this historically was true for when id or external identifier was
58 * present. However, CRM-17275 determined that a dedupe-match could over-ride
59 * external identifier.
60 *
61 * @var bool
62 */
63 protected $_updateWithId;
64 protected $_retCode;
65
66 protected $_externalIdentifierIndex;
67 protected $_allExternalIdentifiers;
68 protected $_parseStreetAddress;
69
70 /**
71 * Array of successfully imported contact id's
72 *
73 * @var array
74 */
75 protected $_newContacts;
76
77 /**
78 * Line count id.
79 *
80 * @var int
81 */
82 protected $_lineCount;
83
84 /**
85 * Array of successfully imported related contact id's
86 *
87 * @var array
88 */
89 protected $_newRelatedContacts;
90
91 /**
92 * Array of all the contacts whose street addresses are not parsed.
93 * of this import process
94 * @var array
95 */
96 protected $_unparsedStreetAddressContacts;
97
98 /**
99 * Class constructor.
100 *
101 * @param array $mapperKeys
102 * @param array $mapperLocType
103 * @param array $mapperPhoneType
104 * @param array $mapperImProvider
105 * @param array $mapperRelated
106 * @param array $mapperRelatedContactType
107 * @param array $mapperRelatedContactDetails
108 * @param array $mapperRelatedContactLocType
109 * @param array $mapperRelatedContactPhoneType
110 * @param array $mapperRelatedContactImProvider
111 * @param array $mapperWebsiteType
112 * @param array $mapperRelatedContactWebsiteType
113 */
114 public function __construct(
115 $mapperKeys, $mapperLocType = [], $mapperPhoneType = [], $mapperImProvider = [], $mapperRelated = [], $mapperRelatedContactType = [], $mapperRelatedContactDetails = [], $mapperRelatedContactLocType = [], $mapperRelatedContactPhoneType = [], $mapperRelatedContactImProvider = [],
116 $mapperWebsiteType = [], $mapperRelatedContactWebsiteType = []
117 ) {
118 parent::__construct();
119 $this->_mapperKeys = $mapperKeys;
120 $this->_mapperLocType = &$mapperLocType;
121 $this->_mapperPhoneType = &$mapperPhoneType;
122 $this->_mapperWebsiteType = $mapperWebsiteType;
123 // get IM service provider type id for contact
124 $this->_mapperImProvider = &$mapperImProvider;
125 $this->_mapperRelated = &$mapperRelated;
126 $this->_mapperRelatedContactType = &$mapperRelatedContactType;
127 $this->_mapperRelatedContactDetails = &$mapperRelatedContactDetails;
128 $this->_mapperRelatedContactLocType = &$mapperRelatedContactLocType;
129 $this->_mapperRelatedContactPhoneType = &$mapperRelatedContactPhoneType;
130 $this->_mapperRelatedContactWebsiteType = $mapperRelatedContactWebsiteType;
131 // get IM service provider type id for related contact
132 $this->_mapperRelatedContactImProvider = &$mapperRelatedContactImProvider;
133 }
134
135 /**
136 * The initializer code, called before processing.
137 */
138 public function init() {
139 $this->setFieldMetadata();
140 foreach ($this->getImportableFieldsMetadata() as $name => $field) {
141 $this->addField($name, $field['title'], CRM_Utils_Array::value('type', $field), CRM_Utils_Array::value('headerPattern', $field), CRM_Utils_Array::value('dataPattern', $field), CRM_Utils_Array::value('hasLocationType', $field));
142 }
143
144 $this->_newContacts = [];
145
146 $this->setActiveFields($this->_mapperKeys);
147 $this->setActiveFieldLocationTypes($this->_mapperLocType);
148 $this->setActiveFieldPhoneTypes($this->_mapperPhoneType);
149 $this->setActiveFieldWebsiteTypes($this->_mapperWebsiteType);
150 //set active fields of IM provider of contact
151 $this->setActiveFieldImProviders($this->_mapperImProvider);
152
153 //related info
154 $this->setActiveFieldRelated($this->_mapperRelated);
155 $this->setActiveFieldRelatedContactType($this->_mapperRelatedContactType);
156 $this->setActiveFieldRelatedContactDetails($this->_mapperRelatedContactDetails);
157 $this->setActiveFieldRelatedContactLocType($this->_mapperRelatedContactLocType);
158 $this->setActiveFieldRelatedContactPhoneType($this->_mapperRelatedContactPhoneType);
159 $this->setActiveFieldRelatedContactWebsiteType($this->_mapperRelatedContactWebsiteType);
160 //set active fields of IM provider of related contact
161 $this->setActiveFieldRelatedContactImProvider($this->_mapperRelatedContactImProvider);
162
163 $this->_phoneIndex = -1;
164 $this->_emailIndex = -1;
165 $this->_firstNameIndex = -1;
166 $this->_lastNameIndex = -1;
167 $this->_householdNameIndex = -1;
168 $this->_organizationNameIndex = -1;
169 $this->_externalIdentifierIndex = -1;
170
171 $index = 0;
172 foreach ($this->_mapperKeys as $key) {
173 if (substr($key, 0, 5) == 'email' && substr($key, 0, 14) != 'email_greeting') {
174 $this->_emailIndex = $index;
175 $this->_allEmails = [];
176 }
177 if (substr($key, 0, 5) == 'phone') {
178 $this->_phoneIndex = $index;
179 }
180 if ($key == 'first_name') {
181 $this->_firstNameIndex = $index;
182 }
183 if ($key == 'last_name') {
184 $this->_lastNameIndex = $index;
185 }
186 if ($key == 'household_name') {
187 $this->_householdNameIndex = $index;
188 }
189 if ($key == 'organization_name') {
190 $this->_organizationNameIndex = $index;
191 }
192
193 if ($key == 'external_identifier') {
194 $this->_externalIdentifierIndex = $index;
195 $this->_allExternalIdentifiers = [];
196 }
197 $index++;
198 }
199
200 $this->_updateWithId = FALSE;
201 if (in_array('id', $this->_mapperKeys) || ($this->_externalIdentifierIndex >= 0 && in_array($this->_onDuplicate, [
202 CRM_Import_Parser::DUPLICATE_UPDATE,
203 CRM_Import_Parser::DUPLICATE_FILL,
204 ]))) {
205 $this->_updateWithId = TRUE;
206 }
207
208 $this->_parseStreetAddress = CRM_Utils_Array::value('street_address_parsing', CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'address_options'), FALSE);
209 }
210
211 /**
212 * Handle the values in mapField mode.
213 *
214 * @param array $values
215 * The array of values belonging to this line.
216 *
217 * @return bool
218 */
219 public function mapField(&$values) {
220 return CRM_Import_Parser::VALID;
221 }
222
223 /**
224 * Handle the values in preview mode.
225 *
226 * @param array $values
227 * The array of values belonging to this line.
228 *
229 * @return bool
230 * the result of this processing
231 */
232 public function preview(&$values) {
233 return $this->summary($values);
234 }
235
236 /**
237 * Handle the values in summary mode.
238 *
239 * @param array $values
240 * The array of values belonging to this line.
241 *
242 * @return bool
243 * the result of this processing
244 */
245 public function summary(&$values) {
246 $erroneousField = NULL;
247 $response = $this->setActiveFieldValues($values, $erroneousField);
248
249 $errorMessage = NULL;
250 $errorRequired = FALSE;
251 switch ($this->_contactType) {
252 case 'Individual':
253 $missingNames = [];
254 if ($this->_firstNameIndex < 0 || empty($values[$this->_firstNameIndex])) {
255 $errorRequired = TRUE;
256 $missingNames[] = ts('First Name');
257 }
258 if ($this->_lastNameIndex < 0 || empty($values[$this->_lastNameIndex])) {
259 $errorRequired = TRUE;
260 $missingNames[] = ts('Last Name');
261 }
262 if ($errorRequired) {
263 $and = ' ' . ts('and') . ' ';
264 $errorMessage = ts('Missing required fields:') . ' ' . implode($and, $missingNames);
265 }
266 break;
267
268 case 'Household':
269 if ($this->_householdNameIndex < 0 || empty($values[$this->_householdNameIndex])) {
270 $errorRequired = TRUE;
271 $errorMessage = ts('Missing required fields:') . ' ' . ts('Household Name');
272 }
273 break;
274
275 case 'Organization':
276 if ($this->_organizationNameIndex < 0 || empty($values[$this->_organizationNameIndex])) {
277 $errorRequired = TRUE;
278 $errorMessage = ts('Missing required fields:') . ' ' . ts('Organization Name');
279 }
280 break;
281 }
282
283 $statusFieldName = $this->_statusFieldName;
284
285 if ($this->_emailIndex >= 0) {
286 /* If we don't have the required fields, bail */
287
288 if ($this->_contactType == 'Individual' && !$this->_updateWithId) {
289 if ($errorRequired && empty($values[$this->_emailIndex])) {
290 if ($errorMessage) {
291 $errorMessage .= ' ' . ts('OR') . ' ' . ts('Email Address');
292 }
293 else {
294 $errorMessage = ts('Missing required field:') . ' ' . ts('Email Address');
295 }
296 array_unshift($values, $errorMessage);
297 $importRecordParams = [
298 $statusFieldName => 'ERROR',
299 "${statusFieldName}Msg" => $errorMessage,
300 ];
301 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
302
303 return CRM_Import_Parser::ERROR;
304 }
305 }
306
307 $email = $values[$this->_emailIndex] ?? NULL;
308 if ($email) {
309 /* If the email address isn't valid, bail */
310
311 if (!CRM_Utils_Rule::email($email)) {
312 $errorMessage = ts('Invalid Email address');
313 array_unshift($values, $errorMessage);
314 $importRecordParams = [
315 $statusFieldName => 'ERROR',
316 "${statusFieldName}Msg" => $errorMessage,
317 ];
318 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
319
320 return CRM_Import_Parser::ERROR;
321 }
322
323 /* otherwise, count it and move on */
324 $this->_allEmails[$email] = $this->_lineCount;
325 }
326 }
327 elseif ($errorRequired && !$this->_updateWithId) {
328 if ($errorMessage) {
329 $errorMessage .= ' ' . ts('OR') . ' ' . ts('Email Address');
330 }
331 else {
332 $errorMessage = ts('Missing required field:') . ' ' . ts('Email Address');
333 }
334 array_unshift($values, $errorMessage);
335 $importRecordParams = [
336 $statusFieldName => 'ERROR',
337 "${statusFieldName}Msg" => $errorMessage,
338 ];
339 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
340
341 return CRM_Import_Parser::ERROR;
342 }
343
344 //check for duplicate external Identifier
345 $externalID = $values[$this->_externalIdentifierIndex] ?? NULL;
346 if ($externalID) {
347 /* If it's a dupe,external Identifier */
348
349 if ($externalDupe = CRM_Utils_Array::value($externalID, $this->_allExternalIdentifiers)) {
350 $errorMessage = ts('External ID conflicts with record %1', [1 => $externalDupe]);
351 array_unshift($values, $errorMessage);
352 $importRecordParams = [
353 $statusFieldName => 'ERROR',
354 "${statusFieldName}Msg" => $errorMessage,
355 ];
356 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
357 return CRM_Import_Parser::ERROR;
358 }
359 //otherwise, count it and move on
360 $this->_allExternalIdentifiers[$externalID] = $this->_lineCount;
361 }
362
363 //Checking error in custom data
364 $params = &$this->getActiveFieldParams();
365 $params['contact_type'] = $this->_contactType;
366 //date-format part ends
367
368 $errorMessage = NULL;
369
370 //CRM-5125
371 //add custom fields for contact sub type
372 $csType = NULL;
373 if (!empty($this->_contactSubType)) {
374 $csType = $this->_contactSubType;
375 }
376
377 //checking error in custom data
378 $this->isErrorInCustomData($params, $errorMessage, $csType, $this->_relationships);
379
380 //checking error in core data
381 $this->isErrorInCoreData($params, $errorMessage);
382 if ($errorMessage) {
383 $tempMsg = "Invalid value for field(s) : $errorMessage";
384 // put the error message in the import record in the DB
385 $importRecordParams = [
386 $statusFieldName => 'ERROR',
387 "${statusFieldName}Msg" => $tempMsg,
388 ];
389 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
390 array_unshift($values, $tempMsg);
391 $errorMessage = NULL;
392 return CRM_Import_Parser::ERROR;
393 }
394
395 //if user correcting errors by walking back
396 //need to reset status ERROR msg to null
397 //now currently we are having valid data.
398 $importRecordParams = [
399 $statusFieldName => 'NEW',
400 ];
401 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
402
403 return CRM_Import_Parser::VALID;
404 }
405
406 /**
407 * Get Array of all the fields that could potentially be part
408 * import process
409 *
410 * @return array
411 */
412 public function getAllFields() {
413 return $this->_fields;
414 }
415
416 /**
417 * Handle the values in import mode.
418 *
419 * @param int $onDuplicate
420 * The code for what action to take on duplicates.
421 * @param array $values
422 * The array of values belonging to this line.
423 *
424 * @param bool $doGeocodeAddress
425 *
426 * @return bool
427 * the result of this processing
428 *
429 * @throws \CiviCRM_API3_Exception
430 * @throws \CRM_Core_Exception
431 * @throws \API_Exception
432 */
433 public function import($onDuplicate, &$values, $doGeocodeAddress = FALSE) {
434 $config = CRM_Core_Config::singleton();
435 $this->_unparsedStreetAddressContacts = [];
436 if (!$doGeocodeAddress) {
437 // CRM-5854, reset the geocode method to null to prevent geocoding
438 CRM_Utils_GeocodeProvider::disableForSession();
439 }
440
441 // first make sure this is a valid line
442 //$this->_updateWithId = false;
443 $response = $this->summary($values);
444 $statusFieldName = $this->_statusFieldName;
445
446 if ($response != CRM_Import_Parser::VALID) {
447 $importRecordParams = [
448 $statusFieldName => 'INVALID',
449 "${statusFieldName}Msg" => "Invalid (Error Code: $response)",
450 ];
451 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
452 return $response;
453 }
454
455 $params = &$this->getActiveFieldParams();
456 $formatted = [
457 'contact_type' => $this->_contactType,
458 ];
459
460 $contactFields = CRM_Contact_DAO_Contact::import();
461
462 //check if external identifier exists in database
463 if (!empty($params['external_identifier']) && (!empty($params['id']) || in_array($onDuplicate, [
464 CRM_Import_Parser::DUPLICATE_SKIP,
465 CRM_Import_Parser::DUPLICATE_NOCHECK,
466 ]))) {
467
468 $extIDResult = civicrm_api3('Contact', 'get', [
469 'external_identifier' => $params['external_identifier'],
470 'showAll' => 'all',
471 'return' => ['id', 'contact_is_deleted'],
472 ]);
473 if (isset($extIDResult['id'])) {
474 // record with matching external identifier does exist.
475 $internalCid = $extIDResult['id'];
476 if ($internalCid != CRM_Utils_Array::value('id', $params)) {
477 if ($extIDResult['values'][$internalCid]['contact_is_deleted'] == 1) {
478 // And it is deleted. What to do? If we skip it, they user
479 // will be under the impression that the record exists in
480 // the database, yet they won't be able to find it. If we
481 // don't skip it, the database will try to insert a new record
482 // with an external_identifier that is non-unique. So...
483 // we will update this contact to remove the external_identifier
484 // and let a new record be created.
485 $update_params = ['id' => $internalCid, 'external_identifier' => ''];
486 civicrm_api3('Contact', 'create', $update_params);
487 }
488 else {
489 $errorMessage = ts('External ID already exists in Database.');
490 array_unshift($values, $errorMessage);
491 $importRecordParams = [
492 $statusFieldName => 'ERROR',
493 "${statusFieldName}Msg" => $errorMessage,
494 ];
495 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
496 return CRM_Import_Parser::DUPLICATE;
497 }
498 }
499 }
500 }
501
502 if (!empty($this->_contactSubType)) {
503 $params['contact_sub_type'] = $this->_contactSubType;
504 }
505
506 if ($subType = CRM_Utils_Array::value('contact_sub_type', $params)) {
507 if (CRM_Contact_BAO_ContactType::isExtendsContactType($subType, $this->_contactType, FALSE, 'label')) {
508 $subTypes = CRM_Contact_BAO_ContactType::subTypePairs($this->_contactType, FALSE, NULL);
509 $params['contact_sub_type'] = array_search($subType, $subTypes);
510 }
511 elseif (!CRM_Contact_BAO_ContactType::isExtendsContactType($subType, $this->_contactType)) {
512 $message = "Mismatched or Invalid Contact Subtype.";
513 array_unshift($values, $message);
514 return CRM_Import_Parser::NO_MATCH;
515 }
516 }
517
518 // Get contact id to format common data in update/fill mode,
519 // prioritising a dedupe rule check over an external_identifier check, but falling back on ext id.
520 if ($this->_updateWithId && empty($params['id'])) {
521 try {
522 $possibleMatches = $this->getPossibleContactMatches($params);
523 }
524 catch (CRM_Core_Exception $e) {
525 $errorMessage = $e->getMessage();
526 array_unshift($values, $errorMessage);
527
528 $importRecordParams = [
529 $statusFieldName => 'ERROR',
530 "${statusFieldName}Msg" => $errorMessage,
531 ];
532 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
533 return CRM_Import_Parser::ERROR;
534 }
535 foreach ($possibleMatches as $possibleID) {
536 $params['id'] = $formatted['id'] = $possibleID;
537 }
538 }
539 //format common data, CRM-4062
540 $this->formatCommonData($params, $formatted, $contactFields);
541
542 $relationship = FALSE;
543 $createNewContact = TRUE;
544 // Support Match and Update Via Contact ID
545 if ($this->_updateWithId && isset($params['id'])) {
546 $createNewContact = FALSE;
547 // @todo - it feels like all the rows from here to the end of the IF
548 // could be removed in favour of a simple check for whether the contact_type & id match
549 $matchedIDs = $this->getIdsOfMatchingContacts($formatted);
550 if (!empty($matchedIDs)) {
551 if (count($matchedIDs) >= 1) {
552 $updateflag = TRUE;
553 foreach ($matchedIDs as $contactId) {
554 if ($params['id'] == $contactId) {
555 $contactType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_type');
556 if ($formatted['contact_type'] == $contactType) {
557 //validation of subtype for update mode
558 //CRM-5125
559 $contactSubType = NULL;
560 if (!empty($params['contact_sub_type'])) {
561 $contactSubType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_sub_type');
562 }
563
564 if (!empty($contactSubType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($params['id'], $contactSubType) && $contactSubType != CRM_Utils_Array::value('contact_sub_type', $formatted))) {
565
566 $message = "Mismatched contact SubTypes :";
567 array_unshift($values, $message);
568 $updateflag = FALSE;
569 $this->_retCode = CRM_Import_Parser::NO_MATCH;
570 }
571 else {
572 $updateflag = FALSE;
573 $this->_retCode = CRM_Import_Parser::VALID;
574 }
575 }
576 else {
577 $message = "Mismatched contact Types :";
578 array_unshift($values, $message);
579 $updateflag = FALSE;
580 $this->_retCode = CRM_Import_Parser::NO_MATCH;
581 }
582 }
583 }
584 if ($updateflag) {
585 $message = "Mismatched contact IDs OR Mismatched contact Types :";
586 array_unshift($values, $message);
587 $this->_retCode = CRM_Import_Parser::NO_MATCH;
588 }
589 }
590 }
591 else {
592 $contactType = NULL;
593 if (!empty($params['id'])) {
594 $contactType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_type');
595 if ($contactType) {
596 if ($formatted['contact_type'] == $contactType) {
597 //validation of subtype for update mode
598 //CRM-5125
599 $contactSubType = NULL;
600 if (!empty($params['contact_sub_type'])) {
601 $contactSubType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_sub_type');
602 }
603
604 if (!empty($contactSubType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($params['id'], $contactSubType) && $contactSubType != CRM_Utils_Array::value('contact_sub_type', $formatted))) {
605
606 $message = "Mismatched contact SubTypes :";
607 array_unshift($values, $message);
608 $this->_retCode = CRM_Import_Parser::NO_MATCH;
609 }
610 else {
611 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $params['id'], FALSE, $this->_dedupeRuleGroupID);
612 $this->_retCode = CRM_Import_Parser::VALID;
613 }
614 }
615 else {
616 $message = "Mismatched contact Types :";
617 array_unshift($values, $message);
618 $this->_retCode = CRM_Import_Parser::NO_MATCH;
619 }
620 }
621 else {
622 // we should avoid multiple errors for single record
623 // since we have already retCode and we trying to force again.
624 if ($this->_retCode != CRM_Import_Parser::NO_MATCH) {
625 $message = "No contact found for this contact ID:" . $params['id'];
626 array_unshift($values, $message);
627 $this->_retCode = CRM_Import_Parser::NO_MATCH;
628 }
629 }
630 }
631 else {
632 //CRM-4148
633 //now we want to create new contact on update/fill also.
634 $createNewContact = TRUE;
635 }
636 }
637
638 if (isset($newContact) && is_a($newContact, 'CRM_Contact_BAO_Contact')) {
639 $relationship = TRUE;
640 }
641 }
642
643 //fixed CRM-4148
644 //now we create new contact in update/fill mode also.
645 $contactID = NULL;
646 if ($createNewContact || ($this->_retCode != CRM_Import_Parser::NO_MATCH && $this->_updateWithId)) {
647 // @todo - there are multiple places where formatting is done that need consolidation.
648 // This handles where the label has been passed in and it has gotten this far.
649 // probably a bunch of hard-coded stuff could be removed to rely on this.
650 $fields = Contact::getFields(FALSE)
651 ->addWhere('options', '=', TRUE)
652 ->setLoadOptions(TRUE)
653 ->execute()->indexBy('name');
654 foreach ($fields as $fieldName => $fieldSpec) {
655 if (!empty($formatted[$fieldName])
656 && empty($fieldSpec['options'][$formatted[$fieldName]])) {
657 $formatted[$fieldName] = array_search($formatted[$fieldName], $fieldSpec['options'], TRUE) ?? $formatted[$fieldName];
658 }
659 }
660 //CRM-4430, don't carry if not submitted.
661 if ($this->_updateWithId && !empty($params['id'])) {
662 $contactID = $params['id'];
663 }
664 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID);
665 }
666
667 if (isset($newContact) && is_object($newContact) && ($newContact instanceof CRM_Contact_BAO_Contact)) {
668 $relationship = TRUE;
669 $newContact = clone($newContact);
670 $contactID = $newContact->id;
671 $this->_newContacts[] = $contactID;
672
673 //get return code if we create new contact in update mode, CRM-4148
674 if ($this->_updateWithId) {
675 $this->_retCode = CRM_Import_Parser::VALID;
676 }
677 }
678 elseif (isset($newContact) && CRM_Core_Error::isAPIError($newContact, CRM_Core_Error::DUPLICATE_CONTACT)) {
679 // if duplicate, no need of further processing
680 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
681 $errorMessage = "Skipping duplicate record";
682 array_unshift($values, $errorMessage);
683 $importRecordParams = [
684 $statusFieldName => 'DUPLICATE',
685 "${statusFieldName}Msg" => $errorMessage,
686 ];
687 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
688 return CRM_Import_Parser::DUPLICATE;
689 }
690
691 $relationship = TRUE;
692 // CRM-10433/CRM-20739 - IDs could be string or array; handle accordingly
693 if (!is_array($dupeContactIDs = $newContact['error_message']['params'][0])) {
694 $dupeContactIDs = explode(',', $dupeContactIDs);
695 }
696 $dupeCount = count($dupeContactIDs);
697 $contactID = array_pop($dupeContactIDs);
698 // check to see if we had more than one duplicate contact id.
699 // if we have more than one, the record will be rejected below
700 if ($dupeCount == 1) {
701 // there was only one dupe, we will continue normally...
702 if (!in_array($contactID, $this->_newContacts)) {
703 $this->_newContacts[] = $contactID;
704 }
705 }
706 }
707
708 if ($contactID) {
709 // call import hook
710 $currentImportID = end($values);
711
712 $hookParams = [
713 'contactID' => $contactID,
714 'importID' => $currentImportID,
715 'importTempTable' => $this->_tableName,
716 'fieldHeaders' => $this->_mapperKeys,
717 'fields' => $this->_activeFields,
718 ];
719
720 CRM_Utils_Hook::import('Contact', 'process', $this, $hookParams);
721 }
722
723 if ($relationship) {
724 $primaryContactId = NULL;
725 if (CRM_Core_Error::isAPIError($newContact, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
726 if ($dupeCount == 1 && CRM_Utils_Rule::integer($contactID)) {
727 $primaryContactId = $contactID;
728 }
729 }
730 else {
731 $primaryContactId = $newContact->id;
732 }
733
734 if ((CRM_Core_Error::isAPIError($newContact, CRM_Core_ERROR::DUPLICATE_CONTACT) || is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) {
735
736 //relationship contact insert
737 foreach ($params as $key => $field) {
738 [$id, $first, $second] = CRM_Utils_System::explode('_', $key, 3);
739 if (!($first == 'a' && $second == 'b') && !($first == 'b' && $second == 'a')) {
740 continue;
741 }
742
743 $relationType = new CRM_Contact_DAO_RelationshipType();
744 $relationType->id = $id;
745 $relationType->find(TRUE);
746 $direction = "contact_sub_type_$second";
747
748 $formatting = [
749 'contact_type' => $params[$key]['contact_type'],
750 ];
751
752 //set subtype for related contact CRM-5125
753 if (isset($relationType->$direction)) {
754 //validation of related contact subtype for update mode
755 if ($relCsType = CRM_Utils_Array::value('contact_sub_type', $params[$key]) && $relCsType != $relationType->$direction) {
756 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
757 array_unshift($values, $errorMessage);
758 return CRM_Import_Parser::NO_MATCH;
759 }
760 else {
761 $formatting['contact_sub_type'] = $relationType->$direction;
762 }
763 }
764
765 $contactFields = NULL;
766 $contactFields = CRM_Contact_DAO_Contact::import();
767
768 //Relation on the basis of External Identifier.
769 if (empty($params[$key]['id']) && !empty($params[$key]['external_identifier'])) {
770 $params[$key]['id'] = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['external_identifier'], 'id', 'external_identifier');
771 }
772 // check for valid related contact id in update/fill mode, CRM-4424
773 if (in_array($onDuplicate, [
774 CRM_Import_Parser::DUPLICATE_UPDATE,
775 CRM_Import_Parser::DUPLICATE_FILL,
776 ]) && !empty($params[$key]['id'])) {
777 $relatedContactType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_type');
778 if (!$relatedContactType) {
779 $errorMessage = ts("No contact found for this related contact ID: %1", [1 => $params[$key]['id']]);
780 array_unshift($values, $errorMessage);
781 return CRM_Import_Parser::NO_MATCH;
782 }
783
784 //validation of related contact subtype for update mode
785 //CRM-5125
786 $relatedCsType = NULL;
787 if (!empty($formatting['contact_sub_type'])) {
788 $relatedCsType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_sub_type');
789 }
790
791 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($params[$key]['id'], $relatedCsType) &&
792 $relatedCsType != CRM_Utils_Array::value('contact_sub_type', $formatting))
793 ) {
794 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.") . ' ' . ts("ID: %1", [1 => $params[$key]['id']]);
795 array_unshift($values, $errorMessage);
796 return CRM_Import_Parser::NO_MATCH;
797 }
798 // get related contact id to format data in update/fill mode,
799 //if external identifier is present, CRM-4423
800 $formatting['id'] = $params[$key]['id'];
801 }
802
803 //format common data, CRM-4062
804 $this->formatCommonData($field, $formatting, $contactFields);
805
806 //do we have enough fields to create related contact.
807 $allowToCreate = $this->checkRelatedContactFields($key, $formatting);
808
809 if (!$allowToCreate) {
810 $errorMessage = ts('Related contact required fields are missing.');
811 array_unshift($values, $errorMessage);
812 return CRM_Import_Parser::NO_MATCH;
813 }
814
815 //fixed for CRM-4148
816 if (!empty($params[$key]['id'])) {
817 $contact = [
818 'contact_id' => $params[$key]['id'],
819 ];
820 $defaults = [];
821 $relatedNewContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
822 }
823 else {
824 $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE);
825 }
826
827 if (is_object($relatedNewContact) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) {
828 $relatedNewContact = clone($relatedNewContact);
829 }
830
831 $matchedIDs = [];
832 // To update/fill contact, get the matching contact Ids if duplicate contact found
833 // otherwise get contact Id from object of related contact
834 if (is_array($relatedNewContact) && civicrm_error($relatedNewContact)) {
835 if (CRM_Core_Error::isAPIError($relatedNewContact, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
836 $matchedIDs = $relatedNewContact['error_message']['params'][0];
837 if (!is_array($matchedIDs)) {
838 $matchedIDs = explode(',', $matchedIDs);
839 }
840 }
841 else {
842 $errorMessage = $relatedNewContact['error_message'];
843 array_unshift($values, $errorMessage);
844 $importRecordParams = [
845 $statusFieldName => 'ERROR',
846 "${statusFieldName}Msg" => $errorMessage,
847 ];
848 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
849 return CRM_Import_Parser::ERROR;
850 }
851 }
852 else {
853 $matchedIDs[] = $relatedNewContact->id;
854 }
855 // update/fill related contact after getting matching Contact Ids, CRM-4424
856 if (in_array($onDuplicate, [
857 CRM_Import_Parser::DUPLICATE_UPDATE,
858 CRM_Import_Parser::DUPLICATE_FILL,
859 ])) {
860 //validation of related contact subtype for update mode
861 //CRM-5125
862 $relatedCsType = NULL;
863 if (!empty($formatting['contact_sub_type'])) {
864 $relatedCsType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $matchedIDs[0], 'contact_sub_type');
865 }
866
867 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($matchedIDs[0], $relatedCsType) && $relatedCsType != CRM_Utils_Array::value('contact_sub_type', $formatting))) {
868 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
869 array_unshift($values, $errorMessage);
870 return CRM_Import_Parser::NO_MATCH;
871 }
872 else {
873 $updatedContact = $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]);
874 }
875 }
876 static $relativeContact = [];
877 if (CRM_Core_Error::isAPIError($relatedNewContact, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
878 if (count($matchedIDs) >= 1) {
879 $relContactId = $matchedIDs[0];
880 //add relative contact to count during update & fill mode.
881 //logic to make count distinct by contact id.
882 if ($this->_newRelatedContacts || !empty($relativeContact)) {
883 $reContact = array_keys($relativeContact, $relContactId);
884
885 if (empty($reContact)) {
886 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
887 }
888 }
889 else {
890 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
891 }
892 }
893 }
894 else {
895 $relContactId = $relatedNewContact->id;
896 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
897 }
898
899 if (CRM_Core_Error::isAPIError($relatedNewContact, CRM_Core_ERROR::DUPLICATE_CONTACT) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) {
900 //fix for CRM-1993.Checks for duplicate related contacts
901 if (count($matchedIDs) >= 1) {
902 //if more than one duplicate contact
903 //found, create relationship with first contact
904 // now create the relationship record
905 $relationParams = [];
906 $relationParams = [
907 'relationship_type_id' => $key,
908 'contact_check' => [
909 $relContactId => 1,
910 ],
911 'is_active' => 1,
912 'skipRecentView' => TRUE,
913 ];
914
915 // we only handle related contact success, we ignore failures for now
916 // at some point wold be nice to have related counts as separate
917 $relationIds = [
918 'contact' => $primaryContactId,
919 ];
920
921 [$valid, $invalid, $duplicate, $saved, $relationshipIds] = CRM_Contact_BAO_Relationship::legacyCreateMultiple($relationParams, $relationIds);
922
923 if ($valid || $duplicate) {
924 $relationIds['contactTarget'] = $relContactId;
925 $action = ($duplicate) ? CRM_Core_Action::UPDATE : CRM_Core_Action::ADD;
926 CRM_Contact_BAO_Relationship::relatedMemberships($primaryContactId, $relationParams, $relationIds, $action);
927 }
928
929 //handle current employer, CRM-3532
930 if ($valid) {
931 $allRelationships = CRM_Core_PseudoConstant::relationshipType('name');
932 $relationshipTypeId = str_replace([
933 '_a_b',
934 '_b_a',
935 ], [
936 '',
937 '',
938 ], $key);
939 $relationshipType = str_replace($relationshipTypeId . '_', '', $key);
940 $orgId = $individualId = NULL;
941 if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') {
942 $orgId = $relContactId;
943 $individualId = $primaryContactId;
944 }
945 elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') {
946 $orgId = $primaryContactId;
947 $individualId = $relContactId;
948 }
949 if ($orgId && $individualId) {
950 $currentEmpParams[$individualId] = $orgId;
951 CRM_Contact_BAO_Contact_Utils::setCurrentEmployer($currentEmpParams);
952 }
953 }
954 }
955 }
956 }
957 }
958 }
959 if ($this->_updateWithId) {
960 //return warning if street address is unparsed, CRM-5886
961 return $this->processMessage($values, $statusFieldName, $this->_retCode);
962 }
963 //dupe checking
964 if (is_array($newContact) && civicrm_error($newContact)) {
965 $code = NULL;
966
967 if (($code = CRM_Utils_Array::value('code', $newContact['error_message'])) && ($code == CRM_Core_Error::DUPLICATE_CONTACT)) {
968 return $this->handleDuplicateError($newContact, $statusFieldName, $values, $onDuplicate, $formatted, $contactFields);
969 }
970 // Not a dupe, so we had an error
971 $errorMessage = $newContact['error_message'];
972 array_unshift($values, $errorMessage);
973 $importRecordParams = [
974 $statusFieldName => 'ERROR',
975 "${statusFieldName}Msg" => $errorMessage,
976 ];
977 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
978 return CRM_Import_Parser::ERROR;
979
980 }
981 // sleep(3);
982 return $this->processMessage($values, $statusFieldName, CRM_Import_Parser::VALID);
983 }
984
985 /**
986 * Get the array of successfully imported contact id's
987 *
988 * @return array
989 */
990 public function getImportedContacts() {
991 return $this->_newContacts;
992 }
993
994 /**
995 * Get the array of successfully imported related contact id's
996 *
997 * @return array
998 */
999 public function &getRelatedImportedContacts() {
1000 return $this->_newRelatedContacts;
1001 }
1002
1003 /**
1004 * The initializer code, called before the processing.
1005 */
1006 public function fini() {
1007 }
1008
1009 /**
1010 * Check if an error in custom data.
1011 *
1012 * @param array $params
1013 * @param string $errorMessage
1014 * A string containing all the error-fields.
1015 *
1016 * @param null $csType
1017 * @param null $relationships
1018 */
1019 public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL, $relationships = NULL) {
1020 $dateType = CRM_Core_Session::singleton()->get("dateTypes");
1021
1022 if (!empty($params['contact_sub_type'])) {
1023 $csType = $params['contact_sub_type'] ?? NULL;
1024 }
1025
1026 if (empty($params['contact_type'])) {
1027 $params['contact_type'] = 'Individual';
1028 }
1029
1030 // get array of subtypes - CRM-18708
1031 if (in_array($csType, ['Individual', 'Organization', 'Household'])) {
1032 $csType = self::getSubtypes($params['contact_type']);
1033 }
1034
1035 if (is_array($csType)) {
1036 // fetch custom fields for every subtype and add it to $customFields array
1037 // CRM-18708
1038 $customFields = [];
1039 foreach ($csType as $cType) {
1040 $customFields += CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $cType);
1041 }
1042 }
1043 else {
1044 $customFields = CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $csType);
1045 }
1046
1047 $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address');
1048 $customFields = $customFields + $addressCustomFields;
1049 foreach ($params as $key => $value) {
1050 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
1051 /* check if it's a valid custom field id */
1052
1053 if (!array_key_exists($customFieldID, $customFields)) {
1054 self::addToErrorMsg(ts('field ID'), $errorMessage);
1055 }
1056 // validate null values for required custom fields of type boolean
1057 if (!empty($customFields[$customFieldID]['is_required']) && (empty($params['custom_' . $customFieldID]) && !is_numeric($params['custom_' . $customFieldID])) && $customFields[$customFieldID]['data_type'] == 'Boolean') {
1058 self::addToErrorMsg($customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'], $errorMessage);
1059 }
1060
1061 //For address custom fields, we do get actual custom field value as an inner array of
1062 //values so need to modify
1063 if (array_key_exists($customFieldID, $addressCustomFields)) {
1064 $value = $value[0][$key];
1065 }
1066 /* validate the data against the CF type */
1067
1068 if ($value) {
1069 $dataType = $customFields[$customFieldID]['data_type'];
1070 $htmlType = $customFields[$customFieldID]['html_type'];
1071 $isSerialized = CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]);
1072 if ($dataType == 'Date') {
1073 if (array_key_exists($customFieldID, $addressCustomFields) && CRM_Utils_Date::convertToDefaultDate($params[$key][0], $dateType, $key)) {
1074 $value = $params[$key][0][$key];
1075 }
1076 elseif (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
1077 $value = $params[$key];
1078 }
1079 else {
1080 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1081 }
1082 }
1083 elseif ($dataType == 'Boolean') {
1084 if (CRM_Utils_String::strtoboolstr($value) === FALSE) {
1085 self::addToErrorMsg($customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'], $errorMessage);
1086 }
1087 }
1088 // need not check for label filed import
1089 $selectHtmlTypes = [
1090 'CheckBox',
1091 'Select',
1092 'Radio',
1093 ];
1094 if ((!$isSerialized && !in_array($htmlType, $selectHtmlTypes)) || $dataType == 'Boolean' || $dataType == 'ContactReference') {
1095 $valid = CRM_Core_BAO_CustomValue::typecheck($dataType, $value);
1096 if (!$valid) {
1097 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1098 }
1099 }
1100
1101 // check for values for custom fields for checkboxes and multiselect
1102 if ($isSerialized) {
1103 $value = trim($value);
1104 $value = str_replace('|', ',', $value);
1105 $mulValues = explode(',', $value);
1106 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1107 foreach ($mulValues as $v1) {
1108 if (strlen($v1) == 0) {
1109 continue;
1110 }
1111
1112 $flag = FALSE;
1113 foreach ($customOption as $v2) {
1114 if ((strtolower(trim($v2['label'])) == strtolower(trim($v1))) || (strtolower(trim($v2['value'])) == strtolower(trim($v1)))) {
1115 $flag = TRUE;
1116 }
1117 }
1118
1119 if (!$flag) {
1120 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1121 }
1122 }
1123 }
1124 elseif ($htmlType == 'Select' || ($htmlType == 'Radio' && $dataType != 'Boolean')) {
1125 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1126 $flag = FALSE;
1127 foreach ($customOption as $v2) {
1128 if ((strtolower(trim($v2['label'])) == strtolower(trim($value))) || (strtolower(trim($v2['value'])) == strtolower(trim($value)))) {
1129 $flag = TRUE;
1130 }
1131 }
1132 if (!$flag) {
1133 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1134 }
1135 }
1136 elseif ($isSerialized && $dataType === 'StateProvince') {
1137 $mulValues = explode(',', $value);
1138 foreach ($mulValues as $stateValue) {
1139 if ($stateValue) {
1140 if (self::in_value(trim($stateValue), CRM_Core_PseudoConstant::stateProvinceAbbreviation()) || self::in_value(trim($stateValue), CRM_Core_PseudoConstant::stateProvince())) {
1141 continue;
1142 }
1143 else {
1144 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1145 }
1146 }
1147 }
1148 }
1149 elseif ($isSerialized && $dataType == 'Country') {
1150 $mulValues = explode(',', $value);
1151 foreach ($mulValues as $countryValue) {
1152 if ($countryValue) {
1153 CRM_Core_PseudoConstant::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
1154 CRM_Core_PseudoConstant::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
1155 $limitCodes = CRM_Core_BAO_Country::countryLimit();
1156
1157 $error = TRUE;
1158 foreach ([
1159 $countryNames,
1160 $countryIsoCodes,
1161 $limitCodes,
1162 ] as $values) {
1163 if (in_array(trim($countryValue), $values)) {
1164 $error = FALSE;
1165 break;
1166 }
1167 }
1168
1169 if ($error) {
1170 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1171 }
1172 }
1173 }
1174 }
1175 }
1176 }
1177 elseif (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
1178 //CRM-5125
1179 //supporting custom data of related contact subtypes
1180 $relation = NULL;
1181 if ($relationships) {
1182 if (array_key_exists($key, $relationships)) {
1183 $relation = $key;
1184 }
1185 elseif (CRM_Utils_Array::key($key, $relationships)) {
1186 $relation = CRM_Utils_Array::key($key, $relationships);
1187 }
1188 }
1189 if (!empty($relation)) {
1190 [$id, $first, $second] = CRM_Utils_System::explode('_', $relation, 3);
1191 $direction = "contact_sub_type_$second";
1192 $relationshipType = new CRM_Contact_BAO_RelationshipType();
1193 $relationshipType->id = $id;
1194 if ($relationshipType->find(TRUE)) {
1195 if (isset($relationshipType->$direction)) {
1196 $params[$key]['contact_sub_type'] = $relationshipType->$direction;
1197 }
1198 }
1199 }
1200
1201 self::isErrorInCustomData($params[$key], $errorMessage, $csType, $relationships);
1202 }
1203 }
1204 }
1205
1206 /**
1207 * Check if value present in all genders or.
1208 * as a substring of any gender value, if yes than return corresponding gender.
1209 * eg value might be m/M, ma/MA, mal/MAL, male return 'Male'
1210 * but if value is 'maleabc' than return false
1211 *
1212 * @param string $gender
1213 * Check this value across gender values.
1214 *
1215 * retunr gender value / false
1216 *
1217 * @return bool
1218 */
1219 public function checkGender($gender) {
1220 $gender = trim($gender, '.');
1221 if (!$gender) {
1222 return FALSE;
1223 }
1224
1225 $allGenders = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'gender_id');
1226 foreach ($allGenders as $key => $value) {
1227 if (strlen($gender) > strlen($value)) {
1228 continue;
1229 }
1230 if ($gender == $value) {
1231 return $value;
1232 }
1233 if (substr_compare($value, $gender, 0, strlen($gender), TRUE) === 0) {
1234 return $value;
1235 }
1236 }
1237
1238 return FALSE;
1239 }
1240
1241 /**
1242 * Check if an error in Core( non-custom fields ) field
1243 *
1244 * @param array $params
1245 * @param string $errorMessage
1246 * A string containing all the error-fields.
1247 */
1248 public function isErrorInCoreData($params, &$errorMessage) {
1249 foreach ($params as $key => $value) {
1250 if ($value) {
1251 $session = CRM_Core_Session::singleton();
1252 $dateType = $session->get("dateTypes");
1253
1254 switch ($key) {
1255 case 'birth_date':
1256 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
1257 if (!CRM_Utils_Rule::date($params[$key])) {
1258 self::addToErrorMsg(ts('Birth Date'), $errorMessage);
1259 }
1260 }
1261 else {
1262 self::addToErrorMsg(ts('Birth-Date'), $errorMessage);
1263 }
1264 break;
1265
1266 case 'deceased_date':
1267 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
1268 if (!CRM_Utils_Rule::date($params[$key])) {
1269 self::addToErrorMsg(ts('Deceased Date'), $errorMessage);
1270 }
1271 }
1272 else {
1273 self::addToErrorMsg(ts('Deceased Date'), $errorMessage);
1274 }
1275 break;
1276
1277 case 'is_deceased':
1278 if (CRM_Utils_String::strtoboolstr($value) === FALSE) {
1279 self::addToErrorMsg(ts('Deceased'), $errorMessage);
1280 }
1281 break;
1282
1283 case 'gender_id':
1284 if (!self::checkGender($value)) {
1285 self::addToErrorMsg(ts('Gender'), $errorMessage);
1286 }
1287 break;
1288
1289 case 'preferred_communication_method':
1290 $preffComm = [];
1291 $preffComm = explode(',', $value);
1292 foreach ($preffComm as $v) {
1293 if (!self::in_value(trim($v), CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'preferred_communication_method'))) {
1294 self::addToErrorMsg(ts('Preferred Communication Method'), $errorMessage);
1295 }
1296 }
1297 break;
1298
1299 case 'preferred_mail_format':
1300 if (!array_key_exists(strtolower($value), array_change_key_case(CRM_Core_SelectValues::pmf(), CASE_LOWER))) {
1301 self::addToErrorMsg(ts('Preferred Mail Format'), $errorMessage);
1302 }
1303 break;
1304
1305 case 'individual_prefix':
1306 case 'prefix_id':
1307 if (!self::in_value($value, CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'prefix_id'))) {
1308 self::addToErrorMsg(ts('Individual Prefix'), $errorMessage);
1309 }
1310 break;
1311
1312 case 'individual_suffix':
1313 case 'suffix_id':
1314 if (!self::in_value($value, CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'suffix_id'))) {
1315 self::addToErrorMsg(ts('Individual Suffix'), $errorMessage);
1316 }
1317 break;
1318
1319 case 'state_province':
1320 if (!empty($value)) {
1321 foreach ($value as $stateValue) {
1322 if ($stateValue['state_province']) {
1323 if (self::in_value($stateValue['state_province'], CRM_Core_PseudoConstant::stateProvinceAbbreviation()) ||
1324 self::in_value($stateValue['state_province'], CRM_Core_PseudoConstant::stateProvince())
1325 ) {
1326 continue;
1327 }
1328 else {
1329 self::addToErrorMsg(ts('State/Province'), $errorMessage);
1330 }
1331 }
1332 }
1333 }
1334 break;
1335
1336 case 'country':
1337 if (!empty($value)) {
1338 foreach ($value as $stateValue) {
1339 if ($stateValue['country']) {
1340 CRM_Core_PseudoConstant::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
1341 CRM_Core_PseudoConstant::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
1342 $limitCodes = CRM_Core_BAO_Country::countryLimit();
1343 //If no country is selected in
1344 //localization then take all countries
1345 if (empty($limitCodes)) {
1346 $limitCodes = $countryIsoCodes;
1347 }
1348
1349 if (self::in_value($stateValue['country'], $limitCodes) || self::in_value($stateValue['country'], CRM_Core_PseudoConstant::country())) {
1350 continue;
1351 }
1352 if (self::in_value($stateValue['country'], $countryIsoCodes) || self::in_value($stateValue['country'], $countryNames)) {
1353 self::addToErrorMsg(ts('Country input value is in table but not "available": "This Country is valid but is NOT in the list of Available Countries currently configured for your site. This can be viewed and modifed from Administer > Localization > Languages Currency Locations." '), $errorMessage);
1354 }
1355 else {
1356 self::addToErrorMsg(ts('Country input value not in country table: "The Country value appears to be invalid. It does not match any value in CiviCRM table of countries."'), $errorMessage);
1357 }
1358 }
1359 }
1360 }
1361 break;
1362
1363 case 'county':
1364 if (!empty($value)) {
1365 foreach ($value as $county) {
1366 if ($county['county']) {
1367 $countyNames = CRM_Core_PseudoConstant::county();
1368 if (!empty($county['county']) && !in_array($county['county'], $countyNames)) {
1369 self::addToErrorMsg(ts('County input value not in county table: The County value appears to be invalid. It does not match any value in CiviCRM table of counties.'), $errorMessage);
1370 }
1371 }
1372 }
1373 }
1374 break;
1375
1376 case 'geo_code_1':
1377 if (!empty($value)) {
1378 foreach ($value as $codeValue) {
1379 if (!empty($codeValue['geo_code_1'])) {
1380 if (CRM_Utils_Rule::numeric($codeValue['geo_code_1'])) {
1381 continue;
1382 }
1383 self::addToErrorMsg(ts('Geo code 1'), $errorMessage);
1384 }
1385 }
1386 }
1387 break;
1388
1389 case 'geo_code_2':
1390 if (!empty($value)) {
1391 foreach ($value as $codeValue) {
1392 if (!empty($codeValue['geo_code_2'])) {
1393 if (CRM_Utils_Rule::numeric($codeValue['geo_code_2'])) {
1394 continue;
1395 }
1396 self::addToErrorMsg(ts('Geo code 2'), $errorMessage);
1397 }
1398 }
1399 }
1400 break;
1401
1402 //check for any error in email/postal greeting, addressee,
1403 //custom email/postal greeting, custom addressee, CRM-4575
1404
1405 case 'email_greeting':
1406 $emailGreetingFilter = [
1407 'contact_type' => $this->_contactType,
1408 'greeting_type' => 'email_greeting',
1409 ];
1410 if (!self::in_value($value, CRM_Core_PseudoConstant::greeting($emailGreetingFilter))) {
1411 self::addToErrorMsg(ts('Email Greeting must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Email Greetings for valid values'), $errorMessage);
1412 }
1413 break;
1414
1415 case 'postal_greeting':
1416 $postalGreetingFilter = [
1417 'contact_type' => $this->_contactType,
1418 'greeting_type' => 'postal_greeting',
1419 ];
1420 if (!self::in_value($value, CRM_Core_PseudoConstant::greeting($postalGreetingFilter))) {
1421 self::addToErrorMsg(ts('Postal Greeting must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Postal Greetings for valid values'), $errorMessage);
1422 }
1423 break;
1424
1425 case 'addressee':
1426 $addresseeFilter = [
1427 'contact_type' => $this->_contactType,
1428 'greeting_type' => 'addressee',
1429 ];
1430 if (!self::in_value($value, CRM_Core_PseudoConstant::greeting($addresseeFilter))) {
1431 self::addToErrorMsg(ts('Addressee must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Addressee for valid values'), $errorMessage);
1432 }
1433 break;
1434
1435 case 'email_greeting_custom':
1436 if (array_key_exists('email_greeting', $params)) {
1437 $emailGreetingLabel = key(CRM_Core_OptionGroup::values('email_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1438 if (CRM_Utils_Array::value('email_greeting', $params) != $emailGreetingLabel) {
1439 self::addToErrorMsg(ts('Email Greeting - Custom'), $errorMessage);
1440 }
1441 }
1442 break;
1443
1444 case 'postal_greeting_custom':
1445 if (array_key_exists('postal_greeting', $params)) {
1446 $postalGreetingLabel = key(CRM_Core_OptionGroup::values('postal_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1447 if (CRM_Utils_Array::value('postal_greeting', $params) != $postalGreetingLabel) {
1448 self::addToErrorMsg(ts('Postal Greeting - Custom'), $errorMessage);
1449 }
1450 }
1451 break;
1452
1453 case 'addressee_custom':
1454 if (array_key_exists('addressee', $params)) {
1455 $addresseeLabel = key(CRM_Core_OptionGroup::values('addressee', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1456 if (CRM_Utils_Array::value('addressee', $params) != $addresseeLabel) {
1457 self::addToErrorMsg(ts('Addressee - Custom'), $errorMessage);
1458 }
1459 }
1460 break;
1461
1462 case 'url':
1463 if (is_array($value)) {
1464 foreach ($value as $values) {
1465 if (!empty($values['url']) && !CRM_Utils_Rule::url($values['url'])) {
1466 self::addToErrorMsg(ts('Website'), $errorMessage);
1467 break;
1468 }
1469 }
1470 }
1471 break;
1472
1473 case 'do_not_email':
1474 case 'do_not_phone':
1475 case 'do_not_mail':
1476 case 'do_not_sms':
1477 case 'do_not_trade':
1478 if (CRM_Utils_Rule::boolean($value) == FALSE) {
1479 $key = ucwords(str_replace("_", " ", $key));
1480 self::addToErrorMsg($key, $errorMessage);
1481 }
1482 break;
1483
1484 case 'email':
1485 if (is_array($value)) {
1486 foreach ($value as $values) {
1487 if (!empty($values['email']) && !CRM_Utils_Rule::email($values['email'])) {
1488 self::addToErrorMsg($key, $errorMessage);
1489 break;
1490 }
1491 }
1492 }
1493 break;
1494
1495 default:
1496 if (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
1497 //check for any relationship data ,FIX ME
1498 self::isErrorInCoreData($params[$key], $errorMessage);
1499 }
1500 }
1501 }
1502 }
1503 }
1504
1505 /**
1506 * Ckeck a value present or not in a array.
1507 *
1508 * @param $value
1509 * @param $valueArray
1510 *
1511 * @return bool
1512 */
1513 public static function in_value($value, $valueArray) {
1514 foreach ($valueArray as $key => $v) {
1515 //fix for CRM-1514
1516 if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) {
1517 return TRUE;
1518 }
1519 }
1520 return FALSE;
1521 }
1522
1523 /**
1524 * Build error-message containing error-fields
1525 *
1526 * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function.
1527 *
1528 * @todo just say no!
1529 *
1530 * @param string $errorName
1531 * A string containing error-field name.
1532 * @param string $errorMessage
1533 * A string containing all the error-fields, where the new errorName is concatenated.
1534 *
1535 */
1536 public static function addToErrorMsg($errorName, &$errorMessage) {
1537 if ($errorMessage) {
1538 $errorMessage .= "; $errorName";
1539 }
1540 else {
1541 $errorMessage = $errorName;
1542 }
1543 }
1544
1545 /**
1546 * Method for creating contact.
1547 *
1548 * @param array $formatted
1549 * @param array $contactFields
1550 * @param int $onDuplicate
1551 * @param int $contactId
1552 * @param bool $requiredCheck
1553 * @param int $dedupeRuleGroupID
1554 *
1555 * @return array|bool|\CRM_Contact_BAO_Contact|\CRM_Core_Error|null
1556 */
1557 public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) {
1558 $dupeCheck = FALSE;
1559 $newContact = NULL;
1560
1561 if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser::DUPLICATE_NOCHECK)) {
1562 $dupeCheck = (bool) ($onDuplicate);
1563 }
1564
1565 //get the prefix id etc if exists
1566 CRM_Contact_BAO_Contact::resolveDefaults($formatted, TRUE);
1567
1568 //@todo direct call to API function not supported.
1569 // setting required check to false, CRM-2839
1570 // plus we do our own required check in import
1571 try {
1572 $error = $this->deprecated_contact_check_params($formatted, $dupeCheck, $dedupeRuleGroupID);
1573 if ($error) {
1574 return $error;
1575 }
1576 $this->deprecated_validate_formatted_contact($formatted);
1577 }
1578 catch (CRM_Core_Exception $e) {
1579 return ['error_message' => $e->getMessage(), 'is_error' => 1, 'code' => $e->getCode()];
1580 }
1581
1582 if ($contactId) {
1583 $this->formatParams($formatted, $onDuplicate, (int) $contactId);
1584 }
1585
1586 // Resetting and rebuilding cache could be expensive.
1587 CRM_Core_Config::setPermitCacheFlushMode(FALSE);
1588
1589 // If a user has logged in, or accessed via a checksum
1590 // Then deliberately 'blanking' a value in the profile should remove it from their record
1591 // @todo this should either be TRUE or FALSE in the context of import - once
1592 // we figure out which we can remove all the rest.
1593 // Also note the meaning of this parameter is less than it used to
1594 // be following block cleanup.
1595 $formatted['updateBlankLocInfo'] = TRUE;
1596 if ((CRM_Core_Session::singleton()->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0) {
1597 $formatted['updateBlankLocInfo'] = FALSE;
1598 }
1599
1600 [$data, $contactDetails] = CRM_Contact_BAO_Contact::formatProfileContactParams($formatted, $contactFields, $contactId, NULL, $formatted['contact_type']);
1601
1602 // manage is_opt_out
1603 if (array_key_exists('is_opt_out', $contactFields) && array_key_exists('is_opt_out', $formatted)) {
1604 $wasOptOut = $contactDetails['is_opt_out'] ?? FALSE;
1605 $isOptOut = $formatted['is_opt_out'];
1606 $data['is_opt_out'] = $isOptOut;
1607 // on change, create new civicrm_subscription_history entry
1608 if (($wasOptOut != $isOptOut) && !empty($contactDetails['contact_id'])) {
1609 $shParams = [
1610 'contact_id' => $contactDetails['contact_id'],
1611 'status' => $isOptOut ? 'Removed' : 'Added',
1612 'method' => 'Web',
1613 ];
1614 CRM_Contact_BAO_SubscriptionHistory::create($shParams);
1615 }
1616 }
1617
1618 $contact = civicrm_api3('Contact', 'create', $data);
1619 $cid = $contact['id'];
1620
1621 CRM_Core_Config::setPermitCacheFlushMode(TRUE);
1622
1623 $contact = [
1624 'contact_id' => $cid,
1625 ];
1626
1627 $defaults = [];
1628 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
1629
1630 //get the id of the contact whose street address is not parsable, CRM-5886
1631 if ($this->_parseStreetAddress && is_object($newContact) && property_exists($newContact, 'address') && $newContact->address) {
1632 foreach ($newContact->address as $address) {
1633 if (!empty($address['street_address']) && (empty($address['street_number']) || empty($address['street_name']))) {
1634 $this->_unparsedStreetAddressContacts[] = [
1635 'id' => $newContact->id,
1636 'streetAddress' => $address['street_address'],
1637 ];
1638 }
1639 }
1640 }
1641 return $newContact;
1642 }
1643
1644 /**
1645 * Format params for update and fill mode.
1646 *
1647 * @param array $params
1648 * reference to an array containing all the.
1649 * values for import
1650 * @param int $onDuplicate
1651 * @param int $cid
1652 * contact id.
1653 */
1654 public function formatParams(&$params, $onDuplicate, $cid) {
1655 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
1656 return;
1657 }
1658
1659 $contactParams = [
1660 'contact_id' => $cid,
1661 ];
1662
1663 $defaults = [];
1664 $contactObj = CRM_Contact_BAO_Contact::retrieve($contactParams, $defaults);
1665
1666 $modeUpdate = $modeFill = FALSE;
1667
1668 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE) {
1669 $modeUpdate = TRUE;
1670 }
1671
1672 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_FILL) {
1673 $modeFill = TRUE;
1674 }
1675
1676 $groupTree = CRM_Core_BAO_CustomGroup::getTree($params['contact_type'], NULL, $cid, 0, NULL);
1677 CRM_Core_BAO_CustomGroup::setDefaults($groupTree, $defaults, FALSE, FALSE);
1678
1679 $locationFields = [
1680 'email' => 'email',
1681 'phone' => 'phone',
1682 'im' => 'name',
1683 'website' => 'website',
1684 'address' => 'address',
1685 ];
1686
1687 $contact = get_object_vars($contactObj);
1688
1689 foreach ($params as $key => $value) {
1690 if ($key == 'id' || $key == 'contact_type') {
1691 continue;
1692 }
1693
1694 if (array_key_exists($key, $locationFields)) {
1695 continue;
1696 }
1697 if (in_array($key, [
1698 'email_greeting',
1699 'postal_greeting',
1700 'addressee',
1701 ])) {
1702 // CRM-4575, need to null custom
1703 if ($params["{$key}_id"] != 4) {
1704 $params["{$key}_custom"] = 'null';
1705 }
1706 unset($params[$key]);
1707 }
1708 else {
1709 if ($customFieldId = CRM_Core_BAO_CustomField::getKeyID($key)) {
1710 $custom_params = ['id' => $contact['id'], 'return' => $key];
1711 $getValue = civicrm_api3('Contact', 'getvalue', $custom_params);
1712 if (empty($getValue)) {
1713 unset($getValue);
1714 }
1715 }
1716 else {
1717 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $key);
1718 }
1719 if ($key == 'contact_source') {
1720 $params['source'] = $params[$key];
1721 unset($params[$key]);
1722 }
1723
1724 if ($modeFill && isset($getValue)) {
1725 unset($params[$key]);
1726 if ($customFieldId) {
1727 // Extra values must be unset to ensure the values are not
1728 // imported.
1729 unset($params['custom'][$customFieldId]);
1730 }
1731 }
1732 }
1733 }
1734
1735 foreach ($locationFields as $locKeys) {
1736 if (isset($params[$locKeys]) && is_array($params[$locKeys])) {
1737 foreach ($params[$locKeys] as $key => $value) {
1738 if ($modeFill) {
1739 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $locKeys);
1740
1741 if (isset($getValue)) {
1742 foreach ($getValue as $cnt => $values) {
1743 if ($locKeys == 'website') {
1744 if (($getValue[$cnt]['website_type_id'] == $params[$locKeys][$key]['website_type_id'])) {
1745 unset($params[$locKeys][$key]);
1746 }
1747 }
1748 else {
1749 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']) {
1750 unset($params[$locKeys][$key]);
1751 }
1752 }
1753 }
1754 }
1755 }
1756 }
1757 if (count($params[$locKeys]) == 0) {
1758 unset($params[$locKeys]);
1759 }
1760 }
1761 }
1762 }
1763
1764 /**
1765 * Convert any given date string to default date array.
1766 *
1767 * @param array $params
1768 * Has given date-format.
1769 * @param array $formatted
1770 * Store formatted date in this array.
1771 * @param int $dateType
1772 * Type of date.
1773 * @param string $dateParam
1774 * Index of params.
1775 */
1776 public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) {
1777 //fix for CRM-2687
1778 CRM_Utils_Date::convertToDefaultDate($params, $dateType, $dateParam);
1779 $formatted[$dateParam] = CRM_Utils_Date::processDate($params[$dateParam]);
1780 }
1781
1782 /**
1783 * Generate status and error message for unparsed street address records.
1784 *
1785 * @param array $values
1786 * The array of values belonging to each row.
1787 * @param array $statusFieldName
1788 * Store formatted date in this array.
1789 * @param $returnCode
1790 *
1791 * @return int
1792 */
1793 public function processMessage(&$values, $statusFieldName, $returnCode) {
1794 if (empty($this->_unparsedStreetAddressContacts)) {
1795 $importRecordParams = [
1796 $statusFieldName => 'IMPORTED',
1797 ];
1798 }
1799 else {
1800 $errorMessage = ts("Record imported successfully but unable to parse the street address: ");
1801 foreach ($this->_unparsedStreetAddressContacts as $contactInfo => $contactValue) {
1802 $contactUrl = CRM_Utils_System::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contactValue['id'], TRUE, NULL, FALSE);
1803 $errorMessage .= "\n Contact ID:" . $contactValue['id'] . " <a href=\"$contactUrl\"> " . $contactValue['streetAddress'] . "</a>";
1804 }
1805 array_unshift($values, $errorMessage);
1806 $importRecordParams = [
1807 $statusFieldName => 'ERROR',
1808 "${statusFieldName}Msg" => $errorMessage,
1809 ];
1810 $returnCode = CRM_Import_Parser::UNPARSED_ADDRESS_WARNING;
1811 }
1812 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
1813 return $returnCode;
1814 }
1815
1816 /**
1817 * @param $relKey
1818 * @param array $params
1819 *
1820 * @return bool
1821 */
1822 public function checkRelatedContactFields($relKey, $params) {
1823 //avoid blank contact creation.
1824 $allowToCreate = FALSE;
1825
1826 //build the mapper field array.
1827 static $relatedContactFields = [];
1828 if (!isset($relatedContactFields[$relKey])) {
1829 foreach ($this->_mapperRelated as $key => $name) {
1830 if (!$name) {
1831 continue;
1832 }
1833
1834 if (!empty($relatedContactFields[$name]) && !is_array($relatedContactFields[$name])) {
1835 $relatedContactFields[$name] = [];
1836 }
1837 $fldName = $this->_mapperRelatedContactDetails[$key] ?? NULL;
1838 if ($fldName == 'url') {
1839 $fldName = 'website';
1840 }
1841 if ($fldName) {
1842 $relatedContactFields[$name][] = $fldName;
1843 }
1844 }
1845 }
1846
1847 //validate for passed data.
1848 if (is_array($relatedContactFields[$relKey])) {
1849 foreach ($relatedContactFields[$relKey] as $fld) {
1850 if (!empty($params[$fld])) {
1851 $allowToCreate = TRUE;
1852 break;
1853 }
1854 }
1855 }
1856
1857 return $allowToCreate;
1858 }
1859
1860 /**
1861 * get subtypes given the contact type
1862 *
1863 * @param string $contactType
1864 * @return array $subTypes
1865 */
1866 public static function getSubtypes($contactType) {
1867 $subTypes = [];
1868 $types = CRM_Contact_BAO_ContactType::subTypeInfo($contactType);
1869
1870 if (count($types) > 0) {
1871 foreach ($types as $type) {
1872 $subTypes[] = $type['name'];
1873 }
1874 }
1875 return $subTypes;
1876 }
1877
1878 /**
1879 * Get the possible contact matches.
1880 *
1881 * 1) the chosen dedupe rule falling back to
1882 * 2) a check for the external ID.
1883 *
1884 * @see https://issues.civicrm.org/jira/browse/CRM-17275
1885 *
1886 * @param array $params
1887 *
1888 * @return array
1889 * IDs of possible matches.
1890 *
1891 * @throws \CRM_Core_Exception
1892 * @throws \CiviCRM_API3_Exception
1893 */
1894 protected function getPossibleContactMatches($params) {
1895 $extIDMatch = NULL;
1896
1897 if (!empty($params['external_identifier'])) {
1898 // Check for any match on external id, deleted or otherwise.
1899 $extIDContact = civicrm_api3('Contact', 'get', [
1900 'external_identifier' => $params['external_identifier'],
1901 'showAll' => 'all',
1902 'return' => ['id', 'contact_is_deleted'],
1903 ]);
1904 if (isset($extIDContact['id'])) {
1905 $extIDMatch = $extIDContact['id'];
1906
1907 if ($extIDContact['values'][$extIDMatch]['contact_is_deleted'] == 1) {
1908 // If the contact is deleted, update external identifier to be blank
1909 // to avoid key error from MySQL.
1910 $params = ['id' => $extIDMatch, 'external_identifier' => ''];
1911 civicrm_api3('Contact', 'create', $params);
1912
1913 // And now it is no longer a match.
1914 $extIDMatch = NULL;
1915 }
1916 }
1917 }
1918 $checkParams = ['check_permissions' => FALSE, 'match' => $params];
1919 $checkParams['match']['contact_type'] = $this->_contactType;
1920
1921 $possibleMatches = civicrm_api3('Contact', 'duplicatecheck', $checkParams);
1922 if (!$extIDMatch) {
1923 return array_keys($possibleMatches['values']);
1924 }
1925 if ($possibleMatches['count']) {
1926 if (in_array($extIDMatch, array_keys($possibleMatches['values']))) {
1927 return [$extIDMatch];
1928 }
1929 throw new CRM_Core_Exception(ts(
1930 'Matching this contact based on the de-dupe rule would cause an external ID conflict'));
1931 }
1932 return [$extIDMatch];
1933 }
1934
1935 /**
1936 * Format the form mapping parameters ready for the parser.
1937 *
1938 * @param int $count
1939 * Number of rows.
1940 *
1941 * @return array $parserParameters
1942 */
1943 public static function getParameterForParser($count) {
1944 $baseArray = [];
1945 for ($i = 0; $i < $count; $i++) {
1946 $baseArray[$i] = NULL;
1947 }
1948 $parserParameters['mapperLocType'] = $baseArray;
1949 $parserParameters['mapperPhoneType'] = $baseArray;
1950 $parserParameters['mapperImProvider'] = $baseArray;
1951 $parserParameters['mapperWebsiteType'] = $baseArray;
1952 $parserParameters['mapperRelated'] = $baseArray;
1953 $parserParameters['relatedContactType'] = $baseArray;
1954 $parserParameters['relatedContactDetails'] = $baseArray;
1955 $parserParameters['relatedContactLocType'] = $baseArray;
1956 $parserParameters['relatedContactPhoneType'] = $baseArray;
1957 $parserParameters['relatedContactImProvider'] = $baseArray;
1958 $parserParameters['relatedContactWebsiteType'] = $baseArray;
1959
1960 return $parserParameters;
1961
1962 }
1963
1964 /**
1965 * Set field metadata.
1966 */
1967 protected function setFieldMetadata() {
1968 $this->setImportableFieldsMetadata($this->getContactImportMetadata());
1969 // Probably no longer needed but here for now.
1970 $this->_relationships = $this->getRelationships();
1971 }
1972
1973 /**
1974 * @param array $newContact
1975 * @param $statusFieldName
1976 * @param array $values
1977 * @param int $onDuplicate
1978 * @param array $formatted
1979 * @param array $contactFields
1980 *
1981 * @return int
1982 *
1983 * @throws \CRM_Core_Exception
1984 * @throws \CiviCRM_API3_Exception
1985 * @throws \Civi\API\Exception\UnauthorizedException
1986 */
1987 protected function handleDuplicateError(array $newContact, $statusFieldName, array $values, int $onDuplicate, array $formatted, array $contactFields): int {
1988 $urls = [];
1989 // need to fix at some stage and decide if the error will return an
1990 // array or string, crude hack for now
1991 if (is_array($newContact['error_message']['params'][0])) {
1992 $cids = $newContact['error_message']['params'][0];
1993 }
1994 else {
1995 $cids = explode(',', $newContact['error_message']['params'][0]);
1996 }
1997
1998 foreach ($cids as $cid) {
1999 $urls[] = CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE);
2000 }
2001
2002 $url_string = implode("\n", $urls);
2003
2004 // If we duplicate more than one record, skip no matter what
2005 if (count($cids) > 1) {
2006 $errorMessage = ts('Record duplicates multiple contacts');
2007 $importRecordParams = [
2008 $statusFieldName => 'ERROR',
2009 "${statusFieldName}Msg" => $errorMessage,
2010 ];
2011
2012 //combine error msg to avoid mismatch between error file columns.
2013 $errorMessage .= "\n" . $url_string;
2014 array_unshift($values, $errorMessage);
2015 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2016 return CRM_Import_Parser::ERROR;
2017 }
2018
2019 // Params only had one id, so shift it out
2020 $contactId = array_shift($cids);
2021 $cid = NULL;
2022
2023 $vals = ['contact_id' => $contactId];
2024
2025 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_REPLACE) {
2026 civicrm_api('contact', 'delete', $vals);
2027 $cid = CRM_Contact_BAO_Contact::createProfileContact($formatted, $contactFields, $contactId, NULL, NULL, $formatted['contact_type']);
2028 }
2029 if (in_array((int) $onDuplicate, [CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::DUPLICATE_FILL], TRUE)) {
2030 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactId);
2031 }
2032 // else skip does nothing and just returns an error code.
2033 if ($cid) {
2034 $contact = [
2035 'contact_id' => $cid,
2036 ];
2037 $defaults = [];
2038 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
2039 }
2040
2041 if (civicrm_error($newContact)) {
2042 if (empty($newContact['error_message']['params'])) {
2043 // different kind of error other than DUPLICATE
2044 $errorMessage = $newContact['error_message'];
2045 array_unshift($values, $errorMessage);
2046 $importRecordParams = [
2047 $statusFieldName => 'ERROR',
2048 "${statusFieldName}Msg" => $errorMessage,
2049 ];
2050 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2051 return CRM_Import_Parser::ERROR;
2052 }
2053
2054 $contactID = $newContact['error_message']['params'][0];
2055 if (is_array($contactID)) {
2056 $contactID = array_pop($contactID);
2057 }
2058 if (!in_array($contactID, $this->_newContacts)) {
2059 $this->_newContacts[] = $contactID;
2060 }
2061 }
2062 //CRM-262 No Duplicate Checking
2063 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
2064 array_unshift($values, $url_string);
2065 $importRecordParams = [
2066 $statusFieldName => 'DUPLICATE',
2067 "${statusFieldName}Msg" => "Skipping duplicate record",
2068 ];
2069 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2070 return CRM_Import_Parser::DUPLICATE;
2071 }
2072
2073 $importRecordParams = [
2074 $statusFieldName => 'IMPORTED',
2075 ];
2076 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2077 //return warning if street address is not parsed, CRM-5886
2078 return $this->processMessage($values, $statusFieldName, CRM_Import_Parser::VALID);
2079 }
2080
2081 /**
2082 * Validate a formatted contact parameter list.
2083 *
2084 * @param array $params
2085 * Structured parameter list (as in crm_format_params).
2086 *
2087 * @throw CRM_Core_Error
2088 */
2089 public function deprecated_validate_formatted_contact(&$params): void {
2090 // Look for offending email addresses
2091
2092 if (array_key_exists('email', $params)) {
2093 foreach ($params['email'] as $count => $values) {
2094 if (!is_array($values)) {
2095 continue;
2096 }
2097 if ($email = CRM_Utils_Array::value('email', $values)) {
2098 // validate each email
2099 if (!CRM_Utils_Rule::email($email)) {
2100 throw new CRM_Core_Exception('No valid email address');
2101 }
2102
2103 // check for loc type id.
2104 if (empty($values['location_type_id'])) {
2105 throw new CRM_Core_Exception('Location Type Id missing.');
2106 }
2107 }
2108 }
2109 }
2110
2111 // Validate custom data fields
2112 if (array_key_exists('custom', $params) && is_array($params['custom'])) {
2113 foreach ($params['custom'] as $key => $custom) {
2114 if (is_array($custom)) {
2115 foreach ($custom as $fieldId => $value) {
2116 $valid = CRM_Core_BAO_CustomValue::typecheck(CRM_Utils_Array::value('type', $value),
2117 CRM_Utils_Array::value('value', $value)
2118 );
2119 if (!$valid && $value['is_required']) {
2120 throw new CRM_Core_Exception('Invalid value for custom field \'' .
2121 $custom['name'] . '\''
2122 );
2123 }
2124 if (CRM_Utils_Array::value('type', $custom) == 'Date') {
2125 $params['custom'][$key][$fieldId]['value'] = str_replace('-', '', $params['custom'][$key][$fieldId]['value']);
2126 }
2127 }
2128 }
2129 }
2130 }
2131 }
2132
2133 /**
2134 * @param array $params
2135 * @param bool $dupeCheck
2136 * @param null|int $dedupeRuleGroupID
2137 *
2138 * @throws \CRM_Core_Exception
2139 */
2140 public function deprecated_contact_check_params(
2141 &$params,
2142 $dupeCheck = TRUE,
2143 $dedupeRuleGroupID = NULL) {
2144
2145 $requiredCheck = TRUE;
2146
2147 if (isset($params['id']) && is_numeric($params['id'])) {
2148 $requiredCheck = FALSE;
2149 }
2150 if ($requiredCheck) {
2151 if (isset($params['id'])) {
2152 $required = ['Individual', 'Household', 'Organization'];
2153 }
2154 $required = [
2155 'Individual' => [
2156 ['first_name', 'last_name'],
2157 'email',
2158 ],
2159 'Household' => [
2160 'household_name',
2161 ],
2162 'Organization' => [
2163 'organization_name',
2164 ],
2165 ];
2166
2167 // contact_type has a limited number of valid values
2168 if (empty($params['contact_type'])) {
2169 throw new CRM_Core_Exception("No Contact Type");
2170 }
2171 $fields = $required[$params['contact_type']] ?? NULL;
2172 if ($fields == NULL) {
2173 throw new CRM_Core_Exception("Invalid Contact Type: {$params['contact_type']}");
2174 }
2175
2176 if ($csType = CRM_Utils_Array::value('contact_sub_type', $params)) {
2177 if (!(CRM_Contact_BAO_ContactType::isExtendsContactType($csType, $params['contact_type']))) {
2178 throw new CRM_Core_Exception("Invalid or Mismatched Contact Subtype: " . implode(', ', (array) $csType));
2179 }
2180 }
2181
2182 if (empty($params['contact_id']) && !empty($params['id'])) {
2183 $valid = FALSE;
2184 $error = '';
2185 foreach ($fields as $field) {
2186 if (is_array($field)) {
2187 $valid = TRUE;
2188 foreach ($field as $element) {
2189 if (empty($params[$element])) {
2190 $valid = FALSE;
2191 $error .= $element;
2192 break;
2193 }
2194 }
2195 }
2196 else {
2197 if (!empty($params[$field])) {
2198 $valid = TRUE;
2199 }
2200 }
2201 if ($valid) {
2202 break;
2203 }
2204 }
2205
2206 if (!$valid) {
2207 throw new CRM_Core_Exception("Required fields not found for {$params['contact_type']} : $error");
2208 }
2209 }
2210 }
2211
2212 if ($dupeCheck) {
2213 // @todo switch to using api version
2214 // $dupes = civicrm_api3('Contact', 'duplicatecheck', (array('match' => $params, 'dedupe_rule_id' => $dedupeRuleGroupID)));
2215 // $ids = $dupes['count'] ? implode(',', array_keys($dupes['values'])) : NULL;
2216 $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($params, $params['contact_type'], 'Unsupervised', [], CRM_Utils_Array::value('check_permissions', $params), $dedupeRuleGroupID);
2217 if ($ids != NULL) {
2218 $error = CRM_Core_Error::createError("Found matching contacts: " . implode(',', $ids),
2219 CRM_Core_Error::DUPLICATE_CONTACT,
2220 'Fatal', $ids
2221 );
2222 return civicrm_api3_create_error($error->pop());
2223 }
2224 }
2225
2226 // check for organisations with same name
2227 if (!empty($params['current_employer'])) {
2228 $organizationParams = ['organization_name' => $params['current_employer']];
2229 $dupeIds = CRM_Contact_BAO_Contact::getDuplicateContacts($organizationParams, 'Organization', 'Supervised', [], FALSE);
2230
2231 // check for mismatch employer name and id
2232 if (!empty($params['employer_id']) && !in_array($params['employer_id'], $dupeIds)
2233 ) {
2234 throw new CRM_Core_Exception('Employer name and Employer id Mismatch');
2235 }
2236
2237 // show error if multiple organisation with same name exist
2238 if (empty($params['employer_id']) && (count($dupeIds) > 1)
2239 ) {
2240 return civicrm_api3_create_error('Found more than one Organisation with same Name.');
2241 }
2242 }
2243 }
2244
2245 }