[Test] Remove hard coded it reference
[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
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.
145 * @param array $importableFieldsMetadata
146 */
147 public function setImportableFieldsMetadata(array $importableFieldsMetadata) {
148 $this->importableFieldsMetadata = $importableFieldsMetadata;
149 }
150
69a4c20a 151 /**
100fef9d 152 * Array of the fields that are actually part of the import process
69a4c20a
CW
153 * the position in the array also dictates their position in the import
154 * file
155 * @var array
156 */
157 protected $_activeFields;
158
159 /**
100fef9d 160 * Cache the count of active fields
69a4c20a
CW
161 *
162 * @var int
163 */
164 protected $_activeFieldCount;
165
166 /**
100fef9d 167 * Cache of preview rows
69a4c20a
CW
168 *
169 * @var array
170 */
171 protected $_rows;
172
173 /**
100fef9d 174 * Filename of error data
69a4c20a
CW
175 *
176 * @var string
177 */
178 protected $_errorFileName;
179
180 /**
100fef9d 181 * Filename of conflict data
69a4c20a
CW
182 *
183 * @var string
184 */
185 protected $_conflictFileName;
186
187 /**
100fef9d 188 * Filename of duplicate data
69a4c20a
CW
189 *
190 * @var string
191 */
192 protected $_duplicateFileName;
193
194 /**
100fef9d 195 * Contact type
69a4c20a
CW
196 *
197 * @var int
198 */
199 public $_contactType;
e87ff4ce 200 /**
201 * Contact sub-type
202 *
203 * @var int
204 */
205 public $_contactSubType;
69a4c20a
CW
206
207 /**
e87ff4ce 208 * Class constructor.
69a4c20a 209 */
00be9182 210 public function __construct() {
69a4c20a 211 $this->_maxLinesToProcess = 0;
69a4c20a
CW
212 }
213
214 /**
fe482240 215 * Abstract function definitions.
69a4c20a 216 */
bed98343 217 abstract protected function init();
e0ef6999
EM
218
219 /**
220 * @return mixed
221 */
bed98343 222 abstract protected function fini();
e0ef6999
EM
223
224 /**
2b4bc760 225 * Map field.
226 *
227 * @param array $values
e0ef6999
EM
228 *
229 * @return mixed
230 */
bed98343 231 abstract protected function mapField(&$values);
e0ef6999
EM
232
233 /**
2b4bc760 234 * Preview.
235 *
236 * @param array $values
e0ef6999
EM
237 *
238 * @return mixed
239 */
bed98343 240 abstract protected function preview(&$values);
e0ef6999
EM
241
242 /**
243 * @param $values
244 *
245 * @return mixed
246 */
bed98343 247 abstract protected function summary(&$values);
e0ef6999
EM
248
249 /**
250 * @param $onDuplicate
251 * @param $values
252 *
253 * @return mixed
254 */
bed98343 255 abstract protected function import($onDuplicate, &$values);
69a4c20a
CW
256
257 /**
fe482240 258 * Set and validate field values.
69a4c20a 259 *
5a4f6742 260 * @param array $elements
16b10e64 261 * array.
6f69cc11 262 * @param $erroneousField
16b10e64 263 * reference.
77b97be7
EM
264 *
265 * @return int
69a4c20a 266 */
00be9182 267 public function setActiveFieldValues($elements, &$erroneousField) {
69a4c20a
CW
268 $maxCount = count($elements) < $this->_activeFieldCount ? count($elements) : $this->_activeFieldCount;
269 for ($i = 0; $i < $maxCount; $i++) {
270 $this->_activeFields[$i]->setValue($elements[$i]);
271 }
272
273 // reset all the values that we did not have an equivalent import element
274 for (; $i < $this->_activeFieldCount; $i++) {
275 $this->_activeFields[$i]->resetValue();
276 }
277
278 // now validate the fields and return false if error
279 $valid = self::VALID;
280 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
281 if (!$this->_activeFields[$i]->validate()) {
282 // no need to do any more validation
283 $erroneousField = $i;
284 $valid = self::ERROR;
285 break;
286 }
287 }
288 return $valid;
289 }
290
291 /**
fe482240 292 * Format the field values for input to the api.
69a4c20a 293 *
a6c01b45
CW
294 * @return array
295 * (reference) associative array of name/value pairs
69a4c20a 296 */
00be9182 297 public function &getActiveFieldParams() {
be2fb01f 298 $params = [];
69a4c20a
CW
299 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
300 if (isset($this->_activeFields[$i]->_value)
301 && !isset($params[$this->_activeFields[$i]->_name])
302 && !isset($this->_activeFields[$i]->_related)
303 ) {
304
305 $params[$this->_activeFields[$i]->_name] = $this->_activeFields[$i]->_value;
306 }
307 }
308 return $params;
309 }
310
8cebffb2 311 /**
badf5061
JP
312 * Add progress bar to the import process. Calculates time remaining, status etc.
313 *
8cebffb2 314 * @param $statusID
badf5061 315 * status id of the import process saved in $config->uploadDir.
8cebffb2
JP
316 * @param bool $startImport
317 * True when progress bar is to be initiated.
318 * @param $startTimestamp
319 * Initial timstamp when the import was started.
320 * @param $prevTimestamp
321 * Previous timestamp when this function was last called.
322 * @param $totalRowCount
323 * Total number of rows in the import file.
324 *
325 * @return NULL|$currTimestamp
326 */
327 public function progressImport($statusID, $startImport = TRUE, $startTimestamp = NULL, $prevTimestamp = NULL, $totalRowCount = NULL) {
328 $config = CRM_Core_Config::singleton();
329 $statusFile = "{$config->uploadDir}status_{$statusID}.txt";
330
331 if ($startImport) {
332 $status = "<div class='description'>&nbsp; " . 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 {
338 $rowCount = isset($this->_rowCount) ? $this->_rowCount : $this->_lineCount;
339 $currTimestamp = time();
340 $totalTime = ($currTimestamp - $startTimestamp);
341 $time = ($currTimestamp - $prevTimestamp);
342 $recordsLeft = $totalRowCount - $rowCount;
343 if ($recordsLeft < 0) {
344 $recordsLeft = 0;
345 }
346 $estimatedTime = ($recordsLeft / 50) * $time;
347 $estMinutes = floor($estimatedTime / 60);
348 $timeFormatted = '';
349 if ($estMinutes > 1) {
350 $timeFormatted = $estMinutes . ' ' . ts('minutes') . ' ';
351 $estimatedTime = $estimatedTime - ($estMinutes * 60);
352 }
353 $timeFormatted .= round($estimatedTime) . ' ' . ts('seconds');
354 $processedPercent = (int ) (($rowCount * 100) / $totalRowCount);
355 $statusMsg = ts('%1 of %2 records - %3 remaining',
be2fb01f 356 [1 => $rowCount, 2 => $totalRowCount, 3 => $timeFormatted]
8cebffb2
JP
357 );
358 $status = "<div class=\"description\">&nbsp; <strong>{$statusMsg}</strong></div>";
be2fb01f 359 $contents = json_encode([$processedPercent, $status]);
8cebffb2
JP
360
361 file_put_contents($statusFile, $contents);
362 return $currTimestamp;
363 }
364 }
365
e0ef6999
EM
366 /**
367 * @return array
368 */
00be9182 369 public function getSelectValues() {
be2fb01f 370 $values = [];
69a4c20a
CW
371 foreach ($this->_fields as $name => $field) {
372 $values[$name] = $field->_title;
373 }
374 return $values;
375 }
376
e0ef6999
EM
377 /**
378 * @return array
379 */
00be9182 380 public function getSelectTypes() {
be2fb01f 381 $values = [];
69a4c20a
CW
382 foreach ($this->_fields as $name => $field) {
383 if (isset($field->_hasLocationType)) {
384 $values[$name] = $field->_hasLocationType;
385 }
386 }
387 return $values;
388 }
389
e0ef6999
EM
390 /**
391 * @return array
392 */
00be9182 393 public function getHeaderPatterns() {
be2fb01f 394 $values = [];
69a4c20a
CW
395 foreach ($this->_fields as $name => $field) {
396 if (isset($field->_headerPattern)) {
397 $values[$name] = $field->_headerPattern;
398 }
399 }
400 return $values;
401 }
402
e0ef6999
EM
403 /**
404 * @return array
405 */
00be9182 406 public function getDataPatterns() {
be2fb01f 407 $values = [];
69a4c20a
CW
408 foreach ($this->_fields as $name => $field) {
409 $values[$name] = $field->_dataPattern;
410 }
411 return $values;
412 }
413
414 /**
2b4bc760 415 * Remove single-quote enclosures from a value array (row).
69a4c20a
CW
416 *
417 * @param array $values
418 * @param string $enclosure
419 *
420 * @return void
69a4c20a 421 */
00be9182 422 public static function encloseScrub(&$values, $enclosure = "'") {
69a4c20a
CW
423 if (empty($values)) {
424 return;
425 }
426
427 foreach ($values as $k => $v) {
428 $values[$k] = preg_replace("/^$enclosure(.*)$enclosure$/", '$1', $v);
429 }
430 }
431
432 /**
fe482240 433 * Setter function.
69a4c20a
CW
434 *
435 * @param int $max
436 *
437 * @return void
69a4c20a 438 */
00be9182 439 public function setMaxLinesToProcess($max) {
69a4c20a
CW
440 $this->_maxLinesToProcess = $max;
441 }
442
443 /**
fe482240 444 * Determines the file extension based on error code.
69a4c20a
CW
445 *
446 * @var $type error code constant
447 * @return string
69a4c20a 448 */
00be9182 449 public static function errorFileName($type) {
69a4c20a
CW
450 $fileName = NULL;
451 if (empty($type)) {
452 return $fileName;
453 }
454
455 $config = CRM_Core_Config::singleton();
456 $fileName = $config->uploadDir . "sqlImport";
457 switch ($type) {
458 case self::ERROR:
459 $fileName .= '.errors';
460 break;
461
462 case self::CONFLICT:
463 $fileName .= '.conflicts';
464 break;
465
466 case self::DUPLICATE:
467 $fileName .= '.duplicates';
468 break;
469
470 case self::NO_MATCH:
471 $fileName .= '.mismatch';
472 break;
473
474 case self::UNPARSED_ADDRESS_WARNING:
475 $fileName .= '.unparsedAddress';
476 break;
477 }
478
479 return $fileName;
480 }
481
482 /**
fe482240 483 * Determines the file name based on error code.
69a4c20a
CW
484 *
485 * @var $type error code constant
486 * @return string
69a4c20a 487 */
00be9182 488 public static function saveFileName($type) {
69a4c20a
CW
489 $fileName = NULL;
490 if (empty($type)) {
491 return $fileName;
492 }
493 switch ($type) {
494 case self::ERROR:
495 $fileName = 'Import_Errors.csv';
496 break;
497
498 case self::CONFLICT:
499 $fileName = 'Import_Conflicts.csv';
500 break;
501
502 case self::DUPLICATE:
503 $fileName = 'Import_Duplicates.csv';
504 break;
505
506 case self::NO_MATCH:
507 $fileName = 'Import_Mismatch.csv';
508 break;
509
510 case self::UNPARSED_ADDRESS_WARNING:
511 $fileName = 'Import_Unparsed_Address.csv';
512 break;
513 }
514
515 return $fileName;
516 }
517
56316747 518 /**
519 * Check if contact is a duplicate .
520 *
521 * @param array $formatValues
522 *
523 * @return array
524 */
525 protected function checkContactDuplicate(&$formatValues) {
526 //retrieve contact id using contact dedupe rule
527 $formatValues['contact_type'] = $this->_contactType;
528 $formatValues['version'] = 3;
529 require_once 'CRM/Utils/DeprecatedUtils.php';
530 $error = _civicrm_api3_deprecated_check_contact_dedupe($formatValues);
531 return $error;
532 }
533
14b9e069 534 /**
535 * Parse a field which could be represented by a label or name value rather than the DB value.
536 *
9ae10cd7 537 * We will try to match name first or (per https://lab.civicrm.org/dev/core/issues/1285 if we have an id.
538 *
539 * but if not available then see if we have a label that can be converted to a name.
14b9e069 540 *
541 * @param string|int|null $submittedValue
542 * @param array $fieldSpec
543 * Metadata for the field
544 *
545 * @return mixed
546 */
547 protected function parsePseudoConstantField($submittedValue, $fieldSpec) {
0b742997
SL
548 // 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
549 if (!isset($fieldSpec['bao'])) {
550 return $submittedValue;
551 }
14b9e069 552 /* @var \CRM_Core_DAO $bao */
553 $bao = $fieldSpec['bao'];
554 // For historical reasons use validate as context - ie disabled name matches ARE permitted.
555 $nameOptions = $bao::buildOptions($fieldSpec['name'], 'validate');
9ae10cd7 556 if (isset($nameOptions[$submittedValue])) {
557 return $submittedValue;
558 }
559 if (in_array($submittedValue, $nameOptions)) {
560 return array_search($submittedValue, $nameOptions, TRUE);
561 }
562
563 $labelOptions = array_flip($bao::buildOptions($fieldSpec['name'], 'match'));
564 if (isset($labelOptions[$submittedValue])) {
565 return array_search($labelOptions[$submittedValue], $nameOptions, TRUE);
14b9e069 566 }
567 return '';
568 }
569
ec3811b1 570}