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

528 lines 17.0 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_headings = array();
125 $separate_values = array( 'user_id', 'file', 'data', 'date' );
126 if ( isset( $col->field_options['separate_value'] ) && $col->field_options['separate_value'] && ! in_array( $col->type, $separate_values, true ) ) {
127 $field_headings[ $col->id . '_label' ] = strip_tags( $col->name . ' ' . __( '(label)', 'formidable' ) );
128 }
129
130 $field_headings[ $col->id ] = strip_tags( $col->name );
131 $field_headings = apply_filters(
132 'frm_csv_field_columns',
133 $field_headings,
134 array(
135 'field' => $col,
136 )
137 );
138
139 return $field_headings;
140 }
141
142 private static function csv_headings( &$headings ) {
143 $fields_by_repeater_id = array();
144 $repeater_ids = array();
145
146 foreach ( self::$fields as $col ) {
147 if ( self::is_the_child_of_a_repeater( $col ) ) {
148 $repeater_id = $col->field_options['in_section'];
149 $headings[ 'repeater' . $repeater_id ] = array(); // set a placeholder to maintain order for repeater fields
150
151 if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
152 $fields_by_repeater_id[ $repeater_id ] = array();
153 $repeater_ids[] = $repeater_id;
154 }
155
156 $fields_by_repeater_id[ $repeater_id ][] = $col;
157 } else {
158 $headings += self::field_headings( $col );
159 }
160 }
161 unset( $repeater_id, $col );
162
163 if ( $repeater_ids ) {
164 $where = array( 'field_id' => $repeater_ids );
165 $repeater_meta = FrmDb::get_results( 'frm_item_metas', $where, 'field_id, meta_value' );
166 $max = array_fill_keys( $repeater_ids, 0 );
167
168 foreach ( $repeater_meta as $row ) {
169 $start = strpos( $row->meta_value, 'a:' ) + 2;
170 $end = strpos( $row->meta_value, ':{' );
171 $length = substr( $row->meta_value, $start, $end - $start );
172
173 if ( $length > $max[ $row->field_id ] ) {
174 $max[ $row->field_id ] = $length;
175 }
176 }
177 unset( $start, $end, $length, $row, $repeater_meta, $where );
178
179 $flat = array();
180 foreach ( $headings as $key => $heading ) {
181 if ( is_array( $heading ) ) {
182 $repeater_id = str_replace( 'repeater', '', $key );
183
184 $repeater_headings = array();
185 foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
186 $repeater_headings += self::field_headings( $col );
187 }
188
189 for ( $i = 0; $i < $max[ $repeater_id ]; $i ++ ) {
190 foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
191 $flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
192 }
193 }
194 } else {
195 $flat[ $key ] = $heading;
196 }
197 }
198
199 self::$fields_by_repeater_id = $fields_by_repeater_id;
200
201 unset( $key, $heading, $max, $repeater_headings, $repeater_id, $fields_by_repeater_id );
202
203 $headings = $flat;
204 unset( $flat );
205 }
206
207 if ( self::$comment_count ) {
208 for ( $i = 0; $i < self::$comment_count; $i ++ ) {
209 $headings[ 'comment' . $i ] = __( 'Comment', 'formidable' );
210 $headings[ 'comment_user_id' . $i ] = __( 'Comment User', 'formidable' );
211 $headings[ 'comment_created_at' . $i ] = __( 'Comment Date', 'formidable' );
212 }
213 unset( $i );
214 }
215
216 $headings['created_at'] = __( 'Timestamp', 'formidable' );
217 $headings['updated_at'] = __( 'Last Updated', 'formidable' );
218 $headings['user_id'] = __( 'Created By', 'formidable' );
219 $headings['updated_by'] = __( 'Updated By', 'formidable' );
220 $headings['is_draft'] = __( 'Draft', 'formidable' );
221 $headings['ip'] = __( 'IP', 'formidable' );
222 $headings['id'] = __( 'ID', 'formidable' );
223 $headings['item_key'] = __( 'Key', 'formidable' );
224 if ( self::has_parent_id() ) {
225 $headings['parent_id'] = __( 'Parent ID', 'formidable' );
226 }
227 }
228
229 /**
230 * @param object $field
231 * @return bool
232 */
233 private static function is_the_child_of_a_repeater( $field ) {
234 if ( $field->form_id === self::$form_id || empty( $field->field_options['in_section'] ) ) {
235 return false;
236 }
237
238 $section_id = $field->field_options['in_section'];
239 $section = FrmField::getOne( $section_id );
240
241 if ( ! $section ) {
242 return false;
243 }
244
245 return FrmField::is_repeating_field( $section );
246 }
247
248 private static function has_parent_id() {
249 return self::$has_parent_id;
250 }
251
252 private static function prepare_next_csv_rows( $next_set ) {
253 // order by parent_item_id so children will be first
254 $where = array(
255 'or' => 1,
256 'id' => $next_set,
257 'parent_item_id' => $next_set,
258 );
259 $entries = FrmEntry::getAll( $where, ' ORDER BY parent_item_id DESC', '', true, false );
260
261 foreach ( $entries as $k => $entry ) {
262 self::$entry = $entry;
263 unset( $entry );
264
265 if ( self::$entry->form_id !== self::$form_id ) {
266 self::add_repeat_field_values_to_csv( $entries );
267 } else {
268 self::prepare_csv_row();
269 }
270 }
271 }
272
273 private static function prepare_csv_row() {
274 $row = array();
275 self::add_field_values_to_csv( $row );
276 self::add_entry_data_to_csv( $row );
277 $row = apply_filters(
278 'frm_csv_row',
279 $row,
280 array(
281 'entry' => self::$entry,
282 'date_format' => self::$wp_date_format,
283 'comment_count' => self::$comment_count,
284 )
285 );
286 self::print_csv_row( $row );
287 }
288
289 private static function add_repeat_field_values_to_csv( &$entries ) {
290 if ( isset( self::$entry->metas ) ) {
291 // add child entries to the parent
292 foreach ( self::$entry->metas as $meta_id => $meta_value ) {
293 if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
294 // if the hook is being used to include field keys in the metas array,
295 // we need to skip the keys and only process field ids
296 continue;
297 }
298
299 if ( ! isset( $entries[ self::$entry->parent_item_id ] ) ) {
300 $entries[ self::$entry->parent_item_id ] = new stdClass();
301 $entries[ self::$entry->parent_item_id ]->metas = array();
302 }
303
304 if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
305 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
306 } elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
307 // if the data is here, it should be an array but if this field has collected data
308 // both while inside and outside of the repeating section, it's possible this is a string
309 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
310 }
311
312 //add the repeated values
313 $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
314 }
315
316 self::$entry->metas = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
317 $entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
318 }
319
320 // add the embedded form id
321 if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
322 $entries[ self::$entry->parent_item_id ]->embedded_fields = array();
323 }
324 $entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
325 }
326
327 /**
328 * When an empty field is saved, it isn't saved as a meta value
329 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
330 *
331 * @param array $metas
332 * @param array $entries
333 * @return array
334 */
335 private static function fill_missing_repeater_metas( $metas, &$entries ) {
336 $field_ids = array_keys( $metas );
337 $field_id = end( $field_ids );
338 $field = self::get_field( $field_id );
339
340 if ( ! $field || empty( $field->field_options['in_section'] ) ) {
341 return $metas;
342 }
343
344 $repeater_id = $field->field_options['in_section'];
345 if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
346 return $metas;
347 }
348
349 foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
350 if ( ! isset( $metas[ $repeater_child->id ] ) ) {
351 $metas[ $repeater_child->id ] = '';
352 $entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
353 }
354 }
355
356 return $metas;
357 }
358
359 private static function get_field( $field_id ) {
360 $field_id = (int) $field_id;
361 foreach ( self::$fields as $field ) {
362 if ( $field_id === (int) $field->id ) {
363 return $field;
364 }
365 }
366 return false;
367 }
368
369 private static function add_field_values_to_csv( &$row ) {
370 foreach ( self::$fields as $col ) {
371 $field_value = isset( self::$entry->metas[ $col->id ] ) ? self::$entry->metas[ $col->id ] : false;
372
373 FrmAppHelper::unserialize_or_decode( $field_value );
374 self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
375
376 $field_value = apply_filters(
377 'frm_csv_value',
378 $field_value,
379 array(
380 'field' => $col,
381 'entry' => self::$entry,
382 'separator' => self::$separator,
383 )
384 );
385
386 if ( ! empty( $col->field_options['separate_value'] ) ) {
387 $sep_value = FrmEntriesHelper::display_value(
388 $field_value,
389 $col,
390 array(
391 'type' => $col->type,
392 'post_id' => self::$entry->post_id,
393 'show_icon' => false,
394 'entry_id' => self::$entry->id,
395 'sep' => self::$separator,
396 '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,
397 )
398 );
399
400 $row[ $col->id . '_label' ] = $sep_value;
401 unset( $sep_value );
402 }
403
404 $row[ $col->id ] = $field_value;
405
406 unset( $col, $field_value );
407 }
408 }
409
410 /**
411 * @since 2.0.23
412 */
413 private static function add_array_values_to_columns( &$row, $atts ) {
414 if ( is_array( $atts['field_value'] ) ) {
415 foreach ( $atts['field_value'] as $key => $sub_value ) {
416 $column_key = $atts['col']->id . '_' . $key;
417 if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
418 $row[ $column_key ] = $sub_value;
419 }
420 }
421 }
422 }
423
424 private static function add_entry_data_to_csv( &$row ) {
425 $row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
426 $row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
427 $row['user_id'] = self::$entry->user_id;
428 $row['updated_by'] = self::$entry->updated_by;
429 $row['is_draft'] = self::$entry->is_draft ? '1' : '0';
430 $row['ip'] = self::$entry->ip;
431 $row['id'] = self::$entry->id;
432 $row['item_key'] = self::$entry->item_key;
433 if ( self::has_parent_id() ) {
434 $row['parent_id'] = self::$entry->parent_item_id;
435 }
436 }
437
438 private static function print_csv_row( $rows ) {
439 $sep = '';
440
441 foreach ( self::$headings as $k => $heading ) {
442 if ( isset( $rows[ $k ] ) ) {
443 $row = $rows[ $k ];
444 } else {
445 $row = '';
446 // array indexed data is not at $rows[ $k ]
447 if ( $k[ strlen( $k ) - 1 ] === ']' ) {
448 $start = strrpos( $k, '[' );
449 $key = substr( $k, 0, $start ++ );
450 $index = substr( $k, $start, strlen( $k ) - 1 - $start );
451
452 if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
453 $row = $rows[ $key ][ $index ];
454 }
455
456 unset( $start, $key, $index );
457 }
458 }
459
460 if ( is_array( $row ) ) {
461 // implode the repeated field values
462 $row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
463 }
464
465 $val = self::encode_value( $row );
466 if ( 'return' !== self::$line_break ) {
467 $val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
468 }
469
470 echo $sep . '"' . $val . '"'; // WPCS: XSS ok.
471 $sep = self::$column_separator;
472
473 unset( $k, $row );
474 }
475 echo "\n";
476 }
477
478 public static function encode_value( $line ) {
479 if ( '' === $line ) {
480 return $line;
481 }
482
483 $convmap = false;
484
485 switch ( self::$to_encoding ) {
486 case 'macintosh':
487 // this map was derived from the differences between the MacRoman and UTF-8 Charsets
488 // Reference:
489 // - http://www.alanwood.net/demos/macroman.html
490 $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 );
491 break;
492 case 'ISO-8859-1':
493 $convmap = array( 256, 10000, 0, 0xffff );
494 }
495
496 if ( is_array( $convmap ) ) {
497 $line = mb_encode_numericentity( $line, $convmap, self::$charset );
498 }
499
500 if ( self::$to_encoding !== self::$charset ) {
501 $line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
502 }
503
504 return self::escape_csv( $line );
505 }
506
507 /**
508 * Escape a " in a csv with another "
509 *
510 * @since 2.0
511 * @param mixed $value
512 * @return mixed
513 */
514 public static function escape_csv( $value ) {
515 if ( ! is_string( $value ) ) {
516 return $value;
517 }
518
519 if ( '=' === $value[0] ) {
520 // escape the = to prevent vulnerability
521 $value = "'" . $value;
522 }
523 $value = str_replace( '"', '""', $value );
524
525 return $value;
526 }
527 }
528