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();
?>
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' ),
],
];
?>
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-import',
__( 'Import Forms', 'chargeforms' ),
__( 'Upload a ChargeForms export file. Imported forms are added as new forms — nothing existing is overwritten.', 'chargeforms' )
);
?>
render_migrator_source( $source, $sources[ $source ] );
return;
}
?>
$page, 'per_page' => 20 ] );
?>
'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'];
}
?>
$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' ),
];
?>
$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;
}
}