HEX
Server: LiteSpeed
System: Linux s787.bom1.mysecurecloudhost.com 4.18.0-477.13.1.lve.el8.x86_64 #1 SMP Thu Jun 1 16:40:47 EDT 2023 x86_64
User: mobilech (5348)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/mobilech/alliancelaptoptraining.com.np/wp-content/plugins/tutor/classes/UserPreference.php
<?php
/**
 * User preference manager
 *
 * Handles storing and retrieving per–user preferences (e.g. theme, playback, font scale)
 * in a reusable and extensible way using WordPress user meta.
 *
 * @package Tutor\UserPreference
 * @author Themeum <support@themeum.com>
 * @link https://www.themeum.com/
 * @since 4.0.0
 */

namespace TUTOR;

defined( 'ABSPATH' ) || exit;

use Tutor\Cache\TutorCache;
use Tutor\Helpers\HttpHelper;
use Tutor\Options_V2;
use Tutor\Traits\JsonResponse;

/**
 * Class UserPreference
 *
 * @since 4.0.0
 */
class UserPreference {

	use JsonResponse;

	/**
	 * User meta key for storing all Tutor user preferences.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const META_KEY = 'tutor_user_preferences';

	/**
	 * Theme option: light.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const THEME_LIGHT = 'light';

	/**
	 * Theme option: dark.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const THEME_DARK = 'dark';

	/**
	 * Theme option: system.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const THEME_SYSTEM = 'system';

	/**
	 * Vision option: normal.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const VISION_NORMAL = 'normal';

	/**
	 * Vision option: protanopia.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const VISION_PROTANOPIA = 'protanopia';

	/**
	 * Vision option: deuteranopia.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const VISION_DEUTERANOPIA = 'deuteranopia';

	/**
	 * Vision option: deuteranomaly.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const VISION_DEUTERANOMALY = 'deuteranomaly';

	/**
	 * Available vision options.
	 *
	 * @since 4.0.0
	 *
	 * @var array<string>
	 */
	const VISIONS = array(
		self::VISION_NORMAL,
		self::VISION_PROTANOPIA,
		self::VISION_DEUTERANOPIA,
		self::VISION_DEUTERANOMALY,
	);

	/**
	 * Contrast option: high.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const CONTRAST_HIGH = 'high';

	/**
	 * Motion effects option: auto (follow system).
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const MOTION_EFFECTS_AUTO = 'auto';

	/**
	 * Motion effects option: reduce.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const MOTION_EFFECTS_REDUCE = 'reduce';

	/**
	 * Motion effects option: standard (no reduction).
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const MOTION_EFFECTS_STANDARD = 'standard';

	/**
	 * Available motion effects options.
	 *
	 * @since 4.0.0
	 *
	 * @var array<string>
	 */
	const MOTION_EFFECTS_OPTIONS = array(
		self::MOTION_EFFECTS_AUTO,
		self::MOTION_EFFECTS_REDUCE,
		self::MOTION_EFFECTS_STANDARD,
	);

	/**
	 * Default theme value.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	const DEFAULT_THEME = self::THEME_SYSTEM;

	/**
	 * Available theme options.
	 *
	 * @since 4.0.0
	 *
	 * @var array<string>
	 */
	const THEMES = array( self::THEME_LIGHT, self::THEME_DARK, self::THEME_SYSTEM );

	/**
	 * Default font scale value.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	const DEFAULT_FONT_SCALE = 100;

	/**
	 * Available font scale options.
	 *
	 * @since 4.0.0
	 *
	 * @var array<int>
	 */
	const FONT_SCALE_OPTIONS = array( 60, 80, 100, 120, 140, 160, 180, 200 );

	/**
	 * Register hooks.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $register_hooks Whether to register WordPress hooks.
	 */
	public function __construct( $register_hooks = true ) {
		if ( ! $register_hooks ) {
			return;
		}

		add_action( 'wp_ajax_tutor_save_user_preferences', array( $this, 'ajax_save_user_preferences' ) );
		add_action( 'wp_ajax_tutor_reset_user_preferences', array( $this, 'ajax_reset_user_preferences' ) );
		add_filter( 'language_attributes', array( $this, 'add_theme_attribute' ) );
		add_action( 'wp_head', array( $this, 'apply_inline_styles' ) );
	}

