Tools — a left-nav sidebar of sections (Export Forms, Import * Forms, Import Entries, Migrator, Activity Logs, API Logs), matching the reference product * Forms' own Tools screen. */ class Tools_Page { const SECTIONS = [ 'overview', 'export-forms', 'import-forms', 'import-entries', 'migrator', 'activity-logs', 'api-logs', 'notifications' ]; public function __construct() { add_action( 'admin_menu', [ $this, 'register' ] ); add_action( 'admin_post_chargeforms_export_forms', [ $this, 'handle_export' ] ); add_action( 'admin_post_chargeforms_import_forms', [ $this, 'handle_import' ] ); add_action( 'admin_post_chargeforms_tools_import_entries', [ $this, 'handle_import_entries' ] ); add_action( 'admin_post_chargeforms_migrate_import', [ $this, 'handle_migrate_import' ] ); } public function register() { add_submenu_page( 'chargeforms', __( 'Tools', 'chargeforms' ), __( 'Tools', 'chargeforms' ), 'chargeforms_manage_forms', 'chargeforms-tools', [ $this, 'render' ] ); } private function current_section() { $section = isset( $_GET['section'] ) ? sanitize_key( wp_unslash( $_GET['section'] ) ) : 'overview'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return in_array( $section, self::SECTIONS, true ) ? $section : 'overview'; } private function section_labels() { return [ 'overview' => __( 'Overview', 'chargeforms' ), 'export-forms' => __( 'Export Forms', 'chargeforms' ), 'import-forms' => __( 'Import Forms', 'chargeforms' ), 'import-entries' => __( 'Import Entries', 'chargeforms' ), 'migrator' => __( 'Migrator', 'chargeforms' ), 'activity-logs' => __( 'Activity Logs', 'chargeforms' ), 'api-logs' => __( 'API Logs', 'chargeforms' ), 'notifications' => __( 'Notifications', 'chargeforms' ), ]; } private function section_icons() { return [ 'overview' => 'admin-home', 'export-forms' => 'database-export', 'import-forms' => 'database-import', 'import-entries' => 'upload', 'migrator' => 'migrate', 'activity-logs' => 'clock', 'api-logs' => 'networking', 'notifications' => 'email-alt', ]; } private function section_descriptions() { return [ 'overview' => __( 'A quick look at export/import, migration, and recent activity', 'chargeforms' ), 'export-forms' => __( 'Save forms as a portable JSON file', 'chargeforms' ), 'import-forms' => __( 'Bring in forms from a ChargeForms export', 'chargeforms' ), 'import-entries' => __( 'Bulk-add submissions from a CSV', 'chargeforms' ), 'migrator' => __( 'Bring forms in from other builders', 'chargeforms' ), 'activity-logs' => __( 'Who changed what, and when', 'chargeforms' ), 'api-logs' => __( 'Outgoing integration request history', 'chargeforms' ), 'notifications' => __( 'Owner-notification delivery status', 'chargeforms' ), ]; } public function render() { global $wpdb; $section = $this->current_section(); ?>

