notifier = $notifier; } /** * Hooks. * * @return void */ public function init() { add_action( 'admin_init', array( $this, 'register' ) ); add_action( 'admin_post_evlfl_send_test_email', array( $this, 'handle_test_email' ) ); add_filter( 'option_page_capability_' . self::GROUP, array( 'EVLFL_Plugin', 'capability' ) ); } /** * Tabs: slug => label. * * @return array */ public static function tabs() { return array( 'email' => __( 'Email Notifications', 'accesstrace-for-learndash' ), 'logging' => __( 'Logging', 'accesstrace-for-learndash' ), 'privacy' => __( 'Privacy & Data', 'accesstrace-for-learndash' ), ); } /** * URL of a tab. * * @param string $tab Tab slug. * @return string */ public static function tab_url( $tab = 'email' ) { return add_query_arg( array( 'page' => EVLFL_Plugin::SETTINGS_PAGE, 'tab' => $tab, ), admin_url( 'admin.php' ) ); } /** * Register the setting, sections and fields. * * @return void */ public function register() { register_setting( self::GROUP, EVLFL_Settings::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( 'EVLFL_Settings', 'sanitize' ), 'default' => array(), 'show_in_rest' => false, ) ); } /** * Field definitions: tab => section id => title + fields. * * @return array */ private function fields() { $sources = EVLFL_Event::get_sources(); return array( 'email' => array( 'evlfl_email_alerts' => array( 'title' => __( 'Admin email alerts', 'accesstrace-for-learndash' ), 'description' => __( 'Choose who is emailed, and about which enrollment events.', 'accesstrace-for-learndash' ), 'fields' => array( 'enabled' => array( 'label' => __( 'Enable admin emails', 'accesstrace-for-learndash' ), 'type' => 'checkbox', 'text' => __( 'Send an email when a matching event is logged', 'accesstrace-for-learndash' ), 'desc' => __( 'When off, no email is ever sent.', 'accesstrace-for-learndash' ), ), 'recipients' => array( 'label' => __( 'Recipients', 'accesstrace-for-learndash' ), 'type' => 'text', 'placeholder' => (string) get_option( 'admin_email' ), 'desc' => __( 'Comma-separated email addresses (up to 10). Leave empty to use the site admin email.', 'accesstrace-for-learndash' ), ), 'notify_on' => array( 'label' => __( 'Send alerts for', 'accesstrace-for-learndash' ), 'type' => 'checkboxes', 'options' => EVLFL_Settings::notify_choices(), 'desc' => __( 'An event is emailed when it matches any ticked type. Recommended: Invalid events only, so inboxes are not flooded.', 'accesstrace-for-learndash' ), ), 'sources' => array( 'label' => __( 'Limit by source', 'accesstrace-for-learndash' ), 'type' => 'checkboxes', 'options' => $sources, 'desc' => __( 'Only email for changes from the ticked sources. Leave all unticked for any source.', 'accesstrace-for-learndash' ), ), 'delivery' => array( 'label' => __( 'Delivery', 'accesstrace-for-learndash' ), 'type' => 'select', 'options' => array( 'immediate' => __( 'Immediately (one email per event)', 'accesstrace-for-learndash' ), 'hourly' => __( 'Hourly summary', 'accesstrace-for-learndash' ), 'daily' => __( 'Daily summary', 'accesstrace-for-learndash' ), ), 'desc' => __( 'Summaries are sent with WP-Cron and group events into one email.', 'accesstrace-for-learndash' ), ), 'rate_limit' => array( 'label' => __( 'Rate limit', 'accesstrace-for-learndash' ), 'type' => 'number', 'min' => 1, 'max' => 500, 'after' => __( 'emails per hour (immediate delivery)', 'accesstrace-for-learndash' ), 'desc' => __( 'Protects against bulk imports. Events over the limit are sent together in one summary email.', 'accesstrace-for-learndash' ), ), ), ), 'evlfl_email_content' => array( 'title' => __( 'Email content', 'accesstrace-for-learndash' ), 'description' => __( 'Customize the message. Placeholders are replaced with the details of each event.', 'accesstrace-for-learndash' ), 'fields' => array( 'subject' => array( 'label' => __( 'Subject', 'accesstrace-for-learndash' ), 'type' => 'text', 'default' => EVLFL_Notifier::default_subject(), 'desc' => __( 'Clear the field and save to restore the default.', 'accesstrace-for-learndash' ), ), 'body' => array( 'label' => __( 'Email text', 'accesstrace-for-learndash' ), 'type' => 'textarea', 'default' => EVLFL_Notifier::default_body(), 'placeholders' => true, 'desc' => __( 'Clear the box and save to restore the default text.', 'accesstrace-for-learndash' ), ), 'include_user_email' => array( 'label' => __( 'Student email', 'accesstrace-for-learndash' ), 'type' => 'checkbox', 'text' => __( 'Show the student email address in the message', 'accesstrace-for-learndash' ), 'desc' => __( 'Off by default for privacy. Used by {user_email}.', 'accesstrace-for-learndash' ), ), 'format' => array( 'label' => __( 'Format', 'accesstrace-for-learndash' ), 'type' => 'select', 'options' => array( 'plain' => __( 'Plain text', 'accesstrace-for-learndash' ), 'html' => __( 'HTML', 'accesstrace-for-learndash' ), ), ), 'from_name' => array( 'label' => __( 'Sender name', 'accesstrace-for-learndash' ), 'type' => 'text', 'placeholder' => (string) get_bloginfo( 'name' ), 'desc' => __( 'Optional. Only used together with a sender email.', 'accesstrace-for-learndash' ), ), 'from_email' => array( 'label' => __( 'Sender email', 'accesstrace-for-learndash' ), 'type' => 'email', 'placeholder' => 'wordpress@' . wp_parse_url( home_url(), PHP_URL_HOST ), 'desc' => __( 'Optional. Leave empty to use your site default.', 'accesstrace-for-learndash' ), ), ), ), ), 'logging' => array( 'evlfl_logging' => array( 'title' => __( 'Logging', 'accesstrace-for-learndash' ), 'description' => __( 'Choose what is recorded and how long it is kept.', 'accesstrace-for-learndash' ), 'fields' => array( 'enabled' => array( 'label' => __( 'Enable logging', 'accesstrace-for-learndash' ), 'type' => 'checkbox', 'text' => __( 'Record enrollment and unenrollment events', 'accesstrace-for-learndash' ), 'desc' => __( 'When off, nothing new is recorded and no emails are sent. Existing logs are kept.', 'accesstrace-for-learndash' ), ), 'record_enrolled' => array( 'label' => __( 'Events to record', 'accesstrace-for-learndash' ), 'type' => 'checkbox', 'text' => __( 'Enrollments', 'accesstrace-for-learndash' ), ), 'record_unenrolled' => array( 'label' => '', 'type' => 'checkbox', 'text' => __( 'Unenrollments', 'accesstrace-for-learndash' ), ), 'retention_days' => array( 'label' => __( 'Auto-delete old logs', 'accesstrace-for-learndash' ), 'type' => 'select', 'options' => array( 0 => __( 'Keep forever', 'accesstrace-for-learndash' ), 30 => __( 'After 30 days', 'accesstrace-for-learndash' ), 90 => __( 'After 90 days', 'accesstrace-for-learndash' ), 180 => __( 'After 180 days', 'accesstrace-for-learndash' ), 365 => __( 'After 1 year', 'accesstrace-for-learndash' ), 730 => __( 'After 2 years', 'accesstrace-for-learndash' ), ), 'desc' => __( 'Runs daily in batches through WP-Cron. Deleted events cannot be recovered.', 'accesstrace-for-learndash' ), ), 'ignore_courses' => array( 'label' => __( 'Ignore courses', 'accesstrace-for-learndash' ), 'type' => 'text', 'placeholder' => '12, 34', 'desc' => __( 'Course IDs (comma-separated) that are not logged and never emailed.', 'accesstrace-for-learndash' ), ), ), ), ), 'privacy' => array( 'evlfl_privacy' => array( 'title' => __( 'Privacy & data', 'accesstrace-for-learndash' ), 'description' => __( 'Control what happens to log data when users or the plugin are removed.', 'accesstrace-for-learndash' ), 'fields' => array( 'delete_on_uninstall' => array( 'label' => __( 'Delete data on uninstall', 'accesstrace-for-learndash' ), 'type' => 'checkbox', 'text' => __( 'Remove the log table and all settings when the plugin is deleted', 'accesstrace-for-learndash' ), 'desc' => __( 'Off by default: logs are an audit trail and are kept when the plugin is deleted.', 'accesstrace-for-learndash' ), ), 'erase_on_user_delete' => array( 'label' => __( 'Deleted users', 'accesstrace-for-learndash' ), 'type' => 'checkbox', 'text' => __( 'Erase a user\'s events when their account is deleted', 'accesstrace-for-learndash' ), 'desc' => __( 'Also clears that user as the actor on other events. Export and erase requests from Tools > Export/Erase Personal Data always work.', 'accesstrace-for-learndash' ), ), ), ), ), ); } /** * Render one field's control (no label; the row provides it). * * @param array $f Field definition incl. group and key. * @return void */ private function render_control( array $f ) { $value = EVLFL_Settings::get( $f['group'], $f['key'] ); $id = 'evlfl_' . $f['group'] . '_' . $f['key']; $name = EVLFL_Settings::OPTION . '[' . $f['group'] . '][' . $f['key'] . ']'; switch ( $f['type'] ) { case 'checkbox': printf( '', esc_attr( $id ), esc_attr( $name ), checked( ! empty( $value ), true, false ), esc_html( $f['text'] ) ); break; case 'checkboxes': echo '
' . esc_html( $f['label'] ) . ''; foreach ( $f['options'] as $option => $label ) { printf( '', esc_attr( $name ), esc_attr( $option ), checked( in_array( (string) $option, array_map( 'strval', (array) $value ), true ), true, false ), esc_html( $label ) ); } echo '
'; break; case 'select': printf( ''; break; case 'number': printf( '', esc_attr( $id ), esc_attr( $name ), esc_attr( (string) $value ), (int) $f['min'], (int) $f['max'] ); if ( ! empty( $f['after'] ) ) { echo '' . esc_html( $f['after'] ) . ''; } break; case 'textarea': $shown = '' !== trim( (string) $value ) ? (string) $value : (string) $f['default']; printf( '', esc_attr( $id ), esc_attr( $name ), esc_textarea( $shown ) ); break; default: // text, email. $shown = '' !== trim( (string) $value ) ? (string) $value : ( isset( $f['default'] ) ? (string) $f['default'] : '' ); printf( '', 'email' === $f['type'] ? 'email' : 'text', esc_attr( $id ), esc_attr( $name ), esc_attr( $shown ), esc_attr( isset( $f['placeholder'] ) ? $f['placeholder'] : '' ) ); } if ( ! empty( $f['placeholders'] ) ) { echo '

' . esc_html__( 'Available placeholders:', 'accesstrace-for-learndash' ) . '

'; foreach ( EVLFL_Notifier::placeholders() as $token => $label ) { printf( '{%2$s}', esc_attr( $label ), esc_html( $token ) ); } echo '

'; } if ( ! empty( $f['desc'] ) ) { echo '

' . esc_html( $f['desc'] ) . '

'; } } /** * Read-only status tiles at the top (saved settings). * * @return void */ private function render_summary() { $all = EVLFL_Settings::all(); $days = (int) $all['logging']['retention_days']; $on = __( 'On', 'accesstrace-for-learndash' ); $off = __( 'Off', 'accesstrace-for-learndash' ); $tiles = array( array( __( 'Admin emails', 'accesstrace-for-learndash' ), ! empty( $all['email']['enabled'] ) ? $on : $off, ! empty( $all['email']['enabled'] ) ? 'valid' : 'unknown' ), array( __( 'Email recipients', 'accesstrace-for-learndash' ), number_format_i18n( count( EVLFL_Notifier::recipients( $all['email'] ) ) ), '' ), array( __( 'Logging', 'accesstrace-for-learndash' ), ! empty( $all['logging']['enabled'] ) ? $on : $off, ! empty( $all['logging']['enabled'] ) ? 'valid' : 'invalid' ), /* translators: %s: number of days. */ array( __( 'Auto-delete', 'accesstrace-for-learndash' ), $days > 0 ? sprintf( _n( '%s day', '%s days', $days, 'accesstrace-for-learndash' ), number_format_i18n( $days ) ) : __( 'Never', 'accesstrace-for-learndash' ), '' ), ); echo '
'; foreach ( $tiles as $tile ) { printf( '
%1$s%3$s
', esc_html( $tile[0] ), '' !== $tile[2] ? ' evl-stat__value--' . esc_attr( $tile[2] ) : '', esc_html( $tile[1] ) ); } echo '
'; } /** * Page output. * * @return void */ public function render() { if ( ! current_user_can( EVLFL_Plugin::capability() ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'accesstrace-for-learndash' ), '', array( 'response' => 403 ) ); } $tabs = self::tabs(); $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'email'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab choice, allowlisted below. $tab = isset( $tabs[ $tab ] ) ? $tab : 'email'; $view = add_query_arg( 'page', EVLFL_Plugin::LOGS_PAGE, admin_url( 'admin.php' ) ); ?>


render_notice(); $this->render_summary(); ?>
', esc_attr( EVLFL_Settings::OPTION . '[' . $tab . '][_tab]' ) ); $fields = $this->fields(); foreach ( $fields[ $tab ] as $section ) : ?>

$field ) : $field['group'] = $tab; $field['key'] = $key; $id = 'evlfl_' . $tab . '_' . $key; $labelled = '' !== $field['label']; ?>
render_control( $field ); ?>

' . esc_html__( 'Test email handed to WordPress for delivery. If it does not arrive, check your spam folder or your site\'s mail setup.', 'accesstrace-for-learndash' ) . '

'; } elseif ( 'test_failed' === $key ) { echo '

' . esc_html__( 'The test email could not be sent. Check the recipients and your site\'s mail setup.', 'accesstrace-for-learndash' ) . '

'; } } /** * Send the test email. * * @return void */ public function handle_test_email() { if ( ! current_user_can( EVLFL_Plugin::capability() ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'accesstrace-for-learndash' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'evlfl_send_test_email' ); $ok = $this->notifier->send_test(); wp_safe_redirect( add_query_arg( array( 'page' => EVLFL_Plugin::SETTINGS_PAGE, 'tab' => 'email', 'evlfl_notice' => $ok ? 'test_sent' : 'test_failed', ), admin_url( 'admin.php' ) ) ); exit; } }