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