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

566 lines 18.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 protected static $separator = ', ';
9 protected static $column_separator = ',';
10 protected static $line_break = 'return';
11 protected static $charset = 'UTF-8';
12 protected static $to_encoding = 'UTF-8';
13 protected static $wp_date_format = 'Y-m-d H:i:s';
14 protected static $comment_count = 0;
15 protected static $form_id = 0;
16 protected static $headings = array();
17 protected static $fields = array();
18 protected static $entry;
19 protected static $has_parent_id;
20 protected static $fields_by_repeater_id;
21
22 public static function csv_format_options() {
23 $formats = array( 'UTF-8', 'ISO-8859-1', 'windows-1256', 'windows-1251', 'macintosh' );
24 $formats = apply_filters( 'frm_csv_format_options', $formats );
25
26 return $formats;
27 }
28
29 public static function generate_csv( $atts ) {
30 global $frm_vars;
31 $frm_vars['prevent_caching'] = true;
32
33 self::$fields = $atts['form_cols'];
34 self::$form_id = $atts['form']->id;
35 self::set_class_paramters();
36 self::set_has_parent_id( $atts['form'] );
37
38 $filename = apply_filters( 'frm_csv_filename', gmdate( 'ymdHis', time() ) . '_' . sanitize_title_with_dashes( $atts['form']->name ) . '_formidable_entries.csv', $atts['form'] );
39 unset( $atts['form'], $atts['form_cols'] );
40
41 self::print_file_headers( $filename );
42 unset( $filename );
43
44 $comment_count = FrmDb::get_count(
45 'frm_item_metas',
46 array(
47 'item_id' => $atts['entry_ids'],
48 'field_id' => 0,
49 'meta_value like' => '{',
50 ),
51 array(
52 'group_by' => 'item_id',
53 'order_by' => 'count(*) DESC',
54 'limit' => 1,
55 )
56 );
57 self::$comment_count = $comment_count;
58
59 self::prepare_csv_headings();
60
61 // fetch 20 posts at a time rather than loading the entire table into memory
62 while ( $next_set = array_splice( $atts['entry_ids'], 0, 20 ) ) {
63 self::prepare_next_csv_rows( $next_set );
64 }
65 }
66
67 private static function set_class_paramters() {
68 self::$separator = apply_filters( 'frm_csv_sep', self::$separator );
69 self::$line_break = apply_filters( 'frm_csv_line_break', self::$line_break );
70 self::$wp_date_format = apply_filters( 'frm_csv_date_format', self::$wp_date_format );
71 self::get_csv_format();
72 self::$charset = get_option( 'blog_charset' );
73
74 $col_sep = ( isset( $_POST['csv_col_sep'] ) && ! empty( $_POST['csv_col_sep'] ) ) ? sanitize_text_field( wp_unslash( $_POST['csv_col_sep'] ) ) : self::$column_separator;
75
76 self::$column_separator = apply_filters( 'frm_csv_column_sep', $col_sep );
77 }
78
79 private static function set_has_parent_id( $form ) {
80 self::$has_parent_id = $form->parent_form_id > 0;
81 }
82
83 private static function print_file_headers( $filename ) {
84 header( 'Content-Description: File Transfer' );
85 header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' );
86 header( 'Content-Type: text/csv; charset=' . self::$charset, true );
87 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' );
88 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' );
89 header( 'Cache-Control: no-cache, must-revalidate' );
90 header( 'Pragma: no-cache' );
91
92 do_action(
93 'frm_csv_headers',
94 array(
95 'form_id' => self::$form_id,
96 'fields' => self::$fields,
97 )
98 );
99 }
100
101 public static function get_csv_format() {
102 $csv_format = FrmAppHelper::get_post_param( 'csv_format', 'UTF-8', 'sanitize_text_field' );
103 $csv_format = apply_filters( 'frm_csv_format', $csv_format );
104 self::$to_encoding = $csv_format;
105 }
106
107 private static function prepare_csv_headings() {
108 $headings = array();
109 self::csv_headings( $headings );
110 $headings = apply_filters(
111 'frm_csv_columns',
112 $headings,
113 self::$form_id,
114 array(
115 'fields' => self::$fields,
116 )
117 );
118 self::$headings = $headings;
119
120 self::print_csv_row( $headings );
121 }
122
123 private static function field_headings( $col ) {
124 $field_type_obj = FrmFieldFactory::get_field_factory( $col );
125 if ( ! empty( $field_type_obj->is_combo_field ) ) { // This is combo field.
126 return $field_type_obj->get_export_headings();
127 }
128
129 $field_headings = array();
130 $separate_values = array( 'user_id', 'file', 'data', 'date' );
131 if ( ! empty( $col->field_options['separate_value'] ) && ! in_array( $col->type, $separate_values, true ) ) {
132 $field_headings[ $col->id . '_label' ] = strip_tags( $col->name . ' ' . __( '(label)', 'formidable' ) );
133 }
134
135 $field_headings[ $col->id ] = strip_tags( $col->name );
136 $field_headings = apply_filters(
137 'frm_csv_field_columns',
138 $field_headings,
139 array(
140 'field' => $col,
141 )
142 );
143
144 return $field_headings;
145 }
146
147 private static function csv_headings( &$headings ) {
148 $fields_by_repeater_id = array();
149 $repeater_ids = array();
150
151 foreach ( self::$fields as $col ) {
152 if ( self::is_the_child_of_a_repeater( $col ) ) {
153 $repeater_id = $col->field_options['in_section'];
154 $headings[ 'repeater' . $repeater_id ] = array(); // set a placeholder to maintain order for repeater fields
155
156 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
157 $fields_by_repeater_id[ $repeater_id ] = array();
158 $repeater_ids[] = $repeater_id;
159 }
160
161 $fields_by_repeater_id[ $repeater_id ][] = $col;
162
163 continue;
164 }
165
166 $headings += self::field_headings( $col );
167 }
168 unset( $repeater_id, $col );
169
170 if ( $repeater_ids ) {
171 $where = array( 'field_id' => $repeater_ids );
172 $repeater_meta = FrmDb::get_results( 'frm_item_metas', $where, 'field_id, meta_value' );
173 $max = array_fill_keys( $repeater_ids, 0 );
174
175 foreach ( $repeater_meta as $row ) {
176 $start = strpos( $row->meta_value, 'a:' ) + 2;
177 $end = strpos( $row->meta_value, ':{' );
178 $length = substr( $row->meta_value, $start, $end - $start );
179
180 if ( $length > $max[ $row->field_id ] ) {
181 $max[ $row->field_id ] = $length;
182 }
183 }
184 unset( $start, $end, $length, $row, $repeater_meta, $where );
185
186 $flat = array();
187 foreach ( $headings as $key => $heading ) {
188 if ( is_array( $heading ) ) {
189 $repeater_id = str_replace( 'repeater', '', $key );
190
191 $repeater_headings = array();
192 foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
193 $repeater_headings += self::field_headings( $col );
194 }
195
196 for ( $i = 0; $i < $max[ $repeater_id ]; $i ++ ) {
197 foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
198 $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
199 }
200 }
201 } else {
202 $flat[ $key ] = $heading;
203 }
204 }
205
206 self::$fields_by_repeater_id = $fields_by_repeater_id;
207
208 unset( $key, $heading, $max, $repeater_headings, $repeater_id, $fields_by_repeater_id );
209
210 $headings = $flat;
211 unset( $flat );
212 }
213
214 if ( self::$comment_count ) {
215 for ( $i = 0; $i < self::$comment_count; $i ++ ) {
216 $headings[ 'comment' . $i ] = __( 'Comment', 'formidable' );
217 $headings[ 'comment_user_id' . $i ] = __( 'Comment User', 'formidable' );
218 $headings[ 'comment_created_at' . $i ] = __( 'Comment Date', 'formidable' );
219 }
220 unset( $i );
221 }
222
223 $headings['created_at'] = __( 'Timestamp', 'formidable' );
224 $headings['updated_at'] = __( 'Last Updated', 'formidable' );
225 $headings['user_id'] = __( 'Created By', 'formidable' );
226 $headings['updated_by'] = __( 'Updated By', 'formidable' );
227 $headings['is_draft'] = __( 'Draft', 'formidable' );
228 $headings['ip'] = __( 'IP', 'formidable' );
229 $headings['id'] = __( 'ID', 'formidable' );
230 $headings['item_key'] = __( 'Key', 'formidable' );
231 if ( self::has_parent_id() ) {
232 $headings['parent_id'] = __( 'Parent ID', 'formidable' );
233 }
234
235 $headings = apply_filters( 'frm_export_csv_headings', $headings );
236 }
237
238 /**
239 * @param object $field
240 * @return bool
241 */
242 private static function is_the_child_of_a_repeater( $field ) {
243 if ( $field->form_id === self::$form_id || empty( $field->field_options['in_section'] ) ) {
244 return false;
245 }
246
247 $section_id = $field->field_options['in_section'];
248 $section = FrmField::getOne( $section_id );
249
250 if ( ! $section ) {
251 return false;
252 }
253
254 return FrmField::is_repeating_field( $section );
255 }
256
257 private static function has_parent_id() {
258 return self::$has_parent_id;
259 }
260
261 private static function prepare_next_csv_rows( $next_set ) {
262 // order by parent_item_id so children will be first
263 $where = array(
264 'or' => 1,
265 'id' => $next_set,
266 'parent_item_id' => $next_set,
267 );
268 $entries = FrmEntry::getAll( $where, ' ORDER BY parent_item_id DESC', '', true, false );
269
270 foreach ( $entries as $k => $entry ) {
271 self::$entry = $entry;
272 unset( $entry );
273
274 if ( self::$entry->form_id !== self::$form_id ) {
275 self::add_repeat_field_values_to_csv( $entries );
276 } else {
277 self::prepare_csv_row();
278 }
279 }
280 }
281
282 private static function prepare_csv_row() {
283 $row = array();
284 self::add_field_values_to_csv( $row );
285 self::add_entry_data_to_csv( $row );
286 $row = apply_filters(
287 'frm_csv_row',
288 $row,
289 array(
290 'entry' => self::$entry,
291 'date_format' => self::$wp_date_format,
292 'comment_count' => self::$comment_count,
293 )
294 );
295 self::print_csv_row( $row );
296 }
297
298 private static function add_repeat_field_values_to_csv( &$entries ) {
299 if ( isset( self::$entry->metas ) ) {
300 // add child entries to the parent
301 foreach ( self::$entry->metas as $meta_id => $meta_value ) {
302 if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
303 // if the hook is being used to include field keys in the metas array,
304 // we need to skip the keys and only process field ids
305 continue;
306 }
307
308 if ( ! isset( $entries[ self::$entry->parent_item_id ] ) ) {
309 $entries[ self::$entry->parent_item_id ] = new stdClass();
310 $entries[ self::$entry->parent_item_id ]->metas = array();
311 }
312
313 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
314 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
315 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
316 // if the data is here, it should be an array but if this field has collected data
317 // both while inside and outside of the repeating section, it's possible this is a string
318 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
319 }
320
321 //add the repeated values
322 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
323 }
324
325 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
326 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
327 }
328
329 // add the embedded form id
330 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
331 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
332 }
333 $entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
334 }
335
336 /**
337 * When an empty field is saved, it isn't saved as a meta value
338 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
339 *
340 * @param array $metas
341 * @param array $entries
342 * @return array
343 */
344 private static function fill_missing_repeater_metas( $metas, &$entries ) {
345 $field_ids = array_keys( $metas );
346 $field_id = end( $field_ids );
347 $field = self::get_field( $field_id );
348
349 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
350 return $metas;
351 }
352
353 $repeater_id = $field->field_options['in_section'];
354 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
355 return $metas;
356 }
357
358 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
359 if ( ! isset( $metas[ $repeater_child->id ] ) ) {
360 $metas[ $repeater_child->id ] = '';
361 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
362 }
363 }
364
365 return $metas;
366 }
367
368 private static function get_field( $field_id ) {
369 $field_id = (int) $field_id;
370 foreach ( self::$fields as $field ) {
371 if ( $field_id === (int) $field->id ) {
372 return $field;
373 }
374 }
375 return false;
376 }
377
378 private static function add_field_values_to_csv( &$row ) {
379 foreach ( self::$fields as $col ) {
380 $field_value = isset( self::$entry->metas[ $col->id ] ) ? self::$entry->metas[ $col->id ] : false;
381
382 FrmAppHelper::unserialize_or_decode( $field_value );
383 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
384
385 $field_value = apply_filters(
386 'frm_csv_value',
387 $field_value,
388 array(
389 'field' => $col,
390 'entry' => self::$entry,
391 'separator' => self::$separator,
392 )
393 );
394
395 if ( ! empty( $col->field_options['separate_value'] ) ) {
396 $label_key = $col->id . '_label';
397 if ( self::is_the_child_of_a_repeater( $col ) ) {
398 $row[ $label_key ] = array();
399 foreach ( $field_value as $value ) {
400 $row[ $label_key ][] = self::get_separate_value_label( $value, $col );
401 }
402 } else {
403 $row[ $label_key ] = self::get_separate_value_label( $field_value, $col );
404 }
405 unset( $label_key );
406 }
407
408 $row[ $col->id ] = $field_value;
409
410 unset( $col, $field_value );
411 }
412 }
413
414 /**
415 * @since 5.0.06
416 *
417 * @param mixed $field_value
418 * @param stdClass $field
419 * @return string
420 */
421 private static function get_separate_value_label( $field_value, $field ) {
422 return FrmEntriesHelper::display_value(
423 $field_value,
424 $field,
425 array(
426 'type' => $field->type,
427 'post_id' => self::$entry->post_id,
428 'show_icon' => false,
429 'entry_id' => self::$entry->id,
430 'sep' => self::$separator,
431 '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,
432 )
433 );
434 }
435
436 /**
437 * @since 2.0.23
438 */
439 private static function add_array_values_to_columns( &$row, $atts ) {
440 if ( is_array( $atts['field_value'] ) ) {
441 foreach ( $atts['field_value'] as $key => $sub_value ) {
442 if ( is_array( $sub_value ) ) {
443 // This is combo field inside repeater. The heading key has this format: [86_first[0]].
444 foreach ( $sub_value as $sub_key => $sub_sub_value ) {
445 $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
446 if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
447 $row[ $column_key ] = $sub_sub_value;
448 }
449 }
450
451 continue;
452 }
453
454 $column_key = $atts['col']->id . '_' . $key;
455 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
456 $row[ $column_key ] = $sub_value;
457 }
458 }
459 }
460 }
461
462 private static function add_entry_data_to_csv( &$row ) {
463 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
464 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
465 $row['user_id'] = self::$entry->user_id;
466 $row['updated_by'] = self::$entry->updated_by;
467 $row['is_draft'] = self::$entry->is_draft ? '1' : '0';
468 $row['ip'] = self::$entry->ip;
469 $row['id'] = self::$entry->id;
470 $row['item_key'] = self::$entry->item_key;
471 if ( self::has_parent_id() ) {
472 $row['parent_id'] = self::$entry->parent_item_id;
473 }
474 }
475
476 private static function print_csv_row( $rows ) {
477 $sep = '';
478
479 foreach ( self::$headings as $k => $heading ) {
480 if ( isset( $rows[ $k ] ) ) {
481 $row = $rows[ $k ];
482 } else {
483 $row = '';
484 // array indexed data is not at $rows[ $k ]
485 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
486 $start = strrpos( $k, '[' );
487 $key = substr( $k, 0, $start ++ );
488 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
489
490 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
491 $row = $rows[ $key ][ $index ];
492 }
493
494 unset( $start, $key, $index );
495 }
496 }
497
498 if ( is_array( $row ) ) {
499 // implode the repeated field values
500 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
501 }
502
503 $val = self::encode_value( $row );
504 if ( 'return' !== self::$line_break ) {
505 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
506 }
507
508 echo $sep . '"' . $val . '"'; // WPCS: XSS ok.
509 $sep = self::$column_separator;
510
511 unset( $k, $row );
512 }
513 echo "\n";
514 }
515
516 public static function encode_value( $line ) {
517 if ( '' === $line ) {
518 return $line;
519 }
520
521 $convmap = false;
522
523 switch ( self::$to_encoding ) {
524 case 'macintosh':
525 // this map was derived from the differences between the MacRoman and UTF-8 Charsets
526 // Reference:
527 // - http://www.alanwood.net/demos/macroman.html
528 $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 );
529 break;
530 case 'ISO-8859-1':
531 $convmap = array( 256, 10000, 0, 0xffff );
532 }
533
534 if ( is_array( $convmap ) ) {
535 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
536 }
537
538 if ( self::$to_encoding !== self::$charset ) {
539 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
540 }
541
542 return self::escape_csv( $line );
543 }
544
545 /**
546 * Escape a " in a csv with another "
547 *
548 * @since 2.0
549 * @param mixed $value
550 * @return mixed
551 */
552 public static function escape_csv( $value ) {
553 if ( ! is_string( $value ) ) {
554 return $value;
555 }
556
557 if ( '=' === $value[0] ) {
558 // escape the = to prevent vulnerability
559 $value = "'" . $value;
560 }
561 $value = str_replace( '"', '""', $value );
562
563 return $value;
564 }
565 }
566