true] ); } // Ensure fonts directory exists $font_dir = wp_get_font_dir(); if (!file_exists($font_dir['path'])) { wp_mkdir_p($font_dir['path']); } // Create .htaccess for proper MIME types and CORS (Apache servers) $htaccess_path = trailingslashit($font_dir['path']) . '.htaccess'; if (!file_exists($htaccess_path)) { $htaccess_content = "# Maple Local Fonts - Font MIME types and CORS\n"; $htaccess_content .= "\n"; $htaccess_content .= " AddType font/woff2 .woff2\n"; $htaccess_content .= " AddType font/woff .woff\n"; $htaccess_content .= "\n\n"; $htaccess_content .= "\n"; $htaccess_content .= " \n"; $htaccess_content .= " Header set Access-Control-Allow-Origin \"*\"\n"; $htaccess_content .= " \n"; $htaccess_content .= "\n"; global $wp_filesystem; if (empty($wp_filesystem)) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if ($wp_filesystem) { $wp_filesystem->put_contents($htaccess_path, $htaccess_content, FS_CHMOD_FILE); } } // Flush rewrite rules for fonts CSS endpoint mlf_add_rewrite_rules(); flush_rewrite_rules(); } register_activation_hook(__FILE__, 'mlf_activate'); /** * Flush rewrite rules on deactivation. */ function mlf_deactivate() { flush_rewrite_rules(); } register_deactivation_hook(__FILE__, 'mlf_deactivate'); /** * Check WordPress version on admin init (in case WP was downgraded). */ function mlf_check_version() { if (version_compare(get_bloginfo('version'), '6.5', '<')) { deactivate_plugins(plugin_basename(__FILE__)); add_action('admin_notices', 'mlf_version_notice'); } } add_action('admin_init', 'mlf_check_version'); /** * Ensure fonts directory htaccess exists (for MIME types and CORS). */ function mlf_ensure_htaccess() { $font_dir = wp_get_font_dir(); $htaccess_path = trailingslashit($font_dir['path']) . '.htaccess'; // Always update htaccess to ensure latest headers $htaccess_content = "# Maple Local Fonts - Font MIME types and CORS\n"; $htaccess_content .= "# Required for cross-browser font loading including iOS Safari\n\n"; $htaccess_content .= "\n"; $htaccess_content .= " AddType font/woff2 .woff2\n"; $htaccess_content .= " AddType font/woff .woff\n"; $htaccess_content .= "\n\n"; $htaccess_content .= "\n"; $htaccess_content .= " \n"; $htaccess_content .= " Header set Access-Control-Allow-Origin \"*\"\n"; $htaccess_content .= " Header set Access-Control-Allow-Methods \"GET, OPTIONS\"\n"; $htaccess_content .= " Header set Access-Control-Allow-Headers \"Origin, Content-Type\"\n"; $htaccess_content .= " Header set Cross-Origin-Resource-Policy \"cross-origin\"\n"; $htaccess_content .= " Header set Timing-Allow-Origin \"*\"\n"; $htaccess_content .= " \n"; $htaccess_content .= "\n"; if (file_exists($font_dir['path'])) { global $wp_filesystem; if (empty($wp_filesystem)) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if ($wp_filesystem) { $wp_filesystem->put_contents($htaccess_path, $htaccess_content, FS_CHMOD_FILE); } } } add_action('admin_init', 'mlf_ensure_htaccess'); /** * Display version notice. */ function mlf_version_notice() { echo '

'; esc_html_e('Maple Local Fonts has been deactivated. It requires WordPress 6.5 or higher.', 'maple-local-fonts'); echo '

