PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.25.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.25.1
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.25.1, at classes/helpers/FrmCSVExportHelper.php

797 lines 23.0 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' => 's:7:"comment";',
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 if ( ! empty( $field_headings[ $col->id . '_label' ] ) ) {
331 $field_headings[ $col->id ] = strip_tags( $col->name . ' ' . __( '(value)', 'formidable' ) );
332 } else {
333 $field_headings[ $col->id ] = strip_tags( $col->name );
334 }
335 $field_headings = apply_filters(
336 'frm_csv_field_columns',
337 $field_headings,
338 array_merge(
339 self::get_standard_filter_args(),
340 array( 'field' => $col )
341 )
342 );
343
344 return $field_headings;
345 }
346
347 private static function csv_headings( &$headings ) {
348 $fields_by_repeater_id = array();
349 $repeater_ids = array();
350
351 foreach ( self::$fields as $col ) {
352 if ( self::is_the_child_of_a_repeater( $col ) ) {
353 $repeater_id = $col->field_options['in_section'];
354 // Set a placeholder to maintain order for repeater fields.
355 $headings[ 'repeater' . $repeater_id ] = array();
356
357 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
358 $fields_by_repeater_id[ $repeater_id ] = array();
359 $repeater_ids[] = $repeater_id;
360 }
361
362 $fields_by_repeater_id[ $repeater_id ][] = $col;
363
364 continue;
365 }
366
367 $headings += self::field_headings( $col );
368 }
369 unset( $repeater_id, $col );
370
371 if ( $repeater_ids ) {
372 $where = array( 'field_id' => $repeater_ids );
373 $repeater_meta = FrmDb::get_results( 'frm_item_metas', $where, 'field_id, meta_value' );
374 $max = array_fill_keys( $repeater_ids, 0 );
375
376 foreach ( $repeater_meta as $row ) {
377 $start = strpos( $row->meta_value, 'a:' ) + 2;
378 $end = strpos( $row->meta_value, ':{' );
379 $length = substr( $row->meta_value, $start, $end - $start );
380
381 if ( $length > $max[ $row->field_id ] ) {
382 $max[ $row->field_id ] = $length;
383 }
384 }
385 unset( $start, $end, $length, $row, $repeater_meta, $where );
386
387 $flat = array();
388 foreach ( $headings as $key => $heading ) {
389 if ( is_array( $heading ) ) {
390 $repeater_id = str_replace( 'repeater', '', $key );
391
392 $repeater_headings = array();
393 foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
394 $repeater_headings += self::field_headings( $col );
395 }
396
397 for ( $i = 0; $i < $max[ $repeater_id ]; $i++ ) {
398 foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
399 $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
400 }
401 }
402 } else {
403 $flat[ $key ] = $heading;
404 }
405 }
406
407 self::$fields_by_repeater_id = $fields_by_repeater_id;
408
409 unset( $key, $heading, $max, $repeater_headings, $repeater_id, $fields_by_repeater_id );
410
411 $headings = $flat;
412 unset( $flat );
413 }//end if
414
415 if ( self::$comment_count ) {
416 for ( $i = 0; $i < self::$comment_count; $i++ ) {
417 $headings[ 'comment' . $i ] = __( 'Comment', 'formidable' );
418 $headings[ 'comment_user_id' . $i ] = __( 'Comment User', 'formidable' );
419 $headings[ 'comment_created_at' . $i ] = __( 'Comment Date', 'formidable' );
420 }
421 unset( $i );
422 }
423
424 $headings['created_at'] = __( 'Timestamp', 'formidable' );
425 $headings['updated_at'] = __( 'Last Updated', 'formidable' );
426 $headings['user_id'] = __( 'Created By', 'formidable' );
427 $headings['updated_by'] = __( 'Updated By', 'formidable' );
428 $headings['is_draft'] = __( 'Entry Status', 'formidable' );
429 $headings['ip'] = __( 'IP', 'formidable' );
430 $headings['id'] = __( 'ID', 'formidable' );
431 $headings['item_key'] = __( 'Key', 'formidable' );
432 if ( self::has_parent_id() ) {
433 $headings['parent_id'] = __( 'Parent ID', 'formidable' );
434 }
435
436 $headings = apply_filters( 'frm_export_csv_headings', $headings );
437 }
438
439 /**
440 * @param object $field
441 * @return bool
442 */
443 private static function is_the_child_of_a_repeater( $field ) {
444 if ( $field->form_id === self::$form_id || empty( $field->field_options['in_section'] ) ) {
445 return false;
446 }
447
448 $section_id = $field->field_options['in_section'];
449 $section = FrmField::getOne( $section_id );
450
451 if ( ! $section ) {
452 return false;
453 }
454
455 return FrmField::is_repeating_field( $section );
456 }
457
458 private static function has_parent_id() {
459 return self::$has_parent_id;
460 }
461
462 private static function prepare_next_csv_rows( $next_set ) {
463 if ( FrmAppHelper::pro_is_installed() ) {
464 $where = array(
465 'or' => 1,
466 'id' => $next_set,
467 'parent_item_id' => $next_set,
468 );
469 // Order by parent_item_id so children will be first.
470 $order_by = ' ORDER BY parent_item_id DESC';
471 } else {
472 // When Pro is not installed, only query for direct ID matches only as we do not expect parent item id
473 // matches and the more simplified query is faster.
474 $where = array(
475 'id' => $next_set,
476 );
477 $order_by = '';
478 }
479
480 $entries = FrmEntry::getAll( $where, $order_by, '', true, false );
481
482 foreach ( $entries as $entry ) {
483 self::$entry = $entry;
484 unset( $entry );
485
486 if ( self::$entry->form_id !== self::$form_id ) {
487 self::add_repeat_field_values_to_csv( $entries );
488 } else {
489 self::prepare_csv_row();
490 }
491 }
492 }
493
494 private static function prepare_csv_row() {
495 $row = array();
496 self::add_field_values_to_csv( $row );
497 self::add_entry_data_to_csv( $row );
498 $row = apply_filters(
499 'frm_csv_row',
500 $row,
501 array(
502 'entry' => self::$entry,
503 'date_format' => self::$wp_date_format,
504 'comment_count' => self::$comment_count,
505 'context' => self::$context,
506 )
507 );
508 self::print_csv_row( $row );
509 }
510
511 private static function add_repeat_field_values_to_csv( &$entries ) {
512 if ( isset( self::$entry->metas ) ) {
513 // add child entries to the parent
514 foreach ( self::$entry->metas as $meta_id => $meta_value ) {
515 if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
516 // if the hook is being used to include field keys in the metas array,
517 // we need to skip the keys and only process field ids
518 continue;
519 }
520
521 if ( ! isset( $entries[ self::$entry->parent_item_id ] ) ) {
522 $entries[ self::$entry->parent_item_id ] = new stdClass();
523 $entries[ self::$entry->parent_item_id ]->metas = array();
524 }
525
526 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
527 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
528 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
529 // if the data is here, it should be an array but if this field has collected data
530 // both while inside and outside of the repeating section, it's possible this is a string.
531 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
532 }
533
534 // Add the repeated values.
535 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
536 }//end foreach
537
538 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
539 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
540 }//end if
541
542 // add the embedded form id
543 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
544 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
545 }
546 $entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
547 }
548
549 /**
550 * When an empty field is saved, it isn't saved as a meta value
551 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
552 *
553 * @param array $metas
554 * @param array $entries
555 * @return array
556 */
557 private static function fill_missing_repeater_metas( $metas, &$entries ) {
558 $field_ids = array_keys( $metas );
559 $field_id = end( $field_ids );
560 $field = self::get_field( $field_id );
561
562 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
563 return $metas;
564 }
565
566 $repeater_id = $field->field_options['in_section'];
567 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
568 return $metas;
569 }
570
571 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
572 if ( ! isset( $metas[ $repeater_child->id ] ) ) {
573 $metas[ $repeater_child->id ] = '';
574
575 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) || ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) ) {
576 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] = array();
577 }
578
579 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
580 }
581 }
582
583 return $metas;
584 }
585
586 private static function get_field( $field_id ) {
587 $field_id = (int) $field_id;
588 foreach ( self::$fields as $field ) {
589 if ( $field_id === (int) $field->id ) {
590 return $field;
591 }
592 }
593 return false;
594 }
595
596 private static function add_field_values_to_csv( &$row ) {
597 foreach ( self::$fields as $col ) {
598 $field_value = self::$entry->metas[ $col->id ] ?? false;
599
600 FrmFieldsHelper::prepare_field_value( $field_value, $col->type );
601 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
602
603 $field_value = apply_filters(
604 'frm_csv_value',
605 $field_value,
606 array(
607 'field' => $col,
608 'entry' => self::$entry,
609 'separator' => self::$separator,
610 'context' => self::$context,
611 )
612 );
613
614 if ( ! empty( $col->field_options['separate_value'] ) ) {
615 $label_key = $col->id . '_label';
616 if ( self::is_the_child_of_a_repeater( $col ) ) {
617 $row[ $label_key ] = array();
618
619 if ( is_array( $field_value ) ) {
620 foreach ( $field_value as $value ) {
621 $row[ $label_key ][] = self::get_separate_value_label( $value, $col );
622 }
623 }
624 } else {
625 $row[ $label_key ] = self::get_separate_value_label( $field_value, $col );
626 }
627 unset( $label_key );
628 }
629
630 $row[ $col->id ] = $field_value;
631
632 unset( $col, $field_value );
633 }//end foreach
634 }
635
636 /**
637 * @since 5.0.06
638 *
639 * @param mixed $field_value
640 * @param stdClass $field
641 * @return string
642 */
643 private static function get_separate_value_label( $field_value, $field ) {
644 return FrmEntriesHelper::display_value(
645 $field_value,
646 $field,
647 array(
648 'type' => $field->type,
649 'post_id' => self::$entry->post_id,
650 'show_icon' => false,
651 'entry_id' => self::$entry->id,
652 'sep' => self::$separator,
653 '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,
654 )
655 );
656 }
657
658 /**
659 * @since 2.0.23
660 */
661 private static function add_array_values_to_columns( &$row, $atts ) {
662 if ( is_array( $atts['field_value'] ) ) {
663 foreach ( $atts['field_value'] as $key => $sub_value ) {
664 if ( is_array( $sub_value ) ) {
665 // This is combo field inside repeater. The heading key has this format: [86_first[0]].
666 foreach ( $sub_value as $sub_key => $sub_sub_value ) {
667 $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
668 if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
669 $row[ $column_key ] = $sub_sub_value;
670 }
671 }
672
673 continue;
674 }
675
676 $column_key = $atts['col']->id . '_' . $key;
677 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
678 $row[ $column_key ] = $sub_value;
679 }
680 }
681 }
682 }
683
684 private static function add_entry_data_to_csv( &$row ) {
685 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
686 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
687 $row['user_id'] = self::$entry->user_id;
688 $row['updated_by'] = self::$entry->updated_by;
689 $row['is_draft'] = self::$entry->is_draft;
690 $row['ip'] = self::$entry->ip;
691 $row['id'] = self::$entry->id;
692 $row['item_key'] = self::$entry->item_key;
693 if ( self::has_parent_id() ) {
694 $row['parent_id'] = self::$entry->parent_item_id;
695 }
696 }
697
698 private static function print_csv_row( $rows ) {
699 $sep = '';
700 $echo = 'echo' === self::$mode;
701
702 foreach ( self::$headings as $k => $heading ) {
703 if ( isset( $rows[ $k ] ) ) {
704 $row = $rows[ $k ];
705 } else {
706 $row = '';
707 // array indexed data is not at $rows[ $k ]
708 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
709 $start = strrpos( $k, '[' );
710 $key = substr( $k, 0, $start++ );
711 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
712
713 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
714 $row = $rows[ $key ][ $index ];
715 }
716
717 unset( $start, $key, $index );
718 }
719 }
720
721 if ( is_array( $row ) ) {
722 // implode the repeated field values
723 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
724 }
725
726 $val = self::encode_value( $row );
727 if ( 'return' !== self::$line_break ) {
728 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
729 }
730
731 if ( $echo ) {
732 echo $sep . '"' . $val . '"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
733 } else {
734 fwrite( self::$fp, $sep . '"' . $val . '"' );
735 }
736 $sep = self::$column_separator;
737
738 unset( $k, $row );
739 }//end foreach
740 if ( $echo ) {
741 echo "\n";
742 } else {
743 fwrite( self::$fp, "\n" );
744 }
745 }
746
747 public static function encode_value( $line ) {
748 if ( '' === $line ) {
749 return $line;
750 }
751
752 $convmap = false;
753
754 switch ( self::$to_encoding ) {
755 case 'macintosh':
756 // this map was derived from the differences between the MacRoman and UTF-8 Charsets
757 // Reference:
758 // http://www.alanwood.net/demos/macroman.html.
759 $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 );
760 break;
761 case 'ISO-8859-1':
762 $convmap = array( 256, 10000, 0, 0xffff );
763 }
764
765 if ( is_array( $convmap ) ) {
766 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
767 }
768
769 if ( self::$to_encoding !== self::$charset ) {
770 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
771 }
772
773 return self::escape_csv( $line );
774 }
775
776 /**
777 * Escape a " in a csv with another "
778 *
779 * @since 2.0
780 * @param mixed $value
781 * @return mixed
782 */
783 public static function escape_csv( $value ) {
784 if ( ! is_string( $value ) ) {
785 return $value;
786 }
787
788 if ( '=' === $value[0] ) {
789 // escape the = to prevent vulnerability
790 $value = "'" . $value;
791 }
792 $value = str_replace( '"', '""', $value );
793
794 return $value;
795 }
796 }
797