2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
4 /*********************************************************************************
5 * SugarCRM Community Edition is a customer relationship management program developed by
6 * SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
8 * This program is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU Affero General Public License version 3 as published by the
10 * Free Software Foundation with the addition of the following permission added
11 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
12 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
13 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
15 * This program is distributed in the hope that it will be useful, but WITHOUT
16 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
20 * You should have received a copy of the GNU Affero General Public License along with
21 * this program; if not, see http://www.gnu.org/licenses or write to the Free
22 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
26 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
28 * The interactive user interfaces in modified source and object code versions
29 * of this program must display Appropriate Legal Notices, as required under
30 * Section 5 of the GNU Affero General Public License version 3.
32 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
33 * these Appropriate Legal Notices must retain the display of the "Powered by
34 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
35 * technical reasons, the Appropriate Legal Notices must display the words
36 * "Powered by SugarCRM".
37 ********************************************************************************/
39 /*********************************************************************************
41 * Description: Class to handle processing an import file
42 * Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
43 * All Rights Reserved.
44 ********************************************************************************/
46 require_once('modules/Import/CsvAutoDetect.php');
47 require_once('modules/Import/sources/ImportDataSource.php');
49 class ImportFile extends ImportDataSource
52 * Stores whether or not we are deleting the import file in the destructor
57 * File pointer returned from fopen() call
62 * True if the csv file has a header row.
64 private $_hasHeader = FALSE;
67 * True if the csv file has a header row.
69 private $_detector = null;
74 private $_date_format = false;
79 private $_time_format = false;
82 * The import file map that this import file inherits properties from.
84 private $_importFile = null;
87 * Delimiter string we are using (i.e. , or ;)
92 * Enclosure string we are using (i.e. ' or ")
97 * File encoding, used to translate the data into UTF-8 for display and import
105 * @param string $filename
106 * @param string $delimiter
107 * @param string $enclosure
108 * @param bool $deleteFile
110 public function __construct( $filename, $delimiter = ',', $enclosure = '',$deleteFile = true, $checkUploadPath = TRUE )
112 if ( !is_file($filename) || !is_readable($filename) ) {
116 if ( $checkUploadPath && UploadStream::path($filename) == null )
118 $GLOBALS['log']->fatal("ImportFile detected attempt to access to the following file not within the sugar upload dir: $filename");
122 // turn on auto-detection of line endings to fix bug #10770
123 ini_set('auto_detect_line_endings', '1');
125 $this->_fp = sugar_fopen($filename,'r');
126 $this->_sourcename = $filename;
127 $this->_deleteFile = $deleteFile;
128 $this->_delimiter = ( empty($delimiter) ? ',' : $delimiter );
129 if ($this->_delimiter == '\t') {
130 $this->_delimiter = "\t";
132 $this->_enclosure = ( empty($enclosure) ? '' : trim($enclosure) );
134 // Autodetect does setFpAfterBOM()
135 $this->_encoding = $this->autoDetectCharacterSet();
139 * Remove the BOM (Byte Order Mark) from the beginning of the import row if it exists
142 private function setFpAfterBOM()
144 if($this->_fp === FALSE)
148 $bomCheck = fread($this->_fp, 3);
149 if($bomCheck != pack("CCC",0xef,0xbb,0xbf)) {
156 * Deletes $_importFile if $_deleteFile is true
158 public function __destruct()
160 if ( $this->_deleteFile && $this->fileExists() ) {
162 //Make sure the file exists before unlinking
163 if(file_exists($this->_sourcename)) {
164 unlink($this->_sourcename);
168 ini_restore('auto_detect_line_endings');
172 * This is needed to prevent unserialize vulnerability
174 public function __wakeup()
176 // clean all properties
177 foreach(get_object_vars($this) as $k => $v) {
180 throw new Exception("Not a serializable object");
184 * Returns true if the filename given exists and is readable
188 public function fileExists()
190 return !$this->_fp ? false : true;
194 * Gets the next row from $_importFile
196 * @return array current row of file
198 public function getNextRow()
200 $this->_currentRow = FALSE;
202 if (!$this->fileExists())
207 // explode on delimiter instead if enclosure is an empty string
208 if (empty($this->_enclosure))
210 $row = explode($this->_delimiter, rtrim(fgets($this->_fp, 8192), "\r\n"));
211 if ($row !== false && !(count($row) == 1 && trim($row[0]) == ''))
213 $this->_currentRow = $row;
222 $row = fgetcsv($this->_fp, 8192, $this->_delimiter, $this->_enclosure);
223 if ($row !== false && $row != array(null))
225 $this->_currentRow = $row;
234 foreach ($this->_currentRow as $key => $value)
236 // If encoding is set, convert all values from it
237 if (!empty($this->_encoding))
239 // Convert all values to UTF-8 for display and import purposes
240 $this->_currentRow[$key] = $locale->translateCharset($value, $this->_encoding);
243 // Convert all line endings to the same style as PHP_EOL
244 // Use preg_replace instead of str_replace as str_replace may cause extra lines on Windows
245 $this->_currentRow[$key] = preg_replace("[\r\n|\n|\r]", PHP_EOL, $this->_currentRow[$key]);
250 return $this->_currentRow;
254 * Returns the number of fields in the current row
256 * @return int count of fiels in the current row
258 public function getFieldCount()
260 return count($this->_currentRow);
264 * Determine the number of lines in this file.
268 public function getNumberOfLinesInfile()
275 while( !feof($this->_fp) )
277 if( fgets($this->_fp) !== FALSE)
280 //Reset the fp to after the bom if applicable.
281 $this->setFpAfterBOM();
287 //TODO: Add auto detection for field delim and qualifier properteis.
288 public function autoDetectCSVProperties()
291 $this->_delimiter = ",";
292 $this->_enclosure = '"';
294 $this->_detector = new CsvAutoDetect($this->_sourcename);
296 $delimiter = $enclosure = false;
298 $ret = $this->_detector->getCsvSettings($delimiter, $enclosure);
301 $this->_delimiter = $delimiter;
302 $this->_enclosure = $enclosure;
311 public function getFieldDelimeter()
313 return $this->_delimiter;
316 public function getFieldEnclosure()
318 return $this->_enclosure;
321 public function autoDetectCharacterSet()
323 // If encoding is already detected, just return it
324 if (!empty($this->_encoding))
326 return $this->_encoding;
329 // Move file pointer to start
330 $this->setFpAfterBOM();
333 $user_charset = $locale->getExportCharset();
334 $system_charset = $locale->default_export_charset;
335 $other_charsets = 'UTF-8, UTF-7, ASCII, CP1252, EUC-JP, SJIS, eucJP-win, SJIS-win, JIS, ISO-2022-JP';
336 $detectable_charsets = "UTF-8, {$user_charset}, {$system_charset}, {$other_charsets}";
338 // Bug 26824 - mb_detect_encoding() thinks CP1252 is IS0-8859-1, so use that instead in the encoding list passed to the function
339 $detectable_charsets = str_replace('CP1252', 'ISO-8859-1', $detectable_charsets);
341 // If we are able to detect encoding
342 if (function_exists('mb_detect_encoding'))
344 // Retrieve a sample of data set
347 // Read 10 lines from the file and put them all together in a variable
349 while ($i < 10 && $temp = fgets($this->_fp, 8192))
355 // If we picked any text, try to detect charset
356 if (strlen($text) > 0)
358 $charset_for_import = mb_detect_encoding($text, $detectable_charsets);
362 // If we couldn't detect the charset, set it to default export/import charset
363 if (empty($charset_for_import))
365 $charset_for_import = $locale->getExportCharset();
368 // Reset the fp to after the bom if applicable.
369 $this->setFpAfterBOM();
371 return $charset_for_import;
375 public function getDateFormat()
377 if ($this->_detector) {
378 $this->_date_format = $this->_detector->getDateFormat();
381 return $this->_date_format;
384 public function getTimeFormat()
386 if ($this->_detector) {
387 $this->_time_format = $this->_detector->getTimeFormat();
390 return $this->_time_format;
393 public function setHeaderRow($hasHeader)
395 $this->_hasHeader = $hasHeader;
398 public function hasHeaderRow($autoDetect = TRUE)
402 if (!isset($_REQUEST['import_module']))
405 $module = $_REQUEST['import_module'];
410 if ($this->_detector)
411 $ret = $this->_detector->hasHeader($heading, $module, $this->_encoding);
414 $this->_hasHeader = $heading;
416 return $this->_hasHeader;
419 public function setImportFileMap($map)
421 $this->_importFile = $map;
422 $importMapProperties = array('_delimiter' => 'delimiter','_enclosure' => 'enclosure', '_hasHeader' => 'has_header');
423 //Inject properties from the import map
424 foreach($importMapProperties as $k => $v)
426 $this->$k = $map->$v;
430 //Begin Implementation for SPL's Iterator interface
431 public function key()
433 return $this->_rowsCount;
436 public function current()
438 return $this->_currentRow;
441 public function next()
446 public function valid()
448 return $this->_currentRow !== FALSE;
451 public function rewind()
453 $this->setFpAfterBOM();
458 public function getTotalRecordCount()
460 $totalCount = $this->getNumberOfLinesInfile();
461 if($this->hasHeaderRow(FALSE) && $totalCount > 0)
468 public function loadDataSet($totalItems = 0)
471 $this->_dataSet = array();
473 //If there's a header don't include it.
474 if( $this->hasHeaderRow(FALSE) )
477 while( $this->valid() && $totalItems > count($this->_dataSet) )
479 if($currentLine >= $this->_offset)
481 $this->_dataSet[] = $this->_currentRow;
490 public function getHeaderColumns()
493 if($this->hasHeaderRow(FALSE))
494 return $this->_currentRow;