'accessible_form', 'page' => self::SLUG, ); if ( $tab ) { $args['tab'] = $tab; } return add_query_arg( $args, admin_url( 'edit.php' ) ); } /** * Tab definitions. * * @return array */ private static function tabs() { return Extensions::filter( 'accessible_forms_settings_tabs', array( 'general' => __( 'Overview', 'accessible-forms-by-accessible-org' ), 'email' => __( 'Email delivery', 'accessible-forms-by-accessible-org' ), 'spam' => __( 'Spam protection', 'accessible-forms-by-accessible-org' ), 'privacy' => __( 'Privacy', 'accessible-forms-by-accessible-org' ), ) ); } /** * Show a message above the settings after the next redirect. Add-ons use * this from `accessible_forms_settings_saved`. * * @param string $message Plain-text message. */ public static function flash_error( $message ) { set_transient( 'accessible_forms_settings_error_' . get_current_user_id(), (string) $message, 120 ); } /** * Which keys each tab owns. * * @return array */ private static function tab_keys() { return array( 'general' => array(), 'email' => array( 'mail_from', 'mail_from_name', ), 'spam' => array( 'spam_provider', 'rate_limit', 'max_links', ), 'privacy' => array( 'retention_days', 'spam_retention_days', 'delete_data' ), ); } /** * Handle a settings save. */ public static function save() { if ( ! current_user_can( 'manage_accessible_forms_settings' ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'accessible-forms-by-accessible-org' ) ); } check_admin_referer( self::NONCE_SAVE, isset( $_POST['af_settings_nonce'] ) ? 'af_settings_nonce' : '_wpnonce' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above. $tab = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : 'general'; $map = self::tab_keys(); if ( ! isset( self::tabs()[ $tab ] ) ) { $tab = 'general'; } $map[ $tab ] = $map[ $tab ] ?? array(); // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each key sanitized below. $raw = isset( $_POST['af_settings'] ) && is_array( $_POST['af_settings'] ) ? wp_unslash( $_POST['af_settings'] ) : array(); // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $settings = Settings::get(); $previous = $settings; if ( 'privacy' === $tab && isset( $raw['retention_mode'] ) ) { if ( ! Settings::is_constant( 'retention_days' ) && ! \AccessibleForms\FormOptions::valid_retention_days( $raw, false ) ) { wp_die( esc_html__( 'Enter a whole number of at least 1 day for entry retention. Your settings have not been changed.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 400, 'back_link' => true ) ); } $raw['retention_days'] = \AccessibleForms\FormOptions::retention_days( $raw, false ); } foreach ( $map[ $tab ] as $key ) { if ( ! Settings::is_constant( $key ) ) { $settings[ $key ] = self::sanitize_value( $key, $raw[ $key ] ?? null, $settings[ $key ] ?? '' ); } } // An add-on may refuse an incomplete setup by returning the previous settings. $checked = Extensions::filter( 'accessible_forms_settings_before_save', $settings, $previous, $tab ); if ( is_wp_error( $checked ) ) { wp_die( esc_html( $checked->get_error_message() ), '', array( 'response' => 503, 'back_link' => true ) ); } $settings = is_array( $checked ) ? $checked : $settings; if ( ! Settings::update( $settings ) ) { wp_die( esc_html__( 'Your settings could not be saved. Please go back and try again.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 503, 'back_link' => true ) ); } // Add-ons save their own settings for this tab; the nonce and capability were checked above. if ( ! Extensions::notify( 'accessible_forms_settings_saved', $tab ) ) { wp_die( esc_html__( 'Some add-on settings could not be saved. Please go back, review your settings and try again.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 503, 'back_link' => true ) ); } wp_safe_redirect( add_query_arg( array( 'post_type' => 'accessible_form', 'page' => self::SLUG, 'tab' => $tab, 'af_notice' => 'saved', ), admin_url( 'edit.php' ) ) ); exit(); } /** * Sanitize one setting by the same rules as this screen, for imported settings. * * @param string $key Setting name. * @param mixed $value Incoming value. * @param mixed $existing Currently stored value. * @return mixed */ public static function sanitize_setting( $key, $value, $existing ) { return self::sanitize_value( $key, $value, $existing ); } /** * Sanitize one setting. * * @param string $key Setting name. * @param mixed $value Submitted value. * @param mixed $existing Currently stored value. * @return mixed */ private static function sanitize_value( $key, $value, $existing ) { $value = is_scalar( $value ) ? (string) $value : ''; switch ( $key ) { case 'spam_provider': return isset( Spam::providers()[ $value ] ) ? $value : ( $existing ?: 'basic' ); case 'max_links': return min( 50, max( 0, (int) $value ) ); case 'mail_from': $clean = sanitize_email( (string) $value ); return is_email( $clean ) ? $clean : ''; case 'delete_data': return empty( $value ) ? 0 : 1; case 'rate_limit': case 'retention_days': return max( 0, (int) $value ); case 'spam_retention_days': return Settings::spam_retention_days( $value, is_scalar( $existing ) ? (int) $existing : null ); default: return sanitize_text_field( (string) $value ); } } /** * Handle the test email request. */ public static function test_email() { if ( ! current_user_can( 'manage_accessible_forms_settings' ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'accessible-forms-by-accessible-org' ) ); } check_admin_referer( self::NONCE_TEST, isset( $_POST['af_test_email_nonce'] ) ? 'af_test_email_nonce' : '_wpnonce' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified above. $to = isset( $_POST['af_test_to'] ) ? sanitize_email( wp_unslash( $_POST['af_test_to'] ) ) : ''; if ( ! is_email( $to ) ) { $to = get_option( 'admin_email' ); } $result = Mailer::send_test( $to ); set_transient( 'accessible_forms_test_result_' . get_current_user_id(), array( 'sent' => $result['sent'], 'error' => $result['error'], 'debug' => array_slice( $result['debug'], -40 ), 'transport' => $result['transport'], 'uncertain' => $result['uncertain'], 'routes' => \AccessibleForms\Notifications::mail_routes( $result['routes'] ), 'to' => $to, ), 120 ); wp_safe_redirect( add_query_arg( array( 'post_type' => 'accessible_form', 'page' => self::SLUG, 'tab' => 'email', ), admin_url( 'edit.php' ) ) ); exit(); } /** * Render the settings screen. */ public static function render() { if ( ! current_user_can( 'manage_accessible_forms_settings' ) ) { 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. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; $tabs = self::tabs(); if ( ! isset( $tabs[ $tab ] ) ) { $tab = 'general'; } $settings = Settings::get(); $async_tabs = (array) Extensions::filter( 'accessible_forms_async_tabs', array( 'general', 'email', 'spam', 'privacy' ), 'settings' ); echo '
'; echo '

' . esc_html__( 'Settings', 'accessible-forms-by-accessible-org' ) . '

'; echo '

' . esc_html__( 'Set up delivery, protect your inbox, and choose how long to keep entries.', 'accessible-forms-by-accessible-org' ) . '

'; echo ''; self::render_panel( $tab, $settings, true ); foreach ( array_intersect( array_keys( self::tab_keys() ), array_keys( $tabs ), $async_tabs ) as $key ) { if ( $key !== $tab ) { Navigation::preload( self::url( $key ), static function () use ( $key, $settings ) { self::render_panel( $key, $settings ); } ); } } echo '
'; } /** Each section keeps an independent form, nonce and save contract. */ private static function render_panel( $tab, array $settings, $show_notices = false ) { echo '
'; $settings_error = $show_notices ? get_transient( 'accessible_forms_settings_error_' . get_current_user_id() ) : false; if ( $settings_error ) { echo '

' . esc_html( $settings_error ) . '

'; delete_transient( 'accessible_forms_settings_error_' . get_current_user_id() ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Display only. if ( $show_notices && ! $settings_error && isset( $_GET['af_notice'] ) && 'saved' === sanitize_key( wp_unslash( $_GET['af_notice'] ) ) ) { echo '

' . esc_html__( 'Settings saved.', 'accessible-forms-by-accessible-org' ) . '

'; } // phpcs:enable WordPress.Security.NonceVerification.Recommended echo '
'; echo ''; echo ''; wp_nonce_field( self::NONCE_SAVE, 'af_settings_nonce', false ); switch ( $tab ) { case 'email': self::tab_email( $settings ); break; case 'spam': self::tab_spam( $settings ); break; case 'privacy': self::tab_privacy( $settings ); break; case 'general': self::tab_general( $settings ); break; } // Add-ons add their own settings to any tab, including tabs they added. Extensions::notify( 'accessible_forms_settings_tab', $tab, $settings ); if ( 'general' !== $tab ) { submit_button(); } echo '
'; if ( 'email' === $tab ) { self::test_email_form(); } // Add-ons add tools below a tab's settings form, such as test buttons. Extensions::notify( 'accessible_forms_settings_after', $tab ); echo '
'; } /** * General tab: an overview rather than settings. */ private static function tab_general( array $settings ) { $transport = Mailer::transport(); $default = __( 'WordPress default (wp_mail)', 'accessible-forms-by-accessible-org' ); $from = Settings::from_address(); echo '

' . esc_html__( 'Your next form, ready in a few steps', 'accessible-forms-by-accessible-org' ) . '

'; echo '
  1. ' . esc_html__( 'Create a form and give it a name you will recognize.', 'accessible-forms-by-accessible-org' ) . '
  2. ' . esc_html__( 'Add your fields, set a notification address, and publish.', 'accessible-forms-by-accessible-org' ) . '
  3. ' . esc_html__( 'Place the Accessible Form block on a page and choose your form.', 'accessible-forms-by-accessible-org' ) . '
'; echo '' . esc_html__( 'Create a form', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html__( 'Manage forms', 'accessible-forms-by-accessible-org' ) . '
'; echo '

' . esc_html__( 'Current setup', 'accessible-forms-by-accessible-org' ) . '

'; echo ''; self::status_row( __( 'Email delivery', 'accessible-forms-by-accessible-org' ), $transport === $default ? __( 'WordPress mail', 'accessible-forms-by-accessible-org' ) : $transport, $transport === $default ? 'neutral' : 'ok', $transport === $default ? __( 'Mail is sent exactly as WordPress is already configured, including any SMTP plugin you use.', 'accessible-forms-by-accessible-org' ) : __( 'Used for this plugin’s notifications and test emails.', 'accessible-forms-by-accessible-org' ) ); self::status_row( __( 'From address', 'accessible-forms-by-accessible-org' ), $from['address'] ? $from['address'] : __( 'WordPress default', 'accessible-forms-by-accessible-org' ), 'neutral', '' ); $providers = Spam::providers(); self::status_row( __( 'Spam filtering', 'accessible-forms-by-accessible-org' ), $providers[ Spam::provider() ][0] ?? __( 'Built-in protection', 'accessible-forms-by-accessible-org' ), 'ok', __( 'Review unverified messages in Entries → Needs review. Choose your verification method under Spam protection.', 'accessible-forms-by-accessible-org' ) ); $days = (int) Settings::get()['retention_days']; self::status_row( __( 'Entry retention', 'accessible-forms-by-accessible-org' ), $days > 0 ? /* translators: %d: number of days. */ sprintf( _n( '%d day', '%d days', $days, 'accessible-forms-by-accessible-org' ), $days ) : __( 'Kept indefinitely', 'accessible-forms-by-accessible-org' ), 'neutral', '' ); echo '
'; echo '
' . esc_html__( 'Advanced: configure credentials in wp-config.php', 'accessible-forms-by-accessible-org' ) . ''; echo '

' . esc_html__( 'These constants override what is saved here.', 'accessible-forms-by-accessible-org' ) . '

'; echo '
' .
			esc_html(
				"define( 'ACCESSIBLE_FORMS_MAIL_FROM', 'hello@example.com' );\n" .
					"define( 'ACCESSIBLE_FORMS_MAIL_FROM_NAME', 'Example' );"
			) .
			'
'; } /** * One row of the status table. * * @param string $label Row label. * @param string $value Current value. * @param string $state ok | warn | neutral. * @param string $note Explanatory note. */ private static function status_row( $label, $value, $state, $note ) { $badges = array( 'ok' => __( 'Configured', 'accessible-forms-by-accessible-org' ), 'warn' => __( 'Needs attention', 'accessible-forms-by-accessible-org' ), 'neutral' => '', ); echo '' . esc_html( $label ) . '' . esc_html( $value ) . ''; if ( ! empty( $badges[ $state ] ) ) { echo ' ' . esc_html( $badges[ $state ] ) . ''; } if ( $note ) { echo '

' . esc_html( $note ) . '

'; } echo ''; } /** * Email tab. * * @param array $settings Stored settings. */ private static function tab_email( array $settings ) { echo '

' . esc_html__( 'Sender', 'accessible-forms-by-accessible-org' ) . '

'; echo ''; self::field_row( 'mail_from', __( 'From address', 'accessible-forms-by-accessible-org' ), 'email', $settings['mail_from'], __( 'Leave blank to let WordPress decide. With an SMTP service, this usually has to match the authenticated account.', 'accessible-forms-by-accessible-org' ) ); self::field_row( 'mail_from_name', __( 'From name', 'accessible-forms-by-accessible-org' ), 'text', $settings['mail_from_name'], __( 'Leave blank to let WordPress decide.', 'accessible-forms-by-accessible-org' ) ); echo ''; } /** * Spam tab. * * @param array $settings Stored settings. */ private static function tab_spam( array $settings ) { echo '

' . esc_html__( 'Keep the useful messages', 'accessible-forms-by-accessible-org' ) . '

'; echo '

' . esc_html__( 'Choose one verification method. Local filters run alongside it, and flagged messages stay available for review.', 'accessible-forms-by-accessible-org' ) . '

'; echo '
' . esc_html__( 'Verification method', 'accessible-forms-by-accessible-org' ) . ''; $options = Spam::providers(); $provider = Spam::provider(); foreach ( $options as $value => $copy ) { echo ''; } echo '
'; echo '
' . esc_html__( 'A quiet check, with a clear fallback', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Verification starts automatically when a visitor uses a form. Challenges expire after 20 minutes and can only be used once. No tracking or cookies are added by this check. Your live site needs HTTPS.', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'If JavaScript is off or verification fails, the message is held in Needs review without an email notification. Review this folder regularly and accept legitimate messages.', 'accessible-forms-by-accessible-org' ) . '

