'
. 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 '
';
foreach ( $notice['details'] as $line ) {
echo '- ' . esc_html( $line ) . '
';
}
echo '
';
}
echo '
';
}
}