0 ) { self::render_detail( $entry_id ); } else { self::render_list(); } } // ------------------------------------------------------------------------- // List View // ------------------------------------------------------------------------- private static function render_list(): void { if ( ! class_exists( 'EPPRO_DB' ) ) { echo '
' . esc_html__( 'Essential Pro Pack core is still loading. Please ensure Elementor is active, then reload this page.', 'admore-widgets-suite' ) . '
' . esc_html__( 'Entry not found.', 'admore-widgets-suite' ) . '
'; return; } // Auto-mark as read on view if ( 'unread' === $entry['status'] ) { EPPRO_DB::update_entry_status( $entry_id, 'read' ); $entry['status'] = 'read'; } $fields = json_decode( $entry['fields'], true ) ?: []; $back_url = remove_query_arg( [ 'view', 'entry_id' ] ); ?> 'read', 'mark_unread' => 'unread', 'mark_starred' => 'starred' ]; $deleted = 0; foreach ( $ids as $id ) { if ( 'delete' === $action ) { if ( EPPRO_DB::delete_entry( $id ) ) $deleted++; } elseif ( isset( $statuses[ $action ] ) ) { EPPRO_DB::update_entry_status( $id, $statuses[ $action ] ); } } $redirect = admin_url( 'admin.php?page=eppro-form-entries' ); if ( $deleted > 0 ) { $redirect = add_query_arg( 'deleted', $deleted, $redirect ); } wp_safe_redirect( $redirect ); exit; } // ------------------------------------------------------------------------- // CSV Export // ------------------------------------------------------------------------- public static function handle_export(): void { if ( ! current_user_can( 'manage_options' ) ) wp_die( esc_html__( 'Insufficient permissions.', 'admore-widgets-suite' ) ); check_admin_referer( 'eppro_export_entries' ); $form_id = isset( $_GET['form_id'] ) ? sanitize_key( wp_unslash( $_GET['form_id'] ) ) : ''; $status = isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : ''; // get_entries() returns at most 100 rows per page, so walk every page. $entries = []; $paged = 1; do { $result = EPPRO_DB::get_entries( [ 'form_id' => $form_id, 'status' => $status, 'per_page' => 100, 'paged' => $paged ] ); $batch = $result['entries']; $entries = array_merge( $entries, $batch ); $paged++; } while ( count( $batch ) === 100 && $paged <= 2000 ); if ( empty( $entries ) ) { wp_safe_redirect( admin_url( 'admin.php?page=eppro-form-entries' ) ); exit; } $filename = 'eppro-form-entries-' . gmdate( 'Y-m-d' ) . '.csv'; header( 'Content-Type: text/csv; charset=UTF-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); $out = fopen( 'php://output', 'w' ); // UTF-8 BOM for Excel compatibility. Echoed as a hardcoded literal // (no fwrite()); php://output is the response body, so this writes // straight to the download stream before the CSV rows below. echo "\xEF\xBB\xBF"; // Header row — collect all possible field labels from all entries $all_labels = []; $decoded = []; foreach ( $entries as $entry ) { $fields = json_decode( (string) $entry['fields'], true ); $fields = is_array( $fields ) ? $fields : []; $decoded[] = $fields; $all_labels = array_unique( array_merge( $all_labels, array_map( 'strval', array_keys( $fields ) ) ) ); } $meta_headers = [ 'ID', 'Form', 'Status', 'IP Address', 'Page URL', 'Submitted At' ]; fputcsv( $out, array_map( [ __CLASS__, 'csv_cell' ], array_merge( $meta_headers, $all_labels ) ), ',', '"', '' ); foreach ( $entries as $i => $entry ) { $row = [ $entry['id'], $entry['form_name'] ?: $entry['form_id'], $entry['status'], $entry['ip_address'], $entry['page_url'], $entry['submitted_at'], ]; $fields = $decoded[ $i ]; foreach ( $all_labels as $label ) { $value = $fields[ $label ] ?? ''; $row[] = is_scalar( $value ) ? $value : wp_json_encode( $value ); } fputcsv( $out, array_map( [ __CLASS__, 'csv_cell' ], $row ), ',', '"', '' ); } // No fclose() needed — php://output is a passthrough stream and PHP // flushes and closes it automatically on exit. exit; } /** * Neutralise spreadsheet formulas. Submissions come from the public, and a * cell starting with = + - @ (or a tab/CR) would be executed by Excel or * Google Sheets when the export is opened. */ public static function csv_cell( $value ): string { $value = (string) $value; if ( '' !== $value && in_array( $value[0], [ '=', '+', '-', '@', "\t", "\r" ], true ) ) { $value = "'" . $value; } return $value; } // ------------------------------------------------------------------------- // AJAX: update single entry status // ------------------------------------------------------------------------- public static function ajax_update_status(): void { check_ajax_referer( 'eppro_entries_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( [], 403 ); } $id = absint( $_POST['entry_id'] ?? 0 ); $status = sanitize_key( wp_unslash( $_POST['status'] ?? '' ) ); if ( ! $id || ! $status ) { wp_send_json_error( [ 'message' => 'Invalid parameters.' ] ); } $ok = EPPRO_DB::update_entry_status( $id, $status ); $ok ? wp_send_json_success() : wp_send_json_error( [ 'message' => 'Update failed.' ] ); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private static function status_label( string $status ): string { $map = [ 'unread' => __( 'Unread', 'admore-widgets-suite' ), 'read' => __( 'Read', 'admore-widgets-suite' ), 'starred' => __( 'Starred', 'admore-widgets-suite' ), 'trash' => __( 'Trash', 'admore-widgets-suite' ), ]; return $map[ $status ] ?? ucfirst( $status ); } }