1<?php 2namespace TYPO3\CMS\Core\Utility; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use TYPO3\CMS\Core\IO\CsvStreamFilter; 18 19/** 20 * Class with helper functions for CSV handling 21 */ 22class CsvUtility 23{ 24 /** 25 * whether to passthrough data as is, without any modification 26 */ 27 public const TYPE_PASSTHROUGH = 0; 28 29 /** 30 * whether to remove control characters like `=`, `+`, ... 31 */ 32 public const TYPE_REMOVE_CONTROLS = 1; 33 34 /** 35 * whether to prefix control characters like `=`, `+`, ... 36 * to become `'=`, `'+`, ... 37 */ 38 public const TYPE_PREFIX_CONTROLS = 2; 39 40 /** 41 * Convert a string, formatted as CSV, into an multidimensional array 42 * 43 * This cannot be done by str_getcsv, since it's impossible to handle enclosed cells with a line feed in it 44 * 45 * @param string $input The CSV input 46 * @param string $fieldDelimiter The field delimiter 47 * @param string $fieldEnclosure The field enclosure 48 * @param int $maximumColumns The maximum amount of columns 49 * @return array 50 */ 51 public static function csvToArray($input, $fieldDelimiter = ',', $fieldEnclosure = '"', $maximumColumns = 0) 52 { 53 $multiArray = []; 54 $maximumCellCount = 0; 55 56 if (($handle = fopen('php://memory', 'r+')) !== false) { 57 fwrite($handle, $input); 58 rewind($handle); 59 while (($cells = fgetcsv($handle, 0, $fieldDelimiter, $fieldEnclosure)) !== false) { 60 $maximumCellCount = max(count($cells), $maximumCellCount); 61 $multiArray[] = preg_replace('|<br */?>|i', LF, $cells); 62 } 63 fclose($handle); 64 } 65 66 if ($maximumColumns > $maximumCellCount) { 67 $maximumCellCount = $maximumColumns; 68 } 69 70 foreach ($multiArray as &$row) { 71 for ($key = 0; $key < $maximumCellCount; $key++) { 72 if ( 73 $maximumColumns > 0 74 && $maximumColumns < $maximumCellCount 75 && $key >= $maximumColumns 76 ) { 77 if (isset($row[$key])) { 78 unset($row[$key]); 79 } 80 } elseif (!isset($row[$key])) { 81 $row[$key] = ''; 82 } 83 } 84 } 85 86 return $multiArray; 87 } 88 89 /** 90 * Takes a row and returns a CSV string of the values with $delim (default is ,) and $quote (default is ") as separator chars. 91 * 92 * @param string[] $row Input array of values 93 * @param string $delim Delimited, default is comma 94 * @param string $quote Quote-character to wrap around the values. 95 * @param int $type Output behaviour concerning potentially harmful control literals 96 * @return string A single line of CSV 97 */ 98 public static function csvValues(array $row, string $delim = ',', string $quote = '"', int $type = self::TYPE_REMOVE_CONTROLS) 99 { 100 $resource = fopen('php://temp', 'w'); 101 if (!is_resource($resource)) { 102 throw new \RuntimeException('Cannot open temporary data stream for writing', 1625556521); 103 } 104 $modifier = CsvStreamFilter::applyStreamFilter($resource, false); 105 array_map([self::class, 'assertCellValueType'], $row); 106 if ($type === self::TYPE_REMOVE_CONTROLS) { 107 $row = array_map([self::class, 'removeControlLiterals'], $row); 108 } elseif ($type === self::TYPE_PREFIX_CONTROLS) { 109 $row = array_map([self::class, 'prefixControlLiterals'], $row); 110 } 111 fputcsv($resource, $modifier($row), $delim, $quote); 112 fseek($resource, 0); 113 $content = stream_get_contents($resource); 114 return $content; 115 } 116 117 /** 118 * Prefixes control literals at the beginning of a cell value with a single quote 119 * (e.g. `=+value` --> `'=+value`) 120 * 121 * @param mixed $cellValue 122 * @return bool|int|float|string|null 123 */ 124 protected static function prefixControlLiterals($cellValue) 125 { 126 if (!self::shallFilterValue($cellValue)) { 127 return $cellValue; 128 } 129 $cellValue = (string)$cellValue; 130 return preg_replace('#^([\t\v=+*%/@-])#', '\'${1}', $cellValue); 131 } 132 133 /** 134 * Removes control literals from the beginning of a cell value 135 * (e.g. `=+value` --> `value`) 136 * 137 * @param mixed $cellValue 138 * @return bool|int|float|string|null 139 */ 140 protected static function removeControlLiterals($cellValue) 141 { 142 if (!self::shallFilterValue($cellValue)) { 143 return $cellValue; 144 } 145 $cellValue = (string)$cellValue; 146 return preg_replace('#^([\t\v=+*%/@-]+)+#', '', $cellValue); 147 } 148 149 /** 150 * Asserts scalar or null types for given cell value. 151 * 152 * @param mixed $cellValue 153 */ 154 protected static function assertCellValueType($cellValue): void 155 { 156 // int, float, string, bool, null 157 if ($cellValue === null || is_scalar($cellValue)) { 158 return; 159 } 160 throw new \RuntimeException( 161 sprintf('Unexpected type %s for cell value', gettype($cellValue)), 162 1625562833 163 ); 164 } 165 166 /** 167 * Whether cell value shall be filtered, applies to everything 168 * that is not or cannot be represented as boolean, integer or float. 169 * 170 * @param mixed $cellValue 171 * @return bool 172 */ 173 protected static function shallFilterValue($cellValue): bool 174 { 175 return $cellValue !== null 176 && !is_bool($cellValue) 177 && !is_numeric($cellValue) 178 && !MathUtility::canBeInterpretedAsInteger($cellValue) 179 && !MathUtility::canBeInterpretedAsFloat($cellValue); 180 } 181} 182