3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2014
35 abstract class CRM_Contact_Import_Parser
extends CRM_Import_Parser
{
37 protected $_tableName;
44 * Total number of lines in file
49 * Running total number of un matched Conact
51 protected $_unMatchCount;
54 * Array of unmatched lines
59 * Total number of contacts with unparsed addresses
61 protected $_unparsedAddressCount;
64 * Filename of mismatch data
68 protected $_misMatchFilemName;
70 protected $_primaryKeyName;
71 protected $_statusFieldName;
81 * Dedupe rule group id to use if set
85 public $_dedupeRuleGroupID = NULL;
88 * @param string $tableName
91 * @param int $contactType
92 * @param string $primaryKeyName
93 * @param string $statusFieldName
94 * @param int $onDuplicate
95 * @param int $statusID
96 * @param null $totalRowCount
97 * @param bool $doGeocodeAddress
99 * @param null $contactSubType
100 * @param int $dedupeRuleGroupID
107 $mode = self
::MODE_PREVIEW
,
108 $contactType = self
::CONTACT_INDIVIDUAL
,
109 $primaryKeyName = '_id',
110 $statusFieldName = '_status',
111 $onDuplicate = self
::DUPLICATE_SKIP
,
113 $totalRowCount = NULL,
114 $doGeocodeAddress = FALSE,
115 $timeout = CRM_Contact_Import_Parser
::DEFAULT_TIMEOUT
,
116 $contactSubType = NULL,
117 $dedupeRuleGroupID = NULL
120 // TODO: Make the timeout actually work
121 $this->_onDuplicate
= $onDuplicate;
122 $this->_dedupeRuleGroupID
= $dedupeRuleGroupID;
124 switch ($contactType) {
125 case CRM_Import_Parser
::CONTACT_INDIVIDUAL
:
126 $this->_contactType
= 'Individual';
129 case CRM_Import_Parser
::CONTACT_HOUSEHOLD
:
130 $this->_contactType
= 'Household';
133 case CRM_Import_Parser
::CONTACT_ORGANIZATION
:
134 $this->_contactType
= 'Organization';
137 $this->_contactSubType
= $contactSubType;
141 $this->_rowCount
= $this->_warningCount
= 0;
142 $this->_invalidRowCount
= $this->_validCount
= 0;
143 $this->_totalCount
= $this->_conflictCount
= 0;
145 $this->_errors
= array();
146 $this->_warnings
= array();
147 $this->_conflicts
= array();
148 $this->_unparsedAddresses
= array();
152 $this->_tableName
= $tableName;
153 $this->_primaryKeyName
= $primaryKeyName;
154 $this->_statusFieldName
= $statusFieldName;
156 if ($mode == self
::MODE_MAPFIELD
) {
157 $this->_rows
= array();
160 $this->_activeFieldCount
= count($this->_activeFields
);
163 if ($mode == self
::MODE_IMPORT
) {
164 //get the key of email field
165 foreach ($mapper as $key => $value) {
166 if (strtolower($value) == 'email') {
176 $config = CRM_Core_Config
::singleton();
177 $statusFile = "{$config->uploadDir}status_{$statusID}.txt";
178 $status = "<div class='description'> " . ts('No processing status reported yet.') . "</div>";
180 //do not force the browser to display the save dialog, CRM-7640
181 $contents = json_encode(array(0, $status));
183 file_put_contents($statusFile, $contents);
185 $startTimestamp = $currTimestamp = $prevTimestamp = time();
188 // get the contents of the temp. import table
189 $query = "SELECT * FROM $tableName";
190 if ($mode == self
::MODE_IMPORT
) {
191 $query .= " WHERE $statusFieldName = 'NEW'";
193 $dao = new CRM_Core_DAO();
194 $db = $dao->getDatabaseConnection();
195 $result = $db->query($query);
197 while ($values = $result->fetchRow(DB_FETCHMODE_ORDERED
)) {
200 /* trim whitespace around the values */
203 foreach ($values as $k => $v) {
204 $values[$k] = trim($v, " \t\r\n");
206 if (CRM_Utils_System
::isNull($values)) {
210 $this->_totalCount++
;
212 if ($mode == self
::MODE_MAPFIELD
) {
213 $returnCode = $this->mapField($values);
215 elseif ($mode == self
::MODE_PREVIEW
) {
216 $returnCode = $this->preview($values);
218 elseif ($mode == self
::MODE_SUMMARY
) {
219 $returnCode = $this->summary($values);
221 elseif ($mode == self
::MODE_IMPORT
) {
222 //print "Running parser in import mode<br/>\n";
223 $returnCode = $this->import($onDuplicate, $values, $doGeocodeAddress);
224 if ($statusID && (($this->_rowCount %
$skip) == 0)) {
225 $currTimestamp = time();
226 $totalTime = ($currTimestamp - $startTimestamp);
227 $time = ($currTimestamp - $prevTimestamp);
228 $recordsLeft = $totalRowCount - $this->_rowCount
;
229 if ($recordsLeft < 0) {
232 $estimatedTime = ($recordsLeft / $skip) * $time;
233 $estMinutes = floor($estimatedTime / 60);
235 if ($estMinutes > 1) {
236 $timeFormatted = $estMinutes . ' ' . ts('minutes') . ' ';
237 $estimatedTime = $estimatedTime - ($estMinutes * 60);
239 $timeFormatted .= round($estimatedTime) . ' ' . ts('seconds');
240 $processedPercent = (int ) (($this->_rowCount
* 100) / $totalRowCount);
241 $statusMsg = ts('%1 of %2 records - %3 remaining',
242 array(1 => $this->_rowCount
, 2 => $totalRowCount, 3 => $timeFormatted)
245 <div class=\"description\">
246 <strong>{$statusMsg}</strong>
250 $contents = json_encode(array($processedPercent, $status));
252 file_put_contents($statusFile, $contents);
254 $prevTimestamp = $currTimestamp;
259 $returnCode = self
::ERROR
;
262 // note that a line could be valid but still produce a warning
263 if ($returnCode & self
::VALID
) {
264 $this->_validCount++
;
265 if ($mode == self
::MODE_MAPFIELD
) {
266 $this->_rows
[] = $values;
267 $this->_activeFieldCount
= max($this->_activeFieldCount
, count($values));
271 if ($returnCode & self
::WARNING
) {
272 $this->_warningCount++
;
273 if ($this->_warningCount
< $this->_maxWarningCount
) {
274 $this->_warningCount
[] = $line;
278 if ($returnCode & self
::ERROR
) {
279 $this->_invalidRowCount++
;
280 if ($this->_invalidRowCount
< $this->_maxErrorCount
) {
281 array_unshift($values, $this->_rowCount
);
282 $this->_errors
[] = $values;
286 if ($returnCode & self
::CONFLICT
) {
287 $this->_conflictCount++
;
288 array_unshift($values, $this->_rowCount
);
289 $this->_conflicts
[] = $values;
292 if ($returnCode & self
::NO_MATCH
) {
293 $this->_unMatchCount++
;
294 array_unshift($values, $this->_rowCount
);
295 $this->_unMatch
[] = $values;
298 if ($returnCode & self
::DUPLICATE
) {
299 if ($returnCode & self
::MULTIPLE_DUPE
) {
300 /* TODO: multi-dupes should be counted apart from singles
301 * on non-skip action */
303 $this->_duplicateCount++
;
304 array_unshift($values, $this->_rowCount
);
305 $this->_duplicates
[] = $values;
306 if ($onDuplicate != self
::DUPLICATE_SKIP
) {
307 $this->_validCount++
;
311 if ($returnCode & self
::UNPARSED_ADDRESS_WARNING
) {
312 $this->_unparsedAddressCount++
;
313 array_unshift($values, $this->_rowCount
);
314 $this->_unparsedAddresses
[] = $values;
316 // we give the derived class a way of aborting the process
317 // note that the return code could be multiple code or'ed together
318 if ($returnCode & self
::STOP
) {
322 // if we are done processing the maxNumber of lines, break
323 if ($this->_maxLinesToProcess
> 0 && $this->_validCount
>= $this->_maxLinesToProcess
) {
327 // clean up memory from dao's
328 CRM_Core_DAO
::freeResult();
330 // see if we've hit our timeout yet
331 /* if ( $the_thing_with_the_stuff ) {
336 if ($mode == self
::MODE_PREVIEW ||
$mode == self
::MODE_IMPORT
) {
337 $customHeaders = $mapper;
339 $customfields = CRM_Core_BAO_CustomField
::getFields($this->_contactType
);
340 foreach ($customHeaders as $key => $value) {
341 if ($id = CRM_Core_BAO_CustomField
::getKeyID($value)) {
342 $customHeaders[$key] = $customfields[$id][0];
346 if ($this->_invalidRowCount
) {
347 // removed view url for invlaid contacts
348 $headers = array_merge(array(
354 $this->_errorFileName
= self
::errorFileName(self
::ERROR
);
355 self
::exportCSV($this->_errorFileName
, $headers, $this->_errors
);
357 if ($this->_conflictCount
) {
358 $headers = array_merge(array(
364 $this->_conflictFileName
= self
::errorFileName(self
::CONFLICT
);
365 self
::exportCSV($this->_conflictFileName
, $headers, $this->_conflicts
);
367 if ($this->_duplicateCount
) {
368 $headers = array_merge(array(
370 ts('View Contact URL'),
375 $this->_duplicateFileName
= self
::errorFileName(self
::DUPLICATE
);
376 self
::exportCSV($this->_duplicateFileName
, $headers, $this->_duplicates
);
378 if ($this->_unMatchCount
) {
379 $headers = array_merge(array(
386 $this->_misMatchFilemName
= self
::errorFileName(self
::NO_MATCH
);
387 self
::exportCSV($this->_misMatchFilemName
, $headers, $this->_unMatch
);
389 if ($this->_unparsedAddressCount
) {
390 $headers = array_merge(array(
392 ts('Contact Edit URL'),
396 $this->_errorFileName
= self
::errorFileName(self
::UNPARSED_ADDRESS_WARNING
);
397 self
::exportCSV($this->_errorFileName
, $headers, $this->_unparsedAddresses
);
400 //echo "$this->_totalCount,$this->_invalidRowCount,$this->_conflictCount,$this->_duplicateCount";
401 return $this->fini();
405 * Given a list of the importable field keys that the user has selected
406 * set the active fields array to this list
408 * @param array mapped array of values
412 public function setActiveFields($fieldKeys) {
413 $this->_activeFieldCount
= count($fieldKeys);
414 foreach ($fieldKeys as $key) {
415 if (empty($this->_fields
[$key])) {
416 $this->_activeFields
[] = new CRM_Contact_Import_Field('', ts('- do not import -'));
419 $this->_activeFields
[] = clone($this->_fields
[$key]);
427 public function setActiveFieldLocationTypes($elements) {
428 for ($i = 0; $i < count($elements); $i++
) {
429 $this->_activeFields
[$i]->_hasLocationType
= $elements[$i];
439 public function setActiveFieldPhoneTypes($elements) {
440 for ($i = 0; $i < count($elements); $i++
) {
441 $this->_activeFields
[$i]->_phoneType
= $elements[$i];
448 public function setActiveFieldWebsiteTypes($elements) {
449 for ($i = 0; $i < count($elements); $i++
) {
450 $this->_activeFields
[$i]->_websiteType
= $elements[$i];
455 * Set IM Service Provider type fields
457 * @param array $elements
458 * IM service provider type ids.
462 public function setActiveFieldImProviders($elements) {
463 for ($i = 0; $i < count($elements); $i++
) {
464 $this->_activeFields
[$i]->_imProvider
= $elements[$i];
471 public function setActiveFieldRelated($elements) {
472 for ($i = 0; $i < count($elements); $i++
) {
473 $this->_activeFields
[$i]->_related
= $elements[$i];
480 public function setActiveFieldRelatedContactType($elements) {
481 for ($i = 0; $i < count($elements); $i++
) {
482 $this->_activeFields
[$i]->_relatedContactType
= $elements[$i];
489 public function setActiveFieldRelatedContactDetails($elements) {
490 for ($i = 0; $i < count($elements); $i++
) {
491 $this->_activeFields
[$i]->_relatedContactDetails
= $elements[$i];
498 public function setActiveFieldRelatedContactLocType($elements) {
499 for ($i = 0; $i < count($elements); $i++
) {
500 $this->_activeFields
[$i]->_relatedContactLocType
= $elements[$i];
507 public function setActiveFieldRelatedContactPhoneType($elements) {
508 for ($i = 0; $i < count($elements); $i++
) {
509 $this->_activeFields
[$i]->_relatedContactPhoneType
= $elements[$i];
516 public function setActiveFieldRelatedContactWebsiteType($elements) {
517 for ($i = 0; $i < count($elements); $i++
) {
518 $this->_activeFields
[$i]->_relatedContactWebsiteType
= $elements[$i];
523 * Set IM Service Provider type fields for related contacts
525 * @param array $elements
526 * IM service provider type ids of related contact.
530 public function setActiveFieldRelatedContactImProvider($elements) {
531 for ($i = 0; $i < count($elements); $i++
) {
532 $this->_activeFields
[$i]->_relatedContactImProvider
= $elements[$i];
537 * Format the field values for input to the api
540 * (reference ) associative array of name/value pairs
542 public function &getActiveFieldParams() {
545 for ($i = 0; $i < $this->_activeFieldCount
; $i++
) {
546 if ($this->_activeFields
[$i]->_name
== 'do_not_import') {
550 if (isset($this->_activeFields
[$i]->_value
)) {
551 if (isset($this->_activeFields
[$i]->_hasLocationType
)) {
552 if (!isset($params[$this->_activeFields
[$i]->_name
])) {
553 $params[$this->_activeFields
[$i]->_name
] = array();
557 $this->_activeFields
[$i]->_name
=>
558 $this->_activeFields
[$i]->_value
,
559 'location_type_id' =>
560 $this->_activeFields
[$i]->_hasLocationType
,
563 if (isset($this->_activeFields
[$i]->_phoneType
)) {
564 $value['phone_type_id'] = $this->_activeFields
[$i]->_phoneType
;
567 // get IM service Provider type id
568 if (isset($this->_activeFields
[$i]->_imProvider
)) {
569 $value['provider_id'] = $this->_activeFields
[$i]->_imProvider
;
572 $params[$this->_activeFields
[$i]->_name
][] = $value;
574 elseif (isset($this->_activeFields
[$i]->_websiteType
)) {
576 $this->_activeFields
[$i]->_name
=> $this->_activeFields
[$i]->_value
,
577 'website_type_id' => $this->_activeFields
[$i]->_websiteType
,
580 $params[$this->_activeFields
[$i]->_name
][] = $value;
583 if (!isset($params[$this->_activeFields
[$i]->_name
])) {
584 if (!isset($this->_activeFields
[$i]->_related
)) {
585 $params[$this->_activeFields
[$i]->_name
] = $this->_activeFields
[$i]->_value
;
589 //minor fix for CRM-4062
590 if (isset($this->_activeFields
[$i]->_related
)) {
591 if (!isset($params[$this->_activeFields
[$i]->_related
])) {
592 $params[$this->_activeFields
[$i]->_related
] = array();
595 if (!isset($params[$this->_activeFields
[$i]->_related
]['contact_type']) && !empty($this->_activeFields
[$i]->_relatedContactType
)) {
596 $params[$this->_activeFields
[$i]->_related
]['contact_type'] = $this->_activeFields
[$i]->_relatedContactType
;
599 if (isset($this->_activeFields
[$i]->_relatedContactLocType
) && !empty($this->_activeFields
[$i]->_value
)) {
600 if (!empty($params[$this->_activeFields
[$i]->_related
][$this->_activeFields
[$i]->_relatedContactDetails
]) &&
601 !is_array($params[$this->_activeFields
[$i]->_related
][$this->_activeFields
[$i]->_relatedContactDetails
])
603 $params[$this->_activeFields
[$i]->_related
][$this->_activeFields
[$i]->_relatedContactDetails
] = array();
606 $this->_activeFields
[$i]->_relatedContactDetails
=> $this->_activeFields
[$i]->_value
,
607 'location_type_id' => $this->_activeFields
[$i]->_relatedContactLocType
,
610 if (isset($this->_activeFields
[$i]->_relatedContactPhoneType
)) {
611 $value['phone_type_id'] = $this->_activeFields
[$i]->_relatedContactPhoneType
;
614 // get IM service Provider type id for related contact
615 if (isset($this->_activeFields
[$i]->_relatedContactImProvider
)) {
616 $value['provider_id'] = $this->_activeFields
[$i]->_relatedContactImProvider
;
619 $params[$this->_activeFields
[$i]->_related
][$this->_activeFields
[$i]->_relatedContactDetails
][] = $value;
621 elseif (isset($this->_activeFields
[$i]->_relatedContactWebsiteType
)) {
622 $params[$this->_activeFields
[$i]->_related
][$this->_activeFields
[$i]->_relatedContactDetails
][] = array(
623 'url' => $this->_activeFields
[$i]->_value
,
624 'website_type_id' => $this->_activeFields
[$i]->_relatedContactWebsiteType
,
628 $params[$this->_activeFields
[$i]->_related
][$this->_activeFields
[$i]->_relatedContactDetails
] = $this->_activeFields
[$i]->_value
;
640 public function getColumnPatterns() {
642 foreach ($this->_fields
as $name => $field) {
643 $values[$name] = $field->_columnPattern
;
649 * @param string $name
652 * @param string $headerPattern
653 * @param string $dataPattern
654 * @param bool $hasLocationType
657 $name, $title, $type = CRM_Utils_Type
::T_INT
,
658 $headerPattern = '//', $dataPattern = '//',
659 $hasLocationType = FALSE
661 $this->_fields
[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
663 $this->_fields
['doNotImport'] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern, $hasLocationType);
668 * Store parser values
670 * @param CRM_Core_Session $store
676 public function set($store, $mode = self
::MODE_SUMMARY
) {
677 $store->set('rowCount', $this->_rowCount
);
678 $store->set('fields', $this->getSelectValues());
679 $store->set('fieldTypes', $this->getSelectTypes());
681 $store->set('columnPatterns', $this->getColumnPatterns());
682 $store->set('dataPatterns', $this->getDataPatterns());
683 $store->set('columnCount', $this->_activeFieldCount
);
685 $store->set('totalRowCount', $this->_totalCount
);
686 $store->set('validRowCount', $this->_validCount
);
687 $store->set('invalidRowCount', $this->_invalidRowCount
);
688 $store->set('conflictRowCount', $this->_conflictCount
);
689 $store->set('unMatchCount', $this->_unMatchCount
);
691 switch ($this->_contactType
) {
693 $store->set('contactType', CRM_Import_Parser
::CONTACT_INDIVIDUAL
);
697 $store->set('contactType', CRM_Import_Parser
::CONTACT_HOUSEHOLD
);
701 $store->set('contactType', CRM_Import_Parser
::CONTACT_ORGANIZATION
);
704 if ($this->_invalidRowCount
) {
705 $store->set('errorsFileName', $this->_errorFileName
);
707 if ($this->_conflictCount
) {
708 $store->set('conflictsFileName', $this->_conflictFileName
);
710 if (isset($this->_rows
) && !empty($this->_rows
)) {
711 $store->set('dataValues', $this->_rows
);
714 if ($this->_unMatchCount
) {
715 $store->set('mismatchFileName', $this->_misMatchFilemName
);
718 if ($mode == self
::MODE_IMPORT
) {
719 $store->set('duplicateRowCount', $this->_duplicateCount
);
720 $store->set('unparsedAddressCount', $this->_unparsedAddressCount
);
721 if ($this->_duplicateCount
) {
722 $store->set('duplicatesFileName', $this->_duplicateFileName
);
724 if ($this->_unparsedAddressCount
) {
725 $store->set('errorsFileName', $this->_errorFileName
);
728 //echo "$this->_totalCount,$this->_invalidRowCount,$this->_conflictCount,$this->_duplicateCount";
732 * Export data to a CSV file
734 * @param string $fileName
735 * @param array $header
740 public static function exportCSV($fileName, $header, $data) {
742 if (file_exists($fileName) && !is_writable($fileName)) {
743 CRM_Core_Error
::movedSiteError($fileName);
745 //hack to remove '_status', '_statusMsg' and '_id' from error file
746 $errorValues = array();
747 $dbRecordStatus = array('IMPORTED', 'ERROR', 'DUPLICATE', 'INVALID', 'NEW');
748 foreach ($data as $rowCount => $rowValues) {
750 foreach ($rowValues as $key => $val) {
751 if (in_array($val, $dbRecordStatus) && $count == (count($rowValues) - 3)) {
754 $errorValues[$rowCount][$key] = $val;
758 $data = $errorValues;
761 $fd = fopen($fileName, 'w');
763 foreach ($header as $key => $value) {
764 $header[$key] = "\"$value\"";
766 $config = CRM_Core_Config
::singleton();
767 $output[] = implode($config->fieldSeparator
, $header);
769 foreach ($data as $datum) {
770 foreach ($datum as $key => $value) {
771 $datum[$key] = "\"$value\"";
773 $output[] = implode($config->fieldSeparator
, $datum);
775 fwrite($fd, implode("\n", $output));
780 * Update the record with PK $id in the import database table
783 * @param array $params
787 public function updateImportRecord($id, &$params) {
788 $statusFieldName = $this->_statusFieldName
;
789 $primaryKeyName = $this->_primaryKeyName
;
791 if ($statusFieldName && $primaryKeyName) {
792 $dao = new CRM_Core_DAO();
793 $db = $dao->getDatabaseConnection();
795 $query = "UPDATE $this->_tableName
796 SET $statusFieldName = ?,
797 ${statusFieldName}Msg = ?
798 WHERE $primaryKeyName = ?";
800 $params[$statusFieldName],
801 CRM_Utils_Array
::value("${statusFieldName}Msg", $params),
805 //print "Running query: $query<br/>With arguments: ".$params[$statusFieldName].", ".$params["${statusFieldName}Msg"].", $id<br/>";
807 $db->query($query, $args);