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