Merge pull request #23511 from eileenmcnaughton/import_unused
[civicrm-core.git] / CRM / Import / Parser.php
CommitLineData
ec3811b1
CW
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
ec3811b1 5 | |
bc77d7c0
TO
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 |
ec3811b1 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
ec3811b1 11
7b057b66
EM
12use Civi\Api4\UserJob;
13
ec3811b1
CW
14/**
15 *
16 * @package CRM
ca5cec67 17 * @copyright CiviCRM LLC https://civicrm.org/licensing
ec3811b1 18 */
ec3811b1
CW
19abstract class CRM_Import_Parser {
20 /**
21 * Settings
22 */
ca2057ea 23 const MAX_WARNINGS = 25, DEFAULT_TIMEOUT = 30;
ec3811b1
CW
24
25 /**
26 * Return codes
27 */
7da04cde 28 const VALID = 1, WARNING = 2, ERROR = 4, CONFLICT = 8, STOP = 16, DUPLICATE = 32, MULTIPLE_DUPE = 64, NO_MATCH = 128, UNPARSED_ADDRESS_WARNING = 256;
ec3811b1
CW
29
30 /**
31 * Parser modes
32 */
7da04cde 33 const MODE_MAPFIELD = 1, MODE_PREVIEW = 2, MODE_SUMMARY = 4, MODE_IMPORT = 8;
ec3811b1
CW
34
35 /**
36 * Codes for duplicate record handling
37 */
a87fbfbd 38 const DUPLICATE_SKIP = 1, DUPLICATE_UPDATE = 4, DUPLICATE_FILL = 8, DUPLICATE_NOCHECK = 16;
ec3811b1
CW
39
40 /**
41 * Contact types
42 */
7da04cde 43 const CONTACT_INDIVIDUAL = 1, CONTACT_HOUSEHOLD = 2, CONTACT_ORGANIZATION = 4;
69a4c20a
CW
44
45
7b057b66
EM
46 /**
47 * User job id.
48 *
49 * This is the primary key of the civicrm_user_job table which is used to
50 * track the import.
51 *
52 * @var int
53 */
54 protected $userJobID;
55
56 /**
57 * @return int|null
58 */
59 public function getUserJobID(): ?int {
60 return $this->userJobID;
61 }
62
63 /**
64 * Set user job ID.
65 *
66 * @param int $userJobID
79d21b5b
EM
67 *
68 * @return self
7b057b66 69 */
79d21b5b 70 public function setUserJobID(int $userJobID): self {
7b057b66 71 $this->userJobID = $userJobID;
79d21b5b 72 return $this;
7b057b66
EM
73 }
74
75 /**
76 * Get User Job.
77 *
78 * API call to retrieve the userJob row.
79 *
80 * @return array
81 *
82 * @throws \API_Exception
83 */
84 protected function getUserJob(): array {
85 return UserJob::get()
86 ->addWhere('id', '=', $this->getUserJobID())
87 ->execute()
88 ->first();
89 }
90
5e21b588
EM
91 /**
92 * Get the relevant datasource object.
93 *
94 * @return \CRM_Import_DataSource|null
95 *
96 * @throws \API_Exception
97 */
98 protected function getDataSourceObject(): ?CRM_Import_DataSource {
99 $className = $this->getSubmittedValue('dataSource');
100 if ($className) {
101 /* @var CRM_Import_DataSource $dataSource */
102 return new $className($this->getUserJobID());
103 }
104 return NULL;
105 }
106
52bd01f5
EM
107 /**
108 * Get the submitted value, as stored on the user job.
109 *
110 * @param string $fieldName
111 *
112 * @return mixed
113 *
114 * @throws \API_Exception
115 */
116 protected function getSubmittedValue(string $fieldName) {
117 return $this->getUserJob()['metadata']['submitted_values'][$fieldName];
118 }
119
5e21b588
EM
120 /**
121 * Has the import completed.
122 *
123 * @return bool
124 *
125 * @throws \API_Exception
126 * @throws \CRM_Core_Exception
127 */
128 public function isComplete() :bool {
129 return $this->getDataSourceObject()->isCompleted();
130 }
131
52bd01f5
EM
132 /**
133 * Get configured contact type.
134 *
135 * @throws \API_Exception
136 */
137 protected function getContactType() {
138 if (!$this->_contactType) {
139 $contactTypeMapping = [
140 CRM_Import_Parser::CONTACT_INDIVIDUAL => 'Individual',
141 CRM_Import_Parser::CONTACT_HOUSEHOLD => 'Household',
142 CRM_Import_Parser::CONTACT_ORGANIZATION => 'Organization',
143 ];
144 $this->_contactType = $contactTypeMapping[$this->getSubmittedValue('contactType')];
145 }
146 return $this->_contactType;
147 }
148
80cb71bb
EM
149 /**
150 * Get configured contact type.
151 *
152 * @return string|null
153 *
154 * @throws \API_Exception
155 */
156 public function getContactSubType() {
157 if (!$this->_contactSubType) {
158 $this->_contactSubType = $this->getSubmittedValue('contactSubType');
159 }
160 return $this->_contactSubType;
161 }
162
69a4c20a 163 /**
100fef9d 164 * Total number of non empty lines
971e129b 165 * @var int
69a4c20a
CW
166 */
167 protected $_totalCount;
168
169 /**
100fef9d 170 * Running total number of valid lines
971e129b 171 * @var int
69a4c20a
CW
172 */
173 protected $_validCount;
174
175 /**
100fef9d 176 * Running total number of invalid rows
971e129b 177 * @var int
69a4c20a
CW
178 */
179 protected $_invalidRowCount;
180
181 /**
100fef9d 182 * Maximum number of non-empty/comment lines to process
69a4c20a
CW
183 *
184 * @var int
185 */
186 protected $_maxLinesToProcess;
187
69a4c20a 188 /**
100fef9d 189 * Array of error lines, bounded by MAX_ERROR
971e129b 190 * @var array
69a4c20a
CW
191 */
192 protected $_errors;
193
69a4c20a 194 /**
100fef9d 195 * Total number of duplicate (from database) lines
971e129b 196 * @var int
69a4c20a
CW
197 */
198 protected $_duplicateCount;
199
200 /**
100fef9d 201 * Array of duplicate lines
971e129b 202 * @var array
69a4c20a
CW
203 */
204 protected $_duplicates;
205
69a4c20a 206 /**
100fef9d 207 * Maximum number of warnings to store
971e129b 208 * @var int
69a4c20a
CW
209 */
210 protected $_maxWarningCount = self::MAX_WARNINGS;
211
212 /**
100fef9d 213 * Array of warning lines, bounded by MAX_WARNING
971e129b 214 * @var array
69a4c20a
CW
215 */
216 protected $_warnings;
217
218 /**
100fef9d 219 * Array of all the fields that could potentially be part
69a4c20a
CW
220 * of this import process
221 * @var array
222 */
223 protected $_fields;
224
64cafaa3 225 /**
226 * Metadata for all available fields, keyed by unique name.
227 *
228 * This is intended to supercede $_fields which uses a special sauce format which
229 * importableFieldsMetadata uses the standard getfields type format.
230 *
231 * @var array
232 */
233 protected $importableFieldsMetadata = [];
234
235 /**
236 * Get metadata for all importable fields in std getfields style format.
237 *
238 * @return array
239 */
240 public function getImportableFieldsMetadata(): array {
241 return $this->importableFieldsMetadata;
242 }
243
244 /**
245 * Set metadata for all importable fields in std getfields style format.
f25114b4 246 *
64cafaa3 247 * @param array $importableFieldsMetadata
248 */
f25114b4 249 public function setImportableFieldsMetadata(array $importableFieldsMetadata): void {
64cafaa3 250 $this->importableFieldsMetadata = $importableFieldsMetadata;
251 }
252
69a4c20a 253 /**
100fef9d 254 * Array of the fields that are actually part of the import process
69a4c20a
CW
255 * the position in the array also dictates their position in the import
256 * file
257 * @var array
258 */
259 protected $_activeFields;
260
261 /**
100fef9d 262 * Cache the count of active fields
69a4c20a
CW
263 *
264 * @var int
265 */
266 protected $_activeFieldCount;
267
268 /**
100fef9d 269 * Cache of preview rows
69a4c20a
CW
270 *
271 * @var array
272 */
273 protected $_rows;
274
275 /**
100fef9d 276 * Filename of error data
69a4c20a
CW
277 *
278 * @var string
279 */
280 protected $_errorFileName;
281
69a4c20a 282 /**
100fef9d 283 * Filename of duplicate data
69a4c20a
CW
284 *
285 * @var string
286 */
287 protected $_duplicateFileName;
288
289 /**
100fef9d 290 * Contact type
69a4c20a 291 *
52bd01f5 292 * @var string
69a4c20a
CW
293 */
294 public $_contactType;
80cb71bb 295
0d46885c
EM
296 /**
297 * @param string $contactType
298 *
299 * @return CRM_Import_Parser
300 */
301 public function setContactType(string $contactType): CRM_Import_Parser {
302 $this->_contactType = $contactType;
303 return $this;
304 }
305
e87ff4ce 306 /**
307 * Contact sub-type
308 *
80cb71bb 309 * @var int|null
e87ff4ce 310 */
311 public $_contactSubType;
69a4c20a 312
80cb71bb
EM
313 /**
314 * @param int|null $contactSubType
315 *
316 * @return self
317 */
318 public function setContactSubType(?int $contactSubType): self {
319 $this->_contactSubType = $contactSubType;
320 return $this;
321 }
322
69a4c20a 323 /**
e87ff4ce 324 * Class constructor.
69a4c20a 325 */
00be9182 326 public function __construct() {
69a4c20a 327 $this->_maxLinesToProcess = 0;
69a4c20a
CW
328 }
329
69a4c20a 330 /**
fe482240 331 * Set and validate field values.
69a4c20a 332 *
5a4f6742 333 * @param array $elements
16b10e64 334 * array.
69a4c20a 335 */
1006edc9 336 public function setActiveFieldValues($elements): void {
69a4c20a
CW
337 $maxCount = count($elements) < $this->_activeFieldCount ? count($elements) : $this->_activeFieldCount;
338 for ($i = 0; $i < $maxCount; $i++) {
339 $this->_activeFields[$i]->setValue($elements[$i]);
340 }
341
342 // reset all the values that we did not have an equivalent import element
343 for (; $i < $this->_activeFieldCount; $i++) {
344 $this->_activeFields[$i]->resetValue();
345 }
69a4c20a
CW
346 }
347
348 /**
fe482240 349 * Format the field values for input to the api.
69a4c20a 350 *
a6c01b45
CW
351 * @return array
352 * (reference) associative array of name/value pairs
69a4c20a 353 */
00be9182 354 public function &getActiveFieldParams() {
be2fb01f 355 $params = [];
69a4c20a
CW
356 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
357 if (isset($this->_activeFields[$i]->_value)
358 && !isset($params[$this->_activeFields[$i]->_name])
359 && !isset($this->_activeFields[$i]->_related)
360 ) {
361
362 $params[$this->_activeFields[$i]->_name] = $this->_activeFields[$i]->_value;
363 }
364 }
365 return $params;
366 }
367
8cebffb2 368 /**
badf5061
JP
369 * Add progress bar to the import process. Calculates time remaining, status etc.
370 *
8cebffb2 371 * @param $statusID
badf5061 372 * status id of the import process saved in $config->uploadDir.
8cebffb2
JP
373 * @param bool $startImport
374 * True when progress bar is to be initiated.
375 * @param $startTimestamp
f25114b4 376 * Initial timestamp when the import was started.
8cebffb2
JP
377 * @param $prevTimestamp
378 * Previous timestamp when this function was last called.
379 * @param $totalRowCount
380 * Total number of rows in the import file.
381 *
382 * @return NULL|$currTimestamp
383 */
384 public function progressImport($statusID, $startImport = TRUE, $startTimestamp = NULL, $prevTimestamp = NULL, $totalRowCount = NULL) {
f25114b4 385 $statusFile = CRM_Core_Config::singleton()->uploadDir . "status_{$statusID}.txt";
8cebffb2
JP
386
387 if ($startImport) {
388 $status = "<div class='description'>&nbsp; " . ts('No processing status reported yet.') . "</div>";
389 //do not force the browser to display the save dialog, CRM-7640
be2fb01f 390 $contents = json_encode([0, $status]);
8cebffb2
JP
391 file_put_contents($statusFile, $contents);
392 }
393 else {
2e1f50d6 394 $rowCount = $this->_rowCount ?? $this->_lineCount;
8cebffb2 395 $currTimestamp = time();
8cebffb2
JP
396 $time = ($currTimestamp - $prevTimestamp);
397 $recordsLeft = $totalRowCount - $rowCount;
398 if ($recordsLeft < 0) {
399 $recordsLeft = 0;
400 }
401 $estimatedTime = ($recordsLeft / 50) * $time;
402 $estMinutes = floor($estimatedTime / 60);
403 $timeFormatted = '';
404 if ($estMinutes > 1) {
405 $timeFormatted = $estMinutes . ' ' . ts('minutes') . ' ';
406 $estimatedTime = $estimatedTime - ($estMinutes * 60);
407 }
408 $timeFormatted .= round($estimatedTime) . ' ' . ts('seconds');
409 $processedPercent = (int ) (($rowCount * 100) / $totalRowCount);
410 $statusMsg = ts('%1 of %2 records - %3 remaining',
be2fb01f 411 [1 => $rowCount, 2 => $totalRowCount, 3 => $timeFormatted]
8cebffb2
JP
412 );
413 $status = "<div class=\"description\">&nbsp; <strong>{$statusMsg}</strong></div>";
be2fb01f 414 $contents = json_encode([$processedPercent, $status]);
8cebffb2
JP
415
416 file_put_contents($statusFile, $contents);
417 return $currTimestamp;
418 }
419 }
420
e0ef6999
EM
421 /**
422 * @return array
423 */
f25114b4 424 public function getSelectValues(): array {
be2fb01f 425 $values = [];
69a4c20a
CW
426 foreach ($this->_fields as $name => $field) {
427 $values[$name] = $field->_title;
428 }
429 return $values;
430 }
431
e0ef6999
EM
432 /**
433 * @return array
434 */
00be9182 435 public function getSelectTypes() {
be2fb01f 436 $values = [];
79d21b5b
EM
437 // This is only called from the MapField form in isolation now,
438 // so we need to set the metadata.
439 $this->init();
69a4c20a
CW
440 foreach ($this->_fields as $name => $field) {
441 if (isset($field->_hasLocationType)) {
442 $values[$name] = $field->_hasLocationType;
443 }
444 }
445 return $values;
446 }
447
e0ef6999
EM
448 /**
449 * @return array
450 */
00be9182 451 public function getHeaderPatterns() {
be2fb01f 452 $values = [];
69a4c20a
CW
453 foreach ($this->_fields as $name => $field) {
454 if (isset($field->_headerPattern)) {
455 $values[$name] = $field->_headerPattern;
456 }
457 }
458 return $values;
459 }
460
e0ef6999
EM
461 /**
462 * @return array
463 */
00be9182 464 public function getDataPatterns() {
be2fb01f 465 $values = [];
69a4c20a
CW
466 foreach ($this->_fields as $name => $field) {
467 $values[$name] = $field->_dataPattern;
468 }
469 return $values;
470 }
471
472 /**
2b4bc760 473 * Remove single-quote enclosures from a value array (row).
69a4c20a
CW
474 *
475 * @param array $values
476 * @param string $enclosure
477 *
478 * @return void
69a4c20a 479 */
00be9182 480 public static function encloseScrub(&$values, $enclosure = "'") {
69a4c20a
CW
481 if (empty($values)) {
482 return;
483 }
484
485 foreach ($values as $k => $v) {
486 $values[$k] = preg_replace("/^$enclosure(.*)$enclosure$/", '$1', $v);
487 }
488 }
489
490 /**
fe482240 491 * Setter function.
69a4c20a
CW
492 *
493 * @param int $max
494 *
495 * @return void
69a4c20a 496 */
00be9182 497 public function setMaxLinesToProcess($max) {
69a4c20a
CW
498 $this->_maxLinesToProcess = $max;
499 }
500
7e56b830
EM
501 /**
502 * Validate that we have the required fields to create the contact or find it to update.
503 *
504 * Note that the users duplicate selection affects this as follows
505 * - if they did not select an update variant then the id field is not
506 * permitted in the mapping - so we can assume the presence of id means
507 * we should use it
508 * - the external_identifier field is valid in place of the other fields
509 * when they have chosen update or fill - in this case we are only looking
510 * to update an existing contact.
511 *
512 * @param string $contactType
513 * @param array $params
514 * @param bool $isPermitExistingMatchFields
7d2012dc
EM
515 * True if the it is enough to have fields which will enable us to find
516 * an existing contact (eg. external_identifier).
517 * @param string $prefixString
518 * String to include in the exception (e.g '(Child of)' if we are validating
519 * a related contact.
7e56b830
EM
520 *
521 * @return void
522 * @throws \CRM_Core_Exception
523 */
7d2012dc 524 protected function validateRequiredContactFields(string $contactType, array $params, bool $isPermitExistingMatchFields = TRUE, $prefixString = ''): void {
7e56b830
EM
525 if (!empty($params['id'])) {
526 return;
527 }
528 $requiredFields = [
529 'Individual' => [
530 'first_name_last_name' => ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')],
531 'email' => ts('Email Address'),
532 ],
533 'Organization' => ['organization_name' => ts('Organization Name')],
534 'Household' => ['household_name' => ts('Household Name')],
535 ][$contactType];
536 if ($isPermitExistingMatchFields) {
537 $requiredFields['external_identifier'] = ts('External Identifier');
538 // Historically just an email has been accepted as it is 'usually good enough'
539 // for a dedupe rule look up - but really this is a stand in for
540 // whatever is needed to find an existing matching contact using the
541 // specified dedupe rule (or the default Unsupervised if not specified).
542 $requiredFields['email'] = ts('Email Address');
543 }
7d2012dc 544 $this->validateRequiredFields($requiredFields, $params, $prefixString);
7e56b830
EM
545 }
546
69a4c20a 547 /**
fe482240 548 * Determines the file extension based on error code.
69a4c20a 549 *
f54e87d9 550 * @var int $type error code constant
69a4c20a 551 * @return string
69a4c20a 552 */
00be9182 553 public static function errorFileName($type) {
69a4c20a
CW
554 $fileName = NULL;
555 if (empty($type)) {
556 return $fileName;
557 }
558
559 $config = CRM_Core_Config::singleton();
560 $fileName = $config->uploadDir . "sqlImport";
561 switch ($type) {
562 case self::ERROR:
563 $fileName .= '.errors';
564 break;
565
69a4c20a
CW
566 case self::DUPLICATE:
567 $fileName .= '.duplicates';
568 break;
569
570 case self::NO_MATCH:
571 $fileName .= '.mismatch';
572 break;
573
574 case self::UNPARSED_ADDRESS_WARNING:
575 $fileName .= '.unparsedAddress';
576 break;
577 }
578
579 return $fileName;
580 }
581
582 /**
fe482240 583 * Determines the file name based on error code.
69a4c20a
CW
584 *
585 * @var $type error code constant
586 * @return string
69a4c20a 587 */
00be9182 588 public static function saveFileName($type) {
69a4c20a
CW
589 $fileName = NULL;
590 if (empty($type)) {
591 return $fileName;
592 }
593 switch ($type) {
594 case self::ERROR:
595 $fileName = 'Import_Errors.csv';
596 break;
597
69a4c20a
CW
598 case self::DUPLICATE:
599 $fileName = 'Import_Duplicates.csv';
600 break;
601
602 case self::NO_MATCH:
603 $fileName = 'Import_Mismatch.csv';
604 break;
605
606 case self::UNPARSED_ADDRESS_WARNING:
607 $fileName = 'Import_Unparsed_Address.csv';
608 break;
609 }
610
611 return $fileName;
612 }
613
56316747 614 /**
615 * Check if contact is a duplicate .
616 *
617 * @param array $formatValues
618 *
619 * @return array
620 */
621 protected function checkContactDuplicate(&$formatValues) {
622 //retrieve contact id using contact dedupe rule
95519b12 623 $formatValues['contact_type'] = $formatValues['contact_type'] ?? $this->_contactType;
56316747 624 $formatValues['version'] = 3;
625 require_once 'CRM/Utils/DeprecatedUtils.php';
bd7c6219 626 $params = $formatValues;
627 static $cIndieFields = NULL;
628 static $defaultLocationId = NULL;
629
630 $contactType = $params['contact_type'];
631 if ($cIndieFields == NULL) {
632 $cTempIndieFields = CRM_Contact_BAO_Contact::importableFields($contactType);
633 $cIndieFields = $cTempIndieFields;
634
635 $defaultLocation = CRM_Core_BAO_LocationType::getDefault();
636
637 // set the value to default location id else set to 1
638 if (!$defaultLocationId = (int) $defaultLocation->id) {
639 $defaultLocationId = 1;
640 }
641 }
642
643 $locationFields = CRM_Contact_BAO_Query::$_locationSpecificFields;
644
645 $contactFormatted = [];
646 foreach ($params as $key => $field) {
647 if ($field == NULL || $field === '') {
648 continue;
649 }
650 // CRM-17040, Considering only primary contact when importing contributions. So contribution inserts into primary contact
651 // instead of soft credit contact.
652 if (is_array($field) && $key != "soft_credit") {
653 foreach ($field as $value) {
654 $break = FALSE;
655 if (is_array($value)) {
656 foreach ($value as $name => $testForEmpty) {
657 if ($name !== 'phone_type' &&
658 ($testForEmpty === '' || $testForEmpty == NULL)
659 ) {
660 $break = TRUE;
661 break;
662 }
663 }
664 }
665 else {
666 $break = TRUE;
667 }
668 if (!$break) {
f8909307 669 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
bd7c6219 670 }
671 }
672 continue;
673 }
674
675 $value = [$key => $field];
676
677 // check if location related field, then we need to add primary location type
678 if (in_array($key, $locationFields)) {
679 $value['location_type_id'] = $defaultLocationId;
680 }
681 elseif (array_key_exists($key, $cIndieFields)) {
682 $value['contact_type'] = $contactType;
683 }
684
f8909307 685 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
bd7c6219 686 }
687
688 $contactFormatted['contact_type'] = $contactType;
689
690 return _civicrm_api3_deprecated_duplicate_formatted_contact($contactFormatted);
56316747 691 }
692
f8909307
EM
693 /**
694 * This function adds the contact variable in $values to the
695 * parameter list $params. For most cases, $values should have length 1. If
696 * the variable being added is a child of Location, a location_type_id must
697 * also be included. If it is a child of phone, a phone_type must be included.
698 *
699 * @param array $values
700 * The variable(s) to be added.
701 * @param array $params
702 * The structured parameter list.
703 *
704 * @return bool|CRM_Utils_Error
705 */
706 private function _civicrm_api3_deprecated_add_formatted_param(&$values, &$params) {
707 // @todo - like most functions in import ... most of this is cruft....
708 // Crawl through the possible classes:
709 // Contact
710 // Individual
711 // Household
712 // Organization
713 // Location
714 // Address
715 // Email
716 // Phone
717 // IM
718 // Note
719 // Custom
720
721 // Cache the various object fields
722 static $fields = NULL;
723
724 if ($fields == NULL) {
725 $fields = [];
726 }
727
728 // first add core contact values since for other Civi modules they are not added
729 require_once 'CRM/Contact/BAO/Contact.php';
730 $contactFields = CRM_Contact_DAO_Contact::fields();
731 _civicrm_api3_store_values($contactFields, $values, $params);
732
733 if (isset($values['contact_type'])) {
734 // we're an individual/household/org property
735
736 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact::fields();
737
738 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
739 return TRUE;
740 }
741
742 if (isset($values['individual_prefix'])) {
743 if (!empty($params['prefix_id'])) {
744 $prefixes = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'prefix_id');
745 $params['prefix'] = $prefixes[$params['prefix_id']];
746 }
747 else {
748 $params['prefix'] = $values['individual_prefix'];
749 }
750 return TRUE;
751 }
752
753 if (isset($values['individual_suffix'])) {
754 if (!empty($params['suffix_id'])) {
755 $suffixes = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'suffix_id');
756 $params['suffix'] = $suffixes[$params['suffix_id']];
757 }
758 else {
759 $params['suffix'] = $values['individual_suffix'];
760 }
761 return TRUE;
762 }
763
764 // CRM-4575
765 if (isset($values['email_greeting'])) {
766 if (!empty($params['email_greeting_id'])) {
767 $emailGreetingFilter = [
768 'contact_type' => $params['contact_type'] ?? NULL,
769 'greeting_type' => 'email_greeting',
770 ];
771 $emailGreetings = CRM_Core_PseudoConstant::greeting($emailGreetingFilter);
772 $params['email_greeting'] = $emailGreetings[$params['email_greeting_id']];
773 }
774 else {
775 $params['email_greeting'] = $values['email_greeting'];
776 }
777
778 return TRUE;
779 }
780
781 if (isset($values['postal_greeting'])) {
782 if (!empty($params['postal_greeting_id'])) {
783 $postalGreetingFilter = [
784 'contact_type' => $params['contact_type'] ?? NULL,
785 'greeting_type' => 'postal_greeting',
786 ];
787 $postalGreetings = CRM_Core_PseudoConstant::greeting($postalGreetingFilter);
788 $params['postal_greeting'] = $postalGreetings[$params['postal_greeting_id']];
789 }
790 else {
791 $params['postal_greeting'] = $values['postal_greeting'];
792 }
793 return TRUE;
794 }
795
796 if (isset($values['addressee'])) {
797 $params['addressee'] = $values['addressee'];
798 return TRUE;
799 }
800
801 if (isset($values['gender'])) {
802 if (!empty($params['gender_id'])) {
803 $genders = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'gender_id');
804 $params['gender'] = $genders[$params['gender_id']];
805 }
806 else {
807 $params['gender'] = $values['gender'];
808 }
809 return TRUE;
810 }
811
812 if (!empty($values['preferred_communication_method'])) {
813 $comm = [];
814 $pcm = array_change_key_case(array_flip(CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'preferred_communication_method')), CASE_LOWER);
815
816 $preffComm = explode(',', $values['preferred_communication_method']);
817 foreach ($preffComm as $v) {
818 $v = strtolower(trim($v));
819 if (array_key_exists($v, $pcm)) {
820 $comm[$pcm[$v]] = 1;
821 }
822 }
823
824 $params['preferred_communication_method'] = $comm;
825 return TRUE;
826 }
827
828 // format the website params.
829 if (!empty($values['url'])) {
830 static $websiteFields;
831 if (!is_array($websiteFields)) {
832 require_once 'CRM/Core/DAO/Website.php';
833 $websiteFields = CRM_Core_DAO_Website::fields();
834 }
835 if (!array_key_exists('website', $params) ||
836 !is_array($params['website'])
837 ) {
838 $params['website'] = [];
839 }
840
841 $websiteCount = count($params['website']);
842 _civicrm_api3_store_values($websiteFields, $values,
843 $params['website'][++$websiteCount]
844 );
845
846 return TRUE;
847 }
848
849 // get the formatted location blocks into params - w/ 3.0 format, CRM-4605
850 if (!empty($values['location_type_id'])) {
851 static $fields = NULL;
852 if ($fields == NULL) {
853 $fields = [];
854 }
855
856 foreach (['Phone', 'Email', 'IM', 'OpenID', 'Phone_Ext'] as $block) {
857 $name = strtolower($block);
858 if (!array_key_exists($name, $values)) {
859 continue;
860 }
861
862 if ($name === 'phone_ext') {
863 $block = 'Phone';
864 }
865
866 // block present in value array.
867 if (!array_key_exists($name, $params) || !is_array($params[$name])) {
868 $params[$name] = [];
869 }
870
871 if (!array_key_exists($block, $fields)) {
872 $className = "CRM_Core_DAO_$block";
873 $fields[$block] =& $className::fields();
874 }
875
876 $blockCnt = count($params[$name]);
877
878 // copy value to dao field name.
879 if ($name == 'im') {
880 $values['name'] = $values[$name];
881 }
882
883 _civicrm_api3_store_values($fields[$block], $values,
884 $params[$name][++$blockCnt]
885 );
886
887 if (empty($params['id']) && ($blockCnt == 1)) {
888 $params[$name][$blockCnt]['is_primary'] = TRUE;
889 }
890
891 // we only process single block at a time.
892 return TRUE;
893 }
894
895 // handle address fields.
896 if (!array_key_exists('address', $params) || !is_array($params['address'])) {
897 $params['address'] = [];
898 }
899
900 $addressCnt = 1;
901 foreach ($params['address'] as $cnt => $addressBlock) {
902 if (CRM_Utils_Array::value('location_type_id', $values) ==
903 CRM_Utils_Array::value('location_type_id', $addressBlock)
904 ) {
905 $addressCnt = $cnt;
906 break;
907 }
908 $addressCnt++;
909 }
910
911 if (!array_key_exists('Address', $fields)) {
912 $fields['Address'] = CRM_Core_DAO_Address::fields();
913 }
914
915 // Note: we doing multiple value formatting here for address custom fields, plus putting into right format.
916 // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving
917 // the address in CRM_Core_BAO_Address::create method
918 if (!empty($values['location_type_id'])) {
919 static $customFields = [];
920 if (empty($customFields)) {
921 $customFields = CRM_Core_BAO_CustomField::getFields('Address');
922 }
923 // make a copy of values, as we going to make changes
924 $newValues = $values;
925 foreach ($values as $key => $val) {
926 $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key);
927 if ($customFieldID && array_key_exists($customFieldID, $customFields)) {
928 // mark an entry in fields array since we want the value of custom field to be copied
929 $fields['Address'][$key] = NULL;
930
931 $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL;
932 if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]) && $val) {
933 $mulValues = explode(',', $val);
934 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
935 $newValues[$key] = [];
936 foreach ($mulValues as $v1) {
937 foreach ($customOption as $v2) {
938 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
939 (strtolower($v2['value']) == strtolower(trim($v1)))
940 ) {
941 if ($htmlType == 'CheckBox') {
942 $newValues[$key][$v2['value']] = 1;
943 }
944 else {
945 $newValues[$key][] = $v2['value'];
946 }
947 }
948 }
949 }
950 }
951 }
952 }
953 // consider new values
954 $values = $newValues;
955 }
956
957 _civicrm_api3_store_values($fields['Address'], $values, $params['address'][$addressCnt]);
958
959 $addressFields = [
960 'county',
961 'country',
962 'state_province',
963 'supplemental_address_1',
964 'supplemental_address_2',
965 'supplemental_address_3',
966 'StateProvince.name',
967 ];
968
969 foreach ($addressFields as $field) {
970 if (array_key_exists($field, $values)) {
971 if (!array_key_exists('address', $params)) {
972 $params['address'] = [];
973 }
974 $params['address'][$addressCnt][$field] = $values[$field];
975 }
976 }
977
978 if ($addressCnt == 1) {
979
980 $params['address'][$addressCnt]['is_primary'] = TRUE;
981 }
982 return TRUE;
983 }
984
985 if (isset($values['note'])) {
986 // add a note field
987 if (!isset($params['note'])) {
988 $params['note'] = [];
989 }
990 $noteBlock = count($params['note']) + 1;
991
992 $params['note'][$noteBlock] = [];
993 if (!isset($fields['Note'])) {
994 $fields['Note'] = CRM_Core_DAO_Note::fields();
995 }
996
997 // get the current logged in civicrm user
998 $session = CRM_Core_Session::singleton();
999 $userID = $session->get('userID');
1000
1001 if ($userID) {
1002 $values['contact_id'] = $userID;
1003 }
1004
1005 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
1006
1007 return TRUE;
1008 }
1009
1010 // Check for custom field values
1011
1012 if (empty($fields['custom'])) {
1013 $fields['custom'] = &CRM_Core_BAO_CustomField::getFields(CRM_Utils_Array::value('contact_type', $values),
1014 FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE
1015 );
1016 }
1017
1018 foreach ($values as $key => $value) {
1019 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
1020 // check if it's a valid custom field id
1021
1022 if (!array_key_exists($customFieldID, $fields['custom'])) {
1023 return civicrm_api3_create_error('Invalid custom field ID');
1024 }
1025 else {
1026 $params[$key] = $value;
1027 }
1028 }
1029 }
1030 }
1031
14b9e069 1032 /**
1033 * Parse a field which could be represented by a label or name value rather than the DB value.
1034 *
9ae10cd7 1035 * We will try to match name first or (per https://lab.civicrm.org/dev/core/issues/1285 if we have an id.
1036 *
1037 * but if not available then see if we have a label that can be converted to a name.
14b9e069 1038 *
1039 * @param string|int|null $submittedValue
1040 * @param array $fieldSpec
1041 * Metadata for the field
1042 *
1043 * @return mixed
1044 */
1045 protected function parsePseudoConstantField($submittedValue, $fieldSpec) {
0b742997
SL
1046 // dev/core#1289 Somehow we have wound up here but the BAO has not been specified in the fieldspec so we need to check this but future us problem, for now lets just return the submittedValue
1047 if (!isset($fieldSpec['bao'])) {
1048 return $submittedValue;
1049 }
14b9e069 1050 /* @var \CRM_Core_DAO $bao */
1051 $bao = $fieldSpec['bao'];
1052 // For historical reasons use validate as context - ie disabled name matches ARE permitted.
1053 $nameOptions = $bao::buildOptions($fieldSpec['name'], 'validate');
9ae10cd7 1054 if (isset($nameOptions[$submittedValue])) {
1055 return $submittedValue;
1056 }
1057 if (in_array($submittedValue, $nameOptions)) {
1058 return array_search($submittedValue, $nameOptions, TRUE);
1059 }
1060
1061 $labelOptions = array_flip($bao::buildOptions($fieldSpec['name'], 'match'));
1062 if (isset($labelOptions[$submittedValue])) {
1063 return array_search($labelOptions[$submittedValue], $nameOptions, TRUE);
14b9e069 1064 }
1065 return '';
1066 }
1067
be40742b
CW
1068 /**
1069 * This is code extracted from 4 places where this exact snippet was being duplicated.
1070 *
1071 * FIXME: Extracting this was a first step, but there's also
1072 * 1. Inconsistency in the way other select options are handled.
1073 * Contribution adds handling for Select/Radio/Autocomplete
1074 * Participant/Activity only handles Select/Radio and misses Autocomplete
1075 * Membership is missing all of it
1076 * 2. Inconsistency with the way this works vs. how it's implemented in Contact import.
1077 *
1078 * @param $customFieldID
1079 * @param $value
1080 * @param $fieldType
1081 * @return array
1082 */
1083 public static function unserializeCustomValue($customFieldID, $value, $fieldType) {
1084 $mulValues = explode(',', $value);
1085 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1086 $values = [];
1087 foreach ($mulValues as $v1) {
1088 foreach ($customOption as $customValueID => $customLabel) {
1089 $customValue = $customLabel['value'];
1090 if ((strtolower(trim($customLabel['label'])) == strtolower(trim($v1))) ||
1091 (strtolower(trim($customValue)) == strtolower(trim($v1)))
1092 ) {
f6fc1b15 1093 $values[] = $customValue;
be40742b
CW
1094 }
1095 }
1096 }
1097 return $values;
1098 }
1099
a8ea3922 1100 /**
1101 * Get the ids of any contacts that match according to the rule.
1102 *
1103 * @param array $formatted
1104 *
1105 * @return array
1106 */
1107 protected function getIdsOfMatchingContacts(array $formatted):array {
7b9d5911
EM
1108 if ($formatted['id'] ?? NULL) {
1109 return [$formatted['id']];
1110 }
1111
a8ea3922 1112 // the call to the deprecated function seems to add no value other that to do an additional
1113 // check for the contact_id & type.
1114 $error = _civicrm_api3_deprecated_duplicate_formatted_contact($formatted);
1115 if (!CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
1116 return [];
1117 }
1118 if (is_array($error['error_message']['params'][0])) {
1119 return $error['error_message']['params'][0];
1120 }
1121 else {
1122 return explode(',', $error['error_message']['params'][0]);
1123 }
1124 }
1125
7e56b830
EM
1126 /**
1127 * Validate that the field requirements are met in the params.
1128 *
1129 * @param array $requiredFields
1130 * @param array $params
1131 * An array of required fields (fieldName => label)
1132 * - note this follows the and / or array nesting we see in permission checks
1133 * eg.
1134 * [
1135 * 'email' => ts('Email'),
1136 * ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')]
1137 * ]
1138 * Means 'email' OR 'first_name AND 'last_name'.
7d2012dc 1139 * @param string $prefixString
7e56b830 1140 *
7d2012dc 1141 * @throws \CRM_Core_Exception Exception thrown if field requirements are not met.
7e56b830 1142 */
7d2012dc 1143 protected function validateRequiredFields(array $requiredFields, array $params, $prefixString): void {
7e56b830
EM
1144 $missingFields = [];
1145 foreach ($requiredFields as $key => $required) {
1146 if (!is_array($required)) {
1147 $importParameter = $params[$key] ?? [];
1148 if (!is_array($importParameter)) {
1149 if (!empty($importParameter)) {
1150 return;
1151 }
1152 }
1153 else {
1154 foreach ($importParameter as $locationValues) {
1155 if (!empty($locationValues[$key])) {
1156 return;
1157 }
1158 }
1159 }
1160
1161 $missingFields[$key] = $required;
1162 }
1163 else {
1164 foreach ($required as $field => $label) {
1165 if (empty($params[$field])) {
1166 $missing[$field] = $label;
1167 }
1168 }
1169 if (empty($missing)) {
1170 return;
1171 }
1172 $missingFields[$key] = implode(' ' . ts('and') . ' ', $missing);
1173 }
1174 }
7d2012dc 1175 throw new CRM_Core_Exception(($prefixString ? ($prefixString . ' ') : '') . ts('Missing required fields:') . ' ' . implode(' ' . ts('OR') . ' ', $missingFields));
7e56b830
EM
1176 }
1177
19f33b09
EM
1178 /**
1179 * Get the field value, transformed by metadata.
1180 *
1181 * @param string $fieldName
1182 * @param string|int $importedValue
1183 * Value as it came in from the datasource.
1184 *
1185 * @return string|array|bool|int
1186 * @throws \API_Exception
1187 */
1188 protected function getTransformedFieldValue(string $fieldName, $importedValue) {
1189 // For now only do gender_id as we need to work through removing duplicate handling
1190 if ($fieldName !== 'gender_id' || empty($importedValue)) {
1191 return $importedValue;
1192 }
1193 return $this->getFieldOptions($fieldName)[$importedValue] ?? 'invalid_import_value';
1194 }
1195
1196 /**
1197 * @param string $fieldName
1198 *
1199 * @return false|array
1200 *
1201 * @throws \API_Exception
1202 */
1203 protected function getFieldOptions(string $fieldName) {
1204 return $this->getFieldMetadata($fieldName, TRUE)['options'];
1205 }
1206
1207 /**
1208 * Get the metadata for the field.
1209 *
1210 * @param string $fieldName
1211 * @param bool $loadOptions
1212 *
1213 * @return array
1214 * @throws \API_Exception
1215 */
1216 protected function getFieldMetadata(string $fieldName, bool $loadOptions = FALSE): array {
1217 $fieldMetadata = $this->getImportableFieldsMetadata()[$fieldName];
1218 if ($loadOptions && !isset($fieldMetadata['options'])) {
1219 if (empty($fieldMetadata['pseudoconstant'])) {
1220 $this->importableFieldsMetadata[$fieldName]['options'] = FALSE;
1221 }
1222 else {
1223 $options = civicrm_api4($fieldMetadata['entity'], 'getFields', [
1224 'loadOptions' => ['id', 'name', 'label'],
1225 'where' => [['name', '=', $fieldMetadata['name']]],
1226 'select' => ['options'],
1227 ])->first()['options'];
1228 // We create an array of the possible variants - notably including
1229 // name AND label as either might be used, and capitalisation variants.
1230 $values = [];
1231 foreach ($options as $option) {
1232 $values[$option['id']] = $option['id'];
1233 $values[$option['label']] = $option['id'];
1234 $values[$option['name']] = $option['id'];
1235 $values[strtoupper($option['name'])] = $option['id'];
1236 $values[strtolower($option['name'])] = $option['id'];
1237 $values[strtoupper($option['label'])] = $option['id'];
1238 $values[strtolower($option['label'])] = $option['id'];
1239 }
1240 $this->importableFieldsMetadata[$fieldName]['options'] = $values;
1241 }
1242 return $this->importableFieldsMetadata[$fieldName];
1243 }
1244 return $fieldMetadata;
1245 }
1246
ec3811b1 1247}