'; if ( ! Spam::secure_context() ) { echo '

' . esc_html__( 'This site’s address starts with http://. Browsers only run the automatic check over HTTPS, so with this method every message would be held for review and visitors would still see the normal confirmation. Use HTTPS before relying on automatic verification.', 'accessible-forms-by-accessible-org' ) . '

'; } // Add-ons add the panel for their own verification method here. Extensions::notify( 'accessible_forms_spam_provider_panels', $settings ); echo '

' . esc_html__( 'Local filters', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'The hidden honeypot is always active. Adjust the limits below to suit your forms.', 'accessible-forms-by-accessible-org' ) . '

'; self::field_row( 'rate_limit', __( 'Submissions per hour, per IP address', 'accessible-forms-by-accessible-org' ), 'number', $settings['rate_limit'], __( 'Applied separately to each form. People sharing a network may share an IP address. Set to 0 to disable.', 'accessible-forms-by-accessible-org' ) ); self::field_row( 'max_links', __( 'Maximum links per message', 'accessible-forms-by-accessible-org' ), 'number', $settings['max_links'], __( 'Messages with more links are held in Spam. Counts http, https, and www links across the answers. Set to 0 to disable.', 'accessible-forms-by-accessible-org' ) ); // Add-ons add rows for their own content rules here. Extensions::notify( 'accessible_forms_spam_filter_rows', $settings ); echo ''; echo '
' . esc_html__( 'Review, recover, and improve', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Held messages include the reason they were flagged. Mark a genuine message as not spam to restore it to your inbox; this does not automatically send an email. Rate-limited requests are not stored, and the visitor is asked to try later.', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Review spam messages', 'accessible-forms-by-accessible-org' ) . '
'; } /** * Privacy tab. * * @param array $settings Stored settings. */ private static function tab_privacy( array $settings ) { echo '

