post_type ? __( 'Form name, for example Contact us', 'nonprofit-forms' ) : $text;
}
/**
* List table columns.
*
* @param array $cols Columns.
* @return array
*/
public static function columns( $cols ) {
return array(
'cb' => $cols['cb'],
'title' => __( 'Form', 'nonprofit-forms' ),
'shortcode' => __( 'Shortcode', 'nonprofit-forms' ),
'date' => $cols['date'],
);
}
/**
* Column output.
*
* @param string $col Column.
* @param int $post_id Post ID.
* @return void
*/
public static function column_content( $col, $post_id ) {
if ( 'shortcode' === $col ) {
echo '[nonprofit_form id="' . (int) $post_id . '"]';
}
}
/**
* Which forms can the current user read entries for?
*
* @return array{all:bool,ids:int[]}
*/
public static function allowed_forms() {
if ( current_user_can( 'manage_options' ) ) {
return array(
'all' => true,
'ids' => array(),
);
}
$ids = array();
$user = wp_get_current_user();
$posts = get_posts(
array(
'post_type' => NPFRM_Form::POST_TYPE,
'post_status' => 'publish',
'posts_per_page' => 100,
'fields' => 'ids',
)
);
foreach ( $posts as $id ) {
$form = NPFRM_Form::get( $id );
if ( $form && array_intersect( (array) $form['settings']['entry_roles'], (array) $user->roles ) ) {
$ids[] = (int) $id;
}
}
return array(
'all' => false,
'ids' => $ids,
);
}
/**
* May the current user read this form's entries?
*
* @param int $form_id Form ID.
* @return bool
*/
public static function can_view_form( $form_id ) {
$allowed = self::allowed_forms();
return $allowed['all'] || in_array( (int) $form_id, $allowed['ids'], true );
}
/**
* Entries screen address for the current user.
*
* @return string
*/
public static function entries_url() {
return current_user_can( 'manage_options' )
? admin_url( 'edit.php?post_type=' . NPFRM_Form::POST_TYPE . '&page=npfrm-entries' )
: admin_url( 'admin.php?page=npfrm-entries' );
}
/**
* Menu entries.
*
* @return void
*/
public static function menu() {
$parent = 'edit.php?post_type=' . NPFRM_Form::POST_TYPE;
if ( current_user_can( 'manage_options' ) ) {
$unread = NPFRM_Entries::unread_count();
$badge = $unread ? ' ' . (int) $unread . '' : '';
add_submenu_page( $parent, __( 'Entries', 'nonprofit-forms' ), __( 'Entries', 'nonprofit-forms' ) . $badge, 'manage_options', 'npfrm-entries', array( __CLASS__, 'entries_page' ) );
add_submenu_page( $parent, __( 'Analytics', 'nonprofit-forms' ), __( 'Analytics', 'nonprofit-forms' ), 'manage_options', 'npfrm-analytics', array( __CLASS__, 'analytics_page' ) );
add_submenu_page( $parent, __( 'Settings', 'nonprofit-forms' ), __( 'Settings', 'nonprofit-forms' ), 'manage_options', 'npfrm-settings', array( __CLASS__, 'settings_page' ) );
return;
}
$allowed = self::allowed_forms();
if ( ! empty( $allowed['ids'] ) ) {
add_menu_page( __( 'Form entries', 'nonprofit-forms' ), __( 'Form entries', 'nonprofit-forms' ), 'read', 'npfrm-entries', array( __CLASS__, 'entries_page' ), 'dashicons-feedback', 58 );
}
}
/**
* Settings API.
*
* @return void
*/
public static function register_settings() {
register_setting(
'npfrm_settings',
'npfrm_retention_days',
array(
'type' => 'integer',
'sanitize_callback' => 'absint',
'default' => 0,
)
);
register_setting(
'npfrm_settings',
'npfrm_rate_limit',
array(
'type' => 'integer',
'sanitize_callback' => array( __CLASS__, 'sanitize_rate' ),
'default' => 5,
)
);
register_setting(
'npfrm_settings',
'npfrm_block_disposable',
array(
'type' => 'integer',
'sanitize_callback' => array( __CLASS__, 'sanitize_flag' ),
'default' => 1,
)
);
register_setting(
'npfrm_settings',
'npfrm_spam_keywords',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_textarea_field',
'default' => '',
)
);
}
/**
* Rate limit: 1 to 50 attempts.
*
* @param mixed $v Value.
* @return int
*/
public static function sanitize_rate( $v ) {
return min( 50, max( 1, absint( $v ) ) );
}
/**
* Checkbox to 0 or 1.
*
* @param mixed $v Value.
* @return int
*/
public static function sanitize_flag( $v ) {
return empty( $v ) ? 0 : 1;
}
/**
* Settings page.
*
* @return void
*/
public static function settings_page() {
?>
' . esc_html__( 'Last 30 days. These are plain totals kept on your own site. No cookies, no IP addresses and no outside tools. If a page cache serves your page, some views may not be counted.', 'nonprofit-forms' ) . '
'; if ( empty( $forms ) ) { echo '' . esc_html__( 'No forms yet.', 'nonprofit-forms' ) . '
| ' . esc_html__( 'Form', 'nonprofit-forms' ) . ' | ' . esc_html__( 'Views', 'nonprofit-forms' ) . ' | ' . esc_html__( 'Submitted', 'nonprofit-forms' ) . ' | ' . esc_html__( 'Out of 100 visitors', 'nonprofit-forms' ) . ' |
|---|---|---|---|
| ' . esc_html( $title ) . ' | ' . (int) $views . ' | ' . (int) $subs . ' | '; echo ' ' . (int) $rate . ' |
|
coordinator@example.org. The first part is the field label, then the answer, then who also gets the email.', 'nonprofit-forms' ); ?> |
|
'npfrm_settings[privacy_page]',
'id' => 'npfrm_privacy_page',
'selected' => (int) $s['privacy_page'],
'show_option_none' => __( 'WordPress default privacy page (if set)', 'nonprofit-forms' ),
'option_none_value' => '0',
)
);
?>
[nonprofit_form id="' . (int) $post->ID . '"]
' . esc_html__( 'Or add the "Nonprofit Form" block in the editor.', 'nonprofit-forms' ) . '
'; } /** * Clean a list of role slugs against real roles. * * @param mixed $roles Raw list. * @return string[] */ private static function clean_roles( $roles ) { $valid = array_keys( self::role_choices() ); return array_values( array_intersect( array_map( 'sanitize_key', (array) $roles ), $valid ) ); } /** * Clean a datetime-local value. * * @param mixed $v Raw. * @return string */ private static function clean_datetime( $v ) { $v = is_string( $v ) ? trim( $v ) : ''; return preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $v ) ? $v : ''; } /** * Save the editor. * * @param int $post_id Post ID. * @return void */ public static function save( $post_id ) { if ( ! isset( $_POST['npfrm_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['npfrm_nonce'] ) ), 'npfrm_save_form' ) ) { return; } if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! current_user_can( 'manage_options' ) ) { return; } if ( isset( $_POST['npfrm_fields_json'] ) ) { $json = sanitize_textarea_field( wp_unslash( $_POST['npfrm_fields_json'] ) ); $decoded = json_decode( $json, true ); // json_decode() transforms but does not sanitize; clean every decoded value before use. $decoded = is_array( $decoded ) ? map_deep( $decoded, 'sanitize_textarea_field' ) : array(); $fields = NPFRM_Fields::sanitize_definitions( $decoded ); update_post_meta( $post_id, '_npfrm_fields', wp_slash( wp_json_encode( $fields ) ) ); } // Sanitize on intake (textarea-safe, keeps line breaks), then validate per key below. $in = isset( $_POST['npfrm_settings'] ) && is_array( $_POST['npfrm_settings'] ) ? map_deep( wp_unslash( $_POST['npfrm_settings'] ), 'sanitize_textarea_field' ) : array(); $to = array(); foreach ( explode( ',', isset( $in['to'] ) ? (string) $in['to'] : '' ) as $addr ) { $addr = sanitize_email( trim( $addr ) ); if ( is_email( $addr ) ) { $to[] = $addr; } } $text = function ( $key ) use ( $in ) { return isset( $in[ $key ] ) ? sanitize_text_field( $in[ $key ] ) : ''; }; $access = $text( 'access' ); $contact = sanitize_email( $text( 'notice_contact' ) ); update_post_meta( $post_id, '_npfrm_settings', array( 'to' => $to ? implode( ', ', $to ) : get_option( 'admin_email' ), 'submit' => $text( 'submit' ), 'success' => $text( 'success' ), 'store' => ! empty( $in['store'] ), 'access' => in_array( $access, array( 'everyone', 'logged_in', 'roles' ), true ) ? $access : 'everyone', 'access_roles' => self::clean_roles( isset( $in['access_roles'] ) ? $in['access_roles'] : array() ), 'access_msg' => $text( 'access_msg' ), 'entry_roles' => self::clean_roles( isset( $in['entry_roles'] ) ? $in['entry_roles'] : array() ), 'open_from' => self::clean_datetime( isset( $in['open_from'] ) ? $in['open_from'] : '' ), 'close_at' => self::clean_datetime( isset( $in['close_at'] ) ? $in['close_at'] : '' ), 'max_entries' => isset( $in['max_entries'] ) ? absint( $in['max_entries'] ) : 0, 'closed_msg' => $text( 'closed_msg' ), 'notice_on' => ! empty( $in['notice_on'] ), 'notice_purpose' => $text( 'notice_purpose' ), 'notice_contact' => is_email( $contact ) ? $contact : '', 'privacy_page' => isset( $in['privacy_page'] ) ? absint( $in['privacy_page'] ) : 0, 'routing' => isset( $in['routing'] ) ? sanitize_textarea_field( $in['routing'] ) : '', 'reply_on' => ! empty( $in['reply_on'] ), 'reply_subject' => $text( 'reply_subject' ), 'reply_body' => isset( $in['reply_body'] ) ? sanitize_textarea_field( $in['reply_body'] ) : '', ) ); } /** * Entries screen. * * @return void */ public static function entries_page() { $allowed = self::allowed_forms(); if ( ! $allowed['all'] && empty( $allowed['ids'] ) ) { wp_die( esc_html__( 'You are not allowed to see entries.', 'nonprofit-forms' ) ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only filters. $entry_id = isset( $_GET['entry'] ) ? absint( $_GET['entry'] ) : 0; $form_id = isset( $_GET['form'] ) ? absint( $_GET['form'] ) : 0; $paged = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; $status = isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended $base = self::entries_url(); $forms = NPFRM_Form::choices(); if ( ! $allowed['all'] ) { $forms = array_intersect_key( $forms, array_flip( $allowed['ids'] ) ); } echo '' . esc_html__( 'No entries found.', 'nonprofit-forms' ) . '
'; return; } $is_admin = current_user_can( 'manage_options' ); echo ''; $links = paginate_links( array( 'base' => add_query_arg( 'paged', '%#%', remove_query_arg( 'paged' ) ), 'format' => '', 'current' => $paged, 'total' => (int) ceil( $result['total'] / $per ), 'type' => 'plain', ) ); if ( $links ) { echo wp_kses_post( $links ); } echo ''; } /** * Follow-up statuses. * * @return array */ private static function statuses() { return array( 'new' => __( 'New', 'nonprofit-forms' ), 'contacted' => __( 'Contacted', 'nonprofit-forms' ), 'done' => __( 'Done', 'nonprofit-forms' ), ); } /** * One entry, with status and a private note. * * @param int $entry_id Entry ID. * @param string $base Entries address. * @param array $forms Forms the user can see. * @return void */ private static function entry_detail( $entry_id, $base, array $forms ) { $row = NPFRM_Entries::get( $entry_id ); if ( ! $row || ! self::can_view_form( $row->form_id ) ) { echo '' . esc_html__( 'Entry not found.', 'nonprofit-forms' ) . '
'; return; } NPFRM_Entries::mark_read( $row->id ); echo '← ' . esc_html__( 'All entries', 'nonprofit-forms' ) . '
'; echo '| ' . esc_html__( 'Form', 'nonprofit-forms' ) . ' | ' . esc_html( isset( $forms[ $row->form_id ] ) ? $forms[ $row->form_id ] : '#' . $row->form_id ) . ' |
|---|---|
| ' . esc_html__( 'Received', 'nonprofit-forms' ) . ' | ' . esc_html( get_date_from_gmt( $row->created_at, get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ) . ' |
| ' . esc_html( $pair['label'] ) . ' | ' . nl2br( esc_html( $pair['value'] ) ) . ' |
| ' . esc_html__( 'Privacy notice shown', 'nonprofit-forms' ) . ' | ' . esc_html( $meta['notice'] ) . ' ' . esc_html( $meta['shown_at'] ) . ' |
' . esc_html__( 'Delete this entry', 'nonprofit-forms' ) . ''; if ( $row->email ) { $erase = wp_nonce_url( admin_url( 'admin-post.php?action=npfrm_erase_person&entry=' . $row->id ), 'npfrm_erase_' . $row->id ); echo ' | ' . esc_html__( 'Erase everything from this person', 'nonprofit-forms' ) . ''; } echo '
'; } } /** * Save status and note. * * @return void */ public static function save_entry() { $id = isset( $_POST['entry'] ) ? absint( $_POST['entry'] ) : 0; check_admin_referer( 'npfrm_entry_' . $id, 'npfrm_entry_nonce' ); $row = NPFRM_Entries::get( $id ); if ( ! $row || ! self::can_view_form( $row->form_id ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'nonprofit-forms' ) ); } $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'new'; $note = isset( $_POST['note'] ) ? sanitize_textarea_field( wp_unslash( $_POST['note'] ) ) : ''; NPFRM_Entries::update_follow_up( $id, $status, $note ); wp_safe_redirect( add_query_arg( 'entry', $id, self::entries_url() ) ); exit; } /** * Delete the selected entries (administrators only). * * @return void */ public static function bulk_entries() { check_admin_referer( 'npfrm_bulk', 'npfrm_bulk_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'nonprofit-forms' ) ); } $ids = isset( $_POST['ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['ids'] ) ) : array(); foreach ( $ids as $id ) { NPFRM_Entries::delete( $id ); } wp_safe_redirect( self::entries_url() ); exit; } /** * Erase every entry from one email address (administrators only). * * @return void */ public static function erase_person() { $id = isset( $_GET['entry'] ) ? absint( $_GET['entry'] ) : 0; check_admin_referer( 'npfrm_erase_' . $id ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'nonprofit-forms' ) ); } $row = NPFRM_Entries::get( $id ); if ( $row && $row->email ) { NPFRM_Entries::delete_by_email( $row->email ); } wp_safe_redirect( self::entries_url() ); exit; } /** * Delete one entry. * * @return void */ public static function delete_entry() { $id = isset( $_GET['entry'] ) ? absint( $_GET['entry'] ) : 0; check_admin_referer( 'npfrm_delete_' . $id ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'nonprofit-forms' ) ); } NPFRM_Entries::delete( $id ); wp_safe_redirect( self::entries_url() ); exit; } /** * CSV export for one form. * * @return void */ public static function export_csv() { $form_id = isset( $_GET['form'] ) ? absint( $_GET['form'] ) : 0; check_admin_referer( 'npfrm_export_' . $form_id ); if ( ! $form_id || ! self::can_view_form( $form_id ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'nonprofit-forms' ) ); } $rows = NPFRM_Entries::all_for_form( $form_id ); $headers = array(); foreach ( $rows as $row ) { foreach ( NPFRM_Entries::data( $row ) as $pair ) { if ( ! in_array( $pair['label'], $headers, true ) ) { $headers[] = $pair['label']; } } } nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="entries-' . $form_id . '-' . gmdate( 'Ymd' ) . '.csv"' ); $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- BOM so Excel reads Indian scripts. fputcsv( $out, array_map( array( __CLASS__, 'csv_safe' ), array_merge( array( 'Date (UTC)' ), $headers ) ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions foreach ( $rows as $row ) { $map = array(); foreach ( NPFRM_Entries::data( $row ) as $pair ) { $map[ $pair['label'] ] = $pair['value']; } $line = array( $row->created_at ); foreach ( $headers as $h ) { $line[] = isset( $map[ $h ] ) ? $map[ $h ] : ''; } fputcsv( $out, array_map( array( __CLASS__, 'csv_safe' ), $line ) ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } /** * Stop spreadsheet formula injection. * * @param string $cell Cell. * @return string */ public static function csv_safe( $cell ) { $cell = (string) $cell; return preg_match( '/^[=+\-@\t\r]/', $cell ) ? "'" . $cell : $cell; } /** * Admin notice: no published privacy policy page linked from forms. * * @return void */ public static function privacy_notice() { if ( ! current_user_can( 'manage_options' ) ) { return; } $pid = (int) get_option( 'wp_page_for_privacy_policy' ); if ( $pid > 0 && 'publish' === get_post_status( $pid ) ) { return; } echo 'Nonprofit Forms: '; if ( $pid > 0 && 'draft' === get_post_status( $pid ) ) { esc_html_e( 'Your privacy policy page is still a draft. Forms will link to it once it is published.', 'nonprofit-forms' ); echo ' ' . esc_html__( 'Review and publish it', 'nonprofit-forms' ) . ''; } else { esc_html_e( 'No published privacy policy page yet. Forms show the privacy link only when one exists, so visitors never see a dead link.', 'nonprofit-forms' ); echo ' ' . esc_html__( 'Create a starter draft page', 'nonprofit-forms' ) . ''; } echo '