+
diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php
index 972a86a..4a90034 100644
--- a/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php
+++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-ajax-handler.php
@@ -29,6 +29,8 @@ class MLF_Ajax_Handler {
public function __construct() {
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
+ add_action('wp_ajax_mlf_check_updates', [$this, 'handle_check_updates']);
+ add_action('wp_ajax_mlf_update_font', [$this, 'handle_update_font']);
// NEVER add wp_ajax_nopriv_ - admin only functionality
// Initialize rate limiter: 10 requests per minute
@@ -77,6 +79,10 @@ class MLF_Ajax_Handler {
// Validate include_italic (boolean)
$include_italic = isset($_POST['include_italic']) && $_POST['include_italic'] === '1';
+ // Get version info (optional)
+ $font_version = isset($_POST['font_version']) ? sanitize_text_field(wp_unslash($_POST['font_version'])) : '';
+ $font_last_modified = isset($_POST['font_last_modified']) ? sanitize_text_field(wp_unslash($_POST['font_last_modified'])) : '';
+
// 5. PROCESS REQUEST
try {
$downloader = new MLF_Font_Downloader();
@@ -91,7 +97,9 @@ class MLF_Ajax_Handler {
$result = $registry->register_font(
$download_result['font_name'],
$download_result['font_slug'],
- $download_result['files']
+ $download_result['files'],
+ $font_version,
+ $font_last_modified
);
if (is_wp_error($result)) {
@@ -170,6 +178,166 @@ class MLF_Ajax_Handler {
}
}
+ /**
+ * Handle check for updates AJAX request.
+ */
+ public function handle_check_updates() {
+ // 1. NONCE CHECK
+ if (!check_ajax_referer('mlf_check_updates', 'nonce', false)) {
+ wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
+ }
+
+ // 2. CAPABILITY CHECK
+ $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
+ if (!current_user_can($capability)) {
+ wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
+ }
+
+ // 3. RATE LIMIT CHECK
+ if (!$this->rate_limiter->check_and_record('check_updates')) {
+ wp_send_json_error([
+ 'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
+ ], 429);
+ }
+
+ // 4. GET INSTALLED FONTS AND CHECK VERSIONS
+ try {
+ $registry = new MLF_Font_Registry();
+ $installed_fonts = $registry->get_imported_fonts();
+
+ if (empty($installed_fonts)) {
+ wp_send_json_success(['updates' => []]);
+ }
+
+ $font_search = new MLF_Font_Search();
+ $updates = [];
+
+ foreach ($installed_fonts as $font) {
+ $current_version = $font_search->get_font_version($font['name']);
+
+ if ($current_version && !empty($current_version['version'])) {
+ $installed_version = $font['version'] ?? '';
+
+ // Compare versions
+ if (!empty($installed_version) && $installed_version !== $current_version['version']) {
+ $updates[$font['id']] = [
+ 'installed_version' => $installed_version,
+ 'latest_version' => $current_version['version'],
+ 'last_modified' => $current_version['lastModified'],
+ ];
+ } elseif (empty($installed_version)) {
+ // No version stored, consider it needs update
+ $updates[$font['id']] = [
+ 'installed_version' => '',
+ 'latest_version' => $current_version['version'],
+ 'last_modified' => $current_version['lastModified'],
+ ];
+ }
+ }
+ }
+
+ wp_send_json_success(['updates' => $updates]);
+ } catch (Exception $e) {
+ error_log('MLF Check Updates Error: ' . sanitize_text_field($e->getMessage()));
+ wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
+ }
+ }
+
+ /**
+ * Handle font update (re-download) AJAX request.
+ */
+ public function handle_update_font() {
+ // 1. NONCE CHECK
+ if (!check_ajax_referer('mlf_update_font', 'nonce', false)) {
+ wp_send_json_error(['message' => __('Security check failed.', 'maple-local-fonts')], 403);
+ }
+
+ // 2. CAPABILITY CHECK
+ $capability = function_exists('mlf_get_capability') ? mlf_get_capability() : 'edit_theme_options';
+ if (!current_user_can($capability)) {
+ wp_send_json_error(['message' => __('Unauthorized.', 'maple-local-fonts')], 403);
+ }
+
+ // 3. RATE LIMIT CHECK
+ if (!$this->rate_limiter->check_and_record('update')) {
+ wp_send_json_error([
+ 'message' => __('Too many requests. Please wait a moment and try again.', 'maple-local-fonts'),
+ ], 429);
+ }
+
+ // 4. INPUT VALIDATION
+ $font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
+
+ if ($font_id < 1) {
+ wp_send_json_error(['message' => __('Invalid font ID.', 'maple-local-fonts')]);
+ }
+
+ // Verify font exists and is one we imported
+ $font = get_post($font_id);
+ if (!$font || $font->post_type !== 'wp_font_family') {
+ wp_send_json_error(['message' => __('Font not found.', 'maple-local-fonts')]);
+ }
+
+ if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
+ wp_send_json_error(['message' => __('Cannot update fonts not imported by this plugin.', 'maple-local-fonts')]);
+ }
+
+ // Get font name from post content
+ $settings = json_decode($font->post_content, true);
+ $font_name = $settings['name'] ?? $font->post_title;
+
+ // 5. DELETE OLD FONT AND RE-DOWNLOAD
+ try {
+ $registry = new MLF_Font_Registry();
+
+ // Delete old font
+ $delete_result = $registry->delete_font($font_id);
+ if (is_wp_error($delete_result)) {
+ wp_send_json_error(['message' => $this->get_user_error_message($delete_result)]);
+ }
+
+ // Get latest version info
+ $font_search = new MLF_Font_Search();
+ $version_info = $font_search->get_font_version($font_name);
+ $font_version = $version_info['version'] ?? '';
+ $font_last_modified = $version_info['lastModified'] ?? '';
+
+ // Re-download font (include italic by default)
+ $downloader = new MLF_Font_Downloader();
+ $download_result = $downloader->download($font_name, true);
+
+ if (is_wp_error($download_result)) {
+ wp_send_json_error(['message' => $this->get_user_error_message($download_result)]);
+ }
+
+ // Register font with new version info
+ $result = $registry->register_font(
+ $download_result['font_name'],
+ $download_result['font_slug'],
+ $download_result['files'],
+ $font_version,
+ $font_last_modified
+ );
+
+ if (is_wp_error($result)) {
+ wp_send_json_error(['message' => $this->get_user_error_message($result)]);
+ }
+
+ wp_send_json_success([
+ 'message' => sprintf(
+ /* translators: %s: font name */
+ __('Successfully updated %s.', 'maple-local-fonts'),
+ esc_html($font_name)
+ ),
+ 'font_id' => $result,
+ 'version' => $font_version,
+ ]);
+ } catch (Exception $e) {
+ error_log('MLF Update Font Error: ' . sanitize_text_field($e->getMessage()));
+ wp_send_json_error(['message' => __('An unexpected error occurred.', 'maple-local-fonts')]);
+ }
+ }
+
/**
* Convert internal error codes to user-friendly messages.
*
diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php
index c9584d6..d83f15d 100644
--- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php
+++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-downloader.php
@@ -25,11 +25,14 @@ class MLF_Font_Downloader {
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
/**
- * All available font weights.
+ * Common font weights to request.
+ *
+ * Most fonts support at least 300-700. We also include 800-900 for fonts
+ * that support Black/Heavy weights.
*
* @var array
*/
- private $all_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
+ private $all_weights = [300, 400, 500, 600, 700, 800, 900];
/**
* Download a font from Google Fonts.
@@ -151,6 +154,8 @@ class MLF_Font_Downloader {
/**
* Download static fonts (fallback when variable not available).
*
+ * Tries different weight combinations since fonts support varying weights.
+ *
* @param string $font_name Font family name.
* @param bool $include_italic Whether to include italic styles.
* @return array|WP_Error Download result or error.
@@ -158,8 +163,28 @@ class MLF_Font_Downloader {
private function download_static_fonts($font_name, $include_italic) {
$styles = $include_italic ? ['normal', 'italic'] : ['normal'];
- // Fetch CSS from Google
- $css = $this->fetch_static_css($font_name, $this->all_weights, $styles);
+ // Try different weight combinations - fonts support varying weights
+ $weight_sets = [
+ [300, 400, 500, 600, 700, 800, 900], // Full range
+ [300, 400, 500, 600, 700, 800], // Without 900
+ [300, 400, 500, 600, 700], // Common range
+ [400, 500, 600, 700], // Without light
+ [400, 700], // Just regular and bold
+ ];
+
+ $css = null;
+ foreach ($weight_sets as $weights) {
+ $css = $this->fetch_static_css($font_name, $weights, $styles);
+
+ if (!is_wp_error($css)) {
+ break;
+ }
+
+ // If error is not "font not found", stop trying
+ if ($css->get_error_code() !== 'font_not_found') {
+ return $css;
+ }
+ }
if (is_wp_error($css)) {
return $css;
@@ -191,6 +216,9 @@ class MLF_Font_Downloader {
/**
* Fetch variable font CSS from Google Fonts API.
*
+ * Tries progressively smaller weight ranges until one works,
+ * since different fonts support different weight ranges.
+ *
* @param string $font_name Font family name.
* @param bool $italic Whether to fetch italic variant.
* @return string|WP_Error CSS content or error.
@@ -198,15 +226,26 @@ class MLF_Font_Downloader {
private function fetch_variable_css($font_name, $italic = false) {
$family = str_replace(' ', '+', $font_name);
- if ($italic) {
- // Request italic variable font
- $url = "https://fonts.googleapis.com/css2?family={$family}:ital,wght@1,100..900&display=swap";
- } else {
- // Request roman variable font
- $url = "https://fonts.googleapis.com/css2?family={$family}:wght@100..900&display=swap";
+ // Try different weight ranges - fonts support varying ranges
+ $weight_ranges = ['300..900', '300..800', '300..700', '400..700'];
+
+ foreach ($weight_ranges as $range) {
+ if ($italic) {
+ $url = "https://fonts.googleapis.com/css2?family={$family}:ital,wght@1,{$range}&display=swap";
+ } else {
+ $url = "https://fonts.googleapis.com/css2?family={$family}:wght@{$range}&display=swap";
+ }
+
+ $result = $this->fetch_css($url);
+
+ // If successful or error is not "font not found", return
+ if (!is_wp_error($result) || $result->get_error_code() !== 'font_not_found') {
+ return $result;
+ }
}
- return $this->fetch_css($url);
+ // All ranges failed
+ return new WP_Error('no_variable', 'Variable font not available');
}
/**
diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php
index 3241108..171c1f8 100644
--- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php
+++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-registry.php
@@ -19,12 +19,14 @@ class MLF_Font_Registry {
/**
* Register a font family with WordPress Font Library.
*
- * @param string $font_name Display name (e.g., "Open Sans").
- * @param string $font_slug Slug (e.g., "open-sans").
- * @param array $files Array of downloaded file data.
+ * @param string $font_name Display name (e.g., "Open Sans").
+ * @param string $font_slug Slug (e.g., "open-sans").
+ * @param array $files Array of downloaded file data.
+ * @param string $font_version Google Fonts version (e.g., "v35").
+ * @param string $last_modified Google Fonts last modified date.
* @return int|WP_Error Font family post ID or error.
*/
- public function register_font($font_name, $font_slug, $files) {
+ public function register_font($font_name, $font_slug, $files, $font_version = '', $last_modified = '') {
// Check if font already exists
$existing = get_posts([
'post_type' => 'wp_font_family',
@@ -88,6 +90,14 @@ class MLF_Font_Registry {
update_post_meta($family_id, '_mlf_imported', '1');
update_post_meta($family_id, '_mlf_import_date', current_time('mysql'));
+ // Store version info for update checking
+ if (!empty($font_version)) {
+ update_post_meta($family_id, '_mlf_font_version', $font_version);
+ }
+ if (!empty($last_modified)) {
+ update_post_meta($family_id, '_mlf_font_last_modified', $last_modified);
+ }
+
// Create font face posts (children) - WordPress also reads these
foreach ($files as $file) {
$filename = basename($file['path']);
@@ -190,6 +200,11 @@ class MLF_Font_Registry {
delete_transient('global_styles');
delete_transient('global_styles_' . get_stylesheet());
+ // Clear WP_Theme_JSON caches
+ if (class_exists('WP_Theme_JSON_Resolver')) {
+ WP_Theme_JSON_Resolver::clean_cached_data();
+ }
+
// Clear object cache for post queries
wp_cache_flush_group('posts');
@@ -248,10 +263,14 @@ class MLF_Font_Registry {
$faces_by_font[$parent_id][] = $face;
}
- // Batch get all import dates in single query
+ // Batch get all metadata
$import_dates = [];
+ $font_versions = [];
+ $font_last_modified = [];
foreach ($font_ids as $font_id) {
$import_dates[$font_id] = get_post_meta($font_id, '_mlf_import_date', true);
+ $font_versions[$font_id] = get_post_meta($font_id, '_mlf_font_version', true);
+ $font_last_modified[$font_id] = get_post_meta($font_id, '_mlf_font_last_modified', true);
}
$result = [];
@@ -285,11 +304,13 @@ class MLF_Font_Registry {
});
$result[] = [
- 'id' => $font->ID,
- 'name' => $settings['name'] ?? $font->post_title,
- 'slug' => $settings['slug'] ?? $font->post_name,
- 'variants' => $variants,
- 'import_date' => $import_dates[$font->ID] ?? '',
+ 'id' => $font->ID,
+ 'name' => $settings['name'] ?? $font->post_title,
+ 'slug' => $settings['slug'] ?? $font->post_name,
+ 'variants' => $variants,
+ 'import_date' => $import_dates[$font->ID] ?? '',
+ 'version' => $font_versions[$font->ID] ?? '',
+ 'last_modified' => $font_last_modified[$font->ID] ?? '',
];
}
diff --git a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php
index 59ac8e1..f52ecd3 100644
--- a/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php
+++ b/native/wordpress/maple-fonts-wp/includes/class-mlf-font-search.php
@@ -38,11 +38,13 @@ class MLF_Font_Search {
const METADATA_URL = 'https://fonts.google.com/metadata/fonts';
/**
- * Maximum metadata response size (2MB).
+ * Maximum metadata response size (10MB).
+ *
+ * Google Fonts metadata for 1500+ fonts can be 5-8MB.
*
* @var int
*/
- const MAX_METADATA_SIZE = 2 * 1024 * 1024;
+ const MAX_METADATA_SIZE = 10 * 1024 * 1024;
/**
* Rate limiter instance.
@@ -165,10 +167,12 @@ class MLF_Font_Search {
$fonts = [];
foreach ($data['familyMetadataList'] as $font) {
$fonts[] = [
- 'family' => $font['family'],
- 'category' => $font['category'] ?? 'sans-serif',
- 'variants' => $font['fonts'] ?? [],
- 'subsets' => $font['subsets'] ?? ['latin'],
+ 'family' => $font['family'],
+ 'category' => $font['category'] ?? 'sans-serif',
+ 'variants' => $font['fonts'] ?? [],
+ 'subsets' => $font['subsets'] ?? ['latin'],
+ 'version' => $font['version'] ?? '',
+ 'lastModified' => $font['lastModified'] ?? '',
];
}
@@ -253,6 +257,8 @@ class MLF_Font_Search {
'has_variable' => $has_variable,
'has_italic' => $has_italic,
'weights' => $weights,
+ 'version' => $font['version'] ?? '',
+ 'lastModified' => $font['lastModified'] ?? '',
];
}
@@ -262,4 +268,31 @@ class MLF_Font_Search {
public function clear_cache() {
delete_transient(self::CACHE_KEY);
}
+
+ /**
+ * Get version info for a specific font.
+ *
+ * @param string $font_family Font family name.
+ * @return array|null Version info or null if not found.
+ */
+ public function get_font_version($font_family) {
+ $fonts = $this->get_fonts_metadata();
+
+ if (is_wp_error($fonts)) {
+ return null;
+ }
+
+ $font_family_lower = strtolower($font_family);
+
+ foreach ($fonts as $font) {
+ if (strtolower($font['family']) === $font_family_lower) {
+ return [
+ 'version' => $font['version'] ?? '',
+ 'lastModified' => $font['lastModified'] ?? '',
+ ];
+ }
+ }
+
+ return null;
+ }
}
diff --git a/native/wordpress/maple-fonts-wp/maple-local-fonts.php b/native/wordpress/maple-fonts-wp/maple-local-fonts.php
index 48d0317..82cc2fe 100644
--- a/native/wordpress/maple-fonts-wp/maple-local-fonts.php
+++ b/native/wordpress/maple-fonts-wp/maple-local-fonts.php
@@ -151,6 +151,72 @@ function mlf_get_capability() {
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.
+ *
+ * @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) {
+ $registry = new MLF_Font_Registry();
+ $fonts = $registry->get_imported_fonts();
+
+ if (empty($fonts)) {
+ return $theme_json;
+ }
+
+ $font_families = [];
+
+ foreach ($fonts as $font) {
+ $font_faces = [];
+
+ foreach ($font['variants'] as $variant) {
+ $weight = $variant['weight'];
+ $style = $variant['style'];
+
+ // Build filename based on our naming convention
+ $font_slug = sanitize_title($font['name']);
+ 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);
+ }
+
+ $font_dir = wp_get_font_dir();
+ $font_url = trailingslashit($font_dir['url']) . $filename;
+
+ $font_faces[] = [
+ 'fontFamily' => $font['name'],
+ 'fontWeight' => $weight,
+ 'fontStyle' => $style,
+ 'src' => [$font_url],
+ ];
+ }
+
+ $font_families[] = [
+ 'name' => $font['name'],
+ 'slug' => $font['slug'],
+ 'fontFamily' => "'{$font['name']}', sans-serif",
+ 'fontFace' => $font_faces,
+ ];
+ }
+
+ $new_data = [
+ 'version' => 2,
+ 'settings' => [
+ 'typography' => [
+ 'fontFamilies' => $font_families,
+ ],
+ ],
+ ];
+
+ return $theme_json->update_with($new_data);
+}
+add_filter('wp_theme_json_data_user', 'mlf_add_fonts_to_theme_json');
+
/**
* Register admin menu.
*/
@@ -201,36 +267,46 @@ function mlf_enqueue_admin_assets($hook) {
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',
[],
- MLF_VERSION
+ $css_version
);
wp_enqueue_script(
'mlf-admin',
MLF_PLUGIN_URL . 'assets/admin.js',
['jquery'],
- MLF_VERSION,
+ $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'),
- 'strings' => [
+ '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'),
- 'searchPlaceholder' => __('Search Google Fonts...', 'maple-local-fonts'),
'searching' => __('Searching...', 'maple-local-fonts'),
'noResults' => __('No fonts found. Try a different search term.', 'maple-local-fonts'),
- 'selectFont' => __('Select a font from the search results above.', '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'),
],
]);
}