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

892 lines 24.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
118 if ( ! $on_global_settings_page ) {
119 array_splice( $formats, 1, 0, 'UTF-8 with BOM' );
120 }
121
122 return apply_filters( 'frm_csv_format_options', $formats );
123 }
124
125 /**
126 * @param array $atts
127 *
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
149 if ( ! self::$fp ) {
150 return false;
151 }
152 } elseif ( 'echo' === self::$mode ) {
153 self::print_file_headers( $filename );
154 }
155
156 unset( $filename );
157
158 self::$comment_count = FrmDb::get_count(
159 'frm_item_metas',
160 array(
161 'item_id' => $atts['entry_ids'],
162 'field_id' => 0,
163 'meta_value like' => 's:7:"comment";',
164 ),
165 array(
166 'group_by' => 'item_id',
167 'order_by' => 'count(*) DESC',
168 'limit' => 1,
169 )
170 );
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 *
207 * @return void
208 */
209 private static function after_generate_csv( $atts ) {
210 /**
211 * @since 6.8.4
212 *
213 * @param array $atts {
214 *
215 * @type object $form
216 * @type array $entry_ids
217 * @type array $form_cols
218 * }
219 */
220 do_action( 'frm_after_generate_csv', $atts );
221 }
222
223 /**
224 * @since 5.0.16
225 *
226 * @param stdClass $form
227 *
228 * @return string
229 */
230 private static function generate_csv_filename( $form ) {
231 $filename = gmdate( 'ymdHis', time() ) . '_' . sanitize_title_with_dashes( $form->name ) . '_formidable_entries.csv';
232 return apply_filters( 'frm_csv_filename', $filename, $form, self::get_standard_filter_args() );
233 }
234
235 /**
236 * @since 5.0.16
237 *
238 * @return array
239 */
240 private static function get_standard_filter_args() {
241 return array(
242 'context' => self::$context,
243 'meta' => self::$meta,
244 );
245 }
246
247 /**
248 * @return void
249 */
250 private static function set_class_parameters() {
251 $args = self::get_standard_filter_args();
252 self::$separator = apply_filters( 'frm_csv_sep', self::$separator, $args );
253 self::$line_break = apply_filters( 'frm_csv_line_break', self::$line_break, $args );
254 self::$wp_date_format = apply_filters( 'frm_csv_date_format', self::$wp_date_format, $args );
255 self::get_csv_format();
256 self::$charset = get_option( 'blog_charset' );
257
258 // phpcs:ignore WordPress.Security.NonceVerification.Missing
259 $col_sep = ! empty( $_POST['csv_col_sep'] ) ? sanitize_text_field( wp_unslash( $_POST['csv_col_sep'] ) ) : self::$column_separator;
260
261 self::$column_separator = apply_filters( 'frm_csv_column_sep', $col_sep, $args );
262 }
263
264 /**
265 * @param object $form
266 *
267 * @return void
268 */
269 private static function set_has_parent_id( $form ) {
270 self::$has_parent_id = $form->parent_form_id > 0;
271 }
272
273 /**
274 * @param string $filename
275 *
276 * @return void
277 */
278 private static function print_file_headers( $filename ) {
279 header( 'Content-Description: File Transfer' );
280 header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' );
281 header( 'Content-Type: text/csv; charset=' . self::$charset, true );
282 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' );
283 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
284 header( 'Cache-Control: no-cache, must-revalidate' );
285 header( 'Pragma: no-cache' );
286
287 if ( self::$include_bom ) {
288 echo esc_html( chr( 239 ) . chr( 187 ) . chr( 191 ) );
289 }
290
291 do_action(
292 'frm_csv_headers',
293 array(
294 'form_id' => self::$form_id,
295 'fields' => self::$fields,
296 )
297 );
298 }
299
300 /**
301 * Check the POST request data for the CSV format to use.
302 *
303 * @return void
304 */
305 public static function get_csv_format() {
306 $csv_format = FrmAppHelper::get_post_param( 'csv_format', 'UTF-8', 'sanitize_text_field' );
307
308 if ( 'UTF-8 with BOM' === $csv_format ) {
309 self::$include_bom = true;
310 $csv_format = 'UTF-8';
311 }
312
313 $csv_format = apply_filters( 'frm_csv_format', $csv_format, self::get_standard_filter_args() );
314 self::$to_encoding = $csv_format;
315 }
316
317 /**
318 * @return void
319 */
320 private static function prepare_csv_headings() {
321 $headings = array();
322 self::csv_headings( $headings );
323 $headings = apply_filters(
324 'frm_csv_columns',
325 $headings,
326 self::$form_id,
327 array_merge(
328 self::get_standard_filter_args(),
329 array( 'fields' => self::$fields )
330 )
331 );
332 self::$headings = $headings;
333
334 self::print_csv_row( $headings );
335 }
336
337 /**
338 * @param object $col
339 *
340 * @return array
341 */
342 private static function field_headings( $col ) {
343 $field_type_obj = FrmFieldFactory::get_field_factory( $col );
344
345 if ( ! empty( $field_type_obj->is_combo_field ) ) {
346 // This is combo field.
347 return $field_type_obj->get_export_headings();
348 }
349
350 $field_headings = array();
351 $separate_values = array( 'user_id', 'file', 'data', 'date' );
352
353 if ( ! empty( $col->field_options['separate_value'] ) && ! in_array( $col->type, $separate_values, true ) ) {
354 $field_headings[ $col->id . '_label' ] = strip_tags( $col->name . ' ' . __( '(label)', 'formidable' ) );
355 }
356
357 if ( ! empty( $field_headings[ $col->id . '_label' ] ) ) {
358 $field_headings[ $col->id ] = strip_tags( $col->name . ' ' . __( '(value)', 'formidable' ) );
359 } else {
360 $field_headings[ $col->id ] = strip_tags( $col->name );
361 }
362
363 return apply_filters(
364 'frm_csv_field_columns',
365 $field_headings,
366 array_merge(
367 self::get_standard_filter_args(),
368 array( 'field' => $col )
369 )
370 );
371 }
372
373 /**
374 * @param array $headings
375 *
376 * @return void
377 */
378 private static function csv_headings( &$headings ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
379 $fields_by_repeater_id = array();
380 $repeater_ids = array();
381
382 foreach ( self::$fields as $col ) {
383 if ( self::is_the_child_of_a_repeater( $col ) ) {
384 $repeater_id = $col->field_options['in_section'];
385 // Set a placeholder to maintain order for repeater fields.
386 $headings[ 'repeater' . $repeater_id ] = array();
387
388 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
389 $fields_by_repeater_id[ $repeater_id ] = array();
390 $repeater_ids[] = $repeater_id;
391 }
392
393 $fields_by_repeater_id[ $repeater_id ][] = $col;
394
395 continue;
396 }
397
398 $headings += self::field_headings( $col );
399 }
400 unset( $repeater_id, $col );
401
402 if ( $repeater_ids ) {
403 $where = array( 'field_id' => $repeater_ids );
404 $repeater_meta = FrmDb::get_results( 'frm_item_metas', $where, 'field_id, meta_value' );
405 $max = array_fill_keys( $repeater_ids, 0 );
406
407 foreach ( $repeater_meta as $row ) {
408 $start = strpos( $row->meta_value, 'a:' ) + 2;
409 $end = strpos( $row->meta_value, ':{' );
410 $length = substr( $row->meta_value, $start, $end - $start );
411
412 if ( $length > $max[ $row->field_id ] ) {
413 $max[ $row->field_id ] = $length;
414 }
415 }
416 unset( $start, $end, $length, $row, $repeater_meta, $where );
417
418 $flat = array();
419
420 foreach ( $headings as $key => $heading ) {
421 if ( ! is_array( $heading ) ) {
422 $flat[ $key ] = $heading;
423 continue;
424 }
425
426 $repeater_id = str_replace( 'repeater', '', $key );
427 $repeater_headings = array();
428
429 foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
430 $repeater_headings += self::field_headings( $col );
431 }
432
433 for ( $i = 0; $i < $max[ $repeater_id ]; $i++ ) {
434 foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
435 $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
436 }
437 }
438 }
439
440 self::$fields_by_repeater_id = $fields_by_repeater_id;
441
442 unset( $key, $heading, $max, $repeater_headings, $repeater_id, $fields_by_repeater_id );
443
444 $headings = $flat;
445 unset( $flat );
446 }//end if
447
448 if ( self::$comment_count ) {
449 for ( $i = 0; $i < self::$comment_count; $i++ ) {
450 $headings[ 'comment' . $i ] = __( 'Comment', 'formidable' );
451 $headings[ 'comment_user_id' . $i ] = __( 'Comment User', 'formidable' );
452 $headings[ 'comment_created_at' . $i ] = __( 'Comment Date', 'formidable' );
453 }
454 unset( $i );
455 }
456
457 $headings['created_at'] = __( 'Timestamp', 'formidable' );
458 $headings['updated_at'] = __( 'Last Updated', 'formidable' );
459 $headings['user_id'] = __( 'Created By', 'formidable' );
460 $headings['updated_by'] = __( 'Updated By', 'formidable' );
461 $headings['is_draft'] = __( 'Entry Status', 'formidable' );
462 $headings['ip'] = __( 'IP', 'formidable' );
463 $headings['id'] = __( 'ID', 'formidable' );
464 $headings['item_key'] = __( 'Key', 'formidable' );
465
466 if ( self::has_parent_id() ) {
467 $headings['parent_id'] = __( 'Parent ID', 'formidable' );
468 }
469
470 $headings = apply_filters( 'frm_export_csv_headings', $headings );
471 }
472
473 /**
474 * @param object $field
475 *
476 * @return bool
477 */
478 private static function is_the_child_of_a_repeater( $field ) {
479 if ( $field->form_id === self::$form_id || empty( $field->field_options['in_section'] ) ) {
480 return false;
481 }
482
483 $section_id = $field->field_options['in_section'];
484 $section = FrmField::getOne( $section_id );
485
486 return $section && FrmField::is_repeating_field( $section );
487 }
488
489 /**
490 * @return bool
491 */
492 private static function has_parent_id() {
493 return self::$has_parent_id;
494 }
495
496 /**
497 * @param array $next_set
498 *
499 * @return void
500 */
501 private static function prepare_next_csv_rows( $next_set ) {
502 if ( FrmAppHelper::pro_is_installed() ) {
503 $where = array(
504 'or' => 1,
505 'id' => $next_set,
506 'parent_item_id' => $next_set,
507 );
508 // Order by parent_item_id so children will be first.
509 $order_by = ' ORDER BY parent_item_id DESC';
510 } else {
511 // When Pro is not installed, only query for direct ID matches only as we do not expect parent item id
512 // Matches and the more simplified query is faster.
513 $where = array(
514 'id' => $next_set,
515 );
516 $order_by = '';
517 }
518
519 $entries = FrmEntry::getAll( $where, $order_by, '', true, false );
520
521 foreach ( $entries as $entry ) {
522 self::$entry = $entry;
523 unset( $entry );
524
525 if ( self::$entry->form_id === self::$form_id ) {
526 self::prepare_csv_row();
527 } else {
528 self::add_repeat_field_values_to_csv( $entries );
529 }
530 }
531 }
532
533 /**
534 * @return void
535 */
536 private static function prepare_csv_row() {
537 $row = array();
538 self::add_field_values_to_csv( $row );
539 self::add_entry_data_to_csv( $row );
540 $row = apply_filters(
541 'frm_csv_row',
542 $row,
543 array(
544 'entry' => self::$entry,
545 'date_format' => self::$wp_date_format,
546 'comment_count' => self::$comment_count,
547 'context' => self::$context,
548 )
549 );
550 self::print_csv_row( $row );
551 }
552
553 /**
554 * @param array $entries
555 *
556 * @return void
557 */
558 private static function add_repeat_field_values_to_csv( &$entries ) {
559 if ( isset( self::$entry->metas ) ) {
560 // Add child entries to the parent
561 foreach ( self::$entry->metas as $meta_id => $meta_value ) {
562 if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
563 // If the hook is being used to include field keys in the metas array,
564 // We need to skip the keys and only process field ids
565 continue;
566 }
567
568 if ( ! isset( $entries[ self::$entry->parent_item_id ] ) ) {
569 $entries[ self::$entry->parent_item_id ] = new stdClass();
570 $entries[ self::$entry->parent_item_id ]->metas = array();
571 }
572
573 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
574 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
575 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
576 // If the data is here, it should be an array but if this field has collected data
577 // Both while inside and outside of the repeating section, it's possible this is a string.
578 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
579 }
580
581 // Add the repeated values.
582 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
583 }//end foreach
584
585 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
586 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
587 }//end if
588
589 // Add the embedded form id
590 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
591 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
592 }
593
594 $entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
595 }
596
597 /**
598 * When an empty field is saved, it isn't saved as a meta value
599 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
600 *
601 * @param array $metas
602 * @param array $entries
603 *
604 * @return array
605 */
606 private static function fill_missing_repeater_metas( $metas, &$entries ) {
607 $field_id = array_key_last( $metas );
608 $field = self::get_field( $field_id );
609
610 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
611 return $metas;
612 }
613
614 $repeater_id = $field->field_options['in_section'];
615
616 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
617 return $metas;
618 }
619
620 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
621 if ( isset( $metas[ $repeater_child->id ] ) ) {
622 continue;
623 }
624
625 $metas[ $repeater_child->id ] = '';
626
627 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) || ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) ) { // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
628 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] = array();
629 }
630
631 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
632 }
633
634 return $metas;
635 }
636
637 /**
638 * @param int|string $field_id
639 *
640 * @return false|object
641 */
642 private static function get_field( $field_id ) {
643 $field_id = (int) $field_id;
644
645 foreach ( self::$fields as $field ) {
646 if ( $field_id === (int) $field->id ) {
647 return $field;
648 }
649 }
650
651 return false;
652 }
653
654 /**
655 * @param array $row
656 *
657 * @return void
658 */
659 private static function add_field_values_to_csv( &$row ) {
660 foreach ( self::$fields as $col ) {
661 $field_value = self::$entry->metas[ $col->id ] ?? false;
662
663 FrmFieldsHelper::prepare_field_value( $field_value, $col->type );
664 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
665
666 $field_value = apply_filters(
667 'frm_csv_value',
668 $field_value,
669 array(
670 'field' => $col,
671 'entry' => self::$entry,
672 'separator' => self::$separator,
673 'context' => self::$context,
674 )
675 );
676
677 if ( ! empty( $col->field_options['separate_value'] ) ) {
678 $label_key = $col->id . '_label';
679
680 if ( self::is_the_child_of_a_repeater( $col ) ) {
681 $row[ $label_key ] = array();
682
683 if ( is_array( $field_value ) ) {
684 foreach ( $field_value as $value ) {
685 $row[ $label_key ][] = self::get_separate_value_label( $value, $col );
686 }
687 }
688 } else {
689 $row[ $label_key ] = self::get_separate_value_label( $field_value, $col );
690 }
691 unset( $label_key );
692 }
693
694 $row[ $col->id ] = $field_value;
695
696 unset( $col, $field_value );
697 }//end foreach
698 }
699
700 /**
701 * @since 5.0.06
702 *
703 * @param mixed $field_value
704 * @param stdClass $field
705 *
706 * @return string
707 */
708 private static function get_separate_value_label( $field_value, $field ) {
709 return FrmEntriesHelper::display_value(
710 $field_value,
711 $field,
712 array(
713 'type' => $field->type,
714 'post_id' => self::$entry->post_id,
715 'show_icon' => false,
716 'entry_id' => self::$entry->id,
717 'sep' => self::$separator,
718 '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, // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
719 )
720 );
721 }
722
723 /**
724 * @since 2.0.23
725 *
726 * @param array $row
727 * @param array $atts
728 *
729 * @return void
730 */
731 private static function add_array_values_to_columns( &$row, $atts ) {
732 if ( ! is_array( $atts['field_value'] ) ) {
733 return;
734 }
735
736 foreach ( $atts['field_value'] as $key => $sub_value ) {
737 if ( is_array( $sub_value ) ) {
738 // This is combo field inside repeater. The heading key has this format: [86_first[0]].
739 foreach ( $sub_value as $sub_key => $sub_sub_value ) {
740 $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
741
742 if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
743 $row[ $column_key ] = $sub_sub_value;
744 }
745 }
746
747 continue;
748 }
749
750 $column_key = $atts['col']->id . '_' . $key;
751
752 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
753 $row[ $column_key ] = $sub_value;
754 }
755 }
756 }
757
758 /**
759 * @param array $row
760 *
761 * @return void
762 */
763 private static function add_entry_data_to_csv( &$row ) {
764 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
765 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
766 $row['user_id'] = self::$entry->user_id;
767 $row['updated_by'] = self::$entry->updated_by;
768 $row['is_draft'] = self::$entry->is_draft;
769 $row['ip'] = self::$entry->ip;
770 $row['id'] = self::$entry->id;
771 $row['item_key'] = self::$entry->item_key;
772
773 if ( self::has_parent_id() ) {
774 $row['parent_id'] = self::$entry->parent_item_id;
775 }
776 }
777
778 /**
779 * @param array $rows
780 *
781 * @return void
782 */
783 private static function print_csv_row( $rows ) {
784 $sep = '';
785 $echo = 'echo' === self::$mode;
786
787 foreach ( self::$headings as $k => $heading ) {
788 if ( isset( $rows[ $k ] ) ) {
789 $row = $rows[ $k ];
790 } else {
791 $row = '';
792
793 // array indexed data is not at $rows[ $k ]
794 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
795 $start = strrpos( $k, '[' );
796 $key = substr( $k, 0, $start++ );
797 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
798
799 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
800 $row = $rows[ $key ][ $index ];
801 }
802
803 unset( $start, $key, $index );
804 }
805 }
806
807 if ( is_array( $row ) ) {
808 // Implode the repeated field values
809 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
810 }
811
812 $val = self::encode_value( $row );
813
814 if ( 'return' !== self::$line_break ) {
815 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
816 }
817
818 if ( $echo ) {
819 echo $sep . '"' . $val . '"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
820 } else {
821 fwrite( self::$fp, $sep . '"' . $val . '"' );
822 }
823
824 $sep = self::$column_separator;
825
826 unset( $k, $row );
827 }//end foreach
828
829 if ( $echo ) {
830 echo "\n";
831 } else {
832 fwrite( self::$fp, "\n" );
833 }
834 }
835
836 /**
837 * @param string $line
838 *
839 * @return string
840 */
841 public static function encode_value( $line ) {
842 if ( '' === $line ) {
843 return $line;
844 }
845
846 $convmap = false;
847
848 switch ( self::$to_encoding ) {
849 case 'macintosh':
850 // This map was derived from the differences between the MacRoman and UTF-8 Charsets
851 // Reference:
852 // http://www.alanwood.net/demos/macroman.html.
853 $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 ); // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
854 break;
855 case 'ISO-8859-1':
856 $convmap = array( 256, 10000, 0, 0xffff );
857 }
858
859 if ( is_array( $convmap ) ) {
860 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
861 }
862
863 if ( self::$to_encoding !== self::$charset ) {
864 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
865 }
866
867 return self::escape_csv( $line );
868 }
869
870 /**
871 * Escape a " in a csv with another "
872 *
873 * @since 2.0
874 *
875 * @param mixed $value
876 *
877 * @return mixed
878 */
879 public static function escape_csv( $value ) {
880 if ( ! is_string( $value ) ) {
881 return $value;
882 }
883
884 if ( '=' === $value[0] ) {
885 // escape the = to prevent vulnerability
886 $value = "'" . $value;
887 }
888
889 return str_replace( '"', '""', $value );
890 }
891 }
892