]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - modules/Import/sources/ImportFile.php
Release 6.5.9
[Github/sugarcrm.git] / modules / Import / sources / ImportFile.php
1 <?php
2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3
4 /*********************************************************************************
5  * SugarCRM Community Edition is a customer relationship management program developed by
6  * SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
7  * 
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.
14  * 
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
18  * details.
19  * 
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
23  * 02110-1301 USA.
24  * 
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.
27  * 
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.
31  * 
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  ********************************************************************************/
38
39 /*********************************************************************************
40
41  * Description: Class to handle processing an import file
42  * Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
43  * All Rights Reserved.
44  ********************************************************************************/
45
46 require_once('modules/Import/CsvAutoDetect.php');
47 require_once('modules/Import/sources/ImportDataSource.php');
48
49 class ImportFile extends ImportDataSource
50 {
51     /**
52      * Stores whether or not we are deleting the import file in the destructor
53      */
54     private $_deleteFile;
55
56     /**
57      * File pointer returned from fopen() call
58      */
59     private $_fp = FALSE;
60
61     /**
62      * True if the csv file has a header row.
63      */
64     private $_hasHeader = FALSE;
65
66     /**
67      * True if the csv file has a header row.
68      */
69     private $_detector = null;
70
71     /**
72      * CSV date format
73      */
74     private $_date_format = false;
75
76     /**
77      * CSV time format
78      */
79     private $_time_format = false;
80
81     /**
82      * The import file map that this import file inherits properties from.
83      */
84     private $_importFile = null;
85
86     /**
87      * Delimiter string we are using (i.e. , or ;)
88      */
89     private $_delimiter;
90
91     /**
92      * Enclosure string we are using (i.e. ' or ")
93      */
94     private $_enclosure;
95     
96     /**
97      * File encoding, used to translate the data into UTF-8 for display and import
98      */
99     private $_encoding;
100
101
102     /**
103      * Constructor
104      *
105      * @param string $filename
106      * @param string $delimiter
107      * @param string $enclosure
108      * @param bool   $deleteFile
109      */
110     public function __construct( $filename, $delimiter  = ',', $enclosure  = '',$deleteFile = true, $checkUploadPath = TRUE )
111     {
112         if ( !is_file($filename) || !is_readable($filename) ) {
113             return false;
114         }
115
116         if ( $checkUploadPath && UploadStream::path($filename) == null )
117         {
118             $GLOBALS['log']->fatal("ImportFile detected attempt to access to the following file not within the sugar upload dir: $filename");
119             return null;
120         }
121
122         // turn on auto-detection of line endings to fix bug #10770
123         ini_set('auto_detect_line_endings', '1');
124
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";
131         }
132         $this->_enclosure  = ( empty($enclosure) ? '' : trim($enclosure) );
133
134         // Autodetect does setFpAfterBOM()
135         $this->_encoding = $this->autoDetectCharacterSet();
136     }
137
138     /**
139      * Remove the BOM (Byte Order Mark) from the beginning of the import row if it exists
140      * @return void
141      */
142     private function setFpAfterBOM()
143     {
144         if($this->_fp === FALSE)
145             return;
146
147         rewind($this->_fp);
148         $bomCheck = fread($this->_fp, 3);
149         if($bomCheck != pack("CCC",0xef,0xbb,0xbf)) {
150             rewind($this->_fp);
151         }
152     }
153     /**
154      * Destructor
155      *
156      * Deletes $_importFile if $_deleteFile is true
157      */
158     public function __destruct()
159     {
160         if ( $this->_deleteFile && $this->fileExists() ) {
161             fclose($this->_fp);
162             //Make sure the file exists before unlinking
163             if(file_exists($this->_sourcename)) {
164                unlink($this->_sourcename);
165             }
166         }
167
168         ini_restore('auto_detect_line_endings');
169     }
170
171     /**
172          * This is needed to prevent unserialize vulnerability
173      */
174     public function __wakeup()
175     {
176         // clean all properties
177         foreach(get_object_vars($this) as $k => $v) {
178             $this->$k = null;
179         }
180         throw new Exception("Not a serializable object");
181     }
182
183     /**
184      * Returns true if the filename given exists and is readable
185      *
186      * @return bool
187      */
188     public function fileExists()
189     {
190         return !$this->_fp ? false : true;
191     }
192
193     /**
194      * Gets the next row from $_importFile
195      *
196      * @return array current row of file
197      */
198     public function getNextRow()
199     {
200         $this->_currentRow = FALSE;
201
202         if (!$this->fileExists())
203         {
204             return false;
205         }
206
207         // explode on delimiter instead if enclosure is an empty string
208         if (empty($this->_enclosure))
209         {
210             $row = explode($this->_delimiter, rtrim(fgets($this->_fp, 8192), "\r\n"));
211             if ($row !== false && !(count($row) == 1 && trim($row[0]) == ''))
212             {
213                 $this->_currentRow = $row;
214             }
215             else
216             {
217                 return false;
218             }
219         }
220         else
221         {
222             $row = fgetcsv($this->_fp, 8192, $this->_delimiter, $this->_enclosure);
223             if ($row !== false && $row != array(null))
224             {
225                 $this->_currentRow = $row;
226             }
227             else
228             {
229                 return false;
230             }
231         }
232         
233         global $locale;
234         foreach ($this->_currentRow as $key => $value)
235         {
236             // If encoding is set, convert all values from it
237             if (!empty($this->_encoding))
238             {
239                 // Convert all values to UTF-8 for display and import purposes
240                 $this->_currentRow[$key] = $locale->translateCharset($value, $this->_encoding);
241             }
242             
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]);
246         }
247         
248         $this->_rowsCount++;
249
250         return $this->_currentRow;
251     }
252
253     /**
254      * Returns the number of fields in the current row
255      *
256      * @return int count of fiels in the current row
257      */
258     public function getFieldCount()
259     {
260         return count($this->_currentRow);
261     }
262
263     /**
264      * Determine the number of lines in this file.
265      *
266      * @return int
267      */
268     public function getNumberOfLinesInfile()
269     {
270         $lineCount = 0;
271
272         if ($this->_fp )
273         {
274             rewind($this->_fp);
275             while( !feof($this->_fp) )
276             {
277                 if( fgets($this->_fp) !== FALSE)
278                     $lineCount++;
279             }
280             //Reset the fp to after the bom if applicable.
281             $this->setFpAfterBOM();
282         }
283
284         return $lineCount;
285     }
286
287     //TODO: Add auto detection for field delim and qualifier properteis.
288     public function autoDetectCSVProperties()
289     {
290         // defaults
291         $this->_delimiter  = ",";
292         $this->_enclosure  = '"';
293
294         $this->_detector = new CsvAutoDetect($this->_sourcename);
295
296         $delimiter = $enclosure = false;
297
298         $ret = $this->_detector->getCsvSettings($delimiter, $enclosure);
299         if ($ret)
300         {
301             $this->_delimiter = $delimiter;
302             $this->_enclosure = $enclosure;
303             return TRUE;
304         }
305         else
306         {
307             return FALSE;
308         }
309     }
310
311     public function getFieldDelimeter()
312     {
313         return $this->_delimiter;
314     }
315
316     public function getFieldEnclosure()
317     {
318         return $this->_enclosure;
319     }
320
321     public function autoDetectCharacterSet()
322     {
323         // If encoding is already detected, just return it
324         if (!empty($this->_encoding))
325         {
326             return $this->_encoding;
327         }
328         
329         // Move file pointer to start
330         $this->setFpAfterBOM();
331         
332         global $locale;
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}";
337
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);
340         
341         // If we are able to detect encoding
342         if (function_exists('mb_detect_encoding'))
343         {
344             // Retrieve a sample of data set
345             $text = '';
346             
347             // Read 10 lines from the file and put them all together in a variable
348             $i = 0;
349             while ($i < 10 && $temp = fgets($this->_fp, 8192))
350             {
351                 $text .= $temp;
352                 $i++;
353             }
354             
355             // If we picked any text, try to detect charset
356             if (strlen($text) > 0)
357             {
358                 $charset_for_import = mb_detect_encoding($text, $detectable_charsets);
359             }
360         }
361         
362         // If we couldn't detect the charset, set it to default export/import charset 
363         if (empty($charset_for_import))
364         {
365             $charset_for_import = $locale->getExportCharset(); 
366         }
367         
368         // Reset the fp to after the bom if applicable.
369         $this->setFpAfterBOM();
370         
371         return $charset_for_import;
372
373     }
374
375     public function getDateFormat()
376     {
377         if ($this->_detector) {
378             $this->_date_format = $this->_detector->getDateFormat();
379         }
380
381         return $this->_date_format;
382     }
383
384     public function getTimeFormat()
385     {
386         if ($this->_detector) {
387             $this->_time_format = $this->_detector->getTimeFormat();
388         }
389
390         return $this->_time_format;
391     }
392
393     public function setHeaderRow($hasHeader)
394     {
395         $this->_hasHeader = $hasHeader;
396     }
397
398     public function hasHeaderRow($autoDetect = TRUE)
399     {
400         if($autoDetect)
401         {
402             if (!isset($_REQUEST['import_module']))
403                 return FALSE;
404
405             $module = $_REQUEST['import_module'];
406
407             $ret = FALSE;
408             $heading = FALSE;
409
410             if ($this->_detector)
411                 $ret = $this->_detector->hasHeader($heading, $module, $this->_encoding);
412
413             if ($ret)
414                 $this->_hasHeader = $heading;
415         }
416         return $this->_hasHeader;
417     }
418
419     public function setImportFileMap($map)
420     {
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)
425         {
426             $this->$k = $map->$v;
427         }
428     }
429
430     //Begin Implementation for SPL's Iterator interface
431     public function key()
432     {
433         return $this->_rowsCount;
434     }
435
436     public function current()
437     {
438         return $this->_currentRow;
439     }
440
441     public function next()
442     {
443         $this->getNextRow();
444     }
445
446     public function valid()
447     {
448         return $this->_currentRow !== FALSE;
449     }
450
451     public function rewind()
452     {
453         $this->setFpAfterBOM();
454         //Load our first row
455         $this->getNextRow();
456     }
457
458     public function getTotalRecordCount()
459     {
460         $totalCount = $this->getNumberOfLinesInfile();
461         if($this->hasHeaderRow(FALSE) && $totalCount > 0)
462         {
463             $totalCount--;
464         }
465         return $totalCount;
466     }
467
468     public function loadDataSet($totalItems = 0)
469     {
470         $currentLine = 0;
471         $this->_dataSet = array();
472         $this->rewind();
473         //If there's a header don't include it.
474         if( $this->hasHeaderRow(FALSE) )
475             $this->next();
476
477         while( $this->valid() &&  $totalItems > count($this->_dataSet) )
478         {
479             if($currentLine >= $this->_offset)
480             {
481                 $this->_dataSet[] = $this->_currentRow;
482             }
483             $this->next();
484             $currentLine++;
485         }
486
487         return $this;
488     }
489
490     public function getHeaderColumns()
491     {
492         $this->rewind();
493         if($this->hasHeaderRow(FALSE))
494             return $this->_currentRow;
495         else
496             return FALSE;
497     }
498
499 }