Completed
Push — master ( e9070d...37062a )
by Stephanie
18s queued 11s
created

FrmCSVExportHelper::fill_missing_repeater_metas()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 2
dl 0
loc 23
rs 8.9297
c 0
b 0
f 0
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $repeater_ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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 ) {
0 ignored issues
show
Bug introduced by
The expression $repeater_meta of type array|null|string|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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 ]->metas[ $meta_id ] ) ) {
300
					$entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
301
				} elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
302
					// if the data is here, it should be an array but if this field has collected data
303
					// both while inside and outside of the repeating section, it's possible this is a string
304
					$entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
305
				}
306
307
				//add the repeated values
308
				$entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
309
			}
310
311
			self::$entry->metas                              = self::fill_missing_repeater_metas( self::$entry->metas, $entries );
312
			$entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
313
		}
314
315
		// add the embedded form id
316
		if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
317
			$entries[ self::$entry->parent_item_id ]->embedded_fields = array();
318
		}
319
		$entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
320
	}
321
322
	/**
323
	 * When an empty field is saved, it isn't saved as a meta value
324
	 * The export needs all of the meta to be filled in, so we put blank strings for every missing repeater child
325
	 *
326
	 * @param array $metas
327
	 * @param array $entries
328
	 * @return array
329
	 */
330
	private static function fill_missing_repeater_metas( $metas, &$entries ) {
331
		$field_ids = array_keys( $metas );
332
		$field_id  = end( $field_ids );
333
		$field     = self::get_field( $field_id );
334
335
		if ( ! $field || empty( $field->field_options['in_section'] ) ) {
336
			return $metas;
337
		}
338
339
		$repeater_id = $field->field_options['in_section'];
340
		if ( ! isset( self::$fields_by_repeater_id[ $repeater_id ] ) ) {
341
			return $metas;
342
		}
343
344
		foreach ( self::$fields_by_repeater_id[ $repeater_id ] as $repeater_child ) {
345
			if ( ! isset( $metas[ $repeater_child->id ] ) ) {
346
				$metas[ $repeater_child->id ]                                            = '';
347
				$entries[ self::$entry->parent_item_id ]->metas[ $repeater_child->id ][] = '';
348
			}
349
		}
350
351
		return $metas;
352
	}
353
354
	private static function get_field( $field_id ) {
355
		$field_id = (int) $field_id;
356
		foreach ( self::$fields as $field ) {
357
			if ( $field_id === (int) $field->id ) {
358
				return $field;
359
			}
360
		}
361
		return false;
362
	}
363
364
	private static function add_field_values_to_csv( &$row ) {
365
		foreach ( self::$fields as $col ) {
366
			$field_value = isset( self::$entry->metas[ $col->id ] ) ? self::$entry->metas[ $col->id ] : false;
367
368
			FrmAppHelper::unserialize_or_decode( $field_value );
369
			self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
370
371
			$field_value = apply_filters(
372
				'frm_csv_value',
373
				$field_value,
374
				array(
375
					'field'     => $col,
376
					'entry'     => self::$entry,
377
					'separator' => self::$separator,
378
				)
379
			);
380
381
			if ( ! empty( $col->field_options['separate_value'] ) ) {
382
				$sep_value = FrmEntriesHelper::display_value(
383
					$field_value,
384
					$col,
385
					array(
386
						'type'              => $col->type,
387
						'post_id'           => self::$entry->post_id,
388
						'show_icon'         => false,
389
						'entry_id'          => self::$entry->id,
390
						'sep'               => self::$separator,
391
						'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,
392
					)
393
				);
394
395
				$row[ $col->id . '_label' ] = $sep_value;
396
				unset( $sep_value );
397
			}
398
399
			$row[ $col->id ] = $field_value;
400
401
			unset( $col, $field_value );
402
		}
403
	}
404
405
	/**
406
	 * @since 2.0.23
407
	 */
408
	private static function add_array_values_to_columns( &$row, $atts ) {
409
		if ( is_array( $atts['field_value'] ) ) {
410
			foreach ( $atts['field_value'] as $key => $sub_value ) {
411
				$column_key = $atts['col']->id . '_' . $key;
412
				if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
413
					$row[ $column_key ] = $sub_value;
414
				}
415
			}
416
		}
417
	}
418
419
	private static function add_entry_data_to_csv( &$row ) {
420
		$row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
421
		$row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
422
		$row['user_id']    = self::$entry->user_id;
423
		$row['updated_by'] = self::$entry->updated_by;
424
		$row['is_draft']   = self::$entry->is_draft ? '1' : '0';
425
		$row['ip']         = self::$entry->ip;
426
		$row['id']         = self::$entry->id;
427
		$row['item_key']   = self::$entry->item_key;
428
		if ( self::has_parent_id() ) {
429
			$row['parent_id'] = self::$entry->parent_item_id;
430
		}
431
	}
432
433
	private static function print_csv_row( $rows ) {
434
		$sep = '';
435
436
		foreach ( self::$headings as $k => $heading ) {
437
			if ( isset( $rows[ $k ] ) ) {
438
				$row = $rows[ $k ];
439
			} else {
440
				$row = '';
441
				// array indexed data is not at $rows[ $k ]
442
				if ( $k[ strlen( $k ) - 1 ] === ']' ) {
443
					$start = strrpos( $k, '[' );
444
					$key   = substr( $k, 0, $start ++ );
445
					$index = substr( $k, $start, strlen( $k ) - 1 - $start );
446
447
					if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
448
						$row = $rows[ $key ][ $index ];
449
					}
450
451
					unset( $start, $key, $index );
452
				}
453
			}
454
455
			if ( is_array( $row ) ) {
456
				// implode the repeated field values
457
				$row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
458
			}
459
460
			$val = self::encode_value( $row );
461
			if ( 'return' !== self::$line_break ) {
462
				$val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
463
			}
464
465
			echo $sep . '"' . $val . '"'; // WPCS: XSS ok.
466
			$sep = self::$column_separator;
467
468
			unset( $k, $row );
469
		}
470
		echo "\n";
471
	}
472
473
	public static function encode_value( $line ) {
474
		if ( '' === $line ) {
475
			return $line;
476
		}
477
478
		$convmap = false;
479
480
		switch ( self::$to_encoding ) {
481
			case 'macintosh':
482
				// this map was derived from the differences between the MacRoman and UTF-8 Charsets
483
				// Reference:
484
				//   - http://www.alanwood.net/demos/macroman.html
485
				$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 );
486
				break;
487
			case 'ISO-8859-1':
488
				$convmap = array( 256, 10000, 0, 0xffff );
489
		}
490
491
		if ( is_array( $convmap ) ) {
492
			$line = mb_encode_numericentity( $line, $convmap, self::$charset );
493
		}
494
495
		if ( self::$to_encoding !== self::$charset ) {
496
			$line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
497
		}
498
499
		return self::escape_csv( $line );
500
	}
501
502
	/**
503
	 * Escape a " in a csv with another "
504
	 *
505
	 * @since 2.0
506
	 */
507
	public static function escape_csv( $value ) {
508
		if ( '=' === $value[0] ) {
509
			// escape the = to prevent vulnerability
510
			$value = "'" . $value;
511
		}
512
		$value = str_replace( '"', '""', $value );
513
514
		return $value;
515
	}
516
}
517