'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 '
'; echo ''; echo ''; self::view_field(); wp_nonce_field( self::NONCE ); if ( Capabilities::entry( $entry, 'manage_accessible_form_entries' ) ) { echo '

'; if ( in_array( $entry['status'], array( 'spam', 'review' ), true ) ) { echo '

'; if ( Capabilities::entry( $entry, 'send_accessible_form_notifications' ) ) { echo '

'; } } elseif ( 'trash' === $entry['status'] ) { echo '

'; } else { echo '

'; } } if ( Capabilities::entry( $entry, 'delete_accessible_form_entries' ) ) { if ( 'trash' !== $entry['status'] ) { echo '

' . esc_html__( 'Trash is deleted after 30 days, or earlier if the entry’s retention expires.', 'accessible-forms-by-accessible-org' ) . '

'; } else { echo '

'; } } echo '
'; 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 ''; } 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 ''; } }