render_export_forms(); $this->render_system_status( $wpdb ); break; case 'import-forms': $this->render_import_forms(); break; case 'import-entries': $this->render_import_entries(); break; case 'migrator': $this->render_migrator(); break; case 'activity-logs': $this->render_activity_logs(); break; case 'api-logs': $this->render_api_logs(); break; case 'notifications': $this->render_notifications(); break; default: $this->render_overview( $wpdb ); } ?>
Overview — the design's single-page dashboard * (design_handoff_chargeforms's Tools screen: a 2-col tool-card grid, * Migrator, Activity/API log summaries side by side, System Status). * Deliberately additive, not a replacement: every existing full * section (Export Forms, Import Forms, Import Entries, Migrator, * Activity Logs, API Logs, Notifications) keeps its own real, * full-featured page and every existing deep link * (`?section=import-entries` etc., used elsewhere in the admin) keeps * working exactly as before — this only adds the landing view the * design shows, with each card linking to the real thing rather than * duplicating its UI. */ private function render_overview( $wpdb ) { $tool_cards = [ [ 'icon' => 'database-export', 'label' => __( 'Export Forms', 'chargeforms' ), 'desc' => __( 'Save forms as a portable JSON file.', 'chargeforms' ), 'href' => admin_url( 'admin.php?page=chargeforms-tools§ion=export-forms' ), 'action' => __( 'Export Forms', 'chargeforms' ), ], [ 'icon' => 'database-import', 'label' => __( 'Import Forms', 'chargeforms' ), 'desc' => __( 'Bring in forms from a ChargeForms export.', 'chargeforms' ), 'href' => admin_url( 'admin.php?page=chargeforms-tools§ion=import-forms' ), 'action' => __( 'Import Forms', 'chargeforms' ), ], [ 'icon' => 'upload', 'label' => __( 'Import Entries', 'chargeforms' ), 'desc' => __( 'Bulk-add submissions to a form from a CSV file.', 'chargeforms' ), 'href' => admin_url( 'admin.php?page=chargeforms-tools§ion=import-entries' ), 'action' => __( 'Import Entries', 'chargeforms' ), ], ]; ?>

render_migrator(); ?>
render_activity_log_summary(); ?> render_api_log_summary(); ?>
render_system_status( $wpdb ); ?>
1, 'per_page' => 5 ] ); ?>

1, 'per_page' => 5 ] ); $forms = Form::all(); $by_id = []; foreach ( $forms as $form ) { $by_id[ $form['id'] ] = $form['title']; } ?>

render_card_head( 'database-export', __( 'Export Forms', 'chargeforms' ), __( 'Select the forms you would like to export. ChargeForms will create a JSON file you can save and later bring back in with Import Forms.', 'chargeforms' ) ); ?>

[ 'type' => true, 'id' => true, 'name' => true, 'value' => true ] ] ); ?>
render_card_head( 'database-import', __( 'Import Forms', 'chargeforms' ), __( 'Upload a ChargeForms export file. Imported forms are added as new forms — nothing existing is overwritten.', 'chargeforms' ) ); ?>
render_card_head( 'upload', __( 'Import Entries', 'chargeforms' ), __( 'Upload a CSV in the same format ChargeForms exports — a "Date" column followed by one column per form field, in field order.', 'chargeforms' ) ); ?>

render_migrator_source( $source, $sources[ $source ] ); return; } ?>
render_card_head( 'migrate', __( 'Migrator', 'chargeforms' ), __( 'Bring forms in from another plugin already on this site. ChargeForms looks for real data — nothing here is guessed.', 'chargeforms' ) ); ?>

$info ) : ?> render_source_icon( $info ); ?> 0 ) : ?>
← render_card_head( $info['icon'], $info['label'], __( 'Each form imports as a new, separate ChargeForms form — nothing on the original plugin is changed or removed.', 'chargeforms' ), '', $info['logo'] ?? '' ); ?>
0 ) { printf( '

' . esc_html( /* translators: 1: number of forms detected, 2: source plugin name */ _n( '%1$d %2$s form was detected, but the field-mapping for this source isn\'t built yet.', '%1$d %2$s forms were detected, but the field-mapping for this source isn\'t built yet.', $info['count'], 'chargeforms' ) ) . '

', (int) $info['count'], esc_html( $info['label'] ) ); } else { printf( '

%s

', esc_html__( 'Not detected on this site, and the field-mapping for this source isn\'t built yet either way.', 'chargeforms' ) ); } ?>

$page, 'per_page' => 20 ] ); ?>
render_card_head( 'clock', __( 'Activity Logs', 'chargeforms' ), __( 'Who changed what across ChargeForms — forms created/edited/deleted, entries deleted, and settings saves.', 'chargeforms' ) ); ?>

