settings_repository = $settings_repository;
$this->integration_status_provider = $integration_status_provider;
$this->dashboard_view_model = $dashboard_view_model;
$this->diagnostics_provider = $diagnostics_provider;
}
/**
* Render the admin page.
*/
public function render(): void {
if ( ! Capability::can_manage_settings() ) {
wp_die( esc_html__( 'You do not have permission to access 2o2code Spamless Forms settings.', '2o2code-spamless-forms' ) );
}
$notice = $this->maybe_handle_save();
$tab = $this->get_current_tab();
echo '
';
echo '
' . esc_html__( '2o2code Spamless Forms', '2o2code-spamless-forms' ) . ' ';
if ( '' !== $notice ) {
echo '
' . esc_html( $notice ) . '
';
}
$this->render_tabs( $tab );
switch ( $tab ) {
case self::TAB_PROTECTION:
$this->render_protection();
break;
case self::TAB_INTEGRATIONS:
$this->render_integrations();
break;
case self::TAB_ACTIVITY:
$this->render_activity();
break;
case self::TAB_TOOLS:
$this->render_tools();
break;
case self::TAB_ABOUT:
$this->render_about();
break;
default:
$this->render_dashboard();
break;
}
echo '
';
}
/**
* Render tab navigation.
*
* @param string $current_tab Current tab.
*/
private function render_tabs( string $current_tab ): void {
echo '';
foreach ( $this->get_tabs() as $tab => $label ) {
$class = 'nav-tab';
if ( $current_tab === $tab ) {
$class .= ' nav-tab-active';
}
$aria_current = $current_tab === $tab ? 'page' : '';
echo '' . esc_html( $label ) . ' ';
}
echo ' ';
}
/**
* Render dashboard screen.
*/
private function render_dashboard(): void {
$data = $this->dashboard_view_model->get_data();
$status = isset( $data['protection_status'] ) && is_array( $data['protection_status'] ) ? $data['protection_status'] : $this->get_default_status();
$summary = isset( $data['summary'] ) && is_array( $data['summary'] ) ? $data['summary'] : $this->get_default_summary();
$event_summary = isset( $data['event_summary'] ) && is_array( $data['event_summary'] ) ? $data['event_summary'] : $this->get_default_event_summary();
$integrations = isset( $data['integrations'] ) && is_array( $data['integrations'] ) ? $data['integrations'] : array();
$events = isset( $data['recent_events'] ) && is_array( $data['recent_events'] ) ? $data['recent_events'] : array();
$settings = $this->settings_repository->get_settings();
$activity_text = $this->get_activity_summary_label( $event_summary, ! empty( $settings['log_allowed_events'] ) );
echo '';
$this->render_page_header(
'twootwocode-zsf-dashboard-title',
__( 'Dashboard', '2o2code-spamless-forms' ),
__( 'A quick read on protection, setup health, and what to check next.', '2o2code-spamless-forms' )
);
$this->render_status_band( $status, (string) $data['strictness'] );
echo '';
$integration_metric = $this->get_integration_metric_copy(
(string) $settings['protection_mode'],
$this->count_protected_integrations( $integrations ),
(int) $summary['total']
);
$this->render_metric_card(
$integration_metric['label'],
$integration_metric['value'],
$integration_metric['description']
);
$this->render_metric_card(
__( 'Event logging', '2o2code-spamless-forms' ),
! empty( $settings['event_logging_enabled'] ) ? __( 'Enabled', '2o2code-spamless-forms' ) : __( 'Disabled', '2o2code-spamless-forms' ),
! empty( $settings['event_logging_enabled'] ) ? __( 'Minimal metadata can appear in Activity.', '2o2code-spamless-forms' ) : __( 'Activity will be limited while logging is off.', '2o2code-spamless-forms' )
);
$this->render_metric_card(
__( 'Privacy-safe logging', '2o2code-spamless-forms' ),
__( 'Minimal metadata', '2o2code-spamless-forms' ),
__( 'No full submissions, raw IPs, raw emails, or raw user agents are stored.', '2o2code-spamless-forms' )
);
$this->render_metric_card(
__( 'Recent activity', '2o2code-spamless-forms' ),
$activity_text,
__( 'Counts come from stored events only.', '2o2code-spamless-forms' )
);
echo '
';
$this->render_next_action( $status, $summary, $events, $settings );
$this->render_onboarding_checklist();
echo '';
echo '
';
echo '
' . esc_html__( 'Integration status', '2o2code-spamless-forms' ) . ' ';
$this->render_dashboard_integration_summary( $integrations, (string) $settings['protection_mode'] );
echo '';
echo '
';
echo '
';
$this->render_privacy_note( (string) $data['privacy_note'] );
$this->render_privacy_note( (string) $data['event_privacy_note'] );
echo ' ';
}
/**
* Render protection settings screen.
*/
private function render_protection(): void {
$settings = $this->settings_repository->get_settings();
echo '';
$this->render_page_header(
'twootwocode-zsf-protection-title',
__( 'Protection', '2o2code-spamless-forms' ),
__( 'Choose how 2o2code Spamless Forms should handle suspicious submissions.', '2o2code-spamless-forms' )
);
echo '';
echo ' ';
}
/**
* Render integrations screen.
*/
private function render_integrations(): void {
$settings = $this->settings_repository->get_settings();
$integrations = $this->integration_status_provider->get_statuses();
$data = $this->dashboard_view_model->get_data();
$events = isset( $data['activity_events'] ) && is_array( $data['activity_events'] ) ? $data['activity_events'] : array();
echo '';
$this->render_page_header(
'twootwocode-zsf-integrations-title',
__( 'Integrations', '2o2code-spamless-forms' ),
__( 'Review what is detected and choose which Free integrations should be protected.', '2o2code-spamless-forms' )
);
if ( ! $this->has_detected_external_integration( $integrations ) ) {
$this->render_empty_state(
__( 'No supported form plugin has been detected yet.', '2o2code-spamless-forms' ),
__( 'Native WordPress comments, login, and registration can still be protected when they are available and enabled.', '2o2code-spamless-forms' )
);
}
echo '';
echo ' ';
}
/**
* Render activity screen.
*/
private function render_activity(): void {
$data = $this->dashboard_view_model->get_data();
$events = isset( $data['activity_events'] ) && is_array( $data['activity_events'] ) ? $data['activity_events'] : array();
$event_summary = isset( $data['event_summary'] ) && is_array( $data['event_summary'] ) ? $data['event_summary'] : $this->get_default_event_summary();
$settings = $this->settings_repository->get_settings();
$show_allowed = ! empty( $settings['log_allowed_events'] );
echo '';
$this->render_page_header(
'twootwocode-zsf-activity-title',
__( 'Activity', '2o2code-spamless-forms' ),
$show_allowed
? __( 'Recent allowed, blocked, and suspicious decisions from privacy-safe event logging.', '2o2code-spamless-forms' )
: __( 'Recent blocked and suspicious decisions from privacy-safe event logging.', '2o2code-spamless-forms' )
);
echo '';
$this->render_metric_card( __( 'Blocked', '2o2code-spamless-forms' ), (string) (int) $event_summary['blocked'], __( 'High-confidence spam that was blocked.', '2o2code-spamless-forms' ) );
$this->render_metric_card( __( 'Suspicious', '2o2code-spamless-forms' ), (string) (int) $event_summary['suspicious'], __( 'Submissions flagged without being blocked.', '2o2code-spamless-forms' ) );
if ( $show_allowed ) {
$this->render_metric_card( __( 'Allowed', '2o2code-spamless-forms' ), (string) (int) $event_summary['allowed'], __( 'Successful submissions recorded for temporary troubleshooting.', '2o2code-spamless-forms' ) );
}
$this->render_metric_card( __( 'Retention period', '2o2code-spamless-forms' ), sprintf( '%d', (int) $settings['event_retention_days'] ), __( 'Days that minimal Activity metadata is kept.', '2o2code-spamless-forms' ) );
echo '
';
if ( empty( $settings['event_logging_enabled'] ) ) {
$this->render_empty_state(
__( 'Event logging is disabled.', '2o2code-spamless-forms' ),
__( 'Activity will be limited until minimal event logging is enabled on the Protection page.', '2o2code-spamless-forms' )
);
}
$this->render_recent_events( $events );
$this->render_privacy_note( __( 'Activity never shows full submissions, raw IP addresses, raw emails, raw user agents, tokens, secrets, or stored hashes.', '2o2code-spamless-forms' ) );
echo ' ';
}
/**
* Render tools screen.
*/
private function render_tools(): void {
$data = $this->diagnostics_provider->get_data();
echo '';
$this->render_page_header(
'twootwocode-zsf-tools-title',
__( 'Tools', '2o2code-spamless-forms' ),
__( 'Support-safe diagnostics for the Free build.', '2o2code-spamless-forms' )
);
$this->render_privacy_note( __( 'Diagnostics show safe configuration details only. They do not include secrets, raw submissions, raw IP addresses, raw emails, raw user agents, tokens, or stored hashes.', '2o2code-spamless-forms' ) );
echo '';
echo '
';
echo '
' . esc_html__( 'System information', '2o2code-spamless-forms' ) . ' ';
echo '
';
$this->render_diagnostic_row( __( 'Plugin version', '2o2code-spamless-forms' ), (string) $data['plugin_version'] );
$this->render_diagnostic_row( __( 'WordPress version', '2o2code-spamless-forms' ), '' === $data['wordpress_version'] ? __( 'Unavailable in this context', '2o2code-spamless-forms' ) : (string) $data['wordpress_version'] );
$this->render_diagnostic_row( __( 'PHP version', '2o2code-spamless-forms' ), (string) $data['php_version'] );
$this->render_diagnostic_row( __( 'Build profile', '2o2code-spamless-forms' ), (string) $data['build_profile'] );
$this->render_diagnostic_row( __( 'Composer autoload', '2o2code-spamless-forms' ), ! empty( $data['vendor_autoload_available'] ) ? __( 'Available', '2o2code-spamless-forms' ) : __( 'Not available', '2o2code-spamless-forms' ) );
echo '
';
echo '
';
echo '
';
echo '
' . esc_html__( 'Runtime state', '2o2code-spamless-forms' ) . ' ';
echo '
';
$this->render_diagnostic_row( __( 'Protection mode', '2o2code-spamless-forms' ), $this->get_mode_label( (string) $data['protection_mode'] ) );
$this->render_diagnostic_row( __( 'Strictness', '2o2code-spamless-forms' ), $this->get_strictness_label( (string) $data['strictness'] ) );
$this->render_diagnostic_row( __( 'Event logging', '2o2code-spamless-forms' ), ! empty( $data['event_logging_enabled'] ) ? __( 'Enabled', '2o2code-spamless-forms' ) : __( 'Disabled', '2o2code-spamless-forms' ) );
$this->render_diagnostic_row( __( 'Allowed-submission logging', '2o2code-spamless-forms' ), ! empty( $data['log_allowed_events'] ) ? __( 'Enabled', '2o2code-spamless-forms' ) : __( 'Disabled', '2o2code-spamless-forms' ) );
$this->render_diagnostic_row( __( 'Stored event count', '2o2code-spamless-forms' ), (string) (int) $data['stored_event_count'] );
$this->render_diagnostic_row( __( 'Runtime PRO modules', '2o2code-spamless-forms' ), (string) (int) $data['pro_module_count_loaded'] );
echo '
';
echo '
';
echo '
';
echo '' . esc_html__( 'Integration availability', '2o2code-spamless-forms' ) . ' ';
$this->render_integration_table( isset( $data['integrations'] ) && is_array( $data['integrations'] ) ? $data['integrations'] : array(), (string) $data['protection_mode'] );
echo '' . esc_html__( 'Package and runtime safety', '2o2code-spamless-forms' ) . ' ';
echo '';
echo '' . esc_html__( 'Free runtime defaults to the free build profile.', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'No external anti-spam API is required.', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Full submission storage is disabled.', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'This Free version contains no license or purchase controls.', '2o2code-spamless-forms' ) . ' ';
echo ' ';
echo '' . esc_html__( 'Required PHP extensions', '2o2code-spamless-forms' ) . ' ';
echo '';
$extensions = isset( $data['php_extensions'] ) && is_array( $data['php_extensions'] ) ? $data['php_extensions'] : array();
foreach ( $extensions as $extension => $available ) {
$this->render_diagnostic_row( (string) $extension, ! empty( $available ) ? __( 'Available', '2o2code-spamless-forms' ) : __( 'Missing', '2o2code-spamless-forms' ) );
}
echo '
';
echo ' ';
}
/**
* Render About screen.
*/
private function render_about(): void {
echo '';
$this->render_page_header(
'twootwocode-zsf-about-title',
__( 'About 2o2code Spamless Forms', '2o2code-spamless-forms' ),
__( 'Local, no-API, no-CAPTCHA anti-spam protection for WordPress forms.', '2o2code-spamless-forms' )
);
echo '';
echo '
';
echo '
' . esc_html__( 'What it does', '2o2code-spamless-forms' ) . ' ';
echo '
' . esc_html__( '2o2code Spamless Forms checks submissions on this site. It does not require an external anti-spam account, CAPTCHA challenge, tracking pixel, or remote scoring API.', '2o2code-spamless-forms' ) . '
';
echo '
';
echo '
';
echo '
' . esc_html__( 'Privacy first', '2o2code-spamless-forms' ) . ' ';
echo '
' . esc_html__( 'When Activity logging is enabled, 2o2code Spamless Forms stores minimal event metadata. Full submissions, raw IP addresses, raw emails, and raw user agents are not stored.', '2o2code-spamless-forms' ) . '
';
echo '
';
echo '
';
echo '' . esc_html__( 'Included Free features', '2o2code-spamless-forms' ) . ' ';
echo '';
foreach ( $this->get_free_feature_labels() as $feature ) {
echo '' . esc_html( $feature ) . ' ';
}
echo ' ';
echo '' . esc_html__( 'Supported Free integrations', '2o2code-spamless-forms' ) . ' ';
echo '';
foreach ( $this->get_supported_free_integration_labels() as $integration ) {
echo '' . esc_html( $integration ) . ' ';
}
echo ' ';
echo '' . esc_html__( 'Ongoing maintenance', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( '2o2code Spamless Forms is actively maintained for supported WordPress versions and form integrations. Compatibility, security, privacy, and usability improvements are prioritized as WordPress and supported plugins evolve.', '2o2code-spamless-forms' ) . '
';
echo ' ';
}
/**
* Render protection mode settings.
*
* @param array $settings Settings.
*/
private function render_protection_mode_settings( array $settings ): void {
echo '';
echo '' . esc_html__( 'Mode', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Choose whether to block spam, observe decisions without blocking, or temporarily disable protection.', '2o2code-spamless-forms' ) . '
';
$this->render_radio_cards(
'protection_mode',
(string) $settings['protection_mode'],
array(
SettingsSnapshot::MODE_PROTECT => array(
'label' => __( 'Protect', '2o2code-spamless-forms' ),
'description' => __( 'Blocks high-confidence spam. Activity is recorded only when event logging is enabled.', '2o2code-spamless-forms' ),
),
SettingsSnapshot::MODE_MONITOR => array(
'label' => __( 'Monitor', '2o2code-spamless-forms' ),
'description' => __( 'Observes spam-related decisions without blocking submissions. Event logging must be enabled for Activity.', '2o2code-spamless-forms' ),
),
SettingsSnapshot::MODE_DISABLED => array(
'label' => __( 'Disabled', '2o2code-spamless-forms' ),
'description' => __( 'Allows all submissions and creates no new Activity events. Use only while troubleshooting.', '2o2code-spamless-forms' ),
),
)
);
if ( SettingsSnapshot::MODE_DISABLED === (string) $settings['protection_mode'] ) {
$this->render_warning_note( __( 'Disabled mode turns protection and new event logging off. Strictness and threshold settings remain saved but do not affect submissions until protection is enabled again.', '2o2code-spamless-forms' ) );
}
echo ' ';
}
/**
* Render strictness settings.
*
* @param array $settings Settings.
*/
private function render_strictness_settings( array $settings ): void {
echo '';
echo '' . esc_html__( 'Strictness', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Strictness controls timing-only signals. Hard spam signals are blocked in Protect, recorded in Monitor, and ignored in Disabled mode.', '2o2code-spamless-forms' ) . '
';
$this->render_radio_cards(
'strictness',
(string) $settings['strictness'],
array(
SettingsSnapshot::STRICTNESS_SAFE => array(
'label' => __( 'Safe', '2o2code-spamless-forms' ),
'description' => __( 'Ignores timing-only signals for the lowest false-positive risk.', '2o2code-spamless-forms' ),
),
SettingsSnapshot::STRICTNESS_BALANCED => array(
'label' => __( 'Balanced', '2o2code-spamless-forms' ),
'description' => __( 'Records timing-only signals as Suspicious without blocking. Recommended for most sites.', '2o2code-spamless-forms' ),
),
SettingsSnapshot::STRICTNESS_STRICT => array(
'label' => __( 'Strict', '2o2code-spamless-forms' ),
'description' => __( 'Blocks submissions completed below the minimum fill time in Protect mode. Test carefully before using on busy forms.', '2o2code-spamless-forms' ),
),
)
);
echo ' ';
}
/**
* Render logging and privacy settings.
*
* @param array $settings Settings.
*/
private function render_logging_privacy_settings( array $settings ): void {
echo '';
echo '' . esc_html__( 'Logging and privacy', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Event logging stores minimal metadata only. Full submissions, raw IP addresses, raw emails, and raw user agents are not stored.', '2o2code-spamless-forms' ) . '
';
$this->render_checkbox_field(
'event_logging_enabled',
__( 'Enable minimal event logging', '2o2code-spamless-forms' ),
! empty( $settings['event_logging_enabled'] ),
__( 'Records Blocked and Suspicious decisions as minimal Activity metadata. Allowed decisions require both this setting and the separate option below.', '2o2code-spamless-forms' )
);
$this->render_number_field(
'event_retention_days',
__( 'Retention days', '2o2code-spamless-forms' ),
(string) $settings['event_retention_days'],
1,
90,
'1',
__( 'Events older than this are removed when new events are written.', '2o2code-spamless-forms' )
);
$this->render_number_field(
'max_event_records',
__( 'Maximum stored events', '2o2code-spamless-forms' ),
(string) $settings['max_event_records'],
25,
1000,
'1',
__( 'Limits how many Activity events are kept.', '2o2code-spamless-forms' )
);
$this->render_checkbox_field(
'log_allowed_events',
__( 'Log allowed submissions', '2o2code-spamless-forms' ),
! empty( $settings['log_allowed_events'] ),
__( 'Records successful submissions as Allowed in Activity. Requires event logging and Protect or Monitor mode.', '2o2code-spamless-forms' )
);
if ( empty( $settings['event_logging_enabled'] ) ) {
$this->render_warning_note( __( 'Event logging is off, so no new Activity is created. Allowed-submission logging has no effect. Retention and maximum-event settings remain saved, and existing Activity remains subject to those limits.', '2o2code-spamless-forms' ) );
}
$this->render_checkbox_field(
'delete_data_on_uninstall',
__( 'Delete data on uninstall', '2o2code-spamless-forms' ),
! empty( $settings['delete_data_on_uninstall'] ),
__( 'When enabled, plugin settings and minimal event metadata are removed when the plugin is deleted.', '2o2code-spamless-forms' )
);
echo ' ';
}
/**
* Render advanced settings.
*
* @param array $settings Settings.
*/
private function render_advanced_settings( array $settings ): void {
echo '';
echo '' . esc_html__( 'Advanced thresholds', '2o2code-spamless-forms' ) . ' ';
echo '';
echo '' . esc_html__( 'Advanced', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'These defaults are intentionally conservative. Change them only when a site needs different sensitivity.', '2o2code-spamless-forms' ) . '
';
$this->render_number_field( 'minimum_fill_time_seconds', __( 'Minimum fill time', '2o2code-spamless-forms' ), (string) $settings['minimum_fill_time_seconds'], 1, 60, '1', __( 'Shortest expected time, in seconds, for a human submission.', '2o2code-spamless-forms' ) );
$this->render_number_field( 'token_lifetime_seconds', __( 'Token lifetime', '2o2code-spamless-forms' ), (string) $settings['token_lifetime_seconds'], 60, 86400, '60', __( 'How long a generated form token remains valid.', '2o2code-spamless-forms' ) );
$this->render_number_field( 'rate_limit_window_seconds', __( 'Rate limit window', '2o2code-spamless-forms' ), (string) $settings['rate_limit_window_seconds'], 60, 86400, '60', __( 'Time window used for local rate-limit checks.', '2o2code-spamless-forms' ) );
$this->render_number_field( 'rate_limit_threshold', __( 'Rate limit threshold', '2o2code-spamless-forms' ), (string) $settings['rate_limit_threshold'], 1, 1000, '1', __( 'Maximum allowed submissions in the rate-limit window.', '2o2code-spamless-forms' ) );
$this->render_number_field( 'link_density_threshold', __( 'Link density threshold', '2o2code-spamless-forms' ), (string) $settings['link_density_threshold'], 0.05, 1, '0.05', __( 'Maximum link density before a submission is treated as suspicious.', '2o2code-spamless-forms' ) );
echo ' ';
echo ' ';
}
/**
* Render form security fields.
*
* @param string $section Settings section.
*/
private function render_form_security_fields( string $section ): void {
if ( function_exists( 'wp_nonce_field' ) ) {
wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD );
}
echo ' ';
echo ' ';
}
/**
* Render a page title and description.
*
* @param string $id Heading ID.
* @param string $title Title.
* @param string $description Description.
*/
private function render_page_header( string $id, string $title, string $description ): void {
echo '';
}
/**
* Render the main dashboard status band.
*
* @param array $status Status data.
* @param string $strictness Strictness label.
*/
private function render_status_band( array $status, string $strictness ): void {
$status_key = isset( $status['key'] ) && is_scalar( $status['key'] ) ? (string) $status['key'] : 'protect';
$headline = isset( $status['headline'] ) && is_scalar( $status['headline'] ) ? (string) $status['headline'] : __( 'Protection is active.', '2o2code-spamless-forms' );
$description = isset( $status['description'] ) && is_scalar( $status['description'] ) ? (string) $status['description'] : __( '2o2code Spamless Forms is protecting enabled integrations with local checks.', '2o2code-spamless-forms' );
$mode_label = isset( $status['label'] ) && is_scalar( $status['label'] ) ? (string) $status['label'] : __( 'Protect', '2o2code-spamless-forms' );
$sanitized_status_key = preg_replace( '/[^a-z0-9_-]/', '', strtolower( $status_key ) );
$status_key = is_string( $sanitized_status_key ) && '' !== $sanitized_status_key ? $sanitized_status_key : 'protect';
$status_class = 'twootwocode-zsf-admin__status-band twootwocode-zsf-admin__status-band--' . $status_key;
echo '';
echo '
';
echo '
' . esc_html__( 'Current protection', '2o2code-spamless-forms' ) . ' ';
echo '
' . esc_html( $headline ) . ' ';
echo '
' . esc_html( $description ) . '
';
echo '
';
echo '
';
echo '
' . esc_html__( 'Mode', '2o2code-spamless-forms' ) . ' ' . esc_html( $mode_label ) . ' ';
echo '
' . esc_html__( 'Strictness', '2o2code-spamless-forms' ) . ' ' . esc_html( $strictness ) . ' ';
echo ' ';
echo '
';
}
/**
* Render the next recommended action panel.
*
* @param array $status Protection status.
* @param array $summary Summary.
* @param array> $events Events.
* @param array $settings Settings.
*/
private function render_next_action( array $status, array $summary, array $events, array $settings ): void {
$action = $this->get_next_recommended_action( $status, $summary, $events, $settings );
echo '';
}
/**
* Render a lightweight onboarding checklist.
*/
private function render_onboarding_checklist(): void {
echo '';
echo '
' . esc_html__( 'First-run checklist', '2o2code-spamless-forms' ) . ' ';
echo '
';
echo '' . esc_html__( 'Review detected integrations.', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Keep the recommended defaults unless this is a sensitive form.', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Submit the form once with normal test content.', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Submit again with obvious spam-like content and confirm Activity is recorded.', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Use Monitor first if this form is business-critical.', '2o2code-spamless-forms' ) . ' ';
echo ' ';
echo '
';
}
/**
* Render dashboard integration summary.
*
* @param array> $integrations Integrations.
* @param string $protection_mode Protection mode.
*/
private function render_dashboard_integration_summary( array $integrations, string $protection_mode ): void {
if ( array() === $integrations ) {
$this->render_empty_state(
__( 'No integration status is available yet.', '2o2code-spamless-forms' ),
__( 'Open Integrations after activation to review supported Free sources.', '2o2code-spamless-forms' )
);
return;
}
echo '';
foreach ( $integrations as $integration ) {
$state = $this->get_integration_state_label( $integration, $protection_mode );
echo '';
echo '' . esc_html( (string) $integration['label'] ) . ' ';
echo '' . esc_html( $state ) . ' ';
echo ' ';
}
echo ' ';
}
/**
* Render integration management cards.
*
* @param array> $integrations Integrations.
* @param array $settings Settings.
* @param array> $events Recent events.
*/
private function render_integration_management_cards( array $integrations, array $settings, array $events ): void {
if ( array() === $integrations ) {
$this->render_empty_state(
__( 'No Free integration status is available yet.', '2o2code-spamless-forms' ),
__( 'The Free version supports Contact Form 7, WPForms Lite, Elementor Form Lite, WordPress comments, WordPress login, and WordPress registration.', '2o2code-spamless-forms' )
);
return;
}
echo '';
foreach ( $integrations as $integration ) {
$setting_key = isset( $integration['setting_key'] ) ? (string) $integration['setting_key'] : '';
if ( '' === $setting_key ) {
continue;
}
$key = (string) $integration['key'];
$last_event = $this->get_latest_event_for_source( $events, $key );
$is_enabled = ! empty( $settings[ $setting_key ] );
$is_detected = ! empty( $integration['available'] );
echo '
';
echo '';
echo '';
$this->render_status_pill( $is_detected ? __( 'Detected', '2o2code-spamless-forms' ) : __( 'Not detected', '2o2code-spamless-forms' ), $is_detected );
$this->render_status_pill( $is_enabled ? __( 'Enabled', '2o2code-spamless-forms' ) : __( 'Disabled', '2o2code-spamless-forms' ), $is_enabled );
echo '
';
echo '' . esc_html( $this->get_integration_state_label( $integration, (string) $settings['protection_mode'] ) ) . '
';
echo '' . esc_html( $this->get_integration_helper_text( $key ) ) . '
';
echo '' . esc_html( $this->get_last_event_label( $last_event ) ) . '
';
echo '';
echo ' ';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr returns a fixed boolean attribute string.
echo ' checked_attr( $is_enabled ) . ' />';
echo '' . esc_html__( 'Enable protection', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'When enabled, this integration is protected or monitored whenever it is detected and the selected mode is active.', '2o2code-spamless-forms' ) . ' ';
echo ' ';
echo '';
echo '
' . esc_html__( 'How to test', '2o2code-spamless-forms' ) . ' ';
echo '
' . esc_html__( 'Submit the form once with normal test content.', '2o2code-spamless-forms' ) . '
';
echo '
' . esc_html__( 'Submit again with obvious spam-like content and confirm the activity is recorded.', '2o2code-spamless-forms' ) . '
';
echo '
';
echo ' ';
}
if ( $this->is_free_build() ) {
$this->render_elementor_pro_information_card();
}
echo '
';
}
/**
* Render the non-interactive Elementor Pro boundary note in Free.
*/
private function render_elementor_pro_information_card(): void {
echo '';
echo '';
echo '';
$this->render_status_pill( __( 'Planned for PRO', '2o2code-spamless-forms' ), false );
echo '
';
echo '' . esc_html__( 'Elementor Pro Forms protection is planned for a future PRO release.', '2o2code-spamless-forms' ) . '
';
echo '' . esc_html__( 'Elementor Form Lite is included in Free. Elementor Atomic Forms are not supported.', '2o2code-spamless-forms' ) . '
';
echo ' ';
}
/**
* Render safe recent event rows.
*
* @param array> $events Recent events.
*/
private function render_recent_events( array $events ): void {
if ( array() === $events ) {
$this->render_empty_state(
__( 'No activity to display yet.', '2o2code-spamless-forms' ),
__( 'Recent decisions will appear here when their logging options are enabled. This is normal for a new install or a quiet site.', '2o2code-spamless-forms' )
);
return;
}
echo '';
echo '
';
echo '';
echo '' . esc_html__( 'Date', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Source', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Action', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Reason', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Score', '2o2code-spamless-forms' ) . ' ';
echo ' ';
foreach ( $events as $event ) {
$reason_labels = isset( $event['reason_labels'] ) && is_array( $event['reason_labels'] ) ? $event['reason_labels'] : array();
echo '';
echo '' . esc_html( $this->format_event_date( isset( $event['created_at'] ) ? (int) $event['created_at'] : 0 ) ) . ' ';
echo '' . esc_html( $this->get_event_source_label( isset( $event['source_key'] ) ? (string) $event['source_key'] : '' ) ) . ' ';
echo '' . esc_html( $this->get_event_action_label( isset( $event['action'] ) ? (string) $event['action'] : '' ) ) . ' ';
echo '' . esc_html( implode( ', ', array_map( 'strval', $reason_labels ) ) ) . ' ';
echo '' . esc_html( (string) ( isset( $event['score'] ) ? (int) $event['score'] : 0 ) ) . ' ';
echo ' ';
}
echo '
';
echo '
';
}
/**
* Render integration diagnostics table.
*
* @param array> $integrations Integrations.
* @param string $protection_mode Protection mode.
*/
private function render_integration_table( array $integrations, string $protection_mode ): void {
if ( array() === $integrations ) {
$this->render_empty_state(
__( 'Integration status is unavailable.', '2o2code-spamless-forms' ),
__( 'Refresh this page. If the problem continues, verify that 2o2code Spamless Forms is active.', '2o2code-spamless-forms' )
);
return;
}
echo '';
echo '
';
echo '';
echo '' . esc_html__( 'Integration', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Source key', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Detected', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Enabled', '2o2code-spamless-forms' ) . ' ';
echo '' . esc_html__( 'Integration state', '2o2code-spamless-forms' ) . ' ';
echo ' ';
foreach ( $integrations as $integration ) {
echo '';
echo '' . esc_html( (string) $integration['label'] ) . ' ';
echo '' . esc_html( (string) $integration['key'] ) . ' ';
echo '' . esc_html( ! empty( $integration['available'] ) ? __( 'Yes', '2o2code-spamless-forms' ) : __( 'No', '2o2code-spamless-forms' ) ) . ' ';
echo '' . esc_html( ! empty( $integration['enabled'] ) ? __( 'Yes', '2o2code-spamless-forms' ) : __( 'No', '2o2code-spamless-forms' ) ) . ' ';
echo '' . esc_html( $this->get_integration_state_label( $integration, $protection_mode ) ) . ' ';
echo ' ';
}
echo '
';
echo '
';
}
/**
* Build a concise activity summary from visible event classes.
*
* @param array $summary Event summary.
* @param bool $show_allowed Whether allowed events are visible.
*/
private function get_activity_summary_label( array $summary, bool $show_allowed ): string {
if ( $show_allowed ) {
return sprintf(
/* translators: 1: blocked count, 2: suspicious count, 3: allowed count. */
__( '%1$d blocked, %2$d suspicious, %3$d allowed', '2o2code-spamless-forms' ),
isset( $summary['blocked'] ) ? (int) $summary['blocked'] : 0,
isset( $summary['suspicious'] ) ? (int) $summary['suspicious'] : 0,
isset( $summary['allowed'] ) ? (int) $summary['allowed'] : 0
);
}
return sprintf(
/* translators: 1: blocked count, 2: suspicious count. */
__( '%1$d blocked, %2$d suspicious', '2o2code-spamless-forms' ),
isset( $summary['blocked'] ) ? (int) $summary['blocked'] : 0,
isset( $summary['suspicious'] ) ? (int) $summary['suspicious'] : 0
);
}
/**
* Render a metric card.
*
* @param string $label Metric label.
* @param string $value Metric value.
* @param string $description Metric description.
*/
private function render_metric_card( string $label, string $value, string $description = '' ): void {
echo '';
echo '' . esc_html( $label ) . ' ';
echo '' . esc_html( $value ) . ' ';
if ( '' !== $description ) {
echo '' . esc_html( $description ) . ' ';
}
echo '
';
}
/**
* Render radio-card controls.
*
* @param string $key Setting key.
* @param string $value Current value.
* @param array $options Options.
*/
private function render_radio_cards( string $key, string $value, array $options ): void {
echo '';
foreach ( $options as $option_value => $option ) {
$field_id = 'twootwocode-zsf-' . str_replace( '_', '-', $key ) . '-' . str_replace( '_', '-', (string) $option_value );
echo '';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr returns a fixed boolean attribute string.
echo ' checked_attr( $value === (string) $option_value ) . ' />';
echo '' . esc_html( $option['label'] ) . ' ';
echo '' . esc_html( $option['description'] ) . ' ';
echo ' ';
}
echo '
';
}
/**
* Render a number field.
*
* @param string $key Setting key.
* @param string $label Field label.
* @param string $value Current value.
* @param int|float $min Minimum.
* @param int|float $max Maximum.
* @param string $step Step.
* @param string $description Description.
*/
private function render_number_field( string $key, string $label, string $value, $min, $max, string $step, string $description = '' ): void {
echo '';
echo '' . esc_html( $label ) . ' ';
echo ' ';
if ( '' !== $description ) {
echo '' . esc_html( $description ) . ' ';
}
echo ' ';
}
/**
* Render a checkbox field.
*
* @param string $key Setting key.
* @param string $label Field label.
* @param bool $checked Whether checked.
* @param string $description Description.
*/
private function render_checkbox_field( string $key, string $label, bool $checked, string $description ): void {
echo '';
echo ' ';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr returns a fixed boolean attribute string.
echo ' checked_attr( $checked ) . ' />';
echo '' . esc_html( $label ) . ' ';
echo '' . esc_html( $description ) . ' ';
echo ' ';
}
/**
* Render a status pill.
*
* @param string $label Pill label.
* @param bool $positive Whether status is positive.
*/
private function render_status_pill( string $label, bool $positive ): void {
$class = $positive ? 'twootwocode-zsf-admin__pill twootwocode-zsf-admin__pill--positive' : 'twootwocode-zsf-admin__pill';
echo '' . esc_html( $label ) . ' ';
}
/**
* Render a privacy note.
*
* @param string $text Note text.
*/
private function render_privacy_note( string $text ): void {
echo '' . esc_html( $text ) . '
';
}
/**
* Render a warning note.
*
* @param string $text Note text.
*/
private function render_warning_note( string $text ): void {
echo '' . esc_html( $text ) . '
';
}
/**
* Render an empty state.
*
* @param string $title Title.
* @param string $description Description.
*/
private function render_empty_state( string $title, string $description ): void {
echo '';
echo '
' . esc_html( $title ) . ' ';
echo '
' . esc_html( $description ) . '
';
echo '
';
}
/**
* Render a diagnostics row.
*
* @param string $label Label.
* @param string $value Value.
*/
private function render_diagnostic_row( string $label, string $value ): void {
echo '' . esc_html( $label ) . ' ' . esc_html( $value ) . ' ';
}
/**
* Handle the settings save action.
*/
private function maybe_handle_save(): string {
if ( 'POST' !== $this->get_request_method() ) {
return '';
}
if ( ! Capability::can_manage_settings() ) {
wp_die( esc_html__( 'You do not have permission to change 2o2code Spamless Forms settings.', '2o2code-spamless-forms' ) );
}
$action = $this->get_post_scalar( 'twootwocode_zsf_action' );
if ( self::ACTION_SAVE !== $action ) {
return '';
}
$nonce_is_valid = function_exists( 'wp_verify_nonce' ) && wp_verify_nonce( $this->get_post_scalar( self::NONCE_FIELD ), self::NONCE_ACTION );
if ( ! $nonce_is_valid ) {
wp_die( esc_html__( 'Settings could not be verified. Please refresh and try again.', '2o2code-spamless-forms' ) );
}
$section = $this->get_post_scalar( self::SECTION_FIELD );
if ( ! in_array( $section, array( self::SECTION_PROTECTION, self::SECTION_INTEGRATIONS ), true ) ) {
return __( 'Settings could not be saved. Please refresh and try again.', '2o2code-spamless-forms' );
}
$settings = array_merge(
$this->settings_repository->get_settings(),
$this->prepare_settings_for_save( $this->get_post_settings(), $section )
);
if ( ! $this->settings_repository->save( $settings ) ) {
return __( 'Settings could not be saved. Please refresh and try again.', '2o2code-spamless-forms' );
}
return __( 'Settings saved.', '2o2code-spamless-forms' );
}
/**
* Prepare known settings from posted form data.
*
* @param array $posted_settings Posted settings.
* @param string $section Settings section.
*
* @return array
*/
private function prepare_settings_for_save( array $posted_settings, string $section ): array {
if ( self::SECTION_INTEGRATIONS === $section ) {
return $this->prepare_integration_settings_for_save( $posted_settings );
}
return $this->prepare_protection_settings_for_save( $posted_settings );
}
/**
* Prepare known protection settings.
*
* @param array $posted_settings Posted settings.
*
* @return array
*/
private function prepare_protection_settings_for_save( array $posted_settings ): array {
$known_keys = array(
'protection_mode',
'strictness',
'minimum_fill_time_seconds',
'token_lifetime_seconds',
'rate_limit_window_seconds',
'rate_limit_threshold',
'link_density_threshold',
'event_retention_days',
'max_event_records',
);
$settings = array();
foreach ( $known_keys as $known_key ) {
if ( isset( $posted_settings[ $known_key ] ) && is_scalar( $posted_settings[ $known_key ] ) ) {
$settings[ $known_key ] = sanitize_text_field( wp_unslash( $posted_settings[ $known_key ] ) );
}
}
foreach ( array( 'event_logging_enabled', 'log_allowed_events', 'delete_data_on_uninstall' ) as $boolean_key ) {
$settings[ $boolean_key ] = isset( $posted_settings[ $boolean_key ] ) && is_scalar( $posted_settings[ $boolean_key ] )
? sanitize_text_field( wp_unslash( $posted_settings[ $boolean_key ] ) )
: '0';
}
return $settings;
}
/**
* Prepare known integration settings.
*
* @param array $posted_settings Posted settings.
*
* @return array
*/
private function prepare_integration_settings_for_save( array $posted_settings ): array {
$settings = array();
foreach ( $this->integration_status_provider->get_setting_keys() as $setting_key ) {
$settings[ $setting_key ] = isset( $posted_settings[ $setting_key ] ) && is_scalar( $posted_settings[ $setting_key ] )
? sanitize_text_field( wp_unslash( $posted_settings[ $setting_key ] ) )
: '0';
}
return $settings;
}
/**
* Get supported tabs.
*
* @return array
*/
private function get_tabs(): array {
return array(
self::TAB_DASHBOARD => __( 'Dashboard', '2o2code-spamless-forms' ),
self::TAB_PROTECTION => __( 'Protection', '2o2code-spamless-forms' ),
self::TAB_INTEGRATIONS => __( 'Integrations', '2o2code-spamless-forms' ),
self::TAB_ACTIVITY => __( 'Activity', '2o2code-spamless-forms' ),
self::TAB_TOOLS => __( 'Tools', '2o2code-spamless-forms' ),
self::TAB_ABOUT => __( 'About', '2o2code-spamless-forms' ),
);
}
/**
* Get current tab.
*/
private function get_current_tab(): string {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reading a tab value does not change state.
$tab = isset( $_GET['tab'] ) && is_scalar( $_GET['tab'] )
? sanitize_text_field( wp_unslash( $_GET['tab'] ) )
: self::TAB_DASHBOARD;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return array_key_exists( $tab, $this->get_tabs() ) ? $tab : self::TAB_DASHBOARD;
}
/**
* Get current request method.
*/
private function get_request_method(): string {
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
return '';
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Request method is normalized immediately.
return strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) );
}
/**
* Get a scalar POST value.
*
* @param string $key POST key.
*/
private function get_post_scalar( string $key ): string {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce is checked before mutating settings.
if ( ! isset( $_POST[ $key ] ) || ! is_scalar( $_POST[ $key ] ) ) {
return '';
}
return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
* Get posted settings.
*
* @return array
*/
private function get_post_settings(): array {
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Settings are allowlisted and sanitized before saving.
if ( ! isset( $_POST['twootwocode_zsf_settings'] ) || ! is_array( $_POST['twootwocode_zsf_settings'] ) ) {
return array();
}
return wp_unslash( $_POST['twootwocode_zsf_settings'] );
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
/**
* Get tab URL.
*
* @param string $tab Tab key.
*/
private function get_tab_url( string $tab ): string {
if ( ! function_exists( 'admin_url' ) ) {
return '';
}
return admin_url( 'options-general.php?page=' . AdminMenu::PAGE_SLUG . '&tab=' . rawurlencode( $tab ) );
}
/**
* Checked attribute helper.
*
* @param bool $checked Whether checked.
*/
private function checked_attr( bool $checked ): string {
return $checked ? ' checked="checked"' : '';
}
/**
* Get a safe integration label for an event source.
*
* @param string $source_key Source key.
*/
private function get_event_source_label( string $source_key ): string {
foreach ( $this->integration_status_provider->get_statuses() as $integration ) {
if ( $source_key === (string) $integration['key'] ) {
return (string) $integration['label'];
}
}
return __( 'Unknown source', '2o2code-spamless-forms' );
}
/**
* Get a safe action label.
*
* @param string $action Event action.
*/
private function get_event_action_label( string $action ): string {
if ( SpamEvent::ACTION_BLOCKED === $action ) {
return __( 'Blocked', '2o2code-spamless-forms' );
}
if ( SpamEvent::ACTION_SUSPICIOUS === $action ) {
return __( 'Suspicious', '2o2code-spamless-forms' );
}
if ( SpamEvent::ACTION_ALLOWED === $action ) {
return __( 'Allowed', '2o2code-spamless-forms' );
}
return __( 'Unknown', '2o2code-spamless-forms' );
}
/**
* Format event timestamp.
*
* @param int $timestamp UTC timestamp.
*/
private function format_event_date( int $timestamp ): string {
if ( $timestamp < 1 ) {
return '';
}
if ( function_exists( 'date_i18n' ) ) {
return date_i18n( 'Y-m-d H:i', $timestamp, true );
}
return gmdate( 'Y-m-d H:i', $timestamp );
}
/**
* Count integrations that are enabled and detected.
*
* @param array> $integrations Integrations.
*/
private function count_protected_integrations( array $integrations ): int {
$count = 0;
foreach ( $integrations as $integration ) {
if ( ! empty( $integration['available'] ) && ! empty( $integration['enabled'] ) ) {
++$count;
}
}
return $count;
}
/**
* Get mode-aware integration metric copy.
*
* @param string $protection_mode Protection mode.
* @param int $active_count Detected and enabled integration count.
* @param int $total_count Total integration count.
*
* @return array{label:string,value:string,description:string}
*/
private function get_integration_metric_copy( string $protection_mode, int $active_count, int $total_count ): array {
if ( SettingsSnapshot::MODE_MONITOR === $protection_mode ) {
return array(
'label' => __( 'Monitored integrations', '2o2code-spamless-forms' ),
'value' => sprintf(
/* translators: 1: monitored integrations, 2: total integrations. */
__( '%1$d of %2$d monitored', '2o2code-spamless-forms' ),
$active_count,
$total_count
),
'description' => __( 'Enabled and detected integrations are observed without blocking.', '2o2code-spamless-forms' ),
);
}
if ( SettingsSnapshot::MODE_DISABLED === $protection_mode ) {
return array(
'label' => __( 'Enabled integrations', '2o2code-spamless-forms' ),
'value' => sprintf(
/* translators: 1: ready integrations, 2: total integrations. */
__( '%1$d of %2$d ready', '2o2code-spamless-forms' ),
$active_count,
$total_count
),
'description' => __( 'Integration preferences remain saved while protection is disabled.', '2o2code-spamless-forms' ),
);
}
return array(
'label' => __( 'Protected integrations', '2o2code-spamless-forms' ),
'value' => sprintf(
/* translators: 1: protected integrations, 2: total integrations. */
__( '%1$d of %2$d protected', '2o2code-spamless-forms' ),
$active_count,
$total_count
),
'description' => __( 'Enabled and detected integrations are protected.', '2o2code-spamless-forms' ),
);
}
/**
* Determine whether an external form plugin is detected.
*
* @param array> $integrations Integrations.
*/
private function has_detected_external_integration( array $integrations ): bool {
foreach ( $integrations as $integration ) {
$key = isset( $integration['key'] ) ? (string) $integration['key'] : '';
if ( ! $this->is_native_integration( $key ) && ! empty( $integration['available'] ) ) {
return true;
}
}
return false;
}
/**
* Check whether an integration is a native WordPress source.
*
* @param string $key Integration key.
*/
private function is_native_integration( string $key ): bool {
return in_array( $key, array( 'wordpress_comments', 'wordpress_login', 'wordpress_registration' ), true );
}
/**
* Get integration state label.
*
* @param array $integration Integration row.
* @param string $protection_mode Protection mode.
*/
private function get_integration_state_label( array $integration, string $protection_mode = SettingsSnapshot::MODE_PROTECT ): string {
$available = ! empty( $integration['available'] );
$enabled = ! empty( $integration['enabled'] );
if ( $available && $enabled ) {
if ( SettingsSnapshot::MODE_MONITOR === $protection_mode ) {
return __( 'Detected and monitored.', '2o2code-spamless-forms' );
}
if ( SettingsSnapshot::MODE_DISABLED === $protection_mode ) {
return __( 'Detected and enabled, but protection is disabled.', '2o2code-spamless-forms' );
}
return __( 'Detected and protected.', '2o2code-spamless-forms' );
}
if ( $available ) {
return __( 'Detected, but protection is off for this integration.', '2o2code-spamless-forms' );
}
if ( $enabled ) {
return __( 'Enabled preference saved; integration not detected.', '2o2code-spamless-forms' );
}
return __( 'Not detected or enabled.', '2o2code-spamless-forms' );
}
/**
* Get integration helper text.
*
* @param string $key Integration key.
*/
private function get_integration_helper_text( string $key ): string {
if ( 'contact_form_7' === $key ) {
return __( 'Protects Contact Form 7 submissions when the plugin is active.', '2o2code-spamless-forms' );
}
if ( 'wpforms' === $key ) {
return __( 'Protects WPForms Lite submissions when WPForms Lite is active.', '2o2code-spamless-forms' );
}
if ( 'elementor_form_lite' === $key ) {
return __( 'Requires both Elementor and the Hello Plus companion plugin. Submit a Form Lite test and confirm its Activity source is Elementor Form Lite.', '2o2code-spamless-forms' );
}
if ( 'wordpress_comments' === $key ) {
return __( 'Protects native WordPress comment submissions.', '2o2code-spamless-forms' );
}
if ( 'wordpress_login' === $key ) {
return __( 'Protects native WordPress login attempts.', '2o2code-spamless-forms' );
}
if ( 'wordpress_registration' === $key ) {
return __( 'Protects native WordPress registration attempts.', '2o2code-spamless-forms' );
}
return __( 'Protects this supported Free source when available.', '2o2code-spamless-forms' );
}
/**
* Get latest event for a source from safe recent events.
*
* @param array> $events Events.
* @param string $source_key Source key.
*
* @return array|null
*/
private function get_latest_event_for_source( array $events, string $source_key ): ?array {
foreach ( $events as $event ) {
if ( isset( $event['source_key'] ) && $source_key === (string) $event['source_key'] ) {
return $event;
}
}
return null;
}
/**
* Get last event label.
*
* @param array|null $event Event.
*/
private function get_last_event_label( ?array $event ): string {
if ( null === $event ) {
return __( 'No activity recorded yet.', '2o2code-spamless-forms' );
}
return sprintf(
/* translators: %s: formatted event date. */
__( 'Last activity: %s', '2o2code-spamless-forms' ),
$this->format_event_date( isset( $event['created_at'] ) ? (int) $event['created_at'] : 0 )
);
}
/**
* Get next recommended action.
*
* @param array $status Status.
* @param array $summary Summary.
* @param array> $events Events.
* @param array $settings Settings.
*
* @return array{title:string,description:string,button:string,url:string}
*/
private function get_next_recommended_action( array $status, array $summary, array $events, array $settings ): array {
$status_key = isset( $status['key'] ) && is_scalar( $status['key'] ) ? (string) $status['key'] : 'protect';
if ( 'disabled' === $status_key ) {
return array(
'title' => __( 'Review Protection settings', '2o2code-spamless-forms' ),
'description' => __( 'Protection is disabled. Turn it back on when troubleshooting is complete.', '2o2code-spamless-forms' ),
'button' => __( 'Review Protection', '2o2code-spamless-forms' ),
'url' => $this->get_tab_url( self::TAB_PROTECTION ),
);
}
if ( empty( $settings['event_logging_enabled'] ) ) {
return array(
'title' => __( 'Review Protection settings', '2o2code-spamless-forms' ),
'description' => __( 'Event logging is off, so Activity will have limited detail.', '2o2code-spamless-forms' ),
'button' => __( 'Review Protection', '2o2code-spamless-forms' ),
'url' => $this->get_tab_url( self::TAB_PROTECTION ),
);
}
if ( isset( $summary['enabled'] ) && (int) $summary['enabled'] < 1 ) {
return array(
'title' => __( 'Check Integrations', '2o2code-spamless-forms' ),
'description' => __( 'Choose which supported Free sources should be protected.', '2o2code-spamless-forms' ),
'button' => __( 'Open Integrations', '2o2code-spamless-forms' ),
'url' => $this->get_tab_url( self::TAB_INTEGRATIONS ),
);
}
if ( array() === $events ) {
return array(
'title' => __( 'Test a form', '2o2code-spamless-forms' ),
'description' => __( 'Submit normal and spam-like test content, then confirm Activity stays privacy-safe.', '2o2code-spamless-forms' ),
'button' => __( 'View Integrations', '2o2code-spamless-forms' ),
'url' => $this->get_tab_url( self::TAB_INTEGRATIONS ),
);
}
return array(
'title' => __( 'Review recent Activity', '2o2code-spamless-forms' ),
'description' => __( 'Recent blocked or suspicious decisions are available without exposing raw submission data.', '2o2code-spamless-forms' ),
'button' => __( 'View Activity', '2o2code-spamless-forms' ),
'url' => $this->get_tab_url( self::TAB_ACTIVITY ),
);
}
/**
* Get default status data.
*
* @return array{key:string,label:string,headline:string,description:string}
*/
private function get_default_status(): array {
return array(
'key' => 'protect',
'label' => __( 'Protect', '2o2code-spamless-forms' ),
'headline' => __( 'Protection is active.', '2o2code-spamless-forms' ),
'description' => __( '2o2code Spamless Forms is protecting enabled integrations with local checks.', '2o2code-spamless-forms' ),
);
}
/**
* Get default integration summary.
*
* @return array{total:int,available:int,enabled:int}
*/
private function get_default_summary(): array {
return array(
'total' => 0,
'available' => 0,
'enabled' => 0,
);
}
/**
* Get default event summary.
*
* @return array{allowed:int,blocked:int,suspicious:int,total:int}
*/
private function get_default_event_summary(): array {
return array(
'allowed' => 0,
'blocked' => 0,
'suspicious' => 0,
'total' => 0,
);
}
/**
* Get mode label.
*
* @param string $mode Mode.
*/
private function get_mode_label( string $mode ): string {
if ( SettingsSnapshot::MODE_MONITOR === $mode ) {
return __( 'Monitor', '2o2code-spamless-forms' );
}
if ( SettingsSnapshot::MODE_DISABLED === $mode ) {
return __( 'Disabled', '2o2code-spamless-forms' );
}
return __( 'Protect', '2o2code-spamless-forms' );
}
/**
* Get strictness label.
*
* @param string $strictness Strictness.
*/
private function get_strictness_label( string $strictness ): string {
if ( SettingsSnapshot::STRICTNESS_SAFE === $strictness ) {
return __( 'Safe', '2o2code-spamless-forms' );
}
if ( SettingsSnapshot::STRICTNESS_STRICT === $strictness ) {
return __( 'Strict', '2o2code-spamless-forms' );
}
return __( 'Balanced', '2o2code-spamless-forms' );
}
/**
* Get free feature labels.
*
* @return array
*/
private function get_free_feature_labels(): array {
return array(
__( 'Local form checks with no external anti-spam API.', '2o2code-spamless-forms' ),
__( 'No CAPTCHA or puzzle challenges.', '2o2code-spamless-forms' ),
__( 'Protect, Monitor, and Disabled modes.', '2o2code-spamless-forms' ),
__( 'Safe, Balanced, and Strict sensitivity settings.', '2o2code-spamless-forms' ),
__( 'Privacy-first defaults with no full submission storage.', '2o2code-spamless-forms' ),
__( 'Simple Activity review using minimal metadata only.', '2o2code-spamless-forms' ),
);
}
/**
* Get supported Free integration labels.
*
* @return array
*/
private function get_supported_free_integration_labels(): array {
return array(
__( 'Contact Form 7', '2o2code-spamless-forms' ),
__( 'WPForms Lite', '2o2code-spamless-forms' ),
__( 'Elementor Form Lite', '2o2code-spamless-forms' ),
__( 'WordPress comments', '2o2code-spamless-forms' ),
__( 'WordPress login', '2o2code-spamless-forms' ),
__( 'WordPress registration', '2o2code-spamless-forms' ),
);
}
/**
* Whether the current package is the Free build.
*/
private function is_free_build(): bool {
return ( new BuildProfileResolver() )->resolve()->is_free();
}
}