	/**
	 * Apply user preference inline styles (font scale, background color).
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function apply_inline_styles() {
		if ( ! tutor_utils()->is_dashboard_page() && ! tutor_utils()->is_learning_area() ) {
			return;
		}
		$prefs          = $this->get_preferences();
		$font_scale     = isset( $prefs['font_scale'] ) ? (int) $prefs['font_scale'] : self::DEFAULT_FONT_SCALE;
		$base_font_size = 16;
		$font_size      = ( $base_font_size * $font_scale ) / self::DEFAULT_FONT_SCALE;
		echo '<style>html { background-color: var(--tutor-surface-base); } :root { font-size: ' . esc_attr( $font_size ) . 'px; }</style>';
	}

	/**
	 * Add a theme attribute to the <html> tag.
	 *
	 * @since 4.0.0
	 *
	 * @param string $output Language attributes.
	 *
	 * @return string
	 */
	public function add_theme_attribute( $output ) {
		$is_logged_in  = is_user_logged_in();
		$is_tutor_page = tutor_utils()->is_dashboard_page() || tutor_utils()->is_learning_area();

		if ( ! $is_logged_in || ! $is_tutor_page ) {
			return $output;
		}

		$prefs = $this->get_preferences();
		$theme = isset( $prefs['theme'] ) ? (string) $prefs['theme'] : self::DEFAULT_THEME;
		if ( ! in_array( $theme, self::THEMES, true ) ) {
			$theme = self::DEFAULT_THEME;
		}

		$vision = isset( $prefs['vision'] ) ? (string) $prefs['vision'] : self::VISION_NORMAL;
		if ( ! in_array( $vision, self::VISIONS, true ) ) {
			$vision = self::VISION_NORMAL;
		}

		$contrast      = isset( $prefs['contrast'] ) ? (string) $prefs['contrast'] : '';
		$contrast_attr = '';
		if ( self::CONTRAST_HIGH === $contrast ) {
			$contrast_attr = ' data-tutor-contrast="' . esc_attr( self::CONTRAST_HIGH ) . '"';
		}

		$motion_effects = isset( $prefs['motion_effects'] ) ? (string) $prefs['motion_effects'] : self::MOTION_EFFECTS_AUTO;
		if ( ! in_array( $motion_effects, self::MOTION_EFFECTS_OPTIONS, true ) ) {
			$motion_effects = self::MOTION_EFFECTS_AUTO;
		}

		$motion_effects_attr = '';
		if ( self::MOTION_EFFECTS_REDUCE === $motion_effects ) {
			$motion_effects_attr = ' data-tutor-motion="reduce"';
		} elseif ( self::MOTION_EFFECTS_AUTO === $motion_effects ) {
			$motion_effects_attr = ' data-tutor-motion="auto"';
		}

		$vision_attr = self::VISION_NORMAL !== $vision
			? ' data-tutor-vision="' . esc_attr( $vision ) . '"'
			: '';

		return $output . ' data-tutor-theme="' . esc_attr( $theme ) . '"' . $vision_attr . $contrast_attr . $motion_effects_attr;
	}

	/**
	 * Get merged preferences for a user.
	 *
	 * Stored values override defaults; unknown keys are ignored.
	 *
	 * @since 4.0.0
	 *
	 * @param int $user_id Optional. User ID. Defaults to current user.
	 *
	 * @return array
	 */
	public static function get_preferences( $user_id = 0 ) {
		$user_id = tutor_utils()->get_user_id( $user_id );
		if ( ! $user_id ) {
			return array();
		}

		// Check cache first.
		$cache_key = 'get_preferences_' . $user_id;
		$cached    = TutorCache::get( $cache_key );
		if ( false !== $cached ) {
			return $cached;
		}

		// Get from database.
		$preferences = get_user_meta( $user_id, self::META_KEY, true );
		if ( ! is_array( $preferences ) ) {
			$preferences = array();
		}
		$result = array_merge( self::get_default_preferences(), $preferences );

		// Store in cache.
		TutorCache::set( $cache_key, $result );

		return $result;
	}

