' . esc_html__( 'Gravity Forms must be installed and activated to use EntryPort for Gravity Forms.', 'entryporter-for-gravity-forms' ) . '

'; return; } $this->render_notice(); $forms = GFAPI::get_forms(); ?>

current_user_can_import() ) { wp_die( esc_html__( 'You do not have permission to import Gravity Forms entries.', 'entryporter-for-gravity-forms' ), esc_html__( 'Permission denied', 'entryporter-for-gravity-forms' ), array( 'response' => 403 ) ); } $form_id = isset( $_POST['entryporter_form_id'] ) ? absint( wp_unslash( $_POST['entryporter_form_id'] ) ) : 0; if ( $form_id <= 0 ) { $this->redirect_with_notice( 'error', __( 'Please select a valid form.', 'entryporter-for-gravity-forms' ) ); } $form = class_exists( 'GFAPI' ) ? GFAPI::get_form( $form_id ) : false; if ( empty( $form ) || is_wp_error( $form ) ) { $this->redirect_with_notice( 'error', __( 'The selected form could not be found.', 'entryporter-for-gravity-forms' ) ); } // Sample download branch. if ( isset( $_POST['entryporter_sample'] ) ) { $importer = new EntryPorter_Importer( $form_id ); $importer->stream_sample_csv( $form ); exit; } $this->process_upload( $form_id ); } /** * Validate the uploaded file, run the import, and redirect with a notice. * * @param int $form_id Target Gravity Form ID. * @return void */ private function process_upload( $form_id ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_import_request(). if ( empty( $_FILES['entryporter_file'] ) || ! isset( $_FILES['entryporter_file']['error'] ) ) { $this->redirect_with_notice( 'error', __( 'Please choose a file to import.', 'entryporter-for-gravity-forms' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_import_request(). $upload_error = (int) $_FILES['entryporter_file']['error']; if ( UPLOAD_ERR_NO_FILE === $upload_error ) { $this->redirect_with_notice( 'error', __( 'Please choose a file to import.', 'entryporter-for-gravity-forms' ) ); } if ( UPLOAD_ERR_OK !== $upload_error ) { $this->redirect_with_notice( 'error', __( 'The file could not be uploaded. Please try again.', 'entryporter-for-gravity-forms' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_import_request(). $original_name = isset( $_FILES['entryporter_file']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['entryporter_file']['name'] ) ) : ''; $extension = strtolower( pathinfo( $original_name, PATHINFO_EXTENSION ) ); if ( ! in_array( $extension, array( 'csv', 'xlsx' ), true ) ) { $this->redirect_with_notice( 'error', __( 'Unsupported file type. Please upload a .csv or .xlsx file.', 'entryporter-for-gravity-forms' ) ); } /* * The temp path is generated by PHP, not submitted by the client, and it * is never unslashed or sanitized here: on Windows it contains directory * separators such as D:\xampp\tmp\phpA1B2.tmp, and stripping those * backslashes would corrupt the path so that is_uploaded_file() below * fails on every upload. The is_uploaded_file() check in * validate_file_contents() is what makes the value safe to read. */ // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified in handle_import_request(); value is a server-generated temp path validated by is_uploaded_file() below. $tmp_path = isset( $_FILES['entryporter_file']['tmp_name'] ) ? $_FILES['entryporter_file']['tmp_name'] : ''; $type_check = $this->validate_file_contents( $tmp_path, $extension ); if ( is_wp_error( $type_check ) ) { $this->redirect_with_notice( 'error', $type_check->get_error_message() ); } require_once ABSPATH . 'wp-admin/includes/file.php'; /* * WordPress' own MIME sniffing stays disabled here because browsers and * operating systems report CSV under many inconsistent types, which * rejects legitimate files. The uploaded bytes are instead verified * against an explicit allow list in validate_file_contents() above, and * the extension is already restricted to csv/xlsx, so a file whose real * contents do not match a spreadsheet never reaches this point. */ $overrides = array( 'test_form' => false, 'test_type' => false, ); // wp_handle_upload runs is_uploaded_file() internally; the request is nonce-checked above. // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash $moved = wp_handle_upload( $_FILES['entryporter_file'], $overrides ); if ( ! is_array( $moved ) || isset( $moved['error'] ) ) { $message = ( is_array( $moved ) && isset( $moved['error'] ) ) ? $moved['error'] : __( 'The file could not be processed.', 'entryporter-for-gravity-forms' ); $this->redirect_with_notice( 'error', $message ); } $importer = new EntryPorter_Importer( $form_id ); $result = $importer->import( $moved['file'], $extension ); // The uploaded copy is no longer needed once parsed. wp_delete_file( $moved['file'] ); if ( is_wp_error( $result ) ) { $this->redirect_with_notice( 'error', $result->get_error_message() ); } $this->redirect_with_result( $result ); } /** * Verify that an uploaded file's actual contents look like a spreadsheet. * * The extension allow list alone cannot tell a real CSV from, say, a PHP * script renamed to .csv. This inspects the bytes on disk and rejects * anything whose detected media type is not plausible for the claimed * extension. Detection is deliberately permissive within that set, because * CSV is reported inconsistently across platforms and a legitimate file * must never be refused. * * @param string $tmp_path Temporary upload path. * @param string $extension Lowercase extension without the dot. * @return true|WP_Error True when the file is acceptable, WP_Error otherwise. */ private function validate_file_contents( $tmp_path, $extension ) { if ( ! is_string( $tmp_path ) || '' === $tmp_path || ! is_uploaded_file( $tmp_path ) ) { return new WP_Error( 'entryporter_invalid_upload', __( 'The uploaded file could not be verified. Please try again.', 'entryporter-for-gravity-forms' ) ); } // Without finfo the extension allow list remains the only available check. if ( ! function_exists( 'finfo_open' ) ) { return true; } $finfo = finfo_open( FILEINFO_MIME_TYPE ); if ( false === $finfo ) { return true; } $real_mime = finfo_file( $finfo, $tmp_path ); finfo_close( $finfo ); if ( ! is_string( $real_mime ) || '' === $real_mime ) { return true; } $allowed = array( 'csv' => array( 'text/csv', 'text/plain', 'text/x-csv', 'text/comma-separated-values', 'application/csv', 'application/x-csv', 'application/vnd.ms-excel', ), 'xlsx' => array( 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip', 'application/x-zip-compressed', 'application/octet-stream', ), ); if ( isset( $allowed[ $extension ] ) && in_array( $real_mime, $allowed[ $extension ], true ) ) { return true; } return new WP_Error( 'entryporter_type_mismatch', __( 'The uploaded file does not appear to be a valid CSV or Excel spreadsheet. Please upload a file exported by Exportly for Gravity.', 'entryporter-for-gravity-forms' ) ); } /** * Store a result summary and redirect back to the import screen. * * @param array $result Import result summary. * @return void */ private function redirect_with_result( array $result ) { $imported = isset( $result['imported'] ) ? (int) $result['imported'] : 0; $failed = isset( $result['failed'] ) ? (int) $result['failed'] : 0; $errors = isset( $result['errors'] ) && is_array( $result['errors'] ) ? $result['errors'] : array(); if ( $imported > 0 && 0 === $failed ) { $message = sprintf( /* translators: %d: number of imported entries. */ _n( '%d entry imported successfully.', '%d entries imported successfully.', $imported, 'entryporter-for-gravity-forms' ), $imported ); $this->redirect_with_notice( 'success', $message ); } if ( $imported > 0 && $failed > 0 ) { $message = sprintf( /* translators: 1: imported count, 2: failed count. */ __( '%1$d entries imported, %2$d rows could not be imported.', 'entryporter-for-gravity-forms' ), $imported, $failed ); $this->redirect_with_notice( 'warning', $message, $errors ); } $this->redirect_with_notice( 'error', __( 'No entries were imported. Please check the file and try again.', 'entryporter-for-gravity-forms' ), $errors ); } /** * Persist a notice for the current user and redirect to the import screen. * * @param string $type Notice type: success, warning, or error. * @param string $message Primary message. * @param array $details Optional list of detail lines. * @return void */ private function redirect_with_notice( $type, $message, $details = array() ) { set_transient( self::NOTICE_PREFIX . get_current_user_id(), array( 'type' => $type, 'message' => $message, 'details' => is_array( $details ) ? $details : array(), ), 60 ); wp_safe_redirect( admin_url( 'admin.php?page=entryporter-for-gravity-forms' ) ); exit; } /** * Output and clear any pending notice for the current user. * * @return void */ private function render_notice() { $key = self::NOTICE_PREFIX . get_current_user_id(); $notice = get_transient( $key ); if ( empty( $notice ) || ! is_array( $notice ) ) { return; } delete_transient( $key ); $type = isset( $notice['type'] ) ? $notice['type'] : 'info'; $classes = array( 'success' => 'notice-success', 'warning' => 'notice-warning', 'error' => 'notice-error', ); $class = isset( $classes[ $type ] ) ? $classes[ $type ] : 'notice-info'; echo '

' . esc_html( $notice['message'] ) . '

'; if ( ! empty( $notice['details'] ) && is_array( $notice['details'] ) ) { echo ''; } echo '
'; } }