Create parent for parser classes CRM-11254
[civicrm-core.git] / CRM / Member / Import / Parser.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.3 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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 +--------------------------------------------------------------------+
26*/
27
28/**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2013
32 * $Id$
33 *
34 */
35
36
ec3811b1 37abstract class CRM_Member_Import_Parser extends CRM_Import_Parser {
6a488035
TO
38
39 protected $_fileName;
40
41 /**#@+
42 * @access protected
43 * @var integer
44 */
45
46 /**
47 * imported file size
48 */
49 protected $_fileSize;
50
51 /**
52 * seperator being used
53 */
54 protected $_seperator;
55
56 /**
57 * total number of lines in file
58 */
59 protected $_lineCount;
60
61 /**
62 * total number of non empty lines
63 */
64 protected $_totalCount;
65
66 /**
67 * running total number of valid lines
68 */
69 protected $_validCount;
70
71 /**
72 * running total number of invalid rows
73 */
74 protected $_invalidRowCount;
75
76 /**
77 * maximum number of invalid rows to store
78 */
79 protected $_maxErrorCount;
80
81 /**
82 * array of error lines, bounded by MAX_ERROR
83 */
84 protected $_errors;
85
86 /**
87 * total number of conflict lines
88 */
89 protected $_conflictCount;
90
91 /**
92 * array of conflict lines
93 */
94 protected $_conflicts;
95
96 /**
97 * total number of duplicate (from database) lines
98 */
99 protected $_duplicateCount;
100
101 /**
102 * array of duplicate lines
103 */
104 protected $_duplicates;
105
106 /**
107 * running total number of warnings
108 */
109 protected $_warningCount;
110
111 /**
112 * maximum number of warnings to store
113 */
114 protected $_maxWarningCount = self::MAX_WARNINGS;
115
116 /**
117 * array of warning lines, bounded by MAX_WARNING
118 */
119 protected $_warnings;
120
121 /**
122 * array of all the fields that could potentially be part
123 * of this import process
124 * @var array
125 */
126 protected $_fields;
127
128 /**
129 * array of the fields that are actually part of the import process
130 * the position in the array also dictates their position in the import
131 * file
132 * @var array
133 */
134 protected $_activeFields;
135
136 /**
137 * cache the count of active fields
138 *
139 * @var int
140 */
141 protected $_activeFieldCount;
142
143 /**
144 * maximum number of non-empty/comment lines to process
145 *
146 * @var int
147 */
148 protected $_maxLinesToProcess;
149
150 /**
151 * cache of preview rows
152 *
153 * @var array
154 */
155 protected $_rows;
156
157 /**
158 * filename of error data
159 *
160 * @var string
161 */
162 protected $_errorFileName;
163
164 /**
165 * filename of conflict data
166 *
167 * @var string
168 */
169 protected $_conflictFileName;
170
171 /**
172 * filename of duplicate data
173 *
174 * @var string
175 */
176 protected $_duplicateFileName;
177
178 /**
179 * whether the file has a column header or not
180 *
181 * @var boolean
182 */
183 protected $_haveColumnHeader;
184
185 /**
186 * contact type
187 *
188 * @var int
189 */
190
191 public $_contactType;
192 function __construct() {
193 $this->_maxLinesToProcess = 0;
194 $this->_maxErrorCount = self::MAX_ERRORS;
195 }
196
197 abstract function init();
198 function run($fileName,
199 $seperator = ',',
200 &$mapper,
201 $skipColumnHeader = FALSE,
202 $mode = self::MODE_PREVIEW,
203 $contactType = self::CONTACT_INDIVIDUAL,
204 $onDuplicate = self::DUPLICATE_SKIP
205 ) {
206 if (!is_array($fileName)) {
207 CRM_Core_Error::fatal();
208 }
209 $fileName = $fileName['name'];
210
211 switch ($contactType) {
212 case self::CONTACT_INDIVIDUAL:
213 $this->_contactType = 'Individual';
214 break;
215
216 case self::CONTACT_HOUSEHOLD:
217 $this->_contactType = 'Household';
218 break;
219
220 case self::CONTACT_ORGANIZATION:
221 $this->_contactType = 'Organization';
222 }
223
224 $this->init();
225
226 $this->_haveColumnHeader = $skipColumnHeader;
227
228 $this->_seperator = $seperator;
229
230 $fd = fopen($fileName, "r");
231 if (!$fd) {
232 return FALSE;
233 }
234
235 $this->_lineCount = $this->_warningCount = 0;
236 $this->_invalidRowCount = $this->_validCount = 0;
237 $this->_totalCount = $this->_conflictCount = 0;
238
239 $this->_errors = array();
240 $this->_warnings = array();
241 $this->_conflicts = array();
242
243 $this->_fileSize = number_format(filesize($fileName) / 1024.0, 2);
244
245 if ($mode == self::MODE_MAPFIELD) {
246 $this->_rows = array();
247 }
248 else {
249 $this->_activeFieldCount = count($this->_activeFields);
250 }
251
252 while (!feof($fd)) {
253 $this->_lineCount++;
254
255 $values = fgetcsv($fd, 8192, $seperator);
256 if (!$values) {
257 continue;
258 }
259
260 self::encloseScrub($values);
261
262 // skip column header if we're not in mapfield mode
263 if ($mode != self::MODE_MAPFIELD && $skipColumnHeader) {
264 $skipColumnHeader = FALSE;
265 continue;
266 }
267
268 /* trim whitespace around the values */
269
270 $empty = TRUE;
271 foreach ($values as $k => $v) {
272 $values[$k] = trim($v, " \t\r\n");
273 }
274 if (CRM_Utils_System::isNull($values)) {
275 continue;
276 }
277
278 $this->_totalCount++;
279
280 if ($mode == self::MODE_MAPFIELD) {
281 $returnCode = $this->mapField($values);
282 }
283 elseif ($mode == self::MODE_PREVIEW) {
284 $returnCode = $this->preview($values);
285 }
286 elseif ($mode == self::MODE_SUMMARY) {
287 $returnCode = $this->summary($values);
288 }
289 elseif ($mode == self::MODE_IMPORT) {
290 $returnCode = $this->import($onDuplicate, $values);
291 }
292 else {
293 $returnCode = self::ERROR;
294 }
295
296 // note that a line could be valid but still produce a warning
297 if ($returnCode & self::VALID) {
298 $this->_validCount++;
299 if ($mode == self::MODE_MAPFIELD) {
300 $this->_rows[] = $values;
301 $this->_activeFieldCount = max($this->_activeFieldCount, count($values));
302 }
303 }
304
305 if ($returnCode & self::WARNING) {
306 $this->_warningCount++;
307 if ($this->_warningCount < $this->_maxWarningCount) {
308 $this->_warningCount[] = $line;
309 }
310 }
311
312 if ($returnCode & self::ERROR) {
313 $this->_invalidRowCount++;
314 if ($this->_invalidRowCount < $this->_maxErrorCount) {
315 $recordNumber = $this->_lineCount;
316 array_unshift($values, $recordNumber);
317 $this->_errors[] = $values;
318 }
319 }
320
321 if ($returnCode & self::CONFLICT) {
322 $this->_conflictCount++;
323 $recordNumber = $this->_lineCount;
324 array_unshift($values, $recordNumber);
325 $this->_conflicts[] = $values;
326 }
327
328 if ($returnCode & self::DUPLICATE) {
329 if ($returnCode & self::MULTIPLE_DUPE) {
330 /* TODO: multi-dupes should be counted apart from singles
331 * on non-skip action */
332 }
333 $this->_duplicateCount++;
334 $recordNumber = $this->_lineCount;
335 array_unshift($values, $recordNumber);
336 $this->_duplicates[] = $values;
337 if ($onDuplicate != self::DUPLICATE_SKIP) {
338 $this->_validCount++;
339 }
340 }
341
342 // we give the derived class a way of aborting the process
343 // note that the return code could be multiple code or'ed together
344 if ($returnCode & self::STOP) {
345 break;
346 }
347
348 // if we are done processing the maxNumber of lines, break
349 if ($this->_maxLinesToProcess > 0 && $this->_validCount >= $this->_maxLinesToProcess) {
350 break;
351 }
352 }
353
354 fclose($fd);
355
356 if ($mode == self::MODE_PREVIEW || $mode == self::MODE_IMPORT) {
357 $customHeaders = $mapper;
358
359 $customfields = CRM_Core_BAO_CustomField::getFields('Membership');
360 foreach ($customHeaders as $key => $value) {
361 if ($id = CRM_Core_BAO_CustomField::getKeyID($value)) {
362 $customHeaders[$key] = $customfields[$id][0];
363 }
364 }
365 if ($this->_invalidRowCount) {
366 // removed view url for invlaid contacts
367 $headers = array_merge(array(ts('Line Number'),
368 ts('Reason'),
369 ),
370 $customHeaders
371 );
372 $this->_errorFileName = self::errorFileName(self::ERROR);
373
374 self::exportCSV($this->_errorFileName, $headers, $this->_errors);
375 }
376 if ($this->_conflictCount) {
377 $headers = array_merge(array(ts('Line Number'),
378 ts('Reason'),
379 ),
380 $customHeaders
381 );
382 $this->_conflictFileName = self::errorFileName(self::CONFLICT);
383 self::exportCSV($this->_conflictFileName, $headers, $this->_conflicts);
384 }
385 if ($this->_duplicateCount) {
386 $headers = array_merge(array(ts('Line Number'),
387 ts('View Membership URL'),
388 ),
389 $customHeaders
390 );
391
392 $this->_duplicateFileName = self::errorFileName(self::DUPLICATE);
393 self::exportCSV($this->_duplicateFileName, $headers, $this->_duplicates);
394 }
395 }
396 return $this->fini();
397 }
398
399 abstract function mapField(&$values);
400 abstract function preview(&$values);
401 abstract function summary(&$values);
402 abstract function import($onDuplicate, &$values);
403
404 abstract function fini();
405
406 /**
407 * Given a list of the importable field keys that the user has selected
408 * set the active fields array to this list
409 *
410 * @param array mapped array of values
411 *
412 * @return void
413 * @access public
414 */
415 function setActiveFields($fieldKeys) {
416 $this->_activeFieldCount = count($fieldKeys);
417 foreach ($fieldKeys as $key) {
418 if (empty($this->_fields[$key])) {
419 $this->_activeFields[] = new CRM_Member_Import_Field('', ts('- do not import -'));
420 }
421 else {
422 $this->_activeFields[] = clone($this->_fields[$key]);
423 }
424 }
425 }
426
427 /*function setActiveFieldLocationTypes( $elements ) {
428 for ($i = 0; $i < count( $elements ); $i++) {
429 $this->_activeFields[$i]->_hasLocationType = $elements[$i];
430 }
431 }
c77a4757 432
6a488035
TO
433 function setActiveFieldPhoneTypes( $elements ) {
434 for ($i = 0; $i < count( $elements ); $i++) {
435 $this->_activeFields[$i]->_phoneType = $elements[$i];
436 }
437 }*/
438 function setActiveFieldValues($elements, &$erroneousField) {
439 $maxCount = count($elements) < $this->_activeFieldCount ? count($elements) : $this->_activeFieldCount;
440 for ($i = 0; $i < $maxCount; $i++) {
441 $this->_activeFields[$i]->setValue($elements[$i]);
442 }
443
444 // reset all the values that we did not have an equivalent import element
445 for (; $i < $this->_activeFieldCount; $i++) {
446 $this->_activeFields[$i]->resetValue();
447 }
448
449 // now validate the fields and return false if error
450 $valid = self::VALID;
451 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
452 if (!$this->_activeFields[$i]->validate()) {
453 // no need to do any more validation
454 $erroneousField = $i;
455 $valid = self::ERROR;
456 break;
457 }
458 }
459 return $valid;
460 }
461
462 /**
463 * function to format the field values for input to the api
464 *
465 * @return array (reference ) associative array of name/value pairs
466 * @access public
467 */
468 function &getActiveFieldParams() {
469 $params = array();
470 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
471 if (isset($this->_activeFields[$i]->_value)
472 && !isset($params[$this->_activeFields[$i]->_name])
473 && !isset($this->_activeFields[$i]->_related)
474 ) {
475
476 $params[$this->_activeFields[$i]->_name] = $this->_activeFields[$i]->_value;
477 }
478 }
479 return $params;
480 }
481
482 function getSelectValues() {
483 $values = array();
484 foreach ($this->_fields as $name => $field) {
485 $values[$name] = $field->_title;
486 }
487 return $values;
488 }
489
490 function getSelectTypes() {
491 $values = array();
492 foreach ($this->_fields as $name => $field) {
493 if (isset($field->_hasLocationType)) {
494 $values[$name] = $field->_hasLocationType;
495 }
496 }
497 return $values;
498 }
499
500 function getHeaderPatterns() {
501 $values = array();
502 foreach ($this->_fields as $name => $field) {
503 if (isset($field->_headerPattern)) {
504 $values[$name] = $field->_headerPattern;
505 }
506 }
507 return $values;
508 }
509
510 function getDataPatterns() {
511 $values = array();
512 foreach ($this->_fields as $name => $field) {
513 $values[$name] = $field->_dataPattern;
514 }
515 return $values;
516 }
517
518 function addField($name, $title, $type = CRM_Utils_Type::T_INT, $headerPattern = '//', $dataPattern = '//') {
519 if (empty($name)) {
520 $this->_fields['doNotImport'] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
521 }
522 else {
523
524 //$tempField = CRM_Contact_BAO_Contact::importableFields('Individual', null );
525 $tempField = CRM_Contact_BAO_Contact::importableFields('All', NULL);
526 if (!array_key_exists($name, $tempField)) {
527 $this->_fields[$name] = new CRM_Member_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
528 }
529 else {
719a6fec 530 $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
6a488035
TO
531 CRM_Utils_Array::value('hasLocationType', $tempField[$name])
532 );
533 }
534 }
535 }
536
537 /**
538 * setter function
539 *
540 * @param int $max
541 *
542 * @return void
543 * @access public
544 */
545 function setMaxLinesToProcess($max) {
546 $this->_maxLinesToProcess = $max;
547 }
548
549 /**
550 * Store parser values
551 *
552 * @param CRM_Core_Session $store
553 *
554 * @return void
555 * @access public
556 */
557 function set($store, $mode = self::MODE_SUMMARY) {
558 $store->set('fileSize', $this->_fileSize);
559 $store->set('lineCount', $this->_lineCount);
560 $store->set('seperator', $this->_seperator);
561 $store->set('fields', $this->getSelectValues());
562 $store->set('fieldTypes', $this->getSelectTypes());
563
564 $store->set('headerPatterns', $this->getHeaderPatterns());
565 $store->set('dataPatterns', $this->getDataPatterns());
566 $store->set('columnCount', $this->_activeFieldCount);
567
568 $store->set('totalRowCount', $this->_totalCount);
569 $store->set('validRowCount', $this->_validCount);
570 $store->set('invalidRowCount', $this->_invalidRowCount);
571 $store->set('conflictRowCount', $this->_conflictCount);
572
573 switch ($this->_contactType) {
574 case 'Individual':
575 $store->set('contactType', CRM_Member_Import_Parser::CONTACT_INDIVIDUAL);
576 break;
577
578 case 'Household':
579 $store->set('contactType', CRM_Member_Import_Parser::CONTACT_HOUSEHOLD);
580 break;
581
582 case 'Organization':
583 $store->set('contactType', CRM_Member_Import_Parser::CONTACT_ORGANIZATION);
584 }
585
586 if ($this->_invalidRowCount) {
587 $store->set('errorsFileName', $this->_errorFileName);
588 }
589 if ($this->_conflictCount) {
590 $store->set('conflictsFileName', $this->_conflictFileName);
591 }
592 if (isset($this->_rows) && !empty($this->_rows)) {
593 $store->set('dataValues', $this->_rows);
594 }
595
596 if ($mode == self::MODE_IMPORT) {
597 $store->set('duplicateRowCount', $this->_duplicateCount);
598 if ($this->_duplicateCount) {
599 $store->set('duplicatesFileName', $this->_duplicateFileName);
600 }
601 }
602 }
603
604 /**
605 * Export data to a CSV file
606 *
607 * @param string $filename
608 * @param array $header
609 * @param data $data
610 *
611 * @return void
612 * @access public
613 */
614 static function exportCSV($fileName, $header, $data) {
615 $output = array();
616 $fd = fopen($fileName, 'w');
617
618 foreach ($header as $key => $value) {
619 $header[$key] = "\"$value\"";
620 }
621 $config = CRM_Core_Config::singleton();
622 $output[] = implode($config->fieldSeparator, $header);
623
624 foreach ($data as $datum) {
625 foreach ($datum as $key => $value) {
626 if (is_array($value)) {
627 foreach ($value[0] as $k1 => $v1) {
628 if ($k1 == 'location_type_id') {
629 continue;
630 }
631 $datum[$k1] = $v1;
632 }
633 }
634 else {
635 $datum[$key] = "\"$value\"";
636 }
637 }
638 $output[] = implode($config->fieldSeparator, $datum);
639 }
640 fwrite($fd, implode("\n", $output));
641 fclose($fd);
642 }
643
644 /**
645 * Remove single-quote enclosures from a value array (row)
646 *
647 * @param array $values
648 * @param string $enclosure
649 *
650 * @return void
651 * @static
652 * @access public
653 */
654 static function encloseScrub(&$values, $enclosure = "'") {
655 if (empty($values)) {
656 return;
657 }
658
659 foreach ($values as $k => $v) {
c77a4757 660 $values[$k] = preg_replace("/^$enclosure(.*)$enclosure$/", '$1', $v);
6a488035
TO
661 }
662 }
663
664 function errorFileName($type) {
719a6fec 665 $fileName = CRM_Contact_Import_Parser::errorFileName($type);
6a488035
TO
666 return $fileName;
667 }
668
669 function saveFileName($type) {
719a6fec 670 $fileName = CRM_Contact_Import_Parser::saveFileName($type);
6a488035
TO
671 return $fileName;
672 }
673}
674