	/**
	 * Get a single preference value with default support.
	 *
	 * @since 4.0.0
	 *
	 * @param string     $key      Preference key.
	 * @param mixed      $fallback Default value if not set. If false, use internal defaults.
	 * @param int|string $user_id  Optional user id.
	 *
	 * @return mixed
	 */
	public static function get( $key, $fallback = false, $user_id = 0 ) {
		$prefs = self::get_preferences( $user_id );
		return isset( $prefs[ $key ] ) ? $prefs[ $key ] : $fallback;
	}

	/**
	 * Persist preferences to user meta.
	 *
	 * @since 4.0.0
	 *
	 * @param array<string,mixed> $prefs   Raw preferences to save.
	 * @param int                 $user_id Optional. User ID. Defaults to current user.
	 *
	 * @return array|false Final saved preferences.
	 */
	public function save_preferences( array $prefs, $user_id = 0 ) {
		$user_id = tutor_utils()->get_user_id( $user_id );

		if ( ! $user_id ) {
			return false;
		}

		// Get from database.
		$current_preferences = get_user_meta( $user_id, self::META_KEY, true );
		if ( ! is_array( $current_preferences ) ) {
			$current_preferences = array();
		}

		$combined_preferences = array_merge( $current_preferences, $prefs );

		$preferences = apply_filters( 'tutor_user_preference_data', $combined_preferences, $user_id );

		update_user_meta( $user_id, self::META_KEY, $preferences );

		do_action( 'tutor_user_preference_after_saved', $user_id, $preferences );

		return $preferences;
	}

