option_name, $this->option_name, array( $this, 'sanitize_options' ) ); } public function setup_admin_notice_suppression( $screen ) { if ( ! $this->is_fastlayer_admin_screen( $screen ) ) { return; } // Use CSS instead of removing notice callbacks so that only third-party // admin notices are hidden on WP FastLayer screens. add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_notice_scope_css' ) ); } public function enqueue_admin_notice_scope_css() { wp_register_style( 'fastlayer-admin-notice-scope', false, array(), FASTLAYER_VERSION ); wp_enqueue_style( 'fastlayer-admin-notice-scope' ); wp_add_inline_style( 'fastlayer-admin-notice-scope', '.wrap .notice:not(.wpfastlayer-notice), .wrap .update-nag, .wrap .notice-warning:not(.wpfastlayer-notice), .wrap .notice-info:not(.wpfastlayer-notice) { display: none !important; }' ); } private function is_fastlayer_admin_screen( $screen ) { if ( ! is_object( $screen ) ) { $screen = get_current_screen(); } if ( ! $screen || empty( $screen->id ) ) { return false; } $screen_id = (string) $screen->id; $screen_base = isset( $screen->base ) ? (string) $screen->base : ''; return false !== strpos( $screen_id, 'wp-fastlayer' ) || false !== strpos( $screen_base, 'wp-fastlayer' ); } /** * Matches the admin_enqueue_scripts hook suffix used by the FastLayer screens. * * WordPress derives the hook suffix of a submenu page from the sanitized * menu title of its parent (wp-admin/menu.php), so a page registered under * the "wp-fastlayer" slug but titled "FastLayer" resolves to * "fastlayer_page_" rather than "wp-fastlayer_page_". Both the * legacy and the current forms are accepted so the localized admin script * data is always emitted on FastLayer screens. */ private function is_fastlayer_admin_hook( $hook ) { $hook = is_string( $hook ) ? $hook : ''; if ( '' === $hook ) { return false; } $accepted_prefixes = array( 'toplevel_page_wp-fastlayer', 'wp-fastlayer_page_', 'toplevel_page_fastlayer', 'fastlayer_page_', ); foreach ( $accepted_prefixes as $prefix ) { if ( 0 === strpos( $hook, $prefix ) ) { return true; } } return false; } public function add_plugin_action_links( $links ) { if ( ! current_user_can( 'manage_options' ) ) { return $links; } $settings_link = sprintf( '%2$s', esc_url( admin_url( 'admin.php?page=wp-fastlayer' ) ), esc_html__( 'Settings', 'fastlayer' ) ); array_unshift( $links, $settings_link ); return $links; } public function handle_cache_debug_test() { check_admin_referer( 'fastlayer_cache_debug_test_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $cache_dir = FASTLAYER_CACHE_DIR; $result = array( 'cache_dir' => wp_normalize_path( $cache_dir ), 'writable' => false, 'write' => 'not_attempted', 'read' => 'not_attempted', 'delete' => 'not_attempted', 'detail' => '', 'timestamp' => time(), ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { \fastlayer_debug_log( 'WP FastLayer: cache debug test started. dir=' . wp_normalize_path( $cache_dir ) ); } if ( ! is_dir( $cache_dir ) ) { $result['detail'] = 'Cache directory does not exist.'; } else { $result['writable'] = wp_is_writable( $cache_dir ); $test_file = trailingslashit( wp_normalize_path( $cache_dir ) ) . '_admin_cache_debug_' . wp_rand() . '.tmp'; $written = @file_put_contents( $test_file, 'wp-fastlayer-admin-debug', LOCK_EX ); $result['write'] = false === $written ? 'failed' : 'success'; $result['detail'] = 'write_bytes=' . ( false === $written ? '0' : $written ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { \fastlayer_debug_log( 'WP FastLayer: cache debug test write result=' . $result['write'] . ' ; file=' . basename( $test_file ) . ' ; dir=' . wp_normalize_path( $cache_dir ) ); } if ( false !== $written && file_exists( $test_file ) ) { $result['read'] = @file_get_contents( $test_file ) === 'wp-fastlayer-admin-debug' ? 'success' : 'mismatch'; // wp_delete_file() is the recommended replacement for unlink(). // It has no reliable return value before WordPress 6.7, so the // outcome is verified by checking whether the file still exists. wp_delete_file( $test_file ); $result['delete'] = file_exists( $test_file ) ? 'failed' : 'success'; if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { \fastlayer_debug_log( 'WP FastLayer: cache debug test read=' . $result['read'] . ' ; delete=' . $result['delete'] ); } } } set_transient( 'fastlayer_cache_debug_test_result', $result, 300 ); $redirect_url = wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=wp-fastlayer-cache' ); $redirect_url = add_query_arg( 'cache_debug_test', '1', $redirect_url ); wp_safe_redirect( $redirect_url ); exit; } public function sanitize_options( $input ) { $defaults = Bootstrap::get_default_options(); $existing_options = wp_parse_args( get_option( $this->option_name, array() ), $defaults ); $sanitized = $existing_options; $input = is_array( $input ) ? $input : array(); $checkbox_fields = array( 'enable_cache', 'enable_preload', 'enable_minify', 'enable_minify_css', 'enable_minify_js', 'enable_lazyload', 'enable_lazyload_iframes', 'enable_lazyload_videos', 'enable_smart_lcp_optimization', 'enable_lcp_preload', 'enable_lcp_hero_background', 'enable_lcp_debug_mode', 'enable_webp', 'enable_bulk_webp_generation', 'enable_image_delivery', 'enable_progressive_image_loading', 'enable_lqip_placeholders', 'enable_css_combine', 'enable_css_remove_comments', 'enable_css_remove_unused', 'enable_smart_gutenberg_css_optimization', 'enable_google_fonts_display', 'enable_google_fonts_preconnect', 'enable_google_fonts_combine', 'enable_local_font_display_swap', 'enable_elementor_icon_font_swap', 'enable_preload_fonts', 'enable_disable_dashicons', 'enable_css_inline', 'enable_css_delivery', 'enable_critical_css', 'enable_html_remove_comments', 'enable_html_remove_whitespace', 'enable_html_preserve_gutenberg_comments', 'enable_js_combine', 'enable_js_remove_comments', 'enable_js_defer', 'enable_js_async', 'enable_js_safe_mode', 'enable_js_smart_delay', 'enable_js_smart_delay_debug', 'debug_mode', 'enable_cdn', 'enable_imagekit_cdn', 'imagekit_auto_quality', 'imagekit_auto_format', 'imagekit_runtime_correction', 'imagekit_debug_overlay', 'db_clean_post_revisions', 'db_clean_trashed_posts', 'db_clean_spam_comments', 'db_clean_auto_drafts', 'db_clean_all_transients', 'db_clean_duplicated_postmeta', 'db_clean_duplicated_commentmeta', 'db_clean_duplicated_usermeta', 'db_clean_duplicated_termmeta', 'db_clean_expired_transients', 'db_clean_oembed_cache', 'db_clean_orphaned_postmeta', 'db_clean_orphaned_usermeta', 'db_clean_orphaned_termmeta', 'db_clean_orphaned_commentmeta' ); $keys_to_compare = array( 'enable_minify', 'enable_html_remove_comments', 'enable_html_remove_whitespace', 'enable_html_preserve_gutenberg_comments', 'enable_minify_css', 'enable_css_remove_comments', 'enable_css_remove_unused', 'enable_google_fonts_display', 'enable_google_fonts_preconnect', 'enable_google_fonts_combine', 'enable_local_font_display_swap', 'enable_elementor_icon_font_swap', 'enable_preload_fonts', 'preload_fonts_list', 'enable_disable_dashicons', 'enable_css_inline', 'enable_css_delivery', 'enable_critical_css', 'enable_js_defer', 'enable_js_smart_delay', 'enable_js_async', 'enable_js_combine', 'enable_cache', 'enable_cdn', 'enable_webp', 'enable_image_delivery', 'enable_progressive_image_loading', 'enable_lqip_placeholders', 'lqip_quality', 'lqip_width', 'lqip_blur_intensity', 'lqip_fade_duration', 'enable_smart_lcp_optimization', 'enable_lcp_preload', ); foreach ( $checkbox_fields as $field ) { if ( array_key_exists( $field, $input ) ) { $sanitized[$field] = '1' === (string) $input[ $field ] ? '1' : '0'; } } if ( isset( $sanitized['enable_js_defer'] ) && '1' === $sanitized['enable_js_defer'] ) { $sanitized['enable_js_combine'] = '0'; } if ( isset( $sanitized['enable_js_smart_delay'] ) && '1' === $sanitized['enable_js_smart_delay'] ) { $sanitized['enable_js_async'] = '0'; $sanitized['enable_js_combine'] = '0'; } $sanitized['cache_expiration'] = isset( $input['cache_expiration'] ) ? absint( $input['cache_expiration'] ) : ( isset( $existing_options['cache_expiration'] ) ? $existing_options['cache_expiration'] : 10 ); $sanitized['exclude_urls'] = isset( $input['exclude_urls'] ) ? sanitize_textarea_field( $input['exclude_urls'] ) : ( isset( $existing_options['exclude_urls'] ) ? $existing_options['exclude_urls'] : '' ); $sanitized['exclude_cookies'] = isset( $input['exclude_cookies'] ) ? sanitize_textarea_field( $input['exclude_cookies'] ) : ( isset( $existing_options['exclude_cookies'] ) ? $existing_options['exclude_cookies'] : '' ); $sanitized['exclude_user_agents'] = isset( $input['exclude_user_agents'] ) ? sanitize_textarea_field( $input['exclude_user_agents'] ) : ( isset( $existing_options['exclude_user_agents'] ) ? $existing_options['exclude_user_agents'] : '' ); $sanitized['exclude_css_files'] = isset( $input['exclude_css_files'] ) ? sanitize_textarea_field( $input['exclude_css_files'] ) : ( isset( $existing_options['exclude_css_files'] ) ? $existing_options['exclude_css_files'] : '' ); $sanitized['exclude_js_files'] = isset( $input['exclude_js_files'] ) ? sanitize_textarea_field( $input['exclude_js_files'] ) : ( isset( $existing_options['exclude_js_files'] ) ? $existing_options['exclude_js_files'] : '' ); $sanitized['exclude_js_defer'] = isset( $input['exclude_js_defer'] ) ? sanitize_textarea_field( $input['exclude_js_defer'] ) : ( isset( $existing_options['exclude_js_defer'] ) ? $existing_options['exclude_js_defer'] : '' ); $timeout_input = isset( $input['smart_js_delay_timeout'] ) ? absint( $input['smart_js_delay_timeout'] ) : ( isset( $existing_options['smart_js_delay_timeout'] ) ? absint( $existing_options['smart_js_delay_timeout'] ) : 6000 ); $sanitized['smart_js_delay_timeout'] = (string) max( 1000, min( 15000, $timeout_input ) ); $sanitized['smart_js_delay_exclusions'] = isset( $input['smart_js_delay_exclusions'] ) ? sanitize_textarea_field( $input['smart_js_delay_exclusions'] ) : ( isset( $existing_options['smart_js_delay_exclusions'] ) ? $existing_options['smart_js_delay_exclusions'] : '' ); $sanitized['critical_css_custom'] = isset( $input['critical_css_custom'] ) ? wp_kses_post( $input['critical_css_custom'] ) : ( isset( $existing_options['critical_css_custom'] ) ? $existing_options['critical_css_custom'] : '' ); $sanitized['cdn_cname'] = isset( $input['cdn_cname'] ) ? esc_url_raw( $input['cdn_cname'] ) : ( isset( $existing_options['cdn_cname'] ) ? $existing_options['cdn_cname'] : '' ); $sanitized['imagekit_endpoint'] = isset( $input['imagekit_endpoint'] ) ? esc_url_raw( trim( $input['imagekit_endpoint'] ) ) : ( isset( $existing_options['imagekit_endpoint'] ) ? $existing_options['imagekit_endpoint'] : '' ); $sanitized['imagekit_origin_path_mode'] = isset( $input['imagekit_origin_path_mode'] ) ? sanitize_key( $input['imagekit_origin_path_mode'] ) : ( isset( $existing_options['imagekit_origin_path_mode'] ) ? $existing_options['imagekit_origin_path_mode'] : 'full_uploads_path' ); if ( ! in_array( $sanitized['imagekit_origin_path_mode'], array( 'full_uploads_path', 'uploads_relative_path' ), true ) ) { $sanitized['imagekit_origin_path_mode'] = 'full_uploads_path'; } $sanitized['imagekit_auto_format_mode'] = isset( $input['imagekit_auto_format_mode'] ) ? sanitize_key( $input['imagekit_auto_format_mode'] ) : ( isset( $existing_options['imagekit_auto_format_mode'] ) ? $existing_options['imagekit_auto_format_mode'] : 'f-auto' ); if ( ! in_array( $sanitized['imagekit_auto_format_mode'], array( 'f-auto', 'fm-auto' ), true ) ) { $sanitized['imagekit_auto_format_mode'] = 'f-auto'; } $ratio_input = isset( $input['imagekit_max_oversize_ratio'] ) ? (float) $input['imagekit_max_oversize_ratio'] : ( isset( $existing_options['imagekit_max_oversize_ratio'] ) ? (float) $existing_options['imagekit_max_oversize_ratio'] : 1.5 ); $sanitized['imagekit_max_oversize_ratio'] = (string) round( max( 1.0, min( 3.0, $ratio_input ) ), 2 ); $sanitized['webp_quality'] = isset( $input['webp_quality'] ) ? min( 100, max( 50, absint( $input['webp_quality'] ) ) ) : ( isset( $existing_options['webp_quality'] ) ? absint( $existing_options['webp_quality'] ) : 80 ); $sanitized['enable_progressive_image_loading'] = isset( $input['enable_progressive_image_loading'] ) ? '1' === (string) $input['enable_progressive_image_loading'] ? '1' : '0' : ( isset( $existing_options['enable_progressive_image_loading'] ) ? $existing_options['enable_progressive_image_loading'] : '0' ); $sanitized['enable_lqip_placeholders'] = isset( $input['enable_lqip_placeholders'] ) ? '1' === (string) $input['enable_lqip_placeholders'] ? '1' : '0' : ( isset( $existing_options['enable_lqip_placeholders'] ) ? $existing_options['enable_lqip_placeholders'] : '1' ); $sanitized['lqip_quality'] = isset( $input['lqip_quality'] ) ? min( 50, max( 1, absint( $input['lqip_quality'] ) ) ) : ( isset( $existing_options['lqip_quality'] ) ? absint( $existing_options['lqip_quality'] ) : 25 ); $sanitized['lqip_width'] = isset( $input['lqip_width'] ) ? min( 80, max( 20, absint( $input['lqip_width'] ) ) ) : ( isset( $existing_options['lqip_width'] ) ? absint( $existing_options['lqip_width'] ) : 40 ); $sanitized['lqip_blur_intensity'] = isset( $input['lqip_blur_intensity'] ) ? min( 50, max( 0, absint( $input['lqip_blur_intensity'] ) ) ) : ( isset( $existing_options['lqip_blur_intensity'] ) ? absint( $existing_options['lqip_blur_intensity'] ) : 20 ); $sanitized['lqip_fade_duration'] = isset( $input['lqip_fade_duration'] ) ? min( 1000, max( 0, absint( $input['lqip_fade_duration'] ) ) ) : ( isset( $existing_options['lqip_fade_duration'] ) ? absint( $existing_options['lqip_fade_duration'] ) : 250 ); $sanitized['enable_html_remove_comments'] = isset( $input['enable_html_remove_comments'] ) ? '1' === (string) $input['enable_html_remove_comments'] ? '1' : '0' : ( isset( $existing_options['enable_html_remove_comments'] ) ? $existing_options['enable_html_remove_comments'] : '1' ); $sanitized['enable_html_remove_whitespace'] = isset( $input['enable_html_remove_whitespace'] ) ? '1' === (string) $input['enable_html_remove_whitespace'] ? '1' : '0' : ( isset( $existing_options['enable_html_remove_whitespace'] ) ? $existing_options['enable_html_remove_whitespace'] : '1' ); $sanitized['enable_html_preserve_gutenberg_comments'] = isset( $input['enable_html_preserve_gutenberg_comments'] ) ? '1' === (string) $input['enable_html_preserve_gutenberg_comments'] ? '1' : '0' : ( isset( $existing_options['enable_html_preserve_gutenberg_comments'] ) ? $existing_options['enable_html_preserve_gutenberg_comments'] : '1' ); $sanitized['enable_css_remove_unused'] = isset( $input['enable_css_remove_unused'] ) ? '1' === (string) $input['enable_css_remove_unused'] ? '1' : '0' : ( isset( $existing_options['enable_css_remove_unused'] ) ? $existing_options['enable_css_remove_unused'] : '0' ); $sanitized['enable_google_fonts_display'] = isset( $input['enable_google_fonts_display'] ) ? '1' === (string) $input['enable_google_fonts_display'] ? '1' : '0' : ( isset( $existing_options['enable_google_fonts_display'] ) ? $existing_options['enable_google_fonts_display'] : ( isset( $existing_options['enable_font_display_swap'] ) ? $existing_options['enable_font_display_swap'] : '1' ) ); $sanitized['preload_fonts_list'] = isset( $input['preload_fonts_list'] ) ? sanitize_textarea_field( $input['preload_fonts_list'] ) : ( isset( $existing_options['preload_fonts_list'] ) ? $existing_options['preload_fonts_list'] : '' ); $sanitized['_version'] = FASTLAYER_VERSION; if ( $this->should_clear_cache_on_option_change( $existing_options, $sanitized, $keys_to_compare ) ) { Page_Cache::get_instance()->clear_all_cache(); } // Debug logging: capture sanitized array before returning (only when WP_DEBUG is enabled). if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { \fastlayer_debug_log( 'CSS OPTION SANITIZED: ' . wp_json_encode( $sanitized ) ); } Bootstrap::write_static_config( $sanitized ); return $sanitized; } private function should_clear_cache_on_option_change( $existing_options, $new_options, $keys ) { foreach ( $keys as $key ) { $old_value = isset( $existing_options[ $key ] ) ? $existing_options[ $key ] : '0'; $new_value = isset( $new_options[ $key ] ) ? $new_options[ $key ] : '0'; if ( (string) $old_value !== (string) $new_value ) { return true; } } return false; } private function render_toggle_input( $field, $value, $class = '', $attributes = '' ) { $name = sprintf( '%s[%s]', $this->option_name, $field ); $high_risk = $this->is_high_risk_setting( $field ); ?> data-high-risk="" data-high-risk-field="" data-high-risk-label="get_high_risk_feature_map()[ $field ]['label'] : '' ); ?>" data-high-risk-summary="get_high_risk_feature_map()[ $field ]['summary'] : '' ); ?>" data-original-value="" > get_high_risk_feature_map(); return isset( $high_risk_features[ $field ] ); } private function get_high_risk_feature_map() { return array( 'enable_css_combine' => array( 'label' => __( 'Combine CSS Files', 'fastlayer' ), 'summary' => __( 'Combining CSS files can change CSS execution order and may affect layout or dynamic styling.', 'fastlayer' ), ), 'enable_css_delivery' => array( 'label' => __( 'Optimize CSS Delivery', 'fastlayer' ), 'summary' => __( 'Optimizing CSS delivery can affect how styles are loaded and may impact theme or page rendering.', 'fastlayer' ), ), 'enable_critical_css' => array( 'label' => __( 'Inline Critical CSS', 'fastlayer' ), 'summary' => __( 'Inlined critical CSS can improve first paint but may alter how page styles are applied.', 'fastlayer' ), ), 'enable_js_combine' => array( 'label' => __( 'Combine JavaScript Files', 'fastlayer' ), 'summary' => __( 'Combining JavaScript may delay or change execution order and can affect scripts, widgets, and page behavior.', 'fastlayer' ), ), 'enable_js_defer' => array( 'label' => __( 'Defer JavaScript Execution', 'fastlayer' ), 'summary' => __( 'Deferring JavaScript may prevent scripts from running at the expected time, affecting layout, sliders, or forms.', 'fastlayer' ), ), 'enable_js_smart_delay' => array( 'label' => __( 'Enable Smart JS Delay', 'fastlayer' ), 'summary' => __( 'Delaying scripts until user interaction can improve performance but may impact third-party integrations and dynamic content.', 'fastlayer' ), ), 'enable_lazyload' => array( 'label' => __( 'Enable Lazyload', 'fastlayer' ), 'summary' => __( 'Lazyloading images can impact how media is displayed and may affect carousels, galleries, or Elementor layouts.', 'fastlayer' ), ), 'enable_lazyload_iframes' => array( 'label' => __( 'Lazyload iframes', 'fastlayer' ), 'summary' => __( 'Lazyloading iframes may delay third-party embeds and affect interactive content or tracking scripts.', 'fastlayer' ), ), 'enable_lazyload_videos' => array( 'label' => __( 'Lazyload videos', 'fastlayer' ), 'summary' => __( 'Lazyloading videos can alter playback timing and may change how media is presented on pages.', 'fastlayer' ), ), ); } private function get_newly_enabled_high_risk_features( $existing_options, $sanitized_options ) { $newly_enabled = array(); foreach ( $this->get_high_risk_feature_map() as $field => $meta ) { $old_value = isset( $existing_options[ $field ] ) ? (string) $existing_options[ $field ] : '0'; $new_value = isset( $sanitized_options[ $field ] ) ? (string) $sanitized_options[ $field ] : '0'; if ( '0' === $old_value && '1' === $new_value ) { $newly_enabled[ $field ] = $meta; } } return $newly_enabled; } private function record_high_risk_confirmation_log( $existing_options, $sanitized_options, $newly_enabled_features ) { $log = isset( $existing_options['high_risk_confirmation_log'] ) && is_array( $existing_options['high_risk_confirmation_log'] ) ? $existing_options['high_risk_confirmation_log'] : array(); $current_user = wp_get_current_user(); $user_name = is_object( $current_user ) && ! empty( $current_user->user_login ) ? $current_user->user_login : ''; $timestamp = current_time( 'mysql' ); foreach ( $newly_enabled_features as $field => $meta ) { $log[ $field ] = array( 'label' => $meta['label'], 'enabled_by' => $user_name, 'enabled_at' => $timestamp, ); } return $log; } private function get_high_risk_architecture_support() { return array( 'safe_mode' => false, 'emergency_recovery_mode' => false, 'rollback_system' => false, 'last_known_working_config' => false, ); } private function is_bulk_webp_generation_enabled( $options = null ) { if ( ! is_array( $options ) ) { $options = get_option( $this->option_name, array() ); } return ! isset( $options['enable_bulk_webp_generation'] ) || '1' === (string) $options['enable_bulk_webp_generation']; } private function render_help_icon( $text ) { return sprintf( '?', esc_attr( $text ) ); } private function get_admin_tabs() { return array( '' => array( 'wp-fastlayer' => array( __( 'Dashboard', 'fastlayer' ), 'dashicons-dashboard' ), 'wp-fastlayer-cache' => array( __( 'Cache', 'fastlayer' ), 'dashicons-image-filter' ), 'wp-fastlayer-html' => array( __( 'HTML', 'fastlayer' ), 'dashicons-editor-code' ), 'wp-fastlayer-css' => array( __( 'CSS', 'fastlayer' ), 'dashicons-admin-appearance' ), 'wp-fastlayer-fonts' => array( __( 'Fonts', 'fastlayer' ), 'dashicons-editor-textcolor' ), 'wp-fastlayer-js' => array( __( 'JS', 'fastlayer' ), 'dashicons-admin-page' ), 'wp-fastlayer-media' => array( __( 'Media', 'fastlayer' ), 'dashicons-format-image' ), 'wp-fastlayer-cdn' => array( __( 'CDN', 'fastlayer' ), 'dashicons-cloud' ), 'wp-fastlayer-database' => array( __( 'Database', 'fastlayer' ), 'dashicons-database' ), 'wp-fastlayer-import-export' => array( __( 'Import / Export', 'fastlayer' ), 'dashicons-admin-tools' ), 'wp-fastlayer-advanced' => array( __( 'Advanced', 'fastlayer' ), 'dashicons-admin-generic' ), ), ); } private function render_page_navigation() { $current = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : 'wp-fastlayer'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. $groups = $this->get_admin_tabs(); echo ''; } private function render_save_bar() { $settings_updated = isset( $_GET['settings-updated'] ) ? sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. echo '
'; if ( 'true' === $settings_updated ) { echo '
'; echo ''; echo '' . esc_html__( 'Settings saved successfully.', 'fastlayer' ) . ''; echo '
'; } submit_button( __( 'Save Changes', 'fastlayer' ), 'primary', 'submit', true, array( 'class' => 'wpfl-submit-btn' ) ); echo '
'; } private function render_page_header( $title, $description = '', $help_url = '' ) { ?> render_page_navigation(); $this->render_settings_saved_notice(); } private function render_settings_saved_notice() { if ( function_exists( 'settings_errors' ) ) { settings_errors( $this->option_name ); } $imagekit_notice = get_transient( 'fastlayer_imagekit_test_notice' ); if ( is_array( $imagekit_notice ) && ! empty( $imagekit_notice['message'] ) ) { $notice_class = ( isset( $imagekit_notice['type'] ) && 'success' === $imagekit_notice['type'] ) ? 'notice-success' : 'notice-error'; echo '

' . esc_html( $imagekit_notice['message'] ) . '

'; delete_transient( 'fastlayer_imagekit_test_notice' ); } } /** * Returns the curated one-click performance presets. * * @return array> */ public function get_performance_presets() { $safe = array( 'enable_cache' => '1', 'enable_preload' => '1', 'cache_expiration' => 24, 'enable_minify' => '1', 'enable_html_remove_comments' => '1', 'enable_html_remove_whitespace' => '1', 'enable_html_preserve_gutenberg_comments' => '1', 'enable_minify_css' => '1', 'enable_css_combine' => '0', 'enable_css_remove_comments' => '1', 'enable_css_remove_unused' => '0', 'enable_smart_gutenberg_css_optimization' => '1', 'enable_google_fonts_display' => '1', 'enable_css_inline' => '0', 'enable_css_delivery' => '0', 'enable_critical_css' => '0', 'enable_minify_js' => '1', 'enable_js_combine' => '0', 'enable_js_remove_comments' => '1', 'enable_js_defer' => '1', 'enable_js_async' => '0', 'enable_js_safe_mode' => '1', 'enable_js_smart_delay' => '0', 'enable_lazyload' => '1', 'enable_lazyload_iframes' => '1', 'enable_lazyload_videos' => '0', 'enable_smart_lcp_optimization' => '1', 'enable_lcp_preload' => '1', 'enable_lcp_hero_background' => '0', 'enable_webp' => '1', 'enable_image_delivery' => '1', 'enable_progressive_image_loading' => '0', ); $recommended = array_merge( $safe, array( 'enable_css_remove_unused' => '1', 'enable_css_inline' => '1', 'enable_css_delivery' => '1', 'enable_js_safe_mode' => '0', 'enable_js_smart_delay' => '1', 'enable_lazyload_videos' => '1', 'enable_lcp_hero_background' => '1', ) ); $extreme = array_merge( $recommended, array( 'enable_css_combine' => '1', 'enable_critical_css' => '1', 'enable_progressive_image_loading' => '1', 'enable_lqip_placeholders' => '1', ) ); return array( 'safe' => array( 'label' => __( 'Safe Mode (Balanced)', 'fastlayer' ), 'badge' => __( '100% Theme Compatible', 'fastlayer' ), 'description' => __( 'Page caching, preloading, HTML/CSS/JS minification, Safe JS Defer, WebP delivery, and Lazy Load without aggressive script delaying.', 'fastlayer' ), 'highlights' => array( __( 'Page Cache + Automatic Preloading', 'fastlayer' ), __( 'HTML, CSS & JS Minification', 'fastlayer' ), __( 'Safe JS Defer + Lazy Load + WebP', 'fastlayer' ), ), 'settings' => $safe, ), 'recommended' => array( 'label' => __( 'WP Rocket Style (Recommended)', 'fastlayer' ), 'badge' => __( '★ Best for 95+ PageSpeed', 'fastlayer' ), 'description' => __( 'Golden configuration: Page Cache, Smart JS Delay (interaction trigger), Non-Blocking CSS Delivery, Unused Block CSS Removal, Smart LCP Preload, Google Fonts Preconnect & Swap, and WebP.', 'fastlayer' ), 'highlights' => array( __( 'Smart JS Delay (Interaction Trigger)', 'fastlayer' ), __( 'Non-Blocking CSS + Unused Block CSS Strip', 'fastlayer' ), __( 'Smart LCP Preload + Google Fonts Preconnect', 'fastlayer' ), ), 'settings' => $recommended, ), 'extreme' => array( 'label' => __( 'Extreme Turbo (Max 100/100)', 'fastlayer' ), 'badge' => __( 'Maximum Acceleration', 'fastlayer' ), 'description' => __( 'All Recommended optimizations plus CSS Combination, Inline Critical CSS, and Progressive LQIP Blur Image Placeholders for maximum Core Web Vitals scores.', 'fastlayer' ), 'highlights' => array( __( 'Everything in Recommended Preset', 'fastlayer' ), __( 'Combine CSS + Inline Critical CSS', 'fastlayer' ), __( 'Progressive LQIP Blur Placeholders', 'fastlayer' ), ), 'settings' => $extreme, ), ); } /** * Detects which performance preset matches the current options, if any. * * @param array $options Current plugin options. * @return string Preset key ('safe', 'recommended', 'extreme') or '' if custom. */ public function detect_active_preset( $options ) { if ( ! is_array( $options ) || empty( $options['enable_cache'] ) || '1' !== (string) $options['enable_cache'] ) { return ''; } $presets = $this->get_performance_presets(); foreach ( array( 'extreme', 'recommended', 'safe' ) as $preset_key ) { $matches = true; foreach ( $presets[ $preset_key ]['settings'] as $k => $v ) { if ( 'cache_expiration' === $k ) { continue; } $current_val = isset( $options[ $k ] ) ? (string) $options[ $k ] : '0'; if ( $current_val !== (string) $v ) { $matches = false; break; } } if ( $matches ) { return $preset_key; } } return ''; } /** * Handles the one-click performance preset form submission. */ public function apply_performance_preset() { check_admin_referer( 'fastlayer_apply_preset_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $preset_key = isset( $_POST['preset'] ) ? sanitize_key( wp_unslash( $_POST['preset'] ) ) : ''; $presets = $this->get_performance_presets(); if ( '' === $preset_key || ! isset( $presets[ $preset_key ] ) ) { wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer&preset=invalid' ) ); exit; } $existing = wp_parse_args( get_option( $this->option_name, array() ), Bootstrap::get_default_options() ); $updated = array_merge( $existing, $presets[ $preset_key ]['settings'] ); $updated['_version'] = FASTLAYER_VERSION; update_option( $this->option_name, $updated ); Bootstrap::write_static_config( $updated ); Page_Cache::get_instance()->clear_all_cache(); $this->clear_minify_cache(); wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer&preset=' . rawurlencode( $preset_key ) ) ); exit; } /** * Handles the AJAX one-click performance preset application with real-time status. */ public function ajax_apply_performance_preset() { check_ajax_referer( 'fastlayer_apply_preset_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to change optimization settings.', 'fastlayer' ) ), 403 ); } $preset_key = isset( $_POST['preset'] ) ? sanitize_key( wp_unslash( $_POST['preset'] ) ) : ''; $presets = $this->get_performance_presets(); if ( '' === $preset_key || ! isset( $presets[ $preset_key ] ) ) { wp_send_json_error( array( 'message' => __( 'Invalid performance preset selected.', 'fastlayer' ) ), 400 ); } $preset_info = $presets[ $preset_key ]; $existing = wp_parse_args( get_option( $this->option_name, array() ), Bootstrap::get_default_options() ); $updated = array_merge( $existing, $preset_info['settings'] ); $updated['_version'] = FASTLAYER_VERSION; $saved = update_option( $this->option_name, $updated ); $config_written = Bootstrap::write_static_config( $updated ); Page_Cache::get_instance()->clear_all_cache(); $this->clear_minify_cache(); wp_send_json_success( array( 'preset_key' => $preset_key, 'preset_label' => $preset_info['label'], 'message' => sprintf( /* translators: %s: Preset name */ __( 'Preset "%s" applied and cache refreshed successfully!', 'fastlayer' ), $preset_info['label'] ), ) ); } /** * Exports all FastLayer settings as a JSON file download. * * Verifies nonce and capability, then streams the current options as * a JSON attachment so the user can back up their configuration. */ public function handle_export_settings() { check_admin_referer( 'fastlayer_export_settings_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( __( 'You do not have permission to export FastLayer settings.', 'fastlayer' ) ); } $defaults = Bootstrap::get_default_options(); $export_data = wp_parse_args( get_option( $this->option_name, array() ), $defaults ); $export_payload = array( 'plugin' => 'fastlayer', 'version' => FASTLAYER_VERSION, 'export_date' => current_time( 'mysql' ), 'settings' => $export_data, ); $json = wp_json_encode( $export_payload ); if ( false === $json ) { wp_die( __( 'Failed to prepare settings export.', 'fastlayer' ) ); } $filename = 'fastlayer-settings-' . date( 'Ymd-His' ) . '.json'; nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' ); header( 'Content-Length: ' . (string) strlen( $json ) ); header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' ); header( 'Expires: 0' ); echo $json; exit; } /** * Imports FastLayer settings from an uploaded JSON file. * * Verifies nonce and capability, validates the uploaded file is JSON, * sanitizes every setting against the plugin defaults, saves the merged * result, writes the static config, clears the cache, then redirects * back to the Dashboard with a success notice. */ public function handle_import_settings() { check_admin_referer( 'fastlayer_import_settings_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( __( 'You do not have permission to import FastLayer settings.', 'fastlayer' ) ); } if ( empty( $_FILES ) || ! isset( $_FILES['fastlayer_settings_file'] ) ) { wp_safe_redirect( add_query_arg( 'import_error', 'missing', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } $file = $_FILES['fastlayer_settings_file']; if ( UPLOAD_ERR_OK !== $file['error'] ) { wp_safe_redirect( add_query_arg( 'import_error', 'upload_failed', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } $mime_type = wp_check_filetype( $file['name'] ); if ( 'json' !== $mime_type['ext'] ) { wp_safe_redirect( add_query_arg( 'import_error', 'invalid_type', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } $content = file_get_contents( $file['tmp_name'] ); if ( false === $content ) { wp_safe_redirect( add_query_arg( 'import_error', 'read_failed', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } $decoded = json_decode( $content, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) { wp_safe_redirect( add_query_arg( 'import_error', 'invalid_json', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } // Accept both wrapper format { plugin, version, settings } and raw settings array. $imported_settings = isset( $decoded['settings'] ) && is_array( $decoded['settings'] ) ? $decoded['settings'] : $decoded; if ( ! is_array( $imported_settings ) ) { wp_safe_redirect( add_query_arg( 'import_error', 'invalid_settings', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } $defaults = Bootstrap::get_default_options(); $sanitized = $defaults; $checkbox_fields = array( 'enable_cache', 'enable_preload', 'enable_minify', 'enable_minify_css', 'enable_minify_js', 'enable_lazyload', 'enable_lazyload_iframes', 'enable_lazyload_videos', 'enable_smart_lcp_optimization', 'enable_lcp_preload', 'enable_lcp_hero_background', 'enable_lcp_debug_mode', 'enable_webp', 'enable_bulk_webp_generation', 'enable_image_delivery', 'enable_progressive_image_loading', 'enable_lqip_placeholders', 'enable_css_combine', 'enable_css_remove_comments', 'enable_css_remove_unused', 'enable_smart_gutenberg_css_optimization', 'enable_google_fonts_display', 'enable_css_inline', 'enable_css_delivery', 'enable_critical_css', 'enable_html_remove_comments', 'enable_html_remove_whitespace', 'enable_html_preserve_gutenberg_comments', 'enable_js_combine', 'enable_js_remove_comments', 'enable_js_defer', 'enable_js_async', 'enable_js_safe_mode', 'enable_js_smart_delay', 'enable_js_smart_delay_debug', 'debug_mode', 'enable_cdn', 'enable_imagekit_cdn', 'imagekit_auto_quality', 'imagekit_auto_format', 'imagekit_runtime_correction', 'imagekit_debug_overlay', 'db_clean_post_revisions', 'db_clean_trashed_posts', 'db_clean_spam_comments', 'db_clean_auto_drafts', 'db_clean_all_transients', 'db_clean_duplicated_postmeta', 'db_clean_duplicated_commentmeta', 'db_clean_duplicated_usermeta', 'db_clean_duplicated_termmeta', 'db_clean_expired_transients', 'db_clean_oembed_cache', 'db_clean_orphaned_postmeta', 'db_clean_orphaned_usermeta', 'db_clean_orphaned_termmeta', 'db_clean_orphaned_commentmeta', ); foreach ( $checkbox_fields as $field ) { if ( isset( $imported_settings[ $field ] ) ) { $sanitized[ $field ] = '1' === (string) $imported_settings[ $field ] ? '1' : '0'; } } $numeric_fields = array( 'cache_expiration' => array( 1, 720 ), 'smart_js_delay_timeout' => array( 1000, 15000 ), 'webp_quality' => array( 50, 100 ), 'lqip_quality' => array( 1, 50 ), 'lqip_width' => array( 20, 80 ), 'lqip_blur_intensity' => array( 0, 50 ), 'lqip_fade_duration' => array( 0, 1000 ), 'imagekit_max_oversize_ratio' => array( 1, 3 ), ); foreach ( $numeric_fields as $field => $range ) { if ( ! isset( $imported_settings[ $field ] ) ) { continue; } $val = absint( $imported_settings[ $field ] ); $min = $range[0]; $max = $range[1]; if ( $val < $min ) { $val = $min; } if ( $val > $max ) { $val = $max; } $sanitized[ $field ] = $val; } $textarea_fields = array( 'exclude_urls', 'exclude_cookies', 'exclude_user_agents', 'exclude_css_files', 'exclude_js_files', 'exclude_js_defer', 'smart_js_delay_exclusions', 'critical_css_custom', ); foreach ( $textarea_fields as $field ) { if ( isset( $imported_settings[ $field ] ) ) { $sanitized[ $field ] = sanitize_textarea_field( $imported_settings[ $field ] ); } } $url_fields = array( 'cdn_cname', 'imagekit_endpoint' ); foreach ( $url_fields as $field ) { if ( isset( $imported_settings[ $field ] ) ) { $sanitized[ $field ] = esc_url_raw( trim( $imported_settings[ $field ] ) ); } } $select_fields = array( 'imagekit_origin_path_mode' => array( 'full_uploads_path', 'uploads_relative_path' ), 'imagekit_auto_format_mode' => array( 'f-auto', 'fm-auto' ), ); foreach ( $select_fields as $field => $allowed ) { if ( isset( $imported_settings[ $field ] ) && in_array( $imported_settings[ $field ], $allowed, true ) ) { $sanitized[ $field ] = sanitize_key( $imported_settings[ $field ] ); } } $sanitized['_version'] = FASTLAYER_VERSION; $saved = update_option( $this->option_name, $sanitized ); if ( false === $saved ) { wp_safe_redirect( add_query_arg( 'import_error', 'save_failed', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } Bootstrap::write_static_config( $sanitized ); Page_Cache::get_instance()->clear_all_cache(); wp_safe_redirect( add_query_arg( 'import_success', '1', admin_url( 'admin.php?page=wp-fastlayer' ) ) ); exit; } /** * Fetches genuine real-time Google PageSpeed Insights score via Google's free REST API. */ public function ajax_fetch_pagespeed() { check_ajax_referer( 'fastlayer_pagespeed_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'fastlayer' ) ), 403 ); } $strategy = isset( $_POST['strategy'] ) && 'desktop' === sanitize_key( wp_unslash( $_POST['strategy'] ) ) ? 'desktop' : 'mobile'; $site_url = home_url( '/' ); // If local or private IP, return friendly notice $host = wp_parse_url( $site_url, PHP_URL_HOST ); if ( 'localhost' === $host || '127.0.0.1' === $host || substr( (string) $host, -6 ) === '.local' || substr( (string) $host, -5 ) === '.test' ) { wp_send_json_error( array( 'is_local' => true, 'message' => __( 'Google PageSpeed Insights API requires a live, publicly accessible domain. Local test environments cannot be accessed by Google servers directly.', 'fastlayer' ), ), 400 ); } $api_endpoint = add_query_arg( array( 'url' => rawurlencode( $site_url ), 'strategy' => $strategy, 'category' => 'performance', ), 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed' ); $response = wp_remote_get( $api_endpoint, array( 'timeout' => 60, 'user-agent' => 'FastLayer-WordPress-Plugin/' . FASTLAYER_VERSION, ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( array( 'message' => $response->get_error_message() ), 500 ); } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( empty( $data['lighthouseResult']['categories']['performance']['score'] ) ) { $api_error = isset( $data['error']['message'] ) ? $data['error']['message'] : __( 'Unable to retrieve audit score from Google.', 'fastlayer' ); wp_send_json_error( array( 'message' => $api_error ), 500 ); } $score = round( (float) $data['lighthouseResult']['categories']['performance']['score'] * 100 ); $audits = $data['lighthouseResult']['audits'] ?? array(); $fcp = isset( $audits['first-contentful-paint']['displayValue'] ) ? $audits['first-contentful-paint']['displayValue'] : ''; $lcp = isset( $audits['largest-contentful-paint']['displayValue'] ) ? $audits['largest-contentful-paint']['displayValue'] : ''; $cls = isset( $audits['cumulative-layout-shift']['displayValue'] ) ? $audits['cumulative-layout-shift']['displayValue'] : ''; $tbt = isset( $audits['total-blocking-time']['displayValue'] ) ? $audits['total-blocking-time']['displayValue'] : ''; $cached_result = array( 'strategy' => $strategy, 'score' => $score, 'fcp' => $fcp, 'lcp' => $lcp, 'cls' => $cls, 'tbt' => $tbt, 'checked_at' => current_time( 'mysql' ), ); update_option( 'fastlayer_pagespeed_last_audit_' . $strategy, $cached_result ); wp_send_json_success( $cached_result ); } public function clear_cache( $redirect_url = '' ) { check_admin_referer( 'fastlayer_clear_cache_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $cache_stats_before = $this->get_cache_stats(); $this->log_cache_clear( 'Cache clear started.', $cache_stats_before ); $invalidation = Page_Cache::get_instance(); $invalidation->clear_all_cache(); $minify_stats = $this->clear_minify_cache(); $preload_stats = $this->clear_preload_cache_artifacts(); if ( function_exists( 'delete_transient' ) ) { \delete_transient( 'fastlayer_cache_debug_trace' ); } $cache_stats_after = $this->get_cache_stats(); $this->log_cache_clear( 'Cache clear completed.', $cache_stats_after, $minify_stats, $preload_stats ); wp_safe_redirect( $this->get_safe_redirect_url( $redirect_url, admin_url( 'admin.php?page=wp-fastlayer&cache=cleared' ) ) ); exit; } public function manual_preload( $redirect_url = '' ) { check_admin_referer( 'fastlayer_preload_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $preload = Page_Cache::get_instance(); $status = $preload->preload_cache( 'manual' ); if ( function_exists( 'delete_transient' ) ) { \delete_transient( 'fastlayer_cache_debug_trace' ); } wp_safe_redirect( $this->get_safe_redirect_url( $redirect_url, admin_url( 'admin.php?page=wp-fastlayer&preload=' . rawurlencode( $status ) ) ) ); exit; } /** * Reports the live preload job state so the progress shown in the admin * reflects the job that is actually running rather than the moment the * request that started it returned. */ public function ajax_get_preload_status() { check_ajax_referer( 'fastlayer_preload_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to view the preload status.', 'fastlayer' ) ), 403 ); } wp_send_json_success( Page_Cache::get_instance()->get_preload_state() ); } public function ajax_clear_cache() { check_ajax_referer( 'fastlayer_clear_cache_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'fastlayer' ) ), 403 ); } $cache_stats_before = $this->get_cache_stats(); $this->log_cache_clear( 'AJAX Cache clear started.', $cache_stats_before ); $invalidation = Page_Cache::get_instance(); $invalidation->clear_all_cache(); $minify_stats = $this->clear_minify_cache(); $preload_stats = $this->clear_preload_cache_artifacts(); if ( function_exists( 'delete_transient' ) ) { \delete_transient( 'fastlayer_cache_debug_trace' ); } $cache_stats_after = $this->get_cache_stats(); $this->log_cache_clear( 'AJAX Cache clear completed.', $cache_stats_after, $minify_stats, $preload_stats ); wp_send_json_success( array( 'message' => __( 'All cache cleared successfully!', 'fastlayer' ), 'stats' => array( 'files' => number_format( $cache_stats_after['files'] ), 'size' => size_format( $cache_stats_after['size'] ), 'raw_files' => $cache_stats_after['files'], 'raw_size' => $cache_stats_after['size'], ), ) ); } public function ajax_manual_preload() { check_ajax_referer( 'fastlayer_preload_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'fastlayer' ) ), 403 ); } $preload = Page_Cache::get_instance(); $status = $preload->preload_cache( 'manual' ); if ( function_exists( 'delete_transient' ) ) { \delete_transient( 'fastlayer_cache_debug_trace' ); } $state = $preload->get_preload_state(); wp_send_json_success( array( 'message' => __( 'Cache preload started successfully!', 'fastlayer' ), 'status' => $status, 'state' => $state, ) ); } public function test_imagekit_connection() { check_admin_referer( 'fastlayer_test_imagekit_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $options = get_option( $this->option_name, array() ); $endpoint = isset( $options['imagekit_endpoint'] ) ? trim( (string) $options['imagekit_endpoint'] ) : ''; if ( '' === $endpoint ) { set_transient( 'fastlayer_imagekit_test_notice', array( 'type' => 'error', 'message' => __( 'ImageKit test failed: Endpoint is missing. Please enter your ImageKit URL Endpoint first.', 'fastlayer' ), ), 120 ); wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-cdn' ) ); exit; } if ( ! $this->is_valid_imagekit_endpoint( $endpoint ) ) { set_transient( 'fastlayer_imagekit_test_notice', array( 'type' => 'error', 'message' => __( 'ImageKit test failed: The endpoint format looks invalid. Use a full URL like https://ik.imagekit.io/your_endpoint', 'fastlayer' ), ), 120 ); wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-cdn' ) ); exit; } $test_url = $this->build_imagekit_test_url( $endpoint, $options ); if ( ! $this->is_permitted_imagekit_request_url( $test_url ) ) { set_transient( 'fastlayer_imagekit_test_notice', array( 'type' => 'error', 'message' => __( 'ImageKit test failed: The endpoint format looks invalid. Use a full URL like https://ik.imagekit.io/your_endpoint', 'fastlayer' ), ), 120 ); wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-cdn' ) ); exit; } $response = wp_safe_remote_head( $test_url, array( 'timeout' => 8, 'redirection' => 2, 'headers' => array( 'Accept' => 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', ), ) ); if ( is_wp_error( $response ) ) { $response = wp_safe_remote_get( $test_url, array( 'timeout' => 8, 'redirection' => 2, 'headers' => array( 'Accept' => 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', ), ) ); } if ( is_wp_error( $response ) ) { set_transient( 'fastlayer_imagekit_test_notice', array( 'type' => 'error', 'message' => sprintf( /* translators: %s: WP error from remote request. */ __( 'ImageKit test failed: %s', 'fastlayer' ), $response->get_error_message() ), ), 120 ); } else { $code = (int) wp_remote_retrieve_response_code( $response ); $mode = $this->get_imagekit_origin_path_mode( $options ); if ( $code >= 200 && $code < 500 ) { set_transient( 'fastlayer_imagekit_test_notice', array( 'type' => 'success', 'message' => sprintf( /* translators: 1: rewrite mode, 2: tested URL, 3: HTTP code. */ __( 'ImageKit test successful (%1$s): %2$s (HTTP %3$d).', 'fastlayer' ), $mode, $test_url, $code ), ), 120 ); } else { set_transient( 'fastlayer_imagekit_test_notice', array( 'type' => 'error', 'message' => sprintf( /* translators: 1: rewrite mode, 2: tested URL, 3: HTTP code. */ __( 'ImageKit test failed (%1$s): %2$s returned HTTP %3$d.', 'fastlayer' ), $mode, $test_url, $code ), ), 120 ); } } wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-cdn' ) ); exit; } public function generate_image_debug_report() { check_admin_referer( 'fastlayer_generate_image_debug_report_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $samples = get_option( 'fastlayer_image_runtime_debug_samples', array() ); $samples = is_array( $samples ) ? $samples : array(); $report = $this->build_image_runtime_debug_report( $samples ); set_transient( 'fastlayer_image_debug_report_payload', $report, 1800 ); wp_safe_redirect( add_query_arg( 'image_debug_report', 'generated', admin_url( 'admin.php?page=wp-fastlayer-cdn' ) ) ); exit; } public function download_image_debug_report() { check_admin_referer( 'fastlayer_download_image_debug_report_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $report = get_transient( 'fastlayer_image_debug_report_payload' ); if ( ! is_array( $report ) ) { $samples = get_option( 'fastlayer_image_runtime_debug_samples', array() ); $report = $this->build_image_runtime_debug_report( is_array( $samples ) ? $samples : array() ); } nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=wp-fastlayer-responsive-image-debug-report.json' ); echo wp_json_encode( $report, JSON_PRETTY_PRINT ); exit; } private function build_image_runtime_debug_report( array $samples ) { $summary = array( 'total_samples' => count( $samples ), 'oversized_images' => 0, 'runtime_corrections' => 0, 'max_oversize_ratio' => 0, 'avg_oversize_ratio' => 0, ); $ratio_total = 0.0; foreach ( $samples as $sample ) { if ( ! is_array( $sample ) ) { continue; } $ratio = isset( $sample['oversizeRatio'] ) ? (float) $sample['oversizeRatio'] : 0.0; $ratio_total += $ratio; if ( $ratio > 1.5 ) { $summary['oversized_images']++; } if ( ! empty( $sample['runtimeCorrectionApplied'] ) ) { $summary['runtime_corrections']++; } if ( $ratio > $summary['max_oversize_ratio'] ) { $summary['max_oversize_ratio'] = round( $ratio, 3 ); } } if ( $summary['total_samples'] > 0 ) { $summary['avg_oversize_ratio'] = round( $ratio_total / $summary['total_samples'], 3 ); } return array( 'generated_at' => current_time( 'mysql' ), 'generated_timestamp' => time(), 'summary' => $summary, 'samples' => array_values( $samples ), ); } private function is_valid_imagekit_endpoint( $endpoint ) { $endpoint = trim( (string) $endpoint ); if ( '' === $endpoint ) { return false; } if ( ! preg_match( '#^https?://#i', $endpoint ) ) { $endpoint = 'https://' . ltrim( $endpoint, '/' ); } if ( ! filter_var( $endpoint, FILTER_VALIDATE_URL ) ) { return false; } $parts = wp_parse_url( $endpoint ); if ( empty( $parts['host'] ) ) { return false; } return false !== filter_var( $parts['host'], FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME ); } private function is_permitted_imagekit_request_url( $url ) { $url = trim( (string) $url ); if ( '' === $url ) { return false; } $parsed = wp_parse_url( $url ); if ( ! is_array( $parsed ) || empty( $parsed['host'] ) || empty( $parsed['scheme'] ) ) { return false; } if ( ! in_array( strtolower( (string) $parsed['scheme'] ), array( 'http', 'https' ), true ) ) { return false; } if ( isset( $parsed['user'] ) || isset( $parsed['pass'] ) ) { return false; } $host = strtolower( (string) $parsed['host'] ); if ( false !== filter_var( $host, FILTER_VALIDATE_IP ) ) { return false; } if ( 'imagekit.io' !== $host && '.imagekit.io' !== substr( $host, -12 ) ) { return false; } return false !== filter_var( $host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME ); } private function build_imagekit_test_url( $endpoint, $options ) { $endpoint = trim( (string) $endpoint ); if ( ! preg_match( '#^https?://#i', $endpoint ) ) { $endpoint = 'https://' . ltrim( $endpoint, '/' ); } $endpoint = untrailingslashit( $endpoint ); $original_test_url = ''; $attachment = get_posts( array( 'post_type' => 'attachment', 'post_mime_type' => 'image', 'posts_per_page' => 1, 'post_status' => 'inherit', 'fields' => 'ids', ) ); if ( ! empty( $attachment ) ) { $attachment_url = $this->get_local_upload_attachment_url( (int) $attachment[0] ); if ( is_string( $attachment_url ) && '' !== $attachment_url ) { $original_test_url = $attachment_url; } } if ( '' === $original_test_url ) { $uploads = wp_upload_dir(); $baseurl = isset( $uploads['baseurl'] ) && is_string( $uploads['baseurl'] ) ? untrailingslashit( $uploads['baseurl'] ) : ''; $original_test_url = '' !== $baseurl ? $baseurl . '/demo.jpg' : content_url( 'uploads' ) . '/demo.jpg'; } $url = $this->build_imagekit_rewritten_url( $endpoint, $original_test_url, $options ); if ( '' === $url ) { $url = $endpoint . '/demo.jpg'; } return $this->append_imagekit_transform( $url, 400, $options ); } private function get_imagekit_origin_path_mode( $options ) { $mode = isset( $options['imagekit_origin_path_mode'] ) ? sanitize_key( (string) $options['imagekit_origin_path_mode'] ) : 'full_uploads_path'; return in_array( $mode, array( 'full_uploads_path', 'uploads_relative_path' ), true ) ? $mode : 'full_uploads_path'; } private function build_imagekit_rewritten_url( $endpoint, $original_url, $options ) { $endpoint = untrailingslashit( (string) $endpoint ); $parsed = wp_parse_url( (string) $original_url ); if ( '' === $endpoint || empty( $parsed['path'] ) ) { return ''; } if ( ! empty( $parsed['host'] ) && false !== stripos( (string) $parsed['host'], 'imagekit.io' ) ) { return ''; } $normalized_path = preg_replace( '#/+#', '/', (string) $parsed['path'] ); $normalized_path = is_string( $normalized_path ) ? $normalized_path : (string) $parsed['path']; $normalized_path = '/' . ltrim( $normalized_path, '/' ); $uploads = wp_upload_dir(); $uploads_path = wp_parse_url( isset( $uploads['baseurl'] ) ? $uploads['baseurl'] : '', PHP_URL_PATH ); $uploads_path = is_string( $uploads_path ) ? '/' . ltrim( untrailingslashit( $uploads_path ), '/' ) : ''; if ( '' === $uploads_path || 0 !== strpos( $normalized_path, $uploads_path ) ) { return ''; } $mode = $this->get_imagekit_origin_path_mode( $options ); $relative_path = ltrim( substr( $normalized_path, strlen( $uploads_path ) ), '/' ); $rewritten_path = 'uploads_relative_path' === $mode ? $relative_path : ltrim( $normalized_path, '/' ); $rewritten_path = $this->normalize_imagekit_rewritten_path( $rewritten_path, $mode ); $rewritten_path = $this->strip_endpoint_prefix_from_rewritten_path( $endpoint, $rewritten_path ); if ( '' === $rewritten_path ) { return ''; } return $this->build_imagekit_endpoint_url( $endpoint, $rewritten_path ); } private function append_imagekit_transform( $url, $width, $options ) { if ( '' === $url ) { return ''; } $transforms = array(); $format_transform = $this->get_imagekit_auto_format_transform( $options ); if ( '' !== $format_transform ) { $transforms[] = $format_transform; } if ( ! isset( $options['imagekit_auto_quality'] ) || '1' === (string) $options['imagekit_auto_quality'] ) { $transforms[] = 'q-auto'; } $transforms[] = 'w-' . max( 1, absint( $width ) ); return str_replace( '%2C', ',', add_query_arg( 'tr', implode( ',', $transforms ), $url ) ); } private function get_imagekit_auto_format_transform( $options ) { if ( isset( $options['imagekit_auto_format'] ) && '1' === (string) $options['imagekit_auto_format'] ) { if ( isset( $options['imagekit_auto_format_mode'] ) && 'fm-auto' === $options['imagekit_auto_format_mode'] ) { return 'fm-auto'; } return 'f-auto'; } return ''; } private function get_imagekit_debug_data( $imagekit_url ) { $data = array( 'final_url' => $imagekit_url, 'accept_header' => 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', 'content_type' => 'n/a', 'cache_status' => 'n/a', 'cache_control' => 'n/a', 'vary_header' => 'n/a', 'detected_format' => 'unknown', 'modern_format_detected'=> false, 'http_code' => 'n/a', 'is_background_image' => false, 'normalized_path' => '', 'duplicate_path_segments'=> array(), 'path_warning' => '', 'error' => '', ); if ( ! $this->is_permitted_imagekit_request_url( $imagekit_url ) ) { $data['error'] = __( 'No ImageKit URL available for testing.', 'fastlayer' ); return $data; } $parsed_debug_url = wp_parse_url( $imagekit_url ); $path_for_debug = isset( $parsed_debug_url['path'] ) ? (string) $parsed_debug_url['path'] : ''; $normalized_path = preg_replace( '#/+#', '/', $path_for_debug ); $normalized_path = is_string( $normalized_path ) ? $normalized_path : $path_for_debug; $data['normalized_path'] = $normalized_path; $data['duplicate_path_segments'] = $this->detect_duplicate_path_segments( $normalized_path ); if ( ! empty( $data['duplicate_path_segments'] ) ) { $data['path_warning'] = __( 'Duplicate path segments detected in rewritten URL path.', 'fastlayer' ); } $response = wp_safe_remote_get( $imagekit_url, array( 'timeout' => 10, 'redirection' => 2, 'headers' => array( 'Accept' => $data['accept_header'], ), ) ); if ( is_wp_error( $response ) ) { $data['error'] = $response->get_error_message(); return $data; } $headers = wp_remote_retrieve_headers( $response ); $content_type = wp_remote_retrieve_header( $response, 'content-type' ); $cache_header = wp_remote_retrieve_header( $response, 'x-cache' ); if ( empty( $cache_header ) ) { $cache_header = wp_remote_retrieve_header( $response, 'cf-cache-status' ); } if ( empty( $cache_header ) ) { $cache_header = wp_remote_retrieve_header( $response, 'x-imagekit-cache' ); } $cache_control = wp_remote_retrieve_header( $response, 'cache-control' ); $vary_header = wp_remote_retrieve_header( $response, 'vary' ); $data['http_code'] = (string) wp_remote_retrieve_response_code( $response ); $data['content_type'] = $content_type ? (string) $content_type : 'n/a'; $data['cache_status'] = $cache_header ? (string) $cache_header : 'n/a'; $data['cache_control'] = $cache_control ? (string) $cache_control : 'n/a'; $data['vary_header'] = $vary_header ? (string) $vary_header : 'n/a'; $data['detected_format'] = $this->detect_image_format_from_content_type( $data['content_type'] ); $data['modern_format_detected'] = in_array( $data['detected_format'], array( 'webp', 'avif' ), true ); if ( is_object( $headers ) && method_exists( $headers, 'getAll' ) ) { $all_headers = $headers->getAll(); foreach ( array( 'x-cache', 'cf-cache-status', 'x-imagekit-cache', 'cache-control', 'vary' ) as $header_name ) { if ( isset( $all_headers[ $header_name ] ) && 'n/a' === $data['cache_status'] ) { $data['cache_status'] = (string) $all_headers[ $header_name ]; } } } if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { \fastlayer_debug_log( '[WP FastLayer ImageKit] imagekit_request_url=' . $data['final_url'] . ' | browser_accept_header=' . $data['accept_header'] . ' | final_content_type=' . $data['content_type'] . ' | modern_format_detected=' . ( $data['modern_format_detected'] ? 'yes' : 'no' ) . ' | imagekit_cache_status=' . $data['cache_status'] ); } return $data; } private function detect_image_format_from_content_type( $content_type ) { $content_type = strtolower( (string) $content_type ); if ( false !== strpos( $content_type, 'image/avif' ) ) { return 'avif'; } if ( false !== strpos( $content_type, 'image/webp' ) ) { return 'webp'; } if ( false !== strpos( $content_type, 'image/png' ) ) { return 'png'; } if ( false !== strpos( $content_type, 'image/jpeg' ) || false !== strpos( $content_type, 'image/jpg' ) ) { return 'jpg'; } return 'unknown'; } public function reset_safe_mode_settings() { check_admin_referer( 'fastlayer_reset_safe_mode_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $defaults = Bootstrap::get_default_options(); $safe_options = array_merge( $defaults, array( 'enable_cache' => '1', 'enable_minify' => '1', 'enable_minify_css' => '1', 'enable_minify_js' => '1', 'enable_lazyload' => '1', 'enable_instant_click' => '1', 'enable_js_defer' => '0', 'enable_js_async' => '0', 'enable_js_smart_delay' => '0', 'enable_css_combine' => '0', 'enable_js_combine' => '0', ) ); update_option( $this->option_name, $safe_options ); Bootstrap::write_static_config( $safe_options ); $this->clear_cache(); wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer&preset=safe' ) ); exit; } public function clear_css_js_cache( $redirect_url = '' ) { check_admin_referer( 'fastlayer_clear_cache_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $cache_stats_before = $this->get_cache_stats(); $this->log_cache_clear( 'CSS/JS cache clear started.', $cache_stats_before ); $minify_stats = $this->clear_minify_cache(); if ( function_exists( 'delete_transient' ) ) { \delete_transient( 'fastlayer_cache_debug_trace' ); } $cache_stats_after = $this->get_cache_stats(); $this->log_cache_clear( 'CSS/JS cache clear completed.', $cache_stats_after, $minify_stats ); wp_safe_redirect( $this->get_safe_redirect_url( $redirect_url, admin_url( 'admin.php?page=wp-fastlayer&css_js_cache=cleared' ) ) ); exit; } public function generate_critical_css() { check_admin_referer( 'fastlayer_generate_critical_css_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } $css = $this->build_critical_css_sample(); if ( $css ) { $options = get_option( $this->option_name, array() ); $options['critical_css_custom'] = wp_kses_post( $css ); update_option( $this->option_name, $options ); wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-css&critical_css=generated' ) ); exit; } wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-css&critical_css=error' ) ); exit; } public function bulk_generate_webp() { check_admin_referer( 'fastlayer_bulk_webp_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die(); } if ( ! $this->is_bulk_webp_generation_enabled() ) { wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-media' ) ); exit; } $webp = Webp::get_instance(); if ( ! $webp->is_webp_supported() ) { wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-media&webp_bulk=unsupported' ) ); exit; } if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- large bulk operation legitimately needs the default time limit lifted; guarded by function_exists( 'set_time_limit' ). } $count = $webp->bulk_generate_webp(); if ( $count > 0 ) { wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-media&webp_bulk=success&count=' . absint( $count ) ) ); } else { wp_safe_redirect( admin_url( 'admin.php?page=wp-fastlayer-media&webp_bulk=error' ) ); } exit; } public function ajax_bulk_generate_webp_batch() { check_ajax_referer( 'fastlayer_bulk_webp_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to perform this action.', 'fastlayer' ) ), 403 ); } if ( ! $this->is_bulk_webp_generation_enabled() ) { wp_send_json_error( array( 'message' => __( 'Bulk WebP generation is currently disabled. Existing generated WebP files will continue working normally.', 'fastlayer' ), ), 403 ); } $webp = Webp::get_instance(); if ( ! $webp->is_webp_supported() ) { wp_send_json_error( array( 'message' => __( 'WebP conversion is not supported on this server.', 'fastlayer' ), 'support' => $webp->get_webp_support_details() ) ); } $page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1; $batch_size = isset( $_POST['batch_size'] ) ? absint( $_POST['batch_size'] ) : 20; $batch_size = $batch_size > 0 ? min( 50, $batch_size ) : 20; $page = $page > 0 ? $page : 1; $result = $webp->bulk_generate_webp_batch( $page, $batch_size ); wp_send_json_success( $result ); } public function ajax_get_webp_summary() { check_ajax_referer( 'fastlayer_bulk_webp_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to view WebP summary data.', 'fastlayer' ) ), 403 ); } wp_send_json_success( Webp::get_instance()->get_conversion_summary() ); } public function ajax_save_webp_last_run() { check_ajax_referer( 'fastlayer_bulk_webp_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to save WebP run data.', 'fastlayer' ) ), 403 ); } $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'success'; $duration = isset( $_POST['duration'] ) ? absint( $_POST['duration'] ) : 0; Webp::get_instance()->save_last_run_summary( $status, $duration ); wp_send_json_success( Webp::get_instance()->get_conversion_summary() ); } private function build_critical_css_sample() { $home_url = home_url( '/' ); $response = wp_remote_get( $home_url, array( 'timeout' => 30 ) ); if ( is_wp_error( $response ) ) { return ''; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return ''; } preg_match_all( '/]+rel=["\']stylesheet["\'][^>]*>/i', $body, $matches ); if ( empty( $matches[0] ) ) { return ''; } $css = ''; foreach ( $matches[0] as $link_tag ) { if ( preg_match( '/href=["\']([^"\']+)["\']/', $link_tag, $href_match ) ) { $stylesheet_url = esc_url_raw( $href_match[1] ); if ( $this->is_internal_url( $stylesheet_url ) ) { $path = $this->url_to_path( $stylesheet_url ); if ( $path && is_readable( $path ) ) { $css .= file_get_contents( $path ) . "\n"; } } } if ( strlen( $css ) > 20000 ) { break; } } $css = preg_replace( '!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css ); $css = preg_replace( '/\s+/', ' ', $css ); return trim( substr( $css, 0, 20000 ) ); } private function is_internal_url( $url ) { $parsed = wp_parse_url( $url ); if ( empty( $parsed['host'] ) ) { return true; } $site = wp_parse_url( site_url() ); return isset( $site['host'] ) && strtolower( $parsed['host'] ) === strtolower( $site['host'] ); } private function url_to_path( $url ) { return Bootstrap::url_to_path( $url ); } private function get_cache_stats() { $stats = array( 'files' => 0, 'size' => 0, 'html_files' => 0, 'gzip_files' => 0, 'webp_files' => 0, 'mobile_files' => 0, 'desktop_files' => 0, 'cache_dirs' => array(), 'cache_dir_found' => false, ); $cache_dirs = $this->detect_cache_directories(); if ( empty( $cache_dirs ) && defined( 'FASTLAYER_CACHE_DIR' ) && is_dir( FASTLAYER_CACHE_DIR ) ) { $cache_dirs[] = trailingslashit( wp_normalize_path( FASTLAYER_CACHE_DIR ) ); } $stats['cache_dirs'] = $cache_dirs; if ( empty( $cache_dirs ) ) { $this->log_cache_stats_debug( $stats ); return $stats; } $stats['cache_dir_found'] = true; $visited_paths = array(); foreach ( $cache_dirs as $cache_dir ) { $real_cache_dir = realpath( $cache_dir ); if ( false === $real_cache_dir ) { if ( is_dir( $cache_dir ) ) { $real_cache_dir = wp_normalize_path( untrailingslashit( $cache_dir ) ); } else { continue; } } if ( isset( $visited_paths[ $real_cache_dir ] ) ) { continue; } $visited_paths[ $real_cache_dir ] = true; $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $real_cache_dir, \RecursiveDirectoryIterator::SKIP_DOTS ), \RecursiveIteratorIterator::SELF_FIRST ); foreach ( $iterator as $file ) { if ( ! $file->isFile() ) { continue; } $file_size = (int) $file->getSize(); if ( $file_size <= 0 ) { continue; } $path = str_replace( '\\', '/', $file->getPathname() ); $filename = strtolower( $file->getFilename() ); $extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); if ( preg_match( '/\.(tmp|temp|part|lock)$/', $filename ) || preg_match( '/(^|\/)tmp(\/|$)/', $path ) ) { continue; } $stats['files']++; $stats['size'] += $file_size; if ( in_array( $extension, array( 'html', 'htm' ), true ) ) { $stats['html_files']++; } if ( in_array( $extension, array( 'gz', 'gzip' ), true ) ) { $stats['gzip_files']++; } if ( 'webp' === $extension ) { $stats['webp_files']++; } if ( false !== strpos( $path, '/mobile/' ) || false !== strpos( $filename, 'mobile' ) ) { $stats['mobile_files']++; } if ( false !== strpos( $path, '/desktop/' ) || false !== strpos( $filename, 'desktop' ) ) { $stats['desktop_files']++; } } } // Keep the main dashboard card focused on real cached page artifacts. if ( $stats['html_files'] > 0 ) { $stats['files'] = $stats['html_files']; } $this->log_cache_stats_debug( $stats ); return $stats; } private function detect_cache_directories() { $candidates = array(); if ( defined( 'FASTLAYER_CACHE_DIR' ) ) { $candidates[] = trailingslashit( wp_normalize_path( FASTLAYER_CACHE_DIR ) ); } if ( defined( 'WP_CONTENT_DIR' ) ) { $candidates[] = WP_CONTENT_DIR . '/cache/wp-fastlayer/'; $pattern = WP_CONTENT_DIR . '/cache/*fastlayer*'; $matched_dirs = glob( $pattern, GLOB_ONLYDIR ); if ( is_array( $matched_dirs ) ) { foreach ( $matched_dirs as $matched_dir ) { $candidates[] = trailingslashit( $matched_dir ); } } // Discover real cache path by scanning for md5-style generated cache files. $cache_root = WP_CONTENT_DIR . '/cache/'; if ( is_dir( $cache_root ) ) { $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $cache_root, \RecursiveDirectoryIterator::SKIP_DOTS ), \RecursiveIteratorIterator::SELF_FIRST ); foreach ( $iterator as $item ) { if ( ! $item->isFile() ) { continue; } $name = strtolower( $item->getFilename() ); if ( ! preg_match( '/^[a-f0-9]{32}\.(html|htm|gz|gzip)$/', $name ) ) { continue; } $candidates[] = trailingslashit( wp_normalize_path( dirname( $item->getPathname() ) ) ); } } } $valid_dirs = array(); $seen = array(); foreach ( $candidates as $candidate ) { $normalized = wp_normalize_path( untrailingslashit( (string) $candidate ) ); if ( '' === $normalized || isset( $seen[ $normalized ] ) ) { continue; } $seen[ $normalized ] = true; if ( is_dir( $normalized ) ) { $valid_dirs[] = trailingslashit( $normalized ); } } return $valid_dirs; } private function log_cache_stats_debug( $stats ) { if ( ! class_exists( 'FastLayer\\Logger' ) ) { return; } $payload = array( 'cache_dirs' => isset( $stats['cache_dirs'] ) ? $stats['cache_dirs'] : array(), 'cache_dir_found' => ! empty( $stats['cache_dir_found'] ), 'cached_pages' => isset( $stats['files'] ) ? (int) $stats['files'] : 0, 'total_size_bytes' => isset( $stats['size'] ) ? (int) $stats['size'] : 0, 'html_files' => isset( $stats['html_files'] ) ? (int) $stats['html_files'] : 0, 'gzip_files' => isset( $stats['gzip_files'] ) ? (int) $stats['gzip_files'] : 0, 'webp_files' => isset( $stats['webp_files'] ) ? (int) $stats['webp_files'] : 0, 'mobile_files' => isset( $stats['mobile_files'] ) ? (int) $stats['mobile_files'] : 0, 'desktop_files' => isset( $stats['desktop_files'] ) ? (int) $stats['desktop_files'] : 0, 'calculated_size' => isset( $stats['size'] ) ? size_format( (int) $stats['size'] ) : '0 B', ); Bootstrap::get_instance()->log( '[Dashboard Cache Stats] ' . wp_json_encode( $payload ), 'debug' ); } private function clear_minify_cache() { $upload_dir = wp_upload_dir(); $min_dir = trailingslashit( $upload_dir['basedir'] ) . 'wp-fastlayer-min/'; return $this->clear_cache_directory( $min_dir ); } private function get_safe_redirect_url( $redirect_url, $fallback ) { $redirect_url = trim( (string) $redirect_url ); $redirect_url = esc_url_raw( $redirect_url ); $redirect_url = wp_validate_redirect( $redirect_url, $fallback ); if ( empty( $redirect_url ) ) { return $fallback; } return $redirect_url; } private function clear_preload_cache_artifacts() { $upload_dir = wp_upload_dir(); $results = array( 'files_deleted' => 0, 'bytes_deleted' => 0, 'dirs' => array(), ); $candidate_dirs = array( trailingslashit( $upload_dir['basedir'] ) . 'wp-fastlayer-preload/', ); foreach ( $candidate_dirs as $dir ) { if ( is_dir( $dir ) ) { $results['dirs'][] = $dir; $cleared = $this->clear_cache_directory( $dir ); $results['files_deleted'] += $cleared['files_deleted']; $results['bytes_deleted'] += $cleared['bytes_deleted']; } } return $results; } private function clear_cache_directory( $directory ) { $stats = array( 'files_deleted' => 0, 'bytes_deleted' => 0, ); if ( ! is_dir( $directory ) ) { return $stats; } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $directory, \RecursiveDirectoryIterator::SKIP_DOTS ), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $iterator as $item ) { if ( $item->isFile() ) { $stats['bytes_deleted'] += (int) $item->getSize(); $stats['files_deleted']++; wp_delete_file( $item->getPathname() ); continue; } if ( $item->isDir() ) { Bootstrap::delete_empty_directory( $item->getPathname() ); } } return $stats; } private function log_cache_clear( $message, $cache_stats, $minify_stats = null, $upload_stats = null ) { if ( ! class_exists( 'FastLayer\\Logger' ) ) { return; } $payload = array( 'message' => $message, 'cache_stats' => $cache_stats, 'minify_stats' => $minify_stats, 'upload_stats' => $upload_stats, ); Bootstrap::get_instance()->log( '[Cache Clear] ' . wp_json_encode( $payload ), 'cache' ); } public function enqueue_admin_assets( $hook ) { if ( ! $this->is_fastlayer_admin_hook( $hook ) ) { return; } wp_enqueue_style( 'dashicons' ); wp_enqueue_style( 'fastlayer-admin', FASTLAYER_URL . 'assets/css/admin.css', array(), FASTLAYER_VERSION ); wp_enqueue_script( 'fastlayer-admin', FASTLAYER_URL . 'assets/js/admin.js', array( 'jquery' ), FASTLAYER_VERSION, true ); $uploads = wp_upload_dir(); $uploads_path = wp_parse_url( isset( $uploads['baseurl'] ) ? $uploads['baseurl'] : '', PHP_URL_PATH ); $uploads_path = is_string( $uploads_path ) ? '/' . ltrim( untrailingslashit( $uploads_path ), '/' ) : '/wp-content/uploads'; wp_localize_script( 'fastlayer-admin', 'fastLayerAdmin', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'fastlayer_bulk_webp_nonce' ), 'clear_cache_nonce' => wp_create_nonce( 'fastlayer_clear_cache_nonce' ), 'preload_nonce' => wp_create_nonce( 'fastlayer_preload_nonce' ), 'bulk_button_text' => __( 'Generate WebP for Existing Images', 'fastlayer' ), 'bulk_cancel_text' => __( 'Cancel Conversion', 'fastlayer' ), 'bulk_running' => __( 'Starting WebP conversion...', 'fastlayer' ), 'bulk_processing' => __( 'Preparing next batch...', 'fastlayer' ), /* translators: %current%: current image number; %total%: total number of images. */ 'bulk_progress_template' => __( 'Converting image %current% of %total%', 'fastlayer' ), /* translators: %converted%: number of images already converted; %total%: total number of images. */ 'bulk_progress_count_template' => __( '%converted% / %total% converted', 'fastlayer' ), 'bulk_success' => __( 'WebP generation completed.', 'fastlayer' ), 'bulk_success_notice' => __( 'WebP generation completed successfully.', 'fastlayer' ), 'bulk_error' => __( 'WebP generation failed.', 'fastlayer' ), 'bulk_error_notice' => __( 'WebP generation stopped before completion. Please review the error details and try again.', 'fastlayer' ), 'bulk_ready' => __( 'Ready', 'fastlayer' ), 'bulk_running_label' => __( 'Running', 'fastlayer' ), 'bulk_completed_label' => __( 'Completed', 'fastlayer' ), 'bulk_failed_label' => __( 'Error', 'fastlayer' ), 'bulk_cancelled_label' => __( 'Cancelled', 'fastlayer' ), 'bulk_empty_notice' => __( 'No eligible JPEG or PNG images were found for conversion.', 'fastlayer' ), 'bulk_empty_message' => __( 'No images needed WebP conversion.', 'fastlayer' ), 'bulk_cancel_requested' => __( 'Cancelling WebP conversion after the current request...', 'fastlayer' ), 'bulk_cancelled_notice' => __( 'WebP conversion was cancelled. Partial progress has been saved.', 'fastlayer' ), 'bulk_cancelled_message' => __( 'WebP conversion cancelled.', 'fastlayer' ), 'bulk_percentage_template' => __( '%percent%% complete', 'fastlayer' ), 'import_choose_file' => __( 'Choose JSON file', 'fastlayer' ), /* translators: %converted%: converted count; %failed%: failed count; %remaining%: remaining count. These are client-side JavaScript template tokens, not PHP sprintf placeholders. */ 'bulk_summary_template' => __( '%converted% converted, %failed% failed, %remaining% remaining.', 'fastlayer' ), // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText -- client-side JavaScript template tokens; reordering them as sprintf placeholders would break the JS substitution. 'bulk_refreshing' => __( 'Refreshing conversion stats...', 'fastlayer' ), 'webp_summary_loading' => __( 'Loading conversion summary...', 'fastlayer' ), 'webp_summary_error' => __( 'Unable to load current conversion summary.', 'fastlayer' ), 'webp_summary_updated' => __( 'Conversion summary updated.', 'fastlayer' ), 'webp_summary_coverage' => __( '%percent%% of eligible images already have WebP files.', 'fastlayer' ), 'webp_last_run_template' => __( 'Last run: %time%', 'fastlayer' ), /* translators: %duration%: duration of the last WebP conversion run. */ 'webp_last_duration_template' => __( 'Duration: %duration%', 'fastlayer' ), /* translators: %status%: status of the last WebP conversion run. */ 'webp_last_status_template' => __( 'Status: %status%', 'fastlayer' ), 'bulk_pause_text' => __( 'Pause Conversion', 'fastlayer' ), 'bulk_resume_text' => __( 'Resume Conversion', 'fastlayer' ), 'bulk_pausing' => __( 'Pausing after the current batch...', 'fastlayer' ), 'bulk_resuming' => __( 'Resuming WebP conversion...', 'fastlayer' ), 'bulk_paused_label' => __( 'Paused', 'fastlayer' ), 'bulk_paused_message' => __( 'WebP conversion is paused. Resume when you are ready.', 'fastlayer' ), 'bulk_log_started' => __( 'WebP conversion started. Processing batches in the background.', 'fastlayer' ), 'bulk_log_pause_requested' => __( 'Pause requested. Current batch will complete before stopping.', 'fastlayer' ), 'bulk_log_paused' => __( 'Conversion paused after the current batch.', 'fastlayer' ), 'bulk_log_resumed' => __( 'Resuming conversion from the remaining images.', 'fastlayer' ), 'bulk_log_batch_started' => __( 'Starting batch %page% (%processed% expected).', 'fastlayer' ), /* translators: %page%: batch page number; %processed%: images processed; %converted%: images converted; %failed%: images that failed. These are client-side JavaScript template tokens, not PHP sprintf placeholders. */ 'bulk_log_batch_completed' => __( 'Batch %page% completed: %processed% processed, %converted% converted, %failed% failed.', 'fastlayer' ), // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText -- client-side JavaScript template tokens; reordering them as sprintf placeholders would break the JS substitution. 'bulk_batch_progress_template' => __( 'Batch %page% progress: %processed% / %total%', 'fastlayer' ), /* translators: %page%: batch page number; %count%: total number of batches; %current%: current image number; %total%: total number of images. These are client-side JavaScript template tokens, not PHP sprintf placeholders. */ 'bulk_status_template' => __( 'Processing batch %page% of %count% - Converting image %current% of %total%', 'fastlayer' ), // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText -- client-side JavaScript template tokens; reordering them as sprintf placeholders would break the JS substitution. /* translators: %eta%: estimated time remaining for the WebP conversion. */ 'bulk_eta_template' => __( 'Estimated time remaining: %eta%', 'fastlayer' ), 'bulk_eta_unavailable' => __( 'ETA unavailable until conversion starts.', 'fastlayer' ), 'bulk_view_logs_text' => __( 'View Detailed Logs', 'fastlayer' ), 'bulk_hide_logs_text' => __( 'Hide Detailed Logs', 'fastlayer' ), 'db_cleanup_processing' => __( 'Cleaning...', 'fastlayer' ), 'db_cleanup_default' => __( 'Clean Now', 'fastlayer' ), 'db_cleanup_action' => 'fastlayer_database_cleanup', 'db_cleanup_stats_action' => 'fastlayer_database_stats', 'db_cleanup_nonce' => wp_create_nonce( 'fastlayer_optimize_database_nonce' ), 'db_cleanup_modal_title' => __( 'Database cleanup in progress', 'fastlayer' ), 'db_cleanup_modal_starting' => __( 'Starting database cleanup...', 'fastlayer' ), 'db_cleanup_modal_running' => __( 'Removing selected records and optimizing tables. Please keep this tab open.', 'fastlayer' ), 'db_cleanup_modal_finalizing' => __( 'Refreshing database statistics...', 'fastlayer' ), 'db_cleanup_modal_success_title' => __( 'Database cleanup complete', 'fastlayer' ), 'db_cleanup_modal_error_title' => __( 'Database cleanup failed', 'fastlayer' ), /* translators: %s: number of database rows removed. */ 'db_cleanup_modal_rows_removed' => __( 'Records removed: %s', 'fastlayer' ), 'db_cleanup_modal_no_items' => __( 'No database cleanup action was selected, so nothing was removed.', 'fastlayer' ), 'db_cleanup_modal_stats_refreshed' => __( 'All cleanup counters now reflect fresh database queries.', 'fastlayer' ), 'db_cleanup_modal_close' => __( 'Close', 'fastlayer' ), 'db_cleanup_modal_retry' => __( 'Try Again', 'fastlayer' ), 'db_cleanup_modal_result_header' => __( 'Cleanup results', 'fastlayer' ), 'db_cleanup_modal_continue' => __( 'Refreshing page...', 'fastlayer' ), 'db_cleanup_running' => __( 'Database cleanup already running', 'fastlayer' ), 'preload_status_action' => 'fastlayer_preload_status', 'preload_status_nonce' => wp_create_nonce( 'fastlayer_preload_nonce' ), 'preload_status_queued' => __( 'Queued. Warming pages in the background...', 'fastlayer' ), 'preload_status_running' => __( 'Running. Warming pages in the background...', 'fastlayer' ), 'preload_status_completed' => __( 'Completed.', 'fastlayer' ), 'preload_status_completed_with_errors' => __( 'Completed with errors.', 'fastlayer' ), 'preload_status_failed' => __( 'Failed.', 'fastlayer' ), 'preload_status_never_run' => __( 'No preload job has been run yet.', 'fastlayer' ), 'preload_status_progress_template' => __( '%processed% of %total% pages processed', 'fastlayer' ), /* translators: %succeeded%: number of pages warmed successfully. */ 'preload_status_warmed_template' => __( '%succeeded% warmed', 'fastlayer' ), /* translators: %failed%: number of pages that failed to warm. */ 'preload_status_failed_template' => __( '%failed% failed', 'fastlayer' ), /* translators: %url%: page URL; %reason%: reason the page could not be warmed. */ 'preload_status_error_row_template' => __( '%url% (%reason%)', 'fastlayer' ), 'preload_status_errors_heading' => __( 'Pages that could not be warmed', 'fastlayer' ), 'copy_text' => __( 'Copy Example', 'fastlayer' ), 'copied_text' => __( 'Copied', 'fastlayer' ), 'copy_failed_text' => __( 'Copy failed', 'fastlayer' ), 'imagekit_uploads_path' => $uploads_path, 'imagekit_preview_missing_endpoint' => __( 'Enter ImageKit endpoint to generate preview URL.', 'fastlayer' ), 'imagekit_preview_invalid' => __( 'Preview unavailable for this URL and mode combination.', 'fastlayer' ), 'preset_action' => 'fastlayer_apply_preset_ajax', 'preset_nonce' => wp_create_nonce( 'fastlayer_apply_preset_nonce' ), 'preset_confirm_title' => __( 'Apply Performance Preset', 'fastlayer' ), 'preset_confirm_desc' => __( 'This will optimize caching, CSS/JS delivery, and script execution tailored for this profile. Any custom exclusions and CDN settings will be preserved.', 'fastlayer' ), 'preset_applying' => __( 'Applying preset settings...', 'fastlayer' ), 'preset_clearing_cache' => __( 'Purging existing page & asset caches...', 'fastlayer' ), 'preset_syncing_config' => __( 'Writing static drop-in configuration...', 'fastlayer' ), 'preset_success_title' => __( 'Preset Activated Successfully!', 'fastlayer' ), 'preset_error_title' => __( 'Failed to Apply Preset', 'fastlayer' ), 'preset_btn_apply' => __( 'Confirm & Apply', 'fastlayer' ), 'preset_btn_close' => __( 'Done', 'fastlayer' ), 'preset_btn_cancel' => __( 'Cancel', 'fastlayer' ), ) ); } /** * Reports the outcome of the request that started a preload job. * * A job no longer finishes inside the request that starts it, so the notice * only reports what actually happened at that point: whether it was queued, * refused because one was already running, or skipped. Claiming success here * would report a warming job that had not warmed anything yet. */ private function render_preload_result_notice() { $result = isset( $_GET['preload'] ) ? sanitize_key( wp_unslash( $_GET['preload'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. if ( '' === $result ) { return; } switch ( $result ) { case 'started': $type = 'notice-info'; $message = __( 'Cache preload started in the background. Progress is shown below and you can safely leave this page.', 'fastlayer' ); break; case 'busy': $type = 'notice-warning'; $message = __( 'A cache preload job is already running, so this request did not start a second one.', 'fastlayer' ); break; case 'disabled': $type = 'notice-warning'; $message = __( 'Cache preloading is disabled, so nothing was queued.', 'fastlayer' ); break; case 'error': $type = 'notice-error'; $message = __( 'The cache preload job could not be started.', 'fastlayer' ); break; default: return; } printf( '

%2$s

', esc_attr( $type ), esc_html( $message ) ); } /** * Renders the live preload job state. The markup carries the raw numbers and * the localized labels so the same panel can be refreshed in place while the * job runs, instead of only showing the state at page load. */ private function render_preload_status_panel() { $state = Page_Cache::get_instance()->get_preload_state(); $status = isset( $state['status'] ) ? sanitize_key( $state['status'] ) : ''; $total = isset( $state['total'] ) ? (int) $state['total'] : 0; $processed = isset( $state['processed'] ) ? (int) $state['processed'] : 0; $succeeded = isset( $state['succeeded'] ) ? (int) $state['succeeded'] : 0; $failed = isset( $state['failed'] ) ? (int) $state['failed'] : 0; $message = isset( $state['message'] ) ? (string) $state['message'] : ''; $errors = ( isset( $state['errors'] ) && is_array( $state['errors'] ) ) ? $state['errors'] : array(); $is_active = in_array( $status, array( 'queued', 'running' ), true ); $labels = array( 'queued' => __( 'Queued', 'fastlayer' ), 'running' => __( 'Running', 'fastlayer' ), 'completed' => __( 'Completed', 'fastlayer' ), 'completed_with_errors' => __( 'Completed with errors', 'fastlayer' ), 'failed' => __( 'Failed', 'fastlayer' ), 'stopped' => __( 'Paused', 'fastlayer' ), ); $status_label = isset( $labels[ $status ] ) ? $labels[ $status ] : __( 'Not started', 'fastlayer' ); $percentage = ( $total > 0 ) ? min( 100, round( ( $processed / $total ) * 100 ) ) : 0; ?>

—

,

  • ()
option_name, array() ); $cache_stats = $this->get_cache_stats(); $cache_notice = isset( $_GET['cache'] ) ? sanitize_text_field( wp_unslash( $_GET['cache'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. $css_js_cache_notice = isset( $_GET['css_js_cache'] ) ? sanitize_text_field( wp_unslash( $_GET['css_js_cache'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. $preset_notice = isset( $_GET['preset'] ) ? sanitize_key( wp_unslash( $_GET['preset'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. $presets = $this->get_performance_presets(); $active_preset = $this->detect_active_preset( $options ); ?>

render_preload_result_notice(); ?> render_preload_status_panel(); ?>

__( 'The uploaded file is not valid JSON.', 'fastlayer' ), 'invalid_type' => __( 'The uploaded file is not a valid .json file.', 'fastlayer' ), 'upload_failed' => __( 'The settings file could not be uploaded.', 'fastlayer' ), 'read_failed' => __( 'The settings file could not be read.', 'fastlayer' ), 'invalid_settings' => __( 'The settings file does not contain valid FastLayer settings.', 'fastlayer' ), 'save_failed' => __( 'The imported settings could not be saved.', 'fastlayer' ), 'missing' => __( 'No settings file was provided.', 'fastlayer' ), ); echo esc_html( $error_messages[ $import_error ] ?? __( 'There was an error importing the settings file.', 'fastlayer' ) ); ?>

render_page_header( 'FastLayer', '', '' ); ?>

h

$preset_data ) : ?>

= 90 ? esc_html__( 'Good Performance', 'fastlayer' ) : esc_html__( 'Needs Optimization', 'fastlayer' ) ) : esc_html__( 'No Audit Yet', 'fastlayer' ); ?>
>
FCP
LCP
CLS

Features

Dashboard

View cache statistics, storage usage and quick optimization actions from a single dashboard.

HTML Optimization

Minify HTML output, remove unnecessary comments and reduce page size.

CSS Optimization

Minify CSS, generate Critical CSS and improve render performance.

JavaScript Optimization

Minify, defer and delay JavaScript safely without breaking functionality.

Media Optimization

WebP conversion, LQIP placeholders, lazy loading and smart LCP optimization.

CDN

Configure CDN hostname and ImageKit integration for faster asset delivery.

Database

Clean revisions, transients, orphan metadata and optimize your database safely.

render_preset_modal(); ?>
option_name, array() ); ?>
render_page_header( __( 'Cache Settings', 'fastlayer' ), __( 'Manage page caching and preload behavior for your site.', 'fastlayer' ) ); ?>
option_name ); ?>

Page Cache

Cache your pages to serve static HTML files to your visitors, reducing the load on your server.

How long the cache files should be kept before being automatically cleared.

Cache Preloading

Automatically preload the cache after it's been cleared. This simulates a visit to your site to create the cache files.

render_preload_result_notice(); ?> render_preload_status_panel(); ?> render_save_bar(); ?>

Temporary Cache Debug

This panel is for temporary diagnostics only. It displays the last cache flow trace and allows a direct filesystem test.

Cache directory
Directory writable Yes' : 'No'; ?>
Last cache hook
Last URL
Last skip reason
Last save result
Last cache file path
Last debug timestamp
Filesystem test result Write:
Read:
Delete:
Detail: No test result available.
option_name, array() ); ?>
render_page_header( __( 'HTML Optimization', 'fastlayer' ), __( 'Minify and compress HTML output to reduce page size.', 'fastlayer' ) ); ?>
option_name ); ?>

so block markup remains intact.', 'fastlayer' ); ?>

render_save_bar(); ?>
option_name, array() ); ?>
render_page_header( __( 'CSS Optimization', 'fastlayer' ), __( 'Optimize CSS delivery, combine files and manage critical CSS.', 'fastlayer' ), '#' ); ?>
option_name ); ?>

CSS Minification render_help_icon( 'Minify CSS removes whitespace and comments to reduce file size and speed up CSS delivery.' ) ); ?>

Minify CSS files to reduce their size and improve page load time.

CSS File Optimization render_help_icon( 'Optimize delivery, combine, and inline CSS while excluding files as needed for compatibility.' ) ); ?>

Optimize CSS delivery by preloading styles and loading them non-blocking. This improves rendering while keeping compatibility with dynamic CSS.

Attempt to reduce unused CSS by using critical CSS and optimized delivery. Keep this enabled only if you have custom critical CSS in place.

Unload unused Gutenberg block CSS on pages that do not use block editor styles.

Append display=swap to Google Fonts URLs for safe text font loading, while skipping Material Icons and symbols.

Combine multiple CSS files into a single file to reduce HTTP requests while preserving media-specific styles.

Remove comments from CSS files to further reduce file size.

Inline critical CSS above the fold for faster perceived page load time.

Inline the critical CSS needed for above-the-fold rendering to speed up the first paint.

Paste your critical CSS here. You can also generate a homepage sample to get started.

Specify URLs of CSS files to be excluded from minification (one per line).
Internal: The domain part of the URL will be stripped automatically. Use (.*).css wildcards to exclude all CSS files located at a specific path.
3rd Party: Use either the full URL path or only the domain name, to exclude external CSS. More info

render_save_bar(); ?>
option_name, array() ); $preload_fonts_list = isset( $options['preload_fonts_list'] ) ? (string) $options['preload_fonts_list'] : ''; $font_count = ! empty( trim( $preload_fonts_list ) ) ? count( array_filter( array_map( 'trim', explode( "\n", $preload_fonts_list ) ) ) ) : 0; ?>
render_page_header( __( 'Fonts & Typography', 'fastlayer' ), __( 'Eliminate render-blocking webfont delays, preconnect Google Fonts, and enforce font-display: swap for Elementor & custom fonts.', 'fastlayer' ), '#' ); ?>
option_name ); ?>

1,100ms+

into the document . Fonts are downloaded in parallel with initial HTML, ensuring they are ready before first paint.', 'fastlayer' ); ?>

render_save_bar(); ?>
option_name, array() ); ?>
render_page_header( __( 'JavaScript Optimization', 'fastlayer' ), __( 'Minify and defer/async JavaScript to reduce render-blocking.', 'fastlayer' ), '#' ); ?>
option_name ); ?>

JavaScript Minification render_help_icon( 'Minify JS by removing comments and whitespace to reduce file size and speed up page load.' ) ); ?>

Minify JavaScript files to reduce their size and improve page load time.

JavaScript File Optimization render_help_icon( 'Control combination, defer, and exclusions for JavaScript files to avoid render-blocking and compatibility issues.' ) ); ?>

Specify URLs of JavaScript files to be excluded from minification and concatenation (one per line).
/wp-content/themes/some-theme/(.*).js
Internal: The domain part of the URL will be stripped automatically. Use (.*).js wildcards to exclude all JS files located at a specific path.
3rd Party: Use either the full URL path or only the domain name, to exclude external JS. More info

Combine multiple JavaScript files into a single file to reduce HTTP requests. For compatibility and best results, this option is disabled when delay javascript execution is enabled.

Remove comments from JavaScript files to further reduce file size.

Load JavaScript deferred eliminates render-blocking JS on your site and can improve load time. More info

Use async loading for eligible scripts to improve execution speed. Safe mode and exclusions are still respected.

Specify URLs or keywords of JavaScript files to be excluded from defer (one per line). Also, please check our documentation for a list of compatibility exclusions.

Safe Mode prevents all internal scripts from being delayed.

Delay only eligible non-critical scripts until the first user interaction. This mode keeps critical dependencies, Elementor, WooCommerce checkout/cart, and navigation scripts running immediately.

If no interaction happens, delayed scripts are restored automatically after this timeout.

One pattern per line. Supports handle-based exclusions with handle:keyword, URL keywords, and regex patterns (for example /gtag|analytics/i or regex:/hotjar|clarity/i).

Log delayed, excluded, and restored scripts with timing details in the browser console using the [WP FastLayer Delay] prefix.

One-click exclusions: When using the Delay JavaScript feature, you might notice some elements in the viewport take time to appear. If needed, add keywords or URLs for plugins, themes, analytics, payment processors, or other services below to ensure they are not delayed.

render_save_bar(); ?>
option_name, array() ); $bulk_webp_enabled = $this->is_bulk_webp_generation_enabled( $options ); $webp_bulk = isset( $_GET['webp_bulk'] ) ? sanitize_text_field( wp_unslash( $_GET['webp_bulk'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. $webp_count = isset( $_GET['count'] ) ? absint( $_GET['count'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. $webp_summary = Webp::get_instance()->get_conversion_summary(); ?>
render_page_header( __( 'Media Optimization', 'fastlayer' ), __( 'Configure WebP conversion and lazy-loading for images and media.', 'fastlayer' ) ); ?> get_webp_support_details(); ?>


get_webp_support_details(); ?>


option_name ); ?>

Image Optimization

Automatically convert JPEG and PNG images to WebP format for better compression and faster loading.

Serve optimized responsive image sizes and WebP variants based on frontend display dimensions.

Choose the WebP quality level for new image conversions. Higher values preserve more detail.

Use local LQIP placeholders for smoother transitions while optimized images load.

Create tiny blurred WebP placeholders for local images during upload and bulk conversion.

Set the placeholder width in pixels for low-resolution blurred previews.

Lower values reduce placeholder file size while preserving basic blur shapes.

Control the blur effect applied to the low-quality placeholder background.

Define the transition duration used for image loading placeholders.

Detect one primary above-the-fold LCP image or hero background and prioritize it safely without disabling lazyload globally.

Add one preload hint only for the detected primary LCP image/background to improve discovery timing.

Allow hero and Elementor background-image sections near the top to be considered for single-image LCP preload.

Log detected LCP element, preload URL, and attribute decisions with the [WP FastLayer LCP] prefix.

Bulk WebP Generation

Disable bulk WebP generation if your hosting environment, storage limits, or third-party image/CDN setup already handles optimization.

%

Lazy Load Images

Lazy load images to improve page load time by only loading images when they enter the viewport.

Lazy Load Iframes & Videos

Lazy load iframes (YouTube, Vimeo, etc.) to improve initial page load time.

Lazy load HTML5 videos to improve initial page load time.

render_save_bar(); ?>
option_name, array() ); $imagekit_endpoint = isset( $options['imagekit_endpoint'] ) ? trim( (string) $options['imagekit_endpoint'] ) : ''; $imagekit_enabled = ! empty( $options['enable_imagekit_cdn'] ); $max_oversize_ratio = isset( $options['imagekit_max_oversize_ratio'] ) ? (float) $options['imagekit_max_oversize_ratio'] : 1.5; $image_debug_report = get_transient( 'fastlayer_image_debug_report_payload' ); $imagekit_mode = $this->get_imagekit_origin_path_mode( $options ); $preview_original = $this->get_imagekit_preview_original_url(); $preview_rewritten = $this->build_imagekit_rewritten_url( $imagekit_endpoint, $preview_original, $options ); $preview_rewritten = $this->append_imagekit_transform( $preview_rewritten, 400, $options ); $imagekit_debug = $this->get_imagekit_debug_data( $preview_rewritten ); $imagekit_valid = '' !== $imagekit_endpoint && $this->is_valid_imagekit_endpoint( $imagekit_endpoint ); $status_label = __( 'Not Configured', 'fastlayer' ); $status_class = 'wpfl-imagekit-status-not-configured'; if ( '' !== $imagekit_endpoint && ! $imagekit_valid ) { $status_label = __( 'Invalid Endpoint', 'fastlayer' ); $status_class = 'wpfl-imagekit-status-invalid'; } elseif ( $imagekit_valid ) { $status_label = __( 'Connected', 'fastlayer' ); $status_class = 'wpfl-imagekit-status-connected'; } $test_url = wp_nonce_url( admin_url( 'admin-post.php?action=fastlayer_test_imagekit' ), 'fastlayer_test_imagekit_nonce' ); $generate_debug_report_url = wp_nonce_url( admin_url( 'admin-post.php?action=fastlayer_generate_image_debug_report' ), 'fastlayer_generate_image_debug_report_nonce' ); $download_debug_report_url = wp_nonce_url( admin_url( 'admin-post.php?action=fastlayer_download_image_debug_report' ), 'fastlayer_download_image_debug_report_nonce' ); ?>
render_page_header( __( 'CDN & Media Delivery', 'fastlayer' ), __( 'Accelerate global asset delivery and real-time image optimization with CDN and ImageKit.', 'fastlayer' ) ); ?>
option_name ); ?>

1

2

URL Endpoint.', 'fastlayer' ); ?>

https://ik.imagekit.io/demo
3

' . esc_html__( 'Yes (Next-Gen)', 'fastlayer' ) . '' : '' . esc_html__( 'Standard', 'fastlayer' ) . ''; ?>

1

2

3

render_save_bar(); ?>
'attachment', 'post_mime_type' => 'image', 'posts_per_page' => 1, 'post_status' => 'inherit', 'fields' => 'ids', ) ); if ( ! empty( $attachment ) ) { $attachment_url = $this->get_local_upload_attachment_url( (int) $attachment[0] ); if ( is_string( $attachment_url ) && '' !== $attachment_url ) { return $attachment_url; } } $uploads = wp_upload_dir(); $baseurl = isset( $uploads['baseurl'] ) && is_string( $uploads['baseurl'] ) ? untrailingslashit( $uploads['baseurl'] ) : ''; if ( '' !== $baseurl ) { return $baseurl . '/demo.jpg'; } return content_url( 'uploads' ) . '/demo.jpg'; } private function get_local_upload_attachment_url( $attachment_id ) { $attachment_id = absint( $attachment_id ); if ( $attachment_id <= 0 ) { return ''; } $relative_file = get_post_meta( $attachment_id, '_wp_attached_file', true ); if ( ! is_string( $relative_file ) || '' === $relative_file ) { return ''; } $relative_file = ltrim( (string) preg_replace( '#/+#', '/', $relative_file ), '/' ); $uploads = wp_upload_dir(); $basedir = isset( $uploads['basedir'] ) && is_string( $uploads['basedir'] ) ? untrailingslashit( $uploads['basedir'] ) : ''; $baseurl = isset( $uploads['baseurl'] ) && is_string( $uploads['baseurl'] ) ? untrailingslashit( $uploads['baseurl'] ) : ''; if ( '' === $baseurl || '' === $basedir ) { return ''; } $candidate_file = $basedir . '/' . $relative_file; if ( ! file_exists( $candidate_file ) ) { return ''; } return $baseurl . '/' . $relative_file; } private function build_imagekit_endpoint_url( $endpoint, $rewritten_path ) { $endpoint = untrailingslashit( (string) $endpoint ); $endpoint_path = wp_parse_url( $endpoint, PHP_URL_PATH ); $endpoint_segments = is_string( $endpoint_path ) ? array_values( array_filter( explode( '/', trim( $endpoint_path, '/' ) ) ) ) : array(); $path_segments = array_values( array_filter( explode( '/', trim( (string) $rewritten_path, '/' ) ) ) ); if ( ! empty( $endpoint_segments ) && count( $path_segments ) >= count( $endpoint_segments ) ) { $prefix = array_slice( $path_segments, 0, count( $endpoint_segments ) ); if ( implode( '/', $prefix ) === implode( '/', $endpoint_segments ) ) { $path_segments = array_slice( $path_segments, count( $endpoint_segments ) ); } } return $endpoint . '/' . implode( '/', $path_segments ); } private function normalize_imagekit_rewritten_path( $rewritten_path, $mode ) { $path = preg_replace( '#/+#', '/', (string) $rewritten_path ); $path = is_string( $path ) ? $path : (string) $rewritten_path; $path = ltrim( $path, '/' ); if ( 'uploads_relative_path' === $mode ) { $path = preg_replace( '#^wp-content/uploads/#i', '', $path ); $path = preg_replace( '#^uploads/#i', '', $path ); } return trim( (string) preg_replace( '#(^|/)uploads/uploads(/|$)#i', '$1uploads$2', $path ), '/' ); } private function strip_endpoint_prefix_from_rewritten_path( $endpoint, $rewritten_path ) { $endpoint_path = wp_parse_url( (string) $endpoint, PHP_URL_PATH ); if ( ! is_string( $endpoint_path ) || '' === $endpoint_path || '/' === $endpoint_path ) { return trim( (string) $rewritten_path, '/' ); } $endpoint_segments = array_values( array_filter( explode( '/', trim( $endpoint_path, '/' ) ) ) ); $path_segments = array_values( array_filter( explode( '/', trim( (string) $rewritten_path, '/' ) ) ) ); if ( empty( $endpoint_segments ) || count( $path_segments ) < count( $endpoint_segments ) ) { return trim( (string) $rewritten_path, '/' ); } $prefix = array_slice( $path_segments, 0, count( $endpoint_segments ) ); if ( implode( '/', $prefix ) === implode( '/', $endpoint_segments ) ) { $path_segments = array_slice( $path_segments, count( $endpoint_segments ) ); } return implode( '/', $path_segments ); } private function detect_duplicate_path_segments( $path ) { $segments = array_values( array_filter( explode( '/', trim( (string) $path, '/' ) ) ) ); $duplicates = array(); for ( $i = 1, $count = count( $segments ); $i < $count; $i++ ) { if ( strtolower( $segments[ $i ] ) === strtolower( $segments[ $i - 1 ] ) ) { $duplicates[] = $segments[ $i ]; } } return array_values( array_unique( $duplicates ) ); } public function render_advanced_page() { $options = get_option( $this->option_name, array() ); ?>
render_page_header( __( 'Advanced Rules', 'fastlayer' ), __( 'Exclude URLs, cookies and user agents from caching for compatibility.', 'fastlayer' ) ); ?>
option_name ); ?>

