MDL-47686 assign: Fix restoring from a mod_assignment backup.
[moodle.git] / lib / csvlib.class.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * This is a one-line short description of the file
19  *
20  * You can have a rather longer description of the file as well,
21  * if you like, and it can span multiple lines.
22  *
23  * @package    core
24  * @subpackage lib
25  * @copyright  Petr Skoda
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 defined('MOODLE_INTERNAL') || die();
31 /**
32  * Utitily class for importing of CSV files.
33  * @copyright Petr Skoda
34  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  * @package   moodlecore
36  */
37 class csv_import_reader {
38     /**
39      * @var int import identifier
40      */
41     var $_iid;
42     /**
43      * @var string which script imports?
44      */
45     var $_type;
46     /**
47      * @var string|null Null if ok, error msg otherwise
48      */
49     var $_error;
50     /**
51      * @var array cached columns
52      */
53     var $_columns;
54     /**
55      * @var object file handle used during import
56      */
57     var $_fp;
59     /**
60      * Contructor
61      *
62      * @param int $iid import identifier
63      * @param string $type which script imports?
64      */
65     function csv_import_reader($iid, $type) {
66         $this->_iid  = $iid;
67         $this->_type = $type;
68     }
70     /**
71      * Parse this content
72      *
73      * @global object
74      * @global object
75      * @param string $content passed by ref for memory reasons, unset after return
76      * @param string $encoding content encoding
77      * @param string $delimiter_name separator (comma, semicolon, colon, cfg)
78      * @param string $column_validation name of function for columns validation, must have one param $columns
79      * @param string $enclosure field wrapper. One character only.
80      * @return bool false if error, count of data lines if ok; use get_error() to get error string
81      */
82     function load_csv_content(&$content, $encoding, $delimiter_name, $column_validation=null, $enclosure='"') {
83         global $USER, $CFG;
85         $this->close();
86         $this->_error = null;
88         $content = core_text::convert($content, $encoding, 'utf-8');
89         // remove Unicode BOM from first line
90         $content = core_text::trim_utf8_bom($content);
91         // Fix mac/dos newlines
92         $content = preg_replace('!\r\n?!', "\n", $content);
93         // Remove any spaces or new lines at the end of the file.
94         if ($delimiter_name == 'tab') {
95             // trim() by default removes tabs from the end of content which is undesirable in a tab separated file.
96             $content = trim($content, chr(0x20) . chr(0x0A) . chr(0x0D) . chr(0x00) . chr(0x0B));
97         } else {
98             $content = trim($content);
99         }
101         $csv_delimiter = csv_import_reader::get_delimiter($delimiter_name);
102         // $csv_encode    = csv_import_reader::get_encoded_delimiter($delimiter_name);
104         // Create a temporary file and store the csv file there,
105         // do not try using fgetcsv() because there is nothing
106         // to split rows properly - fgetcsv() itself can not do it.
107         $tempfile = tempnam(make_temp_directory('/csvimport'), 'tmp');
108         if (!$fp = fopen($tempfile, 'w+b')) {
109             $this->_error = get_string('cannotsavedata', 'error');
110             @unlink($tempfile);
111             return false;
112         }
113         fwrite($fp, $content);
114         fseek($fp, 0);
115         // Create an array to store the imported data for error checking.
116         $columns = array();
117         // str_getcsv doesn't iterate through the csv data properly. It has
118         // problems with line returns.
119         while ($fgetdata = fgetcsv($fp, 0, $csv_delimiter, $enclosure)) {
120             // Check to see if we have an empty line.
121             if (count($fgetdata) == 1) {
122                 if ($fgetdata[0] !== null) {
123                     // The element has data. Add it to the array.
124                     $columns[] = $fgetdata;
125                 }
126             } else {
127                 $columns[] = $fgetdata;
128             }
129         }
130         $col_count = 0;
132         // process header - list of columns
133         if (!isset($columns[0])) {
134             $this->_error = get_string('csvemptyfile', 'error');
135             fclose($fp);
136             unlink($tempfile);
137             return false;
138         } else {
139             $col_count = count($columns[0]);
140         }
142         // Column validation.
143         if ($column_validation) {
144             $result = $column_validation($columns[0]);
145             if ($result !== true) {
146                 $this->_error = $result;
147                 fclose($fp);
148                 unlink($tempfile);
149                 return false;
150             }
151         }
153         $this->_columns = $columns[0]; // cached columns
154         // check to make sure that the data columns match up with the headers.
155         foreach ($columns as $rowdata) {
156             if (count($rowdata) !== $col_count) {
157                 $this->_error = get_string('csvweirdcolumns', 'error');
158                 fclose($fp);
159                 unlink($tempfile);
160                 $this->cleanup();
161                 return false;
162             }
163         }
165         $filename = $CFG->tempdir.'/csvimport/'.$this->_type.'/'.$USER->id.'/'.$this->_iid;
166         $filepointer = fopen($filename, "w");
167         // The information has been stored in csv format, as serialized data has issues
168         // with special characters and line returns.
169         $storedata = csv_export_writer::print_array($columns, ',', '"', true);
170         fwrite($filepointer, $storedata);
172         fclose($fp);
173         unlink($tempfile);
174         fclose($filepointer);
176         $datacount = count($columns);
177         return $datacount;
178     }
180     /**
181      * Returns list of columns
182      *
183      * @return array
184      */
185     function get_columns() {
186         if (isset($this->_columns)) {
187             return $this->_columns;
188         }
190         global $USER, $CFG;
192         $filename = $CFG->tempdir.'/csvimport/'.$this->_type.'/'.$USER->id.'/'.$this->_iid;
193         if (!file_exists($filename)) {
194             return false;
195         }
196         $fp = fopen($filename, "r");
197         $line = fgetcsv($fp);
198         fclose($fp);
199         if ($line === false) {
200             return false;
201         }
202         $this->_columns = $line;
203         return $this->_columns;
204     }
206     /**
207      * Init iterator.
208      *
209      * @global object
210      * @global object
211      * @return bool Success
212      */
213     function init() {
214         global $CFG, $USER;
216         if (!empty($this->_fp)) {
217             $this->close();
218         }
219         $filename = $CFG->tempdir.'/csvimport/'.$this->_type.'/'.$USER->id.'/'.$this->_iid;
220         if (!file_exists($filename)) {
221             return false;
222         }
223         if (!$this->_fp = fopen($filename, "r")) {
224             return false;
225         }
226         //skip header
227         return (fgetcsv($this->_fp) !== false);
228     }
230     /**
231      * Get next line
232      *
233      * @return mixed false, or an array of values
234      */
235     function next() {
236         if (empty($this->_fp) or feof($this->_fp)) {
237             return false;
238         }
239         if ($ser = fgetcsv($this->_fp)) {
240             return $ser;
241         } else {
242             return false;
243         }
244     }
246     /**
247      * Release iteration related resources
248      *
249      * @return void
250      */
251     function close() {
252         if (!empty($this->_fp)) {
253             fclose($this->_fp);
254             $this->_fp = null;
255         }
256     }
258     /**
259      * Get last error
260      *
261      * @return string error text of null if none
262      */
263     function get_error() {
264         return $this->_error;
265     }
267     /**
268      * Cleanup temporary data
269      *
270      * @global object
271      * @global object
272      * @param boolean $full true means do a full cleanup - all sessions for current user, false only the active iid
273      */
274     function cleanup($full=false) {
275         global $USER, $CFG;
277         if ($full) {
278             @remove_dir($CFG->tempdir.'/csvimport/'.$this->_type.'/'.$USER->id);
279         } else {
280             @unlink($CFG->tempdir.'/csvimport/'.$this->_type.'/'.$USER->id.'/'.$this->_iid);
281         }
282     }
284     /**
285      * Get list of cvs delimiters
286      *
287      * @return array suitable for selection box
288      */
289     static function get_delimiter_list() {
290         global $CFG;
291         $delimiters = array('comma'=>',', 'semicolon'=>';', 'colon'=>':', 'tab'=>'\\t');
292         if (isset($CFG->CSV_DELIMITER) and strlen($CFG->CSV_DELIMITER) === 1 and !in_array($CFG->CSV_DELIMITER, $delimiters)) {
293             $delimiters['cfg'] = $CFG->CSV_DELIMITER;
294         }
295         return $delimiters;
296     }
298     /**
299      * Get delimiter character
300      *
301      * @param string separator name
302      * @return string delimiter char
303      */
304     static function get_delimiter($delimiter_name) {
305         global $CFG;
306         switch ($delimiter_name) {
307             case 'colon':     return ':';
308             case 'semicolon': return ';';
309             case 'tab':       return "\t";
310             case 'cfg':       if (isset($CFG->CSV_DELIMITER)) { return $CFG->CSV_DELIMITER; } // no break; fall back to comma
311             case 'comma':     return ',';
312             default :         return ',';  // If anything else comes in, default to comma.
313         }
314     }
316     /**
317      * Get encoded delimiter character
318      *
319      * @global object
320      * @param string separator name
321      * @return string encoded delimiter char
322      */
323     static function get_encoded_delimiter($delimiter_name) {
324         global $CFG;
325         if ($delimiter_name == 'cfg' and isset($CFG->CSV_ENCODE)) {
326             return $CFG->CSV_ENCODE;
327         }
328         $delimiter = csv_import_reader::get_delimiter($delimiter_name);
329         return '&#'.ord($delimiter);
330     }
332     /**
333      * Create new import id
334      *
335      * @global object
336      * @param string who imports?
337      * @return int iid
338      */
339     static function get_new_iid($type) {
340         global $USER;
342         $filename = make_temp_directory('csvimport/'.$type.'/'.$USER->id);
344         // use current (non-conflicting) time stamp
345         $iiid = time();
346         while (file_exists($filename.'/'.$iiid)) {
347             $iiid--;
348         }
350         return $iiid;
351     }
354 /**
355  * Utitily class for exporting of CSV files.
356  * @copyright 2012 Adrian Greeve
357  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
358  * @package   core
359  * @category  csv
360  */
361 class csv_export_writer {
362     /**
363      * @var string $delimiter  The name of the delimiter. Supported types(comma, tab, semicolon, colon, cfg)
364      */
365     var $delimiter;
366     /**
367      * @var string $csvenclosure  How fields with spaces and commas are enclosed.
368      */
369     var $csvenclosure;
370     /**
371      * @var string $mimetype  Mimetype of the file we are exporting.
372      */
373     var $mimetype;
374     /**
375      * @var string $filename  The filename for the csv file to be downloaded.
376      */
377     var $filename;
378     /**
379      * @var string $path  The directory path for storing the temporary csv file.
380      */
381     var $path;
382     /**
383      * @var resource $fp  File pointer for the csv file.
384      */
385     protected $fp;
387     /**
388      * Constructor for the csv export reader
389      *
390      * @param string $delimiter      The name of the character used to seperate fields. Supported types(comma, tab, semicolon, colon, cfg)
391      * @param string $enclosure      The character used for determining the enclosures.
392      * @param string $mimetype       Mime type of the file that we are exporting.
393      */
394     public function __construct($delimiter = 'comma', $enclosure = '"', $mimetype = 'application/download') {
395         $this->delimiter = $delimiter;
396         // Check that the enclosure is a single character.
397         if (strlen($enclosure) == 1) {
398             $this->csvenclosure = $enclosure;
399         } else {
400             $this->csvenclosure = '"';
401         }
402         $this->filename = "Moodle-data-export.csv";
403         $this->mimetype = $mimetype;
404     }
406     /**
407      * Set the file path to the temporary file.
408      */
409     protected function set_temp_file_path() {
410         global $USER, $CFG;
411         make_temp_directory('csvimport/' . $USER->id);
412         $path = $CFG->tempdir . '/csvimport/' . $USER->id. '/' . $this->filename;
413         // Check to see if the file exists, if so delete it.
414         if (file_exists($path)) {
415             unlink($path);
416         }
417         $this->path = $path;
418     }
420     /**
421      * Add data to the temporary file in csv format
422      *
423      * @param array $row  An array of values.
424      */
425     public function add_data($row) {
426         if(!isset($this->path)) {
427             $this->set_temp_file_path();
428             $this->fp = fopen($this->path, 'w+');
429         }
430         $delimiter = csv_import_reader::get_delimiter($this->delimiter);
431         fputcsv($this->fp, $row, $delimiter, $this->csvenclosure);
432     }
434     /**
435      * Echos or returns a csv data line by line for displaying.
436      *
437      * @param bool $return  Set to true to return a string with the csv data.
438      * @return string       csv data.
439      */
440     public function print_csv_data($return = false) {
441         fseek($this->fp, 0);
442         $returnstring = '';
443         while (($content = fgets($this->fp)) !== false) {
444             if (!$return){
445                 echo $content;
446             } else {
447                 $returnstring .= $content;
448             }
449         }
450         if ($return) {
451             return $returnstring;
452         }
453     }
455     /**
456      * Set the filename for the uploaded csv file
457      *
458      * @param string $dataname    The name of the module.
459      * @param string $extenstion  File extension for the file.
460      */
461     public function set_filename($dataname, $extension = '.csv') {
462         $filename = clean_filename($dataname);
463         $filename .= clean_filename('-' . gmdate("Ymd_Hi"));
464         $filename .= clean_filename("-{$this->delimiter}_separated");
465         $filename .= $extension;
466         $this->filename = $filename;
467     }
469     /**
470      * Output file headers to initialise the download of the file.
471      */
472     protected function send_header() {
473         global $CFG;
475         if (defined('BEHAT_SITE_RUNNING')) {
476             // For text based formats - we cannot test the output with behat if we force a file download.
477             return;
478         }
479         if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.
480             header('Cache-Control: max-age=10');
481             header('Pragma: ');
482         } else { //normal http - prevent caching at all cost
483             header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0');
484             header('Pragma: no-cache');
485         }
486         header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
487         header("Content-Type: $this->mimetype\n");
488         header("Content-Disposition: attachment; filename=\"$this->filename\"");
489     }
491     /**
492      * Download the csv file.
493      */
494     public function download_file() {
495         $this->send_header();
496         $this->print_csv_data();
497         exit;
498     }
500     /**
501      * Creates a file for downloading an array into a deliminated format.
502      * This function is useful if you are happy with the defaults and all of your
503      * information is in one array.
504      *
505      * @param string $filename    The filename of the file being created.
506      * @param array $records      An array of information to be converted.
507      * @param string $delimiter   The name of the delimiter. Supported types(comma, tab, semicolon, colon, cfg)
508      * @param string $enclosure   How speical fields are enclosed.
509      */
510     public static function download_array($filename, array &$records, $delimiter = 'comma', $enclosure='"') {
511         $csvdata = new csv_export_writer($delimiter, $enclosure);
512         $csvdata->set_filename($filename);
513         foreach ($records as $row) {
514             $csvdata->add_data($row);
515         }
516         $csvdata->download_file();
517     }
519     /**
520      * This will convert an array of values into a deliminated string.
521      * Like the above function, this is for convenience.
522      *
523      * @param array $records     An array of information to be converted.
524      * @param string $delimiter  The name of the delimiter. Supported types(comma, tab, semicolon, colon, cfg)
525      * @param string $enclosure  How speical fields are enclosed.
526      * @param bool $return       If true will return a string with the csv data.
527      * @return string            csv data.
528      */
529     public static function print_array(array &$records, $delimiter = 'comma', $enclosure = '"', $return = false) {
530         $csvdata = new csv_export_writer($delimiter, $enclosure);
531         foreach ($records as $row) {
532             $csvdata->add_data($row);
533         }
534         $data = $csvdata->print_csv_data($return);
535         if ($return) {
536             return $data;
537         }
538     }
540     /**
541      * Make sure that everything is closed when we are finished.
542      */
543     public function __destruct() {
544         fclose($this->fp);
545         unlink($this->path);
546     }