PluginProbe
TablePress – Tables in WordPress made easy / 1.9.2
TablePress – Tables in WordPress made easy v1.9.2
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / libraries / csv-parser.class.php

csv-parser.class.php in TablePress – Tables in WordPress made easy 1.9.2, at libraries/csv-parser.class.php

328 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CSV Parsing class for TablePress, used for import of CSV files
4 *
5 * @package TablePress
6 * @subpackage Import
7 * @author Tobias Bäthge
8 * @since 1.0.0
9 */
10
11 // Prohibit direct script loading.
12 defined( 'ABSPATH' ) || die( 'No direct script access allowed!' );
13
14 /**
15 * CSV Parsing class
16 * @package TablePress
17 * @subpackage Import
18 * @author Tobias Bäthge
19 * @since 1.0.0
20 */
21 class CSV_Parser {
22
23 /**
24 * The used character for the enclosure of a cell. Defaults to quotation mark ".
25 *
26 * @since 1.0.0
27 * @var string
28 */
29 protected $enclosure = '"';
30
31 /**
32 * Number of rows to analyze when attempting to auto-detect the CSV delimiter.
33 *
34 * @since 1.0.0
35 * @var int
36 */
37 protected $delimiter_search_max_lines = 15;
38
39 /**
40 * Characters to ignore when attempting to auto-detect delimiter.
41 *
42 * @since 1.0.0
43 * @var string
44 */
45 protected $non_delimiter_chars = "a-zA-Z0-9\n\r";
46
47 /**
48 * The preferred delimiter characters, only used when all filtering method return multiple possible delimiters (happens very rarely).
49 *
50 * @since 1.0.0
51 * @var string
52 */
53 protected $preferred_delimiter_chars = ";,\t";
54
55 /**
56 * The CSV data string that shall be parsed to an array.
57 *
58 * @since 1.0.0
59 * @var string
60 */
61 protected $import_data;
62
63 /**
64 * The error state while parsing input data.
65 *
66 * 0 = No errors found. Everything should be fine.
67 * 1 = A hopefully correctable syntax error was found.
68 * 2 = The enclosure character was found in a non-enclosed field. This means the file is either corrupt,
69 * or does not follow the common CSV standard. Please validate the parsed data manually.
70 *
71 * @since 1.0.0
72 * @var int
73 */
74 public $error = 0;
75
76 /**
77 * Detailed error information.
78 *
79 * @since 1.0.0
80 * @var array
81 */
82 public $error_info = array();
83
84 /**
85 * Class Constructor.
86 *
87 * @since 1.0.0
88 */
89 public function __construct() {
90 // Unused.
91 }
92
93 /**
94 * Load CSV data that shall be parsed.
95 *
96 * @since 1.0.0
97 *
98 * @param string $data Data to be parsed.
99 */
100 public function load_data( $data ) {
101 // Check for mandatory trailing line break.
102 if ( "\n" !== substr( $data, -1 ) ) {
103 $data .= "\n";
104 }
105 $this->import_data = $data;
106 }
107
108 /**
109 * Detect the CSV delimiter, by analyzing some rows to determine the most probable delimiter character.
110 *
111 * @since 1.0.0
112 *
113 * @return string Most probable delimiter character.
114 */
115 public function find_delimiter() {
116 $data = &$this->import_data;
117
118 $delimiter_count = array();
119 $enclosed = false;
120 $current_line = 0;
121
122 // Walk through each character in the CSV string (up to $this->delimiter_search_max_lines) and search potential delimiter characters.
123 $data_length = strlen( $data );
124 for ( $i = 0; $i < $data_length; $i++ ) {
125 $prev_char = ( $i - 1 >= 0 ) ? $data[ $i - 1 ] : '';
126 $curr_char = $data[ $i ];
127 $next_char = ( $i + 1 < $data_length ) ? $data[ $i + 1 ] : '';
128
129 if ( $curr_char === $this->enclosure ) {
130 // Open and closing quotes.
131 if ( ! $enclosed || $next_char !== $this->enclosure ) {
132 $enclosed = ! $enclosed; // Flip bool.
133 } elseif ( $enclosed ) {
134 $i++; // Skip next character.
135 }
136 } elseif ( ( "\n" === $curr_char && "\r" !== $prev_char || "\r" === $curr_char ) && ! $enclosed ) {
137 // Reached end of a line.
138 $current_line++;
139 if ( $current_line >= $this->delimiter_search_max_lines ) {
140 break;
141 }
142 } elseif ( ! $enclosed ) {
143 // At this point, $curr_char seems to be used as a delimiter, as it is not enclosed.
144 // Count $curr_char if it is not in the $this->non_delimiter_chars list
145 if ( 0 === preg_match( '#[' . $this->non_delimiter_chars . ']#i', $curr_char ) ) {
146 if ( ! isset( $delimiter_count[ $curr_char ][ $current_line ] ) ) {
147 $delimiter_count[ $curr_char ][ $current_line ] = 0; // Initialize empty
148 }
149 $delimiter_count[ $curr_char ][ $current_line ]++;
150 }
151 }
152 }
153
154 // Find most probable delimiter, by sorting their counts.
155 $potential_delimiters = array();
156 foreach ( $delimiter_count as $char => $line_counts ) {
157 $is_possible_delimiter = $this->_check_delimiter_count( $char, $line_counts, $current_line );
158 if ( false !== $is_possible_delimiter ) {
159 $potential_delimiters[ $is_possible_delimiter ] = $char;
160 }
161 }
162 ksort( $potential_delimiters );
163
164 // If no valid delimiter was found, use the character that was found in most rows.
165 if ( empty( $potential_delimiters ) ) {
166 $delimiter_counts = array_map( 'count', $delimiter_count );
167 arsort( $delimiter_counts, SORT_NUMERIC );
168 $potential_delimiters = array_keys( $delimiter_counts );
169 }
170
171 // Return first array element, as that has the highest count.
172 return array_shift( $potential_delimiters );
173 }
174
175 /**
176 * Check if passed character can be a delimiter, by checking counts in each line.
177 *
178 * @since 1.0.0
179 *
180 * @param string $char Character to check.
181 * @param array $line_counts Counts for the characters in the lines.
182 * @param int $number_lines Number of lines.
183 * @return bool|string False if delimiter is not possible, string to be used as a sort key if character could be a delimiter.
184 */
185 protected function _check_delimiter_count( $char, array $line_counts, $number_lines ) {
186 // Was the potential delimiter found in every line?
187 if ( count( $line_counts ) !== $number_lines ) {
188 return false;
189 }
190
191 // Check if the count in every line is the same (or one higher for an "almost").
192 $first = null;
193 $equal = null;
194 $almost = false;
195 foreach ( $line_counts as $line => $count ) {
196 if ( is_null( $first ) ) {
197 $first = $count;
198 } elseif ( $count === $first && false !== $equal ) {
199 $equal = true;
200 } elseif ( $count === $first + 1 && false !== $equal ) {
201 $equal = true;
202 $almost = true;
203 } else {
204 $equal = false;
205 }
206 }
207 // Check equality only if there's more than one line.
208 if ( $number_lines > 1 && ! $equal ) {
209 return false;
210 }
211
212 // At this point, count is equal in all lines, so determine a string to sort priority.
213 $match = ( $almost ) ? 2 : 1;
214 $pref = strpos( $this->preferred_delimiter_chars, $char );
215 $pref = ( false !== $pref ) ? str_pad( $pref, 3, '0', STR_PAD_LEFT ) : '999';
216 return $pref . $match . '.' . ( 99999 - str_pad( $first, 5, '0', STR_PAD_LEFT ) );
217 }
218
219 /**
220 * Parse CSV string into a two-dimensional array.
221 *
222 * @since 1.0.0
223 *
224 * @param string $delimiter Delimiter character for the CSV parsing.
225 * @return array Two-dimensional array with the data from the CSV string.
226 */
227 public function parse( $delimiter ) {
228 $data = &$this->import_data;
229
230 // Filter delimiter from the list, if it is a whitespace character.
231 $white_spaces = str_replace( $delimiter, '', " \t\x0B\0" );
232
233 $rows = array(); // Complete rows.
234 $row = array(); // Row that is currently built.
235 $column = 0; // Current column index.
236 $cell_content = ''; // Content of the currently processed cell.
237 $enclosed = false;
238 $was_enclosed = false; // To determine if the cell content will be trimmed of whitespace (only for enclosed cells).
239
240 // Walk through each character in the CSV string.
241 $data_length = strlen( $data );
242 for ( $i = 0; $i < $data_length; $i++ ) {
243 $curr_char = $data[ $i ];
244 $next_char = ( $i + 1 < $data_length ) ? $data[ $i + 1 ] : '';
245
246 if ( $curr_char === $this->enclosure ) {
247 // Open/close quotes, and inline quotes.
248 if ( ! $enclosed ) {
249 if ( '' === ltrim( $cell_content, $white_spaces ) ) {
250 $enclosed = true;
251 $was_enclosed = true;
252 } else {
253 $this->error = 2;
254 $error_line = count( $rows ) + 1;
255 $error_column = $column + 1;
256 if ( ! isset( $this->error_info[ "{$error_line}-{$error_column}" ] ) ) {
257 $this->error_info[ "{$error_line}-{$error_column}" ] = array(
258 'type' => 2,
259 'info' => "Syntax error found in line {$error_line}. Non-enclosed fields can not contain double-quotes.",
260 'line' => $error_line,
261 'column' => $error_column,
262 );
263 }
264 $cell_content .= $curr_char;
265 }
266 } elseif ( $next_char === $this->enclosure ) {
267 // Enclosure character within enclosed cell (" encoded as "").
268 $cell_content .= $curr_char;
269 $i++; // Skip next character
270 } elseif ( $next_char !== $delimiter && "\r" !== $next_char && "\n" !== $next_char ) {
271 // for-loop (instead of while-loop) that skips whitespace.
272 for ( $x = ( $i + 1 ); isset( $data[ $x ] ) && '' === ltrim( $data[ $x ], $white_spaces ); $x++ ) {
273 // Action is in iterator check.
274 }
275 if ( $data[ $x ] === $delimiter ) {
276 $enclosed = false;
277 $i = $x;
278 } else {
279 if ( $this->error < 1 ) {
280 $this->error = 1;
281 }
282 $error_line = count( $rows ) + 1;
283 $error_column = $column + 1;
284 if ( ! isset( $this->error_info[ "{$error_line}-{$error_column}" ] ) ) {
285 $this->error_info[ "{$error_line}-{$error_column}" ] = array(
286 'type' => 1,
287 'info' => "Syntax error found in line {$error_line}. A single double-quote was found within an enclosed string. Enclosed double-quotes must be escaped with a second double-quote.",
288 'line' => $error_line,
289 'column' => $error_column,
290 );
291 }
292 $cell_content .= $curr_char;
293 $enclosed = false;
294 }
295 } else {
296 // The " was the closing one for the cell.
297 $enclosed = false;
298 }
299 } elseif ( ( $curr_char === $delimiter || "\n" === $curr_char || "\r" === $curr_char ) && ! $enclosed ) {
300 // End of cell (by $delimiter), or end of line (by line break, and not enclosed!).
301
302 $row[ $column ] = ( $was_enclosed ) ? $cell_content : trim( $cell_content );
303 $cell_content = '';
304 $was_enclosed = false;
305 $column++;
306
307 // End of line.
308 if ( "\n" === $curr_char || "\r" === $curr_char ) {
309 // Append completed row.
310 $rows[] = $row;
311 $row = array();
312 $column = 0;
313 if ( "\r" === $curr_char && "\n" === $next_char ) {
314 // Skip next character in \r\n line breaks.
315 $i++;
316 }
317 }
318 } else {
319 // Append character to current cell.
320 $cell_content .= $curr_char;
321 }
322 }
323
324 return $rows;
325 }
326
327 } // class CSV_Parser
328