From: Michael J. Rubinsky Date: Fri, 5 Feb 2010 15:18:32 +0000 (-0500) Subject: Fix layout of File_Csv pacakge X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=17e1125fd3b03136b5c20853072853d3a7f59eaf;p=horde.git Fix layout of File_Csv pacakge --- diff --git a/framework/File_Csv/lib/Horde/Csv.php b/framework/File_Csv/lib/Horde/Csv.php deleted file mode 100644 index dce2760d1..000000000 --- a/framework/File_Csv/lib/Horde/Csv.php +++ /dev/null @@ -1,531 +0,0 @@ - - * Copyright 2005-2010 The Horde Project (http://www.horde.org/) - * - * This source file is subject to version 2.0 of the PHP license, that is - * bundled with this package in the file LICENSE, and is available at through - * the world-wide-web at http://www.php.net/license/2_02.txt. If you did not - * receive a copy of the PHP license and are unable to obtain it through the - * world-wide-web, please send a note to license@php.net so we can mail you a - * copy immediately. - * - * @author Tomas Von Veschler Cox - * @author Jan Schneider - * @category Horde - * @package Horde_File_Csv - */ -class Horde_File_Csv -{ - /** Mode to use for reading from files */ - const MODE_READ = 'rb'; - - /** Mode to use for truncating files, then writing */ - const MODE_WRITE = 'wb'; - - /** Mode to use for appending to files */ - const MODE_APPEND = 'ab'; - - /** - * Discovers the format of a CSV file (the number of fields, the - * separator, the quote string, and the line break). - * - * We can't use the auto_detect_line_endings PHP setting, because it's not - * supported by fgets() contrary to what the manual says. - * - * @param string $file The CSV file name - * @param array $extraSeps Extra separators that should be checked for. - * - * @return array The format hash. - * @throws Horde_File_Csv_Exception - */ - static public function discoverFormat($file, $extraSeps = array()) - { - if (!$fp = @fopen($file, 'r')) { - throw new Horde_File_Csv_Exception('Could not open file: ' . $file); - } - - $seps = array("\t", ';', ':', ',', '~'); - $seps = array_merge($seps, $extraSeps); - $conf = $matches = array(); - $crlf = null; - - /* Take the first 10 lines and store the number of ocurrences for each - * separator in each line. */ - for ($i = 0; ($i < 10) && ($line = fgets($fp));) { - /* Do we have Mac line endings? */ - $lines = preg_split('/\r(?!\n)/', $line, 10); - $j = 0; - $c = count($lines); - if ($c > 1) { - $crlf = "\r"; - } - while ($i < 10 && $j < $c) { - $line = $lines[$j]; - if (!isset($crlf)) { - foreach (array("\r\n", "\n") as $c) { - if (substr($line, -strlen($c)) == $c) { - $crlf = $c; - break; - } - } - } - ++$i; - ++$j; - foreach ($seps as $sep) { - $matches[$sep][$i] = substr_count($line, $sep); - } - } - } - - if (isset($crlf)) { - $conf['crlf'] = $crlf; - } - - /* Group the results by amount of equal occurrences. */ - $amount = $fields = array(); - foreach ($matches as $sep => $lines) { - $times = array(0 => 0); - foreach ($lines as $num) { - if ($num > 0) { - $times[$num] = (isset($times[$num])) ? $times[$num] + 1 : 1; - } - } - arsort($times); - $fields[$sep] = key($times); - $amount[$sep] = $times[key($times)]; - } - arsort($amount); - $sep = key($amount); - - $conf['fields'] = $fields[$sep] + 1; - $conf['sep'] = $sep; - - /* Test if there are fields with quotes around in the first 10 - * lines. */ - $quotes = '"\''; - $quote = ''; - rewind($fp); - for ($i = 0; ($i < 10) && ($line = fgets($fp)); ++$i) { - if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U", $line, $match)) { - if ($match[1] == $match[2]) { - $quote = $match[1]; - break; - } - } - if (preg_match("|^([$quotes]).*([$quotes])$sep|", $line, $match) || - preg_match("|([$quotes]).*([$quotes])$sep\s$|Us", $line, $match)) { - if ($match[1] == $match[2]) { - $quote = $match[1]; - break; - } - } - } - $conf['quote'] = $quote; - - fclose($fp); - - // XXX What about trying to discover the "header"? - return $conf; - } - - /** - * Reads a row from a CSV file and returns it as an array. - * - * This method normalizes linebreaks to single newline characters (0x0a). - * - * @param string $file The name of the CSV file. - * @param array $conf The configuration for the CSV file. - * - * @return array|boolean The CSV data or false if no more data available. - * @throws Horde_File_Csv_Exception - */ - static public function read($file, &$conf) - { - $fp = self::getPointer($file, $conf, self::MODE_READ); - - $line = fgets($fp); - $line_length = strlen($line); - - /* Use readQuoted() if we have Mac line endings. */ - if (preg_match('/\r(?!\n)/', $line)) { - fseek($fp, -$line_length, SEEK_CUR); - return self::readQuoted($file, $conf); - } - - /* Normalize line endings. */ - $line = str_replace("\r\n", "\n", $line); - if (!strlen(trim($line))) { - return false; - } - - self::_line(self::_line() + 1); - - if ($conf['fields'] == 1) { - return array($line); - } - - $fields = explode($conf['sep'], $line); - if ($conf['quote']) { - $last = ltrim($fields[count($fields) - 1]); - /* Fallback to read the line with readQuoted() if we assume that - * the simple explode won't work right. */ - $last_len = strlen($last); - if (($last_len && - $last[$last_len - 1] == "\n" && - $last[0] == $conf['quote'] && - $last[strlen(rtrim($last)) - 1] != $conf['quote']) || - (count($fields) != $conf['fields']) - // XXX perhaps there is a separator inside a quoted field - // preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line) - ) { - fseek($fp, -$line_length, SEEK_CUR); - return self::readQuoted($file, $conf); - } else { - foreach ($fields as $k => $v) { - $fields[$k] = self::unquote(trim($v), $conf['quote']); - } - } - } else { - foreach ($fields as $k => $v) { - $fields[$k] = trim($v); - } - } - - if (count($fields) < $conf['fields']) { - self::warning(sprintf(_("Wrong number of fields in line %d. Expected %d, found %d."), self::_line(), $conf['fields'], count($fields))); - $fields = array_merge($fields, array_fill(0, $conf['fields'] - count($fields), '')); - } elseif (count($fields) > $conf['fields']) { - self::warning(sprintf(_("More fields found in line %d than the expected %d."), self::_line(), $conf['fields'])); - array_splice($fields, $conf['fields']); - } - - return $fields; - } - - /** - * Reads a row from a CSV file and returns it as an array. - * - * This method is able to read fields with multiline data and normalizes - * linebreaks to single newline characters (0x0a). - * - * @param string $file The name of the CSV file. - * @param array $conf The configuration for the CSV file. - * - * @return array|boolean The CSV data or false if no more data available. - * @throws Horde_File_Csv_Exception - */ - static public function readQuoted($file, &$conf) - { - $fp = self::getPointer($file, $conf, self::MODE_READ); - - /* A buffer with all characters of the current field read so far. */ - $buff = ''; - /* The current character. */ - $c = null; - /* The read fields. */ - $ret = false; - /* The number of the current field. */ - $i = 0; - /* Are we inside a quoted field? */ - $in_quote = false; - /* Did we just process an escaped quote? */ - $quote_escaped = false; - /* Is the last processed quote the first of a field? */ - $first_quote = false; - - while (($ch = fgetc($fp)) !== false) { - /* Normalize line breaks. */ - if ($ch == $conf['crlf']) { - $ch = "\n"; - } elseif (strlen($conf['crlf']) == 2 && $ch == $conf['crlf'][0]) { - $next = fgetc($fp); - if (!$next) { - break; - } - if ($next == $conf['crlf'][1]) { - $ch = "\n"; - } - } - - /* Previous character. */ - $prev = $c; - /* Current character. */ - $c = $ch; - - /* Simple character. */ - if ($c != $conf['quote'] && - $c != $conf['sep'] && - $c != "\n") { - $buff .= $c; - if (!$i) { - $i = 1; - } - $first_quote = $quote_escaped = false; - continue; - } - - if ($c == $conf['quote'] && !$in_quote) { - /* Quoted field begins. */ - $in_quote = true; - $buff = ''; - if (!$i) { - $i = 1; - } - } elseif ($in_quote) { - /* We do NOT check for the closing quote immediately, but when - * we got the character AFTER the closing quote. */ - if ($c == $conf['quote'] && $prev == $conf['quote'] && - !$quote_escaped) { - /* Escaped (double) quotes. */ - $first_quote = $quote_escaped = true; - $prev = null; - /* Simply skip the second quote. */ - continue; - } elseif ($c == $conf['sep'] && $prev == $conf['quote']) { - /* Quoted field ends with a delimiter. */ - $in_quote = $quote_escaped = false; - $first_quote = true; - } elseif ($c == "\n") { - /* We have a linebreak inside the quotes. */ - if (strlen($buff) == 1 && - $buff[0] == $conf['quote'] && - $quote_escaped && $first_quote) { - /* A line break after a closing quote of an empty - * field, field and row end here. */ - $in_quote = false; - } elseif (strlen($buff) >= 1 && - $buff[strlen($buff) - 1] == $conf['quote'] && - !$quote_escaped && !$first_quote) { - /* A line break after a closing quote, field and row - * end here. This is NOT the closing quote if we - * either process an escaped (double) quote, or if the - * quote before the line break was the opening - * quote. */ - $in_quote = false; - } else { - /* Only increment the line number. Line breaks inside - * quoted fields are part of the field content. */ - self::_line(self::_line() + 1); - } - $quote_escaped = false; - $first_quote = true; - } - } - - if (!$in_quote && - ($c == $conf['sep'] || $c == "\n")) { - /* End of line or end of field. */ - if ($c == $conf['sep'] && - (count($ret) + 1) == $conf['fields']) { - /* More fields than expected. Forward the line pointer to - * the EOL and drop the remainder. */ - while ($c !== false && $c != "\n") { - $c = fgetc($fp); - } - self::warning(sprintf('More fields found in line %d than the expected %d.', self::_line(), $conf['fields'])); - } - - if ($c == "\n" && - $i != $conf['fields']) { - /* Less fields than expected. */ - if ($i == 0) { - /* Skip empty lines. */ - return $ret; - } - self::warning(sprintf('Wrong number of fields in line %d. Expected %d, found %d.', self::_line(), $conf['fields'], $i)); - - $ret[] = self::unquote($buff, $conf['quote']); - return array_merge($ret, array_fill(0, $conf['fields'] - $i, '')); - } - - /* Remove surrounding quotes from quoted fields. */ - $ret[] = ($buff == '"') - ? '' - : self::unquote($buff, $conf['quote']); - if (count($ret) == $conf['fields']) { - return $ret; - } - - $buff = ''; - ++$i; - continue; - } - $buff .= $c; - } - - return $ret; - } - - /** - * Writes a hash into a CSV file. - * - * @param string $file The name of the CSV file. - * @param array $fields The CSV data. - * @param array $conf The configuration for the CSV file. - * - * @throws Horde_File_Csv_Exception - */ - static public function write($file, $fields, &$conf) - { - $fp = self::getPointer($file, $conf, self::MODE_WRITE); - - if (count($fields) != $conf['fields']) { - throw new Horde_File_Csv_Exception(sprintf('Wrong number of fields. Expected %d, found %d.', $conf['fields'], count($fields))); - } - - $write = ''; - for ($i = 0; $i < count($fields); ++$i) { - $write .= (!is_numeric($fields[$i]) && $conf['quote']) - ? $conf['quote'] . $fields[$i] . $conf['quote'] - : $fields[$i]; - $write .= ($i < (count($fields) - 1)) - ? $conf['sep'] - : $conf['crlf']; - } - - if (!fwrite($fp, $write)) { - throw new Horde_File_Csv_Exception(sprintf('Cannot write to file "%s"', $file)); - } - - fclose($fp); - } - - /** - * Removes surrounding quotes from a string and normalizes linebreaks. - * - * @param string $field The string to unquote. - * @param string $quote The quote character. - * @param string $crlf The linebreak character. - * - * @return string The unquoted data. - */ - static public function unquote($field, $quote, $crlf = null) - { - /* Skip empty fields (form: ;;) */ - if (!strlen($field)) { - return $field; - } - - if ($quote && $field[0] == $quote && - $field[strlen($field) - 1] == $quote) { - /* Normalize only for BC. */ - if ($crlf) { - $field = str_replace($crlf, "\n", $field); - } - return substr($field, 1, -1); - } - - return $field; - } - - /** - * Sets or gets the current line being parsed. - * - * @param integer $line If specified, the current line. - * - * @return integer The current line. - */ - static protected function _line($line = null) - { - static $current_line = 0; - - if (!is_null($line)) { - $current_line = $line; - } - - return $current_line; - } - - /** - * Adds a warning to or retrieves and resets the warning stack. - * - * @param string A warning string. If not specified, the existing - * warnings will be returned instead and the warning stack - * gets emptied. - * - * @return array If no parameter has been specified, the list of existing - * warnings. - */ - static public function warning($warning = null) - { - static $warnings = array(); - - if (is_null($warning)) { - $return = $warnings; - $warnings = array(); - return $return; - } - - $warnings[] = $warning; - } - - /** - * Returns or creates the file descriptor associated with a file. - * - * @param string $file The name of the file - * @param array $conf The configuration - * @param string $mode The open mode. self::MODE_READ or - * self::MODE_WRITE. - * - * @return resource The file resource. - * - * @throws Horde_File_Csv_Exception - */ - static public function getPointer($file, &$conf, $mode = self::MODE_READ) - { - static $resources = array(); - static $config = array(); - - if (isset($resources[$file])) { - $conf = $config[$file]; - return $resources[$file]; - } - - if (!is_array($conf)) { - throw new Horde_File_Csv_Exception('Invalid configuration.'); - } - - if (!isset($conf['fields']) || !is_numeric($conf['fields'])) { - throw new Horde_File_Csv_Exception('The number of fields must be numeric.'); - } - - if (isset($conf['sep'])) { - if (strlen($conf['sep']) != 1) { - throw new Horde_File_Csv_Exception('The separator must be one single character.'); - } - } elseif ($conf['fields'] > 1) { - throw new Horde_File_Csv_Exception('No separator specified.'); - } - - if (!empty($conf['quote'])) { - if (strlen($conf['quote']) != 1) { - throw new Horde_File_Csv_Exception('The quote character must be one single character.'); - } - } else { - $conf['quote'] = ''; - } - - if (!isset($conf['crlf'])) { - $conf['crlf'] = "\n"; - } - - $config[$file] = $conf; - - $fp = @fopen($file, $mode); - if (!is_resource($fp)) { - throw new Horde_File_Csv_Exception(sprintf('Cannot open file "%s".', $file)); - } - $resources[$file] = $fp; - self::_line(0); - - if (($mode == self::MODE_READ) && !empty($conf['header'])) { - self::read($file, $conf); - } - - return $fp; - } - -} diff --git a/framework/File_Csv/lib/Horde/Csv/Exception.php b/framework/File_Csv/lib/Horde/Csv/Exception.php deleted file mode 100644 index ed5bb5be1..000000000 --- a/framework/File_Csv/lib/Horde/Csv/Exception.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @category Horde - * @package Horde_File_Csv - */ -class Horde_File_Csv_Exception extends Horde_Exception -{ -} diff --git a/framework/File_Csv/lib/Horde/File/Csv.php b/framework/File_Csv/lib/Horde/File/Csv.php new file mode 100644 index 000000000..dce2760d1 --- /dev/null +++ b/framework/File_Csv/lib/Horde/File/Csv.php @@ -0,0 +1,531 @@ + + * Copyright 2005-2010 The Horde Project (http://www.horde.org/) + * + * This source file is subject to version 2.0 of the PHP license, that is + * bundled with this package in the file LICENSE, and is available at through + * the world-wide-web at http://www.php.net/license/2_02.txt. If you did not + * receive a copy of the PHP license and are unable to obtain it through the + * world-wide-web, please send a note to license@php.net so we can mail you a + * copy immediately. + * + * @author Tomas Von Veschler Cox + * @author Jan Schneider + * @category Horde + * @package Horde_File_Csv + */ +class Horde_File_Csv +{ + /** Mode to use for reading from files */ + const MODE_READ = 'rb'; + + /** Mode to use for truncating files, then writing */ + const MODE_WRITE = 'wb'; + + /** Mode to use for appending to files */ + const MODE_APPEND = 'ab'; + + /** + * Discovers the format of a CSV file (the number of fields, the + * separator, the quote string, and the line break). + * + * We can't use the auto_detect_line_endings PHP setting, because it's not + * supported by fgets() contrary to what the manual says. + * + * @param string $file The CSV file name + * @param array $extraSeps Extra separators that should be checked for. + * + * @return array The format hash. + * @throws Horde_File_Csv_Exception + */ + static public function discoverFormat($file, $extraSeps = array()) + { + if (!$fp = @fopen($file, 'r')) { + throw new Horde_File_Csv_Exception('Could not open file: ' . $file); + } + + $seps = array("\t", ';', ':', ',', '~'); + $seps = array_merge($seps, $extraSeps); + $conf = $matches = array(); + $crlf = null; + + /* Take the first 10 lines and store the number of ocurrences for each + * separator in each line. */ + for ($i = 0; ($i < 10) && ($line = fgets($fp));) { + /* Do we have Mac line endings? */ + $lines = preg_split('/\r(?!\n)/', $line, 10); + $j = 0; + $c = count($lines); + if ($c > 1) { + $crlf = "\r"; + } + while ($i < 10 && $j < $c) { + $line = $lines[$j]; + if (!isset($crlf)) { + foreach (array("\r\n", "\n") as $c) { + if (substr($line, -strlen($c)) == $c) { + $crlf = $c; + break; + } + } + } + ++$i; + ++$j; + foreach ($seps as $sep) { + $matches[$sep][$i] = substr_count($line, $sep); + } + } + } + + if (isset($crlf)) { + $conf['crlf'] = $crlf; + } + + /* Group the results by amount of equal occurrences. */ + $amount = $fields = array(); + foreach ($matches as $sep => $lines) { + $times = array(0 => 0); + foreach ($lines as $num) { + if ($num > 0) { + $times[$num] = (isset($times[$num])) ? $times[$num] + 1 : 1; + } + } + arsort($times); + $fields[$sep] = key($times); + $amount[$sep] = $times[key($times)]; + } + arsort($amount); + $sep = key($amount); + + $conf['fields'] = $fields[$sep] + 1; + $conf['sep'] = $sep; + + /* Test if there are fields with quotes around in the first 10 + * lines. */ + $quotes = '"\''; + $quote = ''; + rewind($fp); + for ($i = 0; ($i < 10) && ($line = fgets($fp)); ++$i) { + if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U", $line, $match)) { + if ($match[1] == $match[2]) { + $quote = $match[1]; + break; + } + } + if (preg_match("|^([$quotes]).*([$quotes])$sep|", $line, $match) || + preg_match("|([$quotes]).*([$quotes])$sep\s$|Us", $line, $match)) { + if ($match[1] == $match[2]) { + $quote = $match[1]; + break; + } + } + } + $conf['quote'] = $quote; + + fclose($fp); + + // XXX What about trying to discover the "header"? + return $conf; + } + + /** + * Reads a row from a CSV file and returns it as an array. + * + * This method normalizes linebreaks to single newline characters (0x0a). + * + * @param string $file The name of the CSV file. + * @param array $conf The configuration for the CSV file. + * + * @return array|boolean The CSV data or false if no more data available. + * @throws Horde_File_Csv_Exception + */ + static public function read($file, &$conf) + { + $fp = self::getPointer($file, $conf, self::MODE_READ); + + $line = fgets($fp); + $line_length = strlen($line); + + /* Use readQuoted() if we have Mac line endings. */ + if (preg_match('/\r(?!\n)/', $line)) { + fseek($fp, -$line_length, SEEK_CUR); + return self::readQuoted($file, $conf); + } + + /* Normalize line endings. */ + $line = str_replace("\r\n", "\n", $line); + if (!strlen(trim($line))) { + return false; + } + + self::_line(self::_line() + 1); + + if ($conf['fields'] == 1) { + return array($line); + } + + $fields = explode($conf['sep'], $line); + if ($conf['quote']) { + $last = ltrim($fields[count($fields) - 1]); + /* Fallback to read the line with readQuoted() if we assume that + * the simple explode won't work right. */ + $last_len = strlen($last); + if (($last_len && + $last[$last_len - 1] == "\n" && + $last[0] == $conf['quote'] && + $last[strlen(rtrim($last)) - 1] != $conf['quote']) || + (count($fields) != $conf['fields']) + // XXX perhaps there is a separator inside a quoted field + // preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line) + ) { + fseek($fp, -$line_length, SEEK_CUR); + return self::readQuoted($file, $conf); + } else { + foreach ($fields as $k => $v) { + $fields[$k] = self::unquote(trim($v), $conf['quote']); + } + } + } else { + foreach ($fields as $k => $v) { + $fields[$k] = trim($v); + } + } + + if (count($fields) < $conf['fields']) { + self::warning(sprintf(_("Wrong number of fields in line %d. Expected %d, found %d."), self::_line(), $conf['fields'], count($fields))); + $fields = array_merge($fields, array_fill(0, $conf['fields'] - count($fields), '')); + } elseif (count($fields) > $conf['fields']) { + self::warning(sprintf(_("More fields found in line %d than the expected %d."), self::_line(), $conf['fields'])); + array_splice($fields, $conf['fields']); + } + + return $fields; + } + + /** + * Reads a row from a CSV file and returns it as an array. + * + * This method is able to read fields with multiline data and normalizes + * linebreaks to single newline characters (0x0a). + * + * @param string $file The name of the CSV file. + * @param array $conf The configuration for the CSV file. + * + * @return array|boolean The CSV data or false if no more data available. + * @throws Horde_File_Csv_Exception + */ + static public function readQuoted($file, &$conf) + { + $fp = self::getPointer($file, $conf, self::MODE_READ); + + /* A buffer with all characters of the current field read so far. */ + $buff = ''; + /* The current character. */ + $c = null; + /* The read fields. */ + $ret = false; + /* The number of the current field. */ + $i = 0; + /* Are we inside a quoted field? */ + $in_quote = false; + /* Did we just process an escaped quote? */ + $quote_escaped = false; + /* Is the last processed quote the first of a field? */ + $first_quote = false; + + while (($ch = fgetc($fp)) !== false) { + /* Normalize line breaks. */ + if ($ch == $conf['crlf']) { + $ch = "\n"; + } elseif (strlen($conf['crlf']) == 2 && $ch == $conf['crlf'][0]) { + $next = fgetc($fp); + if (!$next) { + break; + } + if ($next == $conf['crlf'][1]) { + $ch = "\n"; + } + } + + /* Previous character. */ + $prev = $c; + /* Current character. */ + $c = $ch; + + /* Simple character. */ + if ($c != $conf['quote'] && + $c != $conf['sep'] && + $c != "\n") { + $buff .= $c; + if (!$i) { + $i = 1; + } + $first_quote = $quote_escaped = false; + continue; + } + + if ($c == $conf['quote'] && !$in_quote) { + /* Quoted field begins. */ + $in_quote = true; + $buff = ''; + if (!$i) { + $i = 1; + } + } elseif ($in_quote) { + /* We do NOT check for the closing quote immediately, but when + * we got the character AFTER the closing quote. */ + if ($c == $conf['quote'] && $prev == $conf['quote'] && + !$quote_escaped) { + /* Escaped (double) quotes. */ + $first_quote = $quote_escaped = true; + $prev = null; + /* Simply skip the second quote. */ + continue; + } elseif ($c == $conf['sep'] && $prev == $conf['quote']) { + /* Quoted field ends with a delimiter. */ + $in_quote = $quote_escaped = false; + $first_quote = true; + } elseif ($c == "\n") { + /* We have a linebreak inside the quotes. */ + if (strlen($buff) == 1 && + $buff[0] == $conf['quote'] && + $quote_escaped && $first_quote) { + /* A line break after a closing quote of an empty + * field, field and row end here. */ + $in_quote = false; + } elseif (strlen($buff) >= 1 && + $buff[strlen($buff) - 1] == $conf['quote'] && + !$quote_escaped && !$first_quote) { + /* A line break after a closing quote, field and row + * end here. This is NOT the closing quote if we + * either process an escaped (double) quote, or if the + * quote before the line break was the opening + * quote. */ + $in_quote = false; + } else { + /* Only increment the line number. Line breaks inside + * quoted fields are part of the field content. */ + self::_line(self::_line() + 1); + } + $quote_escaped = false; + $first_quote = true; + } + } + + if (!$in_quote && + ($c == $conf['sep'] || $c == "\n")) { + /* End of line or end of field. */ + if ($c == $conf['sep'] && + (count($ret) + 1) == $conf['fields']) { + /* More fields than expected. Forward the line pointer to + * the EOL and drop the remainder. */ + while ($c !== false && $c != "\n") { + $c = fgetc($fp); + } + self::warning(sprintf('More fields found in line %d than the expected %d.', self::_line(), $conf['fields'])); + } + + if ($c == "\n" && + $i != $conf['fields']) { + /* Less fields than expected. */ + if ($i == 0) { + /* Skip empty lines. */ + return $ret; + } + self::warning(sprintf('Wrong number of fields in line %d. Expected %d, found %d.', self::_line(), $conf['fields'], $i)); + + $ret[] = self::unquote($buff, $conf['quote']); + return array_merge($ret, array_fill(0, $conf['fields'] - $i, '')); + } + + /* Remove surrounding quotes from quoted fields. */ + $ret[] = ($buff == '"') + ? '' + : self::unquote($buff, $conf['quote']); + if (count($ret) == $conf['fields']) { + return $ret; + } + + $buff = ''; + ++$i; + continue; + } + $buff .= $c; + } + + return $ret; + } + + /** + * Writes a hash into a CSV file. + * + * @param string $file The name of the CSV file. + * @param array $fields The CSV data. + * @param array $conf The configuration for the CSV file. + * + * @throws Horde_File_Csv_Exception + */ + static public function write($file, $fields, &$conf) + { + $fp = self::getPointer($file, $conf, self::MODE_WRITE); + + if (count($fields) != $conf['fields']) { + throw new Horde_File_Csv_Exception(sprintf('Wrong number of fields. Expected %d, found %d.', $conf['fields'], count($fields))); + } + + $write = ''; + for ($i = 0; $i < count($fields); ++$i) { + $write .= (!is_numeric($fields[$i]) && $conf['quote']) + ? $conf['quote'] . $fields[$i] . $conf['quote'] + : $fields[$i]; + $write .= ($i < (count($fields) - 1)) + ? $conf['sep'] + : $conf['crlf']; + } + + if (!fwrite($fp, $write)) { + throw new Horde_File_Csv_Exception(sprintf('Cannot write to file "%s"', $file)); + } + + fclose($fp); + } + + /** + * Removes surrounding quotes from a string and normalizes linebreaks. + * + * @param string $field The string to unquote. + * @param string $quote The quote character. + * @param string $crlf The linebreak character. + * + * @return string The unquoted data. + */ + static public function unquote($field, $quote, $crlf = null) + { + /* Skip empty fields (form: ;;) */ + if (!strlen($field)) { + return $field; + } + + if ($quote && $field[0] == $quote && + $field[strlen($field) - 1] == $quote) { + /* Normalize only for BC. */ + if ($crlf) { + $field = str_replace($crlf, "\n", $field); + } + return substr($field, 1, -1); + } + + return $field; + } + + /** + * Sets or gets the current line being parsed. + * + * @param integer $line If specified, the current line. + * + * @return integer The current line. + */ + static protected function _line($line = null) + { + static $current_line = 0; + + if (!is_null($line)) { + $current_line = $line; + } + + return $current_line; + } + + /** + * Adds a warning to or retrieves and resets the warning stack. + * + * @param string A warning string. If not specified, the existing + * warnings will be returned instead and the warning stack + * gets emptied. + * + * @return array If no parameter has been specified, the list of existing + * warnings. + */ + static public function warning($warning = null) + { + static $warnings = array(); + + if (is_null($warning)) { + $return = $warnings; + $warnings = array(); + return $return; + } + + $warnings[] = $warning; + } + + /** + * Returns or creates the file descriptor associated with a file. + * + * @param string $file The name of the file + * @param array $conf The configuration + * @param string $mode The open mode. self::MODE_READ or + * self::MODE_WRITE. + * + * @return resource The file resource. + * + * @throws Horde_File_Csv_Exception + */ + static public function getPointer($file, &$conf, $mode = self::MODE_READ) + { + static $resources = array(); + static $config = array(); + + if (isset($resources[$file])) { + $conf = $config[$file]; + return $resources[$file]; + } + + if (!is_array($conf)) { + throw new Horde_File_Csv_Exception('Invalid configuration.'); + } + + if (!isset($conf['fields']) || !is_numeric($conf['fields'])) { + throw new Horde_File_Csv_Exception('The number of fields must be numeric.'); + } + + if (isset($conf['sep'])) { + if (strlen($conf['sep']) != 1) { + throw new Horde_File_Csv_Exception('The separator must be one single character.'); + } + } elseif ($conf['fields'] > 1) { + throw new Horde_File_Csv_Exception('No separator specified.'); + } + + if (!empty($conf['quote'])) { + if (strlen($conf['quote']) != 1) { + throw new Horde_File_Csv_Exception('The quote character must be one single character.'); + } + } else { + $conf['quote'] = ''; + } + + if (!isset($conf['crlf'])) { + $conf['crlf'] = "\n"; + } + + $config[$file] = $conf; + + $fp = @fopen($file, $mode); + if (!is_resource($fp)) { + throw new Horde_File_Csv_Exception(sprintf('Cannot open file "%s".', $file)); + } + $resources[$file] = $fp; + self::_line(0); + + if (($mode == self::MODE_READ) && !empty($conf['header'])) { + self::read($file, $conf); + } + + return $fp; + } + +} diff --git a/framework/File_Csv/lib/Horde/File/Csv/Exception.php b/framework/File_Csv/lib/Horde/File/Csv/Exception.php new file mode 100644 index 000000000..ed5bb5be1 --- /dev/null +++ b/framework/File_Csv/lib/Horde/File/Csv/Exception.php @@ -0,0 +1,20 @@ + + * @category Horde + * @package Horde_File_Csv + */ +class Horde_File_Csv_Exception extends Horde_Exception +{ +} diff --git a/framework/File_Csv/package.xml b/framework/File_Csv/package.xml index 5c0650695..6abe60b79 100644 --- a/framework/File_Csv/package.xml +++ b/framework/File_Csv/package.xml @@ -31,10 +31,12 @@ is a fork of the File_CSV class of PEAR's File package. - - - - + + + + + + @@ -82,8 +84,8 @@ is a fork of the File_CSV class of PEAR's File package. - - + +