Merge pull request #14937 from eileenmcnaughton/export_cust_test
[civicrm-core.git] / CRM / Import / Parser.php
CommitLineData
ec3811b1
CW
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
ec3811b1 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
ec3811b1
CW
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
ec3811b1
CW
27
28/**
29 *
30 * @package CRM
6b83d5bd 31 * @copyright CiviCRM LLC (c) 2004-2019
ec3811b1 32 */
ec3811b1
CW
33abstract class CRM_Import_Parser {
34 /**
35 * Settings
36 */
ca2057ea 37 const MAX_WARNINGS = 25, DEFAULT_TIMEOUT = 30;
ec3811b1
CW
38
39 /**
40 * Return codes
41 */
7da04cde 42 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
43
44 /**
45 * Parser modes
46 */
7da04cde 47 const MODE_MAPFIELD = 1, MODE_PREVIEW = 2, MODE_SUMMARY = 4, MODE_IMPORT = 8;
ec3811b1
CW
48
49 /**
50 * Codes for duplicate record handling
51 */
7da04cde 52 const DUPLICATE_SKIP = 1, DUPLICATE_REPLACE = 2, DUPLICATE_UPDATE = 4, DUPLICATE_FILL = 8, DUPLICATE_NOCHECK = 16;
ec3811b1
CW
53
54 /**
55 * Contact types
56 */
7da04cde 57 const CONTACT_INDIVIDUAL = 1, CONTACT_HOUSEHOLD = 2, CONTACT_ORGANIZATION = 4;
69a4c20a
CW
58
59
60 /**
100fef9d 61 * Total number of non empty lines
971e129b 62 * @var int
69a4c20a
CW
63 */
64 protected $_totalCount;
65
66 /**
100fef9d 67 * Running total number of valid lines
971e129b 68 * @var int
69a4c20a
CW
69 */
70 protected $_validCount;
71
72 /**
100fef9d 73 * Running total number of invalid rows
971e129b 74 * @var int
69a4c20a
CW
75 */
76 protected $_invalidRowCount;
77
78 /**
100fef9d 79 * Maximum number of non-empty/comment lines to process
69a4c20a
CW
80 *
81 * @var int
82 */
83 protected $_maxLinesToProcess;
84
69a4c20a 85 /**
100fef9d 86 * Array of error lines, bounded by MAX_ERROR
971e129b 87 * @var array
69a4c20a
CW
88 */
89 protected $_errors;
90
91 /**
100fef9d 92 * Total number of conflict lines
971e129b 93 * @var int
69a4c20a
CW
94 */
95 protected $_conflictCount;
96
97 /**
100fef9d 98 * Array of conflict lines
971e129b 99 * @var array
69a4c20a
CW
100 */
101 protected $_conflicts;
102
103 /**
100fef9d 104 * Total number of duplicate (from database) lines
971e129b 105 * @var int
69a4c20a
CW
106 */
107 protected $_duplicateCount;
108
109 /**
100fef9d 110 * Array of duplicate lines
971e129b 111 * @var array
69a4c20a
CW
112 */
113 protected $_duplicates;
114
115 /**
100fef9d 116 * Running total number of warnings
971e129b 117 * @var int
69a4c20a
CW
118 */
119 protected $_warningCount;
120
121 /**
100fef9d 122 * Maximum number of warnings to store
971e129b 123 * @var int
69a4c20a
CW
124 */
125 protected $_maxWarningCount = self::MAX_WARNINGS;
126
127 /**
100fef9d 128 * Array of warning lines, bounded by MAX_WARNING
971e129b 129 * @var array
69a4c20a
CW
130 */
131 protected $_warnings;
132
133 /**
100fef9d 134 * Array of all the fields that could potentially be part
69a4c20a
CW
135 * of this import process
136 * @var array
137 */
138 protected $_fields;
139
140 /**
100fef9d 141 * Array of the fields that are actually part of the import process
69a4c20a
CW
142 * the position in the array also dictates their position in the import
143 * file
144 * @var array
145 */
146 protected $_activeFields;
147
148 /**
100fef9d 149 * Cache the count of active fields
69a4c20a
CW
150 *
151 * @var int
152 */
153 protected $_activeFieldCount;
154
155 /**
100fef9d 156 * Cache of preview rows
69a4c20a
CW
157 *
158 * @var array
159 */
160 protected $_rows;
161
162 /**
100fef9d 163 * Filename of error data
69a4c20a
CW
164 *
165 * @var string
166 */
167 protected $_errorFileName;
168
169 /**
100fef9d 170 * Filename of conflict data
69a4c20a
CW
171 *
172 * @var string
173 */
174 protected $_conflictFileName;
175
176 /**
100fef9d 177 * Filename of duplicate data
69a4c20a
CW
178 *
179 * @var string
180 */
181 protected $_duplicateFileName;
182
183 /**
100fef9d 184 * Contact type
69a4c20a
CW
185 *
186 * @var int
187 */
188 public $_contactType;
e87ff4ce 189 /**
190 * Contact sub-type
191 *
192 * @var int
193 */
194 public $_contactSubType;
69a4c20a
CW
195
196 /**
e87ff4ce 197 * Class constructor.
69a4c20a 198 */
00be9182 199 public function __construct() {
69a4c20a 200 $this->_maxLinesToProcess = 0;
69a4c20a
CW
201 }
202
203 /**
fe482240 204 * Abstract function definitions.
69a4c20a 205 */
bed98343 206 abstract protected function init();
e0ef6999
EM
207
208 /**
209 * @return mixed
210 */
bed98343 211 abstract protected function fini();
e0ef6999
EM
212
213 /**
2b4bc760 214 * Map field.
215 *
216 * @param array $values
e0ef6999
EM
217 *
218 * @return mixed
219 */
bed98343 220 abstract protected function mapField(&$values);
e0ef6999
EM
221
222 /**
2b4bc760 223 * Preview.
224 *
225 * @param array $values
e0ef6999
EM
226 *
227 * @return mixed
228 */
bed98343 229 abstract protected function preview(&$values);
e0ef6999
EM
230
231 /**
232 * @param $values
233 *
234 * @return mixed
235 */
bed98343 236 abstract protected function summary(&$values);
e0ef6999
EM
237
238 /**
239 * @param $onDuplicate
240 * @param $values
241 *
242 * @return mixed
243 */
bed98343 244 abstract protected function import($onDuplicate, &$values);
69a4c20a
CW
245
246 /**
fe482240 247 * Set and validate field values.
69a4c20a 248 *
5a4f6742 249 * @param array $elements
16b10e64 250 * array.
6f69cc11 251 * @param $erroneousField
16b10e64 252 * reference.
77b97be7
EM
253 *
254 * @return int
69a4c20a 255 */
00be9182 256 public function setActiveFieldValues($elements, &$erroneousField) {
69a4c20a
CW
257 $maxCount = count($elements) < $this->_activeFieldCount ? count($elements) : $this->_activeFieldCount;
258 for ($i = 0; $i < $maxCount; $i++) {
259 $this->_activeFields[$i]->setValue($elements[$i]);
260 }
261
262 // reset all the values that we did not have an equivalent import element
263 for (; $i < $this->_activeFieldCount; $i++) {
264 $this->_activeFields[$i]->resetValue();
265 }
266
267 // now validate the fields and return false if error
268 $valid = self::VALID;
269 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
270 if (!$this->_activeFields[$i]->validate()) {
271 // no need to do any more validation
272 $erroneousField = $i;
273 $valid = self::ERROR;
274 break;
275 }
276 }
277 return $valid;
278 }
279
280 /**
fe482240 281 * Format the field values for input to the api.
69a4c20a 282 *
a6c01b45
CW
283 * @return array
284 * (reference) associative array of name/value pairs
69a4c20a 285 */
00be9182 286 public function &getActiveFieldParams() {
be2fb01f 287 $params = [];
69a4c20a
CW
288 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
289 if (isset($this->_activeFields[$i]->_value)
290 && !isset($params[$this->_activeFields[$i]->_name])
291 && !isset($this->_activeFields[$i]->_related)
292 ) {
293
294 $params[$this->_activeFields[$i]->_name] = $this->_activeFields[$i]->_value;
295 }
296 }
297 return $params;
298 }
299
8cebffb2 300 /**
badf5061
JP
301 * Add progress bar to the import process. Calculates time remaining, status etc.
302 *
8cebffb2 303 * @param $statusID
badf5061 304 * status id of the import process saved in $config->uploadDir.
8cebffb2
JP
305 * @param bool $startImport
306 * True when progress bar is to be initiated.
307 * @param $startTimestamp
308 * Initial timstamp when the import was started.
309 * @param $prevTimestamp
310 * Previous timestamp when this function was last called.
311 * @param $totalRowCount
312 * Total number of rows in the import file.
313 *
314 * @return NULL|$currTimestamp
315 */
316 public function progressImport($statusID, $startImport = TRUE, $startTimestamp = NULL, $prevTimestamp = NULL, $totalRowCount = NULL) {
317 $config = CRM_Core_Config::singleton();
318 $statusFile = "{$config->uploadDir}status_{$statusID}.txt";
319
320 if ($startImport) {
321 $status = "<div class='description'>&nbsp; " . ts('No processing status reported yet.') . "</div>";
322 //do not force the browser to display the save dialog, CRM-7640
be2fb01f 323 $contents = json_encode([0, $status]);
8cebffb2
JP
324 file_put_contents($statusFile, $contents);
325 }
326 else {
327 $rowCount = isset($this->_rowCount) ? $this->_rowCount : $this->_lineCount;
328 $currTimestamp = time();
329 $totalTime = ($currTimestamp - $startTimestamp);
330 $time = ($currTimestamp - $prevTimestamp);
331 $recordsLeft = $totalRowCount - $rowCount;
332 if ($recordsLeft < 0) {
333 $recordsLeft = 0;
334 }
335 $estimatedTime = ($recordsLeft / 50) * $time;
336 $estMinutes = floor($estimatedTime / 60);
337 $timeFormatted = '';
338 if ($estMinutes > 1) {
339 $timeFormatted = $estMinutes . ' ' . ts('minutes') . ' ';
340 $estimatedTime = $estimatedTime - ($estMinutes * 60);
341 }
342 $timeFormatted .= round($estimatedTime) . ' ' . ts('seconds');
343 $processedPercent = (int ) (($rowCount * 100) / $totalRowCount);
344 $statusMsg = ts('%1 of %2 records - %3 remaining',
be2fb01f 345 [1 => $rowCount, 2 => $totalRowCount, 3 => $timeFormatted]
8cebffb2
JP
346 );
347 $status = "<div class=\"description\">&nbsp; <strong>{$statusMsg}</strong></div>";
be2fb01f 348 $contents = json_encode([$processedPercent, $status]);
8cebffb2
JP
349
350 file_put_contents($statusFile, $contents);
351 return $currTimestamp;
352 }
353 }
354
e0ef6999
EM
355 /**
356 * @return array
357 */
00be9182 358 public function getSelectValues() {
be2fb01f 359 $values = [];
69a4c20a
CW
360 foreach ($this->_fields as $name => $field) {
361 $values[$name] = $field->_title;
362 }
363 return $values;
364 }
365
e0ef6999
EM
366 /**
367 * @return array
368 */
00be9182 369 public function getSelectTypes() {
be2fb01f 370 $values = [];
69a4c20a
CW
371 foreach ($this->_fields as $name => $field) {
372 if (isset($field->_hasLocationType)) {
373 $values[$name] = $field->_hasLocationType;
374 }
375 }
376 return $values;
377 }
378
e0ef6999
EM
379 /**
380 * @return array
381 */
00be9182 382 public function getHeaderPatterns() {
be2fb01f 383 $values = [];
69a4c20a
CW
384 foreach ($this->_fields as $name => $field) {
385 if (isset($field->_headerPattern)) {
386 $values[$name] = $field->_headerPattern;
387 }
388 }
389 return $values;
390 }
391
e0ef6999
EM
392 /**
393 * @return array
394 */
00be9182 395 public function getDataPatterns() {
be2fb01f 396 $values = [];
69a4c20a
CW
397 foreach ($this->_fields as $name => $field) {
398 $values[$name] = $field->_dataPattern;
399 }
400 return $values;
401 }
402
403 /**
2b4bc760 404 * Remove single-quote enclosures from a value array (row).
69a4c20a
CW
405 *
406 * @param array $values
407 * @param string $enclosure
408 *
409 * @return void
69a4c20a 410 */
00be9182 411 public static function encloseScrub(&$values, $enclosure = "'") {
69a4c20a
CW
412 if (empty($values)) {
413 return;
414 }
415
416 foreach ($values as $k => $v) {
417 $values[$k] = preg_replace("/^$enclosure(.*)$enclosure$/", '$1', $v);
418 }
419 }
420
421 /**
fe482240 422 * Setter function.
69a4c20a
CW
423 *
424 * @param int $max
425 *
426 * @return void
69a4c20a 427 */
00be9182 428 public function setMaxLinesToProcess($max) {
69a4c20a
CW
429 $this->_maxLinesToProcess = $max;
430 }
431
432 /**
fe482240 433 * Determines the file extension based on error code.
69a4c20a
CW
434 *
435 * @var $type error code constant
436 * @return string
69a4c20a 437 */
00be9182 438 public static function errorFileName($type) {
69a4c20a
CW
439 $fileName = NULL;
440 if (empty($type)) {
441 return $fileName;
442 }
443
444 $config = CRM_Core_Config::singleton();
445 $fileName = $config->uploadDir . "sqlImport";
446 switch ($type) {
447 case self::ERROR:
448 $fileName .= '.errors';
449 break;
450
451 case self::CONFLICT:
452 $fileName .= '.conflicts';
453 break;
454
455 case self::DUPLICATE:
456 $fileName .= '.duplicates';
457 break;
458
459 case self::NO_MATCH:
460 $fileName .= '.mismatch';
461 break;
462
463 case self::UNPARSED_ADDRESS_WARNING:
464 $fileName .= '.unparsedAddress';
465 break;
466 }
467
468 return $fileName;
469 }
470
471 /**
fe482240 472 * Determines the file name based on error code.
69a4c20a
CW
473 *
474 * @var $type error code constant
475 * @return string
69a4c20a 476 */
00be9182 477 public static function saveFileName($type) {
69a4c20a
CW
478 $fileName = NULL;
479 if (empty($type)) {
480 return $fileName;
481 }
482 switch ($type) {
483 case self::ERROR:
484 $fileName = 'Import_Errors.csv';
485 break;
486
487 case self::CONFLICT:
488 $fileName = 'Import_Conflicts.csv';
489 break;
490
491 case self::DUPLICATE:
492 $fileName = 'Import_Duplicates.csv';
493 break;
494
495 case self::NO_MATCH:
496 $fileName = 'Import_Mismatch.csv';
497 break;
498
499 case self::UNPARSED_ADDRESS_WARNING:
500 $fileName = 'Import_Unparsed_Address.csv';
501 break;
502 }
503
504 return $fileName;
505 }
506
56316747 507 /**
508 * Check if contact is a duplicate .
509 *
510 * @param array $formatValues
511 *
512 * @return array
513 */
514 protected function checkContactDuplicate(&$formatValues) {
515 //retrieve contact id using contact dedupe rule
516 $formatValues['contact_type'] = $this->_contactType;
517 $formatValues['version'] = 3;
518 require_once 'CRM/Utils/DeprecatedUtils.php';
519 $error = _civicrm_api3_deprecated_check_contact_dedupe($formatValues);
520 return $error;
521 }
522
14b9e069 523 /**
524 * Parse a field which could be represented by a label or name value rather than the DB value.
525 *
526 * We will try to match name first but if not available then see if we have a label that can be converted to a name.
527 *
528 * @param string|int|null $submittedValue
529 * @param array $fieldSpec
530 * Metadata for the field
531 *
532 * @return mixed
533 */
534 protected function parsePseudoConstantField($submittedValue, $fieldSpec) {
535 /* @var \CRM_Core_DAO $bao */
536 $bao = $fieldSpec['bao'];
537 // For historical reasons use validate as context - ie disabled name matches ARE permitted.
538 $nameOptions = $bao::buildOptions($fieldSpec['name'], 'validate');
539 if (!isset($nameOptions[$submittedValue])) {
540 $labelOptions = array_flip($bao::buildOptions($fieldSpec['name'], 'match'));
541 if (isset($labelOptions[$submittedValue])) {
542 return array_search($labelOptions[$submittedValue], $nameOptions, TRUE);
543 }
544 }
545 return '';
546 }
547
ec3811b1 548}