PluginProbe
M Chart / 2.3
M Chart v2.3
2.3.2 2.3.1 2.3 2.2.2 2.2.1 2.2 trunk 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.10 1.10.1 1.11 1.11.1 1.11.2 1.12 1.2 1.2.1 1.3 1.3.1 1.3.2 All 53 releases
m-chart / components / external / parsecsv / src / Csv.php

Csv.php in M Chart 2.3, at components/external/parsecsv/src/Csv.php

1,473 lines 49.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ParseCsv;
4
5 use Illuminate\Support\Collection;
6 use ParseCsv\enums\FileProcessingModeEnum;
7 use ParseCsv\enums\SortEnum;
8 use ParseCsv\extensions\DatatypeTrait;
9
10 class Csv {
11
12 /*
13 https://github.com/parsecsv/parsecsv-for-php
14
15 Fully conforms to the specifications lined out on Wikipedia:
16 - http://en.wikipedia.org/wiki/Comma-separated_values
17
18 Based on the concept of Ming Hong Ng's CsvFileParser class:
19 - http://minghong.blogspot.com/2006/07/csv-parser-for-php.html
20
21 (The MIT license)
22
23 Copyright (c) 2014 Jim Myhrberg.
24
25 Permission is hereby granted, free of charge, to any person obtaining a copy
26 of this software and associated documentation files (the "Software"), to deal
27 in the Software without restriction, including without limitation the rights
28 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29 copies of the Software, and to permit persons to whom the Software is
30 furnished to do so, subject to the following conditions:
31
32 The above copyright notice and this permission notice shall be included in
33 all copies or substantial portions of the Software.
34
35 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
41 THE SOFTWARE.
42
43 For code examples, please read the files within the 'examples' dir.
44 */
45
46 /**
47 * Configuration
48 * - set these options with $object->var_name = 'value';
49 */
50
51 /**
52 * Header row:
53 * Use first line/entry as field names
54 *
55 * @var bool
56 */
57 public $heading = true;
58
59 /**
60 * Override field names
61 *
62 * @var array
63 */
64 public $fields = array();
65
66 /**
67 * Sort CSV by this field
68 *
69 * @var string|null
70 */
71 public $sort_by = null;
72
73 /**
74 * Reverse the sort direction
75 *
76 * @var bool
77 */
78 public $sort_reverse = false;
79
80 /**
81 * Sort behavior passed to sort methods
82 *
83 * regular = SORT_REGULAR
84 * numeric = SORT_NUMERIC
85 * string = SORT_STRING
86 *
87 * @var string|null
88 */
89 public $sort_type = SortEnum::SORT_TYPE_REGULAR;
90
91 /**
92 * Field delimiter character
93 *
94 * @var string
95 */
96 public $delimiter = ',';
97
98 /**
99 * Enclosure character
100 *
101 * This is useful for cell values that are either multi-line
102 * or contain the field delimiter character.
103 *
104 * @var string
105 */
106 public $enclosure = '"';
107
108 /**
109 * Force enclosing all columns.
110 *
111 * If false, only cells that are either multi-line or
112 * contain the field delimiter character are enclosed
113 * in the $enclosure char.
114 *
115 * @var bool
116 */
117 public $enclose_all = false;
118
119 /**
120 * Basic SQL-Like conditions for row matching
121 *
122 * @var string|null
123 */
124 public $conditions = null;
125
126 /**
127 * Number of rows to ignore from beginning of data. If present, the heading
128 * row is also counted (if $this->heading == true). In other words,
129 * $offset == 1 and $offset == 0 have the same meaning in that situation.
130 *
131 * @var int|null
132 */
133 public $offset = null;
134
135 /**
136 * Limits the number of returned rows to the specified amount
137 *
138 * @var int|null
139 */
140 public $limit = null;
141
142 /**
143 * Number of rows to analyze when attempting to auto-detect delimiter
144 *
145 * @var int
146 */
147 public $auto_depth = 15;
148
149 /**
150 * Characters that should be ignored when attempting to auto-detect delimiter
151 *
152 * @var string
153 */
154 public $auto_non_chars = "a-zA-Z0-9\n\r";
155
156 /**
157 * preferred delimiter characters, only used when all filtering method
158 * returns multiple possible delimiters (happens very rarely)
159 *
160 * @var string
161 */
162 public $auto_preferred = ",;\t.:|";
163
164 /**
165 * Should we convert the CSV character encoding?
166 * Used for both parse and unparse operations.
167 *
168 * @var bool
169 */
170 public $convert_encoding = false;
171
172 /**
173 * Set the input encoding
174 *
175 * @var string
176 */
177 public $input_encoding = 'ISO-8859-1';
178
179 /**
180 * Set the output encoding
181 *
182 * @var string
183 */
184 public $output_encoding = 'ISO-8859-1';
185
186 /**
187 * Whether to use mb_convert_encoding() instead of iconv().
188 *
189 * The former is platform-independent whereas the latter is the traditional
190 * default go-to solution.
191 *
192 * @var bool (if false, iconv() is used)
193 */
194 public $use_mb_convert_encoding = false;
195
196 /**
197 * Line feed characters used by unparse, save, and output methods
198 * Popular choices are "\r\n" and "\n".
199 *
200 * @var string
201 */
202 public $linefeed = "\r";
203
204 /**
205 * Sets the output delimiter used by the output method
206 *
207 * @var string
208 */
209 public $output_delimiter = ',';
210
211 /**
212 * Sets the output filename
213 *
214 * @var string
215 */
216 public $output_filename = 'data.csv';
217
218 /**
219 * keep raw file data in memory after successful parsing (useful for debugging)
220 *
221 * @var bool
222 */
223 public $keep_file_data = false;
224
225 /**
226 * Internal variables
227 */
228
229 /**
230 * File
231 * Current Filename
232 *
233 * @var string
234 */
235 public $file;
236
237 /**
238 * File Data
239 * Current file data
240 *
241 * @var string
242 */
243 public $file_data;
244
245 /**
246 * Error
247 * Contains the error code if one occurred
248 *
249 * 0 = No errors found. Everything should be fine :)
250 * 1 = Hopefully correctable syntax error was found.
251 * 2 = Enclosure character (double quote by default)
252 * was found in non-enclosed field. This means
253 * the file is either corrupt, or does not
254 * standard CSV formatting. Please validate
255 * the parsed data yourself.
256 *
257 * @var int
258 */
259 public $error = 0;
260
261 /**
262 * Detailed error information
263 *
264 * @var array
265 */
266 public $error_info = array();
267
268 /**
269 * $titles has 4 distinct tasks:
270 * 1. After reading in CSV data, $titles will contain the column headers
271 * present in the data.
272 *
273 * 2. It defines which fields from the $data array to write e.g. when
274 * calling unparse(), and in which order. This lets you skip columns you
275 * don't want in your output, but are present in $data.
276 * See examples/save_to_file_without_header_row.php.
277 *
278 * 3. It lets you rename columns. See StreamTest::testWriteStream for an
279 * example.
280 *
281 * 4. When writing data and $header is true, then $titles is also used for
282 * the first row.
283 *
284 * @var array
285 */
286 public $titles = array();
287
288 /**
289 * Two-dimensional array of CSV data.
290 * The first dimension are the line numbers. Each line is represented as an array with field names as keys.
291 *
292 * @var array<array>
293 */
294 public $data = array();
295
296 use DatatypeTrait;
297
298 /**
299 * Class constructor
300 *
301 * @param string|null $data The CSV string or a direct file path.
302 *
303 * WARNING: Supplying file paths here is
304 * deprecated. Use parseFile() instead.
305 *
306 * @param int|null $offset Number of rows to ignore from the
307 * beginning of the data
308 * @param int|null $limit Limits the number of returned rows
309 * to specified amount
310 * @param string|null $conditions Basic SQL-like conditions for row
311 * matching
312 * @param null|true $keep_file_data Keep raw file data in memory after
313 * successful parsing
314 * (useful for debugging)
315 */
316 public function __construct($data = null, $offset = null, $limit = null, $conditions = null, $keep_file_data = null) {
317 $this->init($offset, $limit, $conditions, $keep_file_data);
318
319 if (!empty($data)) {
320 $this->parse($data);
321 }
322 }
323
324 /**
325 * @param int|null $offset Number of rows to ignore from the
326 * beginning of the data
327 * @param int|null $limit Limits the number of returned rows
328 * to specified amount
329 * @param string|null $conditions Basic SQL-like conditions for row
330 * matching
331 * @param null|true $keep_file_data Keep raw file data in memory after
332 * successful parsing
333 * (useful for debugging)
334 */
335 public function init($offset = null, $limit = null, $conditions = null, $keep_file_data = null) {
336 if (!is_null($offset)) {
337 $this->offset = $offset;
338 }
339
340 if (!is_null($limit)) {
341 $this->limit = $limit;
342 }
343
344 if (!is_null($conditions)) {
345 $this->conditions = $conditions;
346 }
347
348 if (!is_null($keep_file_data)) {
349 $this->keep_file_data = $keep_file_data;
350 }
351 }
352
353 // ==============================================
354 // ----- [ Main Functions ] ---------------------
355 // ==============================================
356
357 /**
358 * Parse a CSV file or string
359 *
360 * @param string|null $dataString The CSV string or a direct file path
361 * WARNING: Supplying file paths here is
362 * deprecated and will trigger an
363 * E_USER_DEPRECATED error.
364 * @param int|null $offset Number of rows to ignore from the
365 * beginning of the data
366 * @param int|null $limit Limits the number of returned rows to
367 * specified amount
368 * @param string|null $conditions Basic SQL-like conditions for row
369 * matching
370 *
371 * @return bool True on success
372 */
373 public function parse($dataString = null, $offset = null, $limit = null, $conditions = null) {
374 if (is_null($dataString)) {
375 $this->data = $this->parseFile();
376 return $this->data !== false;
377 }
378
379 if (empty($dataString)) {
380 return false;
381 }
382
383 $this->init($offset, $limit, $conditions);
384
385 if (strlen($dataString) <= PHP_MAXPATHLEN && is_readable($dataString)) {
386 $this->file = $dataString;
387 $this->data = $this->parseFile();
388 trigger_error(
389 'Supplying file paths to parse() will no longer ' .
390 'be supported in a future version of ParseCsv. ' .
391 'Use ->parseFile() instead.',
392 E_USER_DEPRECATED
393 );
394 } else {
395 $this->file = null;
396 $this->file_data = &$dataString;
397 $this->data = $this->_parse_string();
398 }
399
400 return $this->data !== false;
401 }
402
403 /**
404 * Save changes, or write a new file and/or data.
405 *
406 * @param string $file File location to save to
407 * @param array $data 2D array of data
408 * @param bool $append Append current data to end of target CSV, if file
409 * exists
410 * @param array $fields Field names. Sets the header. If it is not set
411 * $this->titles would be used instead.
412 *
413 * @return bool
414 * True on success
415 */
416 public function save($file = '', $data = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $fields = array()) {
417 if (empty($file)) {
418 $file = &$this->file;
419 }
420
421 $mode = FileProcessingModeEnum::getAppendMode($append);
422 $is_php = preg_match('/\.php$/i', $file) ? true : false;
423
424 return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode);
425 }
426
427 /**
428 * Generate a CSV-based string for output.
429 *
430 * Useful for exports in web applications.
431 *
432 * @param string|null $filename If a filename is specified here or in the
433 * object, headers and data will be output
434 * directly to browser as a downloadable
435 * file. This file doesn't have to exist on
436 * the server; the parameter only affects
437 * how the download is called to the
438 * browser.
439 * @param array[] $data 2D array with data
440 * @param array $fields Field names
441 * @param string|null $delimiter character used to separate data
442 *
443 * @return string The resulting CSV string
444 */
445 public function output($filename = null, $data = array(), $fields = array(), $delimiter = null) {
446 if (empty($filename)) {
447 $filename = $this->output_filename;
448 }
449
450 if ($delimiter === null) {
451 $delimiter = $this->output_delimiter;
452 }
453
454 $flat_string = $this->unparse($data, $fields, null, null, $delimiter);
455
456 if (!is_null($filename)) {
457 $mime = $delimiter === "\t" ?
458 'text/tab-separated-values' :
459 'application/csv';
460 header('Content-type: ' . $mime);
461 header('Content-Length: ' . strlen($flat_string));
462 header('Cache-Control: no-cache, must-revalidate');
463 header('Pragma: no-cache');
464 header('Expires: 0');
465 header('Content-Disposition: attachment; filename="' . $filename . '"; modification-date="' . date('r') . '";');
466
467 echo $flat_string;
468 }
469
470 return $flat_string;
471 }
472
473 /**
474 * Convert character encoding
475 *
476 * Specify the encoding to use for the next parsing or unparsing.
477 * Calling this function will not change the data held in the object immediately.
478 *
479 * @param string|null $input Input character encoding
480 * If the value null is passed, the existing input encoding remains set (default: ISO-8859-1).
481 * @param string|null $output Output character encoding, uses default if left blank
482 * If the value null is passed, the existing input encoding remains set (default: ISO-8859-1).
483 *
484 * @return void
485 */
486 public function encoding($input = null, $output = null) {
487 $this->convert_encoding = true;
488 if (!is_null($input)) {
489 $this->input_encoding = $input;
490 }
491
492 if (!is_null($output)) {
493 $this->output_encoding = $output;
494 }
495 }
496
497 /**
498 * Auto-detect delimiter: Find delimiter by analyzing a specific number of
499 * rows to determine most probable delimiter character
500 *
501 * @param string|null $file Local CSV file
502 * Supplying CSV data (file content) here is deprecated.
503 * For CSV data, please use autoDetectionForDataString().
504 * Support for CSV data will be removed in v2.0.0.
505 * @param bool $parse True/false parse file directly
506 * @param int|null $search_depth Number of rows to analyze
507 * @param string|null $preferred Preferred delimiter characters
508 * @param string|null $enclosure Enclosure character, default is double quote (").
509 *
510 * @return string|false The detected field delimiter
511 */
512 public function auto($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) {
513 if (is_null($file)) {
514 $file = $this->file;
515 }
516
517 if (empty($search_depth)) {
518 $search_depth = $this->auto_depth;
519 }
520
521 if (is_null($enclosure)) {
522 $enclosure = $this->enclosure;
523 } else {
524 $this->enclosure = $enclosure;
525 }
526
527 if (is_null($preferred)) {
528 $preferred = $this->auto_preferred;
529 }
530
531 if (empty($this->file_data)) {
532 if ($this->_check_data($file)) {
533 $data = &$this->file_data;
534 } else {
535 return false;
536 }
537 } else {
538 $data = &$this->file_data;
539 }
540
541 $this->autoDetectionForDataString($data, $parse, $search_depth, $preferred, $enclosure);
542
543 return $this->delimiter;
544 }
545
546 public function autoDetectionForDataString($data, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) {
547 $this->file_data = &$data;
548 if (!$this->_detect_and_remove_sep_row_from_data($data)) {
549 $this->_guess_delimiter($search_depth, $preferred, $enclosure, $data);
550 }
551
552 // parse data
553 if ($parse) {
554 $this->data = $this->_parse_string();
555 }
556
557 return $this->delimiter;
558 }
559
560 /**
561 * Get total number of data rows (exclusive heading line if present) in CSV
562 * without parsing the whole data string.
563 *
564 * @return bool|int
565 */
566 public function getTotalDataRowCount() {
567 if (empty($this->file_data)) {
568 return false;
569 }
570
571 $data = $this->file_data;
572
573 $this->_detect_and_remove_sep_row_from_data($data);
574
575 $pattern = sprintf('/%1$s[^%1$s]*%1$s/i', $this->enclosure);
576 preg_match_all($pattern, $data, $matches);
577
578 /** @var array[] $matches */
579 foreach ($matches[0] as $match) {
580 if (empty($match) || (strpos($match, $this->enclosure) === false)) {
581 continue;
582 }
583
584 $replace = str_replace(["\r", "\n"], '', $match);
585 $data = str_replace($match, $replace, $data);
586 }
587
588 $headingRow = $this->heading ? 1 : 0;
589
590 return substr_count($data, "\r")
591 + substr_count($data, "\n")
592 - substr_count($data, "\r\n")
593 - $headingRow;
594 }
595
596 // ==============================================
597 // ----- [ Core Functions ] ---------------------
598 // ==============================================
599
600 /**
601 * Read file to string and call _parse_string()
602 *
603 * @param string|null $file Path to a CSV file.
604 * If configured in files such as php.ini,
605 * the path may also contain a protocol:
606 * https://example.org/some/file.csv
607 *
608 * @return array<array>|false
609 */
610 public function parseFile($file = null) {
611 if (is_null($file)) {
612 $file = $this->file;
613 }
614
615 /**
616 * @see self::keep_file_data
617 * Usually, _parse_string will clean this
618 * Instead of leaving stale data for the next parseFile call behind.
619 */
620 if (empty($this->file_data) && !$this->loadFile($file)) {
621 return false;
622 }
623
624 if (empty($this->file_data)) {
625 return false;
626 }
627 return $this->data = $this->_parse_string();
628 }
629
630 /**
631 * Internal function to parse CSV strings to arrays.
632 *
633 * If you need BOM detection or character encoding conversion, please call
634 * $csv->load_data($your_data_string) first, followed by a call to
635 * $csv->parse($csv->file_data).
636 *
637 * To detect field separators, please use auto() instead.
638 *
639 * @param string|null $data CSV data
640 *
641 * @return array<array>|false
642 * 2D array with CSV data, or false on failure
643 */
644 protected function _parse_string($data = null) {
645 if (empty($data)) {
646 if ($this->_check_data()) {
647 $data = &$this->file_data;
648 } else {
649 return false;
650 }
651 }
652
653 $white_spaces = str_replace($this->delimiter, '', " \t\x0B\0");
654
655 $rows = array();
656 $row = array();
657 $row_count = 0;
658 $current = '';
659 $head = !empty($this->fields) ? $this->fields : array();
660 $col = 0;
661 $enclosed = false;
662 $was_enclosed = false;
663 $strlen = strlen($data);
664
665 // force the parser to process end of data as a character (false) when
666 // data does not end with a line feed or carriage return character.
667 $lch = $data[$strlen - 1];
668 if ($lch != "\n" && $lch != "\r") {
669 $data .= "\n";
670 $strlen++;
671 }
672
673 // walk through each character
674 for ($i = 0; $i < $strlen; $i++) {
675 $ch = isset($data[$i]) ? $data[$i] : false;
676 $nch = isset($data[$i + 1]) ? $data[$i + 1] : false;
677
678 // open/close quotes, and inline quotes
679 if ($ch == $this->enclosure) {
680 if (!$enclosed) {
681 if (ltrim($current, $white_spaces) == '') {
682 $enclosed = true;
683 $was_enclosed = true;
684 } else {
685 $this->error = 2;
686 $error_row = count($rows) + 1;
687 $error_col = $col + 1;
688 $index = $error_row . '-' . $error_col;
689 if (!isset($this->error_info[$index])) {
690 $this->error_info[$index] = array(
691 'type' => 2,
692 'info' => 'Syntax error found on row ' . $error_row . '. Non-enclosed fields can not contain double-quotes.',
693 'row' => $error_row,
694 'field' => $error_col,
695 'field_name' => !empty($head[$col]) ? $head[$col] : null,
696 );
697 }
698
699 $current .= $ch;
700 }
701 } elseif ($nch == $this->enclosure) {
702 $current .= $ch;
703 $i++;
704 } elseif ($nch != $this->delimiter && $nch != "\r" && $nch != "\n") {
705 $x = $i + 1;
706 while (isset($data[$x]) && ltrim($data[$x], $white_spaces) == '') {
707 $x++;
708 }
709 if ($data[$x] == $this->delimiter) {
710 $enclosed = false;
711 $i = $x;
712 } else {
713 if ($this->error < 1) {
714 $this->error = 1;
715 }
716
717 $error_row = count($rows) + 1;
718 $error_col = $col + 1;
719 $index = $error_row . '-' . $error_col;
720 if (!isset($this->error_info[$index])) {
721 $this->error_info[$index] = array(
722 'type' => 1,
723 'info' =>
724 'Syntax error found on row ' . (count($rows) + 1) . '. ' .
725 'A single double-quote was found within an enclosed string. ' .
726 'Enclosed double-quotes must be escaped with a second double-quote.',
727 'row' => count($rows) + 1,
728 'field' => $col + 1,
729 'field_name' => !empty($head[$col]) ? $head[$col] : null,
730 );
731 }
732
733 $current .= $ch;
734 $enclosed = false;
735 }
736 } else {
737 $enclosed = false;
738 }
739 // end of field/row/csv
740 } elseif ((in_array($ch, [$this->delimiter, "\n", "\r", false], true)) && !$enclosed) {
741 $key = !empty($head[$col]) ? $head[$col] : $col;
742 $row[$key] = $was_enclosed ? $current : trim($current);
743 $current = '';
744 $was_enclosed = false;
745 $col++;
746
747 // end of row
748 if (in_array($ch, ["\n", "\r", false], true)) {
749 if ($this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions)) {
750 if ($this->heading && empty($head)) {
751 $head = $row;
752 } elseif (empty($this->fields) || (!empty($this->fields) && (($this->heading && $row_count > 0) || !$this->heading))) {
753 if (!empty($this->sort_by) && !empty($row[$this->sort_by])) {
754 $sort_field = $row[$this->sort_by];
755 if (isset($rows[$sort_field])) {
756 $rows[$sort_field . '_0'] = &$rows[$sort_field];
757 unset($rows[$sort_field]);
758 $sn = 1;
759 while (isset($rows[$sort_field . '_' . $sn])) {
760 $sn++;
761 }
762 $rows[$sort_field . '_' . $sn] = $row;
763 } else {
764 $rows[$sort_field] = $row;
765 }
766
767 } else {
768 $rows[] = $row;
769 }
770 }
771 }
772
773 $row = array();
774 $col = 0;
775 $row_count++;
776
777 if ($this->sort_by === null && $this->limit !== null && count($rows) == $this->limit) {
778 $i = $strlen;
779 }
780
781 if ($ch == "\r" && $nch == "\n") {
782 $i++;
783 }
784 }
785
786 // append character to current field
787 } else {
788 $current .= $ch;
789 }
790 }
791
792 $this->titles = $head;
793 if (!empty($this->sort_by)) {
794 $sort_type = SortEnum::getSorting($this->sort_type);
795 $this->sort_reverse ? krsort($rows, $sort_type) : ksort($rows, $sort_type);
796
797 if ($this->offset !== null || $this->limit !== null) {
798 $rows = array_slice($rows, ($this->offset === null ? 0 : $this->offset), $this->limit, true);
799 }
800 }
801
802 if (!$this->keep_file_data) {
803 $this->file_data = null;
804 }
805
806 return $rows;
807 }
808
809 /**
810 * Create CSV data string from array
811 *
812 * @param array[] $data 2D array with data
813 * @param array $fields field names
814 * @param bool $append if true, field names will not be output
815 * @param bool $is_php if a php die() call should be put on the
816 * first line of the file, this is later
817 * ignored when read.
818 * @param string|null $delimiter field delimiter to use
819 *
820 * @return string CSV data
821 */
822 public function unparse($data = array(), $fields = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $is_php = false, $delimiter = null) {
823 if (!is_array($data) || empty($data)) {
824 $data = &$this->data;
825 } else {
826 /** @noinspection ReferenceMismatchInspection */
827 $this->data = $data;
828 }
829
830 if (!is_array($fields) || empty($fields)) {
831 $fields = &$this->titles;
832 }
833
834 if ($delimiter === null) {
835 $delimiter = $this->delimiter;
836 }
837
838 $string = $is_php ? "<?php header('Status: 403'); die(' '); ?>" . $this->linefeed : '';
839 $entry = array();
840
841 // create heading
842 /** @noinspection ReferenceMismatchInspection */
843 $fieldOrder = $this->_validate_fields_for_unparse($fields);
844 if (!$fieldOrder && !empty($data)) {
845 $column_count = count($data[0]);
846 $columns = range(0, $column_count - 1, 1);
847 $fieldOrder = array_combine($columns, $columns);
848 }
849
850 if ($this->heading && !$append && !empty($fields)) {
851 foreach ($fieldOrder as $column_name) {
852 $entry[] = $this->_enclose_value($column_name, $delimiter);
853 }
854
855 $string .= implode($delimiter, $entry) . $this->linefeed;
856 $entry = array();
857 }
858
859 // create data
860 foreach ($data as $key => $row) {
861 foreach (array_keys($fieldOrder) as $index) {
862 $cell_value = $row[$index];
863 $entry[] = $this->_enclose_value($cell_value, $delimiter);
864 }
865
866 $string .= implode($delimiter, $entry) . $this->linefeed;
867 $entry = array();
868 }
869
870 if ($this->convert_encoding) {
871 /** @noinspection PhpComposerExtensionStubsInspection
872 *
873 * If you receive an error at the following 3 lines, you must enable
874 * the following PHP extension:
875 *
876 * - if $use_mb_convert_encoding is true: mbstring
877 * - if $use_mb_convert_encoding is false: iconv
878 */
879 $string = $this->use_mb_convert_encoding ?
880 mb_convert_encoding($string, $this->output_encoding, $this->input_encoding) :
881 iconv($this->input_encoding, $this->output_encoding, $string);
882 }
883
884 return $string;
885 }
886
887 /**
888 * @param array $fields
889 *
890 * @return array|false
891 */
892 private function _validate_fields_for_unparse(array $fields) {
893 if (empty($fields)) {
894 $fields = $this->titles;
895 }
896
897 if (empty($fields)) {
898 return array();
899 }
900
901 // this is needed because sometime titles property is overwritten instead of using fields parameter!
902 $titlesOnParse = !empty($this->data) ? array_keys(reset($this->data)) : array();
903
904 // both are identical, also in ordering OR we have no data (only titles)
905 if (empty($titlesOnParse) || array_values($fields) === array_values($titlesOnParse)) {
906 return array_combine($fields, $fields);
907 }
908
909 // if renaming given by: $oldName => $newName (maybe with reorder and / or subset):
910 // todo: this will only work if titles are unique
911 $fieldOrder = array_intersect(array_flip($fields), $titlesOnParse);
912 if (!empty($fieldOrder)) {
913 return array_flip($fieldOrder);
914 }
915
916 $fieldOrder = array_intersect($fields, $titlesOnParse);
917 if (!empty($fieldOrder)) {
918 return array_combine($fieldOrder, $fieldOrder);
919 }
920
921 // original titles are not given in fields. that is okay if count is okay.
922 if (count($fields) != count($titlesOnParse)) {
923 throw new \UnexpectedValueException(
924 "The specified fields do not match any titles and do not match column count.\n" .
925 "\$fields was " . print_r($fields, true) .
926 "\$titlesOnParse was " . print_r($titlesOnParse, true));
927 }
928
929 return array_combine($titlesOnParse, $fields);
930 }
931
932 /**
933 * Load local file or string.
934 *
935 * Only use this function if auto() and parse() don't handle your data well.
936 *
937 * This function load_data() is able to handle BOMs and encodings. The data
938 * is stored within the $this->file_data class field.
939 *
940 * @param string|null $input CSV file path or CSV data as a string
941 *
942 * Supplying CSV data (file content) here is deprecated.
943 * For CSV data, please use loadDataString().
944 * Support for CSV data will be removed in v2.0.0.
945 *
946 * @return bool True on success
947 * @deprecated Use loadDataString() or loadFile() instead.
948 */
949 public function load_data($input = null) {
950 return $this->loadFile($input);
951 }
952
953 /**
954 * Load a file, but don't parse it.
955 *
956 * Only use this function if auto() and parseFile() don't handle your data well.
957 *
958 * This function is able to handle BOMs and encodings. The data
959 * is stored within the $this->file_data class field.
960 *
961 * @param string|null $file CSV file path
962 *
963 * @return bool True on success
964 */
965 public function loadFile($file = null) {
966 $data = null;
967
968 if (is_null($file)) {
969 $data = $this->_rfile($this->file);
970 } elseif (\strlen($file) <= PHP_MAXPATHLEN && file_exists($file)) {
971 $data = $this->_rfile($file);
972 if ($this->file != $file) {
973 $this->file = $file;
974 }
975 } else {
976 // It is CSV data as a string.
977
978 // WARNING:
979 // Supplying CSV data to load_data() will no longer
980 // be supported in a future version of ParseCsv.
981 // This function will return false for invalid paths from v2.0.0 onwards.
982
983 // Use ->loadDataString() instead.
984
985 $data = $file;
986 }
987
988 return $this->loadDataString($data);
989 }
990
991 /**
992 * Load a data string, but don't parse it.
993 *
994 * Only use this function if autoDetectionForDataString() and parse() don't handle your data well.
995 *
996 * This function is able to handle BOMs and encodings. The data
997 * is stored within the $this->file_data class field.
998 *
999 * @param string|null $file_path CSV file path
1000 *
1001 * @return bool True on success
1002 */
1003 public function loadDataString($data) {
1004 if (!empty($data)) {
1005 if (strpos($data, "\xef\xbb\xbf") === 0) {
1006 // strip off BOM (UTF-8)
1007 $data = substr($data, 3);
1008 $this->encoding('UTF-8');
1009 } elseif (strpos($data, "\xff\xfe") === 0) {
1010 // strip off BOM (UTF-16 little endian)
1011 $data = substr($data, 2);
1012 $this->encoding("UCS-2LE");
1013 } elseif (strpos($data, "\xfe\xff") === 0) {
1014 // strip off BOM (UTF-16 big endian)
1015 $data = substr($data, 2);
1016 $this->encoding("UTF-16");
1017 }
1018
1019 if ($this->convert_encoding && $this->input_encoding !== $this->output_encoding) {
1020 /** @noinspection PhpComposerExtensionStubsInspection
1021 *
1022 * If you receive an error at the following 3 lines, you must enable
1023 * the following PHP extension:
1024 *
1025 * - if $use_mb_convert_encoding is true: mbstring
1026 * - if $use_mb_convert_encoding is false: iconv
1027 */
1028 $data = $this->use_mb_convert_encoding ?
1029 mb_convert_encoding($data, $this->output_encoding, $this->input_encoding) :
1030 iconv($this->input_encoding, $this->output_encoding, $data);
1031 }
1032
1033 if (substr($data, -1) != "\n") {
1034 $data .= "\n";
1035 }
1036
1037 $this->file_data = &$data;
1038 return true;
1039 }
1040
1041 return false;
1042 }
1043
1044 // ==============================================
1045 // ----- [ Internal Functions ] -----------------
1046 // ==============================================
1047
1048 /**
1049 * Validate a row against specified conditions
1050 *
1051 * @param array $row array with values from a row
1052 * @param string|null $conditions specified conditions that the row must match
1053 *
1054 * @return bool
1055 */
1056 protected function _validate_row_conditions($row = array(), $conditions = null) {
1057 if (!empty($row)) {
1058 if (!empty($conditions)) {
1059 $condition_array = (strpos($conditions, ' OR ') !== false) ?
1060 explode(' OR ', $conditions) :
1061 array($conditions);
1062 $or = '';
1063 foreach ($condition_array as $key => $value) {
1064 if (strpos($value, ' AND ') !== false) {
1065 $value = explode(' AND ', $value);
1066 $and = '';
1067
1068 foreach ($value as $k => $v) {
1069 $and .= $this->_validate_row_condition($row, $v);
1070 }
1071
1072 $or .= (strpos($and, '0') !== false) ? '0' : '1';
1073 } else {
1074 $or .= $this->_validate_row_condition($row, $value);
1075 }
1076 }
1077
1078 return strpos($or, '1') !== false;
1079 }
1080
1081 return true;
1082 }
1083
1084 return false;
1085 }
1086
1087 /**
1088 * Validate a row against a single condition
1089 *
1090 * @param array $row array with values from a row
1091 * @param string $condition specified condition that the row must match
1092 *
1093 * @return string single 0 or 1
1094 */
1095 protected function _validate_row_condition($row, $condition) {
1096 $operators = array(
1097 '=',
1098 'equals',
1099 'is',
1100 '!=',
1101 'is not',
1102 '<',
1103 'is less than',
1104 '>',
1105 'is greater than',
1106 '<=',
1107 'is less than or equals',
1108 '>=',
1109 'is greater than or equals',
1110 'contains',
1111 'does not contain',
1112 );
1113
1114 $operators_regex = array();
1115
1116 foreach ($operators as $value) {
1117 $operators_regex[] = preg_quote($value, '/');
1118 }
1119
1120 $operators_regex = implode('|', $operators_regex);
1121
1122 if (preg_match('/^(.+) (' . $operators_regex . ') (.+)$/i', trim($condition), $capture)) {
1123 $field = $capture[1];
1124 $op = strtolower($capture[2]);
1125 $value = $capture[3];
1126 if ($op == 'equals' && preg_match('/^(.+) is (less|greater) than or$/i', $field, $m)) {
1127 $field = $m[1];
1128 $op = strtolower($m[2]) == 'less' ? '<=' : '>=';
1129 }
1130 if ($op == 'is' && preg_match('/^(less|greater) than (.+)$/i', $value, $m)) {
1131 $value = $m[2];
1132 $op = strtolower($m[1]) == 'less' ? '<' : '>';
1133 }
1134 if ($op == 'is' && preg_match('/^not (.+)$/i', $value, $m)) {
1135 $value = $m[1];
1136 $op = '!=';
1137 }
1138
1139 if (preg_match('/^([\'"])(.*)([\'"])$/', $value, $capture) && $capture[1] == $capture[3]) {
1140 $value = strtr($capture[2], array(
1141 "\\n" => "\n",
1142 "\\r" => "\r",
1143 "\\t" => "\t",
1144 ));
1145
1146 $value = stripslashes($value);
1147 }
1148
1149 if (array_key_exists($field, $row)) {
1150 $op_equals = in_array($op, ['=', 'equals', 'is'], true);
1151 if ($op_equals && $row[$field] == $value) {
1152 return '1';
1153 } elseif (($op == '!=' || $op == 'is not') && $row[$field] != $value) {
1154 return '1';
1155 } elseif (($op == '<' || $op == 'is less than') && $row[$field] < $value) {
1156 return '1';
1157 } elseif (($op == '>' || $op == 'is greater than') && $row[$field] > $value) {
1158 return '1';
1159 } elseif (($op == '<=' || $op == 'is less than or equals') && $row[$field] <= $value) {
1160 return '1';
1161 } elseif (($op == '>=' || $op == 'is greater than or equals') && $row[$field] >= $value) {
1162 return '1';
1163 } elseif ($op == 'contains' && preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) {
1164 return '1';
1165 } elseif ($op == 'does not contain' && !preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) {
1166 return '1';
1167 } else {
1168 return '0';
1169 }
1170 }
1171 }
1172
1173 return '1';
1174 }
1175
1176 /**
1177 * Validates if the row is within the offset or not if sorting is disabled
1178 *
1179 * @param int $current_row the current row number being processed
1180 *
1181 * @return bool
1182 */
1183 protected function _validate_offset($current_row) {
1184 return
1185 $this->sort_by !== null ||
1186 $this->offset === null ||
1187 $current_row >= $this->offset ||
1188 ($this->heading && $current_row == 0);
1189 }
1190
1191 /**
1192 * Enclose values if needed
1193 * - only used by unparse()
1194 *
1195 * @param string|null $value Cell value to process
1196 * @param string $delimiter Character to put between cells on the same row
1197 *
1198 * @return string Processed value
1199 */
1200 protected function _enclose_value($value, $delimiter) {
1201 if ($value !== null && $value != '') {
1202 $delimiter_quoted = $delimiter ?
1203 preg_quote($delimiter, '/') . "|"
1204 : '';
1205 $enclosure_quoted = preg_quote($this->enclosure, '/');
1206 $pattern = "/" . $delimiter_quoted . $enclosure_quoted . "|\n|\r/i";
1207 if ($this->enclose_all || preg_match($pattern, $value) || strpos($value, ' ') === 0 || substr($value, -1) == ' ') {
1208 $value = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $value);
1209 $value = $this->enclosure . $value . $this->enclosure;
1210 }
1211 }
1212
1213 return $value;
1214 }
1215
1216 /**
1217 * Check file data
1218 *
1219 * @param string|null $file local filename
1220 *
1221 * @return bool
1222 */
1223 protected function _check_data($file = null) {
1224 if (empty($this->file_data)) {
1225 if (is_null($file)) {
1226 $file = $this->file;
1227 }
1228
1229 return $this->loadFile($file);
1230 }
1231
1232 return true;
1233 }
1234
1235 /**
1236 * Check if passed info might be delimiter.
1237 * Only used by find_delimiter
1238 *
1239 * @param string $char Potential field separating character
1240 * @param array $array Frequency
1241 * @param int $depth Number of analyzed rows
1242 * @param string $preferred Preferred delimiter characters
1243 *
1244 * @return string|false special string used for delimiter selection, or false
1245 */
1246 protected function _check_count($char, $array, $depth, $preferred) {
1247 if ($depth === count($array)) {
1248 $first = null;
1249 $equal = null;
1250 $almost = false;
1251 foreach ($array as $value) {
1252 if ($first == null) {
1253 $first = $value;
1254 } elseif ($value == $first && $equal !== false) {
1255 $equal = true;
1256 } elseif ($value == $first + 1 && $equal !== false) {
1257 $equal = true;
1258 $almost = true;
1259 } else {
1260 $equal = false;
1261 }
1262 }
1263
1264 if ($equal || $depth === 1) {
1265 $match = $almost ? 2 : 1;
1266 $pref = strpos($preferred, $char);
1267 $pref = ($pref !== false) ? str_pad($pref, 3, '0', STR_PAD_LEFT) : '999';
1268
1269 return $pref . $match . '.' . (99999 - str_pad($first, 5, '0', STR_PAD_LEFT));
1270 } else {
1271 return false;
1272 }
1273 }
1274 return false;
1275 }
1276
1277 /**
1278 * Read local file.
1279 *
1280 * @param string $filePath local filename
1281 *
1282 * @return string|false Data from file, or false on failure
1283 */
1284 protected function _rfile($filePath) {
1285 if (is_readable($filePath)) {
1286 $data = file_get_contents($filePath);
1287 if ($data === false) {
1288 return false;
1289 }
1290
1291 if (preg_match('/\.php$/i', $filePath) && preg_match('/<\?.*?\?>(.*)/ms', $data, $strip)) {
1292 // Return section behind closing tags.
1293 // This parsing is deprecated and will be removed in v2.0.0.
1294 $data = ltrim($strip[1]);
1295 }
1296
1297 return rtrim($data, "\r\n");
1298 }
1299
1300 return false;
1301 }
1302
1303 /**
1304 * Write to local file
1305 *
1306 * @param string $file local filename
1307 * @param string $content data to write to file
1308 * @param string $mode fopen() mode
1309 * @param int $lock flock() mode
1310 *
1311 * @return bool
1312 * True on success
1313 *
1314 */
1315 protected function _wfile($file, $content = '', $mode = 'wb', $lock = LOCK_EX) {
1316 if ($fp = fopen($file, $mode)) {
1317 flock($fp, $lock);
1318 $re = fwrite($fp, $content);
1319 $re2 = fclose($fp);
1320
1321 if ($re !== false && $re2 !== false) {
1322 return true;
1323 }
1324 }
1325
1326 return false;
1327 }
1328
1329 /**
1330 * Detect separator using a nonstandard hack: such file starts with the
1331 * first line containing only "sep=;", where the last character is the
1332 * separator. Microsoft Excel is able to open such files.
1333 *
1334 * @param string $data file data
1335 *
1336 * @return string|false detected delimiter, or false if none found
1337 */
1338 protected function _get_delimiter_from_sep_row($data) {
1339 $sep = false;
1340 // 32 bytes should be quite enough data for our sniffing, chosen arbitrarily
1341 $sepPrefix = substr($data, 0, 32);
1342 if (preg_match('/^sep=(.)\\r?\\n/i', $sepPrefix, $sepMatch)) {
1343 // we get separator.
1344 $sep = $sepMatch[1];
1345 }
1346 return $sep;
1347 }
1348
1349 /**
1350 * Support for Excel-compatible sep=? row.
1351 *
1352 * @param string $data_string file data to be updated
1353 *
1354 * @return bool TRUE if sep= line was found at the very beginning of the file
1355 */
1356 protected function _detect_and_remove_sep_row_from_data(&$data_string) {
1357 $sep = $this->_get_delimiter_from_sep_row($data_string);
1358 if ($sep === false) {
1359 return false;
1360 }
1361
1362 $this->delimiter = $sep;
1363
1364 // likely to be 5, but let's not assume we're always single-byte.
1365 $pos = 4 + strlen($sep);
1366 // the next characters should be a line-end
1367 if (substr($data_string, $pos, 1) === "\r") {
1368 $pos++;
1369 }
1370 if (substr($data_string, $pos, 1) === "\n") {
1371 $pos++;
1372 }
1373
1374 // remove delimiter and its line-end (the data param is by-ref!)
1375 $data_string = substr($data_string, $pos);
1376 return true;
1377 }
1378
1379 /**
1380 * @param int $search_depth Number of rows to analyze
1381 * @param string $preferred Preferred delimiter characters
1382 * @param string $enclosure Enclosure character, default is double quote
1383 * @param string $data The file content
1384 */
1385 protected function _guess_delimiter($search_depth, $preferred, $enclosure, $data) {
1386 $chars = [];
1387 $strlen = strlen($data);
1388 $enclosed = false;
1389 $current_row = 1;
1390 $to_end = true;
1391
1392 // The dash is the only character we don't want quoted, as it would
1393 // prevent character ranges within $auto_non_chars:
1394 $quoted_auto_non_chars = preg_quote($this->auto_non_chars, '/');
1395 $quoted_auto_non_chars = str_replace('\-', '-', $quoted_auto_non_chars);
1396 $pattern = '/[' . $quoted_auto_non_chars . ']/i';
1397
1398 // walk specific depth finding possible delimiter characters
1399 for ($i = 0; $i < $strlen; $i++) {
1400 $ch = $data[$i];
1401 $nch = isset($data[$i + 1]) ? $data[$i + 1] : false;
1402 $pch = isset($data[$i - 1]) ? $data[$i - 1] : false;
1403
1404 // open and closing quotes
1405 $is_newline = ($ch == "\n" && $pch != "\r") || $ch == "\r";
1406 if ($ch == $enclosure) {
1407 if (!$enclosed || $nch != $enclosure) {
1408 $enclosed = !$enclosed;
1409 } elseif ($enclosed) {
1410 $i++;
1411 }
1412
1413 // end of row
1414 } elseif ($is_newline && !$enclosed) {
1415 if ($current_row >= $search_depth) {
1416 $strlen = 0;
1417 $to_end = false;
1418 } else {
1419 $current_row++;
1420 }
1421
1422 // count character
1423 } elseif (!$enclosed) {
1424 if (!preg_match($pattern, $ch)) {
1425 if (!isset($chars[$ch][$current_row])) {
1426 $chars[$ch][$current_row] = 1;
1427 } else {
1428 $chars[$ch][$current_row]++;
1429 }
1430 }
1431 }
1432 }
1433
1434 // filtering
1435 $depth = $to_end ? $current_row - 1 : $current_row;
1436 $filtered = [];
1437 foreach ($chars as $char => $value) {
1438 if ($match = $this->_check_count($char, $value, $depth, $preferred)) {
1439 $filtered[$match] = $char;
1440 }
1441 }
1442
1443 // capture most probable delimiter
1444 ksort($filtered);
1445 $this->delimiter = reset($filtered);
1446 }
1447
1448 /**
1449 * getCollection
1450 * Returns a Illuminate/Collection object
1451 * This may prove to be helpful to people who want to
1452 * create macros, and or use map functions
1453 *
1454 * @access public
1455 * @link https://laravel.com/docs/5.6/collections
1456 *
1457 * @throws \ErrorException - If the Illuminate\Support\Collection class is not found
1458 *
1459 * @return Collection
1460 */
1461 public function getCollection() {
1462 //does the Illuminate\Support\Collection class exists?
1463 //this uses the autoloader to try to determine
1464 //@see http://php.net/manual/en/function.class-exists.php
1465 if (class_exists('Illuminate\Support\Collection', true) == false) {
1466 throw new \ErrorException('It would appear you have not installed the illuminate/support package!');
1467 }
1468
1469 //return the collection
1470 return new Collection($this->data);
1471 }
1472 }
1473