PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.22.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.22.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
← All changes | classes/helpers/FrmCSVExportHelper.php +137 -48 6.36.22.1 View file →
@@ -5,94 +5,120 @@
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' );
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 + if ( ! $on_global_settings_page ) {
118 + array_splice( $formats, 1, 0, 'UTF-8 with BOM' );
119 + }
120 +
95 121 $formats = apply_filters( 'frm_csv_format_options', $formats );
96 122
97 123 return $formats;
98 124 }
@@ -98,9 +124,9 @@
98 124 }
99 125
100 126 /**
101 127 * @param array $atts
102 - * @return string|false|void returns a string file path or false if $atts['mode'] is set to 'file'.
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,9 +140,8 @@
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' );
@@ -133,9 +158,9 @@
133 158 'frm_item_metas',
134 159 array(
135 160 'item_id' => $atts['entry_ids'],
136 161 'field_id' => 0,
137 - 'meta_value like' => '{',
162 + 'meta_value like' => 's:7:"comment";',
138 163 ),
139 164 array(
140 165 'group_by' => 'item_id',
141 166 'order_by' => 'count(*) DESC',
@@ -145,20 +170,56 @@
145 170 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 + * @return void
207 + */
208 + private static function after_generate_csv( $atts ) {
209 + /**
210 + * @since 6.8.4
211 + *
212 + * @param array $atts {
213 + * @type object $form
214 + * @type array $entry_ids
215 + * @type array $form_cols
216 + * }
217 + */
218 + do_action( 'frm_after_generate_csv', $atts );
219 + }
220 +
221 + /**
161 222 * @since 5.0.16
162 223 *
163 224 * @param stdClass $form
164 225 * @return string
@@ -205,8 +266,12 @@
205 266 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
206 267 header( 'Cache-Control: no-cache, must-revalidate' );
207 268 header( 'Pragma: no-cache' );
208 269
270 + if ( self::$include_bom ) {
271 + echo esc_html( chr( 239 ) . chr( 187 ) . chr( 191 ) );
272 + }
273 +
209 274 do_action(
210 275 'frm_csv_headers',
211 276 array(
212 277 'form_id' => self::$form_id,
@@ -214,10 +279,21 @@
214 279 )
215 280 );
216 281 }
217 282
283 + /**
284 + * Check the POST request data for the CSV format to use.
285 + *
286 + * @return void
287 + */
218 288 public static function get_csv_format() {
219 - $csv_format = FrmAppHelper::get_post_param( 'csv_format', 'UTF-8', 'sanitize_text_field' );
289 + $csv_format = FrmAppHelper::get_post_param( 'csv_format', 'UTF-8', 'sanitize_text_field' );
290 +
291 + if ( 'UTF-8 with BOM' === $csv_format ) {
292 + self::$include_bom = true;
293 + $csv_format = 'UTF-8';
294 + }
295 +
220 296 $csv_format = apply_filters( 'frm_csv_format', $csv_format, self::get_standard_filter_args() );
221 297 self::$to_encoding = $csv_format;
222 298 }
223 299
@@ -239,9 +315,10 @@
239 315 }
240 316
241 317 private static function field_headings( $col ) {
242 318 $field_type_obj = FrmFieldFactory::get_field_factory( $col );
243 - if ( ! empty( $field_type_obj->is_combo_field ) ) { // This is combo field.
319 + if ( ! empty( $field_type_obj->is_combo_field ) ) {
320 + // This is combo field.
244 321 return $field_type_obj->get_export_headings();
245 322 }
246 323
247 324 $field_headings = array();
@@ -268,10 +345,11 @@
268 345 $repeater_ids = array();
269 346
270 347 foreach ( self::$fields as $col ) {
271 348 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
349 + $repeater_id = $col->field_options['in_section'];
350 + // Set a placeholder to maintain order for repeater fields.
351 + $headings[ 'repeater' . $repeater_id ] = array();
274 352
275 353 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
276 354 $fields_by_repeater_id[ $repeater_id ] = array();
277 355 $repeater_ids[] = $repeater_id;
@@ -311,9 +389,9 @@
311 389 foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
312 390 $repeater_headings += self::field_headings( $col );
313 391 }
314 392
315 - for ( $i = 0; $i < $max[ $repeater_id ]; $i ++ ) {
393 + for ( $i = 0; $i < $max[ $repeater_id ]; $i++ ) {
316 394 foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
317 395 $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
318 396 }
319 397 }
@@ -327,12 +405,12 @@
327 405 unset( $key, $heading, $max, $repeater_headings, $repeater_id, $fields_by_repeater_id );
328 406
329 407 $headings = $flat;
330 408 unset( $flat );
331 - }
409 + }//end if
332 410
333 411 if ( self::$comment_count ) {
334 - for ( $i = 0; $i < self::$comment_count; $i ++ ) {
412 + for ( $i = 0; $i < self::$comment_count; $i++ ) {
335 413 $headings[ 'comment' . $i ] = __( 'Comment', 'formidable' );
336 414 $headings[ 'comment_user_id' . $i ] = __( 'Comment User', 'formidable' );
337 415 $headings[ 'comment_created_at' . $i ] = __( 'Comment Date', 'formidable' );
338 416 }
@@ -342,9 +420,9 @@
342 420 $headings['created_at'] = __( 'Timestamp', 'formidable' );
343 421 $headings['updated_at'] = __( 'Last Updated', 'formidable' );
344 422 $headings['user_id'] = __( 'Created By', 'formidable' );
345 423 $headings['updated_by'] = __( 'Updated By', 'formidable' );
346 - $headings['is_draft'] = __( 'Draft', 'formidable' );
424 + $headings['is_draft'] = __( 'Entry Status', 'formidable' );
347 425 $headings['ip'] = __( 'IP', 'formidable' );
348 426 $headings['id'] = __( 'ID', 'formidable' );
349 427 $headings['item_key'] = __( 'Key', 'formidable' );
350 428 if ( self::has_parent_id() ) {
@@ -377,17 +455,28 @@
377 455 return self::$has_parent_id;
378 456 }
379 457
380 458 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 );
459 + if ( FrmAppHelper::pro_is_installed() ) {
460 + $where = array(
461 + 'or' => 1,
462 + 'id' => $next_set,
463 + 'parent_item_id' => $next_set,
464 + );
465 + // Order by parent_item_id so children will be first.
466 + $order_by = ' ORDER BY parent_item_id DESC';
467 + } else {
468 + // When Pro is not installed, only query for direct ID matches only as we do not expect parent item id
469 + // matches and the more simplified query is faster.
470 + $where = array(
471 + 'id' => $next_set,
472 + );
473 + $order_by = '';
474 + }
388 475
389 - foreach ( $entries as $k => $entry ) {
476 + $entries = FrmEntry::getAll( $where, $order_by, '', true, false );
477 +
478 + foreach ( $entries as $entry ) {
390 479 self::$entry = $entry;
391 480 unset( $entry );
392 481
393 482 if ( self::$entry->form_id !== self::$form_id ) {
@@ -433,19 +522,19 @@
433 522 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
434 523 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
435 524 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
436 525 // 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
526 + // both while inside and outside of the repeating section, it's possible this is a string.
438 527 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
439 528 }
440 529
441 - //add the repeated values
530 + // Add the repeated values.
442 531 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
443 - }
532 + }//end foreach
444 533
445 534 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
446 535 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
447 - }
536 + }//end if
448 537
449 538 // add the embedded form id
450 539 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
451 540 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
@@ -536,9 +625,9 @@
536 625
537 626 $row[ $col->id ] = $field_value;
538 627
539 628 unset( $col, $field_value );
540 - }
629 + }//end foreach
541 630 }
542 631
543 632 /**
544 633 * @since 5.0.06
@@ -556,9 +645,9 @@
556 645 'post_id' => self::$entry->post_id,
557 646 'show_icon' => false,
558 647 'entry_id' => self::$entry->id,
559 648 '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,
649 + '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,
561 650 )
562 651 );
563 652 }
564 653
@@ -592,9 +681,9 @@
592 681 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
593 682 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
594 683 $row['user_id'] = self::$entry->user_id;
595 684 $row['updated_by'] = self::$entry->updated_by;
596 - $row['is_draft'] = self::$entry->is_draft ? '1' : '0';
685 + $row['is_draft'] = self::$entry->is_draft;
597 686 $row['ip'] = self::$entry->ip;
598 687 $row['id'] = self::$entry->id;
599 688 $row['item_key'] = self::$entry->item_key;
600 689 if ( self::has_parent_id() ) {
@@ -613,9 +702,9 @@
613 702 $row = '';
614 703 // array indexed data is not at $rows[ $k ]
615 704 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
616 705 $start = strrpos( $k, '[' );
617 - $key = substr( $k, 0, $start ++ );
706 + $key = substr( $k, 0, $start++ );
618 707 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
619 708
620 709 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
621 710 $row = $rows[ $key ][ $index ];
@@ -642,9 +731,9 @@
642 731 }
643 732 $sep = self::$column_separator;
644 733
645 734 unset( $k, $row );
646 - }
735 + }//end foreach
647 736 if ( $echo ) {
648 737 echo "\n";
649 738 } else {
650 739 fwrite( self::$fp, "\n" );
@@ -661,9 +750,9 @@
661 750 switch ( self::$to_encoding ) {
662 751 case 'macintosh':
663 752 // this map was derived from the differences between the MacRoman and UTF-8 Charsets
664 753 // Reference:
665 - // - http://www.alanwood.net/demos/macroman.html
754 + // http://www.alanwood.net/demos/macroman.html.
666 755 $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 );
667 756 break;
668 757 case 'ISO-8859-1':
669 758 $convmap = array( 256, 10000, 0, 0xffff );