Never Cache URLs

Never Cache Cookies

Never Cache User Agents

Debug Mode

render_save_bar(); ?>
option_name, array() ); ?>
render_page_header( __( 'Import / Export Settings', 'fastlayer' ), __( 'Back up your FastLayer configuration as a JSON file, or restore it on another site.', 'fastlayer' ) ); ?>

__( 'The uploaded file is not valid JSON.', 'fastlayer' ), 'invalid_type' => __( 'The uploaded file is not a valid .json file.', 'fastlayer' ), 'upload_failed' => __( 'The settings file could not be uploaded.', 'fastlayer' ), 'read_failed' => __( 'The settings file could not be read.', 'fastlayer' ), 'invalid_settings' => __( 'The settings file does not contain valid FastLayer settings.', 'fastlayer' ), 'save_failed' => __( 'The imported settings could not be saved.', 'fastlayer' ), 'missing' => __( 'No settings file was provided.', 'fastlayer' ), ); echo esc_html( $error_messages[ $import_error ] ?? __( 'There was an error importing the settings file.', 'fastlayer' ) ); ?>

  • 1
  • 2
  • 3

option_name, array() ); $db_optimization = Database_Optimization::get_instance(); $db_size = $db_optimization->get_database_size(); $table_count = $db_optimization->get_table_count(); $cleanup_stats = $db_optimization->get_cleanup_statistics(); $cleanup_counts = isset( $cleanup_stats['counts'] ) && is_array( $cleanup_stats['counts'] ) ? $cleanup_stats['counts'] : array(); $transient_breakdown = isset( $cleanup_stats['transients'] ) && is_array( $cleanup_stats['transients'] ) ? $cleanup_stats['transients'] : $db_optimization->get_transient_count_breakdown(); /** * Resolves the current count of a single cleanup item from the shared * statistics snapshot so the page and the post-cleanup AJAX refresh * always use exactly the same queries. */ $cleanup_item_count = static function ( $item_id ) use ( $cleanup_counts, $db_optimization ) { if ( isset( $cleanup_counts[ $item_id ] ) ) { return (int) $cleanup_counts[ $item_id ]; } return 0; }; $cleanup_notice = array(); $cleanup_notice_key = 'fastlayer_db_cleanup_notice_' . get_current_user_id(); $stored_cleanup_notice = get_transient( $cleanup_notice_key ); if ( is_array( $stored_cleanup_notice ) ) { $cleanup_notice = $stored_cleanup_notice; delete_transient( $cleanup_notice_key ); } $db_optimized = isset( $_GET['db_optimized'] ) ? sanitize_text_field( wp_unslash( $_GET['db_optimized'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice/deep-link flag produced by the plugin's own wp_safe_redirect(); no state changes. $cleanup_items = array( array( 'id' => 'db_clean_post_revisions', 'label' => 'Post Revisions', 'count' => $cleanup_item_count( 'db_clean_post_revisions' ), 'description' => 'Remove old post revisions' ), array( 'id' => 'db_clean_trashed_posts', 'label' => 'Trashed Posts', 'count' => $cleanup_item_count( 'db_clean_trashed_posts' ), 'description' => 'Remove posts currently in trash' ), array( 'id' => 'db_clean_spam_comments', 'label' => 'Spam Comments', 'count' => $cleanup_item_count( 'db_clean_spam_comments' ), 'description' => 'Remove comments marked as spam' ), array( 'id' => 'db_clean_auto_drafts', 'label' => 'Auto Drafts', 'count' => $cleanup_item_count( 'db_clean_auto_drafts' ), 'description' => 'Remove auto-draft posts created by WordPress' ), array( 'id' => 'db_clean_all_transients', 'label' => 'All Transients', 'count' => $cleanup_item_count( 'db_clean_all_transients' ), /* translators: %timeout%: timeout rows; %active%: active runtime rows; %expired%: expired timeout rows; %no_timeout%: runtime rows without a timeout; %orphans%: orphaned timeout rows. These are client-side JavaScript template tokens, not PHP sprintf placeholders. */ 'description' => __( 'Removable candidates only. Timeout rows: %timeout%, Active runtime: %active%, Expired timeout rows: %expired%, Runtime no-timeout rows: %no_timeout%, Orphan timeout rows: %orphans%', 'fastlayer' ) // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText -- client-side JavaScript template tokens; reordering them as sprintf placeholders would break the JS substitution. ), array( 'id' => 'db_clean_duplicated_postmeta', 'label' => 'Duplicated Posts metadata', 'count' => $cleanup_item_count( 'db_clean_duplicated_postmeta' ), 'description' => 'Remove duplicate post metadata' ), array( 'id' => 'db_clean_duplicated_commentmeta', 'label' => 'Duplicated Comments metadata', 'count' => $cleanup_item_count( 'db_clean_duplicated_commentmeta' ), 'description' => 'Remove duplicate comment metadata' ), array( 'id' => 'db_clean_duplicated_usermeta', 'label' => 'Duplicated User metadata', 'count' => $cleanup_item_count( 'db_clean_duplicated_usermeta' ), 'description' => 'Remove duplicate user metadata' ), array( 'id' => 'db_clean_duplicated_termmeta', 'label' => 'Duplicated Term metadata', 'count' => $cleanup_item_count( 'db_clean_duplicated_termmeta' ), 'description' => 'Remove duplicate term metadata' ), array( 'id' => 'db_clean_expired_transients', 'label' => 'Expired Transients', 'count' => $cleanup_item_count( 'db_clean_expired_transients' ), 'description' => 'Remove expired transient options' ), array( 'id' => 'db_clean_oembed_cache', 'label' => 'oEmbed in Posts metadata', 'count' => $cleanup_item_count( 'db_clean_oembed_cache' ), 'description' => 'Clean oEmbed cache in post metadata' ), array( 'id' => 'db_clean_orphaned_postmeta', 'label' => 'Orphaned Posts metadata', 'count' => $cleanup_item_count( 'db_clean_orphaned_postmeta' ), 'description' => 'Remove metadata for deleted posts' ), array( 'id' => 'db_clean_orphaned_usermeta', 'label' => 'Orphan User metadata', 'count' => $cleanup_item_count( 'db_clean_orphaned_usermeta' ), 'description' => 'Remove metadata for deleted users' ), array( 'id' => 'db_clean_orphaned_termmeta', 'label' => 'Orphan Term metadata', 'count' => $cleanup_item_count( 'db_clean_orphaned_termmeta' ), 'description' => 'Remove metadata for deleted terms' ), array( 'id' => 'db_clean_orphaned_commentmeta', 'label' => 'Orphaned Comments metadata', 'count' => $cleanup_item_count( 'db_clean_orphaned_commentmeta' ), 'description' => 'Remove metadata for deleted comments' ) ); ?>

render_page_header( __( 'Database Options', 'fastlayer' ), __( 'Optimize your database and clean up unused items safely.', 'fastlayer' ) ); ?>
option_name ); ?>

Clean All

Enable all cleanup options at once

format_transient_breakdown( $item['description'], $transient_breakdown ) ); ?>

render_save_bar(); ?>
render_database_cleanup_modal(); ?>
isset( $transient_breakdown['timeout_rows'] ) ? (int) $transient_breakdown['timeout_rows'] : 0, '%active%' => isset( $transient_breakdown['active_runtime_rows'] ) ? (int) $transient_breakdown['active_runtime_rows'] : 0, '%expired%' => isset( $transient_breakdown['expired_timeout_rows'] ) ? (int) $transient_breakdown['expired_timeout_rows'] : 0, '%no_timeout%' => isset( $transient_breakdown['runtime_no_timeout_rows'] ) ? (int) $transient_breakdown['runtime_no_timeout_rows'] : 0, '%orphans%' => isset( $transient_breakdown['orphan_timeout_rows'] ) ? (int) $transient_breakdown['orphan_timeout_rows'] : 0, ); return str_replace( array_keys( $map ), array_map( 'number_format_i18n', array_values( $map ) ), (string) $template ); } /** * Progress overlay shown while the database cleanup request is running. * * The cleanup runs as a single synchronous request, so the progress bar is * intentionally indeterminate: it communicates that work is in progress * without reporting a percentage that the backend cannot provide. */ private function render_database_cleanup_modal() { ?>