Completed
Push — master ( a1ea0d...28ac78 )
by Stephanie
02:33 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 ( $col->form_id === self::$form_id || ! $col->field_options['in_section'] ) {
147
				$headings += self::field_headings( $col );
148
			} else {
149
				$repeater_id = $col->field_options['in_section'];
150
				$section     = FrmField::getOne( $repeater_id );
151
152
				if ( $section && FrmField::is_repeating_field( $section ) ) {
153
					$headings[ 'repeater' . $repeater_id ] = array(); // set a placeholder to maintain order for repeater fields
154
155
					if ( ! isset( $fields_by_repeater_id[ $repeater_id ] ) ) {
156
						$fields_by_repeater_id[ $repeater_id ] = array();
157
						$repeater_ids[]                        = $repeater_id;
158
					}
159
160
					$fields_by_repeater_id[ $repeater_id ][] = $col;
161
				}
162
			}
163
		}
164
		unset( $repeater_id, $col );
165
166
		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...
167
			$where         = array( 'field_id' => $repeater_ids );
168
			$repeater_meta = FrmDb::get_results( 'frm_item_metas', $where, 'field_id, meta_value' );
169
			$max           = array_fill_keys( $repeater_ids, 0 );
170
171
			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...
172
				$start  = strpos( $row->meta_value, 'a:' ) + 2;
173
				$end    = strpos( $row->meta_value, ':{' );
174
				$length = substr( $row->meta_value, $start, $end - $start );
175
176
				if ( $length > $max[ $row->field_id ] ) {
177
					$max[ $row->field_id ] = $length;
178
				}
179
			}
180
			unset( $start, $end, $length, $row, $repeater_meta, $where );
181
182
			$flat = array();
183
			foreach ( $headings as $key => $heading ) {
184
				if ( is_array( $heading ) ) {
185
					$repeater_id = str_replace( 'repeater', '', $key );
186
187
					$repeater_headings = array();
188
					foreach ( $fields_by_repeater_id[ $repeater_id ] as $col ) {
189
						$repeater_headings += self::field_headings( $col );
190
					}
191
192
					for ( $i = 0; $i < $max[ $repeater_id ]; $i ++ ) {
193
						foreach ( $repeater_headings as $repeater_key => $repeater_name ) {
194
							$flat[ $repeater_key . '[' . $i . ']' ] = $repeater_name;
195
						}
196
					}
197
				} else {
198
					$flat[ $key ] = $heading;
199
				}
200
			}
201
			unset( $key, $heading, $max, $repeater_headings, $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
	private static function has_parent_id() {
230
		return self::$has_parent_id;
231
	}
232
233
	private static function prepare_next_csv_rows( $next_set ) {
234
		// order by parent_item_id so children will be first
235
		$where   = array(
236
			'or'             => 1,
237
			'id'             => $next_set,
238
			'parent_item_id' => $next_set,
239
		);
240
		$entries = FrmEntry::getAll( $where, ' ORDER BY parent_item_id DESC', '', true, false );
241
242
		foreach ( $entries as $k => $entry ) {
243
			self::$entry = $entry;
244
			unset( $entry );
245
246
			if ( self::$entry->form_id !== self::$form_id ) {
247
				self::add_repeat_field_values_to_csv( $entries );
248
			} else {
249
				self::prepare_csv_row();
250
			}
251
		}
252
	}
253
254
	private static function prepare_csv_row() {
255
		$row = array();
256
		self::add_field_values_to_csv( $row );
257
		self::add_entry_data_to_csv( $row );
258
		$row = apply_filters(
259
			'frm_csv_row',
260
			$row,
261
			array(
262
				'entry'         => self::$entry,
263
				'date_format'   => self::$wp_date_format,
264
				'comment_count' => self::$comment_count,
265
			)
266
		);
267
		self::print_csv_row( $row );
268
	}
269
270
	private static function add_repeat_field_values_to_csv( &$entries ) {
271
		if ( isset( self::$entry->metas ) ) {
272
			// add child entries to the parent
273
			foreach ( self::$entry->metas as $meta_id => $meta_value ) {
274
				if ( ! is_numeric( $meta_id ) || '' === $meta_value ) {
275
					// if the hook is being used to include field keys in the metas array,
276
					// we need to skip the keys and only process field ids
277
					continue;
278
				}
279
280
				if ( ! isset( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
281
					$entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = array();
282
				} elseif ( ! is_array( $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] ) ) {
283
					// if the data is here, it should be an array but if this field has collected data
284
					// both while inside and outside of the repeating section, it's possible this is a string
285
					$entries[ self::$entry->parent_item_id ]->metas[ $meta_id ] = (array) $entries[ self::$entry->parent_item_id ]->metas[ $meta_id ];
286
				}
287
288
				//add the repeated values
289
				$entries[ self::$entry->parent_item_id ]->metas[ $meta_id ][] = $meta_value;
290
			}
291
			$entries[ self::$entry->parent_item_id ]->metas += self::$entry->metas;
292
		}
293
294
		// add the embedded form id
295
		if ( ! isset( $entries[ self::$entry->parent_item_id ]->embedded_fields ) ) {
296
			$entries[ self::$entry->parent_item_id ]->embedded_fields = array();
297
		}
298
		$entries[ self::$entry->parent_item_id ]->embedded_fields[ self::$entry->id ] = self::$entry->form_id;
299
	}
300
301
	private static function add_field_values_to_csv( &$row ) {
302
		foreach ( self::$fields as $col ) {
303
			$field_value = isset( self::$entry->metas[ $col->id ] ) ? self::$entry->metas[ $col->id ] : false;
304
305
			FrmAppHelper::unserialize_or_decode( $field_value );
306
			self::add_array_values_to_columns( $row, compact( 'col', 'field_value' ) );
307
308
			$field_value = apply_filters(
309
				'frm_csv_value',
310
				$field_value,
311
				array(
312
					'field'     => $col,
313
					'entry'     => self::$entry,
314
					'separator' => self::$separator,
315
				)
316
			);
317
318
			if ( ! empty( $col->field_options['separate_value'] ) ) {
319
				$sep_value = FrmEntriesHelper::display_value(
320
					$field_value,
321
					$col,
322
					array(
323
						'type'              => $col->type,
324
						'post_id'           => self::$entry->post_id,
325
						'show_icon'         => false,
326
						'entry_id'          => self::$entry->id,
327
						'sep'               => self::$separator,
328
						'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,
329
					)
330
				);
331
332
				$row[ $col->id . '_label' ] = $sep_value;
333
				unset( $sep_value );
334
			}
335
336
			$row[ $col->id ] = $field_value;
337
338
			unset( $col, $field_value );
339
		}
340
	}
341
342
	/**
343
	 * @since 2.0.23
344
	 */
345
	private static function add_array_values_to_columns( &$row, $atts ) {
346
		if ( is_array( $atts['field_value'] ) ) {
347
			foreach ( $atts['field_value'] as $key => $sub_value ) {
348
				$column_key = $atts['col']->id . '_' . $key;
349
				if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
350
					$row[ $column_key ] = $sub_value;
351
				}
352
			}
353
		}
354
	}