	/**
	 * AJAX handler: save current user's preferences.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function ajax_save_user_preferences() {
		tutor_utils()->check_nonce();

		$auto_play_next = Input::post( 'auto_play_next', null );
		$theme          = Input::post( 'theme', null );
		$vision         = Input::post( 'vision', null );
		$contrast       = Input::post( 'contrast', null );
		$motion_effects = Input::post( 'motion_effects', null );
		$font_scale     = Input::post( 'font_scale', null );
		$learning_mood  = Input::post( 'learning_mood', null );

		$preferences_settings = array();

		if ( null !== $auto_play_next ) {
			$auto_play_next         = 'true' === $auto_play_next ? true : false;
			$default_auto_play_next = (bool) tutor_utils()->get_option( 'autoload_next_course_content' );
			$preferences_settings   = array_merge( $preferences_settings, self::prepare_preference_setting( 'auto_play_next', $auto_play_next, $default_auto_play_next ) );
		}

		if ( null !== $theme ) {
			if ( ! in_array( $theme, self::THEMES, true ) ) {
				$theme = self::DEFAULT_THEME;
			}
			$preferences_settings['theme'] = $theme;
		}

		if ( null !== $vision ) {
			$vision = (string) $vision;
			if ( ! in_array( $vision, self::VISIONS, true ) ) {
				$vision = self::VISION_NORMAL;
			}
			$preferences_settings = array_merge( $preferences_settings, self::prepare_preference_setting( 'vision', $vision, self::VISION_NORMAL ) );
		}

		if ( null !== $contrast ) {
			$contrast = (string) $contrast;
			// Switch inputs often post "true" when checked.
			if ( 'true' === $contrast ) {
				$contrast = self::CONTRAST_HIGH;
			}
			if ( self::CONTRAST_HIGH !== $contrast ) {
				$contrast = '';
			}
			$preferences_settings = array_merge( $preferences_settings, self::prepare_preference_setting( 'contrast', $contrast, '' ) );
		}

		if ( null !== $motion_effects ) {
			$motion_effects = (string) $motion_effects;
			if ( ! in_array( $motion_effects, self::MOTION_EFFECTS_OPTIONS, true ) ) {
				$motion_effects = self::MOTION_EFFECTS_AUTO;
			}
			$preferences_settings = array_merge( $preferences_settings, self::prepare_preference_setting( 'motion_effects', $motion_effects, self::MOTION_EFFECTS_AUTO ) );
		}

		if ( null !== $font_scale ) {
			$preferences_settings['font_scale'] = (int) $font_scale;
		}

		if ( null !== $learning_mood ) {
			// Validate learning_mood against allowed values.
			$allowed_moods = array( Options_V2::LEARNING_MODE_MODERN, Options_V2::LEARNING_MODE_KIDS );
			if ( ! in_array( $learning_mood, $allowed_moods, true ) ) {
				$learning_mood = Options_V2::LEARNING_MODE_MODERN;
			}

			$default_learning_mood = tutor_utils()->get_option( 'learning_mode', Options_V2::LEARNING_MODE_MODERN );
			$preferences_settings  = array_merge( $preferences_settings, self::prepare_preference_setting( 'learning_mood', $learning_mood, $default_learning_mood ) );
		}

		if ( empty( $preferences_settings ) ) {
			$this->json_response(
				__( 'No changes detected', 'tutor' ),
				null,
				HttpHelper::STATUS_OK
			);
		}

		$preference_data = $this->save_preferences( $preferences_settings, get_current_user_id() );

		if ( false === $preference_data ) {
			$this->json_response(
				__( 'Failed to save preferences', 'tutor' ),
				null,
				HttpHelper::STATUS_NOT_FOUND
			);
		}

		$this->json_response(
			__( 'Preferences saved successfully', 'tutor' ),
			$preference_data
		);
	}

	/**
	 * AJAX handler: reset current user's preferences back to defaults.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function ajax_reset_user_preferences() {
		tutor_utils()->check_nonce();

		$user_id = get_current_user_id();
		if ( ! $user_id ) {
			$this->json_response(
				__( 'Failed to reset preferences', 'tutor' ),
				null,
				HttpHelper::STATUS_NOT_FOUND
			);
		}

		// Delete user meta.
		delete_user_meta( $user_id, self::META_KEY );

		$this->json_response(
			__( 'Preferences reset successfully', 'tutor' ),
			null
		);
	}

	/**
	 * Prepare a single preference setting for saving.
	 *
	 * Only includes the setting if it already exists in user meta OR if it differs from the default.
	 *
	 * @since 4.0.0
	 *
	 * @param string $key           Preference key.
	 * @param mixed  $value         New value to save.
	 * @param mixed  $default_value Global default value for this setting.
	 *
	 * @return array<string,mixed> Key-value pair if it should be saved, empty array otherwise.
	 */
	private static function prepare_preference_setting( $key, $value, $default_value ) {
		if ( self::has_preference( $key ) || $value !== $default_value ) {
			return array( $key => $value );
		}
		return array();
	}

	/**
	 * Check if a preference key exists for a user.
	 *
	 * @since 4.0.0
	 *
	 * @param string $key     Preference key.
	 * @param int    $user_id Optional user ID.
	 *
	 * @return bool
	 */
	public static function has_preference( $key, $user_id = 0 ) {
		$user_id = tutor_utils()->get_user_id( $user_id );
		if ( ! $user_id ) {
			return false;
		}

		$preferences = get_user_meta( $user_id, self::META_KEY, true );
		if ( ! is_array( $preferences ) ) {
			return false;
		}

		return array_key_exists( $key, $preferences );
	}

	/**
	 * Get default preferences.
	 *
	 * @since 4.0.0
	 *
	 * @return array
	 */
	private static function get_default_preferences() {
		// Get defaults with filter.
		$defaults = apply_filters(
			'tutor_user_preference_defaults',
			array(
				'auto_play_next' => (bool) tutor_utils()->get_option( 'autoload_next_course_content' ),
				'theme'          => tutor_utils()->get_option( 'default_theme', Options_V2::DEFAULT_THEME_SYSTEM ),
				'vision'         => self::VISION_NORMAL,
				'contrast'       => '',
				'motion_effects' => self::MOTION_EFFECTS_AUTO,
				'font_scale'     => self::DEFAULT_FONT_SCALE,
				'learning_mood'  => tutor_utils()->get_option( 'learning_mode', Options_V2::LEARNING_MODE_MODERN ),
			)
		);

		return $defaults;
	}

