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