thephpleague /
csv
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 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 function array_reduce; |
||
| 17 | use function implode; |
||
| 18 | use function preg_match; |
||
| 19 | use function preg_quote; |
||
| 20 | use function str_replace; |
||
| 21 | use function strlen; |
||
| 22 | use const PHP_VERSION_ID; |
||
| 23 | use const SEEK_CUR; |
||
| 24 | use const STREAM_FILTER_WRITE; |
||
| 25 | |||
| 26 | /** |
||
| 27 | * A class to insert records into a CSV Document. |
||
| 28 | */ |
||
| 29 | class Writer extends AbstractCsv |
||
| 30 | { |
||
| 31 | /** |
||
| 32 | * callable collection to format the record before insertion. |
||
| 33 | * |
||
| 34 | * @var callable[] |
||
| 35 | */ |
||
| 36 | protected $formatters = []; |
||
| 37 | |||
| 38 | /** |
||
| 39 | * callable collection to validate the record before insertion. |
||
| 40 | * |
||
| 41 | * @var callable[] |
||
| 42 | */ |
||
| 43 | protected $validators = []; |
||
| 44 | |||
| 45 | /** |
||
| 46 | * newline character. |
||
| 47 | * |
||
| 48 | * @var string |
||
| 49 | */ |
||
| 50 | protected $newline = "\n"; |
||
| 51 | |||
| 52 | /** |
||
| 53 | * Insert records count for flushing. |
||
| 54 | * |
||
| 55 | * @var int |
||
| 56 | */ |
||
| 57 | protected $flush_counter = 0; |
||
| 58 | |||
| 59 | /** |
||
| 60 | * Buffer flush threshold. |
||
| 61 | * |
||
| 62 | * @var int|null |
||
| 63 | */ |
||
| 64 | protected $flush_threshold; |
||
| 65 | |||
| 66 | /** |
||
| 67 | * {@inheritdoc} |
||
| 68 | */ |
||
| 69 | protected $stream_filter_mode = STREAM_FILTER_WRITE; |
||
| 70 | |||
| 71 | /** |
||
| 72 | * Regular expression used to detect if RFC4180 formatting is necessary. |
||
| 73 | * |
||
| 74 | * @var string |
||
| 75 | */ |
||
| 76 | protected $rfc4180_regexp; |
||
| 77 | |||
| 78 | /** |
||
| 79 | * double enclosure for RFC4180 compliance. |
||
| 80 | * |
||
| 81 | * @var string |
||
| 82 | */ |
||
| 83 | protected $rfc4180_enclosure; |
||
| 84 | |||
| 85 | /** |
||
| 86 | * {@inheritdoc} |
||
| 87 | */ |
||
| 88 | 15 | protected function resetProperties(): void |
|
| 89 | { |
||
| 90 | 15 | parent::resetProperties(); |
|
| 91 | 15 | $characters = preg_quote($this->delimiter, '/').'|'.preg_quote($this->enclosure, '/'); |
|
| 92 | 15 | $this->rfc4180_regexp = '/[\s|'.$characters.']/x'; |
|
| 93 | 15 | $this->rfc4180_enclosure = $this->enclosure.$this->enclosure; |
|
| 94 | 15 | } |
|
| 95 | |||
| 96 | /** |
||
| 97 | * Returns the current newline sequence characters. |
||
| 98 | */ |
||
| 99 | 3 | public function getNewline(): string |
|
| 100 | { |
||
| 101 | 3 | return $this->newline; |
|
| 102 | } |
||
| 103 | |||
| 104 | /** |
||
| 105 | * Get the flush threshold. |
||
| 106 | * |
||
| 107 | * @return int|null |
||
| 108 | */ |
||
| 109 | 3 | public function getFlushThreshold() |
|
| 110 | { |
||
| 111 | 3 | return $this->flush_threshold; |
|
| 112 | } |
||
| 113 | |||
| 114 | /** |
||
| 115 | * Adds multiple records to the CSV document. |
||
| 116 | * |
||
| 117 | * @see Writer::insertOne |
||
| 118 | */ |
||
| 119 | 6 | public function insertAll(iterable $records): int |
|
| 120 | { |
||
| 121 | 6 | $bytes = 0; |
|
| 122 | 6 | foreach ($records as $record) { |
|
| 123 | 6 | $bytes += $this->insertOne($record); |
|
| 124 | } |
||
| 125 | |||
| 126 | 6 | $this->flush_counter = 0; |
|
| 127 | 6 | $this->document->fflush(); |
|
| 128 | |||
| 129 | 6 | return $bytes; |
|
| 130 | } |
||
| 131 | |||
| 132 | /** |
||
| 133 | * Adds a single record to a CSV document. |
||
| 134 | * |
||
| 135 | * A record is an array that can contains scalar types values, NULL values |
||
| 136 | * or objects implementing the __toString method. |
||
| 137 | * |
||
| 138 | * @throws CannotInsertRecord If the record can not be inserted |
||
| 139 | */ |
||
| 140 | 57 | public function insertOne(array $record): int |
|
| 141 | { |
||
| 142 | 57 | $method = 'addRecord'; |
|
| 143 | 57 | if (70400 > PHP_VERSION_ID && '' === $this->escape) { |
|
| 144 | 20 | $method = 'addRFC4180CompliantRecord'; |
|
| 145 | } |
||
| 146 | |||
| 147 | 57 | $record = array_reduce($this->formatters, [$this, 'formatRecord'], $record); |
|
| 148 | 57 | $this->validateRecord($record); |
|
| 149 | 54 | $bytes = $this->$method($record); |
|
| 150 | 52 | if (false === $bytes || 0 >= $bytes) { |
|
| 151 | 4 | throw CannotInsertRecord::triggerOnInsertion($record); |
|
| 152 | } |
||
| 153 | |||
| 154 | 48 | return $bytes + $this->consolidate(); |
|
| 155 | } |
||
| 156 | |||
| 157 | /** |
||
| 158 | * Adds a single record to a CSV Document using PHP algorithm. |
||
| 159 | * |
||
| 160 | * @see https://php.net/manual/en/function.fputcsv.php |
||
| 161 | * |
||
| 162 | * @return int|false |
||
| 163 | */ |
||
| 164 | 22 | protected function addRecord(array $record) |
|
| 165 | { |
||
| 166 | 22 | return $this->document->fputcsv($record, $this->delimiter, $this->enclosure, $this->escape); |
|
| 167 | } |
||
| 168 | |||
| 169 | /** |
||
| 170 | * Adds a single record to a CSV Document using RFC4180 algorithm. |
||
| 171 | * |
||
| 172 | * @see https://php.net/manual/en/function.fputcsv.php |
||
| 173 | * @see https://php.net/manual/en/function.fwrite.php |
||
| 174 | * @see https://tools.ietf.org/html/rfc4180 |
||
| 175 | * @see http://edoceo.com/utilitas/csv-file-format |
||
| 176 | * |
||
| 177 | * String conversion is done without any check like fputcsv. |
||
| 178 | * |
||
| 179 | * - Emits E_NOTICE on Array conversion (returns the 'Array' string) |
||
| 180 | * - Throws catchable fatal error on objects that can not be converted |
||
| 181 | * - Returns resource id without notice or error (returns 'Resource id #2') |
||
| 182 | * - Converts boolean true to '1', boolean false to the empty string |
||
| 183 | * - Converts null value to the empty string |
||
| 184 | * |
||
| 185 | * Fields must be delimited with enclosures if they contains : |
||
| 186 | * |
||
| 187 | * - Embedded whitespaces |
||
| 188 | * - Embedded delimiters |
||
| 189 | * - Embedded line-breaks |
||
| 190 | * - Embedded enclosures. |
||
| 191 | * |
||
| 192 | * Embedded enclosures must be doubled. |
||
| 193 | * |
||
| 194 | * The LF character is added at the end of each record to mimic fputcsv behavior |
||
| 195 | * |
||
| 196 | * @return int|false |
||
| 197 | */ |
||
| 198 | 20 | protected function addRFC4180CompliantRecord(array $record) |
|
| 199 | { |
||
| 200 | 20 | foreach ($record as &$field) { |
|
| 201 | 20 | $field = (string) $field; |
|
| 202 | 20 | if (1 === preg_match($this->rfc4180_regexp, $field)) { |
|
| 203 | 20 | $field = $this->enclosure.str_replace($this->enclosure, $this->rfc4180_enclosure, $field).$this->enclosure; |
|
| 204 | } |
||
| 205 | } |
||
| 206 | 20 | unset($field); |
|
| 207 | |||
| 208 | 20 | return $this->document->fwrite(implode($this->delimiter, $record)."\n"); |
|
| 209 | } |
||
| 210 | |||
| 211 | /** |
||
| 212 | * Format a record. |
||
| 213 | * |
||
| 214 | * The returned array must contain |
||
| 215 | * - scalar types values, |
||
| 216 | * - NULL values, |
||
| 217 | * - or objects implementing the __toString() method. |
||
| 218 | */ |
||
| 219 | 3 | protected function formatRecord(array $record, callable $formatter): array |
|
| 220 | { |
||
| 221 | 3 | return $formatter($record); |
|
| 222 | } |
||
| 223 | |||
| 224 | /** |
||
| 225 | * Validate a record. |
||
| 226 | * |
||
| 227 | * @throws CannotInsertRecord If the validation failed |
||
| 228 | */ |
||
| 229 | 12 | protected function validateRecord(array $record): void |
|
| 230 | { |
||
| 231 | 12 | foreach ($this->validators as $name => $validator) { |
|
| 232 | 3 | if (true !== $validator($record)) { |
|
| 233 | 3 | throw CannotInsertRecord::triggerOnValidation($name, $record); |
|
| 234 | } |
||
| 235 | } |
||
| 236 | 9 | } |
|
| 237 | |||
| 238 | /** |
||
| 239 | * Apply post insertion actions. |
||
| 240 | */ |
||
| 241 | 12 | protected function consolidate(): int |
|
| 242 | { |
||
| 243 | 12 | $bytes = 0; |
|
| 244 | 12 | if ("\n" !== $this->newline) { |
|
| 245 | 3 | $this->document->fseek(-1, SEEK_CUR); |
|
| 246 | /** @var int $newlineBytes */ |
||
| 247 | 3 | $newlineBytes = $this->document->fwrite($this->newline, strlen($this->newline)); |
|
| 248 | 3 | $bytes = $newlineBytes - 1; |
|
| 249 | } |
||
| 250 | |||
| 251 | 12 | if (null === $this->flush_threshold) { |
|
| 252 | 9 | return $bytes; |
|
| 253 | } |
||
| 254 | |||
| 255 | 3 | ++$this->flush_counter; |
|
| 256 | 3 | if (0 === $this->flush_counter % $this->flush_threshold) { |
|
| 257 | 3 | $this->flush_counter = 0; |
|
| 258 | 3 | $this->document->fflush(); |
|
| 259 | } |
||
| 260 | |||
| 261 | 3 | return $bytes; |
|
| 262 | } |
||
| 263 | |||
| 264 | /** |
||
| 265 | * Adds a record formatter. |
||
| 266 | */ |
||
| 267 | 3 | public function addFormatter(callable $formatter): self |
|
| 268 | { |
||
| 269 | 3 | $this->formatters[] = $formatter; |
|
| 270 | |||
| 271 | 3 | return $this; |
|
| 272 | } |
||
| 273 | |||
| 274 | /** |
||
| 275 | * Adds a record validator. |
||
| 276 | */ |
||
| 277 | 3 | public function addValidator(callable $validator, string $validator_name): self |
|
| 278 | { |
||
| 279 | 3 | $this->validators[$validator_name] = $validator; |
|
| 280 | |||
| 281 | 3 | return $this; |
|
| 282 | } |
||
| 283 | |||
| 284 | /** |
||
| 285 | * Sets the newline sequence. |
||
| 286 | */ |
||
| 287 | 3 | public function setNewline(string $newline): self |
|
| 288 | { |
||
| 289 | 3 | $this->newline = $newline; |
|
| 290 | |||
| 291 | 3 | return $this; |
|
| 292 | } |
||
| 293 | |||
| 294 | /** |
||
| 295 | * Set the flush threshold. |
||
| 296 | * |
||
| 297 | * |
||
| 298 | * @param ?int $threshold |
||
|
0 ignored issues
–
show
|
|||
| 299 | * @throws Exception if the threshold is a integer lesser than 1 |
||
| 300 | */ |
||
| 301 | 9 | public function setFlushThreshold(?int $threshold): self |
|
| 302 | { |
||
| 303 | 9 | if ($threshold === $this->flush_threshold) { |
|
| 304 | 3 | return $this; |
|
| 305 | } |
||
| 306 | |||
| 307 | 9 | if (null !== $threshold && 1 > $threshold) { |
|
| 308 | 3 | throw new InvalidArgument(__METHOD__.'() expects 1 Argument to be null or a valid integer greater or equal to 1'); |
|
| 309 | } |
||
| 310 | |||
| 311 | 9 | $this->flush_threshold = $threshold; |
|
| 312 | 9 | $this->flush_counter = 0; |
|
| 313 | 9 | $this->document->fflush(); |
|
| 314 | |||
| 315 | 9 | return $this; |
|
| 316 | } |
||
| 317 | } |
||
| 318 |
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.