'accessible_form',
'page' => self::SLUG,
'entry' => (int) $id,
'_af_read_nonce' => wp_create_nonce( 'accessible_forms_entry_read_' . (int) $id ),
'view' => $view ? rawurlencode( wp_json_encode( $view ) ) : false,
),
admin_url( 'admin.php' )
);
}
/**
* Handle a single-entry action.
*/
public static function handle_action() {
if ( ! current_user_can( 'read_accessible_form_entries' ) ) {
wp_die( esc_html__( 'You do not have permission to do that.', 'accessible-forms-by-accessible-org' ) );
}
check_admin_referer( self::NONCE );
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified above.
$id = isset( $_POST['entry'] ) ? (int) $_POST['entry'] : 0;
$do = isset( $_POST['af_do'] ) ? sanitize_key( wp_unslash( $_POST['af_do'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Missing
$view = self::view( true );
$entry = $id ? Entries::get( $id ) : null;
if ( ! $entry ) {
wp_safe_redirect( EntriesTable::url( Inbox::url_filters( $view ) ) );
exit();
}
// Add-on actions check their own permissions, then redirect and exit.
if ( ! in_array( $do, array( 'resend', 'accept_notify', 'trash', 'restore', 'read', 'unread', 'spam', 'ham', 'delete' ), true ) ) {
Extensions::notify( 'accessible_forms_entry_action', $do, $entry, $view );
wp_die( esc_html__( 'Unknown action.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 400 ) );
}
$required = in_array( $do, array( 'delete', 'trash' ), true )
? 'delete_accessible_form_entries'
: ( in_array( $do, array( 'resend', 'accept_notify' ), true )
? 'send_accessible_form_notifications'
: 'manage_accessible_form_entries' );
if (
! Capabilities::entry( $entry, $required ) ||
( 'accept_notify' === $do && ! Capabilities::entry( $entry, 'manage_accessible_form_entries' ) )
) {
wp_die(
esc_html__( 'You cannot perform this action on this submission.', 'accessible-forms-by-accessible-org' ),
'',
array(
'response' => 403,
)
);
}
if ( 'delete' === $do && 'trash' !== $entry['status'] ) {
wp_die(
esc_html__( 'Move this submission to Trash before deleting it permanently.', 'accessible-forms-by-accessible-org' ),
'',
array( 'response' => 400 )
);
}
$result = true;
if ( 'resend' === $do ) {
$result = Notifications::queue( $id, true, ! empty( $_POST['use_current'] ) );
}
if ( 'accept_notify' === $do ) {
Entries::set_status( array( $id ), 'received' );
$result = Notifications::queue( $id, true );
if ( is_wp_error( $result ) ) {
// Nothing was queued, so the submission stays where it was, with its reason.
Entries::set_status( array( $id ), $entry['status'], (string) $entry['spam_reason'] );
}
}
if ( 'trash' === $do ) {
Entries::set_status( array( $id ), 'trash' );
Notifications::cancel( $id );
}
if ( 'restore' === $do ) {
Entries::set_status( array( $id ), 'received' );
}
if ( is_wp_error( $result ) ) {
wp_die( esc_html( $result->get_error_message() ), '', array( 'back_link' => true ) );
}
Audit::record( 'entry_' . $do, $entry['form_id'], $id );
switch ( $do ) {
case 'read':
Entries::set_read( array( $id ), true );
break;
case 'unread':
Entries::set_read( array( $id ), false );
wp_safe_redirect( EntriesTable::url( Inbox::url_filters( $view ) ) );
exit();
case 'spam':
Entries::set_status( array( $id ), 'spam' );
break;
case 'ham':
Entries::set_status( array( $id ), 'received' );
break;
case 'delete':
Entries::delete( array( $id ) );
wp_safe_redirect(
add_query_arg( 'af_notice', 'deleted', EntriesTable::url( Inbox::url_filters( $view ) ) )
);
exit();
}
wp_safe_redirect( self::url( $id, $view ) );
exit();
}
/**
* Keep a one-time result for the current user until the next page load.
*
* @param string $type success, info, warning or error.
* @param string $message Message.
*/
public static function flash( $type, $message ) {
set_transient(
'accessible_forms_notice_' . get_current_user_id(),
array(
'type' => $type,
'message' => $message,
),
5 * MINUTE_IN_SECONDS
);
}
/**
* Show the one-time result of the last action, after its redirect. The
* message lives server-side, so a crafted link cannot fake a result.
*/
private static function notice() {
$key = 'accessible_forms_notice_' . get_current_user_id();
$notice = get_transient( $key );
if ( ! is_array( $notice ) || empty( $notice['message'] ) ) {
return;
}
delete_transient( $key );
$type = in_array( $notice['type'] ?? '', array( 'success', 'info', 'warning', 'error' ), true ) ? $notice['type'] : 'info';
echo '
' .
esc_html( $notice['message'] ) . '
';
}
/**
* Render the screen.
*/
public static function render() {
if ( ! current_user_can( 'read_accessible_form_entries' ) ) {
wp_die( esc_html__( 'You do not have permission to view this page.', 'accessible-forms-by-accessible-org' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only.
$id = isset( $_GET['entry'] ) ? (int) $_GET['entry'] : 0;
$entry = $id ? Entries::get( $id ) : null;
echo '';
if ( ! $entry ) {
echo '
' . esc_html__( 'Entry not found', 'accessible-forms-by-accessible-org' ) . ' ';
echo '
' .
esc_html__( 'Back to all entries', 'accessible-forms-by-accessible-org' ) .
'
';
return;
}
if ( ! Capabilities::entry( $entry ) ) {
wp_die(
esc_html__( 'You cannot read this submission.', 'accessible-forms-by-accessible-org' ),
'',
array(
'response' => 403,
)
);
}
// Viewing a manually typed or external URL does not mutate the entry.
$read_nonce = isset( $_GET['_af_read_nonce'] ) && is_string( $_GET['_af_read_nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_af_read_nonce'] ) ) : '';
if ( empty( $entry['is_read'] ) &&
wp_verify_nonce( $read_nonce, 'accessible_forms_entry_read_' . $id ) &&
Capabilities::entry( $entry, 'manage_accessible_form_entries' ) && Entries::set_read( array( $id ), true ) ) {
$entry['is_read'] = true;
}
$labels = Entries::labels( $entry );
$form_title = $entry['snapshot']['title'] ?? get_the_title( $entry['form_id'] );
echo '' .
esc_html__( 'Entry', 'accessible-forms-by-accessible-org' ) .
' #' .
esc_html( (string) $entry['id'] ) .
' ';
echo '' .
esc_html__( 'All entries', 'accessible-forms-by-accessible-org' ) .
' ';
self::navigation( $entry );
$received = \AccessibleForms\EntryTime::display( $entry );
echo '' .
esc_html(
sprintf(
/* translators: 1: form name, 2: date and time the submission was received. */
__( '%1$s · Received %2$s', 'accessible-forms-by-accessible-org' ),
$form_title ? $form_title : __( '(deleted form)', 'accessible-forms-by-accessible-org' ),
$received
)
) .
'
';
echo '';
$states = array( 'received' => __( 'Received', 'accessible-forms-by-accessible-org' ), 'review' => __( 'Needs review', 'accessible-forms-by-accessible-org' ), 'spam' => __( 'Spam', 'accessible-forms-by-accessible-org' ), 'trash' => __( 'Trash', 'accessible-forms-by-accessible-org' ) );
$delivery = array( 'sent' => __( 'Accepted by mail service', 'accessible-forms-by-accessible-org' ), 'queued' => __( 'Queued', 'accessible-forms-by-accessible-org' ), 'uncertain' => __( 'Delivery uncertain', 'accessible-forms-by-accessible-org' ), 'failed' => __( 'Failed', 'accessible-forms-by-accessible-org' ), 'skipped' => __( 'Not sent', 'accessible-forms-by-accessible-org' ) );
echo '
' . esc_html__( 'Entry status', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html( $states[ $entry['status'] ] ?? $entry['status'] ) . '
' . esc_html__( 'Notification', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html( $delivery[ $entry['mail_status'] ] ?? __( 'Unknown', 'accessible-forms-by-accessible-org' ) ) . ' ';
self::notice();
if ( in_array( $entry['status'], array( 'spam', 'review' ), true ) ) {
echo '' .
esc_html(
'review' === $entry['status']
? __(
'This submission needs review. Verification was incomplete or unavailable.',
'accessible-forms-by-accessible-org'
)
: __( 'This entry is marked as spam.', 'accessible-forms-by-accessible-org' )
) .
'
';
$expiry = Entries::held_expiry( $entry );
if ( $expiry ) {
echo '
' .
esc_html(
sprintf(
/* translators: %s: date and time in the site's time zone. */
__( 'Will be deleted automatically on %s.', 'accessible-forms-by-accessible-org' ),
wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $expiry )
)
) .
' ' .
esc_html__( 'Accept it to keep it with your received submissions.', 'accessible-forms-by-accessible-org' ) .
'
';
}
echo '
';
}
if ( 'failed' === $entry['mail_status'] ) {
echo '' .
esc_html__( 'The notification email for this entry did not send.', 'accessible-forms-by-accessible-org' ) .
' ' .
esc_html__(
'The submission itself was stored safely. Only the email failed.',
'accessible-forms-by-accessible-org'
) .
'
';
if ( ! empty( $entry['mail_error'] ) ) {
echo '
' . esc_html( $entry['mail_error'] ) . '
';
}
echo '
';
}
echo '';
// The submission itself.
echo '
' .
esc_html__( 'Submission', 'accessible-forms-by-accessible-org' ) .
' ';
if ( empty( $entry['fields'] ) ) {
echo '
' . esc_html__( 'This entry has no stored values.', 'accessible-forms-by-accessible-org' ) . '
';
} else {
echo '
';
foreach ( Entries::display_values( $entry ) as $key => $value ) {
$label = $labels[ $key ] ?? $key;
$value = is_array( $value ) ? implode( ', ', $value ) : (string) $value;
echo '' . esc_html( $label ) . ' ';
if ( '' === trim( $value ) ) {
echo '' . esc_html__( '(empty)', 'accessible-forms-by-accessible-org' ) . ' ';
} elseif ( is_email( $value ) ) {
echo '' . esc_html( $value ) . ' ';
} else {
echo nl2br( esc_html( $value ) );
}
echo ' ';
}
echo ' ';
}
if ( empty( $entry['snapshot'] ) ) {
echo '
' .
esc_html__(
'Legacy entry: original question labels were not recorded. Field keys are shown.',
'accessible-forms-by-accessible-org'
) .
'
';
}
// Reply, follow-up and history follow as their own cards.
echo '
';
EntryWorkflow::render( $entry );
echo '
';
// Metadata and actions.
echo '
' .
esc_html__( 'Details', 'accessible-forms-by-accessible-org' ) .
' ';
echo '
';
self::meta_row(
__( 'Received', 'accessible-forms-by-accessible-org' ),
\AccessibleForms\EntryTime::display( $entry )
);
self::meta_row(
__( 'Form', 'accessible-forms-by-accessible-org' ),
$form_title ? $form_title : __( '(deleted form)', 'accessible-forms-by-accessible-org' )
);
if ( ! empty( $entry['page_url'] ) ) {
self::meta_row(
__( 'Submitted from', 'accessible-forms-by-accessible-org' ),
$entry['page_url'],
wp_validate_redirect( $entry['page_url'], home_url() )
);
}
$mail_labels = array(
'sent' => __( 'Accepted by mail service', 'accessible-forms-by-accessible-org' ),
'queued' => __( 'Queued', 'accessible-forms-by-accessible-org' ),
'uncertain' => __( 'Delivery uncertain', 'accessible-forms-by-accessible-org' ),
'failed' => __( 'Failed', 'accessible-forms-by-accessible-org' ),
'skipped' => __( 'Not sent', 'accessible-forms-by-accessible-org' ),
);
self::meta_row(
__( 'Notification', 'accessible-forms-by-accessible-org' ),
$mail_labels[ $entry['mail_status'] ] ?? __( 'Unknown', 'accessible-forms-by-accessible-org' )
);
if ( ! empty( $entry['spam_reason'] ) ) {
$reasons = Spam::reason_labels() + array( 'manual' => __( 'Marked by hand', 'accessible-forms-by-accessible-org' ) );
self::meta_row(
__( 'Spam check', 'accessible-forms-by-accessible-org' ),
$reasons[ $entry['spam_reason'] ] ?? $entry['spam_reason']
);
}
echo '
';
echo '
' . esc_html__( 'Technical details', 'accessible-forms-by-accessible-org' ) . ' ';
if ( ! empty( $entry['user_agent'] ) ) {
echo '';
self::meta_row( __( 'Browser', 'accessible-forms-by-accessible-org' ), $entry['user_agent'] );
echo '
';
}
echo '' .
esc_html__(
'The sender\'s IP address is stored only as a salted hash, so it cannot be read back.',
'accessible-forms-by-accessible-org'
) .
'
';
echo '
';
// Actions.
echo '
' .
esc_html__( 'Actions', 'accessible-forms-by-accessible-org' ) .
' ';
echo '
';
}
/**
* Previous and next links.
*
* @param array $entry Current entry.
*/
private static function navigation( array $entry ) {
$view = self::view();
$prev = Entries::adjacent_id( $entry, 'prev', $view );
$next = Entries::adjacent_id( $entry, 'next', $view );
if ( ! $prev && ! $next ) {
return;
}
echo '';
if ( $prev ) {
echo '' .
esc_html__( '← Newer entry', 'accessible-forms-by-accessible-org' ) .
' ';
}
if ( $next ) {
echo '' .
esc_html__( 'Older entry →', 'accessible-forms-by-accessible-org' ) .
' ';
}
echo ' ';
}
private static function view( $posted = false ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Display filters only (POST callers verify their nonce). The JSON text is size-bounded, decoded, then every value is sanitized and allowlisted.
$raw = $posted ? wp_unslash( $_POST['view'] ?? '' ) : wp_unslash( $_GET['view'] ?? '' );
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$json = is_string( $raw ) ? $raw : '';
$view = strlen( $json ) <= 4000 ? json_decode( $json, true ) : null;
return is_array( $view ) ? Inbox::filters( \AccessibleForms\Schema::clean_input( $view ) ) : array();
}
/** Keep the current inbox filters through entry actions. */
public static function view_field() {
echo ' ';
}
/**
* One metadata row.
*
* @param string $label Row label.
* @param string $value Value.
* @param string $link Optional URL to wrap the value in.
*/
private static function meta_row( $label, $value, $link = '' ) {
echo '' . esc_html( $label ) . ' ';
if ( $link ) {
echo '' . esc_html( $value ) . ' ';
} else {
echo esc_html( $value );
}
echo ' ';
}
}