'courses', 'category_base' => 'course-category', 'lesson_category_base' => 'lesson-category', 'show_learner_counts' => false, 'private_catalogue' => false, 'org_catalogue_listing' => false, 'learner_own_courses' => false, 'portal_page_id' => 0, 'keep_managers_out_admin' => true, 'portal_tabs_hidden' => array(), 'learner_menu_hidden' => array(), 'learner_role' => true, 'event_retention_days' => 0, 'delete_data_on_uninstall' => false, 'notify_welcome' => false, 'notify_programme' => false, 'notify_job_role' => false, 'notify_bulk' => false, 'notify_delay_minutes' => 15, 'brand_member_mail' => false, 'manageable_roles_extra' => array(), 'lms_portal_alias' => false, 'legacy_routes' => array(), 'certificate_base' => 'certificate', 'certificate_verify_fields' => array( 'learner_name', 'course_title', 'completed_on' ), 'certificate_verify_page_id' => 0, 'certificate_default_tpl' => 0, 'attendance_default_tpl' => 0, 'ui_primary' => 'rooster', 'ui_dark' => 'off', 'ui_buttons' => 'rooster', 'ui_shell_max' => 0, 'ui_shell_pad' => 15, 'ui_look' => 'rooster-one', 'staff_course_access' => true, 'manager_course_access' => true, 'notify_results' => false, 'question_grace_ms' => 3000, 'feedback_min_group' => 5, 'feedback_comments' => true, 'policies_in_use' => false, 'feedback_in_use' => false, 'account_page_id' => 0, 'notify_announcements' => false, 'notify_live_reminders' => true, 'notify_live_changes' => true, 'notify_live_recordings' => true, 'search_all_languages' => false, 'live_join_before' => 10, 'live_timezone' => '', 'live_hide_ended_links' => true, 'scope_in_use' => false, 'sales_end_pending' => false, 'offers_soft_optin' => false, 'offers_weekly_cap' => 2, 'plan_grace_days' => 14, 'registration_page_id' => 0, 'registration_takeover' => false, 'register_opens' => 'popup', 'quiet_notices' => 'off', 'msg_track' => false, 'msg_quiet_hours' => false, 'msg_quiet_from' => 22, 'msg_quiet_to' => 8, 'learner_inactive_days' => 0, 'apparent_root' => '', 'apparent_root_logo' => 0, 'apparent_root_color' => '', 'person_base' => 'persons', 'person_name_order' => 'family', 'gravatar' => false, 'person_links_new_tab' => false, 'person_redirects' => true, 'inv_rsvp' => 3, 'inv_close_hours' => 24, 'inv_yes_action' => 'none', 'inv_from_name' => '', 'points_enabled' => false, 'points_label' => '', 'points_rate' => 0, 'points_leaderboard_names' => false, 'session_limit' => 0, 'catalogue_per_page' => 0, 'catalogue_filters' => array(), 'catalogue_filter_order' => array(), 'catalogue_featured' => 0, 'catalogue_related' => true, 'catalogue_org_byline' => false, 'course_premoderation' => false, 'guest_preview' => true, 'page_templates_in_use' => false, 'demo_page' => 0, 'media_required_in_use' => false, 'lesson_buttons_orgs' => false, 'assignment_file_types' => array( 'pdf', 'doc', 'docx', 'odt', 'rtf', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'zip' ), 'assignment_max_file_mb' => 10, 'assignment_max_files' => 5, 'assignment_audio' => false, 'assignment_video' => false, 'assignment_media_max_mb' => 50, 'grade_scales_in_use' => false, 'affiliate_link_template' => '', 'question_draws_in_use' => false, 'skills_in_use' => false, 'h5p_hub_enabled' => false, 'h5p_usage_stats' => false, 'h5p_look' => 'rooster', 'h5p_corners' => 'preset', 'h5p_density' => 'preset', 'h5p_buttons' => 'preset', 'course_file_types' => array( 'pdf', 'doc', 'docx', 'odt', 'rtf', 'txt', 'csv', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp', 'zip', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'm4v', 'webm', 'mov', 'mp3', 'm4a', 'wav', 'ogg', 'vtt' ), 'library_open_types' => array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'm4v', 'webm', 'mov', 'mp3', 'm4a', 'wav', 'ogg' ), 'online_tests_course' => 0, 'lesson_media_in_use' => false, 'course_jsonld_faq' => false, 'course_jsonld_rating' => false, 'translation_create' => 'copy', 'default_editor' => 'designer', 'designer_fold_menu' => true, 'admin_ui_language' => 'switcher', 'reading_wpm' => 200, ); public const CHOICES = array( 'ui_primary' => array( 'rooster', 'theme' ), 'ui_dark' => array( 'off', 'auto', 'on' ), 'ui_buttons' => array( 'rooster', 'theme' ), 'quiet_notices' => array( 'off', 'ours', 'all' ), 'register_opens' => array( 'popup', 'page' ), 'person_name_order' => array( 'family', 'written' ), 'inv_yes_action' => array( 'none', 'register', 'enrol' ), 'translation_create' => array( 'copy', 'plugin' ), 'default_editor' => array( 'designer', 'block' ), 'admin_ui_language' => array( 'profile', 'switcher' ), 'h5p_look' => array( 'rooster', 'theme', 'compact' ), 'h5p_corners' => array( 'preset', 'square', 'soft', 'round' ), 'h5p_density' => array( 'preset', 'comfortable', 'compact' ), 'h5p_buttons' => array( 'preset', 'filled', 'outline' ), ); public const CATALOGUE_FILTERS = array( 'category', 'subcategory', 'level', 'format', 'date', 'location', 'price', 'rating', 'instructor', 'status', 'availability' ); public const FILTER_ORDER = array( 'category', 'level', 'format', 'date', 'location', 'price', 'rating', 'instructor', 'status', 'availability' ); public const LEGACY_ROUTE_KEYS = array( 'course', 'player', 'category', 'lesson', 'account' ); private const RANGES = array( 'notify_delay_minutes' => array( 1, 1440 ), 'ui_shell_max' => array( 0, 3840 ), 'ui_shell_pad' => array( 0, 200 ), 'question_grace_ms' => array( 0, 10000 ), 'feedback_min_group' => array( 1, 50 ), 'live_join_before' => array( 0, 1440 ), 'offers_weekly_cap' => array( 0, 14 ), 'plan_grace_days' => array( 0, 90 ), 'msg_quiet_from' => array( 0, 23 ), 'msg_quiet_to' => array( 0, 23 ), 'learner_inactive_days' => array( 0, 365 ), 'inv_rsvp' => array( 0, 7 ), 'inv_close_hours' => array( 0, 720 ), 'points_rate' => array( 0, 100000 ), 'session_limit' => array( 0, 20 ), 'catalogue_per_page' => array( 0, 100 ), 'catalogue_featured' => array( 0, 50 ), 'assignment_max_file_mb' => array( 1, 512 ), 'assignment_max_files' => array( 1, 50 ), 'assignment_media_max_mb' => array( 1, 2048 ), 'reading_wpm' => array( 50, 1000 ), ); public const FROM_NAME_MAX = 100; public static function get( string $key ) { if ( ! array_key_exists( $key, self::DEFAULTS ) ) { return null; } $all = self::all(); return $all[ $key ]; } public static function base( string $key ): string { return in_array( $key, self::BASES, true ) ? urldecode( (string) self::get( $key ) ) : ''; } public static function all(): array { $stored = get_option( self::OPTION, array() ); $stored = is_array( $stored ) ? $stored : array(); if ( $stored !== self::$raw ) { self::$raw = $stored; self::$clean = self::sanitize( $stored ); } return self::$clean; } public static function update( array $values ): void { $old = self::all(); $new = self::sanitize( array_merge( $old, $values ) ); update_option( self::OPTION, $new, true ); foreach ( self::BASES as $key ) { if ( $old[ $key ] !== $new[ $key ] ) { update_option( Migrator::FLUSH_OPTION, 1, true ); break; } } } public static function ensure_defaults(): void { add_option( self::OPTION, self::DEFAULTS, '', true ); add_option( \RoosterLMS\Community\CommunitySettings::OPTION, array(), '', true ); } public static function sanitize( array $values ): array { $clean = array(); foreach ( self::DEFAULTS as $key => $default ) { $value = $values[ $key ] ?? $default; if ( is_bool( $default ) ) { $bool = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); $clean[ $key ] = null === $bool ? $default : $bool; } elseif ( is_int( $default ) ) { $clean[ $key ] = is_numeric( $value ) ? max( 0, (int) $value ) : $default; if ( isset( self::RANGES[ $key ] ) ) { $clean[ $key ] = max( self::RANGES[ $key ][0], min( self::RANGES[ $key ][1], $clean[ $key ] ) ); } } elseif ( is_array( $default ) ) { $clean[ $key ] = self::sanitize_array( $key, $value ); } elseif ( 'live_timezone' === $key ) { $zone = is_string( $value ) ? trim( $value ) : ''; $clean[ $key ] = '' !== $zone && \RoosterLMS\Live\Schedule::is_zone( $zone ) ? $zone : ''; } elseif ( 'apparent_root' === $key ) { $clean[ $key ] = \RoosterLMS\Orgs\ApparentRoot::format( \RoosterLMS\Orgs\ApparentRoot::parse( $value ) ); } elseif ( 'apparent_root_color' === $key ) { $clean[ $key ] = is_string( $value ) ? (string) sanitize_hex_color( strtolower( trim( $value ) ) ) : ''; } elseif ( 'points_label' === $key ) { $clean[ $key ] = is_string( $value ) ? mb_substr( sanitize_text_field( $value ), 0, 60 ) : ''; } elseif ( 'affiliate_link_template' === $key ) { $clean[ $key ] = self::link_template( $value ); } elseif ( 'inv_from_name' === $key ) { $clean[ $key ] = is_string( $value ) ? mb_substr( sanitize_text_field( $value ), 0, self::FROM_NAME_MAX ) : ''; } elseif ( 'ui_look' === $key ) { $value = is_string( $value ) ? strtolower( trim( $value ) ) : ''; $clean[ $key ] = 1 === preg_match( \RoosterLMS\Orgs\OrgSettings::LOOK_KEY, $value ) ? $value : $default; } elseif ( isset( self::CHOICES[ $key ] ) ) { $value = is_string( $value ) ? strtolower( trim( $value ) ) : ''; $hex = 'ui_primary' === $key ? (string) sanitize_hex_color( $value ) : ''; $clean[ $key ] = in_array( $value, self::CHOICES[ $key ], true ) ? $value : ( '' !== $hex ? $hex : $default ); } else { $slug = is_string( $value ) ? sanitize_title( $value ) : ''; $clean[ $key ] = '' !== $slug ? $slug : $default; } } return $clean; } public static function link_template( $value ): string { $value = is_string( $value ) ? trim( $value ) : ''; if ( '' === $value || strlen( $value ) > 500 || 1 !== preg_match( '#^https?://[^\s/?\#{}]+[^\s<>"\']*$#i', $value ) ) { return ''; } return $value; } private static function sanitize_array( string $key, $value ): array { if ( 'portal_tabs_hidden' === $key ) { return \RoosterLMS\Blocks\PortalTabs::clean_list( $value ); } if ( 'learner_menu_hidden' === $key ) { return \RoosterLMS\Account\Sections::clean_hidden( $value ); } if ( in_array( $key, array( 'assignment_file_types', 'course_file_types', 'library_open_types' ), true ) ) { if ( is_string( $value ) ) { $value = preg_split( '/[\s,;|]+/', $value, -1, PREG_SPLIT_NO_EMPTY ); } if ( ! is_array( $value ) ) { return self::DEFAULTS[ $key ]; } $out = array(); foreach ( $value as $ext ) { $ext = is_string( $ext ) ? (string) preg_replace( '/[^a-z0-9]/', '', strtolower( ltrim( trim( $ext ), '.' ) ) ) : ''; if ( '' !== $ext && strlen( $ext ) <= 10 ) { $out[ $ext ] = $ext; } } return array_slice( array_values( $out ), 0, 100 ); } if ( 'catalogue_filters' === $key ) { return is_array( $value ) ? array_values( array_unique( array_intersect( array_map( 'strval', array_filter( $value, 'is_string' ) ), self::CATALOGUE_FILTERS ) ) ) : array(); } if ( 'catalogue_filter_order' === $key ) { $given = is_array( $value ) ? array_values( array_unique( array_intersect( array_map( 'strval', array_filter( $value, 'is_string' ) ), self::FILTER_ORDER ) ) ) : array(); return array() === $given ? array() : array_values( array_merge( $given, array_diff( self::FILTER_ORDER, $given ) ) ); } if ( 'certificate_verify_fields' === $key ) { if ( ! is_array( $value ) ) { return self::DEFAULTS['certificate_verify_fields']; } $wanted = array_map( 'strval', array_filter( $value, 'is_string' ) ); return array_values( array_intersect( self::VERIFY_FIELDS, $wanted ) ); } if ( ! is_array( $value ) ) { return array(); } if ( 'legacy_routes' === $key ) { $out = array(); foreach ( self::LEGACY_ROUTE_KEYS as $route ) { $slug = isset( $value[ $route ] ) && is_string( $value[ $route ] ) ? sanitize_title( $value[ $route ] ) : ''; if ( '' !== $slug ) { $out[ $route ] = $slug; } } return $out; } $out = array(); foreach ( $value as $item ) { $role = is_string( $item ) ? sanitize_key( $item ) : ''; if ( '' !== $role && ! in_array( $role, $out, true ) ) { $out[] = $role; } } return $out; } }