'pubforms_form', 'page' => 'pubforms-entries' ), $args ); if ( $form_id ) { $args['form_id'] = $form_id; } return add_query_arg( $args, admin_url( 'edit.php' ) ); } function pubforms_entry_url( $form_id, $entry_id ) { return pubforms_entries_url( $form_id, array( 'entry' => $entry_id ) ); } /** * Link that serves a stored file (upload or signature) to a signed-in user who can edit the form. * Files are private on disk; this is the only way to reach them. */ function pubforms_file_url( $entry_id, $field_id ) { $url = add_query_arg( array( 'action' => 'pubforms_download', 'entry' => (int) $entry_id, 'field' => (string) $field_id, ), admin_url( 'admin-post.php' ) ); return wp_nonce_url( $url, 'pubforms_download_' . (int) $entry_id ); } /** * Work out which file an entry's field points at and whether this user may have it. * * @return array|WP_Error path, name, type, inline (bool). */ function pubforms_resolve_file( $entry_id, $field_id ) { $entry = pubforms_get_entry( $entry_id ); if ( ! $entry || ! pubforms_request_form( $entry->form_id ) ) { return new WP_Error( 'forbidden', __( 'You do not have permission to view this file.', 'pubforms' ) ); } foreach ( $entry->items as $item ) { if ( ( $item['id'] ?? '' ) !== $field_id || ! in_array( $item['type'] ?? '', array( 'file', 'signature' ), true ) ) { continue; } // Rebuild the path from the file name inside the form's folder, so only files PubForms stored are reachable. $stored = wp_basename( (string) ( $item['filename'] ?? '' ) ); $dir = pubforms_uploads_dir( $entry->form_id ); $path = $dir['path'] . '/' . $stored; if ( '' === $stored || ! is_file( $path ) ) { break; } $is_sig = 'signature' === $item['type']; $name = $is_sig ? 'signature.png' : sanitize_file_name( (string) ( $item['name'] ?? '' ) ); $name = '' !== $name ? $name : $stored; $type = $is_sig ? 'image/png' : (string) wp_check_filetype( $stored )['type']; return array( 'path' => $path, 'name' => $name, 'type' => '' !== $type ? $type : 'application/octet-stream', 'inline' => $is_sig, ); } return new WP_Error( 'missing', __( 'That file could not be found.', 'pubforms' ) ); } add_action( 'admin_post_pubforms_download', function () { $entry_id = absint( $_GET['entry'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- verified by check_admin_referer() on the next line. check_admin_referer( 'pubforms_download_' . $entry_id ); $field = sanitize_key( wp_unslash( $_GET['field'] ?? '' ) ); $file = pubforms_resolve_file( $entry_id, $field ); if ( is_wp_error( $file ) ) { wp_die( esc_html( $file->get_error_message() ), '', array( 'response' => 'forbidden' === $file->get_error_code() ? 403 : 404 ) ); } nocache_headers(); header( 'Content-Type: ' . $file['type'] ); header( 'Content-Disposition: ' . ( $file['inline'] ? 'inline' : 'attachment' ) . '; filename="' . str_replace( '"', '', $file['name'] ) . '"' ); header( 'Content-Length: ' . filesize( $file['path'] ) ); header( 'X-Content-Type-Options: nosniff' ); header( "Content-Security-Policy: default-src 'none'; sandbox" ); readfile( $file['path'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- streams a private file after the capability and nonce checks above. exit; } ); add_action( 'admin_menu', function () { $hook = add_submenu_page( 'edit.php?post_type=pubforms_form', __( 'Entries', 'pubforms' ), __( 'Entries', 'pubforms' ), 'edit_posts', 'pubforms-entries', 'pubforms_render_entries_page' ); add_action( "load-$hook", 'pubforms_entries_page_load' ); } ); function pubforms_notice_messages() { return array( /* translators: %d: number of entries */ 'deleted' => __( 'Deleted %d entries.', 'pubforms' ), /* translators: %d: number of entries */ 'marked' => __( 'Updated %d entries.', 'pubforms' ), 'none' => __( 'Select at least one entry first.', 'pubforms' ), ); } /** * The form an admin request is about, only if the current user may edit it. */ function pubforms_request_form( $form_id ) { $form = $form_id ? pubforms_get_form( $form_id ) : null; return ( $form && current_user_can( 'edit_post', $form['id'] ) ) ? $form : null; } /** * Runs before the page prints: help text and bulk actions (so we can redirect afterwards). */ function pubforms_entries_page_load() { $screen = get_current_screen(); if ( $screen ) { $screen->add_help_tab( array( 'id' => 'pubforms-entries-help', 'title' => __( 'Entries', 'pubforms' ), 'content' => '
' . esc_html__( 'Every submission is kept here, on your own site. Click a date to read an entry. Use the checkboxes and the Bulk actions menu to delete entries or mark them read or unread. Export CSV downloads all of a form\'s entries. No visitor IP address or browser details are stored.', 'pubforms' ) . '
', ) ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- verified below with check_admin_referer. $action = sanitize_key( $_REQUEST['action'] ?? '' ); if ( '' === $action || '-1' === $action ) { $action = sanitize_key( $_REQUEST['action2'] ?? '' ); } if ( ! in_array( $action, array( 'delete', 'mark_read', 'mark_unread' ), true ) || empty( $_REQUEST['form_id'] ) ) { return; } $form_id = absint( $_REQUEST['form_id'] ); $ids = array_map( 'absint', (array) ( $_REQUEST['entry'] ?? array() ) ); // phpcs:enable check_admin_referer( 'bulk-entries' ); if ( ! pubforms_request_form( $form_id ) ) { wp_die( esc_html__( 'You do not have permission to manage these entries.', 'pubforms' ), '', array( 'response' => 403 ) ); } if ( ! $ids ) { wp_safe_redirect( pubforms_entries_url( $form_id, array( 'pubforms_notice' => 'none' ) ) ); exit; } $done = pubforms_bulk_entry_action( $form_id, $action, $ids ); wp_safe_redirect( pubforms_entries_url( $form_id, array( 'pubforms_notice' => 'delete' === $action ? 'deleted' : 'marked', 'n' => $done ) ) ); exit; } /** * Apply a bulk action to entries of one form. Entries of other forms are ignored. * * @return int Entries changed. */ function pubforms_bulk_entry_action( $form_id, $action, $entry_ids ) { $done = 0; foreach ( $entry_ids as $id ) { $entry = pubforms_get_entry( $id ); if ( ! $entry || (int) $entry->form_id !== (int) $form_id ) { continue; } if ( 'delete' === $action ) { $ok = pubforms_delete_entry( $id ); } else { $ok = pubforms_set_entry_status( $id, 'mark_read' === $action ? 'read' : 'unread' ); } if ( $ok ) { $done++; } } return $done; } function pubforms_render_entries_page() { if ( ! current_user_can( 'edit_posts' ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'pubforms' ) ); } echo '' . esc_html( sprintf( $messages[ $code ], absint( $_GET['n'] ?? 0 ) ) ) . '
' . esc_html__( 'No forms yet.', 'pubforms' ) . ' ' . esc_html__( 'Create one', 'pubforms' ) . '
'; return; } echo '| ' . esc_html__( 'Form', 'pubforms' ) . ' | ' . esc_html__( 'Entries', 'pubforms' ) . ' | ' . esc_html__( 'Unread', 'pubforms' ) . ' | ' . esc_html__( 'Abandoned', 'pubforms' ) . ' |
|---|---|---|---|
| ' . esc_html( get_the_title( $form ) ) . ' | ' . (int) $c['total'] . ' | ' . (int) $c['unread'] . ' | ' . (int) $c['partial'] . ' |
| ' . esc_html( $option ) . ' | ' . esc_html( sprintf( __( '%1$d responses (%2$d%%)', 'pubforms' ), $count, (int) round( $count / max( 1, $total ) * 100 ) ) ) . ' |
' . esc_html__( 'Entry not found.', 'pubforms' ) . ' ' . esc_html__( 'Back to entries', 'pubforms' ) . '
'; return; } if ( 'unread' === $entry->status ) { pubforms_set_entry_status( $entry->id, 'read' ); } $delete = wp_nonce_url( add_query_arg( array( 'action' => 'pubforms_delete_entry', 'form_id' => $form['id'], 'entry' => $entry->id ), admin_url( 'admin-post.php' ) ), 'pubforms_delete_' . $entry->id ); ?> 403 ) ); } pubforms_bulk_entry_action( $form_id, 'delete', array( $entry_id ) ); wp_safe_redirect( pubforms_entries_url( $form_id, array( 'pubforms_notice' => 'deleted', 'n' => 1 ) ) ); exit; } ); add_action( 'admin_post_pubforms_export', function () { $form_id = absint( $_GET['form_id'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification check_admin_referer( 'pubforms_export_' . $form_id ); $form = pubforms_request_form( $form_id ); if ( ! $form ) { wp_die( esc_html__( 'You do not have permission to export these entries.', 'pubforms' ), '', array( 'response' => 403 ) ); } nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( sanitize_title( $form['title'] ) . '-entries.csv' ) . '"' ); $filters = array( 'status' => sanitize_key( wp_unslash( $_GET['status'] ?? '' ) ), 'date_from' => sanitize_text_field( wp_unslash( $_GET['date_from'] ?? '' ) ), 'date_to' => sanitize_text_field( wp_unslash( $_GET['date_to'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming the CSV download. foreach ( pubforms_entries_csv_rows( $form, $filters ) as $row ) { fputcsv( $out, $row, ',', '"', '' ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output stream. exit; } ); add_action( 'admin_post_pubforms_save_entry_note', function () { $form_id = absint( $_POST['form_id'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification $entry_id = absint( $_POST['entry'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification check_admin_referer( 'pubforms_entry_note_' . $entry_id, 'pubforms_note_nonce' ); if ( ! pubforms_request_form( $form_id ) ) { wp_die( esc_html__( 'You do not have permission to manage these entries.', 'pubforms' ), '', array( 'response' => 403 ) ); } $entry = pubforms_get_entry( $entry_id ); if ( ! $entry || (int) $entry->form_id !== (int) $form_id ) { wp_die( esc_html__( 'Entry not found.', 'pubforms' ), '', array( 'response' => 404 ) ); } $note = isset( $_POST['pubforms_note'] ) ? wp_unslash( $_POST['pubforms_note'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- sanitised in pubforms_set_entry_note(). pubforms_set_entry_note( $entry_id, $note ); wp_safe_redirect( pubforms_entry_url( $form_id, $entry_id ) ); exit; } );