Passed
Push — master ( 261986...7fbe48 )
by
unknown
13:53
created

CsvUtility::prefixControlLiterals()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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