	/**
	 * Get theme options for UI selects.
	 *
	 * @since 4.0.0
	 *
	 * @return array<int,array{label:string,value:string}>
	 */
	public static function get_theme_options() {
		return array(
			array(
				'label' => __( 'Light', 'tutor' ),
				'value' => self::THEME_LIGHT,
			),
			array(
				'label' => __( 'Dark', 'tutor' ),
				'value' => self::THEME_DARK,
			),
			array(
				'label' => __( 'Auto (System Default)', 'tutor' ),
				'value' => self::THEME_SYSTEM,
			),
		);
	}

	/**
	 * Get vision options for UI selects.
	 *
	 * @since 4.0.0
	 *
	 * @return array<int,array{label:string,value:string}>
	 */
	public static function get_vision_options() {
		return array(
			array(
				'label' => __( 'Normal', 'tutor' ),
				'value' => self::VISION_NORMAL,
			),
			array(
				'label' => __( 'Protanopia', 'tutor' ),
				'value' => self::VISION_PROTANOPIA,
			),
			array(
				'label' => __( 'Deuteranopia', 'tutor' ),
				'value' => self::VISION_DEUTERANOPIA,
			),
			array(
				'label' => __( 'Deuteranomaly', 'tutor' ),
				'value' => self::VISION_DEUTERANOMALY,
			),
		);
	}

	/**
	 * Get learning mood options for UI selects.
	 *
	 * @since 4.0.0
	 *
	 * @return array<int,array{label:string,value:string}>
	 */
	public static function get_learning_mood_options() {
		return array(
			array(
				'label' => __( 'Modern', 'tutor' ),
				'value' => Options_V2::LEARNING_MODE_MODERN,
			),
			array(
				'label' => __( 'Kids', 'tutor' ),
				'value' => Options_V2::LEARNING_MODE_KIDS,
			),
		);
	}

	/**
	 * Get motion effects options for UI selects.
	 *
	 * @since 4.0.0
	 *
	 * @return array<int,array{label:string,value:string}>
	 */
	public static function get_motion_effects_options() {
		return array(
			array(
				'label' => __( 'Reduced Motion', 'tutor' ),
				'value' => self::MOTION_EFFECTS_REDUCE,
			),
			array(
				'label' => __( 'Standard Motion', 'tutor' ),
				'value' => self::MOTION_EFFECTS_STANDARD,
			),
			array(
				'label' => __( 'Auto (System Default)', 'tutor' ),
				'value' => self::MOTION_EFFECTS_AUTO,
			),
		);
	}

	/**
	 * Get font scale options for UI selects.
	 *
	 * Uses a filter to allow customization of available values.
	 *
	 * @since 4.0.0
	 *
	 * @return array<int,array{label:string,value:int}>
	 */
	public static function get_font_scale_options() {
		$values  = apply_filters( 'tutor_user_preference_font_scale_values', self::FONT_SCALE_OPTIONS );
		$options = array();
		foreach ( $values as $value ) {
			$value  = (int) $value;
			$option = array(
				'label' => $value . '%',
				'value' => $value,
			);

			if ( self::DEFAULT_FONT_SCALE === $value ) {
				$option['html_label']    = $value . '% <span class="tutor-badge tutor-ml-4">' . esc_html__( 'Default', 'tutor' ) . '</span>';
				$option['display_label'] = __( 'Default', 'tutor' );
			} elseif ( 140 === $value ) {
				$option['html_label']    = $value . '% <span class="tutor-badge tutor-ml-4">' . esc_html__( 'Large', 'tutor' ) . '</span>';
				$option['display_label'] = __( 'Large', 'tutor' );
			}

			$options[] = $option;
		}
		return $options;
	}
}