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

547 lines 17.6 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 ( isset( $col->field_options['separate_value'] ) && $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
236 /**
237 * @param object $field
238 * @return bool
239 */
240 private static function is_the_child_of_a_repeater( $field ) {
241 if ( $field->form_id === self::$form_id || empty( $field->field_options['in_section'] ) ) {
242 return false;
243 }
244
245 $section_id = $field->field_options['in_section'];
246 $section = FrmField::getOne( $section_id );
247
248 if ( ! $section ) {
249 return false;
250 }
251
252 return FrmField::is_repeating_field( $section );
253 }
254
255 private static function has_parent_id() {
256 return self::$has_parent_id;
257 }
258
259 private static function prepare_next_csv_rows( $next_set ) {
260 // order by parent_item_id so children will be first
261 $where = array(
262 'or' => 1,
263 'id' => $next_set,
264 'parent_item_id' => $next_set,
265 );
266 $entries = FrmEntry::getAll( $where, ' ORDER BY parent_item_id DESC', '', true, false );
267
268 foreach ( $entries as $k => $entry ) {
269 self::$entry = $entry;
270 unset( $entry );
271
272 if ( self::$entry->form_id !== self::$form_id ) {
273 self::add_repeat_field_values_to_csv( $entries );
274 } else {
275 self::prepare_csv_row();
276 }
277 }
278 }
279
280 private static function prepare_csv_row() {
281 $row = array();
282 self::add_field_values_to_csv( $row );
283 self::add_entry_data_to_csv( $row );
284 $row = apply_filters(
285 'frm_csv_row',
286 $row,
287 array(
288 'entry' => self::$entry,
289 'date_format' => self::$wp_date_format,
290 'comment_count' => self::$comment_count,
291 )
292 );
293 self::print_csv_row( $row );
294 }
295
296 private static function add_repeat_field_values_to_csv( &$entries ) {
297 if ( isset( self::$entry->metas ) ) {
298 // add child entries to the parent
299 foreach ( self::$entry->metas as $meta_id => $meta_value ) {
300 if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
301 // if the hook is being used to include field keys in the metas array,
302 // we need to skip the keys and only process field ids
303 continue;
304 }
305
306 if ( ! isset( $entries[ self::$entry->parent_item_id ] ) ) {
307 $entries[ self::$entry->parent_item_id ] = new stdClass();
308 $entries[ self::$entry->parent_item_id ]->metas = array();
309 }
310
311 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
312 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
313 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
314 // if the data is here, it should be an array but if this field has collected data
315 // both while inside and outside of the repeating section, it's possible this is a string
316 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
317 }
318
319 //add the repeated values
320 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
321 }
322
323 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
324 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
325 }
326
327 // add the embedded form id
328 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
329 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
330 }
331 $entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
332 }
333
334 /**
335 * When an empty field is saved, it isn't saved as a meta value
336 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
337 *
338 * @param array $metas
339 * @param array $entries
340 * @return array
341 */
342 private static function fill_missing_repeater_metas( $metas, &$entries ) {
343 $field_ids = array_keys( $metas );
344 $field_id = end( $field_ids );
345 $field = self::get_field( $field_id );
346
347 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
348 return $metas;
349 }
350
351 $repeater_id = $field->field_options['in_section'];
352 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
353 return $metas;
354 }
355
356 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
357 if ( ! isset( $metas[ $repeater_child->id ] ) ) {
358 $metas[ $repeater_child->id ] = '';
359 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
360 }
361 }
362
363 return $metas;
364 }
365
366 private static function get_field( $field_id ) {
367 $field_id = (int) $field_id;
368 foreach ( self::$fields as $field ) {
369 if ( $field_id === (int) $field->id ) {
370 return $field;
371 }
372 }
373 return false;
374 }
375
376 private static function add_field_values_to_csv( &$row ) {
377 foreach ( self::$fields as $col ) {
378 $field_value = isset( self::$entry->metas[ $col->id ] ) ? self::$entry->metas[ $col->id ] : false;
379
380 FrmAppHelper::unserialize_or_decode( $field_value );
381 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
382
383 $field_value = apply_filters(
384 'frm_csv_value',
385 $field_value,
386 array(
387 'field' => $col,
388 'entry' => self::$entry,
389 'separator' => self::$separator,
390 )
391 );
392
393 if ( ! empty( $col->field_options['separate_value'] ) ) {
394 $sep_value = FrmEntriesHelper::display_value(
395 $field_value,
396 $col,
397 array(
398 'type' => $col->type,
399 'post_id' => self::$entry->post_id,
400 'show_icon' => false,
401 'entry_id' => self::$entry->id,
402 'sep' => self::$separator,
403 '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,
404 )
405 );
406
407 $row[ $col->id . '_label' ] = $sep_value;
408 unset( $sep_value );
409 }
410
411 $row[ $col->id ] = $field_value;
412
413 unset( $col, $field_value );
414 }
415 }
416
417 /**
418 * @since 2.0.23
419 */
420 private static function add_array_values_to_columns( &$row, $atts ) {
421 if ( is_array( $atts['field_value'] ) ) {
422 foreach ( $atts['field_value'] as $key => $sub_value ) {
423 if ( is_array( $sub_value ) ) {
424 // This is combo field inside repeater. The heading key has this format: [86_first[0]].
425 foreach ( $sub_value as $sub_key => $sub_sub_value ) {
426 $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
427 if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
428 $row[ $column_key ] = $sub_sub_value;
429 }
430 }
431
432 continue;
433 }
434
435 $column_key = $atts['col']->id . '_' . $key;
436 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
437 $row[ $column_key ] = $sub_value;
438 }
439 }
440 }
441 }
442
443 private static function add_entry_data_to_csv( &$row ) {
444 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
445 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
446 $row['user_id'] = self::$entry->user_id;
447 $row['updated_by'] = self::$entry->updated_by;
448 $row['is_draft'] = self::$entry->is_draft ? '1' : '0';
449 $row['ip'] = self::$entry->ip;
450 $row['id'] = self::$entry->id;
451 $row['item_key'] = self::$entry->item_key;
452 if ( self::has_parent_id() ) {
453 $row['parent_id'] = self::$entry->parent_item_id;
454 }
455 }
456
457 private static function print_csv_row( $rows ) {
458 $sep = '';
459
460 foreach ( self::$headings as $k => $heading ) {
461 if ( isset( $rows[ $k ] ) ) {
462 $row = $rows[ $k ];
463 } else {
464 $row = '';
465 // array indexed data is not at $rows[ $k ]
466 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
467 $start = strrpos( $k, '[' );
468 $key = substr( $k, 0, $start ++ );
469 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
470
471 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
472 $row = $rows[ $key ][ $index ];
473 }
474
475 unset( $start, $key, $index );
476 }
477 }
478
479 if ( is_array( $row ) ) {
480 // implode the repeated field values
481 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
482 }
483
484 $val = self::encode_value( $row );
485 if ( 'return' !== self::$line_break ) {
486 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
487 }
488
489 echo $sep . '"' . $val . '"'; // WPCS: XSS ok.
490 $sep = self::$column_separator;
491
492 unset( $k, $row );
493 }
494 echo "\n";
495 }
496
497 public static function encode_value( $line ) {
498 if ( '' === $line ) {
499 return $line;
500 }
501
502 $convmap = false;
503
504 switch ( self::$to_encoding ) {
505 case 'macintosh':
506 // this map was derived from the differences between the MacRoman and UTF-8 Charsets
507 // Reference:
508 // - http://www.alanwood.net/demos/macroman.html
509 $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 );
510 break;
511 case 'ISO-8859-1':
512 $convmap = array( 256, 10000, 0, 0xffff );
513 }
514
515 if ( is_array( $convmap ) ) {
516 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
517 }
518
519 if ( self::$to_encoding !== self::$charset ) {
520 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
521 }
522
523 return self::escape_csv( $line );
524 }
525
526 /**
527 * Escape a " in a csv with another "
528 *
529 * @since 2.0
530 * @param mixed $value
531 * @return mixed
532 */
533 public static function escape_csv( $value ) {
534 if ( ! is_string( $value ) ) {
535 return $value;
536 }
537
538 if ( '=' === $value[0] ) {
539 // escape the = to prevent vulnerability
540 $value = "'" . $value;
541 }
542 $value = str_replace( '"', '""', $value );
543
544 return $value;
545 }
546 }
547