Completed
Push — master ( aa23c0...364b30 )
by ignace nyamagana
05:47 queued 02:02
created

EscapeFormula::escapeRecord()   A

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