PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.21
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.21
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / helpers / FrmCSVExportHelper.php

FrmCSVExportHelper.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.21, at classes/helpers/FrmCSVExportHelper.php

793 lines 22.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmCSVExportHelper {
7
8 /**
9 * @var string
10 */
11 protected static $separator = ', ';
12
13 /**
14 * @var string
15 */
16 protected static $column_separator = ',';
17
18 /**
19 * @var string
20 */
21 protected static $line_break = 'return';
22
23 /**
24 * @var string
25 */
26 protected static $charset = 'UTF-8';
27
28 /**
29 * @var string
30 */
31 protected static $to_encoding = 'UTF-8';
32
33 /**
34 * @var string
35 */
36 protected static $wp_date_format = 'Y-m-d H:i:s';
37
38 /**
39 * @var int
40 */
41 protected static $comment_count = 0;
42
43 /**
44 * @var int
45 */
46 protected static $form_id = 0;
47
48 /**
49 * @var array
50 */
51 protected static $headings = array();
52
53 /**
54 * @var array
55 */
56 protected static $fields = array();
57
58 /**
59 * @var stdClass|null
60 */
61 protected static $entry;
62
63 /**
64 * @var bool|null
65 */
66 protected static $has_parent_id;
67
68 /**
69 * @var array|null
70 */
71 protected static $fields_by_repeater_id;
72
73 /**
74 * @var string either 'echo' or 'file' are supported.
75 */
76 protected static $mode = 'echo';
77
78 /**
79 * @var resource|null used to write a CSV file in file mode.
80 */
81 protected static $fp;
82
83 /**
84 * @var string the context of the CSV being generated. Possible values include 'email' when used as an email attachment, or 'default'.
85 */
86 protected static $context = 'default';
87
88 /**
89 * @var array
90 */
91 protected static $meta = array();
92
93 /**
94 * Whether to include the BOM (Byte Order Mark) in the CSV file.
95 * Only applicable for UTF-8 exports.
96 *
97 * @since 6.17
98 *
99 * @var bool
100 */
101 private static $include_bom = false;
102
103 /**
104 * Get all options for the CSV export format dropdown.
105 *
106 * @since 6.17 The UTF-8 with BOM option was added.
107 *
108 * @return array
109 */
110 public static function csv_format_options() {
111 $formats = array( 'UTF-8', 'ISO-8859-1', 'windows-1256', 'windows-1251', 'macintosh' );
112
113 // Do not add the UTF-8 with BOM option if this function is called on Global Settings.
114 // This is to improve compatibility with the Export View as CSV add-on (v1.10).
115 // Otherwise, the option will appear twice since it is added in the add-on as well.
116 $on_global_settings_page = 'formidable-settings' === FrmAppHelper::get_param( 'page' );
117 if ( ! $on_global_settings_page ) {
118 array_splice( $formats, 1, 0, 'UTF-8 with BOM' );
119 }
120
121 $formats = apply_filters( 'frm_csv_format_options', $formats );
122
123 return $formats;
124 }
125
126 /**
127 * @param array $atts
128 * @return false|string|null returns a string file path or false if $atts['mode'] is set to 'file'.
129 */
130 public static function generate_csv( $atts ) {
131 global $frm_vars;
132 $frm_vars['prevent_caching'] = true;
133
134 self::$fields = $atts['form_cols'];
135 self::$form_id = $atts['form']->id;
136 self::$mode = ! empty( $atts['mode'] ) && 'file' === $atts['mode'] ? 'file' : 'echo';
137 self::$context = ! empty( $atts['context'] ) ? $atts['context'] : 'default';
138 self::$meta = ! empty( $atts['meta'] ) ? $atts['meta'] : array();
139
140 self::set_class_parameters();
141 self::set_has_parent_id( $atts['form'] );
142
143 $filename = self::generate_csv_filename( $atts['form'] );
144
145 if ( 'file' === self::$mode ) {
146 $filepath = get_temp_dir() . $filename;
147 self::$fp = @fopen( $filepath, 'w' );
148 if ( ! self::$fp ) {
149 return false;
150 }
151 } elseif ( 'echo' === self::$mode ) {
152 self::print_file_headers( $filename );
153 }
154
155 unset( $filename );
156
157 $comment_count = FrmDb::get_count(
158 'frm_item_metas',
159 array(
160 'item_id' => $atts['entry_ids'],
161 'field_id' => 0,
162 'meta_value like' => '{',
163 ),
164 array(
165 'group_by' => 'item_id',
166 'order_by' => 'count(*) DESC',
167 'limit' => 1,
168 )
169 );
170 self::$comment_count = $comment_count;
171
172 self::prepare_csv_headings();
173
174 /**
175 * For a target form with 50k entries (Locations List, with 7 fields), batch sizes of 100
176 * were significantly faster and used less memory, so a filter was added.
177 *
178 * @since 6.17
179 *
180 * @param int $batch_size
181 * @param int $form_id
182 */
183 $csv_export_batch_size = (int) apply_filters( 'frm_csv_export_batch_size', 20, self::$form_id );
184
185 // Fetch posts in batches rather than loading the entire table into memory.
186 while ( $next_set = array_splice( $atts['entry_ids'], 0, $csv_export_batch_size ) ) {
187 self::prepare_next_csv_rows( $next_set );
188 }
189
190 self::after_generate_csv( $atts );
191
192 unset( $atts['form'], $atts['form_cols'] );
193
194 if ( 'file' === self::$mode ) {
195 fclose( self::$fp );
196 return $filepath;
197 }
198
199 return null;
200 }
201
202 /**
203 * @since 6.8.4
204 *
205 * @param array $atts
206 * @return void
207 */
208 private static function after_generate_csv( $atts ) {
209 /**
210 * @since 6.8.4
211 *
212 * @param array $atts {
213 * @type object $form
214 * @type array $entry_ids
215 * @type array $form_cols
216 * }
217 */
218 do_action( 'frm_after_generate_csv', $atts );
219 }
220
221 /**
222 * @since 5.0.16
223 *
224 * @param stdClass $form
225 * @return string
226 */
227 private static function generate_csv_filename( $form ) {
228 $filename = gmdate( 'ymdHis', time() ) . '_' . sanitize_title_with_dashes( $form->name ) . '_formidable_entries.csv';
229 return apply_filters( 'frm_csv_filename', $filename, $form, self::get_standard_filter_args() );
230 }
231
232 /**
233 * @since 5.0.16
234 *
235 * @return array
236 */
237 private static function get_standard_filter_args() {
238 return array(
239 'context' => self::$context,
240 'meta' => self::$meta,
241 );
242 }
243
244 private static function set_class_parameters() {
245 $args = self::get_standard_filter_args();
246 self::$separator = apply_filters( 'frm_csv_sep', self::$separator, $args );
247 self::$line_break = apply_filters( 'frm_csv_line_break', self::$line_break, $args );
248 self::$wp_date_format = apply_filters( 'frm_csv_date_format', self::$wp_date_format, $args );
249 self::get_csv_format();
250 self::$charset = get_option( 'blog_charset' );
251
252 $col_sep = ! empty( $_POST['csv_col_sep'] ) ? sanitize_text_field( wp_unslash( $_POST['csv_col_sep'] ) ) : self::$column_separator; // phpcs:ignore WordPress.Security.NonceVerification.Missing
253
254 self::$column_separator = apply_filters( 'frm_csv_column_sep', $col_sep, $args );
255 }
256
257 private static function set_has_parent_id( $form ) {
258 self::$has_parent_id = $form->parent_form_id > 0;
259 }
260
261 private static function print_file_headers( $filename ) {
262 header( 'Content-Description: File Transfer' );
263 header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' );
264 header( 'Content-Type: text/csv; charset=' . self::$charset, true );
265 header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', mktime( gmdate( 'H' ) + 2, gmdate( 'i' ), gmdate( 's' ), gmdate( 'm' ), gmdate( 'd' ), gmdate( 'Y' ) ) ) . ' GMT' );
266 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
267 header( 'Cache-Control: no-cache, must-revalidate' );
268 header( 'Pragma: no-cache' );
269
270 if ( self::$include_bom ) {
271 echo esc_html( chr( 239 ) . chr( 187 ) . chr( 191 ) );
272 }
273
274 do_action(
275 'frm_csv_headers',
276 array(
277 'form_id' => self::$form_id,
278 'fields' => self::$fields,
279 )
280 );
281 }
282
283 /**
284 * Check the POST request data for the CSV format to use.
285 *
286 * @return void
287 */
288 public static function get_csv_format() {
289 $csv_format = FrmAppHelper::get_post_param( 'csv_format', 'UTF-8', 'sanitize_text_field' );
290
291 if ( 'UTF-8 with BOM' === $csv_format ) {
292 self::$include_bom = true;
293 $csv_format = 'UTF-8';
294 }
295
296 $csv_format = apply_filters( 'frm_csv_format', $csv_format, self::get_standard_filter_args() );
297 self::$to_encoding = $csv_format;
298 }
299
300 private static function prepare_csv_headings() {
301 $headings = array();
302 self::csv_headings( $headings );
303 $headings = apply_filters(
304 'frm_csv_columns',
305 $headings,
306 self::$form_id,
307 array_merge(
308 self::get_standard_filter_args(),
309 array( 'fields' => self::$fields )
310 )
311 );
312 self::$headings = $headings;
313
314 self::print_csv_row( $headings );
315 }
316
317 private static function field_headings( $col ) {
318 $field_type_obj = FrmFieldFactory::get_field_factory( $col );
319 if ( ! empty( $field_type_obj->is_combo_field ) ) {
320 // This is combo field.
321 return $field_type_obj->get_export_headings();
322 }
323
324 $field_headings = array();
325 $separate_values = array( 'user_id', 'file', 'data', 'date' );
326 if ( ! empty( $col->field_options['separate_value'] ) && ! in_array( $col->type, $separate_values, true ) ) {
327 $field_headings[ $col->id . '_label' ] = strip_tags( $col->name . ' ' . __( '(label)', 'formidable' ) );
328 }
329
330 $field_headings[ $col->id ] = strip_tags( $col->name );
331 $field_headings = apply_filters(
332 'frm_csv_field_columns',
333 $field_headings,
334 array_merge(
335 self::get_standard_filter_args(),
336 array( 'field' => $col )
337 )
338 );
339
340 return $field_headings;
341 }
342
343 private static function csv_headings( &$headings ) {
344 $fields_by_repeater_id = array();
345 $repeater_ids = array();
346
347 foreach ( self::$fields as $col ) {
348 if ( self::is_the_child_of_a_repeater( $col ) ) {
349 $repeater_id = $col->field_options['in_section'];
350 // Set a placeholder to maintain order for repeater fields.
351 $headings[ 'repeater' . $repeater_id ] = array();
352
353 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
354 $fields_by_repeater_id[ $repeater_id ] = array();
355 $repeater_ids[] = $repeater_id;
356 }
357
358 $fields_by_repeater_id[ $repeater_id ][] = $col;
359
360 continue;
361 }
362
363 $headings += self::field_headings( $col );
364 }
365 unset( $repeater_id, $col );
366
367 if ( $repeater_ids ) {
368 $where = array( 'field_id' => $repeater_ids );
369 $repeater_meta = FrmDb::get_results( 'frm_item_metas', $where, 'field_id, meta_value' );
370 $max = array_fill_keys( $repeater_ids, 0 );
371
372 foreach ( $repeater_meta as $row ) {
373 $start = strpos( $row->meta_value, 'a:' ) + 2;
374 $end = strpos( $row->meta_value, ':{' );
375 $length = substr( $row->meta_value, $start, $end - $start );
376
377 if ( $length > $max[ $row->field_id ] ) {
378 $max[ $row->field_id ] = $length;
379 }
380 }
381 unset( $start, $end, $length, $row, $repeater_meta, $where );
382
383 $flat = array();
384 foreach ( $headings as $key => $heading ) {
385 if ( is_array( $heading ) ) {
386 $repeater_id = str_replace( 'repeater', '', $key );
387
388 $repeater_headings = array();
389 foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
390 $repeater_headings += self::field_headings( $col );
391 }
392
393 for ( $i = 0; $i < $max[ $repeater_id ]; $i++ ) {
394 foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
395 $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
396 }
397 }
398 } else {
399 $flat[ $key ] = $heading;
400 }
401 }
402
403 self::$fields_by_repeater_id = $fields_by_repeater_id;
404
405 unset( $key, $heading, $max, $repeater_headings, $repeater_id, $fields_by_repeater_id );
406
407 $headings = $flat;
408 unset( $flat );
409 }//end if
410
411 if ( self::$comment_count ) {
412 for ( $i = 0; $i < self::$comment_count; $i++ ) {
413 $headings[ 'comment' . $i ] = __( 'Comment', 'formidable' );
414 $headings[ 'comment_user_id' . $i ] = __( 'Comment User', 'formidable' );
415 $headings[ 'comment_created_at' . $i ] = __( 'Comment Date', 'formidable' );
416 }
417 unset( $i );
418 }
419
420 $headings['created_at'] = __( 'Timestamp', 'formidable' );
421 $headings['updated_at'] = __( 'Last Updated', 'formidable' );
422 $headings['user_id'] = __( 'Created By', 'formidable' );
423 $headings['updated_by'] = __( 'Updated By', 'formidable' );
424 $headings['is_draft'] = __( 'Entry Status', 'formidable' );
425 $headings['ip'] = __( 'IP', 'formidable' );
426 $headings['id'] = __( 'ID', 'formidable' );
427 $headings['item_key'] = __( 'Key', 'formidable' );
428 if ( self::has_parent_id() ) {
429 $headings['parent_id'] = __( 'Parent ID', 'formidable' );
430 }
431
432 $headings = apply_filters( 'frm_export_csv_headings', $headings );
433 }
434
435 /**
436 * @param object $field
437 * @return bool
438 */
439 private static function is_the_child_of_a_repeater( $field ) {
440 if ( $field->form_id === self::$form_id || empty( $field->field_options['in_section'] ) ) {
441 return false;
442 }
443
444 $section_id = $field->field_options['in_section'];
445 $section = FrmField::getOne( $section_id );
446
447 if ( ! $section ) {
448 return false;
449 }
450
451 return FrmField::is_repeating_field( $section );
452 }
453
454 private static function has_parent_id() {
455 return self::$has_parent_id;
456 }
457
458 private static function prepare_next_csv_rows( $next_set ) {
459 if ( FrmAppHelper::pro_is_installed() ) {
460 $where = array(
461 'or' => 1,
462 'id' => $next_set,
463 'parent_item_id' => $next_set,
464 );
465 // Order by parent_item_id so children will be first.
466 $order_by = ' ORDER BY parent_item_id DESC';
467 } else {
468 // When Pro is not installed, only query for direct ID matches only as we do not expect parent item id
469 // matches and the more simplified query is faster.
470 $where = array(
471 'id' => $next_set,
472 );
473 $order_by = '';
474 }
475
476 $entries = FrmEntry::getAll( $where, $order_by, '', true, false );
477
478 foreach ( $entries as $entry ) {
479 self::$entry = $entry;
480 unset( $entry );
481
482 if ( self::$entry->form_id !== self::$form_id ) {
483 self::add_repeat_field_values_to_csv( $entries );
484 } else {
485 self::prepare_csv_row();
486 }
487 }
488 }
489
490 private static function prepare_csv_row() {
491 $row = array();
492 self::add_field_values_to_csv( $row );
493 self::add_entry_data_to_csv( $row );
494 $row = apply_filters(
495 'frm_csv_row',
496 $row,
497 array(
498 'entry' => self::$entry,
499 'date_format' => self::$wp_date_format,
500 'comment_count' => self::$comment_count,
501 'context' => self::$context,
502 )
503 );
504 self::print_csv_row( $row );
505 }
506
507 private static function add_repeat_field_values_to_csv( &$entries ) {
508 if ( isset( self::$entry->metas ) ) {
509 // add child entries to the parent
510 foreach ( self::$entry->metas as $meta_id => $meta_value ) {
511 if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
512 // if the hook is being used to include field keys in the metas array,
513 // we need to skip the keys and only process field ids
514 continue;
515 }
516
517 if ( ! isset( $entries[ self::$entry->parent_item_id ] ) ) {
518 $entries[ self::$entry->parent_item_id ] = new stdClass();
519 $entries[ self::$entry->parent_item_id ]->metas = array();
520 }
521
522 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
523 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
524 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
525 // if the data is here, it should be an array but if this field has collected data
526 // both while inside and outside of the repeating section, it's possible this is a string.
527 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
528 }
529
530 // Add the repeated values.
531 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
532 }//end foreach
533
534 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
535 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
536 }//end if
537
538 // add the embedded form id
539 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
540 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
541 }
542 $entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
543 }
544
545 /**
546 * When an empty field is saved, it isn't saved as a meta value
547 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
548 *
549 * @param array $metas
550 * @param array $entries
551 * @return array
552 */
553 private static function fill_missing_repeater_metas( $metas, &$entries ) {
554 $field_ids = array_keys( $metas );
555 $field_id = end( $field_ids );
556 $field = self::get_field( $field_id );
557
558 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
559 return $metas;
560 }
561
562 $repeater_id = $field->field_options['in_section'];
563 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
564 return $metas;
565 }
566
567 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
568 if ( ! isset( $metas[ $repeater_child->id ] ) ) {
569 $metas[ $repeater_child->id ] = '';
570
571 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) || ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) ) {
572 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] = array();
573 }
574
575 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
576 }
577 }
578
579 return $metas;
580 }
581
582 private static function get_field( $field_id ) {
583 $field_id = (int) $field_id;
584 foreach ( self::$fields as $field ) {
585 if ( $field_id === (int) $field->id ) {
586 return $field;
587 }
588 }
589 return false;
590 }
591
592 private static function add_field_values_to_csv( &$row ) {
593 foreach ( self::$fields as $col ) {
594 $field_value = isset( self::$entry->metas[ $col->id ] ) ? self::$entry->metas[ $col->id ] : false;
595
596 FrmFieldsHelper::prepare_field_value( $field_value, $col->type );
597 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
598
599 $field_value = apply_filters(
600 'frm_csv_value',
601 $field_value,
602 array(
603 'field' => $col,
604 'entry' => self::$entry,
605 'separator' => self::$separator,
606 'context' => self::$context,
607 )
608 );
609
610 if ( ! empty( $col->field_options['separate_value'] ) ) {
611 $label_key = $col->id . '_label';
612 if ( self::is_the_child_of_a_repeater( $col ) ) {
613 $row[ $label_key ] = array();
614
615 if ( is_array( $field_value ) ) {
616 foreach ( $field_value as $value ) {
617 $row[ $label_key ][] = self::get_separate_value_label( $value, $col );
618 }
619 }
620 } else {
621 $row[ $label_key ] = self::get_separate_value_label( $field_value, $col );
622 }
623 unset( $label_key );
624 }
625
626 $row[ $col->id ] = $field_value;
627
628 unset( $col, $field_value );
629 }//end foreach
630 }
631
632 /**
633 * @since 5.0.06
634 *
635 * @param mixed $field_value
636 * @param stdClass $field
637 * @return string
638 */
639 private static function get_separate_value_label( $field_value, $field ) {
640 return FrmEntriesHelper::display_value(
641 $field_value,
642 $field,
643 array(
644 'type' => $field->type,
645 'post_id' => self::$entry->post_id,
646 'show_icon' => false,
647 'entry_id' => self::$entry->id,
648 'sep' => self::$separator,
649 'embedded_field_id' => isset( self::$entry->embedded_fields ) && isset( self::$entry->embedded_fields[ self::$entry->id ] ) ? 'form' . self::$entry->embedded_fields[ self::$entry->id ] : 0,
650 )
651 );
652 }
653
654 /**
655 * @since 2.0.23
656 */
657 private static function add_array_values_to_columns( &$row, $atts ) {
658 if ( is_array( $atts['field_value'] ) ) {
659 foreach ( $atts['field_value'] as $key => $sub_value ) {
660 if ( is_array( $sub_value ) ) {
661 // This is combo field inside repeater. The heading key has this format: [86_first[0]].
662 foreach ( $sub_value as $sub_key => $sub_sub_value ) {
663 $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
664 if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
665 $row[ $column_key ] = $sub_sub_value;
666 }
667 }
668
669 continue;
670 }
671
672 $column_key = $atts['col']->id . '_' . $key;
673 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
674 $row[ $column_key ] = $sub_value;
675 }
676 }
677 }
678 }
679
680 private static function add_entry_data_to_csv( &$row ) {
681 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
682 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
683 $row['user_id'] = self::$entry->user_id;
684 $row['updated_by'] = self::$entry->updated_by;
685 $row['is_draft'] = self::$entry->is_draft;
686 $row['ip'] = self::$entry->ip;
687 $row['id'] = self::$entry->id;
688 $row['item_key'] = self::$entry->item_key;
689 if ( self::has_parent_id() ) {
690 $row['parent_id'] = self::$entry->parent_item_id;
691 }
692 }
693
694 private static function print_csv_row( $rows ) {
695 $sep = '';
696 $echo = 'echo' === self::$mode;
697
698 foreach ( self::$headings as $k => $heading ) {
699 if ( isset( $rows[ $k ] ) ) {
700 $row = $rows[ $k ];
701 } else {
702 $row = '';
703 // array indexed data is not at $rows[ $k ]
704 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
705 $start = strrpos( $k, '[' );
706 $key = substr( $k, 0, $start++ );
707 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
708
709 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
710 $row = $rows[ $key ][ $index ];
711 }
712
713 unset( $start, $key, $index );
714 }
715 }
716
717 if ( is_array( $row ) ) {
718 // implode the repeated field values
719 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
720 }
721
722 $val = self::encode_value( $row );
723 if ( 'return' !== self::$line_break ) {
724 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
725 }
726
727 if ( $echo ) {
728 echo $sep . '"' . $val . '"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
729 } else {
730 fwrite( self::$fp, $sep . '"' . $val . '"' );
731 }
732 $sep = self::$column_separator;
733
734 unset( $k, $row );
735 }//end foreach
736 if ( $echo ) {
737 echo "\n";
738 } else {
739 fwrite( self::$fp, "\n" );
740 }
741 }
742
743 public static function encode_value( $line ) {
744 if ( '' === $line ) {
745 return $line;
746 }
747
748 $convmap = false;
749
750 switch ( self::$to_encoding ) {
751 case 'macintosh':
752 // this map was derived from the differences between the MacRoman and UTF-8 Charsets
753 // Reference:
754 // http://www.alanwood.net/demos/macroman.html.
755 $convmap = array( 256, 304, 0, 0xffff, 306, 337, 0, 0xffff, 340, 375, 0, 0xffff, 377, 401, 0, 0xffff, 403, 709, 0, 0xffff, 712, 727, 0, 0xffff, 734, 936, 0, 0xffff, 938, 959, 0, 0xffff, 961, 8210, 0, 0xffff, 8213, 8215, 0, 0xffff, 8219, 8219, 0, 0xffff, 8227, 8229, 0, 0xffff, 8231, 8239, 0, 0xffff, 8241, 8248, 0, 0xffff, 8251, 8259, 0, 0xffff, 8261, 8363, 0, 0xffff, 8365, 8481, 0, 0xffff, 8483, 8705, 0, 0xffff, 8707, 8709, 0, 0xffff, 8711, 8718, 0, 0xffff, 8720, 8720, 0, 0xffff, 8722, 8729, 0, 0xffff, 8731, 8733, 0, 0xffff, 8735, 8746, 0, 0xffff, 8748, 8775, 0, 0xffff, 8777, 8799, 0, 0xffff, 8801, 8803, 0, 0xffff, 8806, 9673, 0, 0xffff, 9675, 63742, 0, 0xffff, 63744, 64256, 0, 0xffff );
756 break;
757 case 'ISO-8859-1':
758 $convmap = array( 256, 10000, 0, 0xffff );
759 }
760
761 if ( is_array( $convmap ) ) {
762 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
763 }
764
765 if ( self::$to_encoding !== self::$charset ) {
766 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
767 }
768
769 return self::escape_csv( $line );
770 }
771
772 /**
773 * Escape a " in a csv with another "
774 *
775 * @since 2.0
776 * @param mixed $value
777 * @return mixed
778 */
779 public static function escape_csv( $value ) {
780 if ( ! is_string( $value ) ) {
781 return $value;
782 }
783
784 if ( '=' === $value[0] ) {
785 // escape the = to prevent vulnerability
786 $value = "'" . $value;
787 }
788 $value = str_replace( '"', '""', $value );
789
790 return $value;
791 }
792 }
793