array(...), 'logging' => array(...), 'privacy' => array(...) ). */ final class EVLFL_Settings { const OPTION = 'evlfl_settings'; /** * Allowed retention periods in days (0 = keep forever). * * @return int[] */ public static function retention_choices() { return array( 0, 30, 90, 180, 365, 730 ); } /** * Notification triggers. A event matches when ANY ticked trigger matches ("all" matches everything). * * @return array key => label. */ public static function notify_choices() { return array( 'all' => __( 'All events', 'accesstrace-for-learndash' ), 'invalid' => __( 'Invalid events (conflicts worth investigating)', 'accesstrace-for-learndash' ), 'unenrolled' => __( 'Unenrollments', 'accesstrace-for-learndash' ), 'enrolled' => __( 'Enrollments', 'accesstrace-for-learndash' ), 'unknown' => __( 'Unknown-status events', 'accesstrace-for-learndash' ), ); } /** * Default values. Subject and body default to '' meaning "use the built-in translatable text". * * @return array> */ public static function defaults() { return array( 'email' => array( 'enabled' => 0, 'recipients' => '', 'notify_on' => array( 'invalid' ), 'sources' => array(), 'delivery' => 'immediate', 'rate_limit' => 20, 'subject' => '', 'body' => '', 'include_user_email' => 0, 'format' => 'plain', 'from_name' => '', 'from_email' => '', ), 'logging' => array( 'enabled' => 1, 'record_enrolled' => 1, 'record_unenrolled' => 1, 'retention_days' => 0, 'ignore_courses' => '', ), 'privacy' => array( 'delete_on_uninstall' => 0, 'erase_on_user_delete' => 0, ), ); } /** * All settings merged over the defaults. * * @return array> */ public static function all() { $stored = get_option( self::OPTION, array() ); $stored = is_array( $stored ) ? $stored : array(); $out = self::defaults(); foreach ( $out as $group => $values ) { if ( isset( $stored[ $group ] ) && is_array( $stored[ $group ] ) ) { $out[ $group ] = array_merge( $values, array_intersect_key( $stored[ $group ], $values ) ); } } return $out; } /** * One value. * * @param string $group Group. * @param string $key Key. * @return mixed */ public static function get( $group, $key ) { $all = self::all(); return isset( $all[ $group ][ $key ] ) ? $all[ $group ][ $key ] : null; } /** * Whether an event should be written to the log. * * @param array $event Normalized event. * @return bool */ public static function should_log( array $event ) { $log = self::all()['logging']; if ( empty( $log['enabled'] ) ) { return false; } if ( EVLFL_Event::ACTION_ENROLLED === $event['action'] && empty( $log['record_enrolled'] ) ) { return false; } if ( EVLFL_Event::ACTION_UNENROLLED === $event['action'] && empty( $log['record_unenrolled'] ) ) { return false; } return ! in_array( (int) $event['course_id'], self::ids_from_csv( (string) $log['ignore_courses'] ), true ); } /** * Parse "1, 2,3" into unique positive ints. * * @param string $csv Comma-separated list. * @return int[] */ public static function ids_from_csv( $csv ) { $ids = array(); foreach ( explode( ',', (string) $csv ) as $part ) { $n = absint( trim( $part ) ); if ( $n > 0 ) { $ids[ $n ] = $n; } } return array_values( $ids ); } /** * Settings API sanitize callback. Only the groups present in $input are replaced, so saving * one tab never resets the others. * * @param mixed $input Submitted value (already unslashed by options.php). * @return array */ public static function sanitize( $input ) { $stored = get_option( self::OPTION, array() ); $out = is_array( $stored ) ? $stored : array(); if ( ! is_array( $input ) ) { return $out; } if ( isset( $input['email'] ) && is_array( $input['email'] ) ) { $out['email'] = self::sanitize_email_group( $input['email'] ); } if ( isset( $input['logging'] ) && is_array( $input['logging'] ) ) { $out['logging'] = self::sanitize_logging_group( $input['logging'] ); } if ( isset( $input['privacy'] ) && is_array( $input['privacy'] ) ) { $out['privacy'] = self::sanitize_privacy_group( $input['privacy'] ); } return $out; } /** * Email tab. * * @param array $in Input. * @return array */ private static function sanitize_email_group( array $in ) { $defaults = self::defaults()['email']; $valid = array(); $invalid = array(); foreach ( explode( ',', isset( $in['recipients'] ) && is_scalar( $in['recipients'] ) ? (string) $in['recipients'] : '' ) as $candidate ) { $candidate = trim( $candidate ); if ( '' === $candidate ) { continue; } $address = sanitize_email( $candidate ); if ( is_email( $address ) ) { if ( ! isset( $valid[ strtolower( $address ) ] ) ) { $valid[ strtolower( $address ) ] = $address; } } else { $invalid[] = $candidate; } } if ( $invalid ) { add_settings_error( self::OPTION, 'evlfl_bad_recipients', sprintf( /* translators: %s: list of rejected email addresses. */ __( 'These recipients are not valid email addresses and were removed: %s', 'accesstrace-for-learndash' ), esc_html( implode( ', ', array_map( 'sanitize_text_field', $invalid ) ) ) ), 'warning' ); } $notify_on = self::pick_many( isset( $in['notify_on'] ) ? $in['notify_on'] : array(), self::notify_choices() ); $enabled = ! empty( $in['enabled'] ) ? 1 : 0; if ( $enabled && ! $notify_on ) { add_settings_error( self::OPTION, 'evlfl_no_triggers', __( 'Admin emails are on, but no event type is ticked, so no email will be sent.', 'accesstrace-for-learndash' ), 'warning' ); } $delivery = isset( $in['delivery'] ) && is_scalar( $in['delivery'] ) ? sanitize_key( (string) $in['delivery'] ) : ''; $format = isset( $in['format'] ) && is_scalar( $in['format'] ) ? sanitize_key( (string) $in['format'] ) : ''; $limit = isset( $in['rate_limit'] ) && is_scalar( $in['rate_limit'] ) ? absint( $in['rate_limit'] ) : $defaults['rate_limit']; $from_name = isset( $in['from_name'] ) && is_scalar( $in['from_name'] ) ? sanitize_text_field( (string) $in['from_name'] ) : ''; $from_name = trim( str_replace( array( '<', '>', '"' ), '', $from_name ) ); $from_mail = isset( $in['from_email'] ) && is_scalar( $in['from_email'] ) ? sanitize_email( (string) $in['from_email'] ) : ''; return array( 'enabled' => $enabled, 'recipients' => implode( ', ', array_slice( array_values( $valid ), 0, 10 ) ), 'notify_on' => $notify_on, 'sources' => self::pick_many( isset( $in['sources'] ) ? $in['sources'] : array(), EVLFL_Event::get_sources() ), 'delivery' => in_array( $delivery, array( 'immediate', 'hourly', 'daily' ), true ) ? $delivery : $defaults['delivery'], 'rate_limit' => min( 500, max( 1, $limit ) ), 'subject' => isset( $in['subject'] ) && is_scalar( $in['subject'] ) ? substr( sanitize_text_field( (string) $in['subject'] ), 0, 200 ) : '', 'body' => isset( $in['body'] ) && is_scalar( $in['body'] ) ? substr( sanitize_textarea_field( (string) $in['body'] ), 0, 5000 ) : '', 'include_user_email' => ! empty( $in['include_user_email'] ) ? 1 : 0, 'format' => in_array( $format, array( 'plain', 'html' ), true ) ? $format : $defaults['format'], 'from_name' => substr( $from_name, 0, 100 ), 'from_email' => is_email( $from_mail ) ? $from_mail : '', ); } /** * Logging tab. * * @param array $in Input. * @return array */ private static function sanitize_logging_group( array $in ) { $days = isset( $in['retention_days'] ) && is_scalar( $in['retention_days'] ) ? absint( $in['retention_days'] ) : 0; $ids = self::ids_from_csv( isset( $in['ignore_courses'] ) && is_scalar( $in['ignore_courses'] ) ? (string) $in['ignore_courses'] : '' ); return array( 'enabled' => ! empty( $in['enabled'] ) ? 1 : 0, 'record_enrolled' => ! empty( $in['record_enrolled'] ) ? 1 : 0, 'record_unenrolled' => ! empty( $in['record_unenrolled'] ) ? 1 : 0, 'retention_days' => in_array( $days, self::retention_choices(), true ) ? $days : 0, 'ignore_courses' => implode( ', ', array_slice( $ids, 0, 200 ) ), ); } /** * Privacy tab. * * @param array $in Input. * @return array */ private static function sanitize_privacy_group( array $in ) { return array( 'delete_on_uninstall' => ! empty( $in['delete_on_uninstall'] ) ? 1 : 0, 'erase_on_user_delete' => ! empty( $in['erase_on_user_delete'] ) ? 1 : 0, ); } /** * Keep only allowlisted keys. * * @param mixed $values Submitted list. * @param array $allowed Allowlist (key => label). * @return string[] */ private static function pick_many( $values, array $allowed ) { $out = array(); foreach ( (array) $values as $value ) { $value = is_scalar( $value ) ? sanitize_key( (string) $value ) : ''; if ( isset( $allowed[ $value ] ) ) { $out[ $value ] = $value; } } return array_values( $out ); } }