Completed
Push — master ( 210fe1...fb1289 )
by Stephanie
02:49 queued 10s
created

FrmCSVExportHelper::encode_value()   B

Complexity

Conditions 6
Paths 13

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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