EscapeFormula::__invoke()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Csv (https://csv.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Csv;
15
16
use InvalidArgumentException;
17
use function array_fill_keys;
18
use function array_keys;
19
use function array_map;
20
use function array_merge;
21
use function array_unique;
22
use function is_string;
23
use function method_exists;
24
use function sprintf;
25
26
/**
27
 * A Formatter to tackle CSV Formula Injection.
28
 *
29
 * @see http://georgemauer.net/2017/10/07/csv-injection.html
30
 */
31
class EscapeFormula
32
{
33
    /**
34
     * Spreadsheet formula starting character.
35
     */
36
    const FORMULA_STARTING_CHARS = ['=', '-', '+', '@'];
37
38
    /**
39
     * Effective Spreadsheet formula starting characters.
40
     *
41
     * @var array
42
     */
43
    protected $special_chars = [];
44
45
    /**
46
     * Escape character to escape each CSV formula field.
47
     *
48
     * @var string
49
     */
50
    protected $escape;
51
52
    /**
53
     * New instance.
54
     *
55
     * @param string   $escape        escape character to escape each CSV formula field
56
     * @param string[] $special_chars additional spreadsheet formula starting characters
57
     *
58
     */
59 12
    public function __construct(string $escape = "\t", array $special_chars = [])
60
    {
61 12
        $this->escape = $escape;
62 12
        if ([] !== $special_chars) {
63 9
            $special_chars = $this->filterSpecialCharacters(...$special_chars);
64
        }
65
66 6
        $chars = array_merge(self::FORMULA_STARTING_CHARS, $special_chars);
67 6
        $chars = array_unique($chars);
68 6
        $this->special_chars = array_fill_keys($chars, 1);
69 6
    }
70
71
    /**
72
     * Filter submitted special characters.
73
     *
74
     * @param string ...$characters
75
     *
76
     * @throws InvalidArgumentException if the string is not a single character
77
     *
78
     * @return string[]
79
     */
80 6
    protected function filterSpecialCharacters(string ...$characters): array
81
    {
82 6
        foreach ($characters as $str) {
83 6
            if (1 != strlen($str)) {
84 3
                throw new InvalidArgumentException(sprintf('The submitted string %s must be a single character', $str));
85
            }
86
        }
87
88 3
        return $characters;
89
    }
90
91
    /**
92
     * Returns the list of character the instance will escape.
93
     *
94
     * @return string[]
95
     */
96 3
    public function getSpecialCharacters(): array
97
    {
98 3
        return array_keys($this->special_chars);
99
    }
100
101
    /**
102
     * Returns the escape character.
103
     */
104 3
    public function getEscape(): string
105
    {
106 3
        return $this->escape;
107
    }
108
109
    /**
110
     * League CSV formatter hook.
111
     *
112
     * @see escapeRecord
113
     */
114 3
    public function __invoke(array $record): array
115
    {
116 3
        return $this->escapeRecord($record);
117
    }
118
119
    /**
120
     * Escape a CSV record.
121
     */
122 6
    public function escapeRecord(array $record): array
123
    {
124 6
        return array_map([$this, 'escapeField'], $record);
125
    }
126
127
    /**
128
     * Escape a CSV cell if its content is stringable.
129
     *
130
     * @param mixed $cell the content of the cell
131
     *
132
     * @return mixed|string the escaped content
133
     */
134 6
    protected function escapeField($cell)
135
    {
136 6
        if (!$this->isStringable($cell)) {
137 6
            return $cell;
138
        }
139
140 6
        $str_cell = (string) $cell;
141 6
        if (isset($str_cell[0], $this->special_chars[$str_cell[0]])) {
142 6
            return $this->escape.$str_cell;
143
        }
144
145 6
        return $cell;
146
    }
147
148
    /**
149
     * Tells whether the submitted value is stringable.
150
     *
151
     * @param string|object $value
152
     */
153 6
    protected function isStringable($value): bool
154
    {
155 6
        return is_string($value) || method_exists($value, '__toString');
156
    }
157
}
158