]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - modules/Import/sources/ImportFile.php
Release 6.4.0
[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-2011 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     /**
98      * Constructor
99      *
100      * @param string $filename
101      * @param string $delimiter
102      * @param string $enclosure
103      * @param bool   $deleteFile
104      */
105     public function __construct( $filename, $delimiter  = ',', $enclosure  = '',$deleteFile = true, $checkUploadPath = TRUE )
106     {
107         if ( !is_file($filename) || !is_readable($filename) ) {
108             return false;
109         }
110
111         if ( $checkUploadPath && UploadStream::path($filename) == null )
112         {
113             $GLOBALS['log']->fatal("ImportFile detected attempt to access to the following file not within the sugar upload dir: $filename");
114             return null;
115         }
116
117         // turn on auto-detection of line endings to fix bug #10770
118         ini_set('auto_detect_line_endings', '1');
119
120         $this->_fp         = sugar_fopen($filename,'r');
121         $this->_sourcename   = $filename;
122         $this->_deleteFile = $deleteFile;
123         $this->_delimiter  = ( empty($delimiter) ? ',' : $delimiter );
124         if ($this->_delimiter == '\t') {
125             $this->_delimiter = "\t";
126         }
127         $this->_enclosure  = ( empty($enclosure) ? '' : trim($enclosure) );
128         $this->setFpAfterBOM();
129     }
130
131     /**
132      * Remove the BOM (Byte Order Mark) from the beginning of the import row if it exists
133      * @return void
134      */
135     private function setFpAfterBOM()
136     {
137         if($this->_fp === FALSE)
138             return;
139
140         rewind($this->_fp);
141         $bomCheck = fread($this->_fp, 3);
142         if($bomCheck != pack("CCC",0xef,0xbb,0xbf)) {
143             rewind($this->_fp);
144         }
145     }
146     /**
147      * Destructor
148      *
149      * Deletes $_importFile if $_deleteFile is true
150      */
151     public function __destruct()
152     {
153         if ( $this->_deleteFile && $this->fileExists() ) {
154             fclose($this->_fp);
155             //Make sure the file exists before unlinking
156             if(file_exists($this->_sourcename)) {
157                unlink($this->_sourcename);
158             }
159         }
160
161         ini_restore('auto_detect_line_endings');
162     }
163
164     /**
165          * This is needed to prevent unserialize vulnerability
166      */
167     public function __wakeup()
168     {
169         // clean all properties
170         foreach(get_object_vars($this) as $k => $v) {
171             $this->$k = null;
172         }
173         throw new Exception("Not a serializable object");
174     }
175
176     /**
177      * Returns true if the filename given exists and is readable
178      *
179      * @return bool
180      */
181     public function fileExists()
182     {
183         return !$this->_fp ? false : true;
184     }
185
186     /**
187      * Gets the next row from $_importFile
188      *
189      * @return array current row of file
190      */
191     public function getNextRow()
192     {
193         $this->_currentRow = FALSE;
194
195         if (!$this->fileExists())
196             return false;
197
198         // explode on delimiter instead if enclosure is an empty string
199         if ( empty($this->_enclosure) ) {
200             $row = explode($this->_delimiter,rtrim(fgets($this->_fp, 8192),"\r\n"));
201             if ($row !== false && !( count($row) == 1 && trim($row[0]) == '') )
202                 $this->_currentRow = $row;
203             else
204                 return false;
205         }
206         else {
207             $row = fgetcsv($this->_fp, 8192, $this->_delimiter, $this->_enclosure);
208             if ($row !== false && $row != array(null))
209                 $this->_currentRow = $row;
210             else
211                 return false;
212         }
213
214         // Bug 26219 - Convert all line endings to the same style as PHP_EOL
215         foreach ( $this->_currentRow as $key => $value ) {
216             // use preg_replace instead of str_replace as str_replace may cause extra lines on Windows
217             $this->_currentRow[$key] = preg_replace("[\r\n|\n|\r]", PHP_EOL, $value);
218         }
219
220         $this->_rowsCount++;
221
222         return $this->_currentRow;
223     }
224
225     /**
226      * Returns the number of fields in the current row
227      *
228      * @return int count of fiels in the current row
229      */
230     public function getFieldCount()
231     {
232         return count($this->_currentRow);
233     }
234
235     /**
236      * Determine the number of lines in this file.
237      *
238      * @return int
239      */
240     public function getNumberOfLinesInfile()
241     {
242         $lineCount = 0;
243
244         if ($this->_fp )
245         {
246             rewind($this->_fp);
247             while( !feof($this->_fp) )
248             {
249                 if( fgets($this->_fp) !== FALSE)
250                     $lineCount++;
251             }
252             //Reset the fp to after the bom if applicable.
253             $this->setFpAfterBOM();
254         }
255
256         return $lineCount;
257     }
258
259     //TODO: Add auto detection for field delim and qualifier properteis.
260     public function autoDetectCSVProperties()
261     {
262         // defaults
263         $this->_delimiter  = ",";
264         $this->_enclosure  = '"';
265
266         $this->_detector = new CsvAutoDetect($this->_sourcename);
267
268         $delimiter = $enclosure = false;
269
270         $ret = $this->_detector->getCsvSettings($delimiter, $enclosure);
271         if ($ret)
272         {
273             $this->_delimiter = $delimiter;
274             $this->_enclosure = $enclosure;
275             return TRUE;
276         }
277         else
278         {
279             return FALSE;
280         }
281     }
282
283     public function getFieldDelimeter()
284     {
285         return $this->_delimiter;
286     }
287
288     public function getFieldEnclosure()
289     {
290         return $this->_enclosure;
291     }
292
293     public function autoDetectCharacterSet()
294     {
295         global $locale;
296
297         $this->setFpAfterBOM();
298
299         //Retrieve a sample set of data
300         $rows = array();
301
302         $user_charset = $locale->getExportCharset();
303         $system_charset = $locale->default_export_charset;
304         $other_charsets = 'UTF-8, UTF-7, ASCII, CP1252, EUC-JP, SJIS, eucJP-win, SJIS-win, JIS, ISO-2022-JP';
305         $detectable_charsets = "UTF-8, {$user_charset}, {$system_charset}, {$other_charsets}";
306         // Bug 26824 - mb_detect_encoding() thinks CP1252 is IS0-8859-1, so use that instead in the encoding list passed to the function
307         $detectable_charsets = str_replace('CP1252','ISO-8859-1',$detectable_charsets);
308         $charset_for_import = $user_charset; //We will set the default import charset option by user's preference.
309         $able_to_detect = function_exists('mb_detect_encoding');
310         for ( $i = 0; $i < 3; $i++ )
311         {
312             $rows[$i] = $this->getNextRow();
313             if(!empty($rows[$i]) && $able_to_detect)
314             {
315                 foreach($rows[$i] as & $temp_value)
316                 {
317                     $current_charset = mb_detect_encoding($temp_value, $detectable_charsets);
318                     if(!empty($current_charset) && $current_charset != "UTF-8")
319                     {
320                         $temp_value = $locale->translateCharset($temp_value, $current_charset);// we will use utf-8 for displaying the data on the page.
321                         $charset_for_import = $current_charset;
322                         //set the default import charset option according to the current_charset.
323                         //If it is not utf-8, tt may be overwritten by the later one. So the uploaded file should not contain two types of charset($user_charset, $system_charset), and I think this situation will not occur.
324                     }
325                 }
326             }
327         }
328
329         //Reset the fp to after the bom if applicable.
330         $this->setFpAfterBOM();
331
332         return $charset_for_import;
333
334     }
335
336     public function getDateFormat()
337     {
338         if ($this->_detector) {
339             $this->_date_format = $this->_detector->getDateFormat();
340         }
341
342         return $this->_date_format;
343     }
344
345     public function getTimeFormat()
346     {
347         if ($this->_detector) {
348             $this->_time_format = $this->_detector->getTimeFormat();
349         }
350
351         return $this->_time_format;
352     }
353
354     public function setHeaderRow($hasHeader)
355     {
356         $this->_hasHeader = $hasHeader;
357     }
358
359     public function hasHeaderRow($autoDetect = TRUE)
360     {
361         if($autoDetect)
362         {
363             if (!isset($_REQUEST['import_module']))
364                 return FALSE;
365
366             $module = $_REQUEST['import_module'];
367
368             $ret = FALSE;
369             $heading = FALSE;
370
371             if ($this->_detector)
372                 $ret = $this->_detector->hasHeader($heading, $module);
373
374             if ($ret)
375                 $this->_hasHeader = $heading;
376         }
377         return $this->_hasHeader;
378     }
379
380     public function setImportFileMap($map)
381     {
382         $this->_importFile = $map;
383         $importMapProperties = array('_delimiter' => 'delimiter','_enclosure' => 'enclosure', '_hasHeader' => 'has_header');
384         //Inject properties from the import map
385         foreach($importMapProperties as $k => $v)
386         {
387             $this->$k = $map->$v;
388         }
389     }
390
391     //Begin Implementation for SPL's Iterator interface
392     public function key()
393     {
394         return $this->_rowsCount;
395     }
396
397     public function current()
398     {
399         return $this->_currentRow;
400     }
401
402     public function next()
403     {
404         $this->getNextRow();
405     }
406
407     public function valid()
408     {
409         return $this->_currentRow !== FALSE;
410     }
411
412     public function rewind()
413     {
414         $this->setFpAfterBOM();
415         //Load our first row
416         $this->getNextRow();
417     }
418
419     public function getTotalRecordCount()
420     {
421         $totalCount = $this->getNumberOfLinesInfile();
422         if($this->hasHeaderRow(FALSE) && $totalCount > 0)
423         {
424             $totalCount--;
425         }
426         return $totalCount;
427     }
428
429     public function loadDataSet($totalItems = 0)
430     {
431         $currentLine = 0;
432         $this->_dataSet = array();
433         $this->rewind();
434         //If there's a header don't include it.
435         if( $this->hasHeaderRow(FALSE) )
436             $this->next();
437
438         while( $this->valid() &&  $totalItems > count($this->_dataSet) )
439         {
440             if($currentLine >= $this->_offset)
441             {
442                 $this->_dataSet[] = $this->_currentRow;
443             }
444             $this->next();
445             $currentLine++;
446         }
447
448         return $this;
449     }
450
451     public function getHeaderColumns()
452     {
453         $this->rewind();
454         if($this->hasHeaderRow(FALSE))
455             return $this->_currentRow;
456         else
457             return FALSE;
458     }
459
460 }