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

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