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

889 lines 24.1 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 $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 self::$comment_count = $comment_count;
172
173 self::prepare_csv_headings();
174
175 /**
176 * For a target form with 50k entries (Locations List, with 7 fields), batch sizes of 100
177 * were significantly faster and used less memory, so a filter was added.
178 *
179 * @since 6.17
180 *
181 * @param int $batch_size
182 * @param int $form_id
183 */
184 $csv_export_batch_size = (int) apply_filters( 'frm_csv_export_batch_size', 20, self::$form_id );
185
186 // Fetch posts in batches rather than loading the entire table into memory.
187 while ( $next_set = array_splice( $atts['entry_ids'], 0, $csv_export_batch_size ) ) {
188 self::prepare_next_csv_rows( $next_set );
189 }
190
191 self::after_generate_csv( $atts );
192
193 unset( $atts['form'], $atts['form_cols'] );
194
195 if ( 'file' === self::$mode ) {
196 fclose( self::$fp );
197 return $filepath;
198 }
199
200 return null;
201 }
202
203 /**
204 * @since 6.8.4
205 *
206 * @param array $atts
207 *
208 * @return void
209 */
210 private static function after_generate_csv( $atts ) {
211 /**
212 * @since 6.8.4
213 *
214 * @param array $atts {
215 *
216 * @type object $form
217 * @type array $entry_ids
218 * @type array $form_cols
219 * }
220 */
221 do_action( 'frm_after_generate_csv', $atts );
222 }
223
224 /**
225 * @since 5.0.16
226 *
227 * @param stdClass $form
228 *
229 * @return string
230 */
231 private static function generate_csv_filename( $form ) {
232 $filename = gmdate( 'ymdHis', time() ) . '_' . sanitize_title_with_dashes( $form->name ) . '_formidable_entries.csv';
233 return apply_filters( 'frm_csv_filename', $filename, $form, self::get_standard_filter_args() );
234 }
235
236 /**
237 * @since 5.0.16
238 *
239 * @return array
240 */
241 private static function get_standard_filter_args() {
242 return array(
243 'context' => self::$context,
244 'meta' => self::$meta,
245 );
246 }
247
248 /**
249 * @return void
250 */
251 private static function set_class_parameters() {
252 $args = self::get_standard_filter_args();
253 self::$separator = apply_filters( 'frm_csv_sep', self::$separator, $args );
254 self::$line_break = apply_filters( 'frm_csv_line_break', self::$line_break, $args );
255 self::$wp_date_format = apply_filters( 'frm_csv_date_format', self::$wp_date_format, $args );
256 self::get_csv_format();
257 self::$charset = get_option( 'blog_charset' );
258
259 // phpcs:ignore WordPress.Security.NonceVerification.Missing
260 $col_sep = ! empty( $_POST['csv_col_sep'] ) ? sanitize_text_field( wp_unslash( $_POST['csv_col_sep'] ) ) : self::$column_separator;
261
262 self::$column_separator = apply_filters( 'frm_csv_column_sep', $col_sep, $args );
263 }
264
265 /**
266 * @param object $form
267 *
268 * @return void
269 */
270 private static function set_has_parent_id( $form ) {
271 self::$has_parent_id = $form->parent_form_id > 0;
272 }
273
274 /**
275 * @param string $filename
276 *
277 * @return void
278 */
279 private static function print_file_headers( $filename ) {
280 header( 'Content-Description: File Transfer' );
281 header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' );
282 header( 'Content-Type: text/csv; charset=' . self::$charset, true );
283 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' );
284 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
285 header( 'Cache-Control: no-cache, must-revalidate' );
286 header( 'Pragma: no-cache' );
287
288 if ( self::$include_bom ) {
289 echo esc_html( chr( 239 ) . chr( 187 ) . chr( 191 ) );
290 }
291
292 do_action(
293 'frm_csv_headers',
294 array(
295 'form_id' => self::$form_id,
296 'fields' => self::$fields,
297 )
298 );
299 }
300
301 /**
302 * Check the POST request data for the CSV format to use.
303 *
304 * @return void
305 */
306 public static function get_csv_format() {
307 $csv_format = FrmAppHelper::get_post_param( 'csv_format', 'UTF-8', 'sanitize_text_field' );
308
309 if ( 'UTF-8 with BOM' === $csv_format ) {
310 self::$include_bom = true;
311 $csv_format = 'UTF-8';
312 }
313
314 $csv_format = apply_filters( 'frm_csv_format', $csv_format, self::get_standard_filter_args() );
315 self::$to_encoding = $csv_format;
316 }
317
318 /**
319 * @return void
320 */
321 private static function prepare_csv_headings() {
322 $headings = array();
323 self::csv_headings( $headings );
324 $headings = apply_filters(
325 'frm_csv_columns',
326 $headings,
327 self::$form_id,
328 array_merge(
329 self::get_standard_filter_args(),
330 array( 'fields' => self::$fields )
331 )
332 );
333 self::$headings = $headings;
334
335 self::print_csv_row( $headings );
336 }
337
338 /**
339 * @param object $col
340 *
341 * @return array
342 */
343 private static function field_headings( $col ) {
344 $field_type_obj = FrmFieldFactory::get_field_factory( $col );
345
346 if ( ! empty( $field_type_obj->is_combo_field ) ) {
347 // This is combo field.
348 return $field_type_obj->get_export_headings();
349 }
350
351 $field_headings = array();
352 $separate_values = array( 'user_id', 'file', 'data', 'date' );
353
354 if ( ! empty( $col->field_options['separate_value'] ) && ! in_array( $col->type, $separate_values, true ) ) {
355 $field_headings[ $col->id . '_label' ] = strip_tags( $col->name . ' ' . __( '(label)', 'formidable' ) );
356 }
357
358 if ( ! empty( $field_headings[ $col->id . '_label' ] ) ) {
359 $field_headings[ $col->id ] = strip_tags( $col->name . ' ' . __( '(value)', 'formidable' ) );
360 } else {
361 $field_headings[ $col->id ] = strip_tags( $col->name );
362 }
363
364 return apply_filters(
365 'frm_csv_field_columns',
366 $field_headings,
367 array_merge(
368 self::get_standard_filter_args(),
369 array( 'field' => $col )
370 )
371 );
372 }
373
374 /**
375 * @param array $headings
376 *
377 * @return void
378 */
379 private static function csv_headings( &$headings ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
380 $fields_by_repeater_id = array();
381 $repeater_ids = array();
382
383 foreach ( self::$fields as $col ) {
384 if ( self::is_the_child_of_a_repeater( $col ) ) {
385 $repeater_id = $col->field_options['in_section'];
386 // Set a placeholder to maintain order for repeater fields.
387 $headings[ 'repeater' . $repeater_id ] = array();
388
389 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
390 $fields_by_repeater_id[ $repeater_id ] = array();
391 $repeater_ids[] = $repeater_id;
392 }
393
394 $fields_by_repeater_id[ $repeater_id ][] = $col;
395
396 continue;
397 }
398
399 $headings += self::field_headings( $col );
400 }
401 unset( $repeater_id, $col );
402
403 if ( $repeater_ids ) {
404 $where = array( 'field_id' => $repeater_ids );
405 $repeater_meta = FrmDb::get_results( 'frm_item_metas', $where, 'field_id, meta_value' );
406 $max = array_fill_keys( $repeater_ids, 0 );
407
408 foreach ( $repeater_meta as $row ) {
409 $start = strpos( $row->meta_value, 'a:' ) + 2;
410 $end = strpos( $row->meta_value, ':{' );
411 $length = substr( $row->meta_value, $start, $end - $start );
412
413 if ( $length > $max[ $row->field_id ] ) {
414 $max[ $row->field_id ] = $length;
415 }
416 }
417 unset( $start, $end, $length, $row, $repeater_meta, $where );
418
419 $flat = array();
420
421 foreach ( $headings as $key => $heading ) {
422 if ( is_array( $heading ) ) {
423 $repeater_id = str_replace( 'repeater', '', $key );
424 $repeater_headings = array();
425
426 foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
427 $repeater_headings += self::field_headings( $col );
428 }
429
430 for ( $i = 0; $i < $max[ $repeater_id ]; $i++ ) {
431 foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
432 $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
433 }
434 }
435 } else {
436 $flat[ $key ] = $heading;
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_ids = array_keys( $metas );
608 $field_id = end( $field_ids );
609 $field = self::get_field( $field_id );
610
611 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
612 return $metas;
613 }
614
615 $repeater_id = $field->field_options['in_section'];
616
617 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
618 return $metas;
619 }
620
621 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
622 if ( ! isset( $metas[ $repeater_child->id ] ) ) {
623 $metas[ $repeater_child->id ] = '';
624
625 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
626 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] = array();
627 }
628
629 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
630 }
631 }
632
633 return $metas;
634 }
635
636 /**
637 * @param int|string $field_id
638 *
639 * @return false|object
640 */
641 private static function get_field( $field_id ) {
642 $field_id = (int) $field_id;
643
644 foreach ( self::$fields as $field ) {
645 if ( $field_id === (int) $field->id ) {
646 return $field;
647 }
648 }
649
650 return false;
651 }
652
653 /**
654 * @param array $row
655 *
656 * @return void
657 */
658 private static function add_field_values_to_csv( &$row ) {
659 foreach ( self::$fields as $col ) {
660 $field_value = self::$entry->metas[ $col->id ] ?? false;
661
662 FrmFieldsHelper::prepare_field_value( $field_value, $col->type );
663 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
664
665 $field_value = apply_filters(
666 'frm_csv_value',
667 $field_value,
668 array(
669 'field' => $col,
670 'entry' => self::$entry,
671 'separator' => self::$separator,
672 'context' => self::$context,
673 )
674 );
675
676 if ( ! empty( $col->field_options['separate_value'] ) ) {
677 $label_key = $col->id . '_label';
678
679 if ( self::is_the_child_of_a_repeater( $col ) ) {
680 $row[ $label_key ] = array();
681
682 if ( is_array( $field_value ) ) {
683 foreach ( $field_value as $value ) {
684 $row[ $label_key ][] = self::get_separate_value_label( $value, $col );
685 }
686 }
687 } else {
688 $row[ $label_key ] = self::get_separate_value_label( $field_value, $col );
689 }
690 unset( $label_key );
691 }
692
693 $row[ $col->id ] = $field_value;
694
695 unset( $col, $field_value );
696 }//end foreach
697 }
698
699 /**
700 * @since 5.0.06
701 *
702 * @param mixed $field_value
703 * @param stdClass $field
704 *
705 * @return string
706 */
707 private static function get_separate_value_label( $field_value, $field ) {
708 return FrmEntriesHelper::display_value(
709 $field_value,
710 $field,
711 array(
712 'type' => $field->type,
713 'post_id' => self::$entry->post_id,
714 'show_icon' => false,
715 'entry_id' => self::$entry->id,
716 'sep' => self::$separator,
717 '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
718 )
719 );
720 }
721
722 /**
723 * @since 2.0.23
724 *
725 * @param array $row
726 * @param array $atts
727 *
728 * @return void
729 */
730 private static function add_array_values_to_columns( &$row, $atts ) {
731 if ( is_array( $atts['field_value'] ) ) {
732 foreach ( $atts['field_value'] as $key => $sub_value ) {
733 if ( is_array( $sub_value ) ) {
734 // This is combo field inside repeater. The heading key has this format: [86_first[0]].
735 foreach ( $sub_value as $sub_key => $sub_sub_value ) {
736 $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
737
738 if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
739 $row[ $column_key ] = $sub_sub_value;
740 }
741 }
742
743 continue;
744 }
745
746 $column_key = $atts['col']->id . '_' . $key;
747
748 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
749 $row[ $column_key ] = $sub_value;
750 }
751 }
752 }//end if
753 }
754
755 /**
756 * @param array $row
757 *
758 * @return void
759 */
760 private static function add_entry_data_to_csv( &$row ) {
761 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
762 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
763 $row['user_id'] = self::$entry->user_id;
764 $row['updated_by'] = self::$entry->updated_by;
765 $row['is_draft'] = self::$entry->is_draft;
766 $row['ip'] = self::$entry->ip;
767 $row['id'] = self::$entry->id;
768 $row['item_key'] = self::$entry->item_key;
769
770 if ( self::has_parent_id() ) {
771 $row['parent_id'] = self::$entry->parent_item_id;
772 }
773 }
774
775 /**
776 * @param array $rows
777 *
778 * @return void
779 */
780 private static function print_csv_row( $rows ) {
781 $sep = '';
782 $echo = 'echo' === self::$mode;
783
784 foreach ( self::$headings as $k => $heading ) {
785 if ( isset( $rows[ $k ] ) ) {
786 $row = $rows[ $k ];
787 } else {
788 $row = '';
789
790 // array indexed data is not at $rows[ $k ]
791 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
792 $start = strrpos( $k, '[' );
793 $key = substr( $k, 0, $start++ );
794 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
795
796 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
797 $row = $rows[ $key ][ $index ];
798 }
799
800 unset( $start, $key, $index );
801 }
802 }
803
804 if ( is_array( $row ) ) {
805 // implode the repeated field values
806 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
807 }
808
809 $val = self::encode_value( $row );
810
811 if ( 'return' !== self::$line_break ) {
812 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
813 }
814
815 if ( $echo ) {
816 echo $sep . '"' . $val . '"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
817 } else {
818 fwrite( self::$fp, $sep . '"' . $val . '"' );
819 }
820
821 $sep = self::$column_separator;
822
823 unset( $k, $row );
824 }//end foreach
825
826 if ( $echo ) {
827 echo "\n";
828 } else {
829 fwrite( self::$fp, "\n" );
830 }
831 }
832
833 /**
834 * @param string $line
835 *
836 * @return string
837 */
838 public static function encode_value( $line ) {
839 if ( '' === $line ) {
840 return $line;
841 }
842
843 $convmap = false;
844
845 switch ( self::$to_encoding ) {
846 case 'macintosh':
847 // this map was derived from the differences between the MacRoman and UTF-8 Charsets
848 // Reference:
849 // http://www.alanwood.net/demos/macroman.html.
850 $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
851 break;
852 case 'ISO-8859-1':
853 $convmap = array( 256, 10000, 0, 0xffff );
854 }
855
856 if ( is_array( $convmap ) ) {
857 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
858 }
859
860 if ( self::$to_encoding !== self::$charset ) {
861 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
862 }
863
864 return self::escape_csv( $line );
865 }
866
867 /**
868 * Escape a " in a csv with another "
869 *
870 * @since 2.0
871 *
872 * @param mixed $value
873 *
874 * @return mixed
875 */
876 public static function escape_csv( $value ) {
877 if ( ! is_string( $value ) ) {
878 return $value;
879 }
880
881 if ( '=' === $value[0] ) {
882 // escape the = to prevent vulnerability
883 $value = "'" . $value;
884 }
885
886 return str_replace( '"', '""', $value );
887 }
888 }
889