1: <?php
2: /**
3: * @category Horde
4: * @package Data
5: */
6:
7: /**
8: * Horde_Data implementation for comma-separated data (CSV).
9: *
10: * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
11: *
12: * See the enclosed file COPYING for license information (LGPL). If you
13: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
14: *
15: * @author Jan Schneider <jan@horde.org>
16: * @author Chuck Hagenbuch <chuck@horde.org>
17: * @category Horde
18: * @package Data
19: */
20: class Horde_Data_Csv extends Horde_Data_Base
21: {
22: /**
23: * Default charset.
24: *
25: * @var string
26: */
27: protected $_charset = null;
28:
29: /**
30: * MIME content type.
31: *
32: * @var string
33: */
34: protected $_contentType = 'application/csv';
35:
36: /**
37: * File extension.
38: *
39: * @var string
40: */
41: protected $_extension = 'csv';
42:
43: /**
44: * Constructor.
45: *
46: * @param array $params Optional parameters:
47: * <pre>
48: * 'charset' - (string) The default charset.
49: * DEFAULT: NONE
50: * </pre>
51: *
52: * @throws InvalidArgumentException
53: */
54: public function __construct(array $params = array())
55: {
56: if (isset($params['charset'])) {
57: $this->_charset = $params['charset'];
58: unset($params['charset']);
59: }
60:
61: parent::__construct($params);
62: }
63:
64: /**
65: * Imports and parses a CSV file.
66: *
67: * @param string $filename The name of the file to parse.
68: * @param boolean $header Does the first line contain the field/column
69: * names?
70: * @param string $sep The field/column separator.
71: * @param string $quote The quoting character.
72: * @param integer $fields The number or fields/columns.
73: * @param string $charset The file's charset.
74: * @param string $crlf The file's linefeed characters.
75: *
76: * @return array A two-dimensional array of all imported data rows. If
77: * $header was true the rows are associative arrays with the
78: * field/column names as the keys.
79: * @throws Horde_Data_Exception
80: */
81: public function importFile($filename, $header = false, $sep = ',',
82: $quote = '', $fields = null,
83: $import_mapping = array(), $charset = null,
84: $crlf = null)
85: {
86: if (empty($fields)) {
87: return array();
88: }
89:
90: $conf = array(
91: 'length' => $fields,
92: 'quote' => $quote,
93: 'separator' => $sep
94: );
95:
96: $fp = @fopen($filename, 'r');
97: if (!$fp) {
98: throw new Horde_Data_Exception(Horde_Data_Translation::t("Cannot open file."));
99: }
100:
101: /* Strip and keep the first line if it contains the field names. */
102: if ($header) {
103: $head = Horde_Util::getCsv($fp, $conf);
104: if (!$head) {
105: return array();
106: }
107: if (!empty($charset)) {
108: $head = Horde_String::convertCharset($head, $charset, $this->_charset);
109: }
110: }
111:
112: $data = array();
113: while ($line = Horde_Util::getCsv($fp, $conf)) {
114: if (!empty($charset)) {
115: $line = Horde_String::convertCharset($line, $charset, $this->_charset);
116: }
117: if (!isset($head)) {
118: $data[] = $line;
119: } else {
120: $newline = array();
121: for ($i = 0; $i < count($head); $i++) {
122: if (isset($import_mapping[$head[$i]])) {
123: $head[$i] = $import_mapping[$head[$i]];
124: }
125: $cell = $line[$i];
126: $cell = preg_replace("/\"\"/", "\"", $cell);
127: $newline[$head[$i]] = empty($cell) ? '' : $cell;
128: }
129: $data[] = $newline;
130: }
131: }
132:
133: return $data;
134: }
135:
136: /**
137: * Builds a CSV file from a given data structure and returns it as a
138: * string.
139: *
140: * @param array $data A two-dimensional array containing the data set.
141: * @param boolean $header If true, the rows of $data are associative
142: * arrays with field names as their keys.
143: *
144: * @return string The CSV data.
145: */
146: public function exportData($data, $header = false,
147: $export_mapping = array())
148: {
149: if (!is_array($data) || count($data) == 0) {
150: return '';
151: }
152:
153: $export = '';
154: $eol = "\n";
155: $head = array_keys(current($data));
156: if ($header) {
157: foreach ($head as $key) {
158: if (!empty($key)) {
159: if (isset($export_mapping[$key])) {
160: $key = $export_mapping[$key];
161: }
162: $export .= '"' . str_replace('"', '\\"', $key) . '"';
163: }
164: $export .= ',';
165: }
166: $export = substr($export, 0, -1) . $eol;
167: }
168:
169: foreach ($data as $row) {
170: foreach ($head as $key) {
171: $cell = $row[$key];
172: if (!empty($cell) || $cell === 0) {
173: $export .= '"' . str_replace('"', '\\"', $cell) . '"';
174: }
175: $export .= ',';
176: }
177: $export = substr($export, 0, -1) . $eol;
178: }
179:
180: return $export;
181: }
182:
183: /**
184: * Builds a CSV file from a given data structure and triggers its
185: * download. It DOES NOT exit the current script but only outputs the
186: * correct headers and data.
187: *
188: * @param string $filename The name of the file to be downloaded.
189: * @param array $data A two-dimensional array containing the data
190: * set.
191: * @param boolean $header If true, the rows of $data are associative
192: * arrays with field names as their keys.
193: */
194: public function exportFile($filename, $data, $header = false,
195: $export_mapping = array())
196: {
197: if (!isset($this->_browser)) {
198: throw new LogicException('Missing browser parameter.');
199: }
200:
201: $export = $this->exportData($data, $header, $export_mapping);
202: $this->_browser->downloadHeaders($filename, 'application/csv', false, strlen($export));
203: echo $export;
204: }
205:
206: /**
207: * Takes all necessary actions for the given import step, parameters and
208: * form values and returns the next necessary step.
209: *
210: * @param integer $action The current step. One of the IMPORT_* constants.
211: * @param array $param An associative array containing needed
212: * parameters for the current step.
213: *
214: * @return mixed Either the next step as an integer constant or imported
215: * data set after the final step.
216: * @throws Horde_Data_Exception
217: */
218: public function nextStep($action, $param = array())
219: {
220: $session = $GLOBALS['injector']->getInstance('Horde_Session');
221:
222: switch ($action) {
223: case Horde_Data::IMPORT_FILE:
224: parent::nextStep($action, $param);
225:
226: /* Move uploaded file so that we can read it again in the next
227: step after the user gave some format details. */
228: $file_name = Horde_Util::getTempFile('import', false);
229: if (!move_uploaded_file($_FILES['import_file']['tmp_name'], $file_name)) {
230: throw new Horde_Data_Exception(Horde_Data_Translation::t("The uploaded file could not be saved."));
231: }
232: $session->set('horde', 'import_data/file_name', $file_name);
233:
234: /* Check if charset was specified. */
235: $session->set('horde', 'import_data/charset', $this->_vars->charset);
236:
237: /* Read the file's first two lines to show them to the user. */
238: $first_lines = '';
239: if ($fp = @fopen($file_name, 'r')) {
240: for ($line_no = 1, $line = fgets($fp);
241: $line_no <= 3 && $line;
242: $line_no++, $line = fgets($fp)) {
243: $line = Horde_String::convertCharset($line, $this->_vars->charset, $this->_charset);
244: $first_lines .= Horde_String::truncate($line);
245: if (Horde_String::length($line) > 100) {
246: $first_lines .= "\n";
247: }
248: }
249: }
250: $session->set('horde', 'import_data/first_lines', $first_lines);
251:
252: /* Import the first line to guess the number of fields. */
253: if ($first_lines) {
254: rewind($fp);
255: $line = Horde_Util::getCsv($fp);
256: if ($line) {
257: $session->set('horde', 'import_data/fields', count($line));
258: }
259: }
260:
261: return Horde_Data::IMPORT_CSV;
262:
263: case Horde_Data::IMPORT_CSV:
264: $session->set('horde', 'import_data/header', $this->_vars->header);
265: $import_mapping = array();
266: if (isset($param['import_mapping'])) {
267: $import_mapping = $param['import_mapping'];
268: }
269: $session->set('horde', 'import_data/data', $this->importFile(
270: $session->get('horde', 'import_data/file_name'),
271: $this->_vars->header,
272: $this->_vars->sep,
273: $this->_vars->quote,
274: $this->_vars->fields,
275: $import_mapping,
276: $session->get('horde', 'import_data/charset'),
277: $session->get('horde', 'import_data/crlf')
278: ));
279: $session->remove('horde', 'import_data/map');
280: return Horde_Data::IMPORT_MAPPED;
281:
282: default:
283: return parent::nextStep($action, $param);
284: }
285: }
286:
287: }
288: