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