PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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
← All changes | classes/helpers/FrmCSVExportHelper.php +298 -110 6.4.2 → trunk View file →
@@ -5,102 +5,128 @@
5 5
6 6 class FrmCSVExportHelper {
7 7
8 8 /**
9 - * @var string $separator
9 + * @var string
10 10 */
11 11 protected static $separator = ', ';
12 12
13 13 /**
14 - * @var string $column_separator
14 + * @var string
15 15 */
16 16 protected static $column_separator = ',';
17 17
18 18 /**
19 - * @var string $line_break
19 + * @var string
20 20 */
21 21 protected static $line_break = 'return';
22 22
23 23 /**
24 - * @var string $charset
24 + * @var string
25 25 */
26 26 protected static $charset = 'UTF-8';
27 27
28 28 /**
29 - * @var string $to_encoding
29 + * @var string
30 30 */
31 31 protected static $to_encoding = 'UTF-8';
32 32
33 33 /**
34 - * @var string $wp_date_format
34 + * @var string
35 35 */
36 36 protected static $wp_date_format = 'Y-m-d H:i:s';
37 37
38 38 /**
39 - * @var int $comment_count
39 + * @var int
40 40 */
41 41 protected static $comment_count = 0;
42 42
43 43 /**
44 - * @var int $form_id
44 + * @var int
45 45 */
46 46 protected static $form_id = 0;
47 47
48 48 /**
49 - * @var array $headings
49 + * @var array
50 50 */
51 51 protected static $headings = array();
52 52
53 53 /**
54 - * @var array $fields
54 + * @var array
55 55 */
56 56 protected static $fields = array();
57 57
58 58 /**
59 - * @var stdClass|null $entry
59 + * @var stdClass|null
60 60 */
61 61 protected static $entry;
62 62
63 63 /**
64 - * @var bool|null $has_parent_id
64 + * @var bool|null
65 65 */
66 66 protected static $has_parent_id;
67 67
68 68 /**
69 - * @var array|null $fields_by_repeater_id
69 + * @var array|null
70 70 */
71 71 protected static $fields_by_repeater_id;
72 72
73 73 /**
74 - * @var string $mode either 'echo' or 'file' are supported.
74 + * @var string either 'echo' or 'file' are supported.
75 75 */
76 76 protected static $mode = 'echo';
77 77
78 78 /**
79 - * @var resource|null $fp used to write a CSV file in file mode.
79 + * @var resource|null used to write a CSV file in file mode.
80 80 */
81 81 protected static $fp;
82 82
83 83 /**
84 - * @var string $context the context of the CSV being generated. Possible values include 'email' when used as an email attachment, or 'default'.
84 + * @var string the context of the CSV being generated. Possible values include 'email' when used as an email attachment, or 'default'.
85 85 */
86 86 protected static $context = 'default';
87 87
88 88 /**
89 - * @var array $meta
89 + * @var array
90 90 */
91 91 protected static $meta = array();
92 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 + */
93 110 public static function csv_format_options() {
94 111 $formats = array( 'UTF-8', 'ISO-8859-1', 'windows-1256', 'windows-1251', 'macintosh' );
95 - $formats = apply_filters( 'frm_csv_format_options', $formats );
96 112
97 - return $formats;
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 );
98 123 }
99 124
100 125 /**
101 126 * @param array $atts
102 - * @return string|false|void returns a string file path or false if $atts['mode'] is set to 'file'.
127 + *
128 + * @return false|string|null returns a string file path or false if $atts['mode'] is set to 'file'.
103 129 */
104 130 public static function generate_csv( $atts ) {
105 131 global $frm_vars;
106 132 $frm_vars['prevent_caching'] = true;
@@ -114,13 +140,13 @@
114 140 self::set_class_parameters();
115 141 self::set_has_parent_id( $atts['form'] );
116 142
117 143 $filename = self::generate_csv_filename( $atts['form'] );
118 - unset( $atts['form'], $atts['form_cols'] );
119 144
120 145 if ( 'file' === self::$mode ) {
121 146 $filepath = get_temp_dir() . $filename;
122 147 self::$fp = @fopen( $filepath, 'w' );
148 +
123 149 if ( ! self::$fp ) {
124 150 return false;
125 151 }
126 152 } elseif ( 'echo' === self::$mode ) {
@@ -128,14 +154,14 @@
128 154 }
129 155
130 156 unset( $filename );
131 157
132 - $comment_count = FrmDb::get_count(
158 + self::$comment_count = FrmDb::get_count(
133 159 'frm_item_metas',
134 160 array(
135 161 'item_id' => $atts['entry_ids'],
136 162 'field_id' => 0,
137 - 'meta_value like' => '{',
163 + 'meta_value like' => 's:7:"comment";',
138 164 ),
139 165 array(
140 166 'group_by' => 'item_id',
141 167 'order_by' => 'count(*) DESC',
@@ -141,27 +167,65 @@
141 167 'order_by' => 'count(*) DESC',
142 168 'limit' => 1,
143 169 )
144 170 );
145 - self::$comment_count = $comment_count;
146 171
147 172 self::prepare_csv_headings();
148 173
149 - // fetch 20 posts at a time rather than loading the entire table into memory
150 - while ( $next_set = array_splice( $atts['entry_ids'], 0, 20 ) ) {
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 ) ) {
151 187 self::prepare_next_csv_rows( $next_set );
152 188 }
153 189
190 + self::after_generate_csv( $atts );
191 +
192 + unset( $atts['form'], $atts['form_cols'] );
193 +
154 194 if ( 'file' === self::$mode ) {
155 195 fclose( self::$fp );
156 196 return $filepath;
157 197 }
198 +
199 + return null;
158 200 }
159 201
160 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 + /**
161 224 * @since 5.0.16
162 225 *
163 226 * @param stdClass $form
227 + *
164 228 * @return string
165 229 */
166 230 private static function generate_csv_filename( $form ) {
167 231 $filename = gmdate( 'ymdHis', time() ) . '_' . sanitize_title_with_dashes( $form->name ) . '_formidable_entries.csv';
@@ -179,8 +243,11 @@
179 243 'meta' => self::$meta,
180 244 );
181 245 }
182 246
247 + /**
248 + * @return void
249 + */
183 250 private static function set_class_parameters() {
184 251 $args = self::get_standard_filter_args();
185 252 self::$separator = apply_filters( 'frm_csv_sep', self::$separator, $args );
186 253 self::$line_break = apply_filters( 'frm_csv_line_break', self::$line_break, $args );
@@ -187,17 +254,28 @@
187 254 self::$wp_date_format = apply_filters( 'frm_csv_date_format', self::$wp_date_format, $args );
188 255 self::get_csv_format();
189 256 self::$charset = get_option( 'blog_charset' );
190 257
191 - $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
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;
192 260
193 261 self::$column_separator = apply_filters( 'frm_csv_column_sep', $col_sep, $args );
194 262 }
195 263
264 + /**
265 + * @param object $form
266 + *
267 + * @return void
268 + */
196 269 private static function set_has_parent_id( $form ) {
197 270 self::$has_parent_id = $form->parent_form_id > 0;
198 271 }
199 272
273 + /**
274 + * @param string $filename
275 + *
276 + * @return void
277 + */
200 278 private static function print_file_headers( $filename ) {
201 279 header( 'Content-Description: File Transfer' );
202 280 header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' );
203 281 header( 'Content-Type: text/csv; charset=' . self::$charset, true );
@@ -205,8 +283,12 @@
205 283 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
206 284 header( 'Cache-Control: no-cache, must-revalidate' );
207 285 header( 'Pragma: no-cache' );
208 286
287 + if ( self::$include_bom ) {
288 + echo esc_html( chr( 239 ) . chr( 187 ) . chr( 191 ) );
289 + }
290 +
209 291 do_action(
210 292 'frm_csv_headers',
211 293 array(
212 294 'form_id' => self::$form_id,
@@ -214,14 +296,28 @@
214 296 )
215 297 );
216 298 }
217 299
300 + /**
301 + * Check the POST request data for the CSV format to use.
302 + *
303 + * @return void
304 + */
218 305 public static function get_csv_format() {
219 - $csv_format = FrmAppHelper::get_post_param( 'csv_format', 'UTF-8', 'sanitize_text_field' );
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 +
220 313 $csv_format = apply_filters( 'frm_csv_format', $csv_format, self::get_standard_filter_args() );
221 314 self::$to_encoding = $csv_format;
222 315 }
223 316
317 + /**
318 + * @return void
319 + */
224 320 private static function prepare_csv_headings() {
225 321 $headings = array();
226 322 self::csv_headings( $headings );
227 323 $headings = apply_filters(
@@ -237,22 +333,35 @@
237 333
238 334 self::print_csv_row( $headings );
239 335 }
240 336
337 + /**
338 + * @param object $col
339 + *
340 + * @return array
341 + */
241 342 private static function field_headings( $col ) {
242 343 $field_type_obj = FrmFieldFactory::get_field_factory( $col );
243 - if ( ! empty( $field_type_obj->is_combo_field ) ) { // This is combo field.
344 +
345 + if ( ! empty( $field_type_obj->is_combo_field ) ) {
346 + // This is combo field.
244 347 return $field_type_obj->get_export_headings();
245 348 }
246 349
247 350 $field_headings = array();
248 351 $separate_values = array( 'user_id', 'file', 'data', 'date' );
352 +
249 353 if ( ! empty( $col->field_options['separate_value'] ) && ! in_array( $col->type, $separate_values, true ) ) {
250 354 $field_headings[ $col->id . '_label' ] = strip_tags( $col->name . ' ' . __( '(label)', 'formidable' ) );
251 355 }
252 356
253 - $field_headings[ $col->id ] = strip_tags( $col->name );
254 - $field_headings = apply_filters(
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(
255 364 'frm_csv_field_columns',
256 365 $field_headings,
257 366 array_merge(
258 367 self::get_standard_filter_args(),
@@ -258,20 +367,24 @@
258 367 self::get_standard_filter_args(),
259 368 array( 'field' => $col )
260 369 )
261 370 );
262 -
263 - return $field_headings;
264 371 }
265 372
266 - private static function csv_headings( &$headings ) {
373 + /**
374 + * @param array $headings
375 + *
376 + * @return void
377 + */
378 + private static function csv_headings( &$headings ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
267 379 $fields_by_repeater_id = array();
268 380 $repeater_ids = array();
269 381
270 382 foreach ( self::$fields as $col ) {
271 383 if ( self::is_the_child_of_a_repeater( $col ) ) {
272 - $repeater_id = $col->field_options['in_section'];
273 - $headings[ 'repeater' . $repeater_id ] = array(); // set a placeholder to maintain order for repeater fields
384 + $repeater_id = $col->field_options['in_section'];
385 + // Set a placeholder to maintain order for repeater fields.
386 + $headings[ 'repeater' . $repeater_id ] = array();
274 387
275 388 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
276 389 $fields_by_repeater_id[ $repeater_id ] = array();
277 390 $repeater_ids[] = $repeater_id;
@@ -302,24 +415,26 @@
302 415 }
303 416 unset( $start, $end, $length, $row, $repeater_meta, $where );
304 417
305 418 $flat = array();
419 +
306 420 foreach ( $headings as $key => $heading ) {
307 - if ( is_array( $heading ) ) {
308 - $repeater_id = str_replace( 'repeater', '', $key );
421 + if ( ! is_array( $heading ) ) {
422 + $flat[ $key ] = $heading;
423 + continue;
424 + }
309 425
310 - $repeater_headings = array();
311 - foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
312 - $repeater_headings += self::field_headings( $col );
313 - }
426 + $repeater_id = str_replace( 'repeater', '', $key );
427 + $repeater_headings = array();
314 428
315 - for ( $i = 0; $i < $max[ $repeater_id ]; $i ++ ) {
316 - foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
317 - $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
318 - }
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;
319 436 }
320 - } else {
321 - $flat[ $key ] = $heading;
322 437 }
323 438 }
324 439
325 440 self::$fields_by_repeater_id = $fields_by_repeater_id;
@@ -327,12 +442,12 @@
327 442 unset( $key, $heading, $max, $repeater_headings, $repeater_id, $fields_by_repeater_id );
328 443
329 444 $headings = $flat;
330 445 unset( $flat );
331 - }
446 + }//end if
332 447
333 448 if ( self::$comment_count ) {
334 - for ( $i = 0; $i < self::$comment_count; $i ++ ) {
449 + for ( $i = 0; $i < self::$comment_count; $i++ ) {
335 450 $headings[ 'comment' . $i ] = __( 'Comment', 'formidable' );
336 451 $headings[ 'comment_user_id' . $i ] = __( 'Comment User', 'formidable' );
337 452 $headings[ 'comment_created_at' . $i ] = __( 'Comment Date', 'formidable' );
338 453 }
@@ -346,8 +461,9 @@
346 461 $headings['is_draft'] = __( 'Entry Status', 'formidable' );
347 462 $headings['ip'] = __( 'IP', 'formidable' );
348 463 $headings['id'] = __( 'ID', 'formidable' );
349 464 $headings['item_key'] = __( 'Key', 'formidable' );
465 +
350 466 if ( self::has_parent_id() ) {
351 467 $headings['parent_id'] = __( 'Parent ID', 'formidable' );
352 468 }
353 469
@@ -355,8 +471,9 @@
355 471 }
356 472
357 473 /**
358 474 * @param object $field
475 + *
359 476 * @return bool
360 477 */
361 478 private static function is_the_child_of_a_repeater( $field ) {
362 479 if ( $field->form_id === self::$form_id || empty( $field->field_options['in_section'] ) ) {
@@ -365,40 +482,58 @@
365 482
366 483 $section_id = $field->field_options['in_section'];
367 484 $section = FrmField::getOne( $section_id );
368 485
369 - if ( ! $section ) {
370 - return false;
371 - }
372 -
373 - return FrmField::is_repeating_field( $section );
486 + return $section && FrmField::is_repeating_field( $section );
374 487 }
375 488
489 + /**
490 + * @return bool
491 + */
376 492 private static function has_parent_id() {
377 493 return self::$has_parent_id;
378 494 }
379 495
496 + /**
497 + * @param array $next_set
498 + *
499 + * @return void
500 + */
380 501 private static function prepare_next_csv_rows( $next_set ) {
381 - // order by parent_item_id so children will be first
382 - $where = array(
383 - 'or' => 1,
384 - 'id' => $next_set,
385 - 'parent_item_id' => $next_set,
386 - );
387 - $entries = FrmEntry::getAll( $where, ' ORDER BY parent_item_id DESC', '', true, false );
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 + }
388 518
389 - foreach ( $entries as $k => $entry ) {
519 + $entries = FrmEntry::getAll( $where, $order_by, '', true, false );
520 +
521 + foreach ( $entries as $entry ) {
390 522 self::$entry = $entry;
391 523 unset( $entry );
392 524
393 - if ( self::$entry->form_id !== self::$form_id ) {
525 + if ( self::$entry->form_id === self::$form_id ) {
526 + self::prepare_csv_row();
527 + } else {
394 528 self::add_repeat_field_values_to_csv( $entries );
395 - } else {
396 - self::prepare_csv_row();
397 529 }
398 530 }
399 531 }
400 532
533 + /**
534 + * @return void
535 + */
401 536 private static function prepare_csv_row() {
402 537 $row = array();
403 538 self::add_field_values_to_csv( $row );
404 539 self::add_entry_data_to_csv( $row );
@@ -414,15 +549,20 @@
414 549 );
415 550 self::print_csv_row( $row );
416 551 }
417 552
553 + /**
554 + * @param array $entries
555 + *
556 + * @return void
557 + */
418 558 private static function add_repeat_field_values_to_csv( &$entries ) {
419 559 if ( isset( self::$entry->metas ) ) {
420 - // add child entries to the parent
560 + // Add child entries to the parent
421 561 foreach ( self::$entry->metas as $meta_id => $meta_value ) {
422 562 if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
423 - // if the hook is being used to include field keys in the metas array,
424 - // we need to skip the keys and only process field ids
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
425 565 continue;
426 566 }
427 567
428 568 if ( ! isset( $entries[ self::$entry->parent_item_id ] ) ) {
@@ -432,25 +572,26 @@
432 572
433 573 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
434 574 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
435 575 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
436 - // if the data is here, it should be an array but if this field has collected data
437 - // both while inside and outside of the repeating section, it's possible this is a string
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.
438 578 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
439 579 }
440 580
441 - //add the repeated values
581 + // Add the repeated values.
442 582 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
443 - }
583 + }//end foreach
444 584
445 585 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
446 586 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
447 - }
587 + }//end if
448 588
449 - // add the embedded form id
589 + // Add the embedded form id
450 590 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
451 591 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
452 592 }
593 +
453 594 $entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
454 595 }
455 596
456 597 /**
@@ -458,14 +599,14 @@
458 599 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
459 600 *
460 601 * @param array $metas
461 602 * @param array $entries
603 + *
462 604 * @return array
463 605 */
464 606 private static function fill_missing_repeater_metas( $metas, &$entries ) {
465 - $field_ids = array_keys( $metas );
466 - $field_id = end( $field_ids );
467 - $field = self::get_field( $field_id );
607 + $field_id = array_key_last( $metas );
608 + $field = self::get_field( $field_id );
468 609
469 610 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
470 611 return $metas;
471 612 }
@@ -470,40 +611,55 @@
470 611 return $metas;
471 612 }
472 613
473 614 $repeater_id = $field->field_options['in_section'];
615 +
474 616 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
475 617 return $metas;
476 618 }
477 619
478 620 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
479 - if ( ! isset( $metas[ $repeater_child->id ] ) ) {
480 - $metas[ $repeater_child->id ] = '';
621 + if ( isset( $metas[ $repeater_child->id ] ) ) {
622 + continue;
623 + }
481 624
482 - if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) || ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] ) ) {
483 - $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ] = array();
484 - }
625 + $metas[ $repeater_child->id ] = '';
485 626
486 - $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
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();
487 629 }
630 +
631 + $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
488 632 }
489 633
490 634 return $metas;
491 635 }
492 636
637 + /**
638 + * @param int|string $field_id
639 + *
640 + * @return false|object
641 + */
493 642 private static function get_field( $field_id ) {
494 643 $field_id = (int) $field_id;
644 +
495 645 foreach ( self::$fields as $field ) {
496 646 if ( $field_id === (int) $field->id ) {
497 647 return $field;
498 648 }
499 649 }
650 +
500 651 return false;
501 652 }
502 653
654 + /**
655 + * @param array $row
656 + *
657 + * @return void
658 + */
503 659 private static function add_field_values_to_csv( &$row ) {
504 660 foreach ( self::$fields as $col ) {
505 - $field_value = isset( self::$entry->metas[ $col->id ] ) ? self::$entry->metas[ $col->id ] : false;
661 + $field_value = self::$entry->metas[ $col->id ] ?? false;
506 662
507 663 FrmFieldsHelper::prepare_field_value( $field_value, $col->type );
508 664 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
509 665
@@ -519,8 +675,9 @@
519 675 );
520 676
521 677 if ( ! empty( $col->field_options['separate_value'] ) ) {
522 678 $label_key = $col->id . '_label';
679 +
523 680 if ( self::is_the_child_of_a_repeater( $col ) ) {
524 681 $row[ $label_key ] = array();
525 682
526 683 if ( is_array( $field_value ) ) {
@@ -536,9 +693,9 @@
536 693
537 694 $row[ $col->id ] = $field_value;
538 695
539 696 unset( $col, $field_value );
540 - }
697 + }//end foreach
541 698 }
542 699
543 700 /**
544 701 * @since 5.0.06
@@ -544,8 +701,9 @@
544 701 * @since 5.0.06
545 702 *
546 703 * @param mixed $field_value
547 704 * @param stdClass $field
705 + *
548 706 * @return string
549 707 */
550 708 private static function get_separate_value_label( $field_value, $field ) {
551 709 return FrmEntriesHelper::display_value(
@@ -556,9 +714,9 @@
556 714 'post_id' => self::$entry->post_id,
557 715 'show_icon' => false,
558 716 'entry_id' => self::$entry->id,
559 717 'sep' => self::$separator,
560 - '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,
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
561 719 )
562 720 );
563 721 }
564 722
@@ -563,32 +721,46 @@
563 721 }
564 722
565 723 /**
566 724 * @since 2.0.23
725 + *
726 + * @param array $row
727 + * @param array $atts
728 + *
729 + * @return void
567 730 */
568 731 private static function add_array_values_to_columns( &$row, $atts ) {
569 - if ( is_array( $atts['field_value'] ) ) {
570 - foreach ( $atts['field_value'] as $key => $sub_value ) {
571 - if ( is_array( $sub_value ) ) {
572 - // This is combo field inside repeater. The heading key has this format: [86_first[0]].
573 - foreach ( $sub_value as $sub_key => $sub_sub_value ) {
574 - $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
575 - if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
576 - $row[ $column_key ] = $sub_sub_value;
577 - }
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;
578 744 }
745 + }
579 746
580 - continue;
581 - }
747 + continue;
748 + }
582 749
583 - $column_key = $atts['col']->id . '_' . $key;
584 - if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
585 - $row[ $column_key ] = $sub_value;
586 - }
750 + $column_key = $atts['col']->id . '_' . $key;
751 +
752 + if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
753 + $row[ $column_key ] = $sub_value;
587 754 }
588 755 }
589 756 }
590 757
758 + /**
759 + * @param array $row
760 + *
761 + * @return void
762 + */
591 763 private static function add_entry_data_to_csv( &$row ) {
592 764 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
593 765 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
594 766 $row['user_id'] = self::$entry->user_id;
@@ -596,13 +768,19 @@
596 768 $row['is_draft'] = self::$entry->is_draft;
597 769 $row['ip'] = self::$entry->ip;
598 770 $row['id'] = self::$entry->id;
599 771 $row['item_key'] = self::$entry->item_key;
772 +
600 773 if ( self::has_parent_id() ) {
601 774 $row['parent_id'] = self::$entry->parent_item_id;
602 775 }
603 776 }
604 777
778 + /**
779 + * @param array $rows
780 + *
781 + * @return void
782 + */
605 783 private static function print_csv_row( $rows ) {
606 784 $sep = '';
607 785 $echo = 'echo' === self::$mode;
608 786
@@ -610,12 +788,13 @@
610 788 if ( isset( $rows[ $k ] ) ) {
611 789 $row = $rows[ $k ];
612 790 } else {
613 791 $row = '';
792 +
614 793 // array indexed data is not at $rows[ $k ]
615 794 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
616 795 $start = strrpos( $k, '[' );
617 - $key = substr( $k, 0, $start ++ );
796 + $key = substr( $k, 0, $start++ );
618 797 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
619 798
620 799 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
621 800 $row = $rows[ $key ][ $index ];
@@ -625,13 +804,14 @@
625 804 }
626 805 }
627 806
628 807 if ( is_array( $row ) ) {
629 - // implode the repeated field values
808 + // Implode the repeated field values
630 809 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
631 810 }
632 811
633 812 $val = self::encode_value( $row );
813 +
634 814 if ( 'return' !== self::$line_break ) {
635 815 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
636 816 }
637 817
@@ -639,12 +819,14 @@
639 819 echo $sep . '"' . $val . '"'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
640 820 } else {
641 821 fwrite( self::$fp, $sep . '"' . $val . '"' );
642 822 }
823 +
643 824 $sep = self::$column_separator;
644 825
645 826 unset( $k, $row );
646 - }
827 + }//end foreach
828 +
647 829 if ( $echo ) {
648 830 echo "\n";
649 831 } else {
650 832 fwrite( self::$fp, "\n" );
@@ -650,8 +832,13 @@
650 832 fwrite( self::$fp, "\n" );
651 833 }
652 834 }
653 835
836 + /**
837 + * @param string $line
838 + *
839 + * @return string
840 + */
654 841 public static function encode_value( $line ) {
655 842 if ( '' === $line ) {
656 843 return $line;
657 844 }
@@ -659,12 +846,12 @@
659 846 $convmap = false;
660 847
661 848 switch ( self::$to_encoding ) {
662 849 case 'macintosh':
663 - // this map was derived from the differences between the MacRoman and UTF-8 Charsets
850 + // This map was derived from the differences between the MacRoman and UTF-8 Charsets
664 851 // Reference:
665 - // - http://www.alanwood.net/demos/macroman.html
666 - $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 );
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
667 854 break;
668 855 case 'ISO-8859-1':
669 856 $convmap = array( 256, 10000, 0, 0xffff );
670 857 }
@@ -683,9 +870,11 @@
683 870 /**
684 871 * Escape a " in a csv with another "
685 872 *
686 873 * @since 2.0
874 + *
687 875 * @param mixed $value
876 + *
688 877 * @return mixed
689 878 */
690 879 public static function escape_csv( $value ) {
691 880 if ( ! is_string( $value ) ) {
@@ -695,9 +884,8 @@
695 884 if ( '=' === $value[0] ) {
696 885 // escape the = to prevent vulnerability
697 886 $value = "'" . $value;
698 887 }
699 - $value = str_replace( '"', '""', $value );
700 888
701 - return $value;
889 + return str_replace( '"', '""', $value );
702 890 }
703 891 }