diff --git a/admin/user/user_bulk_download.php b/admin/user/user_bulk_download.php index c60c663cc07..21ceba0a773 100644 --- a/admin/user/user_bulk_download.php +++ b/admin/user/user_bulk_download.php @@ -148,23 +148,13 @@ function user_download_csv($fields) { global $CFG, $SESSION, $DB; require_once($CFG->dirroot.'/user/profile/lib.php'); + require_once($CFG->libdir . '/csvlib.class.php'); - $filename = clean_filename(get_string('users').'.csv'); + $filename = clean_filename(get_string('users')); - header("Content-Type: application/download\n"); - header("Content-Disposition: attachment; filename=\"$filename\""); - header("Expires: 0"); - header("Cache-Control: must-revalidate,post-check=0,pre-check=0"); - header("Pragma: public"); - - $delimiter = get_string('listsep', 'langconfig'); - $encdelim = '&#'.ord($delimiter); - - $row = array(); - foreach ($fields as $fieldname) { - $row[] = str_replace($delimiter, $encdelim, $fieldname); - } - echo implode($delimiter, $row)."\n"; + $csvexport = new csv_export_writer(); + $csvexport->set_filename($filename); + $csvexport->add_data($fields); foreach ($SESSION->bulk_users as $userid) { $row = array(); @@ -172,10 +162,19 @@ function user_download_csv($fields) { continue; } profile_load_data($user); + $userprofiledata = array(); foreach ($fields as $field=>$unused) { - $row[] = str_replace($delimiter, $encdelim, $user->$field); + // Custom user profile textarea fields come in an array + // The first element is the text and the second is the format. + // We only take the text. + if (is_array($user->$field)) { + $userprofiledata[] = reset($user->$field); + } else { + $userprofiledata[] = $user->$field; + } } - echo implode($delimiter, $row)."\n"; + $csvexport->add_data($userprofiledata); } + $csvexport->download_file(); die; } diff --git a/grade/export/txt/grade_export_txt.php b/grade/export/txt/grade_export_txt.php index 2f654d673cf..0f76af607bd 100644 --- a/grade/export/txt/grade_export_txt.php +++ b/grade/export/txt/grade_export_txt.php @@ -16,6 +16,7 @@ // along with Moodle. If not, see . require_once($CFG->dirroot.'/grade/export/lib.php'); +require_once($CFG->libdir . '/csvlib.class.php'); class grade_export_txt extends grade_export { @@ -54,46 +55,25 @@ class grade_export_txt extends grade_export { $strgrades = get_string('grades'); $profilefields = grade_helper::get_user_profile_fields($this->course->id, $this->usercustomfields); - switch ($this->separator) { - case 'comma': - $separator = ","; - break; - case 'tab': - default: - $separator = "\t"; - } - - // Print header to force download - if (strpos($CFG->wwwroot, 'https://') === 0) { //https sites - watch out for IE! KB812935 and KB316431 - @header('Cache-Control: max-age=10'); - @header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT'); - @header('Pragma: '); - } else { //normal http - prevent caching at all cost - @header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); - @header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT'); - @header('Pragma: no-cache'); - } - header("Content-Type: application/download\n"); $shortname = format_string($this->course->shortname, true, array('context' => context_course::instance($this->course->id))); $downloadfilename = clean_filename("$shortname $strgrades"); - header("Content-Disposition: attachment; filename=\"$downloadfilename.txt\""); + $csvexport = new csv_export_writer($this->separator); + $csvexport->set_filename($downloadfilename); // Print names of all the fields - $fieldfullnames = array(); + $exporttitle = array(); foreach ($profilefields as $field) { - $fieldfullnames[] = $field->fullname; + $exporttitle[] = $field->fullname; } - echo implode($separator, $fieldfullnames); + // Add a feedback column. foreach ($this->columns as $grade_item) { - echo $separator.$this->format_column_name($grade_item); - - // Add a feedback column. + $exporttitle[] = $this->format_column_name($grade_item); if ($this->export_feedback) { - echo $separator.$this->format_column_name($grade_item, true); + $exporttitle[] = $this->format_column_name($grade_item, true); } } - echo "\n"; + $csvexport->add_data($exporttitle); // Print all the lines of data. $geub = new grade_export_update_buffer(); @@ -103,31 +83,29 @@ class grade_export_txt extends grade_export { $gui->init(); while ($userdata = $gui->next_user()) { + $exportdata = array(); $user = $userdata->user; - $items = array(); foreach ($profilefields as $field) { $fieldvalue = grade_helper::get_user_field_value($user, $field); - $items[] = $fieldvalue; + $exportdata[] = $fieldvalue; } - echo implode($separator, $items); - foreach ($userdata->grades as $itemid => $grade) { if ($export_tracking) { $status = $geub->track($grade); } - echo $separator.$this->format_grade($grade); + $exportdata[] = $this->format_grade($grade); if ($this->export_feedback) { - echo $separator.$this->format_feedback($userdata->feedbacks[$itemid]); + $exportdata[] = $this->format_feedback($userdata->feedbacks[$itemid]); } } - echo "\n"; + $csvexport->add_data($exportdata); } $gui->close(); $geub->close(); - + $csvexport->download_file(); exit; } } diff --git a/lib/csvlib.class.php b/lib/csvlib.class.php index 92296fd481a..421058a4128 100644 --- a/lib/csvlib.class.php +++ b/lib/csvlib.class.php @@ -279,6 +279,7 @@ class csv_import_reader { case 'tab': return "\t"; case 'cfg': if (isset($CFG->CSV_DELIMITER)) { return $CFG->CSV_DELIMITER; } // no break; fall back to comma case 'comma': return ','; + default : return ','; // If anything else comes in, default to comma. } } @@ -319,3 +320,193 @@ class csv_import_reader { return $iiid; } } + +/** + * Utitily class for exporting of CSV files. + * @copyright 2012 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + * @category csv + */ +class csv_export_writer { + /** + * @var string $delimiter The name of the delimiter. Supported types(comma, tab, semicolon, colon, cfg) + */ + var $delimiter; + /** + * @var string $csvenclosure How fields with spaces and commas are enclosed. + */ + var $csvenclosure; + /** + * @var string $mimetype Mimetype of the file we are exporting. + */ + var $mimetype; + /** + * @var string $filename The filename for the csv file to be downloaded. + */ + var $filename; + /** + * @var string $path The directory path for storing the temporary csv file. + */ + var $path; + /** + * @var resource $fp File pointer for the csv file. + */ + protected $fp; + + /** + * Constructor for the csv export reader + * + * @param string $delimiter The name of the character used to seperate fields. Supported types(comma, tab, semicolon, colon, cfg) + * @param string $enclosure The character used for determining the enclosures. + * @param string $mimetype Mime type of the file that we are exporting. + */ + public function __construct($delimiter = 'comma', $enclosure = '"', $mimetype = 'application/download') { + $this->delimiter = $delimiter; + // Check that the enclosure is a single character. + if (strlen($enclosure) == 1) { + $this->csvenclosure = $enclosure; + } else { + $this->csvenclosure = '"'; + } + $this->filename = "Moodle-data-export.csv"; + $this->mimetype = $mimetype; + } + + /** + * Set the file path to the temporary file. + */ + protected function set_temp_file_path() { + global $USER, $CFG; + make_temp_directory('csvimport/' . $USER->id); + $path = $CFG->tempdir . '/csvimport/' . $USER->id. '/' . $this->filename; + // Check to see if the file exists, if so delete it. + if (file_exists($path)) { + unlink($path); + } + $this->path = $path; + } + + /** + * Add data to the temporary file in csv format + * + * @param array $row An array of values. + */ + public function add_data($row) { + if(!isset($this->path)) { + $this->set_temp_file_path(); + $this->fp = fopen($this->path, 'w+'); + } + $delimiter = csv_import_reader::get_delimiter($this->delimiter); + fputcsv($this->fp, $row, $delimiter, $this->csvenclosure); + } + + /** + * Echos or returns a csv data line by line for displaying. + * + * @param bool $return Set to true to return a string with the csv data. + * @return string csv data. + */ + public function print_csv_data($return = false) { + fseek($this->fp, 0); + $returnstring = ''; + while (($content = fgets($this->fp)) !== false) { + if (!$return){ + echo $content; + } else { + $returnstring .= $content; + } + } + if ($return) { + return $returnstring; + } + } + + /** + * Set the filename for the uploaded csv file + * + * @param string $dataname The name of the module. + * @param string $extenstion File extension for the file. + */ + public function set_filename($dataname, $extension = '.csv') { + $filename = clean_filename($dataname); + $filename .= clean_filename('-' . gmdate("Ymd_Hi")); + $filename .= clean_filename("-{$this->delimiter}_separated"); + $filename .= $extension; + $this->filename = $filename; + } + + /** + * Output file headers to initialise the download of the file. + */ + protected function send_header() { + global $CFG; + if (strpos($CFG->wwwroot, 'https://') === 0) { //https sites - watch out for IE! KB812935 and KB316431 + header('Cache-Control: max-age=10'); + header('Pragma: '); + } else { //normal http - prevent caching at all cost + header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); + header('Pragma: no-cache'); + } + header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT'); + header("Content-Type: $this->mimetype\n"); + header("Content-Disposition: attachment; filename=\"$this->filename\""); + } + + /** + * Download the csv file. + */ + public function download_file() { + $this->send_header(); + $this->print_csv_data(); + exit; + } + + /** + * Creates a file for downloading an array into a deliminated format. + * This function is useful if you are happy with the defaults and all of your + * information is in one array. + * + * @param string $filename The filename of the file being created. + * @param array $records An array of information to be converted. + * @param string $delimiter The name of the delimiter. Supported types(comma, tab, semicolon, colon, cfg) + * @param string $enclosure How speical fields are enclosed. + */ + public static function download_array($filename, array &$records, $delimiter = 'comma', $enclosure='"') { + $csvdata = new csv_export_writer($delimiter, $enclosure); + $csvdata->set_filename($filename); + foreach ($records as $row) { + $csvdata->add_data($row); + } + $csvdata->download_file(); + } + + /** + * This will convert an array of values into a deliminated string. + * Like the above function, this is for convenience. + * + * @param array $records An array of information to be converted. + * @param string $delimiter The name of the delimiter. Supported types(comma, tab, semicolon, colon, cfg) + * @param string $enclosure How speical fields are enclosed. + * @param bool $return If true will return a string with the csv data. + * @return string csv data. + */ + public static function print_array(array &$records, $delimiter = 'comma', $enclosure = '"', $return = false) { + $csvdata = new csv_export_writer($delimiter, $enclosure); + foreach ($records as $row) { + $csvdata->add_data($row); + } + $data = $csvdata->print_csv_data($return); + if ($return) { + return $data; + } + } + + /** + * Make sure that everything is closed when we are finished. + */ + public function __destruct() { + fclose($this->fp); + unlink($this->path); + } +} diff --git a/lib/tablelib.php b/lib/tablelib.php index b8105642cfd..1f14082921a 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -1557,18 +1557,19 @@ class table_ods_export_format extends table_spreadsheet_export_format_parent { * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class table_text_export_format_parent extends table_default_export_format_parent { - protected $seperator = "\t"; + protected $seperator = "tab"; protected $mimetype = 'text/tab-separated-values'; protected $ext = '.txt'; + protected $myexporter; + + public function __construct() { + $this->myexporter = new csv_export_writer($this->seperator, '"', $this->mimetype); + } public function start_document($filename) { - $this->filename = $filename . $this->ext; - header('Content-Type: ' . $this->mimetype . '; charset=UTF-8'); - header('Content-Disposition: attachment; filename="' . $this->filename . '"'); - header('Expires: 0'); - header('Cache-Control: must-revalidate,post-check=0,pre-check=0'); - header('Pragma: public'); + $this->filename = $filename; $this->documentstarted = true; + $this->myexporter->set_filename($filename, $this->ext); } public function start_table($sheettitle) { @@ -1576,19 +1577,20 @@ class table_text_export_format_parent extends table_default_export_format_parent } public function output_headers($headers) { - echo $this->format_row($headers); + $this->myexporter->add_data($headers); } public function add_data($row) { - echo $this->format_row($row); + $this->myexporter->add_data($row); return true; } public function finish_table() { - echo "\n\n"; + //nothing to do here } public function finish_document() { + $this->myexporter->download_file(); exit; } @@ -1612,24 +1614,23 @@ class table_text_export_format_parent extends table_default_export_format_parent * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class table_tsv_export_format extends table_text_export_format_parent { - protected $seperator = "\t"; + protected $seperator = "tab"; protected $mimetype = 'text/tab-separated-values'; protected $ext = '.txt'; } - +require_once($CFG->libdir . '/csvlib.class.php'); /** * @package moodlecore * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class table_csv_export_format extends table_text_export_format_parent { - protected $seperator = ","; + protected $seperator = "comma"; protected $mimetype = 'text/csv'; protected $ext = '.csv'; } - /** * @package moodlecore * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} diff --git a/lib/tests/csvclass_test.php b/lib/tests/csvclass_test.php new file mode 100644 index 00000000000..ff932279aa3 --- /dev/null +++ b/lib/tests/csvclass_test.php @@ -0,0 +1,75 @@ +. + +/** + * Tests csv import and export functions + * + * @package core + * @category phpunit + * @copyright 2012 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/lib/csvlib.class.php'); + +class csvclass_testcase extends advanced_testcase { + + var $testdata = array(); + var $teststring = ''; + + protected function setUp(){ + + $this->resetAfterTest(true); + + $csvdata = array(); + $csvdata[0][] = 'fullname'; + $csvdata[0][] = 'description of things'; + $csvdata[0][] = 'beer'; + $csvdata[1][] = 'William B Stacey'; + $csvdata[1][] = '

A field that contains "double quotes"

'; + $csvdata[1][] = 'Asahi'; + $csvdata[2][] = 'Phillip Jenkins'; + $csvdata[2][] = '

This field has

+

Multiple lines

+

and also contains "double quotes"

'; + $csvdata[2][] = 'Yebisu'; + $this->testdata = $csvdata; + + // Please note that each line needs a carriage return. + $this->teststring = 'fullname,"description of things",beer +"William B Stacey","

A field that contains ""double quotes""

",Asahi +"Phillip Jenkins","

This field has

+

Multiple lines

+

and also contains ""double quotes""

",Yebisu +'; + } + + public function test_csv_functions() { + $csvexport = new csv_export_writer(); + $csvexport->set_filename('unittest'); + foreach ($this->testdata as $data) { + $csvexport->add_data($data); + } + $csvoutput = $csvexport->print_csv_data(true); + $this->assertEquals($csvoutput, $this->teststring); + + $test_data = csv_export_writer::print_array($this->testdata, 'comma', '"', true); + $this->assertEquals($test_data, $this->teststring); + } +} diff --git a/mod/data/lib.php b/mod/data/lib.php index 3b3b2a7f5b5..df5fe42b326 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -2646,37 +2646,19 @@ function data_supports($feature) { * @param bool $return * @return string|void */ -function data_export_csv($export, $delimiter_name, $dataname, $count, $return=false) { +function data_export_csv($export, $delimiter_name, $database, $count, $return=false) { global $CFG; require_once($CFG->libdir . '/csvlib.class.php'); - $delimiter = csv_import_reader::get_delimiter($delimiter_name); - $filename = clean_filename("{$dataname}-{$count}_record"); + + $filename = $database . '-' . $count . '-record'; if ($count > 1) { $filename .= 's'; } - $filename .= clean_filename('-' . gmdate("Ymd_Hi")); - $filename .= clean_filename("-{$delimiter_name}_separated"); - $filename .= '.csv'; - if (empty($return)) { - header("Content-Type: application/download\n"); - header("Content-Disposition: attachment; filename=\"$filename\""); - header('Expires: 0'); - header('Cache-Control: must-revalidate,post-check=0,pre-check=0'); - header('Pragma: public'); + if ($return) { + return csv_export_writer::print_array($export, $delimiter_name, '"', true); + } else { + csv_export_writer::download_array($filename, $export, $delimiter_name); } - $encdelim = '&#' . ord($delimiter) . ';'; - $returnstr = ''; - foreach($export as $row) { - foreach($row as $key => $column) { - $row[$key] = str_replace($delimiter, $encdelim, $column); - } - $returnstr .= implode($delimiter, $row) . "\n"; - } - if (empty($return)) { - echo $returnstr; - return; - } - return $returnstr; } /**