From 5f8260b469f87075f8af5a2466ea2df19fbee6fc Mon Sep 17 00:00:00 2001 From: Alexander Rutz Date: Tue, 7 Nov 2017 11:29:30 +0100 Subject: [PATCH 1/6] PHP7 compatibility --- content/content.index.php | 2 +- extension.driver.php | 2 +- extension.meta.xml | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/content/content.index.php b/content/content.index.php index 4cde94a..cbd72f1 100644 --- a/content/content.index.php +++ b/content/content.index.php @@ -11,7 +11,7 @@ public function __construct(&$parent) } */ - public function build() + public function build(array $context = Array()) { parent::build(); parent::addStylesheetToHead(URL . '/extensions/importcsv/assets/importcsv.css'); diff --git a/extension.driver.php b/extension.driver.php index fea88f1..337e357 100644 --- a/extension.driver.php +++ b/extension.driver.php @@ -22,7 +22,7 @@ public function fetchNavigation() } } - public function update() + public function update($previousVersion = false) { if (file_exists(TMP.'/importcsv.csv')) { @unlink(TMP.'/importcsv.csv'); diff --git a/extension.meta.xml b/extension.meta.xml index 865104b..170ec2e 100644 --- a/extension.meta.xml +++ b/extension.meta.xml @@ -20,6 +20,9 @@ + + - PHP7 compatibility + - Fix importer's Member password value (#2) From b02e3e5e445accffc6776ea482612180ce196691 Mon Sep 17 00:00:00 2001 From: Alexander Rutz Date: Wed, 10 Jan 2018 10:31:11 +0100 Subject: [PATCH 2/6] =?UTF-8?q?Array()=20=E2=80=94>=20array()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/content.index.php | 4 ++-- extension.meta.xml | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/content/content.index.php b/content/content.index.php index cbd72f1..4fe470e 100644 --- a/content/content.index.php +++ b/content/content.index.php @@ -11,7 +11,7 @@ public function __construct(&$parent) } */ - public function build(array $context = Array()) + public function build(array $context = array()) { parent::build(); parent::addStylesheetToHead(URL . '/extensions/importcsv/assets/importcsv.css'); @@ -155,7 +155,7 @@ private function __importStep3Page() $this->__addVar('section-id', $sectionID); $this->__addVar('unique-action', $uniqueAction); $this->__addVar('unique-field', $uniqueField); - $this->__addVar('import-url', URL . '/symphony/extension/importcsv/'); + $this->__addVar('import-url', SYMPHONY_URL . '/extension/importcsv/'); // Output the CSV-data: $csvData = $csv->data; diff --git a/extension.meta.xml b/extension.meta.xml index 170ec2e..48dee07 100644 --- a/extension.meta.xml +++ b/extension.meta.xml @@ -20,6 +20,12 @@ + + - Array() —> array() + + + - Include @wdebusschere’s fix `Allow custom backendurl URL -> SYMPHONY_URL` + - PHP7 compatibility From 7e64025b87e7b63ec462f8bf91e250e30c583e0c Mon Sep 17 00:00:00 2001 From: Alexander Rutz Date: Tue, 31 Mar 2020 15:54:19 +0200 Subject: [PATCH 3/6] fix `count()` error in line 158 of the parseCSV-lib for PHP7.4 --- extension.meta.xml | 3 +++ lib/parsecsv-0.3.2/parsecsv.lib.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/extension.meta.xml b/extension.meta.xml index 48dee07..a731d96 100644 --- a/extension.meta.xml +++ b/extension.meta.xml @@ -20,6 +20,9 @@ + + - fix `count()` error in line 158 of the parseCSV-lib for PHP7.4 + - Array() —> array() diff --git a/lib/parsecsv-0.3.2/parsecsv.lib.php b/lib/parsecsv-0.3.2/parsecsv.lib.php index c5941b9..3e4dcdf 100644 --- a/lib/parsecsv-0.3.2/parsecsv.lib.php +++ b/lib/parsecsv-0.3.2/parsecsv.lib.php @@ -155,7 +155,7 @@ class parseCSV { function parseCSV ($input = null, $offset = null, $limit = null, $conditions = null) { if ( $offset !== null ) $this->offset = $offset; if ( $limit !== null ) $this->limit = $limit; - if ( count($conditions) > 0 ) $this->conditions = $conditions; + if ( is_array($conditions) && count($conditions) > 0 ) $this->conditions = $conditions; if ( !empty($input) ) $this->parse($input); } From 75abb9f76403e150d9d56eeec230fb0440b08629 Mon Sep 17 00:00:00 2001 From: animaux Date: Thu, 13 Jan 2022 14:25:05 +0100 Subject: [PATCH 4/6] remove curly brackets syntax in parseCSV-lib for PHP8 --- extension.meta.xml | 3 +++ lib/parsecsv-0.3.2/parsecsv.lib.php | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/extension.meta.xml b/extension.meta.xml index a731d96..7a1423e 100644 --- a/extension.meta.xml +++ b/extension.meta.xml @@ -20,6 +20,9 @@ + + - remove curly brackets syntax in parseCSV-lib for PHP8 + - fix `count()` error in line 158 of the parseCSV-lib for PHP7.4 diff --git a/lib/parsecsv-0.3.2/parsecsv.lib.php b/lib/parsecsv-0.3.2/parsecsv.lib.php index 3e4dcdf..4bdacd9 100644 --- a/lib/parsecsv-0.3.2/parsecsv.lib.php +++ b/lib/parsecsv-0.3.2/parsecsv.lib.php @@ -155,7 +155,7 @@ class parseCSV { function parseCSV ($input = null, $offset = null, $limit = null, $conditions = null) { if ( $offset !== null ) $this->offset = $offset; if ( $limit !== null ) $this->limit = $limit; - if ( is_array($conditions) && count($conditions) > 0 ) $this->conditions = $conditions; + if ( count($conditions) > 0 ) $this->conditions = $conditions; if ( !empty($input) ) $this->parse($input); } @@ -267,9 +267,9 @@ function auto ($file = null, $parse = true, $search_depth = null, $preferred = n // walk specific depth finding posssible delimiter characters for ( $i=0; $i < $strlen; $i++ ) { - $ch = $data{$i}; - $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; - $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; + $ch = $data[$i]; + $nch = ( isset($data[$i+1]) ) ? $data[$i+1] : false ; + $pch = ( isset($data[$i-1]) ) ? $data[$i-1] : false ; // open and closing quotes if ( $ch == $enclosure && (!$enclosed || $nch != $enclosure) ) { @@ -361,9 +361,9 @@ function parse_string ($data = null) { // walk through each character for ( $i=0; $i < $strlen; $i++ ) { - $ch = $data{$i}; - $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; - $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; + $ch = $data[$i]; + $nch = ( isset($data[$i+1]) ) ? $data[$i+1] : false ; + $pch = ( isset($data[$i-1]) ) ? $data[$i-1] : false ; // open and closing quotes if ( $ch == $this->enclosure && (!$enclosed || $nch != $this->enclosure) ) { @@ -604,7 +604,7 @@ function _enclose_value ($value = null) { if ( $value !== null && $value != '' ) { $delimiter = preg_quote($this->delimiter, '/'); $enclosure = preg_quote($this->enclosure, '/'); - if ( preg_match("/".$delimiter."|".$enclosure."|\n|\r/i", $value) || ($value{0} == ' ' || substr($value, -1) == ' ') ) { + if ( preg_match("/".$delimiter."|".$enclosure."|\n|\r/i", $value) || ($value[0] == ' ' || substr($value, -1) == ' ') ) { $value = str_replace($this->enclosure, $this->enclosure.$this->enclosure, $value); $value = $this->enclosure.$value.$this->enclosure; } From a97eef80bef0bb8214f9404d24b9d9f5bf18c288 Mon Sep 17 00:00:00 2001 From: animaux Date: Wed, 18 May 2022 09:33:27 +0200 Subject: [PATCH 5/6] Switch to parsecsv-1.3.2, PHP 8.1 compatibility (may require PHP 7) --- .DS_Store | Bin 0 -> 6148 bytes assets/.listing | 5 + content/.listing | 5 + content/content.index.php | 4 +- drivers/.listing | 13 + extension.meta.xml | 9 +- lib/.listing | 4 + lib/parsecsv-0.3.2/parsecsv.lib.php | 695 -------- lib/parsecsv-1.3.2/.github/workflows/ci.yml | 38 + lib/parsecsv-1.3.2/License.txt | 21 + lib/parsecsv-1.3.2/README.md | 246 +++ lib/parsecsv-1.3.2/composer.json | 57 + lib/parsecsv-1.3.2/parsecsv.lib.php | 24 + lib/parsecsv-1.3.2/src/Csv.php | 1472 +++++++++++++++++ lib/parsecsv-1.3.2/src/enums/AbstractEnum.php | 38 + lib/parsecsv-1.3.2/src/enums/DatatypeEnum.php | 120 ++ .../src/enums/FileProcessingModeEnum.php | 28 + lib/parsecsv-1.3.2/src/enums/SortEnum.php | 29 + .../src/extensions/DatatypeTrait.php | 102 ++ 19 files changed, 2207 insertions(+), 703 deletions(-) create mode 100644 .DS_Store create mode 100644 assets/.listing create mode 100644 content/.listing create mode 100644 drivers/.listing create mode 100644 lib/.listing delete mode 100644 lib/parsecsv-0.3.2/parsecsv.lib.php create mode 100644 lib/parsecsv-1.3.2/.github/workflows/ci.yml create mode 100644 lib/parsecsv-1.3.2/License.txt create mode 100644 lib/parsecsv-1.3.2/README.md create mode 100644 lib/parsecsv-1.3.2/composer.json create mode 100644 lib/parsecsv-1.3.2/parsecsv.lib.php create mode 100644 lib/parsecsv-1.3.2/src/Csv.php create mode 100644 lib/parsecsv-1.3.2/src/enums/AbstractEnum.php create mode 100644 lib/parsecsv-1.3.2/src/enums/DatatypeEnum.php create mode 100644 lib/parsecsv-1.3.2/src/enums/FileProcessingModeEnum.php create mode 100644 lib/parsecsv-1.3.2/src/enums/SortEnum.php create mode 100644 lib/parsecsv-1.3.2/src/extensions/DatatypeTrait.php diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..06a9cf5f4c6904d1369a27de64e7f68db61e519c GIT binary patch literal 6148 zcmeHKy-veG47S?}Dp5l*@B~?qAkBt{$;w3sD@mhSovw*ua zVne91EBi})_MP)pIp>Iohnv}es7FLys9@&^W{1eQXiZw`k!1FBJkq+n9gL>cVp)qe zhcRFb{5J;V-94ZiTH`mT+WXsoOUD;gQB2Be0zWnd_j>-ay{yN~evMh){oE%Url3Qb zQbiA_VlFn`NjCj@L_;mfJd0{)HAz%l&oR~7Kw5dcjlZY2Sx%eJQ*8eEa-sQj1yo85jG+M7UMw-cF{S7mn*t}A)Qzc zAFM00o>17Yj`=emPMilcYYZ3zF$3*z+mrr()qMVslkCnIFb4LD0qzu|Vu(l5-dcM& u>9rYj3>A?$7tp7`Xgl^7(uz-^UNDx)1u$210pWqzhk&QSj4|-1415B`(}FJm literal 0 HcmV?d00001 diff --git a/assets/.listing b/assets/.listing new file mode 100644 index 0000000..da14e61 --- /dev/null +++ b/assets/.listing @@ -0,0 +1,5 @@ +drwxr-xr-x 2 w014b45c w014b45c 4096 Jun 19 2018 . +drwxr-xr-x 6 w014b45c w014b45c 4096 Jun 20 2018 .. +-rw-r--r-- 1 w014b45c w014b45c 1641 Jun 19 2018 importcsv.css +-rw-r--r-- 1 w014b45c w014b45c 397 Jun 19 2018 importcsv.js +-rw-r--r-- 1 w014b45c w014b45c 3187 Jun 19 2018 import.js diff --git a/content/.listing b/content/.listing new file mode 100644 index 0000000..8026cc3 --- /dev/null +++ b/content/.listing @@ -0,0 +1,5 @@ +drwxr-xr-x 2 w014b45c w014b45c 4096 Jun 19 2018 . +drwxr-xr-x 6 w014b45c w014b45c 4096 Jun 20 2018 .. +-rw-r--r-- 1 w014b45c w014b45c 20368 Jun 20 2018 content.index.php +-rw-r--r-- 1 w014b45c w014b45c 5474 Jun 19 2018 index.xsl +-rw-r--r-- 1 w014b45c w014b45c 3877 Jun 19 2018 step2.xsl diff --git a/content/content.index.php b/content/content.index.php index 4fe470e..5e7292b 100644 --- a/content/content.index.php +++ b/content/content.index.php @@ -1,5 +1,5 @@ Import/Export CSV Import a CSV file to create new entries for a certain section, or export an existing section to a CSV file. - https://github.com/symphonists/importcsv + https://github.com/animaux/importcsv Workflow Other @@ -20,11 +20,8 @@ - - - remove curly brackets syntax in parseCSV-lib for PHP8 - - - - fix `count()` error in line 158 of the parseCSV-lib for PHP7.4 + + - Switch to parsecsv-1.3.2, PHP 8.1 compatibility (may require PHP 7) - Array() —> array() diff --git a/lib/.listing b/lib/.listing new file mode 100644 index 0000000..9ce9b19 --- /dev/null +++ b/lib/.listing @@ -0,0 +1,4 @@ +drwxr-xr-x 4 w014b45c w014b45c 4096 Jun 19 2018 . +drwxr-xr-x 6 w014b45c w014b45c 4096 Jun 20 2018 .. +drwxr-xr-x 2 w014b45c w014b45c 4096 Jun 19 2018 parsecsv-0.3.2 +drwxr-xr-x 5 w014b45c w014b45c 4096 Jun 19 2018 parsecsv-1.0.0 diff --git a/lib/parsecsv-0.3.2/parsecsv.lib.php b/lib/parsecsv-0.3.2/parsecsv.lib.php deleted file mode 100644 index 4bdacd9..0000000 --- a/lib/parsecsv-0.3.2/parsecsv.lib.php +++ /dev/null @@ -1,695 +0,0 @@ -data); - ---------------- - # tab delimited, and encoding conversion - $csv = new parseCSV(); - $csv->encoding('UTF-16', 'UTF-8'); - $csv->delimiter = "\t"; - $csv->parse('data.tsv'); - print_r($csv->data); - ---------------- - # auto-detect delimiter character - $csv = new parseCSV(); - $csv->auto('data.csv'); - print_r($csv->data); - ---------------- - # modify data in a csv file - $csv = new parseCSV(); - $csv->sort_by = 'id'; - $csv->parse('data.csv'); - # "4" is the value of the "id" column of the CSV row - $csv->data[4] = array('firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@doe.com'); - $csv->save(); - ---------------- - # add row/entry to end of CSV file - # - only recommended when you know the extact sctructure of the file - $csv = new parseCSV(); - $csv->save('data.csv', array('1986', 'Home', 'Nowhere', ''), true); - ---------------- - # convert 2D array to csv data and send headers - # to browser to treat output as a file and download it - $csv = new parseCSV(); - $csv->output (true, 'movies.csv', $array); - ---------------- - - -*/ - - - /** - * Configuration - * - set these options with $object->var_name = 'value'; - */ - - # use first line/entry as field names - var $heading = true; - - # override field names - var $fields = array(); - - # sort entries by this field - var $sort_by = null; - var $sort_reverse = false; - - # delimiter (comma) and enclosure (double quote) - var $delimiter = ','; - var $enclosure = '"'; - - # basic SQL-like conditions for row matching - var $conditions = null; - - # number of rows to ignore from beginning of data - var $offset = null; - - # limits the number of returned rows to specified amount - var $limit = null; - - # number of rows to analyze when attempting to auto-detect delimiter - var $auto_depth = 15; - - # characters to ignore when attempting to auto-detect delimiter - var $auto_non_chars = "a-zA-Z0-9\n\r"; - - # preferred delimiter characters, only used when all filtering method - # returns multiple possible delimiters (happens very rarely) - var $auto_preferred = ",;\t.:|"; - - # character encoding options - var $convert_encoding = false; - var $input_encoding = 'ISO-8859-1'; - var $output_encoding = 'ISO-8859-1'; - - # used by unparse(), save(), and output() functions - var $linefeed = "\r\n"; - - # only used by output() function - var $output_delimiter = ','; - var $output_filename = 'data.csv'; - - - /** - * Internal variables - */ - - # current file - var $file; - - # loaded file contents - var $file_data; - - # array of field values in data parsed - var $titles = array(); - - # two dimentional array of CSV data - var $data = array(); - - - /** - * Constructor - * @param input CSV file or string - * @return nothing - */ - function parseCSV ($input = null, $offset = null, $limit = null, $conditions = null) { - if ( $offset !== null ) $this->offset = $offset; - if ( $limit !== null ) $this->limit = $limit; - if ( count($conditions) > 0 ) $this->conditions = $conditions; - if ( !empty($input) ) $this->parse($input); - } - - - // ============================================== - // ----- [ Main Functions ] --------------------- - // ============================================== - - /** - * Parse CSV file or string - * @param input CSV file or string - * @return nothing - */ - function parse ($input = null, $offset = null, $limit = null, $conditions = null) { - if ( !empty($input) ) { - if ( $offset !== null ) $this->offset = $offset; - if ( $limit !== null ) $this->limit = $limit; - if ( count($conditions) > 0 ) $this->conditions = $conditions; - if ( is_readable($input) ) { - $this->data = $this->parse_file($input); - } else { - $this->file_data = &$input; - $this->data = $this->parse_string(); - } - if ( $this->data === false ) return false; - } - return true; - } - - /** - * Save changes, or new file and/or data - * @param file file to save to - * @param data 2D array with data - * @param append append current data to end of target CSV if exists - * @param fields field names - * @return true or false - */ - function save ($file = null, $data = array(), $append = false, $fields = array()) { - if ( empty($file) ) $file = &$this->file; - $mode = ( $append ) ? 'at' : 'wt' ; - $is_php = ( preg_match('/\.php$/i', $file) ) ? true : false ; - return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); - } - - /** - * Generate CSV based string for output - * @param output if true, prints headers and strings to browser - * @param filename filename sent to browser in headers if output is true - * @param data 2D array with data - * @param fields field names - * @param delimiter delimiter used to separate data - * @return CSV data using delimiter of choice, or default - */ - function output ($output = true, $filename = null, $data = array(), $fields = array(), $delimiter = null) { - if ( empty($filename) ) $filename = $this->output_filename; - if ( $delimiter === null ) $delimiter = $this->output_delimiter; - $data = $this->unparse($data, $fields, null, null, $delimiter); - if ( $output ) { - header('Content-type: application/csv'); - header('Content-Disposition: inline; filename="'.$filename.'"'); - echo $data; - } - return $data; - } - - /** - * Convert character encoding - * @param input input character encoding, uses default if left blank - * @param output output character encoding, uses default if left blank - * @return nothing - */ - function encoding ($input = null, $output = null) { - $this->convert_encoding = true; - if ( $input !== null ) $this->input_encoding = $input; - if ( $output !== null ) $this->output_encoding = $output; - } - - /** - * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of - * rows to determine most probable delimiter character - * @param file local CSV file - * @param parse true/false parse file directly - * @param search_depth number of rows to analyze - * @param preferred preferred delimiter characters - * @param enclosure enclosure character, default is double quote ("). - * @return delimiter character - */ - function auto ($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { - - if ( $file === null ) $file = $this->file; - if ( empty($search_depth) ) $search_depth = $this->auto_depth; - if ( $enclosure === null ) $enclosure = $this->enclosure; - - if ( $preferred === null ) $preferred = $this->auto_preferred; - - if ( empty($this->file_data) ) { - if ( $this->_check_data($file) ) { - $data = &$this->file_data; - } else return false; - } else { - $data = &$this->file_data; - } - - $chars = array(); - $strlen = strlen($data); - $enclosed = false; - $n = 1; - $to_end = true; - - // walk specific depth finding posssible delimiter characters - for ( $i=0; $i < $strlen; $i++ ) { - $ch = $data[$i]; - $nch = ( isset($data[$i+1]) ) ? $data[$i+1] : false ; - $pch = ( isset($data[$i-1]) ) ? $data[$i-1] : false ; - - // open and closing quotes - if ( $ch == $enclosure && (!$enclosed || $nch != $enclosure) ) { - $enclosed = ( $enclosed ) ? false : true ; - - // inline quotes - } elseif ( $ch == $enclosure && $enclosed ) { - $i++; - - // end of row - } elseif ( ($ch == "\n" && $pch != "\r" || $ch == "\r") && !$enclosed ) { - if ( $n >= $search_depth ) { - $strlen = 0; - $to_end = false; - } else { - $n++; - } - - // count character - } elseif (!$enclosed) { - if ( !preg_match('/['.preg_quote($this->auto_non_chars, '/').']/i', $ch) ) { - if ( !isset($chars[$ch][$n]) ) { - $chars[$ch][$n] = 1; - } else { - $chars[$ch][$n]++; - } - } - } - } - - // filtering - $depth = ( $to_end ) ? $n-1 : $n ; - $filtered = array(); - foreach( $chars as $char => $value ) { - if ( $match = $this->_check_count($char, $value, $depth, $preferred) ) { - $filtered[$match] = $char; - } - } - - // capture most probable delimiter - ksort($filtered); - $delimiter = reset($filtered); - $this->delimiter = $delimiter; - - // parse data - if ( $parse ) $this->data = $this->parse_string(); - - return $delimiter; - - } - - - // ============================================== - // ----- [ Core Functions ] --------------------- - // ============================================== - - /** - * Read file to string and call parse_string() - * @param file local CSV file - * @return 2D array with CSV data, or false on failure - */ - function parse_file ($file = null) { - if ( $file === null ) $file = $this->file; - if ( empty($this->file_data) ) $this->load_data($file); - return ( !empty($this->file_data) ) ? $this->parse_string() : false ; - } - - /** - * Parse CSV strings to arrays - * @param data CSV string - * @return 2D array with CSV data, or false on failure - */ - function parse_string ($data = null) { - if ( empty($data) ) { - if ( $this->_check_data() ) { - $data = &$this->file_data; - } else return false; - } - - $rows = array(); - $row = array(); - $row_count = 0; - $current = ''; - $head = ( !empty($this->fields) ) ? $this->fields : array() ; - $col = 0; - $enclosed = false; - $was_enclosed = false; - $strlen = strlen($data); - - // walk through each character - for ( $i=0; $i < $strlen; $i++ ) { - $ch = $data[$i]; - $nch = ( isset($data[$i+1]) ) ? $data[$i+1] : false ; - $pch = ( isset($data[$i-1]) ) ? $data[$i-1] : false ; - - // open and closing quotes - if ( $ch == $this->enclosure && (!$enclosed || $nch != $this->enclosure) ) { - $enclosed = ( $enclosed ) ? false : true ; - if ( $enclosed ) $was_enclosed = true; - - // inline quotes - } elseif ( $ch == $this->enclosure && $enclosed ) { - $current .= $ch; - $i++; - - // end of field/row - } elseif ( ($ch == $this->delimiter || ($ch == "\n" && $pch != "\r") || $ch == "\r") && !$enclosed ) { - if ( !$was_enclosed ) $current = trim($current); - $key = ( !empty($head[$col]) ) ? $head[$col] : $col ; - $row[$key] = $current; - $current = ''; - $col++; - - // end of row - if ( $ch == "\n" || $ch == "\r" ) { - if ( $this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions) ) { - if ( $this->heading && empty($head) ) { - $head = $row; - } elseif ( empty($this->fields) || (!empty($this->fields) && (($this->heading && $row_count > 0) || !$this->heading)) ) { - if ( !empty($this->sort_by) && !empty($row[$this->sort_by]) ) { - if ( isset($rows[$row[$this->sort_by]]) ) { - $rows[$row[$this->sort_by].'_0'] = &$rows[$row[$this->sort_by]]; - unset($rows[$row[$this->sort_by]]); - for ( $sn=1; isset($rows[$row[$this->sort_by].'_'.$sn]); $sn++ ) {} - $rows[$row[$this->sort_by].'_'.$sn] = $row; - } else $rows[$row[$this->sort_by]] = $row; - } else $rows[] = $row; - } - } - $row = array(); - $col = 0; - $row_count++; - if ( $this->sort_by === null && $this->limit !== null && count($rows) == $this->limit ) { - $i = $strlen; - } - } - - // append character to current field - } else { - $current .= $ch; - } - } - $this->titles = $head; - if ( !empty($this->sort_by) ) { - ( $this->sort_reverse ) ? krsort($rows) : ksort($rows) ; - if ( $this->offset !== null || $this->limit !== null ) { - $rows = array_slice($rows, ($this->offset === null ? 0 : $this->offset) , $this->limit, true); - } - } - return $rows; - } - - /** - * Create CSV data from array - * @param data 2D array with data - * @param fields field names - * @param append if true, field names will not be output - * @param is_php if a php die() call should be put on the first - * line of the file, this is later ignored when read. - * @param delimiter field delimiter to use - * @return CSV data (text string) - */ - function unparse ( $data = array(), $fields = array(), $append = false , $is_php = false, $delimiter = null) { - if ( !is_array($data) || empty($data) ) $data = &$this->data; - if ( !is_array($fields) || empty($fields) ) $fields = &$this->titles; - if ( $delimiter === null ) $delimiter = $this->delimiter; - - $string = ( $is_php ) ? "".$this->linefeed : '' ; - $entry = array(); - - // create heading - if ( $this->heading && !$append ) { - foreach( $fields as $key => $value ) { - $entry[] = $this->_enclose_value($value); - } - $string .= implode($delimiter, $entry).$this->linefeed; - $entry = array(); - } - - // create data - foreach( $data as $key => $row ) { - foreach( $row as $field => $value ) { - $entry[] = $this->_enclose_value($value); - } - $string .= implode($delimiter, $entry).$this->linefeed; - $entry = array(); - } - - return $string; - } - - /** - * Load local file or string - * @param input local CSV file - * @return true or false - */ - function load_data ($input = null) { - $data = null; - $file = null; - if ( $input === null ) { - $file = $this->file; - } elseif ( file_exists($input) ) { - $file = $input; - } else { - $data = $input; - } - if ( !empty($data) || $data = $this->_rfile($file) ) { - if ( $this->file != $file ) $this->file = $file; - if ( preg_match('/\.php$/i', $file) && preg_match('/<\?.*?\?>(.*)/ims', $data, $strip) ) { - $data = ltrim($strip[1]); - } - if ( $this->convert_encoding ) $data = iconv($this->input_encoding, $this->output_encoding, $data); - if ( substr($data, -1) != "\n" ) $data .= "\n"; - $this->file_data = &$data; - return true; - } - return false; - } - - - // ============================================== - // ----- [ Internal Functions ] ----------------- - // ============================================== - - /** - * Validate a row against specified conditions - * @param row array with values from a row - * @param conditions specified conditions that the row must match - * @return true of false - */ - function _validate_row_conditions ($row = array(), $conditions = null) { - if ( !empty($row) ) { - if ( !empty($conditions) ) { - $conditions = (strpos($conditions, ' OR ') !== false) ? explode(' OR ', $conditions) : array($conditions) ; - $or = ''; - foreach( $conditions as $key => $value ) { - if ( strpos($value, ' AND ') !== false ) { - $value = explode(' AND ', $value); - $and = ''; - foreach( $value as $k => $v ) { - $and .= $this->_validate_row_condition($row, $v); - } - $or .= (strpos($and, '0') !== false) ? '0' : '1' ; - } else { - $or .= $this->_validate_row_condition($row, $value); - } - } - return (strpos($or, '1') !== false) ? true : false ; - } - return true; - } - return false; - } - - /** - * Validate a row against a single condition - * @param row array with values from a row - * @param condition specified condition that the row must match - * @return true of false - */ - function _validate_row_condition ($row, $condition) { - $operators = array( - '=', 'equals', 'is', - '!=', 'is not', - '<', 'is less than', - '>', 'is greater than', - '<=', 'is less than or equals', - '>=', 'is greater than or equals', - 'contains', - 'does not contain', - ); - $operators_regex = array(); - foreach( $operators as $value ) { - $operators_regex[] = preg_quote($value, '/'); - } - $operators_regex = implode('|', $operators_regex); - if ( preg_match('/^(.+) ('.$operators_regex.') (.+)$/i', trim($condition), $capture) ) { - $field = $capture[1]; - $op = $capture[2]; - $value = $capture[3]; - if ( preg_match('/^([\'\"]{1})(.*)([\'\"]{1})$/i', $value, $capture) ) { - if ( $capture[1] == $capture[3] ) { - $value = $capture[2]; - $value = str_replace("\\n", "\n", $value); - $value = str_replace("\\r", "\r", $value); - $value = str_replace("\\t", "\t", $value); - $value = stripslashes($value); - } - } - if ( array_key_exists($field, $row) ) { - if ( ($op == '=' || $op == 'equals' || $op == 'is') && $row[$field] == $value ) { - return '1'; - } elseif ( ($op == '!=' || $op == 'is not') && $row[$field] != $value ) { - return '1'; - } elseif ( ($op == '<' || $op == 'is less than' ) && $row[$field] < $value ) { - return '1'; - } elseif ( ($op == '>' || $op == 'is greater than') && $row[$field] > $value ) { - return '1'; - } elseif ( ($op == '<=' || $op == 'is less than or equals' ) && $row[$field] <= $value ) { - return '1'; - } elseif ( ($op == '>=' || $op == 'is greater than or equals') && $row[$field] >= $value ) { - return '1'; - } elseif ( $op == 'contains' && preg_match('/'.preg_quote($value, '/').'/i', $row[$field]) ) { - return '1'; - } elseif ( $op == 'does not contain' && !preg_match('/'.preg_quote($value, '/').'/i', $row[$field]) ) { - return '1'; - } else { - return '0'; - } - } - } - return '1'; - } - - /** - * Validates if the row is within the offset or not if sorting is disabled - * @param current_row the current row number being processed - * @return true of false - */ - function _validate_offset ($current_row) { - if ( $this->sort_by === null && $this->offset !== null && $current_row < $this->offset ) return false; - return true; - } - - /** - * Enclose values if needed - * - only used by unparse() - * @param value string to process - * @return Processed value - */ - function _enclose_value ($value = null) { - if ( $value !== null && $value != '' ) { - $delimiter = preg_quote($this->delimiter, '/'); - $enclosure = preg_quote($this->enclosure, '/'); - if ( preg_match("/".$delimiter."|".$enclosure."|\n|\r/i", $value) || ($value[0] == ' ' || substr($value, -1) == ' ') ) { - $value = str_replace($this->enclosure, $this->enclosure.$this->enclosure, $value); - $value = $this->enclosure.$value.$this->enclosure; - } - } - return $value; - } - - /** - * Check file data - * @param file local filename - * @return true or false - */ - function _check_data ($file = null) { - if ( empty($this->file_data) ) { - if ( $file === null ) $file = $this->file; - return $this->load_data($file); - } - return true; - } - - - /** - * Check if passed info might be delimiter - * - only used by find_delimiter() - * @return special string used for delimiter selection, or false - */ - function _check_count ($char, $array, $depth, $preferred) { - if ( $depth == count($array) ) { - $first = null; - $equal = null; - $almost = false; - foreach( $array as $key => $value ) { - if ( $first == null ) { - $first = $value; - } elseif ( $value == $first && $equal !== false) { - $equal = true; - } elseif ( $value == $first+1 && $equal !== false ) { - $equal = true; - $almost = true; - } else { - $equal = false; - } - } - if ( $equal ) { - $match = ( $almost ) ? 2 : 1 ; - $pref = strpos($preferred, $char); - $pref = ( $pref !== false ) ? str_pad($pref, 3, '0', STR_PAD_LEFT) : '999' ; - return $pref.$match.'.'.(99999 - str_pad($first, 5, '0', STR_PAD_LEFT)); - } else return false; - } - } - - /** - * Read local file - * @param file local filename - * @return Data from file, or false on failure - */ - function _rfile ($file = null) { - if ( is_readable($file) ) { - if ( !($fh = fopen($file, 'r')) ) return false; - $data = fread($fh, filesize($file)); - fclose($fh); - return $data; - } - return false; - } - - /** - * Write to local file - * @param file local filename - * @param string data to write to file - * @param mode fopen() mode - * @param lock flock() mode - * @return true or false - */ - function _wfile ($file, $string = '', $mode = 'wb', $lock = 2) { - if ( $fp = fopen($file, $mode) ) { - flock($fp, $lock); - $re = fwrite($fp, $string); - $re2 = fclose($fp); - if ( $re != false && $re2 != false ) return true; - } - return false; - } - -} - -?> \ No newline at end of file diff --git a/lib/parsecsv-1.3.2/.github/workflows/ci.yml b/lib/parsecsv-1.3.2/.github/workflows/ci.yml new file mode 100644 index 0000000..dfc6dbb --- /dev/null +++ b/lib/parsecsv-1.3.2/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +--- +name: CI +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php_version: + - "7.4" + - "7.3" + - "7.2" + - "7.1" + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php_version }} + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer update + - name: Validate dependencies + run: composer validate + - name: Run tests + run: vendor/bin/phpunit --configuration tests/phpunit.xml diff --git a/lib/parsecsv-1.3.2/License.txt b/lib/parsecsv-1.3.2/License.txt new file mode 100644 index 0000000..84efc1c --- /dev/null +++ b/lib/parsecsv-1.3.2/License.txt @@ -0,0 +1,21 @@ +(The MIT license) + +Copyright (c) 2014 Jim Myhrberg. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/parsecsv-1.3.2/README.md b/lib/parsecsv-1.3.2/README.md new file mode 100644 index 0000000..bc47d9f --- /dev/null +++ b/lib/parsecsv-1.3.2/README.md @@ -0,0 +1,246 @@ +# ParseCsv +[![Financial Contributors on Open Collective](https://opencollective.com/parsecsv/all/badge.svg?label=financial+contributors)](https://opencollective.com/parsecsv) + +ParseCsv is an easy-to-use PHP class that reads and writes CSV data properly. It +fully conforms to the specifications outlined on the on the +[Wikipedia article][CSV] (and thus RFC 4180). It has many advanced features which help make your +life easier when dealing with CSV data. + +You may not need a library at all: before using ParseCsv, please make sure if PHP's own `str_getcsv()`, ``fgetcsv()`` or `fputcsv()` meets your needs. + +This library was originally created in early 2007 by [jimeh](https://github.com/jimeh) due to the lack of built-in +and third-party support for handling CSV data in PHP. + +[csv]: http://en.wikipedia.org/wiki/Comma-separated_values + +## Features + +* ParseCsv is a complete and fully featured CSV solution for PHP +* Supports enclosed values, enclosed commas, double quotes and new lines. +* Automatic delimiter character detection. +* Sort data by specific fields/columns. +* Easy data manipulation. +* Basic SQL-like _conditions_, _offset_ and _limit_ options for filtering + data. +* Error detection for incorrectly formatted input. It attempts to be + intelligent, but can not be trusted 100% due to the structure of CSV, and + how different programs like Excel for example outputs CSV data. +* Support for character encoding conversion using PHP's + `iconv()` and `mb_convert_encoding()` functions. +* Supports PHP 5.5 and higher. + It certainly works with PHP 7.2 and all versions in between. + +## Installation + +Installation is easy using Composer. Just run the following on the +command line: +``` +composer require parsecsv/php-parsecsv +``` + +If you don't use a framework such as Drupal, Laravel, Symfony, Yii etc., +you may have to manually include Composer's autoloader file in your PHP +script: +```php +require_once __DIR__ . '/vendor/autoload.php'; +``` + +#### Without composer +Not recommended, but technically possible: you can also clone the +repository or extract the +[ZIP](https://github.com/parsecsv/parsecsv-for-php/archive/master.zip). +To use ParseCSV, you then have to add a `require 'parsecsv.lib.php';` line. + +## Example Usage + +**Parse a tab-delimited CSV file with encoding conversion** + +```php +$csv = new \ParseCsv\Csv(); +$csv->encoding('UTF-16', 'UTF-8'); +$csv->delimiter = "\t"; +$csv->parseFile('data.tsv'); +print_r($csv->data); +``` + +**Auto-detect field delimiter character** + +```php +$csv = new \ParseCsv\Csv(); +$csv->auto('data.csv'); +print_r($csv->data); +``` + +**Parse data with offset** +* ignoring the first X (e.g. two) rows +```php +$csv = new \ParseCsv\Csv(); +$csv->offset = 2; +$csv->parseFile('data.csv'); +print_r($csv->data); +``` + +**Limit the number of returned data rows** +```php +$csv = new \ParseCsv\Csv(); +$csv->limit = 5; +$csv->parseFile('data.csv'); +print_r($csv->data); +``` + +**Get total number of data rows without parsing whole data** +* Excluding heading line if present (see $csv->header property) +```php +$csv = new \ParseCsv\Csv(); +$csv->loadFile('data.csv'); +$count = $csv->getTotalDataRowCount(); +print_r($count); +``` + +**Get most common data type for each column** + +```php +$csv = new \ParseCsv\Csv('data.csv'); +$csv->getDatatypes(); +print_r($csv->data_types); +``` + +**Modify data in a CSV file** + +Change data values: +```php +$csv = new \ParseCsv\Csv(); +$csv->sort_by = 'id'; +$csv->parseFile('data.csv'); +# "4" is the value of the "id" column of the CSV row +$csv->data[4] = array('firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@doe.com'); +$csv->save(); +``` + +Enclose each data value by quotes: +```php +$csv = new \ParseCsv\Csv(); +$csv->parseFile('data.csv'); +$csv->enclose_all = true; +$csv->save(); +``` + +**Replace field names or set ones if missing** + +```php +$csv = new \ParseCsv\Csv(); +$csv->fields = ['id', 'name', 'category']; +$csv->parseFile('data.csv'); +``` + +**Add row/entry to end of CSV file** + +_Only recommended when you know the exact structure of the file._ + +```php +$csv = new \ParseCsv\Csv(); +$csv->save('data.csv', array(array('1986', 'Home', 'Nowhere', '')), /* append */ true); +``` + +**Convert 2D array to CSV data and send headers to browser to treat output as +a file and download it** + +Your web app users would call this an export. + +```php +$csv = new \ParseCsv\Csv(); +$csv->linefeed = "\n"; +$header = array('field 1', 'field 2'); +$csv->output('movies.csv', $data_array, $header, ','); +``` + +For more complex examples, see the ``tests`` and `examples` directories. + +## Test coverage + +All tests are located in the `tests` directory. To execute tests, run the following commands: + +````bash +composer install +composer run test +```` + +When pushing code to GitHub, tests will be executed using GitHub Actions. The relevant configuration is in the +file `.github/workflows/ci.yml`. To run the `test` action locally, you can execute the following command: + +````bash +make local-ci +```` + +## Security + +If you discover any security related issues, please email ParseCsv@blaeul.de instead of using GitHub issues. + +## Credits + +* ParseCsv is based on the concept of [Ming Hong Ng][ming]'s [CsvFileParser][] + class. + +[ming]: http://minghong.blogspot.com/ +[CsvFileParser]: http://minghong.blogspot.com/2006/07/csv-parser-for-php.html + + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. + +Please find a complete list on the project's [contributors][] page. + +[contributors]: https://github.com/parsecsv/parsecsv-for-php/graphs/contributors + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/parsecsv/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/parsecsv/contribute)] + + + + + + + + + + + + +## License + +(The MIT license) + +Copyright (c) 2014 Jim Myhrberg. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +[![Build Status](https://travis-ci.org/parsecsv/parsecsv-for-php.svg?branch=master)](https://travis-ci.org/parsecsv/parsecsv-for-php) diff --git a/lib/parsecsv-1.3.2/composer.json b/lib/parsecsv-1.3.2/composer.json new file mode 100644 index 0000000..cbbaca0 --- /dev/null +++ b/lib/parsecsv-1.3.2/composer.json @@ -0,0 +1,57 @@ +{ + "name": "parsecsv/php-parsecsv", + "description": "CSV data parser for PHP", + "license": "MIT", + "authors": [ + { + "name": "Jim Myhrberg", + "email": "contact@jimeh.me" + }, + { + "name": "William Knauss", + "email": "will.knauss@gmail.com" + }, + { + "name": "Susann Sgorzaly", + "homepage": "https://github.com/susgo" + }, + { + "name": "Christian Bläul", + "homepage": "https://github.com/Fonata" + } + ], + "autoload": { + "psr-4": { + "ParseCsv\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "ParseCsv\\tests\\": "tests" + } + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "illuminate/support": "Fluent array interface for map functions" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpunit -c tests tests --disallow-test-output --coverage-clover coverage_clover.xml --whitelist src" + ] + }, + "support": { + "issues": "https://github.com/parsecsv/parsecsv-for-php/issues", + "source": "https://github.com/parsecsv/parsecsv-for-php" + } +} diff --git a/lib/parsecsv-1.3.2/parsecsv.lib.php b/lib/parsecsv-1.3.2/parsecsv.lib.php new file mode 100644 index 0000000..265ff22 --- /dev/null +++ b/lib/parsecsv-1.3.2/parsecsv.lib.php @@ -0,0 +1,24 @@ +var_name = 'value'; + */ + + /** + * Header row: + * Use first line/entry as field names + * + * @var bool + */ + public $heading = true; + + /** + * Override field names + * + * @var array + */ + public $fields = array(); + + /** + * Sort CSV by this field + * + * @var string|null + */ + public $sort_by = null; + + /** + * Reverse the sort direction + * + * @var bool + */ + public $sort_reverse = false; + + /** + * Sort behavior passed to sort methods + * + * regular = SORT_REGULAR + * numeric = SORT_NUMERIC + * string = SORT_STRING + * + * @var string|null + */ + public $sort_type = SortEnum::SORT_TYPE_REGULAR; + + /** + * Field delimiter character + * + * @var string + */ + public $delimiter = ','; + + /** + * Enclosure character + * + * This is useful for cell values that are either multi-line + * or contain the field delimiter character. + * + * @var string + */ + public $enclosure = '"'; + + /** + * Force enclosing all columns. + * + * If false, only cells that are either multi-line or + * contain the field delimiter character are enclosed + * in the $enclosure char. + * + * @var bool + */ + public $enclose_all = false; + + /** + * Basic SQL-Like conditions for row matching + * + * @var string|null + */ + public $conditions = null; + + /** + * Number of rows to ignore from beginning of data. If present, the heading + * row is also counted (if $this->heading == true). In other words, + * $offset == 1 and $offset == 0 have the same meaning in that situation. + * + * @var int|null + */ + public $offset = null; + + /** + * Limits the number of returned rows to the specified amount + * + * @var int|null + */ + public $limit = null; + + /** + * Number of rows to analyze when attempting to auto-detect delimiter + * + * @var int + */ + public $auto_depth = 15; + + /** + * Characters that should be ignored when attempting to auto-detect delimiter + * + * @var string + */ + public $auto_non_chars = "a-zA-Z0-9\n\r"; + + /** + * preferred delimiter characters, only used when all filtering method + * returns multiple possible delimiters (happens very rarely) + * + * @var string + */ + public $auto_preferred = ",;\t.:|"; + + /** + * Should we convert the CSV character encoding? + * Used for both parse and unparse operations. + * + * @var bool + */ + public $convert_encoding = false; + + /** + * Set the input encoding + * + * @var string + */ + public $input_encoding = 'ISO-8859-1'; + + /** + * Set the output encoding + * + * @var string + */ + public $output_encoding = 'ISO-8859-1'; + + /** + * Whether to use mb_convert_encoding() instead of iconv(). + * + * The former is platform-independent whereas the latter is the traditional + * default go-to solution. + * + * @var bool (if false, iconv() is used) + */ + public $use_mb_convert_encoding = false; + + /** + * Line feed characters used by unparse, save, and output methods + * Popular choices are "\r\n" and "\n". + * + * @var string + */ + public $linefeed = "\r"; + + /** + * Sets the output delimiter used by the output method + * + * @var string + */ + public $output_delimiter = ','; + + /** + * Sets the output filename + * + * @var string + */ + public $output_filename = 'data.csv'; + + /** + * keep raw file data in memory after successful parsing (useful for debugging) + * + * @var bool + */ + public $keep_file_data = false; + + /** + * Internal variables + */ + + /** + * File + * Current Filename + * + * @var string + */ + public $file; + + /** + * File Data + * Current file data + * + * @var string + */ + public $file_data; + + /** + * Error + * Contains the error code if one occurred + * + * 0 = No errors found. Everything should be fine :) + * 1 = Hopefully correctable syntax error was found. + * 2 = Enclosure character (double quote by default) + * was found in non-enclosed field. This means + * the file is either corrupt, or does not + * standard CSV formatting. Please validate + * the parsed data yourself. + * + * @var int + */ + public $error = 0; + + /** + * Detailed error information + * + * @var array + */ + public $error_info = array(); + + /** + * $titles has 4 distinct tasks: + * 1. After reading in CSV data, $titles will contain the column headers + * present in the data. + * + * 2. It defines which fields from the $data array to write e.g. when + * calling unparse(), and in which order. This lets you skip columns you + * don't want in your output, but are present in $data. + * See examples/save_to_file_without_header_row.php. + * + * 3. It lets you rename columns. See StreamTest::testWriteStream for an + * example. + * + * 4. When writing data and $header is true, then $titles is also used for + * the first row. + * + * @var array + */ + public $titles = array(); + + /** + * Two-dimensional array of CSV data. + * The first dimension are the line numbers. Each line is represented as an array with field names as keys. + * + * @var array + */ + public $data = array(); + + use DatatypeTrait; + + /** + * Class constructor + * + * @param string|null $data The CSV string or a direct file path. + * + * WARNING: Supplying file paths here is + * deprecated. Use parseFile() instead. + * + * @param int|null $offset Number of rows to ignore from the + * beginning of the data + * @param int|null $limit Limits the number of returned rows + * to specified amount + * @param string|null $conditions Basic SQL-like conditions for row + * matching + * @param null|true $keep_file_data Keep raw file data in memory after + * successful parsing + * (useful for debugging) + */ + public function __construct($data = null, $offset = null, $limit = null, $conditions = null, $keep_file_data = null) { + $this->init($offset, $limit, $conditions, $keep_file_data); + + if (!empty($data)) { + $this->parse($data); + } + } + + /** + * @param int|null $offset Number of rows to ignore from the + * beginning of the data + * @param int|null $limit Limits the number of returned rows + * to specified amount + * @param string|null $conditions Basic SQL-like conditions for row + * matching + * @param null|true $keep_file_data Keep raw file data in memory after + * successful parsing + * (useful for debugging) + */ + public function init($offset = null, $limit = null, $conditions = null, $keep_file_data = null) { + if (!is_null($offset)) { + $this->offset = $offset; + } + + if (!is_null($limit)) { + $this->limit = $limit; + } + + if (!is_null($conditions)) { + $this->conditions = $conditions; + } + + if (!is_null($keep_file_data)) { + $this->keep_file_data = $keep_file_data; + } + } + + // ============================================== + // ----- [ Main Functions ] --------------------- + // ============================================== + + /** + * Parse a CSV file or string + * + * @param string|null $dataString The CSV string or a direct file path + * WARNING: Supplying file paths here is + * deprecated and will trigger an + * E_USER_DEPRECATED error. + * @param int|null $offset Number of rows to ignore from the + * beginning of the data + * @param int|null $limit Limits the number of returned rows to + * specified amount + * @param string|null $conditions Basic SQL-like conditions for row + * matching + * + * @return bool True on success + */ + public function parse($dataString = null, $offset = null, $limit = null, $conditions = null) { + if (is_null($dataString)) { + $this->data = $this->parseFile(); + return $this->data !== false; + } + + if (empty($dataString)) { + return false; + } + + $this->init($offset, $limit, $conditions); + + if (strlen($dataString) <= PHP_MAXPATHLEN && is_readable($dataString)) { + $this->file = $dataString; + $this->data = $this->parseFile(); + trigger_error( + 'Supplying file paths to parse() will no longer ' . + 'be supported in a future version of ParseCsv. ' . + 'Use ->parseFile() instead.', + E_USER_DEPRECATED + ); + } else { + $this->file = null; + $this->file_data = &$dataString; + $this->data = $this->_parse_string(); + } + + return $this->data !== false; + } + + /** + * Save changes, or write a new file and/or data. + * + * @param string $file File location to save to + * @param array $data 2D array of data + * @param bool $append Append current data to end of target CSV, if file + * exists + * @param array $fields Field names. Sets the header. If it is not set + * $this->titles would be used instead. + * + * @return bool + * True on success + */ + public function save($file = '', $data = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $fields = array()) { + if (empty($file)) { + $file = &$this->file; + } + + $mode = FileProcessingModeEnum::getAppendMode($append); + $is_php = preg_match('/\.php$/i', $file) ? true : false; + + return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); + } + + /** + * Generate a CSV-based string for output. + * + * Useful for exports in web applications. + * + * @param string|null $filename If a filename is specified here or in the + * object, headers and data will be output + * directly to browser as a downloadable + * file. This file doesn't have to exist on + * the server; the parameter only affects + * how the download is called to the + * browser. + * @param array[] $data 2D array with data + * @param array $fields Field names + * @param string|null $delimiter character used to separate data + * + * @return string The resulting CSV string + */ + public function output($filename = null, $data = array(), $fields = array(), $delimiter = null) { + if (empty($filename)) { + $filename = $this->output_filename; + } + + if ($delimiter === null) { + $delimiter = $this->output_delimiter; + } + + $flat_string = $this->unparse($data, $fields, null, null, $delimiter); + + if (!is_null($filename)) { + $mime = $delimiter === "\t" ? + 'text/tab-separated-values' : + 'application/csv'; + header('Content-type: ' . $mime); + header('Content-Length: ' . strlen($flat_string)); + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); + header('Expires: 0'); + header('Content-Disposition: attachment; filename="' . $filename . '"; modification-date="' . date('r') . '";'); + + echo $flat_string; + } + + return $flat_string; + } + + /** + * Convert character encoding + * + * Specify the encoding to use for the next parsing or unparsing. + * Calling this function will not change the data held in the object immediately. + * + * @param string|null $input Input character encoding + * If the value null is passed, the existing input encoding remains set (default: ISO-8859-1). + * @param string|null $output Output character encoding, uses default if left blank + * If the value null is passed, the existing input encoding remains set (default: ISO-8859-1). + * + * @return void + */ + public function encoding($input = null, $output = null) { + $this->convert_encoding = true; + if (!is_null($input)) { + $this->input_encoding = $input; + } + + if (!is_null($output)) { + $this->output_encoding = $output; + } + } + + /** + * Auto-detect delimiter: Find delimiter by analyzing a specific number of + * rows to determine most probable delimiter character + * + * @param string|null $file Local CSV file + * Supplying CSV data (file content) here is deprecated. + * For CSV data, please use autoDetectionForDataString(). + * Support for CSV data will be removed in v2.0.0. + * @param bool $parse True/false parse file directly + * @param int|null $search_depth Number of rows to analyze + * @param string|null $preferred Preferred delimiter characters + * @param string|null $enclosure Enclosure character, default is double quote ("). + * + * @return string|false The detected field delimiter + */ + public function auto($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { + if (is_null($file)) { + $file = $this->file; + } + + if (empty($search_depth)) { + $search_depth = $this->auto_depth; + } + + if (is_null($enclosure)) { + $enclosure = $this->enclosure; + } else { + $this->enclosure = $enclosure; + } + + if (is_null($preferred)) { + $preferred = $this->auto_preferred; + } + + if (empty($this->file_data)) { + if ($this->_check_data($file)) { + $data = &$this->file_data; + } else { + return false; + } + } else { + $data = &$this->file_data; + } + + $this->autoDetectionForDataString($data, $parse, $search_depth, $preferred, $enclosure); + + return $this->delimiter; + } + + public function autoDetectionForDataString($data, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { + $this->file_data = &$data; + if (!$this->_detect_and_remove_sep_row_from_data($data)) { + $this->_guess_delimiter($search_depth, $preferred, $enclosure, $data); + } + + // parse data + if ($parse) { + $this->data = $this->_parse_string(); + } + + return $this->delimiter; + } + + /** + * Get total number of data rows (exclusive heading line if present) in CSV + * without parsing the whole data string. + * + * @return bool|int + */ + public function getTotalDataRowCount() { + if (empty($this->file_data)) { + return false; + } + + $data = $this->file_data; + + $this->_detect_and_remove_sep_row_from_data($data); + + $pattern = sprintf('/%1$s[^%1$s]*%1$s/i', $this->enclosure); + preg_match_all($pattern, $data, $matches); + + /** @var array[] $matches */ + foreach ($matches[0] as $match) { + if (empty($match) || (strpos($match, $this->enclosure) === false)) { + continue; + } + + $replace = str_replace(["\r", "\n"], '', $match); + $data = str_replace($match, $replace, $data); + } + + $headingRow = $this->heading ? 1 : 0; + + return substr_count($data, "\r") + + substr_count($data, "\n") + - substr_count($data, "\r\n") + - $headingRow; + } + + // ============================================== + // ----- [ Core Functions ] --------------------- + // ============================================== + + /** + * Read file to string and call _parse_string() + * + * @param string|null $file Path to a CSV file. + * If configured in files such as php.ini, + * the path may also contain a protocol: + * https://example.org/some/file.csv + * + * @return array|false + */ + public function parseFile($file = null) { + if (is_null($file)) { + $file = $this->file; + } + + /** + * @see self::keep_file_data + * Usually, _parse_string will clean this + * Instead of leaving stale data for the next parseFile call behind. + */ + if (empty($this->file_data) && !$this->loadFile($file)) { + return false; + } + + if (empty($this->file_data)) { + return false; + } + return $this->data = $this->_parse_string(); + } + + /** + * Internal function to parse CSV strings to arrays. + * + * If you need BOM detection or character encoding conversion, please call + * $csv->load_data($your_data_string) first, followed by a call to + * $csv->parse($csv->file_data). + * + * To detect field separators, please use auto() instead. + * + * @param string|null $data CSV data + * + * @return array|false + * 2D array with CSV data, or false on failure + */ + protected function _parse_string($data = null) { + if (empty($data)) { + if ($this->_check_data()) { + $data = &$this->file_data; + } else { + return false; + } + } + + $white_spaces = str_replace($this->delimiter, '', " \t\x0B\0"); + + $rows = array(); + $row = array(); + $row_count = 0; + $current = ''; + $head = !empty($this->fields) ? $this->fields : array(); + $col = 0; + $enclosed = false; + $was_enclosed = false; + $strlen = strlen($data); + + // force the parser to process end of data as a character (false) when + // data does not end with a line feed or carriage return character. + $lch = $data[$strlen - 1]; + if ($lch != "\n" && $lch != "\r") { + $data .= "\n"; + $strlen++; + } + + // walk through each character + for ($i = 0; $i < $strlen; $i++) { + $ch = isset($data[$i]) ? $data[$i] : false; + $nch = isset($data[$i + 1]) ? $data[$i + 1] : false; + + // open/close quotes, and inline quotes + if ($ch == $this->enclosure) { + if (!$enclosed) { + if (ltrim($current, $white_spaces) == '') { + $enclosed = true; + $was_enclosed = true; + } else { + $this->error = 2; + $error_row = count($rows) + 1; + $error_col = $col + 1; + $index = $error_row . '-' . $error_col; + if (!isset($this->error_info[$index])) { + $this->error_info[$index] = array( + 'type' => 2, + 'info' => 'Syntax error found on row ' . $error_row . '. Non-enclosed fields can not contain double-quotes.', + 'row' => $error_row, + 'field' => $error_col, + 'field_name' => !empty($head[$col]) ? $head[$col] : null, + ); + } + + $current .= $ch; + } + } elseif ($nch == $this->enclosure) { + $current .= $ch; + $i++; + } elseif ($nch != $this->delimiter && $nch != "\r" && $nch != "\n") { + $x = $i + 1; + while (isset($data[$x]) && ltrim($data[$x], $white_spaces) == '') { + $x++; + } + if ($data[$x] == $this->delimiter) { + $enclosed = false; + $i = $x; + } else { + if ($this->error < 1) { + $this->error = 1; + } + + $error_row = count($rows) + 1; + $error_col = $col + 1; + $index = $error_row . '-' . $error_col; + if (!isset($this->error_info[$index])) { + $this->error_info[$index] = array( + 'type' => 1, + 'info' => + 'Syntax error found on row ' . (count($rows) + 1) . '. ' . + 'A single double-quote was found within an enclosed string. ' . + 'Enclosed double-quotes must be escaped with a second double-quote.', + 'row' => count($rows) + 1, + 'field' => $col + 1, + 'field_name' => !empty($head[$col]) ? $head[$col] : null, + ); + } + + $current .= $ch; + $enclosed = false; + } + } else { + $enclosed = false; + } + // end of field/row/csv + } elseif ((in_array($ch, [$this->delimiter, "\n", "\r", false], true)) && !$enclosed) { + $key = !empty($head[$col]) ? $head[$col] : $col; + $row[$key] = $was_enclosed ? $current : trim($current); + $current = ''; + $was_enclosed = false; + $col++; + + // end of row + if (in_array($ch, ["\n", "\r", false], true)) { + if ($this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions)) { + if ($this->heading && empty($head)) { + $head = $row; + } elseif (empty($this->fields) || (!empty($this->fields) && (($this->heading && $row_count > 0) || !$this->heading))) { + if (!empty($this->sort_by) && !empty($row[$this->sort_by])) { + $sort_field = $row[$this->sort_by]; + if (isset($rows[$sort_field])) { + $rows[$sort_field . '_0'] = &$rows[$sort_field]; + unset($rows[$sort_field]); + $sn = 1; + while (isset($rows[$sort_field . '_' . $sn])) { + $sn++; + } + $rows[$sort_field . '_' . $sn] = $row; + } else { + $rows[$sort_field] = $row; + } + + } else { + $rows[] = $row; + } + } + } + + $row = array(); + $col = 0; + $row_count++; + + if ($this->sort_by === null && $this->limit !== null && count($rows) == $this->limit) { + $i = $strlen; + } + + if ($ch == "\r" && $nch == "\n") { + $i++; + } + } + + // append character to current field + } else { + $current .= $ch; + } + } + + $this->titles = $head; + if (!empty($this->sort_by)) { + $sort_type = SortEnum::getSorting($this->sort_type); + $this->sort_reverse ? krsort($rows, $sort_type) : ksort($rows, $sort_type); + + if ($this->offset !== null || $this->limit !== null) { + $rows = array_slice($rows, ($this->offset === null ? 0 : $this->offset), $this->limit, true); + } + } + + if (!$this->keep_file_data) { + $this->file_data = null; + } + + return $rows; + } + + /** + * Create CSV data string from array + * + * @param array[] $data 2D array with data + * @param array $fields field names + * @param bool $append if true, field names will not be output + * @param bool $is_php if a php die() call should be put on the + * first line of the file, this is later + * ignored when read. + * @param string|null $delimiter field delimiter to use + * + * @return string CSV data + */ + public function unparse($data = array(), $fields = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $is_php = false, $delimiter = null) { + if (!is_array($data) || empty($data)) { + $data = &$this->data; + } else { + /** @noinspection ReferenceMismatchInspection */ + $this->data = $data; + } + + if (!is_array($fields) || empty($fields)) { + $fields = &$this->titles; + } + + if ($delimiter === null) { + $delimiter = $this->delimiter; + } + + $string = $is_php ? "" . $this->linefeed : ''; + $entry = array(); + + // create heading + /** @noinspection ReferenceMismatchInspection */ + $fieldOrder = $this->_validate_fields_for_unparse($fields); + if (!$fieldOrder && !empty($data)) { + $column_count = count($data[0]); + $columns = range(0, $column_count - 1, 1); + $fieldOrder = array_combine($columns, $columns); + } + + if ($this->heading && !$append && !empty($fields)) { + foreach ($fieldOrder as $column_name) { + $entry[] = $this->_enclose_value($column_name, $delimiter); + } + + $string .= implode($delimiter, $entry) . $this->linefeed; + $entry = array(); + } + + // create data + foreach ($data as $key => $row) { + foreach (array_keys($fieldOrder) as $index) { + $cell_value = $row[$index]; + $entry[] = $this->_enclose_value($cell_value, $delimiter); + } + + $string .= implode($delimiter, $entry) . $this->linefeed; + $entry = array(); + } + + if ($this->convert_encoding) { + /** @noinspection PhpComposerExtensionStubsInspection + * + * If you receive an error at the following 3 lines, you must enable + * the following PHP extension: + * + * - if $use_mb_convert_encoding is true: mbstring + * - if $use_mb_convert_encoding is false: iconv + */ + $string = $this->use_mb_convert_encoding ? + mb_convert_encoding($string, $this->output_encoding, $this->input_encoding) : + iconv($this->input_encoding, $this->output_encoding, $string); + } + + return $string; + } + + /** + * @param array $fields + * + * @return array|false + */ + private function _validate_fields_for_unparse(array $fields) { + if (empty($fields)) { + $fields = $this->titles; + } + + if (empty($fields)) { + return array(); + } + + // this is needed because sometime titles property is overwritten instead of using fields parameter! + $titlesOnParse = !empty($this->data) ? array_keys(reset($this->data)) : array(); + + // both are identical, also in ordering OR we have no data (only titles) + if (empty($titlesOnParse) || array_values($fields) === array_values($titlesOnParse)) { + return array_combine($fields, $fields); + } + + // if renaming given by: $oldName => $newName (maybe with reorder and / or subset): + // todo: this will only work if titles are unique + $fieldOrder = array_intersect(array_flip($fields), $titlesOnParse); + if (!empty($fieldOrder)) { + return array_flip($fieldOrder); + } + + $fieldOrder = array_intersect($fields, $titlesOnParse); + if (!empty($fieldOrder)) { + return array_combine($fieldOrder, $fieldOrder); + } + + // original titles are not given in fields. that is okay if count is okay. + if (count($fields) != count($titlesOnParse)) { + throw new \UnexpectedValueException( + "The specified fields do not match any titles and do not match column count.\n" . + "\$fields was " . print_r($fields, true) . + "\$titlesOnParse was " . print_r($titlesOnParse, true)); + } + + return array_combine($titlesOnParse, $fields); + } + + /** + * Load local file or string. + * + * Only use this function if auto() and parse() don't handle your data well. + * + * This function load_data() is able to handle BOMs and encodings. The data + * is stored within the $this->file_data class field. + * + * @param string|null $input CSV file path or CSV data as a string + * + * Supplying CSV data (file content) here is deprecated. + * For CSV data, please use loadDataString(). + * Support for CSV data will be removed in v2.0.0. + * + * @return bool True on success + * @deprecated Use loadDataString() or loadFile() instead. + */ + public function load_data($input = null) { + return $this->loadFile($input); + } + + /** + * Load a file, but don't parse it. + * + * Only use this function if auto() and parseFile() don't handle your data well. + * + * This function is able to handle BOMs and encodings. The data + * is stored within the $this->file_data class field. + * + * @param string|null $file CSV file path + * + * @return bool True on success + */ + public function loadFile($file = null) { + $data = null; + + if (is_null($file)) { + $data = $this->_rfile($this->file); + } elseif (\strlen($file) <= PHP_MAXPATHLEN && file_exists($file)) { + $data = $this->_rfile($file); + if ($this->file != $file) { + $this->file = $file; + } + } else { + // It is CSV data as a string. + + // WARNING: + // Supplying CSV data to load_data() will no longer + // be supported in a future version of ParseCsv. + // This function will return false for invalid paths from v2.0.0 onwards. + + // Use ->loadDataString() instead. + + $data = $file; + } + + return $this->loadDataString($data); + } + + /** + * Load a data string, but don't parse it. + * + * Only use this function if autoDetectionForDataString() and parse() don't handle your data well. + * + * This function is able to handle BOMs and encodings. The data + * is stored within the $this->file_data class field. + * + * @param string|null $file_path CSV file path + * + * @return bool True on success + */ + public function loadDataString($data) { + if (!empty($data)) { + if (strpos($data, "\xef\xbb\xbf") === 0) { + // strip off BOM (UTF-8) + $data = substr($data, 3); + $this->encoding('UTF-8'); + } elseif (strpos($data, "\xff\xfe") === 0) { + // strip off BOM (UTF-16 little endian) + $data = substr($data, 2); + $this->encoding("UCS-2LE"); + } elseif (strpos($data, "\xfe\xff") === 0) { + // strip off BOM (UTF-16 big endian) + $data = substr($data, 2); + $this->encoding("UTF-16"); + } + + if ($this->convert_encoding && $this->input_encoding !== $this->output_encoding) { + /** @noinspection PhpComposerExtensionStubsInspection + * + * If you receive an error at the following 3 lines, you must enable + * the following PHP extension: + * + * - if $use_mb_convert_encoding is true: mbstring + * - if $use_mb_convert_encoding is false: iconv + */ + $data = $this->use_mb_convert_encoding ? + mb_convert_encoding($data, $this->output_encoding, $this->input_encoding) : + iconv($this->input_encoding, $this->output_encoding, $data); + } + + if (substr($data, -1) != "\n") { + $data .= "\n"; + } + + $this->file_data = &$data; + return true; + } + + return false; + } + + // ============================================== + // ----- [ Internal Functions ] ----------------- + // ============================================== + + /** + * Validate a row against specified conditions + * + * @param array $row array with values from a row + * @param string|null $conditions specified conditions that the row must match + * + * @return bool + */ + protected function _validate_row_conditions($row = array(), $conditions = null) { + if (!empty($row)) { + if (!empty($conditions)) { + $condition_array = (strpos($conditions, ' OR ') !== false) ? + explode(' OR ', $conditions) : + array($conditions); + $or = ''; + foreach ($condition_array as $key => $value) { + if (strpos($value, ' AND ') !== false) { + $value = explode(' AND ', $value); + $and = ''; + + foreach ($value as $k => $v) { + $and .= $this->_validate_row_condition($row, $v); + } + + $or .= (strpos($and, '0') !== false) ? '0' : '1'; + } else { + $or .= $this->_validate_row_condition($row, $value); + } + } + + return strpos($or, '1') !== false; + } + + return true; + } + + return false; + } + + /** + * Validate a row against a single condition + * + * @param array $row array with values from a row + * @param string $condition specified condition that the row must match + * + * @return string single 0 or 1 + */ + protected function _validate_row_condition($row, $condition) { + $operators = array( + '=', + 'equals', + 'is', + '!=', + 'is not', + '<', + 'is less than', + '>', + 'is greater than', + '<=', + 'is less than or equals', + '>=', + 'is greater than or equals', + 'contains', + 'does not contain', + ); + + $operators_regex = array(); + + foreach ($operators as $value) { + $operators_regex[] = preg_quote($value, '/'); + } + + $operators_regex = implode('|', $operators_regex); + + if (preg_match('/^(.+) (' . $operators_regex . ') (.+)$/i', trim($condition), $capture)) { + $field = $capture[1]; + $op = strtolower($capture[2]); + $value = $capture[3]; + if ($op == 'equals' && preg_match('/^(.+) is (less|greater) than or$/i', $field, $m)) { + $field = $m[1]; + $op = strtolower($m[2]) == 'less' ? '<=' : '>='; + } + if ($op == 'is' && preg_match('/^(less|greater) than (.+)$/i', $value, $m)) { + $value = $m[2]; + $op = strtolower($m[1]) == 'less' ? '<' : '>'; + } + if ($op == 'is' && preg_match('/^not (.+)$/i', $value, $m)) { + $value = $m[1]; + $op = '!='; + } + + if (preg_match('/^([\'"])(.*)([\'"])$/', $value, $capture) && $capture[1] == $capture[3]) { + $value = strtr($capture[2], array( + "\\n" => "\n", + "\\r" => "\r", + "\\t" => "\t", + )); + + $value = stripslashes($value); + } + + if (array_key_exists($field, $row)) { + $op_equals = in_array($op, ['=', 'equals', 'is'], true); + if ($op_equals && $row[$field] == $value) { + return '1'; + } elseif (($op == '!=' || $op == 'is not') && $row[$field] != $value) { + return '1'; + } elseif (($op == '<' || $op == 'is less than') && $row[$field] < $value) { + return '1'; + } elseif (($op == '>' || $op == 'is greater than') && $row[$field] > $value) { + return '1'; + } elseif (($op == '<=' || $op == 'is less than or equals') && $row[$field] <= $value) { + return '1'; + } elseif (($op == '>=' || $op == 'is greater than or equals') && $row[$field] >= $value) { + return '1'; + } elseif ($op == 'contains' && preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) { + return '1'; + } elseif ($op == 'does not contain' && !preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) { + return '1'; + } else { + return '0'; + } + } + } + + return '1'; + } + + /** + * Validates if the row is within the offset or not if sorting is disabled + * + * @param int $current_row the current row number being processed + * + * @return bool + */ + protected function _validate_offset($current_row) { + return + $this->sort_by !== null || + $this->offset === null || + $current_row >= $this->offset || + ($this->heading && $current_row == 0); + } + + /** + * Enclose values if needed + * - only used by unparse() + * + * @param string|null $value Cell value to process + * @param string $delimiter Character to put between cells on the same row + * + * @return string Processed value + */ + protected function _enclose_value($value, $delimiter) { + if ($value !== null && $value != '') { + $delimiter_quoted = $delimiter ? + preg_quote($delimiter, '/') . "|" + : ''; + $enclosure_quoted = preg_quote($this->enclosure, '/'); + $pattern = "/" . $delimiter_quoted . $enclosure_quoted . "|\n|\r/i"; + if ($this->enclose_all || preg_match($pattern, $value) || strpos($value, ' ') === 0 || substr($value, -1) == ' ') { + $value = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $value); + $value = $this->enclosure . $value . $this->enclosure; + } + } + + return $value; + } + + /** + * Check file data + * + * @param string|null $file local filename + * + * @return bool + */ + protected function _check_data($file = null) { + if (empty($this->file_data)) { + if (is_null($file)) { + $file = $this->file; + } + + return $this->loadFile($file); + } + + return true; + } + + /** + * Check if passed info might be delimiter. + * Only used by find_delimiter + * + * @param string $char Potential field separating character + * @param array $array Frequency + * @param int $depth Number of analyzed rows + * @param string $preferred Preferred delimiter characters + * + * @return string|false special string used for delimiter selection, or false + */ + protected function _check_count($char, $array, $depth, $preferred) { + if ($depth === count($array)) { + $first = null; + $equal = null; + $almost = false; + foreach ($array as $value) { + if ($first == null) { + $first = $value; + } elseif ($value == $first && $equal !== false) { + $equal = true; + } elseif ($value == $first + 1 && $equal !== false) { + $equal = true; + $almost = true; + } else { + $equal = false; + } + } + + if ($equal || $depth === 1) { + $match = $almost ? 2 : 1; + $pref = strpos($preferred, $char); + $pref = ($pref !== false) ? str_pad($pref, 3, '0', STR_PAD_LEFT) : '999'; + + return $pref . $match . '.' . (99999 - str_pad($first, 5, '0', STR_PAD_LEFT)); + } else { + return false; + } + } + return false; + } + + /** + * Read local file. + * + * @param string $filePath local filename + * + * @return string|false Data from file, or false on failure + */ + protected function _rfile($filePath) { + if (is_readable($filePath)) { + $data = file_get_contents($filePath); + if ($data === false) { + return false; + } + + if (preg_match('/\.php$/i', $filePath) && preg_match('/<\?.*?\?>(.*)/ms', $data, $strip)) { + // Return section behind closing tags. + // This parsing is deprecated and will be removed in v2.0.0. + $data = ltrim($strip[1]); + } + + return rtrim($data, "\r\n"); + } + + return false; + } + + /** + * Write to local file + * + * @param string $file local filename + * @param string $content data to write to file + * @param string $mode fopen() mode + * @param int $lock flock() mode + * + * @return bool + * True on success + * + */ + protected function _wfile($file, $content = '', $mode = 'wb', $lock = LOCK_EX) { + if ($fp = fopen($file, $mode)) { + flock($fp, $lock); + $re = fwrite($fp, $content); + $re2 = fclose($fp); + + if ($re !== false && $re2 !== false) { + return true; + } + } + + return false; + } + + /** + * Detect separator using a nonstandard hack: such file starts with the + * first line containing only "sep=;", where the last character is the + * separator. Microsoft Excel is able to open such files. + * + * @param string $data file data + * + * @return string|false detected delimiter, or false if none found + */ + protected function _get_delimiter_from_sep_row($data) { + $sep = false; + // 32 bytes should be quite enough data for our sniffing, chosen arbitrarily + $sepPrefix = substr($data, 0, 32); + if (preg_match('/^sep=(.)\\r?\\n/i', $sepPrefix, $sepMatch)) { + // we get separator. + $sep = $sepMatch[1]; + } + return $sep; + } + + /** + * Support for Excel-compatible sep=? row. + * + * @param string $data_string file data to be updated + * + * @return bool TRUE if sep= line was found at the very beginning of the file + */ + protected function _detect_and_remove_sep_row_from_data(&$data_string) { + $sep = $this->_get_delimiter_from_sep_row($data_string); + if ($sep === false) { + return false; + } + + $this->delimiter = $sep; + + // likely to be 5, but let's not assume we're always single-byte. + $pos = 4 + strlen($sep); + // the next characters should be a line-end + if (substr($data_string, $pos, 1) === "\r") { + $pos++; + } + if (substr($data_string, $pos, 1) === "\n") { + $pos++; + } + + // remove delimiter and its line-end (the data param is by-ref!) + $data_string = substr($data_string, $pos); + return true; + } + + /** + * @param int $search_depth Number of rows to analyze + * @param string $preferred Preferred delimiter characters + * @param string $enclosure Enclosure character, default is double quote + * @param string $data The file content + */ + protected function _guess_delimiter($search_depth, $preferred, $enclosure, $data) { + $chars = []; + $strlen = strlen($data); + $enclosed = false; + $current_row = 1; + $to_end = true; + + // The dash is the only character we don't want quoted, as it would + // prevent character ranges within $auto_non_chars: + $quoted_auto_non_chars = preg_quote($this->auto_non_chars, '/'); + $quoted_auto_non_chars = str_replace('\-', '-', $quoted_auto_non_chars); + $pattern = '/[' . $quoted_auto_non_chars . ']/i'; + + // walk specific depth finding possible delimiter characters + for ($i = 0; $i < $strlen; $i++) { + $ch = $data[$i]; + $nch = isset($data[$i + 1]) ? $data[$i + 1] : false; + $pch = isset($data[$i - 1]) ? $data[$i - 1] : false; + + // open and closing quotes + $is_newline = ($ch == "\n" && $pch != "\r") || $ch == "\r"; + if ($ch == $enclosure) { + if (!$enclosed || $nch != $enclosure) { + $enclosed = !$enclosed; + } elseif ($enclosed) { + $i++; + } + + // end of row + } elseif ($is_newline && !$enclosed) { + if ($current_row >= $search_depth) { + $strlen = 0; + $to_end = false; + } else { + $current_row++; + } + + // count character + } elseif (!$enclosed) { + if (!preg_match($pattern, $ch)) { + if (!isset($chars[$ch][$current_row])) { + $chars[$ch][$current_row] = 1; + } else { + $chars[$ch][$current_row]++; + } + } + } + } + + // filtering + $depth = $to_end ? $current_row - 1 : $current_row; + $filtered = []; + foreach ($chars as $char => $value) { + if ($match = $this->_check_count($char, $value, $depth, $preferred)) { + $filtered[$match] = $char; + } + } + + // capture most probable delimiter + ksort($filtered); + $this->delimiter = reset($filtered); + } + + /** + * getCollection + * Returns a Illuminate/Collection object + * This may prove to be helpful to people who want to + * create macros, and or use map functions + * + * @access public + * @link https://laravel.com/docs/5.6/collections + * + * @throws \ErrorException - If the Illuminate\Support\Collection class is not found + * + * @return Collection + */ + public function getCollection() { + //does the Illuminate\Support\Collection class exists? + //this uses the autoloader to try to determine + //@see http://php.net/manual/en/function.class-exists.php + if (class_exists('Illuminate\Support\Collection', true) == false) { + throw new \ErrorException('It would appear you have not installed the illuminate/support package!'); + } + + //return the collection + return new Collection($this->data); + } +} diff --git a/lib/parsecsv-1.3.2/src/enums/AbstractEnum.php b/lib/parsecsv-1.3.2/src/enums/AbstractEnum.php new file mode 100644 index 0000000..19dae3f --- /dev/null +++ b/lib/parsecsv-1.3.2/src/enums/AbstractEnum.php @@ -0,0 +1,38 @@ +isValid($value)) { + throw new \UnexpectedValueException("Value '$value' is not part of the enum " . get_called_class()); + } + $this->value = $value; + } + + public static function getConstants() { + $class = get_called_class(); + $reflection = new \ReflectionClass($class); + + return $reflection->getConstants(); + } + + /** + * Check if enum value is valid + * + * @param $value + * + * @return bool + */ + public static function isValid($value) { + return in_array($value, static::getConstants(), true); + } +} diff --git a/lib/parsecsv-1.3.2/src/enums/DatatypeEnum.php b/lib/parsecsv-1.3.2/src/enums/DatatypeEnum.php new file mode 100644 index 0000000..7f490e9 --- /dev/null +++ b/lib/parsecsv-1.3.2/src/enums/DatatypeEnum.php @@ -0,0 +1,120 @@ + null, + self::TYPE_INT => 'isValidInteger', + self::TYPE_BOOL => 'isValidBoolean', + self::TYPE_FLOAT => 'isValidFloat', + self::TYPE_DATE => 'isValidDate', + ); + + /** + * Checks data type for given string. + * + * @param string $value + * + * @return bool|string + */ + public static function getValidTypeFromSample($value) { + $value = trim((string) $value); + + if (empty($value)) { + return false; + } + + foreach (self::$validators as $type => $validator) { + if ($validator === null) { + continue; + } + + if (method_exists(__CLASS__, $validator) && self::$validator($value)) { + return $type; + } + } + + return self::__DEFAULT; + } + + /** + * Check if string is float value. + * + * @param string $value + * + * @return bool + */ + private static function isValidFloat($value) { + return (bool) preg_match(self::REGEX_FLOAT, $value); + } + + /** + * Check if string is integer value. + * + * @param string $value + * + * @return bool + */ + private static function isValidInteger($value) { + return (bool) preg_match(self::REGEX_INT, $value); + } + + /** + * Check if string is boolean. + * + * @param string $value + * + * @return bool + */ + private static function isValidBoolean($value) { + return (bool) preg_match(self::REGEX_BOOL, $value); + } + + /** + * Check if string is date. + * + * @param string $value + * + * @return bool + */ + private static function isValidDate($value) { + return (bool) strtotime($value); + } +} diff --git a/lib/parsecsv-1.3.2/src/enums/FileProcessingModeEnum.php b/lib/parsecsv-1.3.2/src/enums/FileProcessingModeEnum.php new file mode 100644 index 0000000..b545c68 --- /dev/null +++ b/lib/parsecsv-1.3.2/src/enums/FileProcessingModeEnum.php @@ -0,0 +1,28 @@ + SORT_REGULAR, + self::SORT_TYPE_STRING => SORT_STRING, + self::SORT_TYPE_NUMERIC => SORT_NUMERIC, + ); + + public static function getSorting($type) { + if (array_key_exists($type, self::$sorting)) { + return self::$sorting[$type]; + } + + return self::$sorting[self::__DEFAULT]; + } +} diff --git a/lib/parsecsv-1.3.2/src/extensions/DatatypeTrait.php b/lib/parsecsv-1.3.2/src/extensions/DatatypeTrait.php new file mode 100644 index 0000000..c0a4c10 --- /dev/null +++ b/lib/parsecsv-1.3.2/src/extensions/DatatypeTrait.php @@ -0,0 +1,102 @@ += 5.5 + * + * @uses DatatypeEnum::getValidTypeFromSample + * + * @return array|bool + */ + public function getDatatypes() { + if (empty($this->data)) { + $this->data = $this->_parse_string(); + } + if (!is_array($this->data)) { + throw new \UnexpectedValueException('No data set yet.'); + } + + $result = []; + foreach ($this->titles as $cName) { + $column = array_column($this->data, $cName); + $cDatatypes = array_map(DatatypeEnum::class . '::getValidTypeFromSample', $column); + + $result[$cName] = $this->getMostFrequentDatatypeForColumn($cDatatypes); + } + + $this->data_types = $result; + + return !empty($this->data_types) ? $this->data_types : []; + } + + /** + * Check data type of titles / first row for auto detecting if this could be + * a heading line. + * + * Requires PHP >= 5.5 + * + * @uses DatatypeEnum::getValidTypeFromSample + * + * @return bool + */ + public function autoDetectFileHasHeading() { + if (empty($this->data)) { + throw new \UnexpectedValueException('No data set yet.'); + } + + if ($this->heading) { + $firstRow = $this->titles; + } else { + $firstRow = $this->data[0]; + } + + $firstRow = array_filter($firstRow); + if (empty($firstRow)) { + return false; + } + + $firstRowDatatype = array_map(DatatypeEnum::class . '::getValidTypeFromSample', $firstRow); + + return $this->getMostFrequentDatatypeForColumn($firstRowDatatype) === DatatypeEnum::TYPE_STRING; + } +} From 05d250b63187ce5df1028a90b50d7fbc1df2e329 Mon Sep 17 00:00:00 2001 From: animaux Date: Wed, 14 Aug 2024 08:27:54 +0200 Subject: [PATCH 6/6] Include driver for image_upload field (copied from upload) --- .DS_Store | Bin 6148 -> 6148 bytes drivers/ImportDriver_association.php | 135 +++++++++++++++++++++ drivers/ImportDriver_image_upload.php | 153 ++++++++++++++++++++++++ drivers/ImportDriver_selectbox_link.php | 2 + extension.meta.xml | 16 ++- 5 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 drivers/ImportDriver_association.php create mode 100644 drivers/ImportDriver_image_upload.php diff --git a/.DS_Store b/.DS_Store index 06a9cf5f4c6904d1369a27de64e7f68db61e519c..617172a67f178632639fdcd600e861424fd1c375 100644 GIT binary patch delta 352 zcmZoMXfc@J&nd#dz`)4BAi%IOW6=)tjy}j zE^_84Sj9x3iaJ&vB)%de!{p7ZhW0s=xj=S<>|SQ%W=18eBIAL{v(fdX zFcdLlGL!-J6)_ZJ)5poC2((X|%}@ns((xRrBOv>j8FGNSlF-xxWhXbVNis1dY)pK@ KxS5^fFFydNEI$PR diff --git a/drivers/ImportDriver_association.php b/drivers/ImportDriver_association.php new file mode 100644 index 0000000..004eb3f --- /dev/null +++ b/drivers/ImportDriver_association.php @@ -0,0 +1,135 @@ +type = 'association'; + } + + /** + * Returns related Field ID for this Reference Link + * @todo This only handles a Reference Link that links to one section + * @return integer + */ + private function getRelatedField() + { + // Get the correct ID of the related fields + $related_field = Symphony::Database()->fetchVar('related_field_id', 0, 'SELECT `related_field_id` FROM `tbl_fields_association` WHERE `field_id` = ' . $this->field->get('id')); + + return $related_field; + } + + /** + * Process the data so it can be imported into the entry. + * @param $value + * The value to import + * @param $entry_id + * If a duplicate is found, an entry ID will be provided. + * @return The data returned by the field object + */ + public function import($value, $entry_id = null) + { + // Import selectbox link: + $related_field_id = $this->getRelatedField(); + $data = explode(',', $value); + $related_ids = array('relation_id'=>array()); + foreach ($data as $relationValue) { + $related_ids['relation_id'] = $related_ids['relation_id'] ?? null; + $related_ids['relation_id'][] = Symphony::Database()->fetchVar('entry_id', 0, sprintf(' + SELECT `entry_id` + FROM `tbl_entries_data_%d` + WHERE `value` = "%s"; + ', + $related_field_id, + Symphony::Database()->cleanValue(trim($relationValue)) + )); + } + + return $related_ids; + } + + /** + * Process the data so it can be exported to a CSV + * @param $data + * The data as provided by the entry + * @param $entry_id + * The ID of the entry that is exported + * @return string + * A string representation of the data to import into the CSV file + */ + public function export($data, $entry_id = null) + { + $data['relation_id'] = $data['relation_id'] ?? null; + if (!is_array($data['relation_id'])) { + $data['relation_id'] = array($data['relation_id']); + } + + $related_values = array(); + $related_field_id = $this->getRelatedField(); + foreach ($data['relation_id'] as $relation_id) { + if (!empty($relation_id)) { + $row = Symphony::Database()->fetchRow(0, 'SELECT * FROM `tbl_entries_data_' . $related_field_id . '` WHERE `entry_id` = ' . $relation_id . ';'); + if (isset($row['value'])) { + $related_values[] = trim($row['value']); + } else { + // Fallback to empty value: + $related_values[] = ''; + } + } + } + + return implode(', ', $related_values); + } + + /** + * Scan the database for a specific value + * @param $value + * The value to scan for + * @return null|string + * The ID of the entry found, or null if no match is found. + */ + public function scanDatabase($value) + { + $related_field_id = $this->getRelatedField(); + $searchResult = Symphony::Database()->fetchVar('entry_id', 0, sprintf(' + SELECT `entry_id` + FROM `tbl_entries_data_%d` + WHERE `value` = "%s"; + ', + $related_field_id, + Symphony::Database()->cleanValue(trim($value)) + )); + + // If there is a matching result, it means the entry exists in the SBL section + // Now check to see if there is another entry with this value in the current section + if ($searchResult != false) { + $existing = Symphony::Database()->fetchVar('entry_id', 0, sprintf(' + SELECT `entry_id` + FROM `tbl_entries_data_%d` + WHERE `relation_id` = %d; + ', + $this->field->get('id'), + $searchResult + )); + + if ($existing != false) { + return $existing; + } else { + return null; + } + } else { + return null; + } + } +} diff --git a/drivers/ImportDriver_image_upload.php b/drivers/ImportDriver_image_upload.php new file mode 100644 index 0000000..1c8cce7 --- /dev/null +++ b/drivers/ImportDriver_image_upload.php @@ -0,0 +1,153 @@ +type = 'image_upload'; + } + + /** + * Process the data so it can be imported into the entry. + * @param $value + * The value to import + * @param $entry_id + * If a duplicate is found, an entry ID will be provided. + * @return The data returned by the field object + */ + public function import($value, $entry_id = null) + { + $destination = $this->field->get('destination'); + $filename = str_replace('/workspace/', '/', $destination) . '/' . str_replace($destination, '', trim($value)); + // Check if the file exists: + if (file_exists(DOCROOT . $destination . '/' . trim($value))) { + // File exists, create the link: + // Check if there already exists an entry with this filename. If so, this entry will not be stored (filename must be unique) + $sql = 'SELECT COUNT(*) AS `total` FROM `tbl_entries_data_' . $this->field->get('id') . '` WHERE `file` = \'' . $filename . '\';'; + $total = Symphony::Database()->fetchVar('total', 0, $sql); + if ($total == 0) { + $fileData = $this->field->processRawFieldData($value, $this->field->__OK__); + $fileData['file'] = trim($filename); + $fileData['size'] = filesize(DOCROOT . $destination . '/' . $value); + $fileData['mimetype'] = mime_content_type(DOCROOT . $destination . '/' . $value); + $fileData['meta'] = serialize($this->field->getMetaInfo(DOCROOT . $destination . '/' . $value, $fileData['mimetype'])); + + return $fileData; + } else { + // File already exists, don't store: + return false; + } + } else { + // File is stored in the CSV, but does not exists. Save it anyway, for database sake: + if (!empty($value)) { + $fileData = $this->field->processRawFieldData($value, $this->field->__OK__); + $fileData['file'] = trim($filename); + $fileData['size'] = filesize(DOCROOT . $destination . '/' . $value); + $fileData['mimetype'] = ''; // mime_content_type(DOCROOT . $destination . '/' . $value); + $fileData['meta'] = serialize($this->field->getMetaInfo(DOCROOT . $destination . '/' . $value, $fileData['mimetype'])); + + return $fileData; + } + } + + return false; + } + + /** + * Process the data so it can be exported to a CSV + * @param $data + * The data as provided by the entry + * @param $entry_id + * The ID of the entry that is exported + * @return string + * A string representation of the data to import into the CSV file + */ + public function export($data, $entry_id = null) + { + return trim($data['file']); + } + +} + +if (!function_exists('mime_content_type')) { + + function mime_content_type($filename) + { + $mime_types = array( + + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + + // ms office + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + + // open office + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + ); + + $ext = strtolower(array_pop(explode('.', $filename))); + if (array_key_exists($ext, $mime_types)) { + return $mime_types[$ext]; + } elseif (function_exists('finfo_open')) { + $finfo = finfo_open(FILEINFO_MIME); + $mimetype = finfo_file($finfo, $filename); + finfo_close($finfo); + + return $mimetype; + } else { + return 'application/octet-stream'; + } + } +} diff --git a/drivers/ImportDriver_selectbox_link.php b/drivers/ImportDriver_selectbox_link.php index 2d1ee3f..8580438 100644 --- a/drivers/ImportDriver_selectbox_link.php +++ b/drivers/ImportDriver_selectbox_link.php @@ -43,6 +43,7 @@ public function import($value, $entry_id = null) $data = explode(',', $value); $related_ids = array('relation_id'=>array()); foreach ($data as $relationValue) { + $related_ids['relation_id'] = $related_ids['relation_id'] ?? null; $related_ids['relation_id'][] = Symphony::Database()->fetchVar('entry_id', 0, sprintf(' SELECT `entry_id` FROM `tbl_entries_data_%d` @@ -67,6 +68,7 @@ public function import($value, $entry_id = null) */ public function export($data, $entry_id = null) { + $data['relation_id'] = $data['relation_id'] ?? null; if (!is_array($data['relation_id'])) { $data['relation_id'] = array($data['relation_id']); } diff --git a/extension.meta.xml b/extension.meta.xml index 672fead..bf37a01 100644 --- a/extension.meta.xml +++ b/extension.meta.xml @@ -18,16 +18,26 @@ Twisted Interactive http://www.twisted.nl + + Alexander Rutz + https://animaux.de + - + + - Include driver for image_upload field (copied from upload) + + + - Include driver for association field (copied from select bos fields) and fixes for the latter + + - Switch to parsecsv-1.3.2, PHP 8.1 compatibility (may require PHP 7) - - Array() —> array() + - Array() —> array() - - Include @wdebusschere’s fix `Allow custom backendurl URL -> SYMPHONY_URL` + - Include @wdebusschere’s fix `Allow custom backendurl URL -> SYMPHONY_URL` - PHP7 compatibility