'; } /** * Declare WooCommerce HPOS compatibility. */ function mlf_declare_hpos_compatibility() { if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); } } add_action('before_woocommerce_init', 'mlf_declare_hpos_compatibility'); /** * Load plugin text domain. */ function mlf_load_textdomain() { load_plugin_textdomain('maple-local-fonts', false, dirname(MLF_PLUGIN_BASENAME) . '/languages'); } add_action('plugins_loaded', 'mlf_load_textdomain'); /** * Autoload plugin classes. */ function mlf_autoload($class) { $prefix = 'MLF_'; if (strpos($class, $prefix) !== 0) { return; } $class_name = str_replace($prefix, '', $class); $class_name = str_replace('_', '-', strtolower($class_name)); $file = MLF_PLUGIN_DIR . 'includes/class-mlf-' . $class_name . '.php'; if (file_exists($file)) { require_once $file; } } spl_autoload_register('mlf_autoload'); /** * Initialize the plugin. */ function mlf_init() { // Only load admin functionality if (is_admin()) { new MLF_Admin_Page(); new MLF_Ajax_Handler(); new MLF_Font_Search(); } } add_action('plugins_loaded', 'mlf_init', 20); /** * Register REST API routes. */ function mlf_register_rest_routes() { $controller = new MLF_Rest_Controller(); $controller->register_routes(); // Register font server routes for cross-browser compatibility $font_server = new MLF_Font_Server(); $font_server->register_routes(); } add_action('rest_api_init', 'mlf_register_rest_routes'); /** * Check if we should use REST API for font serving (compatibility mode). * * @return bool True if using REST API font serving. */ function mlf_use_rest_font_serving() { return get_option('mlf_compatibility_mode', true); // Default to true for maximum compatibility } /** * Get the URL for a font file. * * Uses REST API endpoint for compatibility, or direct file URL for performance. * * @param string $filename The font filename. * @return string The font URL. */ function mlf_get_font_url($filename) { if (mlf_use_rest_font_serving()) { // Use REST API endpoint (guaranteed proper headers) return rest_url('mlf/v1/font/' . $filename); } else { // Use direct file URL (faster but depends on server config) $font_dir = wp_get_font_dir(); return trailingslashit($font_dir['url']) . $filename; } } /** * Get the required capability for managing fonts. * * @return string The capability required to manage fonts. */ function mlf_get_capability() { /** * Filter the capability required to manage local fonts. * * @since 1.0.0 * @param string $capability Default capability is 'edit_theme_options'. */ return apply_filters('mlf_manage_fonts_capability', 'edit_theme_options'); } /** * Add imported fonts to the theme.json typography settings. * * This makes fonts appear in the Site Editor typography dropdown * and generates the @font-face CSS for the frontend. * * @param WP_Theme_JSON_Data $theme_json The theme.json data. * @return WP_Theme_JSON_Data Modified theme.json data. */ function mlf_add_fonts_to_theme_json($theme_json) { // Wrap in try-catch to prevent breaking Site Editor if something goes wrong try { $registry = new MLF_Font_Registry(); $fonts = $registry->get_imported_fonts_with_src(); if (empty($fonts) || !is_array($fonts)) { return $theme_json; } // Build our font families $font_families = []; $font_dir = wp_get_font_dir(); if (empty($font_dir['url'])) { return $theme_json; } $font_base_url = trailingslashit($font_dir['url']); foreach ($fonts as $font) { // Validate required font properties if (empty($font['name']) || empty($font['slug']) || empty($font['variants'])) { continue; } $font_faces = []; foreach ($font['variants'] as $variant) { // Validate variant data if (empty($variant['filename'])) { continue; } $weight = !empty($variant['weight']) ? $variant['weight'] : '400'; $style = !empty($variant['style']) ? $variant['style'] : 'normal'; $filename = $variant['filename']; // Use direct file URL $font_url = $font_base_url . $filename; $font_faces[] = [ 'fontFamily' => $font['name'], 'fontWeight' => $weight, 'fontStyle' => $style, 'fontDisplay' => 'swap', 'src' => [$font_url], ]; } // Only add font if it has valid faces if (!empty($font_faces)) { $font_families[] = [ 'name' => $font['name'], 'slug' => $font['slug'], 'fontFamily' => "'{$font['name']}', sans-serif", 'fontFace' => $font_faces, ]; } } // Only update if we have valid fonts if (empty($font_families)) { return $theme_json; } // Use update_with to merge - WordPress handles the merging logic $new_data = [ 'version' => 2, 'settings' => [ 'typography' => [ 'fontFamilies' => $font_families, ], ], ]; return $theme_json->update_with($new_data); } catch (Exception $e) { // Log error but don't break the Site Editor error_log('MLF theme.json filter error: ' . $e->getMessage()); return $theme_json; } } add_filter('wp_theme_json_data_user', 'mlf_add_fonts_to_theme_json', 10); /** * Generate @font-face CSS for imported fonts. * * @return string CSS content. */ function mlf_get_font_face_css() { $registry = new MLF_Font_Registry(); $fonts = $registry->get_imported_fonts(); if (empty($fonts)) { return ''; } $css = ''; foreach ($fonts as $font) { $font_slug = sanitize_title($font['name']); // Escape font name for CSS (handle quotes and special chars) $css_font_name = addcslashes($font['name'], "'\\"); foreach ($font['variants'] as $variant) { $weight = $variant['weight']; $style = $variant['style']; // Build filename based on our naming convention if (strpos($weight, ' ') !== false) { // Variable font $filename = sprintf('%s_%s_variable.woff2', $font_slug, $style); } else { $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); } // Get font URL (uses REST API or direct based on setting) $font_url = mlf_get_font_url($filename); // Ensure HTTPS if site uses HTTPS (important for reverse proxy setups) if (is_ssl() && strpos($font_url, 'http://') === 0) { $font_url = str_replace('http://', 'https://', $font_url); } $css .= "@font-face {"; $css .= "font-family: '{$css_font_name}';"; $css .= "font-style: {$style};"; $css .= "font-weight: {$weight};"; $css .= "font-display: swap;"; $css .= "src: url('" . esc_url($font_url) . "') format('woff2');"; $css .= "}\n"; } } return $css; } /** * Enqueue font face CSS on frontend. * * Uses external CSS file just like Google Fonts does. */ function mlf_enqueue_font_faces() { $registry = new MLF_Font_Registry(); $fonts = $registry->get_imported_fonts(); if (empty($fonts)) { return; } // Load CSS from custom endpoint (like Google Fonts serves CSS) wp_enqueue_style( 'mlf-local-fonts', home_url('/mlf-fonts.css'), [], null // No version string, like Google Fonts ); } add_action('wp_enqueue_scripts', 'mlf_enqueue_font_faces'); /** * Enqueue font face CSS in block editor. */ function mlf_enqueue_editor_font_faces() { $registry = new MLF_Font_Registry(); $fonts = $registry->get_imported_fonts(); if (empty($fonts)) { return; } // Load CSS from custom endpoint (like Google Fonts serves CSS) wp_enqueue_style( 'mlf-local-fonts-editor', home_url('/mlf-fonts.css'), [], null ); } add_action('enqueue_block_editor_assets', 'mlf_enqueue_editor_font_faces'); /** * Add rewrite rule for fonts CSS endpoint. */ function mlf_add_rewrite_rules() { add_rewrite_rule('^mlf-fonts\.css$', 'index.php?mlf_fonts_css=1', 'top'); } add_action('init', 'mlf_add_rewrite_rules'); /** * Add query var for fonts CSS. */ function mlf_add_query_vars($vars) { $vars[] = 'mlf_fonts_css'; return $vars; } add_filter('query_vars', 'mlf_add_query_vars'); /** * Handle fonts CSS request. */ function mlf_handle_fonts_css_request() { if (!get_query_var('mlf_fonts_css')) { return; } // Send CSS headers header('Content-Type: text/css; charset=UTF-8'); header('Access-Control-Allow-Origin: *'); header('Cache-Control: public, max-age=86400'); header('X-Content-Type-Options: nosniff'); $registry = new MLF_Font_Registry(); $fonts = $registry->get_imported_fonts_with_src(); if (empty($fonts)) { echo "/* No fonts installed */\n"; exit; } echo "/* Maple Local Fonts */\n\n"; // Use direct font file URLs $font_dir = wp_get_font_dir(); $font_base_url = trailingslashit($font_dir['url']); foreach ($fonts as $font) { foreach ($font['variants'] as $variant) { $weight = $variant['weight']; $style = $variant['style']; // Use the actual filename from database (stored in src) $filename = $variant['filename']; $file_url = $font_base_url . $filename; echo "@font-face {\n"; echo " font-family: '{$font['name']}';\n"; echo " font-style: {$style};\n"; echo " font-weight: {$weight};\n"; echo " font-display: swap;\n"; echo " src: url({$file_url}) format('woff2');\n"; echo "}\n\n"; } } exit; } add_action('template_redirect', 'mlf_handle_fonts_css_request'); /** * Register admin menu. */ function mlf_register_menu() { add_submenu_page( 'options-general.php', __('Maple Fonts', 'maple-local-fonts'), __('Maple Fonts', 'maple-local-fonts'), mlf_get_capability(), 'maple-local-fonts', 'mlf_render_admin_page' ); } add_action('admin_menu', 'mlf_register_menu'); /** * Add settings link to plugin action links. * * @param array $links Existing plugin action links. * @return array Modified plugin action links. */ function mlf_plugin_action_links($links) { $settings_link = sprintf( '%s', esc_url(admin_url('options-general.php?page=maple-local-fonts')), esc_html__('Settings', 'maple-local-fonts') ); array_unshift($links, $settings_link); return $links; } add_filter('plugin_action_links_' . MLF_PLUGIN_BASENAME, 'mlf_plugin_action_links'); /** * Render admin page (delegates to MLF_Admin_Page). */ function mlf_render_admin_page() { $admin_page = new MLF_Admin_Page(); $admin_page->render(); } /** * Enqueue admin assets. * * @param string $hook The current admin page hook. */ function mlf_enqueue_admin_assets($hook) { if ($hook !== 'settings_page_maple-local-fonts') { return; } // Use filemtime for cache-busting during development $css_version = MLF_VERSION . '.' . filemtime(MLF_PLUGIN_DIR . 'assets/admin.css'); $js_version = MLF_VERSION . '.' . filemtime(MLF_PLUGIN_DIR . 'assets/admin.js'); wp_enqueue_style( 'mlf-admin', MLF_PLUGIN_URL . 'assets/admin.css', [], $css_version ); wp_enqueue_script( 'mlf-admin', MLF_PLUGIN_URL . 'assets/admin.js', ['jquery'], $js_version, true ); wp_localize_script('mlf-admin', 'mapleLocalFontsData', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'downloadNonce' => wp_create_nonce('mlf_download_font'), 'deleteNonce' => wp_create_nonce('mlf_delete_font'), 'searchNonce' => wp_create_nonce('mlf_search_fonts'), 'checkUpdatesNonce' => wp_create_nonce('mlf_check_updates'), 'updateFontNonce' => wp_create_nonce('mlf_update_font'), 'strings' => [ 'downloading' => __('Downloading...', 'maple-local-fonts'), 'deleting' => __('Deleting...', 'maple-local-fonts'), 'updating' => __('Updating...', 'maple-local-fonts'), 'checking' => __('Checking...', 'maple-local-fonts'), 'confirmDelete' => __('Are you sure you want to delete this font?', 'maple-local-fonts'), 'error' => __('An error occurred. Please try again.', 'maple-local-fonts'), 'searching' => __('Searching...', 'maple-local-fonts'), 'noResults' => __('No fonts found. Try a different search term.', 'maple-local-fonts'), 'selectFont' => __('Please select a font first.', 'maple-local-fonts'), 'previewText' => __('Maple Fonts Preview', 'maple-local-fonts'), 'minChars' => __('Please enter at least 2 characters.', 'maple-local-fonts'), 'noUpdates' => __('All fonts are up to date.', 'maple-local-fonts'), 'updatesFound' => __('Updates available for %d font(s).', 'maple-local-fonts'), ], ]); } add_action('admin_enqueue_scripts', 'mlf_enqueue_admin_assets');