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

890 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::add_repeat_field_values_to_csv( $entries );
527 } else {
528 self::prepare_csv_row();
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 $metas[ $repeater_child->id ] = '';
623
624 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
625 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] = array();
626 }
627
628 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
629 }
630 }
631
632 return $metas;
633 }
634
635 /**
636 * @param int|string $field_id
637 *
638 * @return false|object
639 */
640 private static function get_field( $field_id ) {
641 $field_id = (int) $field_id;
642
643 foreach ( self::$fields as $field ) {
644 if ( $field_id === (int) $field->id ) {
645 return $field;
646 }
647 }
648
649 return false;
650 }
651
652 /**
653 * @param array $row
654 *
655 * @return void
656 */
657 private static function add_field_values_to_csv( &$row ) {
658 foreach ( self::$fields as $col ) {
659 $field_value = self::$entry->metas[ $col->id ] ?? false;
660
661 FrmFieldsHelper::prepare_field_value( $field_value, $col->type );
662 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
663
664 $field_value = apply_filters(
665 'frm_csv_value',
666 $field_value,
667 array(
668 'field' => $col,
669 'entry' => self::$entry,
670 'separator' => self::$separator,
671 'context' => self::$context,
672 )
673 );
674
675 if ( ! empty( $col->field_options['separate_value'] ) ) {
676 $label_key = $col->id . '_label';
677
678 if ( self::is_the_child_of_a_repeater( $col ) ) {
679 $row[ $label_key ] = array();
680
681 if ( is_array( $field_value ) ) {
682 foreach ( $field_value as $value ) {
683 $row[ $label_key ][] = self::get_separate_value_label( $value, $col );
684 }
685 }
686 } else {
687 $row[ $label_key ] = self::get_separate_value_label( $field_value, $col );
688 }
689 unset( $label_key );
690 }
691
692 $row[ $col->id ] = $field_value;
693
694 unset( $col, $field_value );
695 }//end foreach
696 }
697
698 /**
699 * @since 5.0.06
700 *
701 * @param mixed $field_value
702 * @param stdClass $field
703 *
704 * @return string
705 */
706 private static function get_separate_value_label( $field_value, $field ) {
707 return FrmEntriesHelper::display_value(
708 $field_value,
709 $field,
710 array(
711 'type' => $field->type,
712 'post_id' => self::$entry->post_id,
713 'show_icon' => false,
714 'entry_id' => self::$entry->id,
715 'sep' => self::$separator,
716 '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
717 )
718 );
719 }
720
721 /**
722 * @since 2.0.23
723 *
724 * @param array $row
725 * @param array $atts
726 *
727 * @return void
728 */
729 private static function add_array_values_to_columns( &$row, $atts ) {
730 if ( ! is_array( $atts['field_value'] ) ) {
731 return;
732 }
733
734 foreach ( $atts['field_value'] as $key => $sub_value ) {
735 if ( is_array( $sub_value ) ) {
736 // This is combo field inside repeater. The heading key has this format: [86_first[0]].
737 foreach ( $sub_value as $sub_key => $sub_sub_value ) {
738 $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
739
740 if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
741 $row[ $column_key ] = $sub_sub_value;
742 }
743 }
744
745 continue;
746 }
747
748 $column_key = $atts['col']->id . '_' . $key;
749
750 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
751 $row[ $column_key ] = $sub_value;
752 }
753 }
754 }
755
756 /**
757 * @param array $row
758 *
759 * @return void
760 */
761 private static function add_entry_data_to_csv( &$row ) {
762 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
763 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
764 $row['user_id'] = self::$entry->user_id;
765 $row['updated_by'] = self::$entry->updated_by;
766 $row['is_draft'] = self::$entry->is_draft;
767 $row['ip'] = self::$entry->ip;
768 $row['id'] = self::$entry->id;
769 $row['item_key'] = self::$entry->item_key;
770
771 if ( self::has_parent_id() ) {
772 $row['parent_id'] = self::$entry->parent_item_id;
773 }
774 }
775
776 /**
777 * @param array $rows
778 *
779 * @return void
780 */
781 private static function print_csv_row( $rows ) {
782 $sep = '';
783 $echo = 'echo' === self::$mode;
784
785 foreach ( self::$headings as $k => $heading ) {
786 if ( isset( $rows[ $k ] ) ) {
787 $row = $rows[ $k ];
788 } else {
789 $row = '';
790
791 // array indexed data is not at $rows[ $k ]
792 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
793 $start = strrpos( $k, '[' );
794 $key = substr( $k, 0, $start++ );
795 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
796
797 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
798 $row = $rows[ $key ][ $index ];
799 }
800
801 unset( $start, $key, $index );
802 }
803 }
804
805 if ( is_array( $row ) ) {
806 // Implode the repeated field values
807 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
808 }
809
810 $val = self::encode_value( $row );
811
812 if ( 'return' !== self::$line_break ) {
813 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
814 }
815
816 if ( $echo ) {
817 echo $sep . '"' . $val . '"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
818 } else {
819 fwrite( self::$fp, $sep . '"' . $val . '"' );
820 }
821
822 $sep = self::$column_separator;
823
824 unset( $k, $row );
825 }//end foreach
826
827 if ( $echo ) {
828 echo "\n";
829 } else {
830 fwrite( self::$fp, "\n" );
831 }
832 }
833
834 /**
835 * @param string $line
836 *
837 * @return string
838 */
839 public static function encode_value( $line ) {
840 if ( '' === $line ) {
841 return $line;
842 }
843
844 $convmap = false;
845
846 switch ( self::$to_encoding ) {
847 case 'macintosh':
848 // This map was derived from the differences between the MacRoman and UTF-8 Charsets
849 // Reference:
850 // http://www.alanwood.net/demos/macroman.html.
851 $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
852 break;
853 case 'ISO-8859-1':
854 $convmap = array( 256, 10000, 0, 0xffff );
855 }
856
857 if ( is_array( $convmap ) ) {
858 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
859 }
860
861 if ( self::$to_encoding !== self::$charset ) {
862 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
863 }
864
865 return self::escape_csv( $line );
866 }
867
868 /**
869 * Escape a " in a csv with another "
870 *
871 * @since 2.0
872 *
873 * @param mixed $value
874 *
875 * @return mixed
876 */
877 public static function escape_csv( $value ) {
878 if ( ! is_string( $value ) ) {
879 return $value;
880 }
881
882 if ( '=' === $value[0] ) {
883 // escape the = to prevent vulnerability
884 $value = "'" . $value;
885 }
886
887 return str_replace( '"', '""', $value );
888 }
889 }
890