[Import][REf] Cleanup required field checking
[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 */
7da04cde 38 const DUPLICATE_SKIP = 1, DUPLICATE_REPLACE = 2, 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
515 *
516 * @return void
517 * @throws \CRM_Core_Exception
518 */
519 protected function validateRequiredContactFields(string $contactType, array $params, bool $isPermitExistingMatchFields = TRUE): void {
520 if (!empty($params['id'])) {
521 return;
522 }
523 $requiredFields = [
524 'Individual' => [
525 'first_name_last_name' => ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')],
526 'email' => ts('Email Address'),
527 ],
528 'Organization' => ['organization_name' => ts('Organization Name')],
529 'Household' => ['household_name' => ts('Household Name')],
530 ][$contactType];
531 if ($isPermitExistingMatchFields) {
532 $requiredFields['external_identifier'] = ts('External Identifier');
533 // Historically just an email has been accepted as it is 'usually good enough'
534 // for a dedupe rule look up - but really this is a stand in for
535 // whatever is needed to find an existing matching contact using the
536 // specified dedupe rule (or the default Unsupervised if not specified).
537 $requiredFields['email'] = ts('Email Address');
538 }
539 $this->validateRequiredFields($requiredFields, $params);
540 }
541
69a4c20a 542 /**
fe482240 543 * Determines the file extension based on error code.
69a4c20a 544 *
f54e87d9 545 * @var int $type error code constant
69a4c20a 546 * @return string
69a4c20a 547 */
00be9182 548 public static function errorFileName($type) {
69a4c20a
CW
549 $fileName = NULL;
550 if (empty($type)) {
551 return $fileName;
552 }
553
554 $config = CRM_Core_Config::singleton();
555 $fileName = $config->uploadDir . "sqlImport";
556 switch ($type) {
557 case self::ERROR:
558 $fileName .= '.errors';
559 break;
560
69a4c20a
CW
561 case self::DUPLICATE:
562 $fileName .= '.duplicates';
563 break;
564
565 case self::NO_MATCH:
566 $fileName .= '.mismatch';
567 break;
568
569 case self::UNPARSED_ADDRESS_WARNING:
570 $fileName .= '.unparsedAddress';
571 break;
572 }
573
574 return $fileName;
575 }
576
577 /**
fe482240 578 * Determines the file name based on error code.
69a4c20a
CW
579 *
580 * @var $type error code constant
581 * @return string
69a4c20a 582 */
00be9182 583 public static function saveFileName($type) {
69a4c20a
CW
584 $fileName = NULL;
585 if (empty($type)) {
586 return $fileName;
587 }
588 switch ($type) {
589 case self::ERROR:
590 $fileName = 'Import_Errors.csv';
591 break;
592
69a4c20a
CW
593 case self::DUPLICATE:
594 $fileName = 'Import_Duplicates.csv';
595 break;
596
597 case self::NO_MATCH:
598 $fileName = 'Import_Mismatch.csv';
599 break;
600
601 case self::UNPARSED_ADDRESS_WARNING:
602 $fileName = 'Import_Unparsed_Address.csv';
603 break;
604 }
605
606 return $fileName;
607 }
608
56316747 609 /**
610 * Check if contact is a duplicate .
611 *
612 * @param array $formatValues
613 *
614 * @return array
615 */
616 protected function checkContactDuplicate(&$formatValues) {
617 //retrieve contact id using contact dedupe rule
95519b12 618 $formatValues['contact_type'] = $formatValues['contact_type'] ?? $this->_contactType;
56316747 619 $formatValues['version'] = 3;
620 require_once 'CRM/Utils/DeprecatedUtils.php';
bd7c6219 621 $params = $formatValues;
622 static $cIndieFields = NULL;
623 static $defaultLocationId = NULL;
624
625 $contactType = $params['contact_type'];
626 if ($cIndieFields == NULL) {
627 $cTempIndieFields = CRM_Contact_BAO_Contact::importableFields($contactType);
628 $cIndieFields = $cTempIndieFields;
629
630 $defaultLocation = CRM_Core_BAO_LocationType::getDefault();
631
632 // set the value to default location id else set to 1
633 if (!$defaultLocationId = (int) $defaultLocation->id) {
634 $defaultLocationId = 1;
635 }
636 }
637
638 $locationFields = CRM_Contact_BAO_Query::$_locationSpecificFields;
639
640 $contactFormatted = [];
641 foreach ($params as $key => $field) {
642 if ($field == NULL || $field === '') {
643 continue;
644 }
645 // CRM-17040, Considering only primary contact when importing contributions. So contribution inserts into primary contact
646 // instead of soft credit contact.
647 if (is_array($field) && $key != "soft_credit") {
648 foreach ($field as $value) {
649 $break = FALSE;
650 if (is_array($value)) {
651 foreach ($value as $name => $testForEmpty) {
652 if ($name !== 'phone_type' &&
653 ($testForEmpty === '' || $testForEmpty == NULL)
654 ) {
655 $break = TRUE;
656 break;
657 }
658 }
659 }
660 else {
661 $break = TRUE;
662 }
663 if (!$break) {
f8909307 664 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
bd7c6219 665 }
666 }
667 continue;
668 }
669
670 $value = [$key => $field];
671
672 // check if location related field, then we need to add primary location type
673 if (in_array($key, $locationFields)) {
674 $value['location_type_id'] = $defaultLocationId;
675 }
676 elseif (array_key_exists($key, $cIndieFields)) {
677 $value['contact_type'] = $contactType;
678 }
679
f8909307 680 $this->_civicrm_api3_deprecated_add_formatted_param($value, $contactFormatted);
bd7c6219 681 }
682
683 $contactFormatted['contact_type'] = $contactType;
684
685 return _civicrm_api3_deprecated_duplicate_formatted_contact($contactFormatted);
56316747 686 }
687
f8909307
EM
688 /**
689 * This function adds the contact variable in $values to the
690 * parameter list $params. For most cases, $values should have length 1. If
691 * the variable being added is a child of Location, a location_type_id must
692 * also be included. If it is a child of phone, a phone_type must be included.
693 *
694 * @param array $values
695 * The variable(s) to be added.
696 * @param array $params
697 * The structured parameter list.
698 *
699 * @return bool|CRM_Utils_Error
700 */
701 private function _civicrm_api3_deprecated_add_formatted_param(&$values, &$params) {
702 // @todo - like most functions in import ... most of this is cruft....
703 // Crawl through the possible classes:
704 // Contact
705 // Individual
706 // Household
707 // Organization
708 // Location
709 // Address
710 // Email
711 // Phone
712 // IM
713 // Note
714 // Custom
715
716 // Cache the various object fields
717 static $fields = NULL;
718
719 if ($fields == NULL) {
720 $fields = [];
721 }
722
723 // first add core contact values since for other Civi modules they are not added
724 require_once 'CRM/Contact/BAO/Contact.php';
725 $contactFields = CRM_Contact_DAO_Contact::fields();
726 _civicrm_api3_store_values($contactFields, $values, $params);
727
728 if (isset($values['contact_type'])) {
729 // we're an individual/household/org property
730
731 $fields[$values['contact_type']] = CRM_Contact_DAO_Contact::fields();
732
733 _civicrm_api3_store_values($fields[$values['contact_type']], $values, $params);
734 return TRUE;
735 }
736
737 if (isset($values['individual_prefix'])) {
738 if (!empty($params['prefix_id'])) {
739 $prefixes = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'prefix_id');
740 $params['prefix'] = $prefixes[$params['prefix_id']];
741 }
742 else {
743 $params['prefix'] = $values['individual_prefix'];
744 }
745 return TRUE;
746 }
747
748 if (isset($values['individual_suffix'])) {
749 if (!empty($params['suffix_id'])) {
750 $suffixes = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'suffix_id');
751 $params['suffix'] = $suffixes[$params['suffix_id']];
752 }
753 else {
754 $params['suffix'] = $values['individual_suffix'];
755 }
756 return TRUE;
757 }
758
759 // CRM-4575
760 if (isset($values['email_greeting'])) {
761 if (!empty($params['email_greeting_id'])) {
762 $emailGreetingFilter = [
763 'contact_type' => $params['contact_type'] ?? NULL,
764 'greeting_type' => 'email_greeting',
765 ];
766 $emailGreetings = CRM_Core_PseudoConstant::greeting($emailGreetingFilter);
767 $params['email_greeting'] = $emailGreetings[$params['email_greeting_id']];
768 }
769 else {
770 $params['email_greeting'] = $values['email_greeting'];
771 }
772
773 return TRUE;
774 }
775
776 if (isset($values['postal_greeting'])) {
777 if (!empty($params['postal_greeting_id'])) {
778 $postalGreetingFilter = [
779 'contact_type' => $params['contact_type'] ?? NULL,
780 'greeting_type' => 'postal_greeting',
781 ];
782 $postalGreetings = CRM_Core_PseudoConstant::greeting($postalGreetingFilter);
783 $params['postal_greeting'] = $postalGreetings[$params['postal_greeting_id']];
784 }
785 else {
786 $params['postal_greeting'] = $values['postal_greeting'];
787 }
788 return TRUE;
789 }
790
791 if (isset($values['addressee'])) {
792 $params['addressee'] = $values['addressee'];
793 return TRUE;
794 }
795
796 if (isset($values['gender'])) {
797 if (!empty($params['gender_id'])) {
798 $genders = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'gender_id');
799 $params['gender'] = $genders[$params['gender_id']];
800 }
801 else {
802 $params['gender'] = $values['gender'];
803 }
804 return TRUE;
805 }
806
807 if (!empty($values['preferred_communication_method'])) {
808 $comm = [];
809 $pcm = array_change_key_case(array_flip(CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'preferred_communication_method')), CASE_LOWER);
810
811 $preffComm = explode(',', $values['preferred_communication_method']);
812 foreach ($preffComm as $v) {
813 $v = strtolower(trim($v));
814 if (array_key_exists($v, $pcm)) {
815 $comm[$pcm[$v]] = 1;
816 }
817 }
818
819 $params['preferred_communication_method'] = $comm;
820 return TRUE;
821 }
822
823 // format the website params.
824 if (!empty($values['url'])) {
825 static $websiteFields;
826 if (!is_array($websiteFields)) {
827 require_once 'CRM/Core/DAO/Website.php';
828 $websiteFields = CRM_Core_DAO_Website::fields();
829 }
830 if (!array_key_exists('website', $params) ||
831 !is_array($params['website'])
832 ) {
833 $params['website'] = [];
834 }
835
836 $websiteCount = count($params['website']);
837 _civicrm_api3_store_values($websiteFields, $values,
838 $params['website'][++$websiteCount]
839 );
840
841 return TRUE;
842 }
843
844 // get the formatted location blocks into params - w/ 3.0 format, CRM-4605
845 if (!empty($values['location_type_id'])) {
846 static $fields = NULL;
847 if ($fields == NULL) {
848 $fields = [];
849 }
850
851 foreach (['Phone', 'Email', 'IM', 'OpenID', 'Phone_Ext'] as $block) {
852 $name = strtolower($block);
853 if (!array_key_exists($name, $values)) {
854 continue;
855 }
856
857 if ($name === 'phone_ext') {
858 $block = 'Phone';
859 }
860
861 // block present in value array.
862 if (!array_key_exists($name, $params) || !is_array($params[$name])) {
863 $params[$name] = [];
864 }
865
866 if (!array_key_exists($block, $fields)) {
867 $className = "CRM_Core_DAO_$block";
868 $fields[$block] =& $className::fields();
869 }
870
871 $blockCnt = count($params[$name]);
872
873 // copy value to dao field name.
874 if ($name == 'im') {
875 $values['name'] = $values[$name];
876 }
877
878 _civicrm_api3_store_values($fields[$block], $values,
879 $params[$name][++$blockCnt]
880 );
881
882 if (empty($params['id']) && ($blockCnt == 1)) {
883 $params[$name][$blockCnt]['is_primary'] = TRUE;
884 }
885
886 // we only process single block at a time.
887 return TRUE;
888 }
889
890 // handle address fields.
891 if (!array_key_exists('address', $params) || !is_array($params['address'])) {
892 $params['address'] = [];
893 }
894
895 $addressCnt = 1;
896 foreach ($params['address'] as $cnt => $addressBlock) {
897 if (CRM_Utils_Array::value('location_type_id', $values) ==
898 CRM_Utils_Array::value('location_type_id', $addressBlock)
899 ) {
900 $addressCnt = $cnt;
901 break;
902 }
903 $addressCnt++;
904 }
905
906 if (!array_key_exists('Address', $fields)) {
907 $fields['Address'] = CRM_Core_DAO_Address::fields();
908 }
909
910 // Note: we doing multiple value formatting here for address custom fields, plus putting into right format.
911 // The actual formatting (like date, country ..etc) for address custom fields is taken care of while saving
912 // the address in CRM_Core_BAO_Address::create method
913 if (!empty($values['location_type_id'])) {
914 static $customFields = [];
915 if (empty($customFields)) {
916 $customFields = CRM_Core_BAO_CustomField::getFields('Address');
917 }
918 // make a copy of values, as we going to make changes
919 $newValues = $values;
920 foreach ($values as $key => $val) {
921 $customFieldID = CRM_Core_BAO_CustomField::getKeyID($key);
922 if ($customFieldID && array_key_exists($customFieldID, $customFields)) {
923 // mark an entry in fields array since we want the value of custom field to be copied
924 $fields['Address'][$key] = NULL;
925
926 $htmlType = $customFields[$customFieldID]['html_type'] ?? NULL;
927 if (CRM_Core_BAO_CustomField::isSerialized($customFields[$customFieldID]) && $val) {
928 $mulValues = explode(',', $val);
929 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
930 $newValues[$key] = [];
931 foreach ($mulValues as $v1) {
932 foreach ($customOption as $v2) {
933 if ((strtolower($v2['label']) == strtolower(trim($v1))) ||
934 (strtolower($v2['value']) == strtolower(trim($v1)))
935 ) {
936 if ($htmlType == 'CheckBox') {
937 $newValues[$key][$v2['value']] = 1;
938 }
939 else {
940 $newValues[$key][] = $v2['value'];
941 }
942 }
943 }
944 }
945 }
946 }
947 }
948 // consider new values
949 $values = $newValues;
950 }
951
952 _civicrm_api3_store_values($fields['Address'], $values, $params['address'][$addressCnt]);
953
954 $addressFields = [
955 'county',
956 'country',
957 'state_province',
958 'supplemental_address_1',
959 'supplemental_address_2',
960 'supplemental_address_3',
961 'StateProvince.name',
962 ];
963
964 foreach ($addressFields as $field) {
965 if (array_key_exists($field, $values)) {
966 if (!array_key_exists('address', $params)) {
967 $params['address'] = [];
968 }
969 $params['address'][$addressCnt][$field] = $values[$field];
970 }
971 }
972
973 if ($addressCnt == 1) {
974
975 $params['address'][$addressCnt]['is_primary'] = TRUE;
976 }
977 return TRUE;
978 }
979
980 if (isset($values['note'])) {
981 // add a note field
982 if (!isset($params['note'])) {
983 $params['note'] = [];
984 }
985 $noteBlock = count($params['note']) + 1;
986
987 $params['note'][$noteBlock] = [];
988 if (!isset($fields['Note'])) {
989 $fields['Note'] = CRM_Core_DAO_Note::fields();
990 }
991
992 // get the current logged in civicrm user
993 $session = CRM_Core_Session::singleton();
994 $userID = $session->get('userID');
995
996 if ($userID) {
997 $values['contact_id'] = $userID;
998 }
999
1000 _civicrm_api3_store_values($fields['Note'], $values, $params['note'][$noteBlock]);
1001
1002 return TRUE;
1003 }
1004
1005 // Check for custom field values
1006
1007 if (empty($fields['custom'])) {
1008 $fields['custom'] = &CRM_Core_BAO_CustomField::getFields(CRM_Utils_Array::value('contact_type', $values),
1009 FALSE, FALSE, NULL, NULL, FALSE, FALSE, FALSE
1010 );
1011 }
1012
1013 foreach ($values as $key => $value) {
1014 if ($customFieldID = CRM_Core_BAO_CustomField::getKeyID($key)) {
1015 // check if it's a valid custom field id
1016
1017 if (!array_key_exists($customFieldID, $fields['custom'])) {
1018 return civicrm_api3_create_error('Invalid custom field ID');
1019 }
1020 else {
1021 $params[$key] = $value;
1022 }
1023 }
1024 }
1025 }
1026
14b9e069 1027 /**
1028 * Parse a field which could be represented by a label or name value rather than the DB value.
1029 *
9ae10cd7 1030 * We will try to match name first or (per https://lab.civicrm.org/dev/core/issues/1285 if we have an id.
1031 *
1032 * but if not available then see if we have a label that can be converted to a name.
14b9e069 1033 *
1034 * @param string|int|null $submittedValue
1035 * @param array $fieldSpec
1036 * Metadata for the field
1037 *
1038 * @return mixed
1039 */
1040 protected function parsePseudoConstantField($submittedValue, $fieldSpec) {
0b742997
SL
1041 // 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
1042 if (!isset($fieldSpec['bao'])) {
1043 return $submittedValue;
1044 }
14b9e069 1045 /* @var \CRM_Core_DAO $bao */
1046 $bao = $fieldSpec['bao'];
1047 // For historical reasons use validate as context - ie disabled name matches ARE permitted.
1048 $nameOptions = $bao::buildOptions($fieldSpec['name'], 'validate');
9ae10cd7 1049 if (isset($nameOptions[$submittedValue])) {
1050 return $submittedValue;
1051 }
1052 if (in_array($submittedValue, $nameOptions)) {
1053 return array_search($submittedValue, $nameOptions, TRUE);
1054 }
1055
1056 $labelOptions = array_flip($bao::buildOptions($fieldSpec['name'], 'match'));
1057 if (isset($labelOptions[$submittedValue])) {
1058 return array_search($labelOptions[$submittedValue], $nameOptions, TRUE);
14b9e069 1059 }
1060 return '';
1061 }
1062
be40742b
CW
1063 /**
1064 * This is code extracted from 4 places where this exact snippet was being duplicated.
1065 *
1066 * FIXME: Extracting this was a first step, but there's also
1067 * 1. Inconsistency in the way other select options are handled.
1068 * Contribution adds handling for Select/Radio/Autocomplete
1069 * Participant/Activity only handles Select/Radio and misses Autocomplete
1070 * Membership is missing all of it
1071 * 2. Inconsistency with the way this works vs. how it's implemented in Contact import.
1072 *
1073 * @param $customFieldID
1074 * @param $value
1075 * @param $fieldType
1076 * @return array
1077 */
1078 public static function unserializeCustomValue($customFieldID, $value, $fieldType) {
1079 $mulValues = explode(',', $value);
1080 $customOption = CRM_Core_BAO_CustomOption::getCustomOption($customFieldID, TRUE);
1081 $values = [];
1082 foreach ($mulValues as $v1) {
1083 foreach ($customOption as $customValueID => $customLabel) {
1084 $customValue = $customLabel['value'];
1085 if ((strtolower(trim($customLabel['label'])) == strtolower(trim($v1))) ||
1086 (strtolower(trim($customValue)) == strtolower(trim($v1)))
1087 ) {
f6fc1b15 1088 $values[] = $customValue;
be40742b
CW
1089 }
1090 }
1091 }
1092 return $values;
1093 }
1094
a8ea3922 1095 /**
1096 * Get the ids of any contacts that match according to the rule.
1097 *
1098 * @param array $formatted
1099 *
1100 * @return array
1101 */
1102 protected function getIdsOfMatchingContacts(array $formatted):array {
1103 // the call to the deprecated function seems to add no value other that to do an additional
1104 // check for the contact_id & type.
1105 $error = _civicrm_api3_deprecated_duplicate_formatted_contact($formatted);
1106 if (!CRM_Core_Error::isAPIError($error, CRM_Core_ERROR::DUPLICATE_CONTACT)) {
1107 return [];
1108 }
1109 if (is_array($error['error_message']['params'][0])) {
1110 return $error['error_message']['params'][0];
1111 }
1112 else {
1113 return explode(',', $error['error_message']['params'][0]);
1114 }
1115 }
1116
7e56b830
EM
1117 /**
1118 * Validate that the field requirements are met in the params.
1119 *
1120 * @param array $requiredFields
1121 * @param array $params
1122 * An array of required fields (fieldName => label)
1123 * - note this follows the and / or array nesting we see in permission checks
1124 * eg.
1125 * [
1126 * 'email' => ts('Email'),
1127 * ['first_name' => ts('First Name'), 'last_name' => ts('Last Name')]
1128 * ]
1129 * Means 'email' OR 'first_name AND 'last_name'.
1130 *
1131 * @throws \CRM_Core_Exception
1132 * Exception thrown if field requirements are not met.
1133 */
1134 protected function validateRequiredFields(array $requiredFields, array $params): void {
1135 $missingFields = [];
1136 foreach ($requiredFields as $key => $required) {
1137 if (!is_array($required)) {
1138 $importParameter = $params[$key] ?? [];
1139 if (!is_array($importParameter)) {
1140 if (!empty($importParameter)) {
1141 return;
1142 }
1143 }
1144 else {
1145 foreach ($importParameter as $locationValues) {
1146 if (!empty($locationValues[$key])) {
1147 return;
1148 }
1149 }
1150 }
1151
1152 $missingFields[$key] = $required;
1153 }
1154 else {
1155 foreach ($required as $field => $label) {
1156 if (empty($params[$field])) {
1157 $missing[$field] = $label;
1158 }
1159 }
1160 if (empty($missing)) {
1161 return;
1162 }
1163 $missingFields[$key] = implode(' ' . ts('and') . ' ', $missing);
1164 }
1165 }
1166 throw new CRM_Core_Exception(ts('Missing required fields:') . ' ' . implode(' ' . ts('OR') . ' ', $missingFields));
1167 }
1168
ec3811b1 1169}