has_cap( 'kconv_view_entries' ) && ! $user->has_cap( 'manage_options' ) && ( ! $requested || $requested === admin_url() ) ) { return admin_url( 'admin.php?page=kconv-entries' ); }
return $redirect;
}
public static function authorize( $cap, $nonce ) {
if ( ! KCONV_Core::can( $cap ) ) { wp_die( esc_html__( 'You do not have permission for this action.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
if ( ! isset( $_REQUEST['_wpnonce'] ) || ! is_string( $_REQUEST['_wpnonce'] ) ) { wp_nonce_ays( $nonce ); }
$token = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( ! wp_verify_nonce( $token, $nonce ) ) { wp_nonce_ays( $nonce ); }
}
public static function filters( $input ) {
$f = array( 'form_id' => 0, 'search' => '', 'from' => '', 'to' => '', 'read' => '', 'starred' => '', 'assigned' => '', 'time_display' => 'site', 'paged' => 1 );
foreach ( $f as $key => $unused ) { if ( isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ) { $f[ $key ] = sanitize_text_field( (string) $input[ $key ] ); } }
$f['form_id'] = absint( $f['form_id'] ); $f['search'] = mb_substr( $f['search'], 0, 200 );
$f['paged'] = max( 1, min( 100000, absint( $f['paged'] ) ) );
if ( ! in_array( $f['time_display'], array( 'site', 'utc' ), true ) ) { $f['time_display'] = 'site'; }
foreach ( array( 'from', 'to' ) as $key ) { if ( '' !== $f[ $key ] && ! KCONV_Core::valid_date( $f[ $key ] ) ) { return new WP_Error( 'date', __( 'Use valid UTC dates.', 'killerconversions' ) ); } }
if ( $f['from'] && $f['to'] && $f['from'] > $f['to'] ) { return new WP_Error( 'date', __( 'The end date must follow the start date.', 'killerconversions' ) ); }
if ( ! in_array( $f['read'], array( '', '0', '1' ), true ) || ! in_array( $f['starred'], array( '', '1' ), true ) || ( '' !== $f['assigned'] && ! ctype_digit( $f['assigned'] ) ) ) { return new WP_Error( 'filter', __( 'Invalid entry filter.', 'killerconversions' ) ); }
return $f;
}
public static function where( $filters ) {
global $wpdb;
$sql = KCONV_Entry_Access::where();
if ( $filters['form_id'] ) { $sql .= $wpdb->prepare( ' AND form_id=%d', $filters['form_id'] ); }
if ( $filters['search'] ) { $sql .= $wpdb->prepare( ' AND payload LIKE %s', '%' . $wpdb->esc_like( $filters['search'] ) . '%' ); }
if ( $filters['from'] ) { $sql .= $wpdb->prepare( ' AND created_at>=%s', $filters['from'] . ' 00:00:00' ); }
if ( $filters['to'] ) { $sql .= $wpdb->prepare( ' AND created_at<=%s', $filters['to'] . ' 23:59:59' ); }
if ( '' !== $filters['read'] ) { $sql .= $wpdb->prepare( ' AND is_read=%d', (int) $filters['read'] ); }
if ( $filters['starred'] ) { $sql .= ' AND starred=1'; }
if ( '' !== $filters['assigned'] ) { $sql .= $wpdb->prepare( ' AND assigned_to=%d', (int) $filters['assigned'] ); }
return $sql;
}
public static function operators() {
return get_users( array( 'capability__in' => array( 'manage_options', 'kconv_view_entries' ), 'number' => 200, 'orderby' => 'display_name', 'fields' => array( 'ID', 'display_name' ) ) );
}
private static function die_error( $error ) { wp_die( esc_html( $error->get_error_message() ), '', array( 'response' => 400, 'back_link' => true ) ); }
public static function assets( $hook ) {
if ( ! KCONV_Admin_Scope::plugin( array( 'kconv-entries' ) ) && ! ( KCONV_Admin_Scope::plugin( array( 'killerconversions' ) ) && ! current_user_can( 'manage_options' ) ) ) { return; }
wp_enqueue_style( 'kconv-inbox', KCONV_URL . 'assets/inbox.css', array( 'kconv-admin' ), KCONV_VERSION );
wp_enqueue_script( 'kconv-inbox', KCONV_URL . 'assets/inbox.js', array('wp-i18n'), KCONV_VERSION, true ); wp_set_script_translations( 'kconv-inbox', 'killerconversions', KCONV_DIR . 'languages' );
}
/** Saved definitions are historical evidence; never silently backfill legacy entries. */
public static function definitions( $row ) {
$raw = isset( $row['schema_snapshot'] ) ? $row['schema_snapshot'] : null;
$snapshot = is_string( $raw ) ? json_decode( $raw, true ) : null;
$valid = is_array( $snapshot ) && isset( $snapshot['version'], $snapshot['title'], $snapshot['fields'], $snapshot['revision'] ) && in_array( $snapshot['version'], array( 1, 2, 3 ), true ) && is_string( $snapshot['title'] ) && is_array( $snapshot['fields'] ) && count( $snapshot['fields'] ) <= KCONV_Core::MAX_FIELDS && is_string( $snapshot['revision'] ) && preg_match( '/^[a-f0-9]{64}$/D', $snapshot['revision'] );
$fields = array();
if ( $valid ) {
foreach ( $snapshot['fields'] as $field ) {
if ( ! is_array( $field ) || ! isset( $field['id'], $field['label'], $field['type'] ) || ! is_string( $field['id'] ) || ! preg_match( '/^[a-z][a-z0-9_]{0,39}$/D', $field['id'] ) || isset( $fields[ $field['id'] ] ) || ! is_string( $field['label'] ) || ! is_string( $field['type'] ) ) { $valid = false; break; }
$fields[ $field['id'] ] = array_intersect_key( $field, array_flip( array( 'label', 'type', 'repeater' ) ) );
}
}
if ( $valid ) { return array( 'source' => 'submission', 'title' => $snapshot['title'], 'fields' => $fields, 'revision' => $snapshot['revision'] ); }
$form = KCONV_Core::get_form( $row['form_id'] ); $fields = array();
if ( $form ) { foreach ( $form['fields'] as $field ) { $fields[ $field['id'] ] = array_intersect_key( $field, array_flip( array( 'label', 'type', 'repeater' ) ) ); } }
return array( 'source' => null === $raw || '' === $raw ? 'legacy-current-labels' : 'snapshot-unavailable-current-labels', 'title' => $form ? $form['title'] : __( 'Deleted form', 'killerconversions' ), 'fields' => $fields, 'revision' => '' );
}
public static function display_time( $utc, $mode = 'site' ) {
$date = DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', (string) $utc, new DateTimeZone( 'UTC' ) );
if ( ! $date || $date->format( 'Y-m-d H:i:s' ) !== $utc ) { return __( 'Time unavailable', 'killerconversions' ); }
$zone = 'utc' === $mode ? new DateTimeZone( 'UTC' ) : wp_timezone();
return wp_date( 'Y-m-d H:i:s', $date->getTimestamp(), $zone ) . ' ' . $zone->getName();
}
public static function return_context( $input, $form_id = 0 ) {
$raw = isset( $input['return_filters'] ) && is_array( $input['return_filters'] ) ? $input['return_filters'] : array( 'form_id' => $form_id );
$filters = self::filters( $raw );
return is_wp_error( $filters ) ? self::filters( array( 'form_id' => $form_id ) ) : $filters;
}
/** Identity is display-only; saved answers are never rewritten or inferred into new records. */
private static function identity( $row ) {
$values = (array) json_decode( $row['payload'], true ); $name = ''; $email = ''; $used = array();
foreach ( array( 'name', 'full_name', 'fullname', 'your_name', 'contact_name' ) as $key ) {
if ( isset( $values[ $key ] ) && is_scalar( $values[ $key ] ) && '' !== trim( (string) $values[ $key ] ) ) { $name = trim( (string) $values[ $key ] ); $used[] = $key; break; }
}
if ( ! $name && isset( $values['first_name'] ) && is_string( $values['first_name'] ) ) {
$name = trim( $values['first_name'] . ' ' . ( isset( $values['last_name'] ) && is_string( $values['last_name'] ) ? $values['last_name'] : '' ) ); $used = array( 'first_name', 'last_name' );
}
$definitions = self::definitions( $row );
foreach ( $definitions['fields'] as $id => $field ) {
if ( 'email' === $field['type'] && isset( $values[ $id ] ) && is_string( $values[ $id ] ) && is_email( $values[ $id ] ) ) { $email = $values[ $id ]; $used[] = $id; break; }
}
if ( ! $email && isset( $values['email'] ) && is_string( $values['email'] ) && is_email( $values['email'] ) ) { $email = $values['email']; $used[] = 'email'; }
$snippet = '';
foreach ( $values as $key => $value ) {
if ( in_array( $key, $used, true ) || ! is_scalar( $value ) || '' === trim( (string) $value ) ) { continue; }
$snippet = preg_replace( '/\s+/u', ' ', (string) $value ); break;
}
/* translators: %d: Stored entry ID. */
$name = $name ? mb_substr( $name, 0, 120 ) : ( $email ?: sprintf( __( 'Entry #%d', 'killerconversions' ), $row['id'] ) );
$initial = mb_substr( $name, 0, 1 );
$initial = function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $initial ) : strtoupper( $initial );
return array( 'name' => $name, 'email' => $email, 'initial' => $initial, 'snippet' => mb_substr( (string) $snippet, 0, 100 ) );
}
private static function mail_badge( $status ) {
$labels = array( 'disabled' => __( 'Staff email off', 'killerconversions' ), 'pending' => __( 'Pending', 'killerconversions' ), 'queued' => __( 'Queued', 'killerconversions' ), 'processing' => __( 'Sending', 'killerconversions' ), 'retry' => __( 'Retry scheduled', 'killerconversions' ), 'accepted' => __( 'Accepted for sending', 'killerconversions' ), 'mail_accepted' => __( 'Accepted for sending', 'killerconversions' ), 'failed' => __( 'Needs attention', 'killerconversions' ), 'mail_failed' => __( 'Needs attention', 'killerconversions' ), 'cancelled' => __( 'Cancelled', 'killerconversions' ), 'blocked' => __( 'Blocked', 'killerconversions' ), 'exhausted' => __( 'Retries exhausted', 'killerconversions' ) );
$tone = in_array( $status, array( 'failed', 'mail_failed', 'blocked', 'exhausted' ), true ) ? 'warning' : ( in_array( $status, array( 'accepted', 'mail_accepted' ), true ) ? 'success' : 'neutral' );
echo '' . esc_html( isset( $labels[ $status ] ) ? $labels[ $status ] : ucwords( str_replace( '_', ' ', $status ) ) ) . ' ';
}
private static function return_fields( $filters ) {
foreach ( $filters as $key => $value ) { echo ' '; }
}
private static function workflow_options() {
if ( KCONV_Core::can( 'kconv_review_entries' ) ) {
foreach ( array( 'read' => __( 'Mark read', 'killerconversions' ), 'unread' => __( 'Mark unread', 'killerconversions' ), 'star' => __( 'Star', 'killerconversions' ), 'unstar' => __( 'Remove star', 'killerconversions' ), 'assign' => __( 'Assign', 'killerconversions' ) ) as $value => $label ) { if ( 'assign' === $value && ! apply_filters( 'kconv_entry_assignment_inline', true ) ) { continue; } echo '' . esc_html( $label ) . ' '; }
}
if ( KCONV_Core::can( 'kconv_export_entries' ) ) { echo '' . esc_html__( 'Export selected', 'killerconversions' ) . ' '; }
if ( KCONV_Core::can( 'kconv_delete_entries' ) ) { echo '' . esc_html__( 'Permanently delete', 'killerconversions' ) . ' '; }
}
public static function screen() {
if ( ! KCONV_Core::can( 'kconv_view_entries' ) ) { return; }
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards.
$id = isset( $_GET['entry_id'] ) && is_scalar( $_GET['entry_id'] ) ? absint( $_GET['entry_id'] ) : 0;
if ( $id ) { self::detail( $id ); return; }
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards.
$f = self::filters( wp_unslash( $_GET ) ); if ( is_wp_error( $f ) ) { self::die_error( $f ); }
global $wpdb; $table = KCONV_Core::table( 'entries' ); $where = self::where( $f );
// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows.
$total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE $where", $table ) );
if ( $wpdb->last_error ) { self::die_error( new WP_Error( 'storage', __( 'Entries could not be read. Please retry after storage is repaired.', 'killerconversions' ) ) ); }
$pages = max( 1, (int) ceil( $total / 25 ) );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards.
$page = isset( $_GET['paged'] ) && is_scalar( $_GET['paged'] ) ? min( $pages, max( 1, absint( $_GET['paged'] ) ) ) : 1;
// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows.
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE $where ORDER BY id DESC LIMIT 25 OFFSET %d", $table, ( $page - 1 ) * 25 ), ARRAY_A );
if ( $wpdb->last_error ) { self::die_error( new WP_Error( 'storage', __( 'Entries could not be read. Please retry after storage is repaired.', 'killerconversions' ) ) ); }
$f['paged'] = $page;
$base = $f; $base['read'] = ''; $base['starred'] = ''; $base['assigned'] = ''; $count_where = self::where( $base );
// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows.
$counts = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS all_entries,SUM(is_read=0) AS unread,SUM(starred=1) AS starred,SUM(assigned_to=%d) AS mine FROM %i WHERE $count_where", get_current_user_id(), $table ), ARRAY_A );
if ( $wpdb->last_error || ! is_array( $counts ) ) { self::die_error( new WP_Error( 'storage', __( 'Entry counts could not be read. Please retry after storage is repaired.', 'killerconversions' ) ) ); }
$operators = self::operators();
$can_bulk = KCONV_Core::can( 'kconv_review_entries' ) || KCONV_Core::can( 'kconv_export_entries' ) || KCONV_Core::can( 'kconv_delete_entries' );
$active_filters = 0;
foreach ( array( 'form_id', 'search', 'from', 'to', 'read', 'starred', 'assigned' ) as $key ) {
if ( in_array( $key, array( 'form_id', 'search' ), true ) ? (bool) $f[ $key ] : '' !== $f[ $key ] ) { ++$active_filters; }
}
echo '
';
if ( is_callable( array( 'KCONV_Admin', 'navigation' ) ) ) { KCONV_Admin::navigation( 'entries' ); }
echo '
';
echo '
';
if ( class_exists( 'KCONV_Operations' ) ) { KCONV_Operations::panel(); }
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards.
if ( isset( $_GET['updated'] ) && '1' === $_GET['updated'] ) { echo '
' . esc_html__( 'Entry changes saved.', 'killerconversions' ) . '
'; }
echo '
';
$views = array( 'all_entries' => array( __( 'All entries', 'killerconversions' ), array(), '' === $f['read'] && ! $f['starred'] && '' === $f['assigned'] ), 'unread' => array( __( 'Unread', 'killerconversions' ), array( 'read' => '0' ), '0' === $f['read'] && ! $f['starred'] && '' === $f['assigned'] ), 'starred' => array( __( 'Starred', 'killerconversions' ), array( 'starred' => '1' ), '' === $f['read'] && '1' === $f['starred'] && '' === $f['assigned'] ), 'mine' => array( __( 'Assigned to me', 'killerconversions' ), array( 'assigned' => get_current_user_id() ), '' === $f['read'] && ! $f['starred'] && (string) get_current_user_id() === $f['assigned'] ) );
foreach ( $views as $key => $view ) {
echo '' . esc_html( $view[0] ) . ' ' . esc_html( number_format_i18n( (int) $counts[ $key ] ) ) . ' ';
}
echo ' ' . esc_html__( 'Search & filters', 'killerconversions' );
if ( $active_filters ) {
/* translators: %d: Number of active entry filters. */
echo '' . esc_html( sprintf( __( '%d active', 'killerconversions' ), $active_filters ) ) . ' ';
}
echo ' ';
echo '';
if ( $can_bulk ) { echo '
' . esc_html__( 'Select page', 'killerconversions' ) . ' '; }
echo '
' . esc_html( number_format_i18n( $total ) ) . ' ' . esc_html( _n( 'matching entry', 'matching entries', $total, 'killerconversions' ) ) . '
';
if ( KCONV_Core::can( 'kconv_export_entries' ) ) {
echo '
' . esc_html__( 'Export CSV', 'killerconversions' ) . ' ↓ ';
}
echo '
';
if ( $can_bulk ) {
echo '';
}
echo '';
if ( $can_bulk ) { echo '' . esc_html__( 'Select', 'killerconversions' ) . ' '; }
echo '' . esc_html__( 'Lead', 'killerconversions' ) . ' ' . esc_html__( 'Form & submitted', 'killerconversions' ) . ' ' . esc_html__( 'Owner', 'killerconversions' ) . ' ' . esc_html__( 'Email & delivery', 'killerconversions' ) . ' ' . esc_html__( 'Open entry', 'killerconversions' ) . ' ';
foreach ( $rows as $row ) {
$identity = self::identity( $row ); $definitions = self::definitions( $row ); $url = add_query_arg( array( 'page' => 'kconv-entries', 'entry_id' => $row['id'], 'return_filters' => $f ), admin_url( 'admin.php' ) );
echo '';
/* translators: %d: Entry ID. */
if ( $can_bulk ) { echo ' '; }
echo '' . esc_html( $identity['initial'] ) . ' ' . esc_html( $identity['name'] ) . ' ' . ( $row['starred'] ? '
★ ' : '' );
if ( $identity['email'] && $identity['email'] !== $identity['name'] ) { echo '
' . esc_html( $identity['email'] ) . ' '; }
if ( $identity['snippet'] ) { echo '
' . esc_html( $identity['snippet'] ) . ' '; }
echo '
' . esc_html__( 'Form & submitted', 'killerconversions' ) . ' ' . esc_html( $definitions['title'] ) . ' #' . esc_html( $row['id'] ) . ' · ' . esc_html( self::display_time( $row['created_at'], $f['time_display'] ) ) . ' ' . esc_html( $row['is_read'] ? __( 'Read', 'killerconversions' ) : __( 'Unread', 'killerconversions' ) ) . '
';
$assignee = $row['assigned_to'] ? get_userdata( $row['assigned_to'] ) : false;
echo '' . esc_html__( 'Owner', 'killerconversions' ) . ' ' . esc_html( $assignee ? $assignee->display_name : __( 'Unassigned', 'killerconversions' ) ) . '
' . esc_html__( 'Email & delivery', 'killerconversions' ) . ' '; self::mail_badge( $row['mail_status'] ); do_action( 'kconv_admin_entry_actions', $row );
/* translators: %d: Entry ID. */
echo '
' . esc_html__( 'Open entry', 'killerconversions' ) . ' ';
}
if ( ! $rows ) { echo ' '; }
echo '
';
KCONV_Pro_Discovery::hint( 'review' );
echo '
';
}
public static function detail( $id ) {
if ( ! KCONV_Core::can( 'kconv_view_entries' ) ) { return; }
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows.
$row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id=%d', KCONV_Core::table( 'entries' ), $id ), ARRAY_A );
if ( $wpdb->last_error ) { self::die_error( new WP_Error( 'storage', __( 'This entry could not be read. Please retry after storage is repaired.', 'killerconversions' ) ) ); }
if ( $row && ! KCONV_Entry_Access::allowed( $row, 'view' ) ) { wp_die( esc_html__( 'You do not have access to this entry.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
if ( ! $row ) { echo ''; return; }
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only detail() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards.
$identity = self::identity( $row ); $definitions = self::definitions( $row ); $context = self::return_context( wp_unslash( $_GET ), $row['form_id'] );
$back = add_query_arg( array_merge( $context, array( 'page' => 'kconv-entries' ) ), admin_url( 'admin.php' ) );
echo '';
if ( is_callable( array( 'KCONV_Admin', 'navigation' ) ) ) { KCONV_Admin::navigation( 'entries' ); }
echo '
';
echo '
' . esc_html( is_rtl() ? '→' : '←' ) . ' ' . esc_html__( 'Back to inbox', 'killerconversions' ) . '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only detail() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards.
if ( isset( $_GET['updated'] ) && '1' === $_GET['updated'] ) { echo '
' . esc_html__( 'Entry changes saved.', 'killerconversions' ) . '
'; }
$answer_values = (array) json_decode( $row['payload'], true );
$answer_count = count( array_diff_key( $answer_values, array( '__kconv_estimate' => true, '__kconv_document' => true ) ) );
/* translators: %s: Number of submitted answer fields, excluding generated estimate and document records. */
$answer_count_label = sprintf( _n( '%s field', '%s fields', $answer_count, 'killerconversions' ), number_format_i18n( $answer_count ) );
echo '
' . esc_html__( 'Submitted answers', 'killerconversions' ) . ' ' . esc_html( $answer_count_label ) . ' ' . esc_html( 'submission' === $definitions['source'] ? __( 'Field labels and types were saved with this submission.', 'killerconversions' ) : ( 'legacy-current-labels' === $definitions['source'] ? __( 'Legacy entry: original field definitions were not saved. Labels shown come from the current form where available.', 'killerconversions' ) : __( 'The saved field definitions are unavailable. Current form labels are shown where available; original answers are unchanged.', 'killerconversions' ) ) ) . '
';
foreach ( $answer_values as $key => $value ) { if ( '__kconv_document' === $key ) { continue; } if ( '__kconv_estimate' === $key && class_exists( 'KCONV_Saved_Estimates' ) ) { echo '
' . esc_html__( 'Saved estimate', 'killerconversions' ) . ' ' . esc_html( KCONV_Saved_Estimates::text( $value ) ) . ' '; continue; } $field = isset( $definitions['fields'][ $key ] ) ? $definitions['fields'][ $key ] : array( 'label' => $key, 'type' => '' ); $display_value = 'submission' === $definitions['source'] && 'checkbox' === $field['type'] && in_array( $value, array( '1', '0', '' ), true ) ? ( '1' === $value ? __( 'Checked', 'killerconversions' ) : __( 'Not checked', 'killerconversions' ) ) : KCONV_Core::value_text( $value ); echo '
' . esc_html( $field['label'] ) . '' . esc_html( $key . ( $field['type'] ? ' · ' . $field['type'] : '' ) ) . ' ' . esc_html( $display_value ) . ' '; }
echo ' '; do_action( 'kconv_admin_entry_detail', $row ); echo ' ';
if ( KCONV_Core::can( 'kconv_review_entries' ) ) {
echo '' . esc_html__( 'Next steps', 'killerconversions' ) . ' ';
}
echo '' . esc_html__( 'Delivery & files', 'killerconversions' ) . ' '; self::mail_badge( $row['mail_status'] );
echo '' . esc_html__( 'This badge shows the staff notification status. Accepted for sending does not confirm inbox delivery. Check Delivery for visitor confirmations and other destinations.', 'killerconversions' ) . '
'; do_action( 'kconv_admin_entry_actions', $row ); echo '' . esc_html__( 'Retention', 'killerconversions' ) . ' ' . esc_html__( 'Retained until', 'killerconversions' ) . ' ' . esc_html( self::display_time( $row['expires_at'], $context['time_display'] ) ) . ' .
' . esc_html__( 'Scheduled deletion depends on WordPress cron.', 'killerconversions' ) . '
';
if ( KCONV_Core::can( 'kconv_delete_entries' ) ) { echo ''; }
echo ' ';
}
public static function bulk() {
if ( ! KCONV_Core::can( 'kconv_view_entries' ) ) { wp_die( esc_html__( 'You do not have permission for this action.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) ) { wp_die( esc_html__( 'You do not have permission for this action.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
$nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'kconv_entries_bulk' ) ) { wp_die( esc_html__( 'You do not have permission for this action.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- IDs are unslashed, restricted to scalar values and normalized with absint; return filters use the bounded filters() validator.
$ids = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? array_values( array_unique( array_filter( array_map( 'absint', array_filter( wp_unslash( $_POST['ids'] ), 'is_scalar' ) ) ) ) ) : array();
if ( ! $ids || count( $ids ) > 100 ) { self::die_error( new WP_Error( 'selection', __( 'Select between 1 and 100 entries.', 'killerconversions' ) ) ); }
$operation = isset( $_POST['operation'] ) && is_string( $_POST['operation'] ) ? sanitize_key( $_POST['operation'] ) : '';
if ( ! KCONV_Entry_Access::assert_ids( $ids, in_array( $operation, array( 'delete', 'export' ), true ) ? $operation : 'review' ) ) { wp_die( esc_html__( 'You do not have access to every selected entry.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
if ( 'delete' === $operation ) {
if ( ! KCONV_Core::can( 'kconv_delete_entries' ) || ! isset( $_POST['confirm_delete'] ) || '1' !== $_POST['confirm_delete'] ) { self::die_error( new WP_Error( 'confirm', __( 'Deletion requires permission and explicit confirmation.', 'killerconversions' ) ) ); }
if ( ! KCONV_Core::delete_entries( $ids, true ) ) { self::die_error( new WP_Error( 'storage', __( 'Entry deletion failed and was rolled back. Please repair storage and retry.', 'killerconversions' ) ) ); }
} elseif ( 'export' === $operation ) {
if ( ! KCONV_Core::can( 'kconv_export_entries' ) ) { wp_die( 'Forbidden', '', array( 'response' => 403 ) ); }
self::export( array( 'layout' => 'wide' ), $ids );
} else {
$result = self::update_workflow( $ids, $operation, isset( $_POST['assigned_to'] ) && is_scalar( $_POST['assigned_to'] ) ? absint( $_POST['assigned_to'] ) : 0 );
if ( is_wp_error( $result ) ) { self::die_error( $result ); }
}
// Return only to known inbox routes; submitted URLs are never trusted.
$return_entry = isset( $_POST['return_entry'] ) && is_scalar( $_POST['return_entry'] ) ? absint( $_POST['return_entry'] ) : 0;
if ( $return_entry && in_array( $return_entry, $ids, true ) && 'delete' !== $operation ) {
$return = array( 'page' => 'kconv-entries', 'entry_id' => $return_entry, 'updated' => '1', 'return_filters' => self::return_context( wp_unslash( $_POST ) ) );
} else {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- IDs are unslashed, restricted to scalar values and normalized with absint; return filters use the bounded filters() validator.
$return_filters = isset( $_POST['return_filters'] ) && is_array( $_POST['return_filters'] ) ? self::filters( wp_unslash( $_POST['return_filters'] ) ) : array();
$return = array_merge( is_wp_error( $return_filters ) ? array() : $return_filters, array( 'page' => 'kconv-entries', 'updated' => '1' ) );
}
wp_safe_redirect( add_query_arg( $return, admin_url( 'admin.php' ) ) ); exit;
}
public static function update_workflow( $ids, $operation, $assignee = 0 ) {
if ( ! KCONV_Core::can( 'kconv_review_entries' ) ) { return new WP_Error( 'forbidden', __( 'Lead review access required.', 'killerconversions' ) ); }
$map = array( 'read' => array( 'is_read', 1 ), 'unread' => array( 'is_read', 0 ), 'star' => array( 'starred', 1 ), 'unstar' => array( 'starred', 0 ), 'assign' => array( 'assigned_to', $assignee ) );
if ( ! isset( $map[ $operation ] ) ) { return new WP_Error( 'operation', __( 'Choose a supported action.', 'killerconversions' ) ); }
if ( 'assign' === $operation && $assignee && ! user_can( $assignee, 'kconv_view_entries' ) && ! user_can( $assignee, 'manage_options' ) ) { return new WP_Error( 'assignee', __( 'Assign entries only to a user who can view them.', 'killerconversions' ) ); }
$ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $ids ) ) ) );
if ( ! $ids || count( $ids ) > 100 ) { return new WP_Error( 'selection', __( 'Select 1–100 entries.', 'killerconversions' ) ); }
if ( ! KCONV_Entry_Access::assert_ids( $ids, 'review' ) ) { return new WP_Error( 'forbidden', __( 'The selection is outside your review access.', 'killerconversions' ) ); }
$override = apply_filters( 'kconv_entry_workflow_override', null, $ids, $operation, $assignee );
if ( null !== $override ) { return $override; }
global $wpdb; list( $column, $value ) = $map[ $operation ]; $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows. Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows. Generated placeholders match the bounded ID array; PHPCS cannot statically count the generated list.
$ok = $wpdb->query( $wpdb->prepare( "UPDATE %i SET %i=%d WHERE id IN ($placeholders) AND " . KCONV_Entry_Access::where( '', 'review' ), array_merge( array( KCONV_Core::table( 'entries' ), $column, $value ), $ids ) ) );
return false === $ok ? new WP_Error( 'storage', __( 'The entry update failed. Please retry.', 'killerconversions' ) ) : true;
}
/**
* Stage and verify the whole export before sending any download headers.
* php://temp spills beyond 2 MiB to a private process-owned temporary file;
* both temporary streams close on success/failure and no public file exists.
*/
public static function prepare_export( $input, $ids = array() ) {
if ( ! KCONV_Core::can( 'kconv_export_entries' ) ) { return new WP_Error( 'forbidden', __( 'Entry export access required.', 'killerconversions' ) ); }
$f = self::filters( $input ); if ( is_wp_error( $f ) ) { return $f; }
$wide = ! isset( $input['layout'] ) || 'long' !== $input['layout'];
global $wpdb; $table = KCONV_Core::table( 'entries' ); $where = self::where( $f ) . ' AND ' . KCONV_Entry_Access::where( '', 'export' );
if ( $ids && ! KCONV_Entry_Access::assert_ids( $ids, 'export' ) ) { return new WP_Error( 'forbidden', __( 'The selection is outside your export access.', 'killerconversions' ) ); }
if ( $ids ) { $ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $ids ) ) ) ); if ( ! $ids || count( $ids ) > 100 ) { return new WP_Error( 'selection', __( 'Select 1–100 entries.', 'killerconversions' ) ); } $where .= ' AND id IN (' . implode( ',', $ids ) . ')'; }
$max_rows = max( 1, min( 100000, (int) apply_filters( 'kconv_export_max_rows', 100000 ) ) );
$max_bytes = max( 1024, min( 67108864, (int) apply_filters( 'kconv_export_max_bytes', 67108864 ) ) );
$deadline = microtime( true ) + 45;
$source = false; $output = false;
try {
// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows.
$selection = $wpdb->get_row( $wpdb->prepare( "SELECT MAX(id) AS ceiling,COUNT(*) AS expected FROM %i WHERE $where", $table ), ARRAY_A );
if ( $wpdb->last_error || ! is_array( $selection ) || ! isset( $selection['expected'] ) ) { throw new RuntimeException( __( 'Entry export could not read its selection. Repair storage and retry; no CSV was downloaded.', 'killerconversions' ) ); }
$expected = (int) $selection['expected'];
if ( $expected > $max_rows ) { throw new RuntimeException( __( 'This export has too many entries. Narrow the form or date filters and retry.', 'killerconversions' ) ); }
$where .= $wpdb->prepare( ' AND id<=%d', (int) $selection['ceiling'] );
$source = fopen( 'php://temp/maxmemory:2097152', 'w+b' );
$output = fopen( 'php://temp/maxmemory:2097152', 'w+b' );
if ( ! is_resource( $source ) || ! is_resource( $output ) ) { throw new RuntimeException( __( 'Private export storage is unavailable. Nothing was downloaded.', 'killerconversions' ) ); }
$columns = array(); $cursor = 0; $count = 0;
do {
// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Inbox/export/bulk queries use validated filters and entry scope; scalar values are prepared, IDs are normalized with absint and operation columns come from a fixed map. Counts and export snapshots deliberately read current private rows.
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE $where AND id>%d ORDER BY id LIMIT 100", $table, $cursor ), ARRAY_A );
if ( $wpdb->last_error || ! is_array( $rows ) ) { throw new RuntimeException( __( 'Entry export stopped because a storage read failed. No partial CSV was downloaded; repair storage and retry.', 'killerconversions' ) ); }
foreach ( $rows as $row ) {
if ( microtime( true ) > $deadline || ++$count > $max_rows ) { throw new RuntimeException( __( 'This export exceeds the preparation limit. Narrow its filters and retry.', 'killerconversions' ) ); }
$cursor = (int) $row['id']; $values = json_decode( $row['payload'], true );
if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $values ) ) { throw new RuntimeException( __( 'An entry contains unreadable stored answers. Repair it before exporting; no incomplete CSV was downloaded.', 'killerconversions' ) ); }
$definitions = self::definitions( $row );
foreach ( $values as $key => $value ) {
$label = isset( $definitions['fields'][ $key ] ) ? $definitions['fields'][ $key ]['label'] : $key;
if ( ! isset( $columns[ $key ] ) ) { $columns[ $key ] = array( 'label' => $label, 'varied' => false ); }
elseif ( $columns[ $key ]['label'] !== $label ) { $columns[ $key ]['varied'] = true; }
}
if ( $wide && count( $columns ) > 250 ) { throw new RuntimeException( __( 'This selection has more than 250 distinct fields. Filter one form or choose one row per field.', 'killerconversions' ) ); }
$record = wp_json_encode( array( 'row' => $row, 'values' => $values, 'definitions' => $definitions ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Private bounded php://temp streams stage and verify the complete CSV before headers; these are process streams, not WP_Filesystem-managed site files. They close on both success and failure.
if ( ! is_string( $record ) || strlen( $record ) + 1 > $max_bytes - ftell( $source ) || strlen( $record ) + 1 !== fwrite( $source, $record . "\n" ) ) { throw new RuntimeException( __( 'This export exceeds private temporary storage or could not be written. Narrow its filters and retry.', 'killerconversions' ) ); }
}
} while ( count( $rows ) === 100 );
if ( $count !== $expected ) { throw new RuntimeException( __( 'The entry selection changed while preparing the export. Please retry; no incomplete CSV was downloaded.', 'killerconversions' ) ); }
if ( ! rewind( $source ) ) { throw new RuntimeException( __( 'Prepared entry data could not be reopened. Nothing was downloaded.', 'killerconversions' ) ); }
ksort( $columns, SORT_STRING );
$base = array( 'Entry ID', 'Form ID', 'Submitted UTC', 'Email status', 'Read', 'Starred', 'Assigned user ID', 'Form title', 'Field definitions source', 'Form revision' );
$headers = array();
if ( $wide ) {
foreach ( $columns as $key => $definition ) { $headers[] = KCONV_Admin::csv_value( $definition['varied'] ? $key . ' [labels vary; see field definitions]' : $definition['label'] . ' [' . $key . ']' ); }
$headers[] = 'Field definitions (JSON)';
} else { $headers = array( 'Field ID', 'Field label', 'Field type', 'Value' ); }
self::write_csv_row( $output, array_merge( $base, $headers ), $max_bytes );
$converted = 0;
while ( false !== ( $line = fgets( $source ) ) ) {
if ( microtime( true ) > $deadline ) { throw new RuntimeException( __( 'This export exceeds the preparation time limit. Narrow its filters and retry.', 'killerconversions' ) ); }
$record = json_decode( $line, true );
if ( ! is_array( $record ) || ! isset( $record['row'], $record['values'], $record['definitions'] ) ) { throw new RuntimeException( __( 'Prepared export data could not be verified. Nothing was downloaded.', 'killerconversions' ) ); }
$row = $record['row']; $values = $record['values']; $definitions = $record['definitions']; $converted++;
$prefix = array( $row['id'], $row['form_id'], $row['created_at'], $row['mail_status'], $row['is_read'], $row['starred'], $row['assigned_to'], KCONV_Admin::csv_value( $definitions['title'] ), $definitions['source'], $definitions['revision'] );
if ( $wide ) {
$data = $prefix;
foreach ( $columns as $key => $unused ) { $data[] = isset( $values[ $key ] ) ? KCONV_Admin::csv_value( $values[ $key ] ) : ''; }
$data[] = wp_json_encode( $definitions['fields'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
self::write_csv_row( $output, $data, $max_bytes );
} else {
foreach ( $values as $key => $value ) {
$definition = isset( $definitions['fields'][ $key ] ) ? $definitions['fields'][ $key ] : array( 'label' => $key, 'type' => '' );
self::write_csv_row( $output, array_merge( $prefix, array( KCONV_Admin::csv_value( $key ), KCONV_Admin::csv_value( $definition['label'] ), KCONV_Admin::csv_value( $definition['type'] ), KCONV_Admin::csv_value( $value ) ) ), $max_bytes );
}
}
}
if ( ! feof( $source ) || $converted !== $count || ! fflush( $output ) ) { throw new RuntimeException( __( 'Export preparation did not complete. No partial CSV was downloaded.', 'killerconversions' ) ); }
$bytes = ftell( $output );
if ( false === $bytes || ! rewind( $output ) ) { throw new RuntimeException( __( 'The completed CSV could not be verified. Nothing was downloaded.', 'killerconversions' ) ); }
$hash = hash_init( 'sha256' ); $verified = hash_update_stream( $hash, $output );
if ( $verified !== $bytes || ! rewind( $output ) ) { throw new RuntimeException( __( 'The completed CSV could not be verified. Nothing was downloaded.', 'killerconversions' ) ); }
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Private bounded php://temp streams stage and verify the complete CSV before headers; these are process streams, not WP_Filesystem-managed site files. They close on both success and failure.
fclose( $source );
return array( 'stream' => $output, 'rows' => $count, 'bytes' => $bytes, 'sha256' => hash_final( $hash ) );
} catch ( Throwable $error ) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Private bounded php://temp streams stage and verify the complete CSV before headers; these are process streams, not WP_Filesystem-managed site files. They close on both success and failure.
if ( is_resource( $source ) ) { fclose( $source ); }
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Private bounded php://temp streams stage and verify the complete CSV before headers; these are process streams, not WP_Filesystem-managed site files. They close on both success and failure.
if ( is_resource( $output ) ) { fclose( $output ); }
return new WP_Error( 'export_preparation', $error instanceof RuntimeException ? $error->getMessage() : __( 'Export preparation failed. No partial CSV was downloaded.', 'killerconversions' ) );
}
}
private static function write_csv_row( $stream, $row, $max_bytes ) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- write_csv_row() throws a plain-text exception caught by prepare_export() and returned as WP_Error; die_error() escapes the message at its HTML output boundary. Escaping here would double-escape translations.
if ( false === fputcsv( $stream, $row, ',', '"', '' ) || ftell( $stream ) > $max_bytes ) { throw new RuntimeException( __( 'The CSV exceeds the export size limit or could not be written. Narrow its filters and retry.', 'killerconversions' ) ); }
}
public static function export( $input, $ids = array() ) {
$prepared = self::prepare_export( $input, $ids );
if ( is_wp_error( $prepared ) ) { self::die_error( $prepared ); }
nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="killerconversions-entries-' . gmdate( 'Y-m-d' ) . '.csv"' ); header( 'X-Content-Type-Options: nosniff' );
header( 'Content-Length: ' . $prepared['bytes'] );
header( 'X-KillerConversions-Export-Rows: ' . $prepared['rows'] );
header( 'X-KillerConversions-Export-SHA256: ' . $prepared['sha256'] );
fpassthru( $prepared['stream'] );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Private bounded php://temp streams stage and verify the complete CSV before headers; these are process streams, not WP_Filesystem-managed site files. They close on both success and failure.
fclose( $prepared['stream'] ); // Closing private temporary stream; no retained export artifact.
exit;
}
public static function clone_form( $id ) {
if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'forbidden', __( 'Form administration required.', 'killerconversions' ) ); }
// Extension operations hold their guard until all reads/writes and rollback complete.
try {
$allowed = apply_filters( 'kconv_clone_operation_allowed', true, $id );
if ( is_wp_error( $allowed ) ) { return $allowed; }
if ( true !== $allowed ) { return new WP_Error( 'kconv_operation_unavailable', __( 'This operation is unavailable. No saved data has changed.', 'killerconversions' ), array( 'status' => 403 ) ); }
return self::clone_configuration( $id );
} finally { do_action( 'kconv_clone_operation_finished', $id ); }
}
private static function clone_configuration( $id ) {
if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'forbidden', __( 'Form administration required.', 'killerconversions' ) ); }
$form = KCONV_Core::get_form( $id ); if ( ! $form ) { return new WP_Error( 'missing', __( 'Form unavailable.', 'killerconversions' ) ); }
$new = wp_insert_post( array( 'post_type' => 'kconv_form', 'post_status' => 'draft', 'post_title' => $form['title'] . ' — copy' ), true );
if ( is_wp_error( $new ) ) { return $new; }
// Templates/flow are reusable. Delivery consent, credentials, mail recipients and entries are not copied.
$schema = array_intersect_key( $form, array_flip( array( 'fields', 'settings', 'estimate', 'steps', 'logic' ) ) );
$schema['settings']['notify_email'] = '';
update_post_meta( $new, '_kconv_schema', wp_slash( $schema ) );
$pro = get_post_meta( $id, '_kconv_pro', true );
if ( is_array( $pro ) ) { $flow = array( 'rules' => isset( $pro['rules'] ) ? $pro['rules'] : array(), 'steps' => isset( $pro['steps'] ) ? $pro['steps'] : array() ); update_post_meta( $new, '_kconv_pro', wp_slash( $flow ) ); }
// Derive premium dependence from the copied design, not omitted delivery settings.
// Ordinary Free steps remain Free; inactive premium pricing/logic stays paused.
if ( get_post_meta( $new, '_kconv_schema', true ) !== $schema || ( is_array( $pro ) && get_post_meta( $new, '_kconv_pro', true ) !== $flow ) || ! KCONV_Free_Ownership::refresh_form( $new ) ) {
wp_delete_post( $new, true );
return new WP_Error( 'storage', __( 'The complete form design could not be copied. Repair storage and try again.', 'killerconversions' ) );
}
return $new;
}
public static function clone_handler() {
if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission for this action.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) ) { wp_die( esc_html__( 'You do not have permission for this action.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
$nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'kconv_clone_form' ) ) { wp_die( esc_html__( 'You do not have permission for this action.', 'killerconversions' ), '', array( 'response' => 403 ) ); }
$id = isset( $_POST['form_id'] ) && is_scalar( $_POST['form_id'] ) ? absint( $_POST['form_id'] ) : 0;
$new = self::clone_form( $id ); if ( is_wp_error( $new ) ) { self::die_error( $new ); }
wp_safe_redirect( admin_url( 'admin.php?page=killerconversions&edit=' . $new ) ); exit;
}
}