'array', 'sanitize_callback' => array( 'I365_PO_Plugin', 'sanitize_settings' ), 'default' => I365_PO_Plugin::defaults(), ) ); } /** * Enqueue admin assets. * * @param string $hook Current page hook. * * @return void */ public static function enqueue_assets( $hook ) { if ( 'settings_page_i365-po-settings' !== $hook ) { return; } wp_enqueue_style( 'i365-po-admin', plugin_dir_url( I365_PO_PLUGIN_FILE ) . 'assets/admin.css', array(), I365_PO_VERSION ); wp_enqueue_script( 'i365-po-admin', plugin_dir_url( I365_PO_PLUGIN_FILE ) . 'assets/admin.js', array(), I365_PO_VERSION, true ); $hero_guess = ''; $hero_path = get_theme_file_path( 'assets/images/hero.webp' ); if ( $hero_path && file_exists( $hero_path ) ) { $hero_guess = get_theme_file_uri( 'assets/images/hero.webp' ); } $font_guess = self::guess_font_url(); wp_localize_script( 'i365-po-admin', 'I365PODetect', array( 'home' => home_url(), 'stylesheet' => get_stylesheet_uri(), 'hero' => $hero_guess, 'font' => $font_guess, 'fontsCdn' => 'https://fonts.gstatic.com', 'nonce' => wp_create_nonce( 'i365-po-detect' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'messages' => array( 'running' => __( 'Detecting from homepage…', '365i-performance-optimizer' ), 'failed' => __( 'Detection failed. Check your homepage is reachable.', '365i-performance-optimizer' ), 'completed'=> __( 'Detection updated fields.', '365i-performance-optimizer' ), ), ) ); // Utilities nonce and messages for backup/restore, profiles, import/export. wp_localize_script( 'i365-po-admin', 'I365POUtilities', array( 'nonce' => wp_create_nonce( 'i365-po-utilities' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'messages' => array( 'confirmRestore' => __( 'Restore this backup? Current settings will be backed up first.', '365i-performance-optimizer' ), 'confirmDeleteBackup' => __( 'Delete this backup permanently?', '365i-performance-optimizer' ), 'confirmApplyProfile' => __( 'Apply this profile? Current settings will be backed up first.', '365i-performance-optimizer' ), 'confirmDeleteProfile' => __( 'Delete this custom profile?', '365i-performance-optimizer' ), 'confirmImport' => __( 'Import these settings? Current settings will be backed up first.', '365i-performance-optimizer' ), 'profileNameRequired' => __( 'Please enter a profile name.', '365i-performance-optimizer' ), 'selectFile' => __( 'Please select a JSON file to import.', '365i-performance-optimizer' ), 'invalidFile' => __( 'Please select a valid JSON file.', '365i-performance-optimizer' ), 'processing' => __( 'Processing…', '365i-performance-optimizer' ), 'error' => __( 'An error occurred. Please try again.', '365i-performance-optimizer' ), ), ) ); } /** * Render settings page. * * @return void */ public static function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $settings = I365_PO_Plugin::get_settings(); ?>

i

i

i

i

i

i

i

i

0 ) : ?>

i

i

__( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $response = wp_remote_get( home_url(), array( 'timeout' => 10, ) ); if ( is_wp_error( $response ) ) { wp_send_json_error( array( 'message' => $response->get_error_message() ) ); } $code = (int) wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); if ( $code >= 400 || empty( $body ) ) { wp_send_json_error( array( 'message' => __( 'Homepage could not be fetched.', '365i-performance-optimizer' ) ) ); } $result = array( 'preconnect' => array_filter( array( home_url(), 'https://fonts.gstatic.com' ) ), 'stylesheet' => '', 'font' => '', 'hero' => '', ); libxml_use_internal_errors( true ); $dom = new \DOMDocument(); $loaded = $dom->loadHTML( $body ); libxml_clear_errors(); if ( $loaded ) { $result['stylesheet'] = self::pick_best_stylesheet( $dom ); $font_pick = self::pick_best_font_v2( $dom ); $result['font'] = $font_pick['primary']; $result['font_list'] = $font_pick['candidates']; $result['hero'] = self::pick_best_image( $dom ); } $result['stylesheet'] = self::strip_query( $result['stylesheet'] ); $result['font'] = self::strip_query( $result['font'] ); $result['hero'] = self::strip_query( $result['hero'] ); if ( ! empty( $result['stylesheet'] ) ) { $response_code = wp_remote_retrieve_response_code( wp_remote_head( $result['stylesheet'], array( 'timeout' => 6, 'redirection' => 1 ) ) ); if ( $response_code >= 400 ) { $result['stylesheet'] = ''; } } wp_send_json_success( $result ); } /** * Normalize relative URL to absolute. * * @param string $url URL. * * @return string */ private static function make_absolute_url( $url ) { if ( empty( $url ) ) { return ''; } // Protocol-relative. if ( 0 === strpos( $url, '//' ) ) { return set_url_scheme( $url, is_ssl() ? 'https' : 'http' ); } $parts = wp_parse_url( $url ); if ( ! empty( $parts['scheme'] ) ) { return $url; } if ( ! empty( $parts['path'] ) && '/' === $parts['path'][0] ) { return home_url( $parts['path'] ); } return trailingslashit( home_url() ) . ltrim( $url, '/' ); } /** * Remove query string for preload targets. * * @param string $url URL. * @return string */ private static function strip_query( $url ) { if ( empty( $url ) ) { return ''; } $parts = wp_parse_url( $url ); if ( empty( $parts['query'] ) ) { return $url; } $queryless = $parts['scheme'] . '://' . $parts['host']; if ( ! empty( $parts['port'] ) ) { $queryless .= ':' . $parts['port']; } if ( ! empty( $parts['path'] ) ) { $queryless .= $parts['path']; } return $queryless; } /** * Choose the most likely primary stylesheet. * * @param \DOMDocument $dom DOM. * @return string */ private static function pick_best_stylesheet( \DOMDocument $dom ) { $links = $dom->getElementsByTagName( 'link' ); $best = ''; $best_s = -INF; $host = wp_parse_url( home_url(), PHP_URL_HOST ); foreach ( $links as $idx => $link ) { $rel = strtolower( $link->getAttribute( 'rel' ) ); $href = $link->getAttribute( 'href' ); if ( false === strpos( $rel, 'stylesheet' ) ) { continue; } if ( empty( $href ) ) { continue; } $url = self::make_absolute_url( $href ); $score = 0 - $idx; // earlier is slightly preferred. $target_host = wp_parse_url( $url, PHP_URL_HOST ); if ( $target_host && $host && $target_host === $host ) { $score += 20; } if ( strpos( $url, 'dashicons' ) !== false || strpos( $url, 'emoji' ) !== false ) { $score -= 50; } if ( strpos( $url, 'googleapis' ) !== false || strpos( $url, 'gstatic' ) !== false ) { $score -= 10; } if ( $score > $best_s ) { $best_s = $score; $best = $url; } } return $best; } /** * Choose the most likely primary font preload or font file. * * @param \DOMDocument $dom DOM. * @return string */ private static function pick_best_font( \DOMDocument $dom ) { $links = $dom->getElementsByTagName( 'link' ); $best = ''; $best_s = -INF; $host = wp_parse_url( home_url(), PHP_URL_HOST ); $google_fonts = ''; $candidates = array(); foreach ( $links as $idx => $link ) { $rel = strtolower( $link->getAttribute( 'rel' ) ); $href = $link->getAttribute( 'href' ); $as = strtolower( $link->getAttribute( 'as' ) ); if ( empty( $href ) ) { continue; } $url = self::make_absolute_url( $href ); // Track Google Fonts stylesheet as fallback (remote or local Elementor cache). if ( empty( $google_fonts ) && ( false !== strpos( $url, 'fonts.googleapis.com' ) || false !== strpos( $url, 'google-fonts/css' ) ) ) { $google_fonts = $url; } if ( false === stripos( $rel, 'preload' ) && ! preg_match( '/\\.woff2?$/i', $url ) ) { continue; } if ( stripos( $rel, 'preload' ) !== false && 'font' !== $as ) { continue; } $score = 0 - $idx; if ( preg_match( '/\\.woff2$/i', $url ) ) { $score += 20; } elseif ( preg_match( '/\\.woff$/i', $url ) ) { $score += 10; } $target_host = wp_parse_url( $url, PHP_URL_HOST ); if ( $target_host ) { // Only allow same-host or fonts.gstatic.com. if ( $host && $target_host === $host ) { $score += 10; } elseif ( false !== strpos( $target_host, 'fonts.gstatic.com' ) ) { $score += 15; } else { continue; } } // Avoid icon fonts. if ( strpos( $url, 'font-awesome' ) !== false || strpos( $url, 'icomoon' ) !== false || strpos( $url, 'icon' ) !== false ) { $score -= 50; } if ( $score > $best_s ) { $best_s = $score; $best = $url; } $candidates[] = $url; } // Keep only same-host or gstatic candidates. $candidates = array_values( array_unique( array_filter( $candidates, function ( $url ) use ( $host ) { $th = wp_parse_url( $url, PHP_URL_HOST ); return $th && ( $th === $host || false !== strpos( $th, 'fonts.gstatic.com' ) ); } ) ) ); // Try to fetch first WOFF2 from Google Fonts CSS (prefer variable). if ( $google_fonts ) { $gf_fonts = self::font_from_stylesheet( $google_fonts ); if ( ! empty( $gf_fonts ) ) { $candidates = array_merge( $gf_fonts, $candidates ); } } $candidates = array_values( array_unique( $candidates ) ); return array( 'primary' => isset( $candidates[0] ) ? $candidates[0] : '', 'candidates' => $candidates, ); } /** * Choose a hero-ish image: prefer hero/cover/banner keywords or larger dimensions. * * @param \DOMDocument $dom DOM. * @return string */ private static function pick_best_image( \DOMDocument $dom ) { $images = $dom->getElementsByTagName( 'img' ); $best = ''; $best_s = -INF; $host = wp_parse_url( home_url(), PHP_URL_HOST ); $keywords_positive = array( 'hero', 'cover', 'banner', 'slider', 'header', 'featured', 'main', 'masthead' ); $keywords_negative = array( 'logo', 'icon', 'avatar', 'thumb', 'thumbnail', 'emoji' ); foreach ( $images as $idx => $img ) { $src = $img->getAttribute( 'src' ); if ( empty( $src ) ) { continue; } $url = self::make_absolute_url( $src ); $score = 0 - $idx; $cls_id = strtolower( $img->getAttribute( 'class' ) . ' ' . $img->getAttribute( 'id' ) . ' ' . $url ); foreach ( $keywords_positive as $word ) { if ( false !== strpos( $cls_id, $word ) ) { $score += 25; } } foreach ( $keywords_negative as $word ) { if ( false !== strpos( $cls_id, $word ) ) { $score -= 30; } } $w = (int) $img->getAttribute( 'width' ); $h = (int) $img->getAttribute( 'height' ); if ( $w >= 1200 || $h >= 800 ) { $score += 30; } elseif ( $w >= 800 || $h >= 600 ) { $score += 20; } elseif ( $w >= 400 || $h >= 300 ) { $score += 10; } else { $score -= 5; } $target_host = wp_parse_url( $url, PHP_URL_HOST ); if ( $target_host && $host && $target_host === $host ) { $score += 5; } if ( $score > $best_s ) { $best_s = $score; $best = $url; } } return $best; } /** * Safe, single-font detector following conservative rules. * * @param \DOMDocument $dom DOM. * @return array {primary, candidates} */ private static function pick_best_font_v2( \DOMDocument $dom ) { $links = $dom->getElementsByTagName( 'link' ); $styles = $dom->getElementsByTagName( 'style' ); $host = wp_parse_url( home_url(), PHP_URL_HOST ); $google_fonts = array(); $primary_family = ''; $priority_selectors = array( 'body', 'html', 'h1', '.entry-title', '.site-title', '.elementor-heading-title' ); foreach ( $styles as $style ) { $css = $style->textContent; foreach ( $priority_selectors as $selector ) { if ( preg_match( '/' . preg_quote( $selector, '/' ) . '\\s*\\{[^}]*font-family\\s*:\\s*([^;}]+)[;}]/i', $css, $m ) ) { $families = explode( ',', $m[1] ); if ( ! empty( $families[0] ) ) { $family = trim( $families[0], "\"' \t\r\n" ); if ( ! in_array( strtolower( $family ), array( 'sans-serif', 'serif', 'monospace', 'inherit' ), true ) ) { $primary_family = $family; break 2; } } } } } foreach ( $links as $link ) { $rel = strtolower( $link->getAttribute( 'rel' ) ); $href = $link->getAttribute( 'href' ); if ( false === strpos( $rel, 'stylesheet' ) || empty( $href ) ) { continue; } $url = self::make_absolute_url( $href ); if ( false !== strpos( $url, 'fonts.googleapis.com' ) || false !== strpos( $url, 'google-fonts/css' ) ) { $google_fonts[] = $url; if ( empty( $primary_family ) && preg_match( '/family=([^:&]+)/', $url, $m ) ) { $family = urldecode( str_replace( '+', ' ', $m[1] ) ); $primary_family = $family; } } } if ( empty( $primary_family ) ) { return array( 'primary' => '', 'candidates' => array(), ); } $candidates = array(); foreach ( $google_fonts as $css_url ) { $fonts = self::font_from_stylesheet( $css_url, $primary_family ); if ( ! empty( $fonts ) ) { foreach ( $fonts as $f ) { $th = wp_parse_url( $f, PHP_URL_HOST ); if ( $th && ( $th === $host || false !== strpos( $th, 'fonts.gstatic.com' ) ) ) { $candidates[] = $f; } } } } $candidates = array_values( array_unique( $candidates ) ); if ( count( $candidates ) > 1 ) { foreach ( $candidates as $cand ) { if ( false !== stripos( $cand, 'var' ) ) { $candidates = array( $cand ); break; } } } $primary = ( count( $candidates ) === 1 ) ? $candidates[0] : ''; return array( 'primary' => $primary, 'candidates' => $candidates, ); } /** * Fetch a stylesheet and return the first usable woff2 from fonts.gstatic.com (prefer variable). * * @param string $url Stylesheet URL. * @param string $family Target family (unused currently, kept for clarity). * @return string */ private static function font_from_stylesheet( $url, $family = '' ) { $response = wp_remote_get( $url, array( 'timeout' => 6, 'redirection' => 2, ) ); if ( is_wp_error( $response ) ) { return ''; } $code = (int) wp_remote_retrieve_response_code( $response ); if ( $code >= 400 ) { return ''; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return ''; } $urls = array(); preg_match_all( '/url\\(([^)]+)\\)/i', $body, $matches ); if ( ! empty( $matches[1] ) ) { foreach ( $matches[1] as $candidate ) { $candidate = trim( $candidate, "\"' \t\n\r\0\x0B" ); if ( preg_match( '/\\.woff2(\\?|$)/i', $candidate ) ) { $target_host = wp_parse_url( $candidate, PHP_URL_HOST ); if ( $target_host && false !== strpos( $target_host, 'fonts.gstatic.com' ) ) { $urls[] = $candidate; } } } } if ( empty( $urls ) ) { return array(); } // Prefer variable font if present. foreach ( $urls as $candidate ) { if ( false !== stripos( $candidate, 'var' ) ) { return array( $candidate ); } } return array( $urls[0] ); } /** * Add contextual help tab. * * @return void */ public static function add_help_tab() { $screen = get_current_screen(); if ( ! $screen || 'settings_page_i365-po-settings' !== $screen->id ) { return; } $screen->add_help_tab( array( 'id' => 'i365-po-help', 'title' => __( 'Performance Optimizer Help', '365i-performance-optimizer' ), 'content' => '

' . esc_html__( 'Toggle the options to match your site: enable speculation for faster navigation, set preconnect/preload URLs to your theme assets, defer non-core scripts, and refine image loading.', '365i-performance-optimizer' ) . '

' . '

' . esc_html__( 'Use Auto-detect to fill fields from your active theme. Exclude checkout/cart paths from speculation. Save to apply changes; Restore defaults to undo.', '365i-performance-optimizer' ) . '

', ) ); } /* |-------------------------------------------------------------------------- | Backup & Restore AJAX Handlers |-------------------------------------------------------------------------- */ /** * AJAX: Restore settings from a backup. * * @return void */ public static function ajax_restore_backup() { check_ajax_referer( 'i365-po-utilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $timestamp = isset( $_POST['timestamp'] ) ? absint( $_POST['timestamp'] ) : 0; if ( empty( $timestamp ) ) { wp_send_json_error( array( 'message' => __( 'Invalid backup timestamp.', '365i-performance-optimizer' ) ) ); } $result = I365_PO_Plugin::restore_backup( $timestamp ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'message' => __( 'Settings restored successfully. Page will reload.', '365i-performance-optimizer' ), 'reload' => true, ) ); } /** * AJAX: Delete a specific backup. * * @return void */ public static function ajax_delete_backup() { check_ajax_referer( 'i365-po-utilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $timestamp = isset( $_POST['timestamp'] ) ? absint( $_POST['timestamp'] ) : 0; if ( empty( $timestamp ) ) { wp_send_json_error( array( 'message' => __( 'Invalid backup timestamp.', '365i-performance-optimizer' ) ) ); } $result = I365_PO_Plugin::delete_backup( $timestamp ); if ( ! $result ) { wp_send_json_error( array( 'message' => __( 'Failed to delete backup.', '365i-performance-optimizer' ) ) ); } wp_send_json_success( array( 'message' => __( 'Backup deleted.', '365i-performance-optimizer' ), 'backups' => self::render_backups_list(), ) ); } /* |-------------------------------------------------------------------------- | Profile AJAX Handlers |-------------------------------------------------------------------------- */ /** * AJAX: Apply a profile. * * @return void */ public static function ajax_apply_profile() { check_ajax_referer( 'i365-po-utilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $slug = isset( $_POST['profile'] ) ? sanitize_text_field( wp_unslash( $_POST['profile'] ) ) : ''; if ( empty( $slug ) ) { wp_send_json_error( array( 'message' => __( 'Invalid profile.', '365i-performance-optimizer' ) ) ); } $result = I365_PO_Plugin::apply_profile( $slug ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } $profiles = I365_PO_Plugin::get_all_profiles(); $profile_name = isset( $profiles[ $slug ]['name'] ) ? $profiles[ $slug ]['name'] : $slug; wp_send_json_success( array( 'message' => sprintf( /* translators: %s: profile name */ __( 'Profile "%s" applied. Page will reload.', '365i-performance-optimizer' ), $profile_name ), 'reload' => true, ) ); } /** * AJAX: Save current settings as a custom profile. * * @return void */ public static function ajax_save_profile() { check_ajax_referer( 'i365-po-utilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; $description = isset( $_POST['description'] ) ? sanitize_text_field( wp_unslash( $_POST['description'] ) ) : ''; if ( empty( $name ) ) { wp_send_json_error( array( 'message' => __( 'Profile name is required.', '365i-performance-optimizer' ) ) ); } $result = I365_PO_Plugin::save_profile( $name, $description ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'message' => sprintf( /* translators: %s: profile name */ __( 'Profile "%s" saved.', '365i-performance-optimizer' ), $name ), 'profiles' => self::render_profiles_list(), ) ); } /** * AJAX: Delete a custom profile. * * @return void */ public static function ajax_delete_profile() { check_ajax_referer( 'i365-po-utilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $slug = isset( $_POST['profile'] ) ? sanitize_text_field( wp_unslash( $_POST['profile'] ) ) : ''; if ( empty( $slug ) ) { wp_send_json_error( array( 'message' => __( 'Invalid profile.', '365i-performance-optimizer' ) ) ); } $result = I365_PO_Plugin::delete_profile( $slug ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'message' => __( 'Profile deleted.', '365i-performance-optimizer' ), 'profiles' => self::render_profiles_list(), ) ); } /* |-------------------------------------------------------------------------- | Import/Export AJAX Handlers |-------------------------------------------------------------------------- */ /** * AJAX: Export settings as JSON. * * @return void */ public static function ajax_export() { check_ajax_referer( 'i365-po-utilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $json = I365_PO_Plugin::export_settings(); wp_send_json_success( array( 'json' => $json, 'filename' => 'performance-optimizer-settings-' . gmdate( 'Y-m-d' ) . '.json', ) ); } /** * AJAX: Import settings from JSON. * * @return void */ public static function ajax_import() { check_ajax_referer( 'i365-po-utilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Unauthorized', '365i-performance-optimizer' ) ) ); } $json = isset( $_POST['json'] ) ? sanitize_textarea_field( wp_unslash( $_POST['json'] ) ) : ''; if ( empty( $json ) ) { wp_send_json_error( array( 'message' => __( 'No settings data provided.', '365i-performance-optimizer' ) ) ); } $result = I365_PO_Plugin::import_settings( $json ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } wp_send_json_success( array( 'message' => __( 'Settings imported successfully. Page will reload.', '365i-performance-optimizer' ), 'reload' => true, ) ); } /* |-------------------------------------------------------------------------- | Helper Render Methods |-------------------------------------------------------------------------- */ /** * Render backups list HTML for AJAX response. * * @return string HTML. */ private static function render_backups_list() { $backups = I365_PO_Plugin::get_backups(); if ( empty( $backups ) ) { return '

' . esc_html__( 'No backups available.', '365i-performance-optimizer' ) . '

'; } $html = ''; return $html; } /** * Render profiles list HTML for AJAX response. * * @return string HTML. */ private static function render_profiles_list() { $profiles = I365_PO_Plugin::get_all_profiles(); $html = '
'; foreach ( $profiles as $slug => $profile ) { $is_builtin = ! empty( $profile['builtin'] ); $html .= '
'; $html .= '

' . esc_html( $profile['name'] ) . '

'; $html .= '

' . esc_html( $profile['description'] ) . '

'; $html .= '
'; $html .= ''; if ( ! $is_builtin ) { $html .= ''; } $html .= '
'; $html .= '
'; } $html .= '
'; return $html; } }