true, 'show_desktop' => true, 'show_tablet' => true, 'show_mobile' => true, 'show_threshold' => 320, 'visibility_mode' => 'threshold', 'scroll_duration' => 460, 'hide_on_short_pages' => true, 'show_progress' => true, 'progress_scope' => 'auto', 'progress_selector' => '', 'return_point_enabled' => false, 'return_label' => '', 'smart_dock_enabled' => true, 'smart_dock_gap' => 12, 'smart_dock_opposite_side' => true, 'smart_dock_selectors' => '', 'desktop_position' => 'bottom-end', 'tablet_position' => 'bottom-end', 'mobile_position' => 'bottom-end', 'desktop_offset_inline' => 24, 'tablet_offset_inline' => 20, 'mobile_offset_inline' => 16, 'desktop_offset_block' => 24, 'tablet_offset_block' => 20, 'mobile_offset_block' => 16, 'desktop_button_size' => 54, 'tablet_button_size' => 52, 'mobile_button_size' => 48, 'desktop_icon_size' => 24, 'tablet_icon_size' => 23, 'mobile_icon_size' => 22, 'z_index' => 9990, 'button_style' => 'gradient', 'button_shape' => 'circle', 'color_source' => 'custom', 'bg_color_start' => '#2563eb', 'bg_color_end' => '#1d4ed8', 'icon_color' => '#ffffff', 'progress_color' => '#93c5fd', 'border_color' => '#ffffff', 'border_width' => 0, 'shadow_enabled' => true, 'hover_animation' => true, 'icon_type' => 'chevron', 'custom_icon_id' => 0, 'show_tooltip' => true, 'button_label' => '', 'mobile_breakpoint' => 782, 'tablet_breakpoint' => 1024, 'delete_on_uninstall' => false, ); } /** * Sanitize settings before storage. * * @param mixed $input Raw settings. * @return array */ public static function sanitize( $input ) { $input = is_array( $input ) ? self::migrate_legacy( $input ) : array(); $clean = self::normalize( $input, true ); self::flush_cache(); return $clean; } /** * Normalize settings at both write and read boundaries. * * @param mixed $input Raw settings. * @param bool $report_errors Whether to register admin settings notices. * @return array */ private static function normalize( $input, $report_errors ) { $input = is_array( $input ) ? $input : array(); $defaults = self::defaults(); $clean = array(); $positions = array( 'top-start', 'top-end', 'bottom-start', 'bottom-end' ); $devices = array( 'desktop', 'tablet', 'mobile' ); $clean['enabled'] = Sanitizer::boolean( $input['enabled'] ?? false ); $clean['show_desktop'] = Sanitizer::boolean( $input['show_desktop'] ?? false ); $clean['show_tablet'] = Sanitizer::boolean( $input['show_tablet'] ?? false ); $clean['show_mobile'] = Sanitizer::boolean( $input['show_mobile'] ?? false ); $clean['show_threshold'] = Sanitizer::integer( $input['show_threshold'] ?? null, 0, 5000, $defaults['show_threshold'] ); $clean['visibility_mode'] = Sanitizer::choice( $input['visibility_mode'] ?? '', array( 'threshold', 'scroll-up' ), $defaults['visibility_mode'] ); $clean['scroll_duration'] = Sanitizer::integer( $input['scroll_duration'] ?? null, 0, 1500, $defaults['scroll_duration'] ); $clean['hide_on_short_pages'] = Sanitizer::boolean( $input['hide_on_short_pages'] ?? false ); $clean['show_progress'] = Sanitizer::boolean( $input['show_progress'] ?? false ); $clean['progress_scope'] = Sanitizer::choice( $input['progress_scope'] ?? '', array( 'auto', 'page', 'custom' ), $defaults['progress_scope'] ); $clean['progress_selector'] = Sanitizer::selector( $input['progress_selector'] ?? '' ); $clean['return_point_enabled'] = Sanitizer::boolean( $input['return_point_enabled'] ?? false ); $clean['return_label'] = Sanitizer::label( $input['return_label'] ?? '', '', 64 ); $clean['smart_dock_enabled'] = Sanitizer::boolean( $input['smart_dock_enabled'] ?? false ); $clean['smart_dock_gap'] = Sanitizer::integer( $input['smart_dock_gap'] ?? null, 4, 48, $defaults['smart_dock_gap'] ); $clean['smart_dock_opposite_side'] = Sanitizer::boolean( $input['smart_dock_opposite_side'] ?? false ); $clean['smart_dock_selectors'] = Sanitizer::selector_lines( $input['smart_dock_selectors'] ?? '' ); foreach ( $devices as $device ) { $clean[ $device . '_position' ] = Sanitizer::choice( $input[ $device . '_position' ] ?? '', $positions, $defaults[ $device . '_position' ] ); $clean[ $device . '_offset_inline' ] = Sanitizer::integer( $input[ $device . '_offset_inline' ] ?? null, 0, 240, $defaults[ $device . '_offset_inline' ] ); $clean[ $device . '_offset_block' ] = Sanitizer::integer( $input[ $device . '_offset_block' ] ?? null, 0, 240, $defaults[ $device . '_offset_block' ] ); $clean[ $device . '_button_size' ] = Sanitizer::integer( $input[ $device . '_button_size' ] ?? null, 44, 96, $defaults[ $device . '_button_size' ] ); $clean[ $device . '_icon_size' ] = Sanitizer::integer( $input[ $device . '_icon_size' ] ?? null, 14, 44, $defaults[ $device . '_icon_size' ] ); $clean[ $device . '_icon_size' ] = min( $clean[ $device . '_icon_size' ], max( 14, $clean[ $device . '_button_size' ] - 12 ) ); } $clean['z_index'] = Sanitizer::integer( $input['z_index'] ?? null, 1, 2147483000, $defaults['z_index'] ); $clean['button_style'] = Sanitizer::choice( $input['button_style'] ?? '', array( 'gradient', 'solid', 'glass', 'outline' ), $defaults['button_style'] ); $clean['button_shape'] = Sanitizer::choice( $input['button_shape'] ?? '', array( 'circle', 'square', 'rounded' ), $defaults['button_shape'] ); $clean['color_source'] = Sanitizer::choice( $input['color_source'] ?? '', array( 'custom', 'theme' ), $defaults['color_source'] ); $clean['bg_color_start'] = Sanitizer::color( $input['bg_color_start'] ?? '', $defaults['bg_color_start'] ); $clean['bg_color_end'] = Sanitizer::color( $input['bg_color_end'] ?? '', $defaults['bg_color_end'] ); $clean['icon_color'] = Sanitizer::color( $input['icon_color'] ?? '', $defaults['icon_color'] ); $clean['progress_color'] = Sanitizer::color( $input['progress_color'] ?? '', $defaults['progress_color'] ); $clean['border_color'] = Sanitizer::color( $input['border_color'] ?? '', $defaults['border_color'] ); $clean['border_width'] = Sanitizer::integer( $input['border_width'] ?? null, 0, 8, $defaults['border_width'] ); $clean['shadow_enabled'] = Sanitizer::boolean( $input['shadow_enabled'] ?? false ); $clean['hover_animation'] = Sanitizer::boolean( $input['hover_animation'] ?? false ); $clean['icon_type'] = Sanitizer::choice( $input['icon_type'] ?? '', array( 'chevron', 'arrow', 'double', 'minimal', 'bar-arrow', 'caret', 'triangle', 'north', 'rocket', 'page-top', 'custom' ), $defaults['icon_type'] ); $clean['custom_icon_id'] = Sanitizer::image_attachment_id( $input['custom_icon_id'] ?? 0 ); $clean['show_tooltip'] = Sanitizer::boolean( $input['show_tooltip'] ?? false ); $clean['button_label'] = Sanitizer::label( $input['button_label'] ?? '', '', 80 ); $clean['mobile_breakpoint'] = Sanitizer::integer( $input['mobile_breakpoint'] ?? null, 320, 1024, $defaults['mobile_breakpoint'] ); $clean['tablet_breakpoint'] = Sanitizer::integer( $input['tablet_breakpoint'] ?? null, 600, 1600, $defaults['tablet_breakpoint'] ); $clean['delete_on_uninstall'] = Sanitizer::boolean( $input['delete_on_uninstall'] ?? false ); if ( $clean['tablet_breakpoint'] <= $clean['mobile_breakpoint'] ) { $clean['tablet_breakpoint'] = min( 1600, max( $defaults['tablet_breakpoint'], $clean['mobile_breakpoint'] + 1 ) ); if ( $report_errors && function_exists( 'add_settings_error' ) ) { add_settings_error( self::OPTION, 'akava_btt_invalid_breakpoints', __( 'The tablet breakpoint must be larger than the mobile breakpoint, so it was adjusted automatically.', 'akava-back-to-top' ), 'warning' ); } } if ( 'custom' === $clean['progress_scope'] && '' === $clean['progress_selector'] ) { $clean['progress_scope'] = 'auto'; if ( $report_errors && function_exists( 'add_settings_error' ) ) { add_settings_error( self::OPTION, 'akava_btt_missing_progress_selector', __( 'ContentProgress was switched to automatic detection because the custom content selector is empty.', 'akava-back-to-top' ), 'warning' ); } } if ( $clean['return_point_enabled'] && '' === $clean['return_label'] ) { $clean['return_point_enabled'] = false; if ( $report_errors && function_exists( 'add_settings_error' ) ) { add_settings_error( self::OPTION, 'akava_btt_missing_return_label', __( 'ReturnPoint was disabled because its accessible return label is empty. Add the label in the language of your site, then enable the feature again.', 'akava-back-to-top' ), 'warning' ); } } if ( 'custom' === $clean['icon_type'] && ! $clean['custom_icon_id'] ) { $clean['icon_type'] = $defaults['icon_type']; if ( $report_errors && function_exists( 'add_settings_error' ) ) { add_settings_error( self::OPTION, 'akava_btt_invalid_custom_icon', __( 'The custom icon was not a valid WordPress image attachment, so the default icon was restored.', 'akava-back-to-top' ), 'warning' ); } } return $clean; } /** * Honor 2.0 filter keys when custom code still supplies the old schema. * * Stored options no longer contain these keys after normalization, so their * presence here indicates an intentional runtime override. * * @param array $settings Filtered settings. * @return array */ private static function apply_runtime_legacy_aliases( array $settings ) { if ( array_key_exists( 'position', $settings ) ) { foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) { $settings[ $device . '_position' ] = $settings['position']; } } if ( array_key_exists( 'offset_inline', $settings ) ) { foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) { $settings[ $device . '_offset_inline' ] = $settings['offset_inline']; } } if ( array_key_exists( 'offset_block', $settings ) ) { foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) { $settings[ $device . '_offset_block' ] = $settings['offset_block']; } } if ( array_key_exists( 'button_size', $settings ) ) { $settings['desktop_button_size'] = $settings['button_size']; $settings['tablet_button_size'] = $settings['button_size']; } if ( array_key_exists( 'button_size_mobile', $settings ) ) { $settings['mobile_button_size'] = $settings['button_size_mobile']; } if ( array_key_exists( 'icon_size', $settings ) ) { foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) { $settings[ $device . '_icon_size' ] = $settings['icon_size']; } } if ( array_key_exists( 'display_mode', $settings ) ) { $mode = Sanitizer::choice( $settings['display_mode'], array( 'all', 'desktop', 'mobile' ), 'all' ); $settings['show_desktop'] = in_array( $mode, array( 'all', 'desktop' ), true ); $settings['show_tablet'] = 'all' === $mode; $settings['show_mobile'] = in_array( $mode, array( 'all', 'mobile' ), true ); } return $settings; } /** * Convert 1.x and 2.0 settings without moving existing buttons unexpectedly. * * @param array $stored Stored settings. * @return array */ public static function migrate_legacy( array $stored ) { // Existing installations keep whole-page progress until the owner opts in. $installed_version = (string) get_option( self::VERSION_OPTION, '' ); if ( ! array_key_exists( 'progress_scope', $stored ) && '' !== $installed_version && version_compare( $installed_version, '2.3.0', '<' ) ) { $stored['progress_scope'] = 'page'; } if ( isset( $stored['offset_x'] ) && ! isset( $stored['offset_inline'] ) ) { $stored['offset_inline'] = $stored['offset_x']; } if ( isset( $stored['offset_y'] ) && ! isset( $stored['offset_block'] ) ) { $stored['offset_block'] = $stored['offset_y']; } $is_rtl = self::site_uses_rtl(); $position_map = $is_rtl ? array( 'top-left' => 'top-end', 'top-right' => 'top-start', 'bottom-left' => 'bottom-end', 'bottom-right' => 'bottom-start', ) : array( 'top-left' => 'top-start', 'top-right' => 'top-end', 'bottom-left' => 'bottom-start', 'bottom-right' => 'bottom-end', ); if ( isset( $stored['position'], $position_map[ $stored['position'] ] ) ) { $stored['position'] = $position_map[ $stored['position'] ]; } if ( isset( $stored['display_mode'] ) && ! isset( $stored['show_desktop'], $stored['show_tablet'], $stored['show_mobile'] ) ) { $stored['show_desktop'] = in_array( $stored['display_mode'], array( 'all', 'desktop' ), true ); $stored['show_tablet'] = 'all' === $stored['display_mode']; $stored['show_mobile'] = in_array( $stored['display_mode'], array( 'all', 'mobile' ), true ); } $legacy_position = isset( $stored['position'] ) ? $stored['position'] : null; $legacy_inline = isset( $stored['offset_inline'] ) ? $stored['offset_inline'] : null; $legacy_block = isset( $stored['offset_block'] ) ? $stored['offset_block'] : null; $legacy_size = isset( $stored['button_size'] ) ? $stored['button_size'] : null; $legacy_mobile = isset( $stored['button_size_mobile'] ) ? $stored['button_size_mobile'] : $legacy_size; $legacy_icon = isset( $stored['icon_size'] ) ? $stored['icon_size'] : null; foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) { if ( null !== $legacy_position && ! isset( $stored[ $device . '_position' ] ) ) { $stored[ $device . '_position' ] = $legacy_position; } if ( null !== $legacy_inline && ! isset( $stored[ $device . '_offset_inline' ] ) ) { $stored[ $device . '_offset_inline' ] = $legacy_inline; } if ( null !== $legacy_block && ! isset( $stored[ $device . '_offset_block' ] ) ) { $stored[ $device . '_offset_block' ] = $legacy_block; } if ( null !== $legacy_icon && ! isset( $stored[ $device . '_icon_size' ] ) ) { $stored[ $device . '_icon_size' ] = $legacy_icon; } } if ( null !== $legacy_size ) { $stored['desktop_button_size'] = $stored['desktop_button_size'] ?? $legacy_size; $stored['tablet_button_size'] = $stored['tablet_button_size'] ?? $legacy_size; } if ( null !== $legacy_mobile ) { $stored['mobile_button_size'] = $stored['mobile_button_size'] ?? $legacy_mobile; } if ( isset( $stored['custom_icon_url'] ) && ! isset( $stored['custom_icon_id'] ) ) { $stored['custom_icon_id'] = attachment_url_to_postid( esc_url_raw( $stored['custom_icon_url'] ) ); } return $stored; } /** * Determine the public site's direction, not the current admin locale. * * @return bool */ private static function site_uses_rtl() { if ( ! function_exists( 'is_rtl' ) ) { return false; } $site_locale = (string) get_option( 'WPLANG', '' ); $site_locale = '' !== $site_locale ? $site_locale : 'en_US'; if ( function_exists( 'switch_to_locale' ) && function_exists( 'restore_previous_locale' ) ) { $switched = switch_to_locale( $site_locale ); $is_rtl = is_rtl(); if ( $switched ) { restore_previous_locale(); } return $is_rtl; } return is_rtl(); } }