$form_id, 'per_page' => 0, 'order' => 'ASC', ) ); // Columns: fixed metadata + all field labels in order of appearance. // Fields with the same label (e.g. "Consent" from different forms) share one column. $columns = array(); // key => label. foreach ( $rows as $row ) { foreach ( $row->data as $field ) { $key = self::column_key( $field['label'] ); if ( ! isset( $columns[ $key ] ) ) { $columns[ $key ] = $field['label']; } } } $name = 'alpeg-forms-' . ( $form_id ? $form_id . '-' : '' ) . gmdate( 'Y-m-d' ) . '.csv'; nocache_headers(); header( 'Content-Type: text/csv; charset=UTF-8' ); header( 'Content-Disposition: attachment; filename="' . $name . '"' ); header( 'X-Content-Type-Options: nosniff' ); $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite $header = array_merge( array( 'ID', __( 'Date', 'alpeg-forms' ), __( 'Form', 'alpeg-forms' ), __( 'Page', 'alpeg-forms' ) ), array_values( $columns ) ); fputcsv( $out, array_map( array( __CLASS__, 'cell' ), $header ), ';', '"', '\\' ); foreach ( $rows as $row ) { $values = array(); foreach ( $row->data as $field ) { $key = self::column_key( $field['label'] ); $val = (string) $field['value']; $values[ $key ] = isset( $values[ $key ] ) && '' !== $values[ $key ] ? $values[ $key ] . ' | ' . $val : $val; } $line = array( $row->id, get_date_from_gmt( $row->created_at, 'd.m.Y H:i' ), $row->form_title, $row->page_url, ); foreach ( array_keys( $columns ) as $key ) { $line[] = isset( $values[ $key ] ) ? $values[ $key ] : ''; } fputcsv( $out, array_map( array( __CLASS__, 'cell' ), $line ), ';', '"', '\\' ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } /** * Column key: labels are compared case-insensitively and without surrounding spaces. * * @param string $label Label. * @return string */ private static function column_key( $label ) { $label = trim( wp_strip_all_tags( (string) $label ) ); return function_exists( 'mb_strtolower' ) ? mb_strtolower( $label, 'UTF-8' ) : strtolower( $label ); } /** * Prepare a cell for Excel. * * - Phone numbers, postcodes and other digit strings that Excel would turn into * a number (losing a leading "+" or "0", or switching to 4,9E+12) are * exported as a text formula ="…". Only digits, "+" and spaces are allowed * there, so nothing can be injected. * - Protection against CSV injection: other values Excel would treat as a * formula get a leading apostrophe. * * @param mixed $value Value. * @return string */ public static function cell( $value ) { $value = (string) $value; // Phone-like: digits, optional leading "+", spaces, ( ) / . - – never quotes. if ( preg_match( '/^\+?[0-9][0-9 ()\/.\-]*$/', $value ) ) { $digits = preg_replace( '/\D/', '', $value ); if ( '+' === $value[0] || ( '0' === $value[0] && ctype_digit( $value ) ) || strlen( $digits ) >= 12 || ( ctype_digit( str_replace( ' ', '', $value ) ) && false !== strpos( $value, ' ' ) ) ) { return '="' . $value . '"'; } return $value; // e.g. "52000", "2026-11-01" or "01234-453587". } if ( '' !== $value && in_array( $value[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) { $value = "'" . $value; } return $value; } }