' . esc_html__( 'Keep only what you need', 'accessible-forms-by-accessible-org' ) . '

'; echo '

' . esc_html__( 'Choose the default retention period for received submissions. Each form can use its own period. Spam and Needs review messages are kept for the shorter period below, and Trash for 30 days. Include your form and any external services in your site’s privacy policy.', 'accessible-forms-by-accessible-org' ) . '

'; echo ''; echo ''; self::field_row( 'spam_retention_days', __( 'Delete spam and Needs review messages after (days)', 'accessible-forms-by-accessible-org' ), 'number', $settings['spam_retention_days'], __( 'Runs daily. Applies to messages marked as spam and messages held for review, counted from when they arrived. Accept a held message to keep it with your received submissions. Between 1 and 3,650 days.', 'accessible-forms-by-accessible-org' ) ); echo ''; echo ''; echo ''; } /** * A labelled input row that respects wp-config.php overrides. * * @param string $key Setting name. * @param string $label Visible label. * @param string $type Input type. * @param mixed $value Current value. * @param string $help Description. */ private static function field_row( $key, $label, $type, $value, $help ) { $id = 'af-' . $key; echo ''; if ( Settings::is_constant( $key ) ) { echo '

' . esc_html( (string) Settings::value( $key ) ) . ' ' . esc_html__( 'set in wp-config.php', 'accessible-forms-by-accessible-org' ) . '

