EscapeFormula   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 137
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 137
ccs 30
cts 30
cp 1
rs 10
c 0
b 0
f 0
wmc 15

8 Methods

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