355
356
	private static function add_entry_data_to_csv( &$row ) {
357
		$row['created_at'] = FrmAppHelper::get_formatted_time( self::$entry->created_at, self::$wp_date_format, ' ' );
358
		$row['updated_at'] = FrmAppHelper::get_formatted_time( self::$entry->updated_at, self::$wp_date_format, ' ' );
359
		$row['user_id']    = self::$entry->user_id;
360
		$row['updated_by'] = self::$entry->updated_by;
361
		$row['is_draft']   = self::$entry->is_draft ? '1' : '0';
362
		$row['ip']         = self::$entry->ip;
363
		$row['id']         = self::$entry->id;
364
		$row['item_key']   = self::$entry->item_key;
365
		if ( self::has_parent_id() ) {
366
			$row['parent_id'] = self::$entry->parent_item_id;
367
		}
368
	}
369
370
	private static function print_csv_row( $rows ) {
371
		$sep = '';
372
373
		foreach ( self::$headings as $k => $heading ) {
374
			if ( isset( $rows[ $k ] ) ) {
375
				$row = $rows[ $k ];
376
			} else {
377
				$row = '';
378
				// array indexed data is not at $rows[ $k ]
379
				if ( $k[ strlen( $k ) - 1 ] === ']' ) {
380
					$start = strrpos( $k, '[' );
381
					$key   = substr( $k, 0, $start ++ );
382
					$index = substr( $k, $start, strlen( $k ) - 1 - $start );
383
384
					if ( isset( $rows[ $key ] ) && isset( $rows[ $key ][ $index ] ) ) {
385
						$row = $rows[ $key ][ $index ];
386
					}
387
388
					unset( $start, $key, $index );
389
				}
390
			}
391
392
			if ( is_array( $row ) ) {
393
				// implode the repeated field values
394
				$row = implode( self::$separator, FrmAppHelper::array_flatten( $row, 'reset' ) );
395
			}
396
397
			$val = self::encode_value( $row );
398
			if ( 'return' !== self::$line_break ) {
399
				$val = str_replace( array( "\r\n", "\r", "\n" ), self::$line_break, $val );
400
			}
401
402
			echo $sep . '"' . $val . '"'; // WPCS: XSS ok.
403
			$sep = self::$column_separator;
404
405
			unset( $k, $row );
406
		}
407
		echo "\n";
408
	}
409
410
	public static function encode_value( $line ) {
411
		if ( '' === $line ) {
412
			return $line;
413
		}
414
415
		$convmap = false;
416
417
		switch ( self::$to_encoding ) {
418
			case 'macintosh':
419
				// this map was derived from the differences between the MacRoman and UTF-8 Charsets
420
				// Reference:
421
				//   - http://www.alanwood.net/demos/macroman.html
422
				$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 );
423
				break;
424
			case 'ISO-8859-1':
425
				$convmap = array( 256, 10000, 0, 0xffff );
426
		}
427
428
		if ( is_array( $convmap ) ) {
429
			$line = mb_encode_numericentity( $line, $convmap, self::$charset );
430
		}
431
432
		if ( self::$to_encoding !== self::$charset ) {
433
			$line = iconv( self::$charset, self::$to_encoding . '//IGNORE', $line );
434
		}
435
436
		return self::escape_csv( $line );
437
	}
438
439
	/**
440
	 * Escape a " in a csv with another "
441
	 *
442
	 * @since 2.0
443
	 */
444
	public static function escape_csv( $value ) {
445
		if ( '=' === $value[0] ) {
446
			// escape the = to prevent vulnerability
447
			$value = "'" . $value;
448
		}
449
		$value = str_replace( '"', '""', $value );
450
451
		return $value;
452
	}
453
}
454