'; } else { printf( '', esc_attr( $type ), esc_attr( $id ), esc_attr( $key ), esc_attr( (string) $value ) ); echo '

' . esc_html( $help ) . '

'; } echo ''; } /** * Test email panel, including the last result. */ private static function test_email_form() { $result = get_transient( 'accessible_forms_test_result_' . get_current_user_id() ); echo '
'; echo '

' . esc_html__( 'Send a test email', 'accessible-forms-by-accessible-org' ) . '

'; echo '

' . esc_html__( 'Save your settings first, then send a test. Check the recipient’s inbox as well as the result below: acceptance by a mail service does not guarantee delivery to the inbox.', 'accessible-forms-by-accessible-org' ) . '

'; if ( is_array( $result ) ) { delete_transient( 'accessible_forms_test_result_' . get_current_user_id() ); $class = $result['sent'] ? 'notice-success' : 'notice-error'; // Keep the delivery result beside its test controls; WordPress otherwise // relocates notices above the page title on ready. echo '

'; echo $result['sent'] ? esc_html__( 'Test email accepted by the mail service.', 'accessible-forms-by-accessible-org' ) : ( ! empty( $result['uncertain'] ) ? esc_html__( 'Test email delivery is uncertain. Check before sending again.', 'accessible-forms-by-accessible-org' ) : esc_html__( 'Test email failed.', 'accessible-forms-by-accessible-org' ) ); echo '

'; echo '

' . esc_html( sprintf( /* translators: 1: recipient, 2: transport description. */ __( 'To %1$s using %2$s', 'accessible-forms-by-accessible-org' ), $result['to'], $result['transport'] ) ) . '

'; EntryWorkflow::mail_routes( $result['routes'] ?? array() ); if ( ! empty( $result['error'] ) ) { echo '

' . esc_html( $result['error'] ) . '

'; } if ( ! empty( $result['debug'] ) ) { echo '
' . esc_html__( 'Mail server conversation', 'accessible-forms-by-accessible-org' ) . ''; echo '
' . esc_html( implode( "\n", $result['debug'] ) ) . '
'; } echo '
'; } echo '
'; echo ''; wp_nonce_field( self::NONCE_TEST, 'af_test_email_nonce', false ); echo '


'; echo '

'; submit_button( __( 'Send test email', 'accessible-forms-by-accessible-org' ), 'secondary', 'af_test_submit', false ); echo '
'; } }