render_simple_pagination( $result, [ 'page' => 'chargeforms-tools', 'section' => 'activity-logs' ] ); ?>
'plus-alt2', 'form_updated' => 'edit', 'form_deleted' => 'trash', 'entry_deleted' => 'trash', 'settings_updated' => 'admin-generic', ]; return $map[ $action ] ?? 'marker'; } /** Which of the semantic tints (success/danger/info) an activity's icon tile gets — a deletion should read as a warning-toned action at a glance, not visually identical to a routine edit. */ private function activity_icon_tone( $action ) { $map = [ 'form_created' => 'is-success', 'form_updated' => 'is-info', 'form_deleted' => 'is-danger', 'entry_deleted' => 'is-danger', 'settings_updated' => 'is-info', ]; return $map[ $action ] ?? ''; } private function render_api_logs() { $page = isset( $_GET['paged'] ) ? max( 1, (int) $_GET['paged'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $result = Api_Log::query( [ 'page' => $page, 'per_page' => 20 ] ); $forms = Form::all(); $by_id = []; foreach ( $forms as $form ) { $by_id[ $form['id'] ] = $form['title']; } ?>
render_card_head( 'networking', __( 'API Logs', 'chargeforms' ), __( 'Every outgoing webhook request ChargeForms has made, and how the endpoint responded — the last 500 calls are kept.', 'chargeforms' ) ); ?>

render_simple_pagination( $result, [ 'page' => 'chargeforms-tools', 'section' => 'api-logs' ] ); ?>
$page, 'per_page' => 20 ] ); $forms = Form::all(); $by_id = []; foreach ( $forms as $form ) { $by_id[ $form['id'] ] = $form['title']; } $status_labels = [ 'pending' => __( 'Pending', 'chargeforms' ), 'sent' => __( 'Sent', 'chargeforms' ), 'retrying' => __( 'Retrying', 'chargeforms' ), 'failed' => __( 'Failed', 'chargeforms' ), ]; ?>
render_card_head( 'email-alt', __( 'Notifications', 'chargeforms' ), __( 'Every owner-notification email ChargeForms has attempted to send, including automatic retries — a failed send is retried up to 3 times, 5 minutes apart, before being marked Failed for good.', 'chargeforms' ) ); ?>

render_simple_pagination( $result, [ 'page' => 'chargeforms-tools', 'section' => 'notifications' ] ); ?>
1 ) : ?> ‹ ›
render_card_head( 'admin-tools', __( 'System Status', 'chargeforms' ), __( 'A quick health check of this install — useful to paste into a support request.', 'chargeforms' ) ); ?>
prefix . $table; $exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $full_name ) ) === $full_name; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared printf( '%2$s%3$s', esc_attr( $exists ? 'is-ok' : 'is-missing' ), wp_kses( Icon::html( $exists ? 'check' : 'x' ), Icon::allowed_svg_html() ), esc_html( $table ) ); } ?>
$form['title'], 'fields' => $form['fields'], 'settings' => $form['settings'], ]; }, $forms ); nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="chargeforms-export-' . gmdate( 'Y-m-d' ) . '.json"' ); echo wp_json_encode( $export, JSON_PRETTY_PRINT ); exit; } public function handle_import() { if ( ! current_user_can( 'chargeforms_manage_forms' ) || ! check_admin_referer( 'chargeforms_import_forms' ) ) { wp_die( esc_html__( 'Not allowed.', 'chargeforms' ), 403 ); } $error = Import_Validator::validate( $_FILES['chargeforms_import_file'] ?? [], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Import_Validator::validate() itself is the sanitization/validation step for this raw upload array. [ 'json' ], [ 'application/json', 'text/plain' ], 5 * MB_IN_BYTES ); if ( $error ) { wp_safe_redirect( add_query_arg( 'chargeforms_error', rawurlencode( $error ), admin_url( 'admin.php?page=chargeforms-tools§ion=import-forms' ) ) ); exit; } // tmp_name is PHP's own upload-tmp-dir path, not attacker-supplied content — Import_Validator::validate() above already checked type/size. $contents = isset( $_FILES['chargeforms_import_file']['tmp_name'] ) ? file_get_contents( $_FILES['chargeforms_import_file']['tmp_name'] ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $data = json_decode( $contents, true ); if ( ! is_array( $data ) ) { wp_safe_redirect( add_query_arg( 'chargeforms_error', rawurlencode( __( 'That file is not a valid ChargeForms export.', 'chargeforms' ) ), admin_url( 'admin.php?page=chargeforms-tools§ion=import-forms' ) ) ); exit; } $imported = 0; foreach ( $data as $item ) { if ( ! is_array( $item ) || empty( $item['title'] ) ) { continue; } $form = Form::create( [ 'title' => $item['title'], 'fields' => $item['fields'] ?? [], 'settings' => $item['settings'] ?? [], ] ); if ( is_wp_error( $form ) ) { continue; } $imported++; } wp_safe_redirect( admin_url( 'admin.php?page=chargeforms-tools§ion=import-forms&chargeforms_imported=' . $imported ) ); exit; } /** * Delegates the actual CSV parsing to Entry_Importer::import_csv() — * the same class Entries_Page::handle_import() uses — instead of a * second, drifting copy of the row-parsing loop. Only the nonce and * redirect target differ between the two screens. */ public function handle_import_entries() { $form_id = isset( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0; if ( ! current_user_can( 'chargeforms_manage_forms' ) || ! check_admin_referer( 'chargeforms_tools_import_entries' ) ) { wp_die( esc_html__( 'Not allowed.', 'chargeforms' ), 403 ); } $form = Form::get( $form_id ); if ( ! $form ) { wp_die( esc_html__( 'Form not found.', 'chargeforms' ), 404 ); } $error = Import_Validator::validate( $_FILES['chargeforms_import_file'] ?? [], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Import_Validator::validate() itself is the sanitization/validation step for this raw upload array. [ 'csv' ], [ 'text/csv', 'text/plain', 'application/csv', 'application/vnd.ms-excel' ], 25 * MB_IN_BYTES ); if ( $error ) { wp_die( esc_html( $error ), 400 ); } $csv_fields = Entry_Exporter::data_fields_for( $form ); // tmp_name is PHP's own upload-tmp-dir path, not attacker-supplied content — Import_Validator::validate() above already checked type/size. $handle = isset( $_FILES['chargeforms_import_file']['tmp_name'] ) ? fopen( $_FILES['chargeforms_import_file']['tmp_name'], 'r' ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! $handle ) { wp_die( esc_html__( 'Could not read the uploaded file.', 'chargeforms' ), 400 ); } $imported = Entry_Importer::import_csv( $form_id, $csv_fields, $handle ); fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose wp_safe_redirect( admin_url( 'admin.php?page=chargeforms-tools§ion=import-entries&chargeforms_imported=' . $imported ) ); exit; } public function handle_migrate_import() { if ( ! current_user_can( 'chargeforms_manage_forms' ) || ! check_admin_referer( 'chargeforms_migrate_import' ) ) { wp_die( esc_html__( 'Not allowed.', 'chargeforms' ), 403 ); } $source = isset( $_GET['source'] ) ? sanitize_key( wp_unslash( $_GET['source'] ) ) : ''; $source_id = isset( $_GET['source_id'] ) ? (int) $_GET['source_id'] : 0; $sources = Migrator::detect_sources(); if ( ! $source_id || ! isset( $sources[ $source ] ) || ! $sources[ $source ]['buildable'] ) { wp_die( esc_html__( 'That migration source is not available.', 'chargeforms' ), 400 ); } $result = Migrator::import_from( $source, $source_id ); if ( is_wp_error( $result ) ) { wp_safe_redirect( admin_url( 'admin.php?page=chargeforms-tools§ion=migrator&migrate_source=' . $source . '&chargeforms_error=1' ) ); exit; } // Form::create() already logs a generic "Created form" activity entry — no need to double-log here. wp_safe_redirect( admin_url( 'admin.php?page=chargeforms-tools§ion=migrator&chargeforms_migrated=1' ) ); exit; } }