Remove last in-between parser class
[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): int {
246 $erroneousField = NULL;
247 $this->setActiveFieldValues($values, $erroneousField);
248 $rowNumber = (int) ($values[count($values) - 1]);
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 if ($this->_emailIndex >= 0) {
284 /* If we don't have the required fields, bail */
285
286 if ($this->_contactType === 'Individual' && !$this->_updateWithId) {
287 if ($errorRequired && empty($values[$this->_emailIndex])) {
288 if ($errorMessage) {
289 $errorMessage .= ' ' . ts('OR') . ' ' . ts('Email Address');
290 }
291 else {
292 $errorMessage = ts('Missing required field:') . ' ' . ts('Email Address');
293 }
294 array_unshift($values, $errorMessage);
295 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
296
297 return CRM_Import_Parser::ERROR;
298 }
299 }
300
301 $email = $values[$this->_emailIndex] ?? NULL;
302 if ($email) {
303 /* If the email address isn't valid, bail */
304
305 if (!CRM_Utils_Rule::email($email)) {
306 $errorMessage = ts('Invalid Email address');
307 array_unshift($values, $errorMessage);
308 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
309
310 return CRM_Import_Parser::ERROR;
311 }
312
313 /* otherwise, count it and move on */
314 $this->_allEmails[$email] = $this->_lineCount;
315 }
316 }
317 elseif ($errorRequired && !$this->_updateWithId) {
318 if ($errorMessage) {
319 $errorMessage .= ' ' . ts('OR') . ' ' . ts('Email Address');
320 }
321 else {
322 $errorMessage = ts('Missing required field:') . ' ' . ts('Email Address');
323 }
324 array_unshift($values, $errorMessage);
325 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
326
327 return CRM_Import_Parser::ERROR;
328 }
329
330 //check for duplicate external Identifier
331 $externalID = $values[$this->_externalIdentifierIndex] ?? NULL;
332 if ($externalID) {
333 /* If it's a dupe,external Identifier */
334
335 if ($externalDupe = CRM_Utils_Array::value($externalID, $this->_allExternalIdentifiers)) {
336 $errorMessage = ts('External ID conflicts with record %1', [1 => $externalDupe]);
337 array_unshift($values, $errorMessage);
338 $this->setImportStatus($rowNumber, 'ERROR', $errorMessage);
339 return CRM_Import_Parser::ERROR;
340 }
341 //otherwise, count it and move on
342 $this->_allExternalIdentifiers[$externalID] = $this->_lineCount;
343 }
344
345 //Checking error in custom data
346 $params = &$this->getActiveFieldParams();
347 $params['contact_type'] = $this->_contactType;
348 //date-format part ends
349
350 $errorMessage = NULL;
351
352 //CRM-5125
353 //add custom fields for contact sub type
354 $csType = NULL;
355 if (!empty($this->_contactSubType)) {
356 $csType = $this->_contactSubType;
357 }
358
359 //checking error in custom data
360 $this->isErrorInCustomData($params, $errorMessage, $csType, $this->_relationships);
361
362 //checking error in core data
363 $this->isErrorInCoreData($params, $errorMessage);
364 if ($errorMessage) {
365 $tempMsg = "Invalid value for field(s) : $errorMessage";
366 $this->setImportStatus($rowNumber, 'ERROR', $tempMsg);
367 array_unshift($values, $tempMsg);
368 $errorMessage = NULL;
369 return CRM_Import_Parser::ERROR;
370 }
371 $this->setImportStatus($rowNumber, 'NEW', '');
372
373 return CRM_Import_Parser::VALID;
374 }
375
376 /**
377 * Get Array of all the fields that could potentially be part
378 * import process
379 *
380 * @return array
381 */
382 public function getAllFields() {
383 return $this->_fields;
384 }
385
386 /**
387 * Handle the values in import mode.
388 *
389 * @param int $onDuplicate
390 * The code for what action to take on duplicates.
391 * @param array $values
392 * The array of values belonging to this line.
393 *
394 * @param bool $doGeocodeAddress
395 *
396 * @return bool
397 * the result of this processing
398 *
399 * @throws \CiviCRM_API3_Exception
400 * @throws \CRM_Core_Exception
401 * @throws \API_Exception
402 */
403 public function import($onDuplicate, &$values, $doGeocodeAddress = FALSE) {
404 $this->_unparsedStreetAddressContacts = [];
405 if (!$doGeocodeAddress) {
406 // CRM-5854, reset the geocode method to null to prevent geocoding
407 CRM_Utils_GeocodeProvider::disableForSession();
408 }
409
410 // first make sure this is a valid line
411 //$this->_updateWithId = false;
412 $response = $this->summary($values);
413 $statusFieldName = $this->_statusFieldName;
414
415 if ($response != CRM_Import_Parser::VALID) {
416 $importRecordParams = [
417 $statusFieldName => 'INVALID',
418 "${statusFieldName}Msg" => "Invalid (Error Code: $response)",
419 ];
420 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
421 return $response;
422 }
423
424 $params = &$this->getActiveFieldParams();
425 $formatted = [
426 'contact_type' => $this->_contactType,
427 ];
428
429 $contactFields = CRM_Contact_DAO_Contact::import();
430
431 //check if external identifier exists in database
432 if (!empty($params['external_identifier']) && (!empty($params['id']) || in_array($onDuplicate, [
433 CRM_Import_Parser::DUPLICATE_SKIP,
434 CRM_Import_Parser::DUPLICATE_NOCHECK,
435 ]))) {
436
437 $extIDResult = civicrm_api3('Contact', 'get', [
438 'external_identifier' => $params['external_identifier'],
439 'showAll' => 'all',
440 'return' => ['id', 'contact_is_deleted'],
441 ]);
442 if (isset($extIDResult['id'])) {
443 // record with matching external identifier does exist.
444 $internalCid = $extIDResult['id'];
445 if ($internalCid != CRM_Utils_Array::value('id', $params)) {
446 if ($extIDResult['values'][$internalCid]['contact_is_deleted'] == 1) {
447 // And it is deleted. What to do? If we skip it, they user
448 // will be under the impression that the record exists in
449 // the database, yet they won't be able to find it. If we
450 // don't skip it, the database will try to insert a new record
451 // with an external_identifier that is non-unique. So...
452 // we will update this contact to remove the external_identifier
453 // and let a new record be created.
454 $update_params = ['id' => $internalCid, 'external_identifier' => ''];
455 civicrm_api3('Contact', 'create', $update_params);
456 }
457 else {
458 $errorMessage = ts('External ID already exists in Database.');
459 array_unshift($values, $errorMessage);
460 $importRecordParams = [
461 $statusFieldName => 'ERROR',
462 "${statusFieldName}Msg" => $errorMessage,
463 ];
464 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
465 return CRM_Import_Parser::DUPLICATE;
466 }
467 }
468 }
469 }
470
471 if (!empty($this->_contactSubType)) {
472 $params['contact_sub_type'] = $this->_contactSubType;
473 }
474
475 if ($subType = CRM_Utils_Array::value('contact_sub_type', $params)) {
476 if (CRM_Contact_BAO_ContactType::isExtendsContactType($subType, $this->_contactType, FALSE, 'label')) {
477 $subTypes = CRM_Contact_BAO_ContactType::subTypePairs($this->_contactType, FALSE, NULL);
478 $params['contact_sub_type'] = array_search($subType, $subTypes);
479 }
480 elseif (!CRM_Contact_BAO_ContactType::isExtendsContactType($subType, $this->_contactType)) {
481 $message = "Mismatched or Invalid Contact Subtype.";
482 array_unshift($values, $message);
483 return CRM_Import_Parser::NO_MATCH;
484 }
485 }
486
487 // Get contact id to format common data in update/fill mode,
488 // prioritising a dedupe rule check over an external_identifier check, but falling back on ext id.
489 if ($this->_updateWithId && empty($params['id'])) {
490 try {
491 $possibleMatches = $this->getPossibleContactMatches($params);
492 }
493 catch (CRM_Core_Exception $e) {
494 $errorMessage = $e->getMessage();
495 array_unshift($values, $errorMessage);
496
497 $importRecordParams = [
498 $statusFieldName => 'ERROR',
499 "${statusFieldName}Msg" => $errorMessage,
500 ];
501 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
502 return CRM_Import_Parser::ERROR;
503 }
504 foreach ($possibleMatches as $possibleID) {
505 $params['id'] = $formatted['id'] = $possibleID;
506 }
507 }
508 //format common data, CRM-4062
509 $this->formatCommonData($params, $formatted, $contactFields);
510
511 $relationship = FALSE;
512 $createNewContact = TRUE;
513 // Support Match and Update Via Contact ID
514 if ($this->_updateWithId && isset($params['id'])) {
515 $createNewContact = FALSE;
516 // @todo - it feels like all the rows from here to the end of the IF
517 // could be removed in favour of a simple check for whether the contact_type & id match
518 $matchedIDs = $this->getIdsOfMatchingContacts($formatted);
519 if (!empty($matchedIDs)) {
520 if (count($matchedIDs) >= 1) {
521 $updateflag = TRUE;
522 foreach ($matchedIDs as $contactId) {
523 if ($params['id'] == $contactId) {
524 $contactType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_type');
525 if ($formatted['contact_type'] == $contactType) {
526 //validation of subtype for update mode
527 //CRM-5125
528 $contactSubType = NULL;
529 if (!empty($params['contact_sub_type'])) {
530 $contactSubType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_sub_type');
531 }
532
533 if (!empty($contactSubType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($params['id'], $contactSubType) && $contactSubType != CRM_Utils_Array::value('contact_sub_type', $formatted))) {
534
535 $message = "Mismatched contact SubTypes :";
536 array_unshift($values, $message);
537 $updateflag = FALSE;
538 $this->_retCode = CRM_Import_Parser::NO_MATCH;
539 }
540 else {
541 $updateflag = FALSE;
542 $this->_retCode = CRM_Import_Parser::VALID;
543 }
544 }
545 else {
546 $message = "Mismatched contact Types :";
547 array_unshift($values, $message);
548 $updateflag = FALSE;
549 $this->_retCode = CRM_Import_Parser::NO_MATCH;
550 }
551 }
552 }
553 if ($updateflag) {
554 $message = "Mismatched contact IDs OR Mismatched contact Types :";
555 array_unshift($values, $message);
556 $this->_retCode = CRM_Import_Parser::NO_MATCH;
557 }
558 }
559 }
560 else {
561 $contactType = NULL;
562 if (!empty($params['id'])) {
563 $contactType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_type');
564 if ($contactType) {
565 if ($formatted['contact_type'] == $contactType) {
566 //validation of subtype for update mode
567 //CRM-5125
568 $contactSubType = NULL;
569 if (!empty($params['contact_sub_type'])) {
570 $contactSubType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params['id'], 'contact_sub_type');
571 }
572
573 if (!empty($contactSubType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($params['id'], $contactSubType) && $contactSubType != CRM_Utils_Array::value('contact_sub_type', $formatted))) {
574
575 $message = "Mismatched contact SubTypes :";
576 array_unshift($values, $message);
577 $this->_retCode = CRM_Import_Parser::NO_MATCH;
578 }
579 else {
580 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $params['id'], FALSE, $this->_dedupeRuleGroupID);
581 $this->_retCode = CRM_Import_Parser::VALID;
582 }
583 }
584 else {
585 $message = "Mismatched contact Types :";
586 array_unshift($values, $message);
587 $this->_retCode = CRM_Import_Parser::NO_MATCH;
588 }
589 }
590 else {
591 // we should avoid multiple errors for single record
592 // since we have already retCode and we trying to force again.
593 if ($this->_retCode != CRM_Import_Parser::NO_MATCH) {
594 $message = "No contact found for this contact ID:" . $params['id'];
595 array_unshift($values, $message);
596 $this->_retCode = CRM_Import_Parser::NO_MATCH;
597 }
598 }
599 }
600 else {
601 //CRM-4148
602 //now we want to create new contact on update/fill also.
603 $createNewContact = TRUE;
604 }
605 }
606
607 if (isset($newContact) && is_a($newContact, 'CRM_Contact_BAO_Contact')) {
608 $relationship = TRUE;
609 }
610 }
611
612 //fixed CRM-4148
613 //now we create new contact in update/fill mode also.
614 $contactID = NULL;
615 if ($createNewContact || ($this->_retCode != CRM_Import_Parser::NO_MATCH && $this->_updateWithId)) {
616 // @todo - there are multiple places where formatting is done that need consolidation.
617 // This handles where the label has been passed in and it has gotten this far.
618 // probably a bunch of hard-coded stuff could be removed to rely on this.
619 $fields = Contact::getFields(FALSE)
620 ->addWhere('options', '=', TRUE)
621 ->setLoadOptions(TRUE)
622 ->execute()->indexBy('name');
623 foreach ($fields as $fieldName => $fieldSpec) {
624 if (isset($formatted[$fieldName]) && is_array($formatted[$fieldName])) {
625 // If we have an array at this stage, it's probably a multi-select
626 // field that has already been parsed properly into the value that
627 // should be inserted into the database.
628 continue;
629 }
630 if (!empty($formatted[$fieldName])
631 && empty($fieldSpec['options'][$formatted[$fieldName]])) {
632 $formatted[$fieldName] = array_search($formatted[$fieldName], $fieldSpec['options'], TRUE) ?? $formatted[$fieldName];
633 }
634 }
635 //CRM-4430, don't carry if not submitted.
636 if ($this->_updateWithId && !empty($params['id'])) {
637 $contactID = $params['id'];
638 }
639 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactID, TRUE, $this->_dedupeRuleGroupID);
640 }
641
642 if (isset($newContact) && is_object($newContact) && ($newContact instanceof CRM_Contact_BAO_Contact)) {
643 $relationship = TRUE;
644 $newContact = clone($newContact);
645 $contactID = $newContact->id;
646 $this->_newContacts[] = $contactID;
647
648 //get return code if we create new contact in update mode, CRM-4148
649 if ($this->_updateWithId) {
650 $this->_retCode = CRM_Import_Parser::VALID;
651 }
652 }
653 elseif (isset($newContact) && CRM_Core_Error::isAPIError($newContact, CRM_Core_Error::DUPLICATE_CONTACT)) {
654 // if duplicate, no need of further processing
655 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
656 $errorMessage = "Skipping duplicate record";
657 array_unshift($values, $errorMessage);
658 $importRecordParams = [
659 $statusFieldName => 'DUPLICATE',
660 "${statusFieldName}Msg" => $errorMessage,
661 ];
662 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
663 return CRM_Import_Parser::DUPLICATE;
664 }
665
666 $relationship = TRUE;
667 // CRM-10433/CRM-20739 - IDs could be string or array; handle accordingly
668 if (!is_array($dupeContactIDs = $newContact['error_message']['params'][0])) {
669 $dupeContactIDs = explode(',', $dupeContactIDs);
670 }
671 $dupeCount = count($dupeContactIDs);
672 $contactID = array_pop($dupeContactIDs);
673 // check to see if we had more than one duplicate contact id.
674 // if we have more than one, the record will be rejected below
675 if ($dupeCount == 1) {
676 // there was only one dupe, we will continue normally...
677 if (!in_array($contactID, $this->_newContacts)) {
678 $this->_newContacts[] = $contactID;
679 }
680 }
681 }
682
683 if ($contactID) {
684 // call import hook
685 $currentImportID = end($values);
686
687 $hookParams = [
688 'contactID' => $contactID,
689 'importID' => $currentImportID,
690 'importTempTable' => $this->_tableName,
691 'fieldHeaders' => $this->_mapperKeys,
692 'fields' => $this->_activeFields,
693 ];
694
695 CRM_Utils_Hook::import('Contact', 'process', $this, $hookParams);
696 }
697
698 if ($relationship) {
699 $primaryContactId = NULL;
700 if (CRM_Core_Error::isAPIError($newContact, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
701 if ($dupeCount == 1 && CRM_Utils_Rule::integer($contactID)) {
702 $primaryContactId = $contactID;
703 }
704 }
705 else {
706 $primaryContactId = $newContact->id;
707 }
708
709 if ((CRM_Core_Error::isAPIError($newContact, CRM_Core_ERROR::DUPLICATE_CONTACT) || is_a($newContact, 'CRM_Contact_BAO_Contact')) && $primaryContactId) {
710
711 //relationship contact insert
712 foreach ($params as $key => $field) {
713 [$id, $first, $second] = CRM_Utils_System::explode('_', $key, 3);
714 if (!($first == 'a' && $second == 'b') && !($first == 'b' && $second == 'a')) {
715 continue;
716 }
717
718 $relationType = new CRM_Contact_DAO_RelationshipType();
719 $relationType->id = $id;
720 $relationType->find(TRUE);
721 $direction = "contact_sub_type_$second";
722
723 $formatting = [
724 'contact_type' => $params[$key]['contact_type'],
725 ];
726
727 //set subtype for related contact CRM-5125
728 if (isset($relationType->$direction)) {
729 //validation of related contact subtype for update mode
730 if ($relCsType = CRM_Utils_Array::value('contact_sub_type', $params[$key]) && $relCsType != $relationType->$direction) {
731 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
732 array_unshift($values, $errorMessage);
733 return CRM_Import_Parser::NO_MATCH;
734 }
735 else {
736 $formatting['contact_sub_type'] = $relationType->$direction;
737 }
738 }
739
740 $contactFields = NULL;
741 $contactFields = CRM_Contact_DAO_Contact::import();
742
743 //Relation on the basis of External Identifier.
744 if (empty($params[$key]['id']) && !empty($params[$key]['external_identifier'])) {
745 $params[$key]['id'] = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['external_identifier'], 'id', 'external_identifier');
746 }
747 // check for valid related contact id in update/fill mode, CRM-4424
748 if (in_array($onDuplicate, [
749 CRM_Import_Parser::DUPLICATE_UPDATE,
750 CRM_Import_Parser::DUPLICATE_FILL,
751 ]) && !empty($params[$key]['id'])) {
752 $relatedContactType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_type');
753 if (!$relatedContactType) {
754 $errorMessage = ts("No contact found for this related contact ID: %1", [1 => $params[$key]['id']]);
755 array_unshift($values, $errorMessage);
756 return CRM_Import_Parser::NO_MATCH;
757 }
758
759 //validation of related contact subtype for update mode
760 //CRM-5125
761 $relatedCsType = NULL;
762 if (!empty($formatting['contact_sub_type'])) {
763 $relatedCsType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $params[$key]['id'], 'contact_sub_type');
764 }
765
766 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($params[$key]['id'], $relatedCsType) &&
767 $relatedCsType != CRM_Utils_Array::value('contact_sub_type', $formatting))
768 ) {
769 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.") . ' ' . ts("ID: %1", [1 => $params[$key]['id']]);
770 array_unshift($values, $errorMessage);
771 return CRM_Import_Parser::NO_MATCH;
772 }
773 // get related contact id to format data in update/fill mode,
774 //if external identifier is present, CRM-4423
775 $formatting['id'] = $params[$key]['id'];
776 }
777
778 //format common data, CRM-4062
779 $this->formatCommonData($field, $formatting, $contactFields);
780
781 //do we have enough fields to create related contact.
782 $allowToCreate = $this->checkRelatedContactFields($key, $formatting);
783
784 if (!$allowToCreate) {
785 $errorMessage = ts('Related contact required fields are missing.');
786 array_unshift($values, $errorMessage);
787 return CRM_Import_Parser::NO_MATCH;
788 }
789
790 //fixed for CRM-4148
791 if (!empty($params[$key]['id'])) {
792 $contact = [
793 'contact_id' => $params[$key]['id'],
794 ];
795 $defaults = [];
796 $relatedNewContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
797 }
798 else {
799 $relatedNewContact = $this->createContact($formatting, $contactFields, $onDuplicate, NULL, FALSE);
800 }
801
802 if (is_object($relatedNewContact) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) {
803 $relatedNewContact = clone($relatedNewContact);
804 }
805
806 $matchedIDs = [];
807 // To update/fill contact, get the matching contact Ids if duplicate contact found
808 // otherwise get contact Id from object of related contact
809 if (is_array($relatedNewContact) && civicrm_error($relatedNewContact)) {
810 if (CRM_Core_Error::isAPIError($relatedNewContact, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
811 $matchedIDs = $relatedNewContact['error_message']['params'][0];
812 if (!is_array($matchedIDs)) {
813 $matchedIDs = explode(',', $matchedIDs);
814 }
815 }
816 else {
817 $errorMessage = $relatedNewContact['error_message'];
818 array_unshift($values, $errorMessage);
819 $importRecordParams = [
820 $statusFieldName => 'ERROR',
821 "${statusFieldName}Msg" => $errorMessage,
822 ];
823 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
824 return CRM_Import_Parser::ERROR;
825 }
826 }
827 else {
828 $matchedIDs[] = $relatedNewContact->id;
829 }
830 // update/fill related contact after getting matching Contact Ids, CRM-4424
831 if (in_array($onDuplicate, [
832 CRM_Import_Parser::DUPLICATE_UPDATE,
833 CRM_Import_Parser::DUPLICATE_FILL,
834 ])) {
835 //validation of related contact subtype for update mode
836 //CRM-5125
837 $relatedCsType = NULL;
838 if (!empty($formatting['contact_sub_type'])) {
839 $relatedCsType = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $matchedIDs[0], 'contact_sub_type');
840 }
841
842 if (!empty($relatedCsType) && (!CRM_Contact_BAO_ContactType::isAllowEdit($matchedIDs[0], $relatedCsType) && $relatedCsType != CRM_Utils_Array::value('contact_sub_type', $formatting))) {
843 $errorMessage = ts("Mismatched or Invalid contact subtype found for this related contact.");
844 array_unshift($values, $errorMessage);
845 return CRM_Import_Parser::NO_MATCH;
846 }
847 else {
848 $updatedContact = $this->createContact($formatting, $contactFields, $onDuplicate, $matchedIDs[0]);
849 }
850 }
851 static $relativeContact = [];
852 if (CRM_Core_Error::isAPIError($relatedNewContact, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
853 if (count($matchedIDs) >= 1) {
854 $relContactId = $matchedIDs[0];
855 //add relative contact to count during update & fill mode.
856 //logic to make count distinct by contact id.
857 if ($this->_newRelatedContacts || !empty($relativeContact)) {
858 $reContact = array_keys($relativeContact, $relContactId);
859
860 if (empty($reContact)) {
861 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
862 }
863 }
864 else {
865 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
866 }
867 }
868 }
869 else {
870 $relContactId = $relatedNewContact->id;
871 $this->_newRelatedContacts[] = $relativeContact[] = $relContactId;
872 }
873
874 if (CRM_Core_Error::isAPIError($relatedNewContact, CRM_Core_ERROR::DUPLICATE_CONTACT) || ($relatedNewContact instanceof CRM_Contact_BAO_Contact)) {
875 //fix for CRM-1993.Checks for duplicate related contacts
876 if (count($matchedIDs) >= 1) {
877 //if more than one duplicate contact
878 //found, create relationship with first contact
879 // now create the relationship record
880 $relationParams = [
881 'relationship_type_id' => $key,
882 'contact_check' => [
883 $relContactId => 1,
884 ],
885 'is_active' => 1,
886 'skipRecentView' => TRUE,
887 ];
888
889 // we only handle related contact success, we ignore failures for now
890 // at some point wold be nice to have related counts as separate
891 $relationIds = [
892 'contact' => $primaryContactId,
893 ];
894
895 [$valid, $duplicate] = self::legacyCreateMultiple($relationParams, $relationIds);
896
897 if ($valid || $duplicate) {
898 $relationIds['contactTarget'] = $relContactId;
899 $action = ($duplicate) ? CRM_Core_Action::UPDATE : CRM_Core_Action::ADD;
900 CRM_Contact_BAO_Relationship::relatedMemberships($primaryContactId, $relationParams, $relationIds, $action);
901 }
902
903 //handle current employer, CRM-3532
904 if ($valid) {
905 $allRelationships = CRM_Core_PseudoConstant::relationshipType('name');
906 $relationshipTypeId = str_replace([
907 '_a_b',
908 '_b_a',
909 ], [
910 '',
911 '',
912 ], $key);
913 $relationshipType = str_replace($relationshipTypeId . '_', '', $key);
914 $orgId = $individualId = NULL;
915 if ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employee of') {
916 $orgId = $relContactId;
917 $individualId = $primaryContactId;
918 }
919 elseif ($allRelationships[$relationshipTypeId]["name_{$relationshipType}"] == 'Employer of') {
920 $orgId = $primaryContactId;
921 $individualId = $relContactId;
922 }
923 if ($orgId && $individualId) {
924 $currentEmpParams[$individualId] = $orgId;
925 CRM_Contact_BAO_Contact_Utils::setCurrentEmployer($currentEmpParams);
926 }
927 }
928 }
929 }
930 }
931 }
932 }
933 if ($this->_updateWithId) {
934 //return warning if street address is unparsed, CRM-5886
935 return $this->processMessage($values, $statusFieldName, $this->_retCode);
936 }
937 //dupe checking
938 if (is_array($newContact) && civicrm_error($newContact)) {
939 $code = NULL;
940
941 if (($code = CRM_Utils_Array::value('code', $newContact['error_message'])) && ($code == CRM_Core_Error::DUPLICATE_CONTACT)) {
942 return $this->handleDuplicateError($newContact, $statusFieldName, $values, $onDuplicate, $formatted, $contactFields);
943 }
944 // Not a dupe, so we had an error
945 $errorMessage = $newContact['error_message'];
946 array_unshift($values, $errorMessage);
947 $importRecordParams = [
948 $statusFieldName => 'ERROR',
949 "${statusFieldName}Msg" => $errorMessage,
950 ];
951 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
952 return CRM_Import_Parser::ERROR;
953
954 }
955 // sleep(3);
956 return $this->processMessage($values, $statusFieldName, CRM_Import_Parser::VALID);
957 }
958
959 /**
960 * Only called from import now... plus one place outside of core & tests.
961 *
962 * @todo - deprecate more aggressively - will involve copying to the import
963 * class, adding a deprecation notice here & removing from tests.
964 *
965 * Takes an associative array and creates a relationship object.
966 *
967 * @deprecated For single creates use the api instead (it's tested).
968 * For multiple a new variant of this function needs to be written and migrated to as this is a bit
969 * nasty
970 *
971 * @param array $params
972 * (reference ) an assoc array of name/value pairs.
973 * @param array $ids
974 * The array that holds all the db ids.
975 * per http://wiki.civicrm.org/confluence/display/CRM/Database+layer
976 * "we are moving away from the $ids param "
977 *
978 * @return array
979 * @throws \CRM_Core_Exception
980 */
981 private static function legacyCreateMultiple($params, $ids = []) {
982 // clarify that the only key ever pass in the ids array is 'contact'
983 // There is legacy handling for other keys but a universe search on
984 // calls to this function (not supported to be called from outside core)
985 // only returns 2 calls - one in CRM_Contact_Import_Parser_Contact
986 // and the other in jma grant applications (CRM_Grant_Form_Grant_Confirm)
987 // both only pass in contact as a key here.
988 $contactID = $ids['contact'];
989 unset($ids);
990 // There is only ever one value passed in from the 2 places above that call
991 // this - by clarifying here like this we can cleanup within this
992 // function without having to do more universe searches.
993 $relatedContactID = key($params['contact_check']);
994
995 // check if the relationship is valid between contacts.
996 // step 1: check if the relationship is valid if not valid skip and keep the count
997 // step 2: check the if two contacts already have a relationship if yes skip and keep the count
998 // step 3: if valid relationship then add the relation and keep the count
999
1000 // step 1
1001 [$contactFields['relationship_type_id'], $firstLetter, $secondLetter] = explode('_', $params['relationship_type_id']);
1002 $contactFields['contact_id_' . $firstLetter] = $contactID;
1003 $contactFields['contact_id_' . $secondLetter] = $relatedContactID;
1004 if (!CRM_Contact_BAO_Relationship::checkRelationshipType($contactFields['contact_id_a'], $contactFields['contact_id_b'],
1005 $contactFields['relationship_type_id'])) {
1006 return [0, 0];
1007 }
1008
1009 if (
1010 CRM_Contact_BAO_Relationship::checkDuplicateRelationship(
1011 $contactFields,
1012 $contactID,
1013 // step 2
1014 $relatedContactID
1015 )
1016 ) {
1017 return [0, 1];
1018 }
1019
1020 $singleInstanceParams = array_merge($params, $contactFields);
1021 CRM_Contact_BAO_Relationship::add($singleInstanceParams);
1022 return [1, 0];
1023 }
1024
1025 /**
1026 * Format common params data to proper format to store.
1027 *
1028 * @param array $params
1029 * Contain record values.
1030 * @param array $formatted
1031 * Array of formatted data.
1032 * @param array $contactFields
1033 * Contact DAO fields.
1034 */
1035 private function formatCommonData($params, &$formatted, $contactFields) {
1036 $csType = [
1037 CRM_Utils_Array::value('contact_type', $formatted),
1038 ];
1039
1040 //CRM-5125
1041 //add custom fields for contact sub type
1042 if (!empty($this->_contactSubType)) {
1043 $csType = $this->_contactSubType;
1044 }
1045
1046 if ($relCsType = CRM_Utils_Array::value('contact_sub_type', $formatted)) {
1047 $csType = $relCsType;
1048 }
1049
1050 $customFields = CRM_Core_BAO_CustomField::getFields($formatted['contact_type'], FALSE, FALSE, $csType);
1051
1052 $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address');
1053 $customFields = $customFields + $addressCustomFields;
1054
1055 //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
1056 $elements = [
1057 'email_greeting_custom' => 'email_greeting',
1058 'postal_greeting_custom' => 'postal_greeting',
1059 'addressee_custom' => 'addressee',
1060 ];
1061 foreach ($elements as $k => $v) {
1062 if (array_key_exists($k, $params) && !(array_key_exists($v, $params))) {
1063 $label = key(CRM_Core_OptionGroup::values($v, TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1064 $params[$v] = $label;
1065 }
1066 }
1067
1068 //format date first
1069 $session = CRM_Core_Session::singleton();
1070 $dateType = $session->get("dateTypes");
1071 foreach ($params as $key => $val) {
1072 $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key);
1073 if ($customFieldID &&
1074 !array_key_exists($customFieldID, $addressCustomFields)
1075 ) {
1076 //we should not update Date to null, CRM-4062
1077 if ($val && ($customFields[$customFieldID]['data_type'] == 'Date')) {
1078 //CRM-21267
1079 CRM_Contact_Import_Parser_Contact::formatCustomDate($params, $formatted, $dateType, $key);
1080 }
1081 elseif ($customFields[$customFieldID]['data_type'] == 'Boolean') {
1082 if (empty($val) && !is_numeric($val) && $this->_onDuplicate == CRM_Import_Parser::DUPLICATE_FILL) {
1083 //retain earlier value when Import mode is `Fill`
1084 unset($params[$key]);
1085 }
1086 else {
1087 $params[$key] = CRM_Utils_String::strtoboolstr($val);
1088 }
1089 }
1090 }
1091
1092 if ($key == 'birth_date' && $val) {
1093 CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key);
1094 }
1095 elseif ($key == 'deceased_date' && $val) {
1096 CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key);
1097 $params['is_deceased'] = 1;
1098 }
1099 elseif ($key == 'is_deceased' && $val) {
1100 $params[$key] = CRM_Utils_String::strtoboolstr($val);
1101 }
1102 }
1103
1104 //now format custom data.
1105 foreach ($params as $key => $field) {
1106 if (is_array($field)) {
1107 $isAddressCustomField = FALSE;
1108 foreach ($field as $value) {
1109 $break = FALSE;
1110 if (is_array($value)) {
1111 foreach ($value as $name => $testForEmpty) {
1112 if ($addressCustomFieldID = CRM_Core_BAO_CustomField::getKeyID($name)) {
1113 $isAddressCustomField = TRUE;
1114 break;
1115 }
1116 // check if $value does not contain IM provider or phoneType
1117 if (($name !== 'phone_type_id' || $name !== 'provider_id') && ($testForEmpty === '' || $testForEmpty == NULL)) {
1118 $break = TRUE;
1119 break;
1120 }
1121 }
1122 }
1123 else {
1124 $break = TRUE;
1125 }
1126
1127 if (!$break) {
1128 if (!empty($value['location_type_id'])) {
1129 $this->formatLocationBlock($value, $formatted);
1130 }
1131 else {
1132 // @todo - this is still reachable - e.g. import with related contact info like firstname,lastname,spouse-first-name,spouse-last-name,spouse-home-phone
1133 CRM_Core_Error::deprecatedFunctionWarning('this is not expected to be reachable now');
1134 $this->formatContactParameters($value, $formatted);
1135 }
1136 }
1137 }
1138 if (!$isAddressCustomField) {
1139 continue;
1140 }
1141 }
1142
1143 $formatValues = [
1144 $key => $field,
1145 ];
1146
1147 if (($key !== 'preferred_communication_method') && (array_key_exists($key, $contactFields))) {
1148 // due to merging of individual table and
1149 // contact table, we need to avoid
1150 // preferred_communication_method forcefully
1151 $formatValues['contact_type'] = $formatted['contact_type'];
1152 }
1153
1154 if ($key == 'id' && isset($field)) {
1155 $formatted[$key] = $field;
1156 }
1157 $this->formatContactParameters($formatValues, $formatted);
1158
1159 //Handling Custom Data
1160 // note: Address custom fields will be handled separately inside formatContactParameters
1161 if (($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) &&
1162 array_key_exists($customFieldID, $customFields) &&
1163 !array_key_exists($customFieldID, $addressCustomFields)
1164 ) {
1165
1166 $extends = $customFields[$customFieldID]['extends'] ?? NULL;
1167 $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL;
1168 $dataType = $customFields[$customFieldID]['data_type'] ?? NULL;
1169 $serialized = CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]);
1170
1171 if (!$serialized && in_array($htmlType, ['Select', 'Radio', 'Autocomplete-Select']) && in_array($dataType, ['String', 'Int'])) {
1172 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1173 foreach ($customOption as $customValue) {
1174 $val = $customValue['value'] ?? NULL;
1175 $label = strtolower($customValue['label'] ?? '');
1176 $value = strtolower(trim($formatted[$key]));
1177 if (($value == $label) || ($value == strtolower($val))) {
1178 $params[$key] = $formatted[$key] = $val;
1179 }
1180 }
1181 }
1182 elseif ($serialized && !empty($formatted[$key]) && !empty($params[$key])) {
1183 $mulValues = explode(',', $formatted[$key]);
1184 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1185 $formatted[$key] = [];
1186 $params[$key] = [];
1187 foreach ($mulValues as $v1) {
1188 foreach ($customOption as $v2) {
1189 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
1190 (strtolower($v2['value']) == strtolower(trim($v1)))
1191 ) {
1192 if ($htmlType == 'CheckBox') {
1193 $params[$key][$v2['value']] = $formatted[$key][$v2['value']] = 1;
1194 }
1195 else {
1196 $params[$key][] = $formatted[$key][] = $v2['value'];
1197 }
1198 }
1199 }
1200 }
1201 }
1202 }
1203 }
1204
1205 if (!empty($key) && ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) && array_key_exists($customFieldID, $customFields) &&
1206 !array_key_exists($customFieldID, $addressCustomFields)
1207 ) {
1208 // @todo calling api functions directly is not supported
1209 _civicrm_api3_custom_format_params($params, $formatted, $extends);
1210 }
1211
1212 // to check if not update mode and unset the fields with empty value.
1213 if (!$this->_updateWithId && array_key_exists('custom', $formatted)) {
1214 foreach ($formatted['custom'] as $customKey => $customvalue) {
1215 if (empty($formatted['custom'][$customKey][-1]['is_required'])) {
1216 $formatted['custom'][$customKey][-1]['is_required'] = $customFields[$customKey]['is_required'];
1217 }
1218 $emptyValue = $customvalue[-1]['value'] ?? NULL;
1219 if (!isset($emptyValue)) {
1220 unset($formatted['custom'][$customKey]);
1221 }
1222 }
1223 }
1224
1225 // parse street address, CRM-5450
1226 if ($this->_parseStreetAddress) {
1227 if (array_key_exists('address', $formatted) && is_array($formatted['address'])) {
1228 foreach ($formatted['address'] as $instance => & $address) {
1229 $streetAddress = $address['street_address'] ?? NULL;
1230 if (empty($streetAddress)) {
1231 continue;
1232 }
1233 // parse address field.
1234 $parsedFields = CRM_Core_BAO_Address::parseStreetAddress($streetAddress);
1235
1236 //street address consider to be parsed properly,
1237 //If we get street_name and street_number.
1238 if (empty($parsedFields['street_name']) || empty($parsedFields['street_number'])) {
1239 $parsedFields = array_fill_keys(array_keys($parsedFields), '');
1240 }
1241
1242 // merge parse address w/ main address block.
1243 $address = array_merge($address, $parsedFields);
1244 }
1245 }
1246 }
1247 }
1248
1249 /**
1250 * Get the array of successfully imported contact id's
1251 *
1252 * @return array
1253 */
1254 public function getImportedContacts() {
1255 return $this->_newContacts;
1256 }
1257
1258 /**
1259 * Get the array of successfully imported related contact id's
1260 *
1261 * @return array
1262 */
1263 public function &getRelatedImportedContacts() {
1264 return $this->_newRelatedContacts;
1265 }
1266
1267 /**
1268 * The initializer code, called before the processing.
1269 */
1270 public function fini() {
1271 }
1272
1273 /**
1274 * Check if an error in custom data.
1275 *
1276 * @param array $params
1277 * @param string $errorMessage
1278 * A string containing all the error-fields.
1279 *
1280 * @param null $csType
1281 * @param null $relationships
1282 */
1283 public static function isErrorInCustomData($params, &$errorMessage, $csType = NULL, $relationships = NULL) {
1284 $dateType = CRM_Core_Session::singleton()->get("dateTypes");
1285
1286 if (!empty($params['contact_sub_type'])) {
1287 $csType = $params['contact_sub_type'] ?? NULL;
1288 }
1289
1290 if (empty($params['contact_type'])) {
1291 $params['contact_type'] = 'Individual';
1292 }
1293
1294 // get array of subtypes - CRM-18708
1295 if (in_array($csType, CRM_Contact_BAO_ContactType::basicTypes(TRUE), TRUE)) {
1296 $csType = self::getSubtypes($params['contact_type']);
1297 }
1298
1299 if (is_array($csType)) {
1300 // fetch custom fields for every subtype and add it to $customFields array
1301 // CRM-18708
1302 $customFields = [];
1303 foreach ($csType as $cType) {
1304 $customFields += CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $cType);
1305 }
1306 }
1307 else {
1308 $customFields = CRM_Core_BAO_CustomField::getFields($params['contact_type'], FALSE, FALSE, $csType);
1309 }
1310
1311 $addressCustomFields = CRM_Core_BAO_CustomField::getFields('Address');
1312 $customFields = $customFields + $addressCustomFields;
1313 foreach ($params as $key => $value) {
1314 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
1315 /* check if it's a valid custom field id */
1316
1317 if (!array_key_exists($customFieldID, $customFields)) {
1318 self::addToErrorMsg(ts('field ID'), $errorMessage);
1319 }
1320 // validate null values for required custom fields of type boolean
1321 if (!empty($customFields[$customFieldID]['is_required']) && (empty($params['custom_' . $customFieldID]) && !is_numeric($params['custom_' . $customFieldID])) && $customFields[$customFieldID]['data_type'] == 'Boolean') {
1322 self::addToErrorMsg($customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'], $errorMessage);
1323 }
1324
1325 //For address custom fields, we do get actual custom field value as an inner array of
1326 //values so need to modify
1327 if (array_key_exists($customFieldID, $addressCustomFields)) {
1328 $value = $value[0][$key];
1329 }
1330 /* validate the data against the CF type */
1331
1332 if ($value) {
1333 $dataType = $customFields[$customFieldID]['data_type'];
1334 $htmlType = $customFields[$customFieldID]['html_type'];
1335 $isSerialized = CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]);
1336 if ($dataType == 'Date') {
1337 if (array_key_exists($customFieldID, $addressCustomFields) && CRM_Utils_Date::convertToDefaultDate($params[$key][0], $dateType, $key)) {
1338 $value = $params[$key][0][$key];
1339 }
1340 elseif (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
1341 $value = $params[$key];
1342 }
1343 else {
1344 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1345 }
1346 }
1347 elseif ($dataType == 'Boolean') {
1348 if (CRM_Utils_String::strtoboolstr($value) === FALSE) {
1349 self::addToErrorMsg($customFields[$customFieldID]['label'] . '::' . $customFields[$customFieldID]['groupTitle'], $errorMessage);
1350 }
1351 }
1352 // need not check for label filed import
1353 $selectHtmlTypes = [
1354 'CheckBox',
1355 'Select',
1356 'Radio',
1357 ];
1358 if ((!$isSerialized && !in_array($htmlType, $selectHtmlTypes)) || $dataType == 'Boolean' || $dataType == 'ContactReference') {
1359 $valid = CRM_Core_BAO_CustomValue::typecheck($dataType, $value);
1360 if (!$valid) {
1361 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1362 }
1363 }
1364
1365 // check for values for custom fields for checkboxes and multiselect
1366 if ($isSerialized && $dataType != 'ContactReference') {
1367 $value = trim($value);
1368 $value = str_replace('|', ',', $value);
1369 $mulValues = explode(',', $value);
1370 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1371 foreach ($mulValues as $v1) {
1372 if (strlen($v1) == 0) {
1373 continue;
1374 }
1375
1376 $flag = FALSE;
1377 foreach ($customOption as $v2) {
1378 if ((strtolower(trim($v2['label'])) == strtolower(trim($v1))) || (strtolower(trim($v2['value'])) == strtolower(trim($v1)))) {
1379 $flag = TRUE;
1380 }
1381 }
1382
1383 if (!$flag) {
1384 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1385 }
1386 }
1387 }
1388 elseif ($htmlType == 'Select' || ($htmlType == 'Radio' && $dataType != 'Boolean')) {
1389 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1390 $flag = FALSE;
1391 foreach ($customOption as $v2) {
1392 if ((strtolower(trim($v2['label'])) == strtolower(trim($value))) || (strtolower(trim($v2['value'])) == strtolower(trim($value)))) {
1393 $flag = TRUE;
1394 }
1395 }
1396 if (!$flag) {
1397 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1398 }
1399 }
1400 elseif ($isSerialized && $dataType === 'StateProvince') {
1401 $mulValues = explode(',', $value);
1402 foreach ($mulValues as $stateValue) {
1403 if ($stateValue) {
1404 if (self::in_value(trim($stateValue), CRM_Core_PseudoConstant::stateProvinceAbbreviation()) || self::in_value(trim($stateValue), CRM_Core_PseudoConstant::stateProvince())) {
1405 continue;
1406 }
1407 else {
1408 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1409 }
1410 }
1411 }
1412 }
1413 elseif ($isSerialized && $dataType == 'Country') {
1414 $mulValues = explode(',', $value);
1415 foreach ($mulValues as $countryValue) {
1416 if ($countryValue) {
1417 CRM_Core_PseudoConstant::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
1418 CRM_Core_PseudoConstant::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
1419 $limitCodes = CRM_Core_BAO_Country::countryLimit();
1420
1421 $error = TRUE;
1422 foreach ([
1423 $countryNames,
1424 $countryIsoCodes,
1425 $limitCodes,
1426 ] as $values) {
1427 if (in_array(trim($countryValue), $values)) {
1428 $error = FALSE;
1429 break;
1430 }
1431 }
1432
1433 if ($error) {
1434 self::addToErrorMsg($customFields[$customFieldID]['label'], $errorMessage);
1435 }
1436 }
1437 }
1438 }
1439 }
1440 }
1441 elseif (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
1442 //CRM-5125
1443 //supporting custom data of related contact subtypes
1444 $relation = NULL;
1445 if ($relationships) {
1446 if (array_key_exists($key, $relationships)) {
1447 $relation = $key;
1448 }
1449 elseif (CRM_Utils_Array::key($key, $relationships)) {
1450 $relation = CRM_Utils_Array::key($key, $relationships);
1451 }
1452 }
1453 if (!empty($relation)) {
1454 [$id, $first, $second] = CRM_Utils_System::explode('_', $relation, 3);
1455 $direction = "contact_sub_type_$second";
1456 $relationshipType = new CRM_Contact_BAO_RelationshipType();
1457 $relationshipType->id = $id;
1458 if ($relationshipType->find(TRUE)) {
1459 if (isset($relationshipType->$direction)) {
1460 $params[$key]['contact_sub_type'] = $relationshipType->$direction;
1461 }
1462 }
1463 }
1464
1465 self::isErrorInCustomData($params[$key], $errorMessage, $csType, $relationships);
1466 }
1467 }
1468 }
1469
1470 /**
1471 * Check if value present in all genders or.
1472 * as a substring of any gender value, if yes than return corresponding gender.
1473 * eg value might be m/M, ma/MA, mal/MAL, male return 'Male'
1474 * but if value is 'maleabc' than return false
1475 *
1476 * @param string $gender
1477 * Check this value across gender values.
1478 *
1479 * retunr gender value / false
1480 *
1481 * @return bool
1482 */
1483 public function checkGender($gender) {
1484 $gender = trim($gender, '.');
1485 if (!$gender) {
1486 return FALSE;
1487 }
1488
1489 $allGenders = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'gender_id');
1490 foreach ($allGenders as $key => $value) {
1491 if (strlen($gender) > strlen($value)) {
1492 continue;
1493 }
1494 if ($gender == $value) {
1495 return $value;
1496 }
1497 if (substr_compare($value, $gender, 0, strlen($gender), TRUE) === 0) {
1498 return $value;
1499 }
1500 }
1501
1502 return FALSE;
1503 }
1504
1505 /**
1506 * Check if an error in Core( non-custom fields ) field
1507 *
1508 * @param array $params
1509 * @param string $errorMessage
1510 * A string containing all the error-fields.
1511 */
1512 public function isErrorInCoreData($params, &$errorMessage) {
1513 foreach ($params as $key => $value) {
1514 if ($value) {
1515 $session = CRM_Core_Session::singleton();
1516 $dateType = $session->get("dateTypes");
1517
1518 switch ($key) {
1519 case 'birth_date':
1520 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
1521 if (!CRM_Utils_Rule::date($params[$key])) {
1522 self::addToErrorMsg(ts('Birth Date'), $errorMessage);
1523 }
1524 }
1525 else {
1526 self::addToErrorMsg(ts('Birth-Date'), $errorMessage);
1527 }
1528 break;
1529
1530 case 'deceased_date':
1531 if (CRM_Utils_Date::convertToDefaultDate($params, $dateType, $key)) {
1532 if (!CRM_Utils_Rule::date($params[$key])) {
1533 self::addToErrorMsg(ts('Deceased Date'), $errorMessage);
1534 }
1535 }
1536 else {
1537 self::addToErrorMsg(ts('Deceased Date'), $errorMessage);
1538 }
1539 break;
1540
1541 case 'is_deceased':
1542 if (CRM_Utils_String::strtoboolstr($value) === FALSE) {
1543 self::addToErrorMsg(ts('Deceased'), $errorMessage);
1544 }
1545 break;
1546
1547 case 'gender_id':
1548 if (!self::checkGender($value)) {
1549 self::addToErrorMsg(ts('Gender'), $errorMessage);
1550 }
1551 break;
1552
1553 case 'preferred_communication_method':
1554 $preffComm = [];
1555 $preffComm = explode(',', $value);
1556 foreach ($preffComm as $v) {
1557 if (!self::in_value(trim($v), CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'preferred_communication_method'))) {
1558 self::addToErrorMsg(ts('Preferred Communication Method'), $errorMessage);
1559 }
1560 }
1561 break;
1562
1563 case 'preferred_mail_format':
1564 if (!array_key_exists(strtolower($value), array_change_key_case(CRM_Core_SelectValues::pmf(), CASE_LOWER))) {
1565 self::addToErrorMsg(ts('Preferred Mail Format'), $errorMessage);
1566 }
1567 break;
1568
1569 case 'individual_prefix':
1570 case 'prefix_id':
1571 if (!self::in_value($value, CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'prefix_id'))) {
1572 self::addToErrorMsg(ts('Individual Prefix'), $errorMessage);
1573 }
1574 break;
1575
1576 case 'individual_suffix':
1577 case 'suffix_id':
1578 if (!self::in_value($value, CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'suffix_id'))) {
1579 self::addToErrorMsg(ts('Individual Suffix'), $errorMessage);
1580 }
1581 break;
1582
1583 case 'state_province':
1584 if (!empty($value)) {
1585 foreach ($value as $stateValue) {
1586 if ($stateValue['state_province']) {
1587 if (self::in_value($stateValue['state_province'], CRM_Core_PseudoConstant::stateProvinceAbbreviation()) ||
1588 self::in_value($stateValue['state_province'], CRM_Core_PseudoConstant::stateProvince())
1589 ) {
1590 continue;
1591 }
1592 else {
1593 self::addToErrorMsg(ts('State/Province'), $errorMessage);
1594 }
1595 }
1596 }
1597 }
1598 break;
1599
1600 case 'country':
1601 if (!empty($value)) {
1602 foreach ($value as $stateValue) {
1603 if ($stateValue['country']) {
1604 CRM_Core_PseudoConstant::populate($countryNames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active');
1605 CRM_Core_PseudoConstant::populate($countryIsoCodes, 'CRM_Core_DAO_Country', TRUE, 'iso_code');
1606 $limitCodes = CRM_Core_BAO_Country::countryLimit();
1607 //If no country is selected in
1608 //localization then take all countries
1609 if (empty($limitCodes)) {
1610 $limitCodes = $countryIsoCodes;
1611 }
1612
1613 if (self::in_value($stateValue['country'], $limitCodes) || self::in_value($stateValue['country'], CRM_Core_PseudoConstant::country())) {
1614 continue;
1615 }
1616 if (self::in_value($stateValue['country'], $countryIsoCodes) || self::in_value($stateValue['country'], $countryNames)) {
1617 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);
1618 }
1619 else {
1620 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);
1621 }
1622 }
1623 }
1624 }
1625 break;
1626
1627 case 'county':
1628 if (!empty($value)) {
1629 foreach ($value as $county) {
1630 if ($county['county']) {
1631 $countyNames = CRM_Core_PseudoConstant::county();
1632 if (!empty($county['county']) && !in_array($county['county'], $countyNames)) {
1633 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);
1634 }
1635 }
1636 }
1637 }
1638 break;
1639
1640 case 'geo_code_1':
1641 if (!empty($value)) {
1642 foreach ($value as $codeValue) {
1643 if (!empty($codeValue['geo_code_1'])) {
1644 if (CRM_Utils_Rule::numeric($codeValue['geo_code_1'])) {
1645 continue;
1646 }
1647 self::addToErrorMsg(ts('Geo code 1'), $errorMessage);
1648 }
1649 }
1650 }
1651 break;
1652
1653 case 'geo_code_2':
1654 if (!empty($value)) {
1655 foreach ($value as $codeValue) {
1656 if (!empty($codeValue['geo_code_2'])) {
1657 if (CRM_Utils_Rule::numeric($codeValue['geo_code_2'])) {
1658 continue;
1659 }
1660 self::addToErrorMsg(ts('Geo code 2'), $errorMessage);
1661 }
1662 }
1663 }
1664 break;
1665
1666 //check for any error in email/postal greeting, addressee,
1667 //custom email/postal greeting, custom addressee, CRM-4575
1668
1669 case 'email_greeting':
1670 $emailGreetingFilter = [
1671 'contact_type' => $this->_contactType,
1672 'greeting_type' => 'email_greeting',
1673 ];
1674 if (!self::in_value($value, CRM_Core_PseudoConstant::greeting($emailGreetingFilter))) {
1675 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);
1676 }
1677 break;
1678
1679 case 'postal_greeting':
1680 $postalGreetingFilter = [
1681 'contact_type' => $this->_contactType,
1682 'greeting_type' => 'postal_greeting',
1683 ];
1684 if (!self::in_value($value, CRM_Core_PseudoConstant::greeting($postalGreetingFilter))) {
1685 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);
1686 }
1687 break;
1688
1689 case 'addressee':
1690 $addresseeFilter = [
1691 'contact_type' => $this->_contactType,
1692 'greeting_type' => 'addressee',
1693 ];
1694 if (!self::in_value($value, CRM_Core_PseudoConstant::greeting($addresseeFilter))) {
1695 self::addToErrorMsg(ts('Addressee must be one of the configured format options. Check Administer >> System Settings >> Option Groups >> Addressee for valid values'), $errorMessage);
1696 }
1697 break;
1698
1699 case 'email_greeting_custom':
1700 if (array_key_exists('email_greeting', $params)) {
1701 $emailGreetingLabel = key(CRM_Core_OptionGroup::values('email_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1702 if (CRM_Utils_Array::value('email_greeting', $params) != $emailGreetingLabel) {
1703 self::addToErrorMsg(ts('Email Greeting - Custom'), $errorMessage);
1704 }
1705 }
1706 break;
1707
1708 case 'postal_greeting_custom':
1709 if (array_key_exists('postal_greeting', $params)) {
1710 $postalGreetingLabel = key(CRM_Core_OptionGroup::values('postal_greeting', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1711 if (CRM_Utils_Array::value('postal_greeting', $params) != $postalGreetingLabel) {
1712 self::addToErrorMsg(ts('Postal Greeting - Custom'), $errorMessage);
1713 }
1714 }
1715 break;
1716
1717 case 'addressee_custom':
1718 if (array_key_exists('addressee', $params)) {
1719 $addresseeLabel = key(CRM_Core_OptionGroup::values('addressee', TRUE, NULL, NULL, 'AND v.name = "Customized"'));
1720 if (CRM_Utils_Array::value('addressee', $params) != $addresseeLabel) {
1721 self::addToErrorMsg(ts('Addressee - Custom'), $errorMessage);
1722 }
1723 }
1724 break;
1725
1726 case 'url':
1727 if (is_array($value)) {
1728 foreach ($value as $values) {
1729 if (!empty($values['url']) && !CRM_Utils_Rule::url($values['url'])) {
1730 self::addToErrorMsg(ts('Website'), $errorMessage);
1731 break;
1732 }
1733 }
1734 }
1735 break;
1736
1737 case 'do_not_email':
1738 case 'do_not_phone':
1739 case 'do_not_mail':
1740 case 'do_not_sms':
1741 case 'do_not_trade':
1742 if (CRM_Utils_Rule::boolean($value) == FALSE) {
1743 $key = ucwords(str_replace("_", " ", $key));
1744 self::addToErrorMsg($key, $errorMessage);
1745 }
1746 break;
1747
1748 case 'email':
1749 if (is_array($value)) {
1750 foreach ($value as $values) {
1751 if (!empty($values['email']) && !CRM_Utils_Rule::email($values['email'])) {
1752 self::addToErrorMsg($key, $errorMessage);
1753 break;
1754 }
1755 }
1756 }
1757 break;
1758
1759 default:
1760 if (is_array($params[$key]) && isset($params[$key]["contact_type"])) {
1761 //check for any relationship data ,FIX ME
1762 self::isErrorInCoreData($params[$key], $errorMessage);
1763 }
1764 }
1765 }
1766 }
1767 }
1768
1769 /**
1770 * Ckeck a value present or not in a array.
1771 *
1772 * @param $value
1773 * @param $valueArray
1774 *
1775 * @return bool
1776 */
1777 public static function in_value($value, $valueArray) {
1778 foreach ($valueArray as $key => $v) {
1779 //fix for CRM-1514
1780 if (strtolower(trim($v, ".")) == strtolower(trim($value, "."))) {
1781 return TRUE;
1782 }
1783 }
1784 return FALSE;
1785 }
1786
1787 /**
1788 * Build error-message containing error-fields
1789 *
1790 * Once upon a time there was a dev who hadn't heard of implode. That dev wrote this function.
1791 *
1792 * @todo just say no!
1793 *
1794 * @param string $errorName
1795 * A string containing error-field name.
1796 * @param string $errorMessage
1797 * A string containing all the error-fields, where the new errorName is concatenated.
1798 *
1799 */
1800 public static function addToErrorMsg($errorName, &$errorMessage) {
1801 if ($errorMessage) {
1802 $errorMessage .= "; $errorName";
1803 }
1804 else {
1805 $errorMessage = $errorName;
1806 }
1807 }
1808
1809 /**
1810 * Method for creating contact.
1811 *
1812 * @param array $formatted
1813 * @param array $contactFields
1814 * @param int $onDuplicate
1815 * @param int $contactId
1816 * @param bool $requiredCheck
1817 * @param int $dedupeRuleGroupID
1818 *
1819 * @return array|bool|\CRM_Contact_BAO_Contact|\CRM_Core_Error|null
1820 */
1821 public function createContact(&$formatted, &$contactFields, $onDuplicate, $contactId = NULL, $requiredCheck = TRUE, $dedupeRuleGroupID = NULL) {
1822 $dupeCheck = FALSE;
1823 $newContact = NULL;
1824
1825 if (is_null($contactId) && ($onDuplicate != CRM_Import_Parser::DUPLICATE_NOCHECK)) {
1826 $dupeCheck = (bool) ($onDuplicate);
1827 }
1828
1829 //get the prefix id etc if exists
1830 CRM_Contact_BAO_Contact::resolveDefaults($formatted, TRUE);
1831
1832 //@todo direct call to API function not supported.
1833 // setting required check to false, CRM-2839
1834 // plus we do our own required check in import
1835 try {
1836 $error = $this->deprecated_contact_check_params($formatted, $dupeCheck, $dedupeRuleGroupID);
1837 if ($error) {
1838 return $error;
1839 }
1840 $this->deprecated_validate_formatted_contact($formatted);
1841 }
1842 catch (CRM_Core_Exception $e) {
1843 return ['error_message' => $e->getMessage(), 'is_error' => 1, 'code' => $e->getCode()];
1844 }
1845
1846 if ($contactId) {
1847 $this->formatParams($formatted, $onDuplicate, (int) $contactId);
1848 }
1849
1850 // Resetting and rebuilding cache could be expensive.
1851 CRM_Core_Config::setPermitCacheFlushMode(FALSE);
1852
1853 // If a user has logged in, or accessed via a checksum
1854 // Then deliberately 'blanking' a value in the profile should remove it from their record
1855 // @todo this should either be TRUE or FALSE in the context of import - once
1856 // we figure out which we can remove all the rest.
1857 // Also note the meaning of this parameter is less than it used to
1858 // be following block cleanup.
1859 $formatted['updateBlankLocInfo'] = TRUE;
1860 if ((CRM_Core_Session::singleton()->get('authSrc') & (CRM_Core_Permission::AUTH_SRC_CHECKSUM + CRM_Core_Permission::AUTH_SRC_LOGIN)) == 0) {
1861 $formatted['updateBlankLocInfo'] = FALSE;
1862 }
1863
1864 [$data, $contactDetails] = CRM_Contact_BAO_Contact::formatProfileContactParams($formatted, $contactFields, $contactId, NULL, $formatted['contact_type']);
1865
1866 // manage is_opt_out
1867 if (array_key_exists('is_opt_out', $contactFields) && array_key_exists('is_opt_out', $formatted)) {
1868 $wasOptOut = $contactDetails['is_opt_out'] ?? FALSE;
1869 $isOptOut = $formatted['is_opt_out'];
1870 $data['is_opt_out'] = $isOptOut;
1871 // on change, create new civicrm_subscription_history entry
1872 if (($wasOptOut != $isOptOut) && !empty($contactDetails['contact_id'])) {
1873 $shParams = [
1874 'contact_id' => $contactDetails['contact_id'],
1875 'status' => $isOptOut ? 'Removed' : 'Added',
1876 'method' => 'Web',
1877 ];
1878 CRM_Contact_BAO_SubscriptionHistory::create($shParams);
1879 }
1880 }
1881
1882 $contact = civicrm_api3('Contact', 'create', $data);
1883 $cid = $contact['id'];
1884
1885 CRM_Core_Config::setPermitCacheFlushMode(TRUE);
1886
1887 $contact = [
1888 'contact_id' => $cid,
1889 ];
1890
1891 $defaults = [];
1892 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
1893
1894 //get the id of the contact whose street address is not parsable, CRM-5886
1895 if ($this->_parseStreetAddress && is_object($newContact) && property_exists($newContact, 'address') && $newContact->address) {
1896 foreach ($newContact->address as $address) {
1897 if (!empty($address['street_address']) && (empty($address['street_number']) || empty($address['street_name']))) {
1898 $this->_unparsedStreetAddressContacts[] = [
1899 'id' => $newContact->id,
1900 'streetAddress' => $address['street_address'],
1901 ];
1902 }
1903 }
1904 }
1905 return $newContact;
1906 }
1907
1908 /**
1909 * Format params for update and fill mode.
1910 *
1911 * @param array $params
1912 * reference to an array containing all the.
1913 * values for import
1914 * @param int $onDuplicate
1915 * @param int $cid
1916 * contact id.
1917 */
1918 public function formatParams(&$params, $onDuplicate, $cid) {
1919 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
1920 return;
1921 }
1922
1923 $contactParams = [
1924 'contact_id' => $cid,
1925 ];
1926
1927 $defaults = [];
1928 $contactObj = CRM_Contact_BAO_Contact::retrieve($contactParams, $defaults);
1929
1930 $modeFill = ($onDuplicate == CRM_Import_Parser::DUPLICATE_FILL);
1931
1932 $groupTree = CRM_Core_BAO_CustomGroup::getTree($params['contact_type'], NULL, $cid, 0, NULL);
1933 CRM_Core_BAO_CustomGroup::setDefaults($groupTree, $defaults, FALSE, FALSE);
1934
1935 $locationFields = [
1936 'email' => 'email',
1937 'phone' => 'phone',
1938 'im' => 'name',
1939 'website' => 'website',
1940 'address' => 'address',
1941 ];
1942
1943 $contact = get_object_vars($contactObj);
1944
1945 foreach ($params as $key => $value) {
1946 if ($key == 'id' || $key == 'contact_type') {
1947 continue;
1948 }
1949
1950 if (array_key_exists($key, $locationFields)) {
1951 continue;
1952 }
1953 if (in_array($key, [
1954 'email_greeting',
1955 'postal_greeting',
1956 'addressee',
1957 ])) {
1958 // CRM-4575, need to null custom
1959 if ($params["{$key}_id"] != 4) {
1960 $params["{$key}_custom"] = 'null';
1961 }
1962 unset($params[$key]);
1963 }
1964 else {
1965 if ($customFieldId = CRM_Core_BAO_CustomField::getKeyID($key)) {
1966 $custom_params = ['id' => $contact['id'], 'return' => $key];
1967 $getValue = civicrm_api3('Contact', 'getvalue', $custom_params);
1968 if (empty($getValue)) {
1969 unset($getValue);
1970 }
1971 }
1972 else {
1973 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $key);
1974 }
1975 if ($key == 'contact_source') {
1976 $params['source'] = $params[$key];
1977 unset($params[$key]);
1978 }
1979
1980 if ($modeFill && isset($getValue)) {
1981 unset($params[$key]);
1982 if ($customFieldId) {
1983 // Extra values must be unset to ensure the values are not
1984 // imported.
1985 unset($params['custom'][$customFieldId]);
1986 }
1987 }
1988 }
1989 }
1990
1991 foreach ($locationFields as $locKeys) {
1992 if (isset($params[$locKeys]) && is_array($params[$locKeys])) {
1993 foreach ($params[$locKeys] as $key => $value) {
1994 if ($modeFill) {
1995 $getValue = CRM_Utils_Array::retrieveValueRecursive($contact, $locKeys);
1996
1997 if (isset($getValue)) {
1998 foreach ($getValue as $cnt => $values) {
1999 if ($locKeys == 'website') {
2000 if (($getValue[$cnt]['website_type_id'] == $params[$locKeys][$key]['website_type_id'])) {
2001 unset($params[$locKeys][$key]);
2002 }
2003 }
2004 else {
2005 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']) {
2006 unset($params[$locKeys][$key]);
2007 }
2008 }
2009 }
2010 }
2011 }
2012 }
2013 if (count($params[$locKeys]) == 0) {
2014 unset($params[$locKeys]);
2015 }
2016 }
2017 }
2018 }
2019
2020 /**
2021 * Convert any given date string to default date array.
2022 *
2023 * @param array $params
2024 * Has given date-format.
2025 * @param array $formatted
2026 * Store formatted date in this array.
2027 * @param int $dateType
2028 * Type of date.
2029 * @param string $dateParam
2030 * Index of params.
2031 */
2032 public static function formatCustomDate(&$params, &$formatted, $dateType, $dateParam) {
2033 //fix for CRM-2687
2034 CRM_Utils_Date::convertToDefaultDate($params, $dateType, $dateParam);
2035 $formatted[$dateParam] = CRM_Utils_Date::processDate($params[$dateParam]);
2036 }
2037
2038 /**
2039 * Generate status and error message for unparsed street address records.
2040 *
2041 * @param array $values
2042 * The array of values belonging to each row.
2043 * @param array $statusFieldName
2044 * Store formatted date in this array.
2045 * @param $returnCode
2046 *
2047 * @return int
2048 */
2049 public function processMessage(&$values, $statusFieldName, $returnCode) {
2050 if (empty($this->_unparsedStreetAddressContacts)) {
2051 $importRecordParams = [
2052 $statusFieldName => 'IMPORTED',
2053 ];
2054 }
2055 else {
2056 $errorMessage = ts("Record imported successfully but unable to parse the street address: ");
2057 foreach ($this->_unparsedStreetAddressContacts as $contactInfo => $contactValue) {
2058 $contactUrl = CRM_Utils_System::url('civicrm/contact/add', 'reset=1&action=update&cid=' . $contactValue['id'], TRUE, NULL, FALSE);
2059 $errorMessage .= "\n Contact ID:" . $contactValue['id'] . " <a href=\"$contactUrl\"> " . $contactValue['streetAddress'] . "</a>";
2060 }
2061 array_unshift($values, $errorMessage);
2062 $importRecordParams = [
2063 $statusFieldName => 'ERROR',
2064 "${statusFieldName}Msg" => $errorMessage,
2065 ];
2066 $returnCode = CRM_Import_Parser::UNPARSED_ADDRESS_WARNING;
2067 }
2068 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2069 return $returnCode;
2070 }
2071
2072 /**
2073 * @param $relKey
2074 * @param array $params
2075 *
2076 * @return bool
2077 */
2078 public function checkRelatedContactFields($relKey, $params) {
2079 //avoid blank contact creation.
2080 $allowToCreate = FALSE;
2081
2082 //build the mapper field array.
2083 static $relatedContactFields = [];
2084 if (!isset($relatedContactFields[$relKey])) {
2085 foreach ($this->_mapperRelated as $key => $name) {
2086 if (!$name) {
2087 continue;
2088 }
2089
2090 if (!empty($relatedContactFields[$name]) && !is_array($relatedContactFields[$name])) {
2091 $relatedContactFields[$name] = [];
2092 }
2093 $fldName = $this->_mapperRelatedContactDetails[$key] ?? NULL;
2094 if ($fldName == 'url') {
2095 $fldName = 'website';
2096 }
2097 if ($fldName) {
2098 $relatedContactFields[$name][] = $fldName;
2099 }
2100 }
2101 }
2102
2103 //validate for passed data.
2104 if (is_array($relatedContactFields[$relKey])) {
2105 foreach ($relatedContactFields[$relKey] as $fld) {
2106 if (!empty($params[$fld])) {
2107 $allowToCreate = TRUE;
2108 break;
2109 }
2110 }
2111 }
2112
2113 return $allowToCreate;
2114 }
2115
2116 /**
2117 * get subtypes given the contact type
2118 *
2119 * @param string $contactType
2120 * @return array $subTypes
2121 */
2122 public static function getSubtypes($contactType) {
2123 $subTypes = [];
2124 $types = CRM_Contact_BAO_ContactType::subTypeInfo($contactType);
2125
2126 if (count($types) > 0) {
2127 foreach ($types as $type) {
2128 $subTypes[] = $type['name'];
2129 }
2130 }
2131 return $subTypes;
2132 }
2133
2134 /**
2135 * Get the possible contact matches.
2136 *
2137 * 1) the chosen dedupe rule falling back to
2138 * 2) a check for the external ID.
2139 *
2140 * @see https://issues.civicrm.org/jira/browse/CRM-17275
2141 *
2142 * @param array $params
2143 *
2144 * @return array
2145 * IDs of possible matches.
2146 *
2147 * @throws \CRM_Core_Exception
2148 * @throws \CiviCRM_API3_Exception
2149 */
2150 protected function getPossibleContactMatches($params) {
2151 $extIDMatch = NULL;
2152
2153 if (!empty($params['external_identifier'])) {
2154 // Check for any match on external id, deleted or otherwise.
2155 $extIDContact = civicrm_api3('Contact', 'get', [
2156 'external_identifier' => $params['external_identifier'],
2157 'showAll' => 'all',
2158 'return' => ['id', 'contact_is_deleted'],
2159 ]);
2160 if (isset($extIDContact['id'])) {
2161 $extIDMatch = $extIDContact['id'];
2162
2163 if ($extIDContact['values'][$extIDMatch]['contact_is_deleted'] == 1) {
2164 // If the contact is deleted, update external identifier to be blank
2165 // to avoid key error from MySQL.
2166 $params = ['id' => $extIDMatch, 'external_identifier' => ''];
2167 civicrm_api3('Contact', 'create', $params);
2168
2169 // And now it is no longer a match.
2170 $extIDMatch = NULL;
2171 }
2172 }
2173 }
2174 $checkParams = ['check_permissions' => FALSE, 'match' => $params];
2175 $checkParams['match']['contact_type'] = $this->_contactType;
2176
2177 $possibleMatches = civicrm_api3('Contact', 'duplicatecheck', $checkParams);
2178 if (!$extIDMatch) {
2179 return array_keys($possibleMatches['values']);
2180 }
2181 if ($possibleMatches['count']) {
2182 if (array_key_exists($extIDMatch, $possibleMatches['values'])) {
2183 return [$extIDMatch];
2184 }
2185 throw new CRM_Core_Exception(ts(
2186 'Matching this contact based on the de-dupe rule would cause an external ID conflict'));
2187 }
2188 return [$extIDMatch];
2189 }
2190
2191 /**
2192 * Format the form mapping parameters ready for the parser.
2193 *
2194 * @param int $count
2195 * Number of rows.
2196 *
2197 * @return array $parserParameters
2198 */
2199 public static function getParameterForParser($count) {
2200 $baseArray = [];
2201 for ($i = 0; $i < $count; $i++) {
2202 $baseArray[$i] = NULL;
2203 }
2204 $parserParameters['mapperLocType'] = $baseArray;
2205 $parserParameters['mapperPhoneType'] = $baseArray;
2206 $parserParameters['mapperImProvider'] = $baseArray;
2207 $parserParameters['mapperWebsiteType'] = $baseArray;
2208 $parserParameters['mapperRelated'] = $baseArray;
2209 $parserParameters['relatedContactType'] = $baseArray;
2210 $parserParameters['relatedContactDetails'] = $baseArray;
2211 $parserParameters['relatedContactLocType'] = $baseArray;
2212 $parserParameters['relatedContactPhoneType'] = $baseArray;
2213 $parserParameters['relatedContactImProvider'] = $baseArray;
2214 $parserParameters['relatedContactWebsiteType'] = $baseArray;
2215
2216 return $parserParameters;
2217
2218 }
2219
2220 /**
2221 * Set field metadata.
2222 */
2223 protected function setFieldMetadata() {
2224 $this->setImportableFieldsMetadata($this->getContactImportMetadata());
2225 // Probably no longer needed but here for now.
2226 $this->_relationships = $this->getRelationships();
2227 }
2228
2229 /**
2230 * @param array $newContact
2231 * @param $statusFieldName
2232 * @param array $values
2233 * @param int $onDuplicate
2234 * @param array $formatted
2235 * @param array $contactFields
2236 *
2237 * @return int
2238 *
2239 * @throws \CRM_Core_Exception
2240 * @throws \CiviCRM_API3_Exception
2241 * @throws \Civi\API\Exception\UnauthorizedException
2242 */
2243 protected function handleDuplicateError(array $newContact, $statusFieldName, array $values, int $onDuplicate, array $formatted, array $contactFields): int {
2244 $urls = [];
2245 // need to fix at some stage and decide if the error will return an
2246 // array or string, crude hack for now
2247 if (is_array($newContact['error_message']['params'][0])) {
2248 $cids = $newContact['error_message']['params'][0];
2249 }
2250 else {
2251 $cids = explode(',', $newContact['error_message']['params'][0]);
2252 }
2253
2254 foreach ($cids as $cid) {
2255 $urls[] = CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $cid, TRUE);
2256 }
2257
2258 $url_string = implode("\n", $urls);
2259
2260 // If we duplicate more than one record, skip no matter what
2261 if (count($cids) > 1) {
2262 $errorMessage = ts('Record duplicates multiple contacts');
2263 $importRecordParams = [
2264 $statusFieldName => 'ERROR',
2265 "${statusFieldName}Msg" => $errorMessage,
2266 ];
2267
2268 //combine error msg to avoid mismatch between error file columns.
2269 $errorMessage .= "\n" . $url_string;
2270 array_unshift($values, $errorMessage);
2271 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2272 return CRM_Import_Parser::ERROR;
2273 }
2274
2275 // Params only had one id, so shift it out
2276 $contactId = array_shift($cids);
2277 $cid = NULL;
2278
2279 $vals = ['contact_id' => $contactId];
2280
2281 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_REPLACE) {
2282 civicrm_api('contact', 'delete', $vals);
2283 $cid = CRM_Contact_BAO_Contact::createProfileContact($formatted, $contactFields, $contactId, NULL, NULL, $formatted['contact_type']);
2284 }
2285 if (in_array((int) $onDuplicate, [CRM_Import_Parser::DUPLICATE_UPDATE, CRM_Import_Parser::DUPLICATE_FILL], TRUE)) {
2286 $newContact = $this->createContact($formatted, $contactFields, $onDuplicate, $contactId);
2287 }
2288 // else skip does nothing and just returns an error code.
2289 if ($cid) {
2290 $contact = [
2291 'contact_id' => $cid,
2292 ];
2293 $defaults = [];
2294 $newContact = CRM_Contact_BAO_Contact::retrieve($contact, $defaults);
2295 }
2296
2297 if (civicrm_error($newContact)) {
2298 if (empty($newContact['error_message']['params'])) {
2299 // different kind of error other than DUPLICATE
2300 $errorMessage = $newContact['error_message'];
2301 array_unshift($values, $errorMessage);
2302 $importRecordParams = [
2303 $statusFieldName => 'ERROR',
2304 "${statusFieldName}Msg" => $errorMessage,
2305 ];
2306 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2307 return CRM_Import_Parser::ERROR;
2308 }
2309
2310 $contactID = $newContact['error_message']['params'][0];
2311 if (is_array($contactID)) {
2312 $contactID = array_pop($contactID);
2313 }
2314 if (!in_array($contactID, $this->_newContacts)) {
2315 $this->_newContacts[] = $contactID;
2316 }
2317 }
2318 //CRM-262 No Duplicate Checking
2319 if ($onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
2320 array_unshift($values, $url_string);
2321 $importRecordParams = [
2322 $statusFieldName => 'DUPLICATE',
2323 "${statusFieldName}Msg" => "Skipping duplicate record",
2324 ];
2325 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2326 return CRM_Import_Parser::DUPLICATE;
2327 }
2328
2329 $importRecordParams = [
2330 $statusFieldName => 'IMPORTED',
2331 ];
2332 $this->updateImportRecord($values[count($values) - 1], $importRecordParams);
2333 //return warning if street address is not parsed, CRM-5886
2334 return $this->processMessage($values, $statusFieldName, CRM_Import_Parser::VALID);
2335 }
2336
2337 /**
2338 * Validate a formatted contact parameter list.
2339 *
2340 * @param array $params
2341 * Structured parameter list (as in crm_format_params).
2342 *
2343 * @throw CRM_Core_Error
2344 */
2345 public function deprecated_validate_formatted_contact(&$params): void {
2346 // Look for offending email addresses
2347
2348 if (array_key_exists('email', $params)) {
2349 foreach ($params['email'] as $count => $values) {
2350 if (!is_array($values)) {
2351 continue;
2352 }
2353 if ($email = CRM_Utils_Array::value('email', $values)) {
2354 // validate each email
2355 if (!CRM_Utils_Rule::email($email)) {
2356 throw new CRM_Core_Exception('No valid email address');
2357 }
2358
2359 // check for loc type id.
2360 if (empty($values['location_type_id'])) {
2361 throw new CRM_Core_Exception('Location Type Id missing.');
2362 }
2363 }
2364 }
2365 }
2366
2367 // Validate custom data fields
2368 if (array_key_exists('custom', $params) && is_array($params['custom'])) {
2369 foreach ($params['custom'] as $key => $custom) {
2370 if (is_array($custom)) {
2371 foreach ($custom as $fieldId => $value) {
2372 $valid = CRM_Core_BAO_CustomValue::typecheck(CRM_Utils_Array::value('type', $value),
2373 CRM_Utils_Array::value('value', $value)
2374 );
2375 if (!$valid && $value['is_required']) {
2376 throw new CRM_Core_Exception('Invalid value for custom field \'' .
2377 $custom['name'] . '\''
2378 );
2379 }
2380 if (CRM_Utils_Array::value('type', $custom) == 'Date') {
2381 $params['custom'][$key][$fieldId]['value'] = str_replace('-', '', $params['custom'][$key][$fieldId]['value']);
2382 }
2383 }
2384 }
2385 }
2386 }
2387 }
2388
2389 /**
2390 * @param array $params
2391 * @param bool $dupeCheck
2392 * @param null|int $dedupeRuleGroupID
2393 *
2394 * @throws \CRM_Core_Exception
2395 */
2396 public function deprecated_contact_check_params(
2397 &$params,
2398 $dupeCheck = TRUE,
2399 $dedupeRuleGroupID = NULL) {
2400
2401 $requiredCheck = TRUE;
2402
2403 if (isset($params['id']) && is_numeric($params['id'])) {
2404 $requiredCheck = FALSE;
2405 }
2406 if ($requiredCheck) {
2407 $required = [
2408 'Individual' => [
2409 ['first_name', 'last_name'],
2410 'email',
2411 ],
2412 'Household' => [
2413 'household_name',
2414 ],
2415 'Organization' => [
2416 'organization_name',
2417 ],
2418 ];
2419
2420 // contact_type has a limited number of valid values
2421 if (empty($params['contact_type'])) {
2422 throw new CRM_Core_Exception("No Contact Type");
2423 }
2424 $fields = $required[$params['contact_type']] ?? NULL;
2425 if ($fields == NULL) {
2426 throw new CRM_Core_Exception("Invalid Contact Type: {$params['contact_type']}");
2427 }
2428
2429 if ($csType = CRM_Utils_Array::value('contact_sub_type', $params)) {
2430 if (!(CRM_Contact_BAO_ContactType::isExtendsContactType($csType, $params['contact_type']))) {
2431 throw new CRM_Core_Exception("Invalid or Mismatched Contact Subtype: " . implode(', ', (array) $csType));
2432 }
2433 }
2434
2435 if (empty($params['contact_id']) && !empty($params['id'])) {
2436 $valid = FALSE;
2437 $error = '';
2438 foreach ($fields as $field) {
2439 if (is_array($field)) {
2440 $valid = TRUE;
2441 foreach ($field as $element) {
2442 if (empty($params[$element])) {
2443 $valid = FALSE;
2444 $error .= $element;
2445 break;
2446 }
2447 }
2448 }
2449 else {
2450 if (!empty($params[$field])) {
2451 $valid = TRUE;
2452 }
2453 }
2454 if ($valid) {
2455 break;
2456 }
2457 }
2458
2459 if (!$valid) {
2460 throw new CRM_Core_Exception("Required fields not found for {$params['contact_type']} : $error");
2461 }
2462 }
2463 }
2464
2465 if ($dupeCheck) {
2466 // @todo switch to using api version
2467 // $dupes = civicrm_api3('Contact', 'duplicatecheck', (array('match' => $params, 'dedupe_rule_id' => $dedupeRuleGroupID)));
2468 // $ids = $dupes['count'] ? implode(',', array_keys($dupes['values'])) : NULL;
2469 $ids = CRM_Contact_BAO_Contact::getDuplicateContacts($params, $params['contact_type'], 'Unsupervised', [], CRM_Utils_Array::value('check_permissions', $params), $dedupeRuleGroupID);
2470 if ($ids != NULL) {
2471 $error = CRM_Core_Error::createError("Found matching contacts: " . implode(',', $ids),
2472 CRM_Core_Error::DUPLICATE_CONTACT,
2473 'Fatal', $ids
2474 );
2475 return civicrm_api3_create_error($error->pop());
2476 }
2477 }
2478
2479 // check for organisations with same name
2480 if (!empty($params['current_employer'])) {
2481 $organizationParams = ['organization_name' => $params['current_employer']];
2482 $dupeIds = CRM_Contact_BAO_Contact::getDuplicateContacts($organizationParams, 'Organization', 'Supervised', [], FALSE);
2483
2484 // check for mismatch employer name and id
2485 if (!empty($params['employer_id']) && !in_array($params['employer_id'], $dupeIds)
2486 ) {
2487 throw new CRM_Core_Exception('Employer name and Employer id Mismatch');
2488 }
2489
2490 // show error if multiple organisation with same name exist
2491 if (empty($params['employer_id']) && (count($dupeIds) > 1)
2492 ) {
2493 return civicrm_api3_create_error('Found more than one Organisation with same Name.');
2494 }
2495 }
2496 }
2497
2498 }